第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
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{}
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 类绑定对象原型属性
//为 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)用于定义数据属性。