对象、类与面向对象编程
# 理解对象
# 属性类型
# 数据类型
数据有四种类型,Configurable、Enumerable、Writable、Value
使用 Object.defineProperty
可以修改默认属性类型
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有4 个特性描述它们的行为。
Configurable表示是否可以通过 delete 删除并重新定义,默认为true
Enumerable表示是否可以通过 for-in 循环返回属性,默认为true
Writable表示是否可以修改属性的值,默认为true
Value表示属性的值,默认为undefined
访问器属性是不能直接定义的,必须使用Object.defineProperty()。下面是一个例子:
let book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, "year", {
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
笔记
在ECMAScript 5 以前,开发者会使用两个非标准的访问创建访问器属性:__define-Getter__()
和__defineSetter__()
。这两个方法最早是Firefox 引入的,后来Safari、Chrome 和Opera 也实现了。
# 定义多个属性
Object.defineProperties()
方法可以定义多个属性
let book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 读取属性的特性
使用 Object.getOwnPropertyDescriptor()
方法可以读取属性的属性描述符
ECMAScript 2017 新增了Object.getOwnPropertyDescriptors()
静态方法。这个方法实际上会在每个自有属性上调用Object.getOwnPropertyDescriptor()
并在一个新对象中返回它们。
# 合并对象
Object.assign()
方法用于将所有可枚举的自有属性从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数。
# 对象标识及相等判定
在ECMAScript 6 之前,有些特殊情况即使是===操作符也无能为力:
// 这些是===符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 这些情况在不同JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定NaN 的相等性,必须使用极为讨厌的isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
2
3
4
5
6
7
8
9
10
11
为改善这类情况,ECMAScript 6 规范新增了Object.is()
,这个方法与===很像,但同时也考虑到了上述边界情形。这个方法必须接收两个参数:
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
// 要检查超过两个值,递归地利用相等性传递即可:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 增强的属性语法
# 属性简写
在属性名和变量名一致的时候,可以不用写两次
# 可计算属性
用[]
包裹属性名,可以动态计算属性名
注意
可计算属性表达式中抛出任何错误都会中断对象创建。如果计算属性的表达式有副作用,那就要小心了,因为如果表达式抛出错误,那么之前完成的计算是不能回滚的。
# 简写方法名
不需要使用 object = { key: [Function] }
这种形式,可以直接使用 object = { key() { } }
这种形式
# 对象解构
可以使用对象解构语法从对象中进行嵌套解构、部分解构、对函数参数进行解构
# 创建对象
# 工厂模式创建
使用函数方式,通过new一个Object并声明属性来批量创建对象。
缺点:没有解决对象标识类型
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
2
3
4
5
6
7
8
9
10
# 构造函数模式
自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
2
3
4
5
6
7
8
笔记
按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在ECMAScript 中区分构造函数和普通函数。毕竟ECMAScript 的构造函数就是能创建对象的函数。
通过构造函数创建实例会执行如下操作:
- 在内存中创建一个新对象
- 这个新对象内部的
[[Prototype]]
特性被赋值为构造函数的 prototype 属性 - 构造函数内部的
this
被赋值为这个新对象(即 this 指向新对象) - 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
笔记
在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加
# 构造函数也是函数
笔记
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用new 操作符调用就是构造函数,而不使用new 操作符调用的函数就是普通函数。
# 构造函数的问题
笔记
构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。
# 原型模式
笔记
使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型
# 理解原型
/**
* 构造函数可以是函数表达式
* 也可以是函数声明,因此以下两种形式都可以:
* function Person() {}
* let Person = function() {}
*/
function Person() {}
/**
* 声明之后,构造函数就有了一个
* 与之关联的原型对象:
*/
console.log(typeof Person.prototype);
console.log(Person.prototype);
// {
// constructor: f Person(),
// __proto__: Object
// }
/**
* 如前所述,构造函数有一个prototype 属性
* 引用其原型对象,而这个原型对象也有一个
* constructor 属性,引用这个构造函数
* 换句话说,两者循环引用:
*/
console.log(Person.prototype.constructor === Person); // true
/**
* 正常的原型链都会终止于Object 的原型对象
* Object 原型的原型是null
*/
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
// }
let person1 = new Person(),
person2 = new Person();
/**
* 构造函数、原型对象和实例
* 是3 个完全不同的对象:
*/
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
/**
* 实例通过__proto__链接到原型对象,
* 它实际上指向隐藏特性[[Prototype]]
*
* 构造函数通过prototype 属性链接到原型对象
*
* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true
/**
* 同一个构造函数创建的两个实例
* 共享同一个原型对象:
*/
console.log(person1.__proto__ === person2.__proto__); // true
/**
* instanceof 检查实例的原型链中
* 是否包含指定构造函数的原型:
*/
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Object 类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值。这样就可以重写一个对象的原型继承关系:
let biped = {
numLegs: 2
};
let person = {
name: 'Matt'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
2
3
4
5
6
7
8
9
10
注意
Object.setPrototypeOf()
可能会严重影响代码性能。Mozilla 文档说得很清楚:“在所有浏览器和JavaScript 引擎中,修改继承关系的影响都是微妙且深远的。这种影响并不仅是执行Object.setPrototypeOf()
语句那么简单,而是会涉及所有访问了那些修改过[[Prototype]]
的对象的代码。”
为避免使用Object.setPrototypeOf()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型:
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
2
3
4
5
6
7
8
笔记
如果想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames()
Object.keys()和Object.getOwnPropertyNames()在适当的时候都可用来代替for-in 循环。
在ECMAScript 6 新增符号类型之后,相应地出现了增加一个Object.getOwnPropertyNames()的兄弟方法的需求,因为以符号为键的属性没有名称的概念。因此,Object.getOwnProperty-Symbols()方法就出现了,这个方法与Object.getOwnPropertyNames()类似,只是针对符号而已。
for-in 循环、Object.keys()、Object.getOwnPropertyNames()、Object.getOwnProperty-Symbols()以及Object.assign()在属性枚举顺序方面有很大区别。for-in 循环和Object.keys()的枚举顺序是不确定的,取决于JavaScript 引擎,可能因浏览器而异。
笔记
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。记住,实例只有指向原型的指针,没有指向构造函数的指针。
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
friend.sayName(); // 错误
2
3
4
5
6
7
8
9
10
11
12
# 原型的问题
- 它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
- 最主要问题源自它的共享特性。
提示
一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。
# 继承
提示
继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。接口继承在ECMAScript 中是不可能的,因为函数没有签名。实现继承是ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。
# 原型链
提示
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例,意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数,这就是原型链。
笔记
确认原型的两种方式:instanceof
和 Object.isPrototypeOf
方法
通过对象字面量添加新方法会导致覆盖后的原型是 Object
的实例,而不是原先原型的实例。
# 原型链的问题
提示
原型链虽然是实现继承的强大工具,但它也有问题。主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过,原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。
原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。
# 盗用构造函数
在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象为上下文执行构造函数。
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {
// 继承 SuperType
SuperType.call(this)
}
let instance1 = new SubType()
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
2
3
4
5
6
7
8
9
10
11
12
13
14
示例中加粗的代码展示了盗用构造函数的调用。通过使用call()(或apply())方法,SuperType构造函数在为SubType 的实例创建的新对象的上下文中执行了。这相当于新的SubType 对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的colors 属性。
相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。
function SuperType(name) {
this.name = name
}
function SubType() {
// 继承 SuperType 并传参
SuperType.call(this, "Nicholas")
// 实例属性
this.age = 29
}
let instance = new SubType()
console.log(instance.name) // "Nicholas"
console.log(instance.age) // 29
2
3
4
5
6
7
8
9
10
11
12
13
14
盗用构造函数的主要缺点:必须在构造函数中定义方法,这样函数复用就无从谈起了。而且子类也不能访问父类原型上定义的方法。
# 组合继承
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
组合继承弥补了原型链和盗用构造函数的不足,是JavaScript 中使用最多的继承模式。而且组合继承也保留了instanceof 操作符和isPrototypeOf()方法识别合成对象的能力。
# 原型式继承
提示
2006 年,Douglas Crockford 写了一篇文章:《JavaScript 中的原型式继承》(“Prototypal Inheritance inJavaScript”)。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法。他的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。
这个object()函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
2
3
4
5
6
7
8
9
10
11
Crockford 推荐的原型式继承适用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。你需要把这个对象先传给object(),然后再对返回的对象进行适当修改。在这个例子中,person 对象定义了另一个对象也应该共享的信息,把它传给object()之后会返回一个新对象。这个新对象的原型是person,意味着它的原型上既有原始值属性又有引用值属性。这也意味着person.friends 不仅是person 的属性,也会跟anotherPerson 和yetAnotherPerson 共享。这里实际上克隆了两个person。
ECMAScript 5 通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。
ECMAScript 5 通过增加Object.create()方法将原型式继承的概念规范化了。
Object.create()的第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
提示
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
# 寄生式继承
与原型式继承比较接近的一种继承方式是寄生式继承(parasitic inheritance),也是Crockford 首倡的一种模式。寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。基本的寄生继承模式如下
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
2
3
4
5
6
7
注意
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
# 寄生组合继承
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
寄生式组合继承的基本模式如下所示:
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
2
3
4
5
这个inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype 对象设置constructor 属性,解决由于重写原型导致默认constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。
寄生式组合继承可以算是引用类型继承的最佳模式。
# 类
类(class)是ECMAScript 中新的基础性语法糖结构,因此刚开始接触时可能会不太习惯。虽然ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
# 类的构成
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。空的类定义照样有效。默认情况下,类定义中的代码都在严格模式下执行。
与函数构造函数一样,多数编程风格都建议类名的首字母要大写,以区别于通过它创建的实例
类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过name 属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符。
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined
2
3
4
5
6
7
8
9
# 类的实例化
使用new 调用类的构造函数会执行如下操作。
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]
指针被赋值为构造函数的prototype 属性。
(3) 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
如果返回的不是this 对象,而是其他对象,那么这个对象不会通过instanceof 操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改。
提示
类构造函数与构造函数的主要区别是,调用类构造函数必须使用new 操作符。而普通构造函数如果不使用new 调用,那么就会以全局的this(通常是window)作为内部对象。
类本身具有与普通构造函数一样的行为。在类的上下文中,类本身在使用new 调用时就会被当成构造函数。重点在于,类中定义的constructor 方法不会被当成构造函数,在对它使用instanceof 操作符时会返回false。但是,如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么instanceof 操作符的返回值会反转。
class Person {}
let p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false
let p2 = new Person.constructor();
console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true
2
3
4
5
6
7
8
9
笔记
类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键
类定义也支持获取和设置访问器。语法与行为跟普通对象一样
可以在类上定义静态方法。这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。
# 非函数原型和类成员
虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
2
3
4
5
# 迭代器与生成器方法
类定义语法支持在原型和类本身上定义生成器方法:
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象:
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog
2
3
4
5
6
7
8
9
10
11
12
13
14
15
也可以只返回迭代器实例:
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
[Symbol.iterator]() {
return this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 类的继承
# 构造函数、HomeObject和 super()
派生类的方法可以通过super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用super 可以调用父类构造函数。
注意
不要在调用super()之前引用this,否则会抛出ReferenceError
在静态方法中可以通过super 调用继承的类上定义的静态方法
class Vehicle {
static identify() {
console.log('vehicle');
}
}
class Bus extends Vehicle {
static identify() {
super.identify();
}
}
Bus.identify(); // vehicle
2
3
4
5
6
7
8
9
10
11
使用super的注意事项
- super 只能在派生类构造函数和静态方法中使用
- 不能单独引用super关键字,要么用super调用构造函数,要么用super引用静态方法
- 调用super会调用父类构造函数,并将返回的实例赋值给this
- super的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
- 如果没有定义类构造函数,在实例化派生类时会调用super,而且会传入所有传给派生类的参数
- 在类构造函数中,不能再调用super之前引用this
- 如果再派生类中显式定义了构造函数,则要么必须再其中调用super,要么在其中返回一个对象。
# 抽象基类
有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。虽然ECMAScript 没有专门支持这种类的语法 ,但通过new.target 也很容易实现。new.target 保存通过new 关键字调用的类或函数。通过在实例化时检测new.target 是不是抽象基类,可以阻止对抽象基类的实例化。
另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过this 关键字来检查相应的方法。
笔记
ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型
# 类混入
把不同类的行为集中到一个类是一种常见的JavaScript 模式。虽然ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。
提示
Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用Object.assign()就可以了。
一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式。通过写一个辅助函数,可以把嵌套调用展开。
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
提示
很多JavaScript 框架(特别是React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。