第8章 对象、类与面向对象编程
# 对象、类与面向对象编程
对象是一组属性的无序集合,包括一系列的键值对。
# 理解对象
# 创建对象的方式
- new Object()
- 对象字面量
- 工厂模式,构造函数模式,原型模式(下一节介绍)
# 属性的类型
分为 2 类,用 2 个中括号来标识内部特性,如[[Enumberable]]。
数据属性
- [[Configurable]]:表示属性是否可以 delete,是否可以修改特性,是否可以改为访问器属性,默认 true。
- [[Enumberable]]:表示属性是否可以通过 for-in 循环遍历到,默认 true。
- [[Writable]]:表示属性的值是否可以被修改,默认 true。
- [[Value]]:表示属性实际的值,默认 undefined。
访问器属性
- [[Configurable]]:同上
- [[Enumberable]]:同上
- [[Get]]:获取函数,在读取属性时调用,默认 undefined。
- [[Set]]:设置函数,在写入属性时调用,默认 undefined。
修改数据属性和访问器属性都是通过
Object.definedPropery()
来实现,分别表示为:// 数据属性 let person = {} Object.defineProperty(person, 'name', { writable: false, value: 'nick', }) console.log(person.name) // nick person.name = 'milk' console.log(person.name) // nick
1
2
3
4
5
6
7
8
9let person = {} let name1 = 'jack' Object.defineProperty(person, 'name', { get() { return name1 }, set(newValue) { console.log('new', newValue) // name1 = newValue }, }) console.log(person.name) name1 = 'rose' console.log(person.name) person.name = 'mike' console.log(person.name) // 输出如下 // jack // rose // new mike // rose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 定义多个属性
可以通过 Object.defineProperties()
来一次性定义多个属性。
let book = {}
Object.defineProperties(book, {
year: {
value: 2017,
},
version: {
value: '1.1.0',
},
})
console.log(book.year) // 2017
console.log(book.version) // '1.1.0'
2
3
4
5
6
7
8
9
10
11
# 读取属性的特性
使用 Object.getOwnPropertyDescriptor()
方法可以取得指定属性的属性描述符。
使用 Object.getOwnPropertyDescriptors()
方法可以得到一个新对象,包括了所有属性及其描述符
# 合并对象
使用 Object.assign()
来合并对象,接收一个目标对象和一个或多个源对象作为参数,将每个源对象中可枚举和自由属性复制到目标对象。
let desc = { id: 'desc' }
let src = { id: 'src' }
let result = Object.assign(desc, src)
console.log(result) // { id: 'src' }
console.log(result === desc) // true
2
3
4
5
如果有多个同名属性,则复制最后一个值
Object.assign()
是浅拷贝let desc = {} let src = { a: {}, b: 1, } let result = Object.assign(desc, src) result.a.b = 2 result.b = 3 console.log(src.a) // { b: 2 } console.log(src.b) // 1
1
2
3
4
5
6
7
8
9
10
# 相等判定
console.log(NaN === NaN) // false
console.log(-0 === +0) // true
console.log(Object.is(NaN, NaN)) // true
console.log(Object.is(-0, +0)) // false
2
3
4
5
# 增强的对象语法
属性值简写
let name = 'myname' // let person = {name:name} // 属性名和变量名一样 let person = { name }
1
2
3可计算属性
或者说在对象字面量中动态命名属性
let name = 'jack' let person = { [name]: 'jack chen', } console.log(person[name])
1
2
3
4
5简写方法名
let person = { say: function() { console.log('hello') }, run() { console.log('run') }, } person.say() person.run()
1
2
3
4
5
6
7
8
9
10
# 对象解构
const person = {
name: 'jack',
age: 18,
favorite: 'swimming',
}
const { name, age, sex = 'man', favorite: hobby } = person
console.log(name, age, sex, hobby) // jack 18 man swimming
2
3
4
5
6
7
# 创建对象
使用 new Object()
或对象字面量方式创建对象很方便,但不能快速复用,即创建具有同样接口的多个对象时,需要重复多次。下面介绍 3 种方式,可以基于接口快速创建对象。
分别是工厂模式、构造函数模式和原型模式。
# 工厂模式
工厂模式是一种按照特定接口创建对象的方式,这种模式最直观的解决了创建多个对象的需求。
缺点:没有解决对象标识问题(即新创建的对象是什么类型)。
解读:对象标识,应该是指这个工厂函数创建的对象和其他工厂函数创建的对象在类别上的区分,即他们共同的模具是什么类型。
function createPerson(name, age) {
let obj = new Object() // 创建对象
obj.name = name // 描述接口(属性)
obj.age = age
obj.say = function() {
// 描述接口(方法)
console.log(this.name)
}
return obj // 返回对象
}
let person1 = createPerson('nick', 20)
let person2 = createPerson('jack', 18)
2
3
4
5
6
7
8
9
10
11
12
# 构造函数模式
看下构造函数如何创建对象
function Person(name, age) {
this.name = name
this.age = age
this.say = function() {
console.log(this.name)
}
}
let person1 = new Person('nick', 20)
let person2 = new Person('jack', 18)
person1.say() // nick
person2.say() // jack
2
3
4
5
6
7
8
9
10
11
# 特点
和工厂模式相比,构造函数模式由如下特点:
- 使用 new 操作符
- 没有显示创建对象
- 属性和方法直接赋值给 this
- 没有 return
- 使用 instanceof 可以确定对象的类型
除此之外没有其他区别,描述调用构造函数的执行过程:
- 在内存中创建一个新对象
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
- 构造函数内部的 this 被赋值为这个新对象
- 执行构造函数内部的代码
- 如果构造函数返回非空对象,则返回该对象;否则返回刚创建的新对象
# 构造函数也是函数
只有使用 new 操作符,就是构造函数调用,否则就是普通函数的调用。
# 缺点
定义的方法在每个实例上都创建一遍。请看:
function Person(name, age) {
this.name = name
this.age = age
// this.say = function () {
// console.log(this.name)
// }
// 等价于
this.say = new Function('console.log(this.name)')
}
let person1 = new Person('nick', 20)
let person2 = new Person('jack', 18)
console.log(person1.say === person2.say) // false
2
3
4
5
6
7
8
9
10
11
12
没必要定义两个不同的 Function 实例。简单解决如下:
function Person(name, age) {
this.name = name
this.age = age
this.say = say
}
function say() {
// 在全局定义一处方法
console.log(this.name)
}
let person1 = new Person('nick', 20)
let person2 = new Person('jack', 18)
console.log(person1.say === person2.say) // true
2
3
4
5
6
7
8
9
10
11
12
不过这样一来,say()定义在了全局作用域,可能有命名冲突,由此引出了原型模式。
# 原型模式
每个函数都有一个 prototype 属性,它是一个对象,即原型对象。
在它上面定义的属性和方法可以被对象实例共享。
回到之前的问题,看原型模式如何解决:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function() {
// 将方法挂载在原型对象上
console.log(this.name)
}
let person1 = new Person('nick', 20)
let person2 = new Person('jack', 18)
person1.say() // nick
person2.say() // jack
console.log(person1.say === person2.say) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
# 理解原型
在构造函数创建时,原型对象默认拥有 constructor 属性,指向与之关联的构造函数。而构造函数的 prototype 属性则指向该原型,因此:
console.log(Person.prototype.constructor === Person) // true
一般情况下,实例可以通过 __proto__
属性来获取它对应的原型,这个不是标准方式,后来 ECMA 提供了访问原型对象的方法 getPrototypeOf
,因此:
console.log(person1.__proto__ === Person.prototype) // true
console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
console.log(person1.__proto__ === person2.__proto__) // true
2
3
如何基于一个对象,并指定其为原型,创建对象
let bird = {
size: 'small',
}
let redBird = Object.create(bird)
redBird.color = 'red'
console.log(redBird.size) // small
console.log(redBird.color) // red
console.log(Object.getPrototypeOf(redBird) === bird) // true
2
3
4
5
6
7
8
# 原型层级
当访问对象的 a 属性时,遵循如下规则:
- 该对象实例有 a 属性吗?有则返回,没有继续搜索
- 该对象的原型有 a 属性吗?有则返回,没有继续搜索
- 该对象的原型的原型有 a 属性吗?有则返回,没有继续搜索
- 以此类推,直到该原型指向 null 时,返回 undefined
function Person() {}
Person.prototype.name = 'jack'
let person1 = new Person()
let person2 = new Person()
console.log(person1.name) // jack
console.log(person2.name) // jack
person1.name = 'zhangsan'
console.log(person1.name) // zhangsan
person1.name = undefined
console.log(person1.name) // undefined
person2.name = null
console.log(person2.name) // null
delete person2.name
console.log(person2.name) // jack
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
另外,可以使用 hasOwnProperty()
来判断属性是否在实例上。
function Person() {}
Person.prototype.name = 'jack'
let person1 = new Person()
let person2 = new Person()
person1.name = 'zhangsan'
console.log(person1.hasOwnProperty('name')) // true
console.log(person2.hasOwnProperty('name')) // false
2
3
4
5
6
7
8
# in 操作符在原型中的使用
in 单独使用和 for-in 使用时,只要对象可以访问该属性,就返回 true,即它会追溯到原型上。
function Person() {}
Person.prototype.name = 'jack'
Person.prototype.age = 20
let person1 = new Person()
let person2 = new Person()
person1.name = 'zhangsan'
console.log('name' in person1) // true
console.log('name' in person2) // true
for (let key in person1) {
console.log(key) // name age
}
for (let key in person2) {
console.log(key) // name age
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如何判断一个属性是原型属性(注意,不是实例属性)
function Person() {}
Person.prototype.name = 'jack'
let person1 = new Person()
let person2 = new Person()
person1.name = 'zhangsan'
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && name in object
}
console.log(hasPrototypeProperty(person1, 'name')) // false
console.log(hasPrototypeProperty(person2, 'name')) // true
2
3
4
5
6
7
8
9
10
11
12
如果只想要遍历实例上可枚举的的属性,不想遍历到原型属性,可以使用 Object.keys()
function Person() {}
Person.prototype.name = 'jack'
Person.prototype.age = 20
let person1 = new Person()
let person2 = new Person()
person1.name = 'zhangsan'
for (let item of Object.keys(person1)) {
console.log(1, item) // name
}
for (let item of Object.keys(person2)) {
console.log(2, item) // (no thing)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
如果想列出所有实例属性,不论是否可以枚举,可以使用 Object.getOwnPropertyNames
function Person() {}
Person.prototype.name = 'jack'
Person.prototype.age = 20
let person1 = new Person()
let person2 = new Person()
person1.name = 'zhangsan'
let keys = Object.getOwnPropertyNames(Person.prototype)
let keys1 = Object.getOwnPropertyNames(person1)
let keys2 = Object.getOwnPropertyNames(person2)
console.log(keys) // [ 'constructor', 'name', 'age' ]
console.log(keys1) // [ 'name' ]
console.log(keys2) // []
2
3
4
5
6
7
8
9
10
11
12
13
14
# 属性枚举顺序
for-in
和 Object.keys()
是不确定的(取决于 js 引擎)。
Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Object.assign()
是确定的:先以升序枚举数值键,然后以插入顺序枚举字符串和符号键,对象字面量则以逗号分隔的顺序插入。
let k1 = Symbol('k1')
let k2 = Symbol('k2')
let o = {
1: 1,
second: 'second',
[k2]: 'k1k1',
first: 'first',
0: 0,
}
o[k1] = 'k2k2'
o[3] = 3
o.third = 'third'
o[2] = 2
console.log(Object.getOwnPropertyNames(o)) // [ '0', '1', '2', '3', 'second', 'first', 'third' ]
console.log(Object.getOwnPropertySymbols(o)) // [ Symbol(k2), Symbol(k1) ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 其他原型语法
上述代码中,为了挂载属性到原型,会重复写 Person.prototype,为了更好的封装,常常写成下面的方式
function Person() {}
Person.prototype = {
name: 'jack',
age: 20,
say() {
console.log(this.name)
},
}
const person = new Person()
console.log(person instanceof Person) // true
console.log(person.constructor === Person) // false
2
3
4
5
6
7
8
9
10
11
12
13
不过这种写法有个问题,即不能依靠 constructor 来识别类型了,因为 prototype 指向了新的对象。
因此加上 constructor。
function Person() {}
Person.prototype = {
constructor: Person,
name: 'jack',
age: 20,
say() {
console.log(this.name)
},
}
2
3
4
5
6
7
8
9
10
不过还不是最完善的,因为这样一来,constructor 变成可枚举属性了,而原生 constructor 默认是不可枚举的。
继续优化。
function Person() {}
Person.prototype = {
name: 'jack',
age: 20,
say() {
console.log(this.name)
},
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person,
})
const person = new Person()
console.log(person instanceof Person) // true
console.log(person.constructor === Person) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 原型的问题
原型模式还有一些问题
- 弱化了向构造函数传递初始化参数的能力
- 由于其共享特性,当共享包含引用值的属性时,会造成实例属性间互相污染(本质在于共享同一个内存地址)
function Person() {}
Person.prototype.colors = ['black', 'white']
const person1 = new Person()
person1.colors.push('yellow')
const person2 = new Person()
person2.colors.push('red')
console.log(person1.colors) // [ 'black', 'white', 'yellow', 'red' ]
console.log(person2.colors) // [ 'black', 'white', 'yellow', 'red' ]
console.log(person1.colors === person2.colors) // true
2
3
4
5
6
7
8
9
10
11
12
13
一般来说,不同的实例应该有属于自己的属性副本。
# 继承
# 原型链继承
基本思想是:子类的原型指向父类的实例,因此子类的实例可以继承父类的属性和方法。
缺点:
子类实例的constructor会指向父类构造函数
由于原型共享的特性,原型中包含的引用值会在所有实例间共享,造成数据污染。
子类在实例化时不能给父类的构造函数传参。
function SuperType() {
this.property = 'super'
this.colors = ['red','blue']
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subProperty = 'sub'
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subProperty
}
const sub1 = new SubType()
sub1.colors.push('green')
const sub2 = new SubType()
console.log(sub2.colors) // [ 'red', 'blue', 'green' ]
console.log(sub1.getSuperValue()) // sub
console.log(sub1.getSubValue()) // super
console.log(sub1.constructor === SuperType) // true 不符合预期
console.log(sub1.constructor === SubType) // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
判断原型和实例的关系有2种方式:
instanceof
console.log(sub instanceof SubType) // true console.log(sub instanceof SuperType) // true
1
2isPrototypeOf()
console.log(SuperType.prototype.isPrototypeOf(sub)) // true console.log(SubType.prototype.isPrototypeOf(sub)) // true
1
2
# 盗用构造函数继承(经典继承)
基本思想是:在子类的构造函数中调用父类构造函数,并修改this指向。
优点:
- 解决了实例的constructor指向问题
- 解决了原型属性共享造成的引用数据互相污染问题
- 子类构造函数中可以向父类构造函数传参
缺点:
- 必须在构造函数定义方法,即方法不能重用(共享)
- 子类不能访问父类原型上的方法。
function SuperType() {
this.colors = ['red', 'blue']
}
function SubType() {
SuperType.call(this) // 继承SuperType
}
// SubType.prototype = new SuperType()
const sub1 = new SubType()
sub1.colors.push('green')
console.log(sub1.colors) // [ 'red', 'blue', 'green' ]
const sub2 = new SubType()
console.log(sub2.colors) // [ 'red', 'blue' ]
console.log(sub1.constructor === SuperType) // false
console.log(sub1.constructor === SubType) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 组合继承
盗用构造函数继承基本不能单独使用,于是结合原型和盗用构造函数,形成了组合继承。是使用最多的一种继承模式。
基本思想是:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。
优点:子类实例既可以使用原型方法,又可以使用父类原型方法。
缺点:父类构造函数调用了2次。
function SuperType(name) {
this.colors = ['red', 'blue']
this.name = name
}
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)
}
const sub1 = new SubType('jack', 10)
sub1.colors.push('green')
console.log(sub1.colors) // [ 'red', 'blue', 'green' ]
sub1.sayName() // jack
sub1.sayAge() // 10
const sub2 = new SubType('rose', 20)
console.log(sub2.colors) // [ 'red', 'blue' ]
sub2.sayName() // rose
sub2.sayAge() // 20
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
# 原型式继承(一种概念或思想)
基本思路是:创建一个临时构造函数,将构造函数的原型指向传入的对象,返回构造函数的实例。。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
2
3
4
5
ES5新增了Object.create()
方法规范了原型式继承的概念,可以理解为基于某个对象进行继承。
# 寄生式继承
基本思想:类似于寄生构造函数和工厂函数,创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original) {
let clone = Object.create(original) // 以original为原型创建新对象
// 增强对象
clone.say = function () {
console.log('hi')
}
return clone
}
2
3
4
5
6
7
8
# 寄生式组合继承
完全体。基本思想是:通过构造函数继承属性,使用混合式原型链继承方法。
优点:解决了组合继承调用2次父类构造函数的问题
function SuperType(name) {
this.colors = ['red', 'blue']
this.name = name
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
// SubType.prototype = new SuperType()
inheritPrototype(SubType, SuperType) // 子原型指向父原型
SubType.prototype.sayAge = function () {
console.log(this.age)
}
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype) // 基于父类原型创建对象
prototype.constructor = subType // 增强对象,修正constructor
subType.prototype = prototype // 子类原型指向该对象
}
const sub1 = new SubType('jack', 10)
sub1.colors.push('green')
console.log(sub1.colors) // [ 'red', 'blue', 'green' ]
sub1.sayName() // jack
sub1.sayAge() // 10
const sub2 = new SubType('rose', 20)
console.log(sub2.colors) // [ 'red', 'blue' ]
sub2.sayName() // rose
sub2.sayAge() // 20
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
# 类
# 类定义
有2种:类声明和类表达式。
// 类声明
class Person{}
// 类表达式
const Animal = class {}
2
3
4
5
# 类的构成
class Person {
// 类的构造函数
constructor(name) {
this.name = name
}
// 类的访问器
get age() {
return 10
}
// 类方法
say() {
console.log(this.name)
}
// 类的静态方法
static say() {
console.log('construct say')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 类构造函数
constructor
关键字用于在类定义块内部创建类的构造函数。默认是空函数。
类实例化时,传入的参数会用作构造函数的参数,与函数一样,构造函数默认返回this对象。
类不能单独调用,必须使用new来调用。
类是一种特殊的函数。
console.log(typeof Person) // function
console.log(Person === Person.prototype.constructor) // true
const p = new Person()
console.log(p instanceof Person) // true
console.log(p.constructor === Person) // true
2
3
4
5
6
# 实例、原型和类成员
# 实例成员
实例成员一般在构造函数中定义,也可以在实例上继续添加。
实例成员对象是独立的,不会在原型上共享。
如上述this.name
属性。
# 原型方法与访问器
类块中的方法就是原型方法,如上述say()
方法。访问器同理,如get age()
。
# 静态类方法
静态类方法使用static
关键字作为前缀,里面的this指向类本身,而不是实例。
# 非函数原型和类成员
类的内部不支持显示定义数据成员,但外部可以手动添加(但不推荐,因为共享引用类型的数据会互相污染)。
class Person {
say() {
console.log(`${Person.hello} ${this.name}`)
}
}
Person.hello = 'nihao'
Person.prototype.name = 'jack'
const p = new Person()
p.say() // nihao jack
2
3
4
5
6
7
8
9
# 继承
类继承使用的是新语法,但背后依旧是原型链。
# 继承基础
类使用extends
实现单继承,它可以继承任何拥有[[Construct]]
和原型的对象,即不仅可以继承类,也可以继承普通的构造函数。
// 继承类
class Vehicle {}
class Bus extends Vehicle {}
const bus = new Bus()
console.log(bus instanceof Vehicle) // true
console.log(bus instanceof Bus) // true
// 继承普通构造函数
function Person() {}
class Engineer extends Person {}
2
3
4
5
6
7
8
9
10
# super
派生类的方法可以通过super
关键字引用到它们的原型。
// 继承类
class Vehicle {
constructor(age = 10) {
this.name = 'bmw'
this.age = age
}
static color() {
return 'red'
}
country() {
console.log(this.name)
return 'china'
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用super()之前使用this 且super()一定要在第一行
super(20)
this.name = 'hongqi'
console.log(this) // Bus { name: 'hongqi', age: 20 }
}
// 静态方法
static sayColor() {
console.log(super.color()) // red
}
// 实例方法
sayCountry() {
console.log(this.name) // hongqi
console.log(super.country()) // hongqi china
}
}
const bus = new Bus()
Bus.sayColor()
bus.sayCountry()
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
注意事项:
super
仅限于类构造函数、实例方法和静态方法内部使用。- 不能单独引用
super
关键字。要么调用构造函数,要么引用方法。 - 调用
super()
会调用父类构造函数,可以传参,并将返回的实例赋值给this
。 - 如果没有定义构造函数,在实例化派生类时会默认调用
super()
,并将相关参数传入。 - 如果派生类有构造函数,
super()
一定要在第一行调用,或者返回一个对象。
# 抽象基类
可能有这样一个类,它可供其他类继承,但本身不被实例化。使用new.target
可以实现。
class BaseClass {
constructor() {
console.log(new.target)
if (new.target === BaseClass) {
throw new Error('BaseClass cannot be instantiated directly')
}
}
}
class MyClass extends BaseClass {}
const my = new MyClass() // [Function: MyClass]
const b = new BaseClass() // [Function: BaseClass]
// Error: BaseClass cannot be instantiated directly
2
3
4
5
6
7
8
9
10
11
12
13
# 继承内置类型
有些内置类型的方法会返回新的实例。默认情况下,返回实例的类型与原始实例的类型是一样的。
class SubArray extends Array {}
const arr1 = new SubArray(1, 2, 3)
const arr2 = arr1.map((item) => item * 2)
console.log(arr1) // SubArray(3) [ 1, 2, 3 ]
console.log(arr2) // SubArray(3) [ 2, 4, 6 ]
console.log(arr1 instanceof SubArray) // true
console.log(arr2 instanceof SubArray) // true 这里map返回的实例类型是原始实例类型
2
3
4
5
6
7
8
这个默认行为可以被Symbol.species
访问器修改。
class SubArray extends Array {
static get [Symbol.species]() {
return Array
}
}
const arr1 = new SubArray(1, 2, 3)
const arr2 = arr1.map((item) => item * 2)
console.log(arr2 instanceof SubArray) // false 这里是map返回的新对象
2
3
4
5
6
7
8
9
# 类混入
通常使用Object.assign
来混入多个对象的属性,可以通过“嵌套”的方式来混入类。
思路是,定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。
混入模式正在被抛弃,替代它的方案是复合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。
class Base {}
let FooMixin = (SuperClass) =>
class extends SuperClass {
foo() {
console.log('foo')
}
}
let BarMixin = (SuperClass) =>
class extends SuperClass {
bar() {
console.log('bar')
}
foo() {
console.log('bar-foo')
}
}
let BazMixin = (SuperClass) =>
class extends SuperClass {
baz() {
console.log('baz')
}
}
// class SubClass extends BazMixin(BarMixin(FooMixin(Base))) {}
function mix(BaseClass, ...Mixins) {
return Mixins.reduce(
(accumulator, current) => current(accumulator),
BaseClass
)
}
class SubClass extends mix(Base, FooMixin, BarMixin, BazMixin) {}
const sub = new SubClass()
sub.foo() // bar-foo 注意这里foo被bar覆盖了
sub.bar() // bar
sub.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38