笔记:《JavaScript学习指南》-第9章对象以及面向对象编程

第9章 对象以及面向对象编程
9.1 属性枚举
9.1.1 for ... in
const o = {a: 1, b: 2, c: 3};

for(let prop in o){
    if(!o.hasOwnProperty(prop)) continue;
    console.log(`${prop}: ${o[prop]}`);
}
for...in 循环不会枚举出键为符号的属性。

9.1.2 Object.keys
Object.keys用来获取对象中所有可枚举属性的字符串属性,并将结果封装成一个数组。

可以输出和for...in 循环一样的的结果:
const o = {a: 1, b: 2, c: 3};

Object.keys(o).forEach(prop => console.log(`${prop}: ${o[prop]}`));

列出某个对象中所有以字符 i 开头的属性:
const o = { apple: 1, ballooon: 2, guitar: 3, iphone: 4 };

Object.keys(o)
    .filter(prop => prop.match(/^i/))
    .forEach(prop => console.log(`${prop}: ${o[prop]}`));    // iphone: 4

9.2 面向对象编程 OOP
OOP 基础理念:对象是一个逻辑相关的数据和功能的集合。
类,指的是通用的东西(车)。
实例(或者对象实例),指具体的东西(某一型号的车)。
功能(车子加速),称作方法。跟类相关,但不涉及特点实例的功能叫做类方法。
OOP还提供一个层次分明的类继承框架。

9.2.1 创建类和实例
ES6有新的语法。
class Car{
    constructor(){
    
    }
}

const car1 = new Car();
const car2 = new Car();
创建了一个名为car的类。并创建它的实例car1和car2。

给Car类添加一些数据和功能:
class Car{
    constructor(make,model){
        this.make = make;
        this.model = model;
        
        this.userGears = ['P','N','R','D'];    //有效的档位
        this.userGear = this.userGears[0];    //默认值
    }

    shift(gear){
        if(this.userGears.indexOf(gear) < 0) throw new Error(`invalid gear: ${gear}`);
        
        this.userGear = gear;
    }
}

const car1 = new Car("Tesla","Model S");
const car2 = new Car("Mzada","3i");

car1.shift("D");
car2.shift("R");

car1.userGear;    //D
car2.userGear;    //R

car2.shift("X");    //抛出错误 invalid gear: X
this 引用了方法被调用时所绑定的实例。
虽然使用了shift方法,防止选择一个无效的档位。但是这种保护有局限,因为可以直接给它赋值:car1.userGear = "X"。使用动态属性可以解决这个问题。

9.2.2 动态属性
动态属性,具有属性的语义,但同时可以像方法一样被调用。
修改car类:
class Car{
    constructor(make,model){
        this.make = make;
        this.model = model;
        
        this._userGears = ['P','N','R','D'];
        this._userGear = this._userGears[0];
    }
    
    //使用了访问器属性特征[[Get]]和[[Set]]
    get userGear(){
        return this._userGear;
    }
    
    set userGear(value){
        if(this._userGears.indexOf(value) < 0) throw new Error(`invalid gear: ${value}`);
        
        this._userGear = value;
    }

    shift(gear){
        this.userGear = gear;
    }
}

const car1 = new Car("Tesla","Model S");
const car2 = new Car("Mzada","3i");

car1.userGear = "X";   //抛出错误  invalid gear: X
car2._userGear = "R";    //成功赋值
例子中,使用“穷人访问限制”给私有属性加下划线作为前缀。能让大家快速识别出哪些代码访问了被保护的属性。
这个也还是没有解决直接赋值:car1._userGear = "X";

如果想要强制私有化,使用WeakMap的实例(第10章),它是被作用域保护的。
通过修改car类,使得当前档位属性真正私有化:
const Car = (function(){
    const carProps = new WeakMap();

    class Car{
        constructor(make,model){
            this.make = make;
            this.model = model;

            this._userGears = ['P','N','R','D'];
        
            carProps.set(this,{
                userGear:this._userGears[0]
            });
        }

        get userGear(){
            return carProps.get(this).userGear;
        }

        set userGear(value){
            if(this._userGears.indexOf(value) < 0) throw new Error(`Invalid gear: ${value}`);

            carProps.get(this).userGear = value;
        }

        shift(gear){
            this.userGear = gear;
        }
}
        return Car;
})(); 
const car1 = new Car("Tesla","Model S");
const car2 = new Car("Mzada","3i");

car1.shift("D");
car2.shift("R");

car1.userGear;    //D
car2.userGear;    //R
这里使用即时调用函数表达式将WeakMap 隐藏在一个闭包内,从而阻止了外界的访问。这个WeakMap 可以安全存储任何不想被Car类外部访问的属性。

另一种方式是使用符号代替属性名。但是类中的符号属性是可以被访问的,所以也可能失效。

9.2.3 类即函数
类实际上就是函数。
ES5中,这样编写 Car类:创建一个函数充当类的构造方法。
虽然 class 的语法更加直观,但JavaScript底层的实现方式并没有发生变化。

9.2.4 原型
在类的实例中,引用一个方法,实际上是在引用原型方法。
(使用( # )来描述原型方法已经成为一个普遍的约定。比如 Car.prototype.shift 写成 Car#shift)
 
每个函数都有一个叫做 prototype 的特殊属性。对于构造函数至关重要。
(按照约定,对象构造器(又指类名)始终以大写字母开头。)

使用 new 来创建一个新的实例,该实例可以访问其构造器的原型对象。对象实例会将它存储在自己的 _proro _ 属性中。( _proro _ 是JavaScript的内部属性,了解该属性但不要改变它)

原型,有一个机制叫做动态调度。当试图访问某个属性或方法时,如果不存在于当前对象中,JavaScript会检查它是否存在于对象原型中。
在实例中定义的方法或者属性会覆盖掉原型中的定义。JavaScript的检测顺序是先实例后原型。

9.2.5 静态方法
静态方法,使用 this 绑定类本身,通常用来执行一些与类相关的任务。

使用汽车VINs(车辆标识符)的例子:
class Car{
    static getNextVin(){
        return Car.nextVin++;    //也可以使用this.nextVin++, 此处使用 Car 是为了强调静态方法
    }

    constructor(make,model){
        this.meke = make;
        this.model = model;
        this.vin = Car.getNextVin();
    }

    static areSimilar(car1,car2){
        return car1.make === car2.make && car1.model ===car2.model;
    }

    static areSame(car1,car2){
        return car1.vin === car2.vin;
    }
}

Car.nextVin = 0;

const car1 = new Car("Tesla","S");    
const car2 = new Car("Mazda","3");   
const car3 = new Car("Mazda","3");    

car1.vin;    //0
car2.vin;    //1
car3.vin;    //2

Car.areSimilar(car2,car3);    //true
Car.areSame(car2,car3);    //false

9.2.6 继承
当创建一个类的实例时,它继承了类原型中所有的功能。
如果一个方法没有在对象原型中找到其定义,它会检测原型的原型。这样就建立了一条原型链。

原型链能够建立类的层次结构。
原型链允许将功能置于最合适的继承层次上。
class Vehicle{
    constructor(){
        this.passengers = [];
        console.log("Vehicle created");
    }
       
     addPassenger(p){
        this.passengers.push(p);
    }
}

//创建 Vehicle 的子类 Car
class Car  extends Vehicle{
    constructor(){
         super();
        console.log("Car created");
    }

    deployAirbags(){
        console.log("BWOOSH!");
    }
}
关键字 extends ,这个语法标志着 Car 是 Vehicle 的子类。 
super() 是一个特殊的函数,它调用了父类的构造器。子类必须调用这个方法,否则会报错。

9.2.7 多态
多态,是面向对象的术语,意思是一个实例不仅是它自身类的实例,也可以被当作它的任何父类的实例来使用。

instanceof运算符,会指出某个对象是否属于某个给定类。
//。。。沿用Vehicle 类和Car类。。。
//创建Vehicle的子类Motorcycle
class Motorcycle extends Vehicle{}

const c = new Car();
const m = new Motorcycle();

m instanceof Car;    //false
m instanceof Motorcycle;    //true
m instanceof Vehicle;    //true
另外,JavaScript中的所有对象都是基类 Object 的实例。即 m instanceof Object; 返回true。

9.2.8 枚举对象属性,回顾

9.2.9 字符串表示
toString() 是基类Object的一个方法,其默认返回“[objec Object]”。
在调试的时候,添加一个用于返回对象的描述信息的toString 方法将会很有用,这可以一下子就获取对象的重要信息。
class Car{
    //...
    toString(){
        return `${this.name} ${this.model}: ${this.vin}`;
}

9.3 多继承、混合类和接口
一些面向对象语言支持多继承,也就是一个类可以有两个直接的父类。

JavaScript是单继承的语言,但可以通过 混入 继承其他类。
混入是指功能按需“混合”。

下面创建一个“可接受保险的”混合类,同时保证它可以用在 Car 类上。
//...沿用前面的car类。。。
class InsurancePolicy{}

function makeInsurable(o){
    o.addInsurancePolicy = function(p){
        this.insurancePolicy = p;
    }

    o.getInsurancePolicy = function(){ 
        return this.insurancePolicy;
    }

    o.isInsured = function(){ 
        return !!this.insurancePolicy;
    }
}

//为 Car 类绑定对象原型属性
makeInsurable(Car.prototype);

const car1 = new Car();

car1.addInsurancePolicy(new InsurancePolicy());
这些方法已经成为 Car类的一部分,从开发的角度看,让这两个重要的类的维护变得更简单了,汽车工程组负责管理和维护Car类,保险组负责管理InsurancePolicy 类和 makeInsurable 混合方法。
混合方法也还不能消除冲突:如果保险组需要在混合类中创建一个shift 方法,就会破坏 Car 类。
假设保险组要不断增加一些通用的方法,可以要求他们使用符号作为键来缓解问题:
class InsurancePolicy{}

const ADD_POLICY = Symbol();
const GET_POLICY = Symbol();
const IS_INSURED = Symbol();
const _POLICY = Symbol();

function makeInsurable(o){
    o[ADD_POLICY] = function(p){ this[_POLICY] = p; }

    o[GET_POLICY] = function(){ return this[_POLICY]; }
    
    o[IS_INSURED] = function(){ return !!this[_POLICY]; }
}
由于符号是唯一的,这就保证了混合方法将不会干扰到 Car 类中已有的功能。
折中的办法:字符串用于定义方法,而符号(例如_POLICY)用于定义数据属性。

猜你喜欢

转载自blog.csdn.net/kjhz_liang/article/details/80531327