JavaScript – 理解 Function, Object, Class, Prototype, This, Mixins
前言
JavaScript (简称 JS) 有几个比较难懂的概念 -- Function、Object、Class、Prototype、This。
这主要是因为 JS 在设计之初需求比较简单 (作为脚本语言),作者没有把它当作一门正儿八经的语言来看待。
而后来在不断打补丁的过程中,又必须向后兼容,于是造就了各种奇葩现象,最终苦了我们这批学习者。
本篇将带你理解 JS 中 Function、Object、Class、Prototype、This 的基本概念。
这对往后我们深入学习前端框架 (如 Angular) 至关重要。
Function
JS 是一门极其灵活的语言,它即支持面向对象编程 (Object-Oriented Programming,简称 OOP),也支持函数式编程 (Functional Programming,简称 FP)。
你会在 JS 中看到许多巧妙的特性,既满足 OOP,又满足 FP。
我们就先从 Functional (函数) 说起吧 🚀。
What is Function?
何为函数?函数是一组执行过程的封装。
下面是两行执行命令,我们可以把它视为一个执行过程。
console.log('Hello World');
console.log('I am Derrick');
透过函数,我们可以把这个执行过程封装起来。
// 把执行过程封装进一个函数里面
function sayHelloWorld() {
console.log('Hello World');
console.log('I am Derrick');
}
sayHelloWorld(); // 调用函数执行函数内的命令
调用函数就会执行整个过程,这样就起到了代码 copy paste 的作用。
parameters & return
为了让封装更灵活,函数支持参数,透过参数来调整执行过程。
function sayHelloWorld(name) {
console.log('Hello World');
console.log(`I am ${name}`);
}
sayHelloWorld('Derrick');
name 就是参数。
执行过程可以产生副作用,也可以生成结果,所以函数还支持返回值。
function generateFullName(firstName, lastName) {
return firstName + ' ' + lastName;
}
console.log(generateFullName('Derrick', 'Yam')); // 'Derrick Yam'
函数出没的地方
函数是一等公民,我们可以直接定义它
function myFunction1(param1){
return 'return value';
}
可以 assign 给变量
const myFunction2 = function (param1){
return 'return value';
};
可以当参数传
[].map(function (value, index) {
return 'value';
});
可以当函数返回值
function myFunction1() {
return function () {};
}
宽松的参数数量
参数的数量是没有严格规定的。
举例:函数声明了一个参数,但在调用时,却传了超过一个参数,这样是 ok 的。
// 函数只需要一个参数
function myFunction1(param1) {
console.log(param1); // 1
}
myFunction1(1, 2, 3); // 但调用时却传了三个参数
传多可以,传少也可以,甚至不传都可以。
function myFunction1(param1) {
console.log(param1); // undefined
}
myFunction1();
myFunction1(undefined); // 和上一行是等价的
参数的默认值 (es6)
function myFunction1(param1 = 'default value') {
console.log(param1);
}
myFunction1(undefined); // 'default value'
myFunction1(); // 'default value'
myFunction1('value'); // 'value'
用 "等于号" 设置 default parameter value
arguments 对象
当参数数量不固定时,可以通过 arguments 对象获取最终调用时传入的参数
function myFunction1(param1 = 'value') {
console.log(arguments[0]); // a
console.log(arguments[1]); // b
console.log(arguments[2]); // c
console.log(arguments.length); // 3
}
myFunction1('a', 'b', 'c');
myFunction1(); // arguments.length = 0 (arguments 不看 default value)
类似 C# 的 params 关键字。
宽松的返回值
即便函数没有返回值,但调用者还是可以把它 assign 给变量,默认的返回值是 undefined。
这个和参数 undefined 概念是同样的。
function myFunction1() {}
const returnValue = myFunction1(); // undefined
总结
以上就是函数的基础知识。
如果你觉得很简单,那就对了。
因为我刻意避开了容易造成混乱的特性,下面会再补上复杂的。
好,我们去下一 part 🚀。
Object
JS 的 Object,又称 "对象",就是 OOP 里的 "对象"。
但有别于纯正的 OOP 语言 (C# / Java),JS 也支持 FP,再加上它是一门动态类型语言,这导致了我们无法直接用 C# / Java 的对象概念去理解 JS 的对象。
结论:要理解 JS 对象,我们需要从一个新的视角切入,然后才慢慢关联到其它纯正 OOP 语言的概念,这样才能把它融会贯通。
object simple look
JS 的对象长这样
const person = {
firstName : 'Derrick',
age: 11,
married: false,
}
知识点:
-
结构
对象的结构是 key-value pair。
firstName 是 key,'Derrick' 是 value。
-
类型
const symbolKey = Symbol('key'); const person = { [symbolKey]: 'value', 10: null, '10': { }, sayHi: function() {} }
key 的类型可以是 string、number、symbol。
注:key 10 和 key '10' 是完完全全等价的哦,所以你也可以认为它其实只支持 string 和 symbol 而已。
value 则可以是任何类型,如:null,undefined,function,嵌套对象等等,通通都可以。
-
no need class
C# / Java 要创建对象必须先定义 class。
JS 是动态类型语言,因此创建对象是不需要定义 class 的。
-
naming
纯正的 OOP 语言通常会把对象的 key 称之为属性 (property),如果 value 类型是函数,那会改称为方法 (method)。
在 JS 则没有分那么清楚,抽象都叫 key,但如果你想表达多一点也可以叫属性或方法。
get, set value
const person = {
firstName : 'Derrick',
age: 11,
married: false,
}
get value by key
console.log(person.firstName); // 'Derrick'
get value by string
const key = 'firstName';
console.log(person[key]); // 'Derrick'
get missing key will return undefined
console.log(person.lastName); // undefined
set value
person.firstName = 'Alex';
console.log(person.firstName); // 'Alex'
提醒:JS 是动态类型语言,set value 的类型不需要和之前的一样,可以换成任何类型。
add, delete key
JS 是动态类型语言,对象的 key 可以动态添加和删除。
const person = {
firstName : 'Derrick',
age: 11,
married: false,
}
add key
person.lastName = 'Yam';
console.log(person.lastName); // 'Yam'
和 set value 的写法一样,如果 key 不存在就 add,key 已存在就 set。
delete key
delete person.firstName;
console.log(person.firstName); // undefined
对象方法
const person = {
firstName: 'Derrick',
lastName: 'Yam',
sayMyName: function() {
console.log(`I am ${person.firstName} ${person.lastName}`)
}
};
person.sayMyName(); // 'I am Derrick Yam'
由于方法调用的时候,对象已经完成定义了,因此方法内可以直接引用 person 对象。
另外,上面是比较 old school 的语法了,新潮的写法如下
const person = {
firstName: 'Derrick',
lastName: 'Yam',
// 省略了 function keyword
sayMyName() {
console.log(`I am ${person.firstName} ${person.lastName}`)
}
};
它俩是完全等价的,只是写法不同而已。
getter
const person = {
firstName : 'Derrick',
lastName : 'Yam',
get fullName() {
return person.firstName + ' ' + person.lastName;
}
}
console.log(person.fullName); // 'Derrick Yam';
person.firstName = 'Alex';
console.log(person.fullName); // 'Alex Yam';
在方法前面加一个 'get' keyword,它就变成 getter 属性了。
setter
有 getter 自然也有 setter,玩法大同小异
const person = {
firstName : 'Derrick',
lastName : 'Yam',
get fullName() {
return person.firstName + ' ' + person.lastName;
},
set fullName(value) {
const [firstName, lastName] = value.split(' ');
person.firstName = firstName;
person.lastName = lastName;
}
}
person.fullName = 'Alex Lee';
console.log(person.firstName); // 'Alex'
console.log(person.lastName); // 'Lee'
在方法前加一个 'set' keyword,它就变成 setter 了。
它的参数就是要 assign 的 value。
get all keys & values
const person = {
firstName : 'Derrick', // string property
lastName : 'Yam', // string property
10: '', // number property
[Symbol()]: '', // symbol property
// getter
get fullName() {
return person.firstName + ' ' + person.lastName;
},
// setter
set fullName(value) {
const [firstName, lastName] = value.split(' ');
person.firstName = firstName;
person.lastName = lastName;
},
// method
sayName() {
return person.fullName;
},
}
想获取所有的 keys 和 values 可以透过以下几个方法:
-
Object.keys
获取所有 keys (属性和方法),但不包括 symbol
const allKeys = Object.keys(person); console.log(allKeys); // ['10', 'firstName', 'lastName', 'fullName', 'sayName']
三个知识点:
a. 不包含 symbol property
b. number property 变成 string 了
c. getter setter fullName 只代表一个 key。
-
Object.values
获取所有 values
const allValues = Object.values(person); console.log(allValues); // ['', 'Derrick', 'Yam', 'Derrick Yam', function]
Object.values 只是一个方便,它完全等价于
const allValues = Object.keys(person).map(key => person[key]);
因此,symbol property 依然不包含在内。
-
Object.entries
获取所有 keys 和 values (它也只是一个方便,依然是基于 Object.keys)
const allKeyAndValues = Object.entries(person); console.log(allKeyAndValues); /* 返回的类型是 array array [ ['10', ''], ['firstName', 'Derrick'], ['lastName', 'Yam'], ['fullName', 'Derrick Yam'], ['sayName', function], ] */ // 搭配 for of 使用 for (const [key, value] of allKeyAndValues) { console.log(key, value); // '10', '' }
-
Object.getOwnPropertySymbols
const allSymbols = Object.getOwnPropertySymbols(person); console.log(allSymbols); // [Symbol()]
Object.keys
,values
,entries
都遍历不出 symbol property;只有Object.getOwnPropertySymbols
可以。反过来
Object.getOwnPropertySymbols
只能遍历出 symbol property;string 和 number 都遍历不出来。
Property Descriptor
对象 key 有一个 Descriptor 概念,我们可以把它视作一个 configuration。
看例子:
-
writable
const person = { firstName : 'Derrick', lastName : 'Yam', } // 设置 person.firstName 的 Descriptor Object.defineProperty(person, 'firstName', { writable: false // disable set }); person.firstName = 'New Name'; // try set new value console.log(person.firstName); // 'Derrick', still the same value, assign value not working anymore
透过
Object.defineProperty
来设置 key 的 Descriptor,它有好几个东西可以配置,writable 是其中一个。
disabled 之后这个 value 就不能改变了,所有 assign value 的操作将被无视。writable: false
意思就是让这个 key 不可以被写入 -- disable set 的功能。另外,writable 也适用于 symbol key。
-
enumerable
Object.defineProperty(person, 'firstName', { enumerable: false }); const allKeys = Object.keys(person); // ['lastName'], firstName 无法被遍历出来
enumerable: false
表示这个 key 无法被遍历出来。Object.keys
,values
,entries
会无视enumerable: false
的 key。如果我们想遍历出所有的 keys,包括
enumerable: false
的话,需要使用Object.getOwnPropertyNames
Object.defineProperty(person, 'firstName', { enumerable: false }); const enumerableKeys = Object.keys(person); // ['lastName'], firstName 无法被遍历出来 const allKeys = Object.getOwnPropertyNames(person); // ['firstName', 'lastName'] 两个 keys 都能遍历出来
另外一点,enumerable 对 symbol key 是无效的,虽然 symbol key 的 enumerable by default 是 false,但即便我们将它设置成 true,它依然无法被
Object.keys
遍历出来,而
Object.getOwnPropertySymbols
则不管 enumerable 是 true 还是 false,它都会遍历 symbol property 出来。 -
configurable
Object.defineProperty(person, 'firstName', { writable: false, // 改了 writable configurable: false // 不允许后续再修改了 }); // 尝试修改会直接报错 TypeError: Cannot redefine property: firstName Object.defineProperty(person, 'firstName', { writable: true, configurable: true });
configurable: false
表示这个 Descriptor 无法再被修改,再尝试修改会直接报错。 -
getter, setter, value
Object.defineProperty
不仅仅可以用来设置 Descriptor,或者说 Descriptor 不仅仅是 configuration。我们看例子:
Object.defineProperty(person, 'firstName', { value: 'New Name' }); console.log(person.firstName); // New Name
define value 等价于 assign value
person.firstName = 'New Name';
效果一模一样。
同样的,假如 firstName property 不存在,那会变成 add new property。define getter setter 也没问题
Object.defineProperty(person, 'fullName', { get() { return person.firstName + ' ' + person.lastName; }, set(value) { const [firstName, lastName] = value.split(' '); person.firstName = firstName; person.lastName = lastName; } }); console.log(person.fullName); // Derrick Yam person.fullName = 'Alex Lee'; console.log(person.firstName); // Alex console.log(person.lastName); // Lee
唯一要注意的是,getter setter 和 value writable 是有冲突的,比如说:我们同时 define getter 和 value 的话,它会报错
因为这样不逻辑,都 getter 了怎么还会有 value 呢?
获取 Property Descriptor
除了可以 define,我们也可以用 Object.getOwnPropertyDescriptor
方法查看当前的 Descriptor。
const person = {
firstName : 'Derrick',
lastName : 'Yam',
}
const descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor); // { configurable: true, enumerable: true, writable: true, value: 'Derrick' }
使用 person.firstName = ''
创建的 key, configurable, enumerable, writable 默认都是 true。
但使用 Object.defineProperty
创建的 key, configurable, enumerable, writable 默认都是 false。
const person = {
firstName : 'Derrick',
}
Object.defineProperty(person, 'lastName', { value: 'Yam' });
const lastNameDescriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log(lastNameDescriptor); // { configurable: false, enumerable: false, writable: false, value: 'Yam' }
注:只有在添加新 key 时,默认才会是 false。如果 key 本来已存在,Object.defineProperty
若没有额外声明 configurable, enumerable, writable,则会保持原有的;若有声明,则会覆盖。
批量设置 Property Descriptor
const person = {
firstName : 'Derrick',
lastName: 'Yam'
}
Object.defineProperties(person, {
firstName: {
writable: false
},
lastName: {
writable: false
},
fullName: {
get() {
return person.firstName + ' ' + person.lastName;
},
enumerable: true,
configurable: true,
}
});
没什么特别的,只是一个上层封装,让它可以批量设置而已。
总结
以上就是 Object 的基础知识。
如果你觉得很简单,那就对了。
因为我刻意避开了容易造成混乱的特性,下面会再补上复杂的。
好,我们去下一 part 🚀。
Class
有对象,有函数,其实已经可以做到 OOP 了。
但是对比其它纯正 OOP 语言 (C#, Java),JS 还少了一个重要的特性 -- Class。
OOP 语言怎么可以少了 class 呢?
于是 es6 推出了 class 语法糖,oh yeah 😎。
等等...为什么是语法糖而不是语法?
因为早在 es6 之前,JS 已经能实现 class 的特性了。
只不过它是利用 Object + Function + Prototype 这三种特性间接实现的。
这种间接的方式有许多问题:难封装,难理解,代码又碎。
后来,随着越来越多人使用 JS,越来越多人骂,为了平息众怒,ECMA 就另外推出了语法糖 -- class。
上面在讲解 Object 和 Function 时,我刻意避开了与 class 相关的知识,所以它们才那么直观好理解;一旦加入 class 相关知识,理解难度就上去了。
我们先搞清楚 JS 上层的 class 语法糖,之后再去看底层 Object + Function + Prototype 是如何实现 class 特性的,Let's go 🚀。
注:说 class 只是语法糖,其实不是很严谨,虽然几乎 99% 的特性确实可以靠非 class 来模拟,但仍然有一些例外。不过哪些例外几乎不会出现在日常编程中,所以我们把 class 当成语法糖来看待会更好理解。
Class Overview
首先,我们要知道 class 对 JS 而言并不是增加了语言的能力,它只是增加了语言的管理而已。
就好比说,JS 要是少了 for, while 和递归,那么语言就少了 looping 的能力,但如果只是少了 for 和 while,那任然可以用递归的方式实现 looping,只是代码不好管理而已。
class 长这样
class Person {
constructor(firstName, age) {
this.firstName = firstName;
this.age = age;
}
}
const person = new Person('Derrick', 20); // { firstName: 'Derrick', age: 20 }
class 是对象的模板,或者说是工厂,它主要的职责是封装对象的结构和创建过程。
上面有许多知识点:
-
new
new 是创建新对象的意思,new Person 会创建一个新的 person 对象。
我们通常把这个对象叫做 instance (实例)。
-
constructor and new Person
constructor 就像一个函数,new Person() 就像一个函数调用。它们之间可以传递参数,这使得创建过程变得更灵活。
-
this
constructor 中的 this 指向即将被创建的新对象 (实例)。
this.firstName = firstName; 就是给这个实例添加一个 firstName 属性,然后把参数 firstName assign 给它作为 value。
用 Object 和 Function 来表达的话,大概长这样:
function newPerson(firstName, age) {
return {
firstName: firstName,
age: age
}
}
const person = newPerson('Derrick', 20);
当然,这个只是表达,实际上要用 Function + Object + Prototype 做出正真的 class 特性是非常复杂的,这个我们下一 part 才讲,先继续看 class 的其它特性。
术语
一些和 class 相关的术语:
-
Object 是对象
-
Class 是类
-
instance 是实例,通过 new Class() 创建出来的对象就叫实例,这个创建过程叫实例化。-- Instance ≼ Object (实例是一种对象)
-
key value 是键值,它指的是对象的内容 { key: 'value' }
-
property 是属性,method 是方法,当一个对象 key 的 value 类型是函数时,这个 key 被称为方法,当类型是非函数时,这个 key 被称为属性。-- property/method ≼ Key (属性和方法是一种键)
-
constructor 是构造函数,构造什么呢?构造出实例的意思 -- constructor ≼ function (constructor 是一种函数)
Class 的特性
1. constructor (构造函数)
class Person {
constructor(firstName, age) {
this.firstName = firstName;
this.age = age;
}
}
constructor 的职责是构造出实例,构造指的是给实例设置属性和方法。每一次 new Person(),constructor 都会被执行,然后返回一个新实例 (虽然 constructor 里并没有写 return)。
constructor 里的 this 指向当前创建的新实例,this.firstName = firstName 就是往这个新实例添加属性。
另外,constructor 里其实是可以写 return 的,return 的类型必须是对象,然后这个对象将作为 new Person 创建的实例。
虽然可以这么做,但这样做很奇怪,没有逻辑,行为也和其它语言也不一致,所以不要这么干,乖乖用 this 让它自然 return this 就好了。
2. define property
在 class 里,想给实例添加属性有两种方式,这两种方式在一些情况下会有区别,但只是一些情况而已。
-
inside constructor
class Person { constructor(firstName, age) { this.firstName = firstName; this.age = age; } }
在 constructor 里 define 属性的方式就是往 this 添加属性。
这里 this 指向实例,而实例是一种对象。所以,我们怎么给对象添加属性就怎么给 this 添加属性 (比如:使用
Object.defineProperty
也可以)。 -
outside constructor
class Person { firstName = 'Derrick'; lastName = 'Yam' } // 等价于 class Person { constructor (){ this.firstName = 'Derrick'; this.lastName = 'Yam' } }
对 define 属性来说,inside 和 ouside constructor 没有什么区别,唯一的区别就是 outside 无法使用到 constructor 参数而已。
3. define method
在讲解 define 方法之前,我们需要朴上一个知识点 -- 对象方法里的 this。
上面我们有提到,constructor 里的 this 是指向新实例。
这种函数 (constructor) 中使用 this 的概念,同样也适用于对象方法里。
const person = {
firstName : 'Derrick',
lastName : 'Yam',
get fullName() {
return this.firstName + ' ' + this.lastName;
},
set fullName(value) {
const [firstName, lastName] = value.split(' ');
this.firstName = firstName;
this.lastName = lastName;
},
sayMyName() {
console.log(this.fullName);
},
}
person.sayMyName(); // 'Derrick Yam'
person.fullName = 'Alex Lee';
console.log(person.firstName); // 'Alex'
console.log(person.lastName); // 'Lee'
get fullName
, set fullName
, sayMyName
这三个方法里的 this 都指向 person 对象。
好,回到主题 -- define 方法。
define 方法和 define 属性一样,使用 inside 和 outside constructor 两种方式。
但是呢,这两种方式 define 出来的方法是有区别的哦。
-
inside constructor
class Person { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; // define getter Object.defineProperty(this, 'fullName', { get() { return this.firstName + ' ' + this.lastName; } }); // define method this.sayMyName = function() { console.log(this.fullName); } } } const person = new Person('Derrick', 'Yam'); person.sayMyName(); // 'Derrick Yam'
在 constructor 里 define 方法的方式就是往 this 添加方法。
这里 this 指向实例,而实例是一种对象。所以,我们怎么给对象添加方法就怎么给 this 添加方法。
inside define 就代码而言挺丑的,对比我们创建对象的写法
const person = { get fullName(){}, sayMyName() {} } class Person { constructor() { Object.defineProperty(this, 'fullName', { get() {} }); this.sayMyName = function() { } } }
之所以会有那么大的区别,是因为 constructor 里不是创建对象,而是给对象添加方法,所以写法会差这么多了。
-
outside constructor
class Person { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } get fullName() { return this.firstName + ' ' + this.lastName; } sayMyName() { console.log(this.fullName); } }
outside constructor 的写法就和创建对象非常相识了,不像 inside define 那么繁琐。
除了代码好看以外,outside 和 inside 还有两大的区别:
a. outside define 的方法不会被 Object.keys 遍历出来
const person = new Person('Derrick', 'Yam'); console.log(Object.keys(person)); // ['firstName', 'lastName']
fullName
和sayMyName
都不在 key list 里。它们不在 key list 不是因为
enumerable: false
,是其它原因造成的,这个细节下面会再讲解。至于,inside define 的话
sayMyName
会在 key list 里,fullName
则不会,因为fullName
是透过Object.defineProperty
添加的,默认是enumerable: false
。b. outside define 的方法是共享的
const person1 = new Person('Derrick', 'Yam'); const person2 = new Person('Derrick', 'Yam'); console.log(person1.sayMyName === person2.sayMyName); // true
不同实例,但方法 reference 是同一个。
如果是 inside define,那每一个实例的方法都是不同的 reference。
其实 outside define 的效果会更好,毕竟方法内容是一样的,共享 reference 可以节省内存。
如果 inside define 要做到相同效果,那代码会变得更丑😅
function sayMyName() { console.log(this.fullName); } function fullName () { return this.firstName + ' ' + this.lastName; } class Person { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; this.sayMyName = sayMyName; Object.defineProperty(this, 'fullName', { get: fullName }) } }
outside define 如何让所有实例共享一个方法 reference,我们下面会再讲解。
-
outside define but use old school syntax
下面这两个写法效果是不一样的哦
class Person { // old school syntax sayMyName = function () { return ''; } // regular syntax sayMyName() { return '' } }
outside define method by old school syntax 等价于 inside define method
下面这两句是完完全全等价的
class Person { constructor() { this.sayMyName = function() { return '' } } sayMyName = function () { return ''; } }
我们不需要理解为什么它会这样,我们不要这样写就可以了。
我们要嘛使用 inside define,要嘛使用 outside define,但不要用 old school syntax。
4. define private key (a.k.a private field)
private key (属性或方法) 是一个比较新的概念,es2022 才推出的。
所谓 private 的意思是,只有在对象里的方法可以访问到 private key。
const person = {
privateProperty: 'value',
method (){
console.log(this.method); // 这里可以访问
}
}
console.log(person.privateProperty); // 这里不可以访问,要返回 undefined
enumerable: false
只是遍历不出来,但是直接 get value by key 仍然是可以访问到的。
所以 JS 需要一种新的机制才能做到正真的 private。
define private key
class Person {
#privateValue
constructor() {
this.#privateValue = 'value'
}
method() {
console.log(this.#privateValue);
console.log(this['#privateValue']); // 会得到 undefined,因为 private key 只能用 dot notation 访问,不能用 bracket notation 访问
}
}
const person = new Person();
console.log(Object.keys(person)); // [] empty array
person.method(); // 'value'
console.log(person.#privateValue); // IDE Error: Private field '#privateValue' must be declared in an enclosing class
几个知识点:
-
一定要使用 class
-
一定要使用 outside constructor define (不一定要放 default value,但一定要 define)
-
只有在 class 方法里才可以访问 private key,通过实例访问 private key 会直接报错
-
没有任何一种方式可以遍历出 private key。
- private key 只能用 dot notation 访问(
this.#privateValue
可以),不能用 bracket notation 访问(this['#privateValue']
不行)。
5. static key (a.k.a static field) & block
class Person {
static publicName = 'public';
static #privateName = 'private';
static {
console.log('before class Person done');
console.log(Person.#privateName); // value
}
}
console.log('class Person done');
console.log(Person.publicName); // public
console.log(Person.#name); // error: Property '#name' is not accessible outside class 'Person' because it has a private identifier
静态 key (属性或方法) 不属于实例,它们属于 class 本身。使用方式是 Class.staticKey
(e.g. Person.publicName
)
另外,静态 key 也可以是 private 的。
static block 是一个 IIFE 立即调用函数,注意看上面的 log('before class Person done')
执行会早于 log('class Person done')
。
6. instanceof 和 constructor
const person = new Person();
console.log(person instanceof Person); // true;
console.log(person.constructor === Person); // true;
透过 instanceof 我们可以判断某个对象是否属于某个 class 的实例。
也可以通过 instance.constructor
获取它的 class。
注:instanceof 并不是透过 instance.constructor
来做判断的哦,instanceof 还会顾虑继承的情况,细节下面会再讲解。
7. inherit 继承 (extends)
class Parent {
constructor(name) {
this.name = name;
}
parentMethod() {}
sameNameMethod() {}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 一定要在 add property 之前调用 super
this.age = age;
}
childMethod() {}
// override Parent method
sameNameMethod() {
// do something decorate
super.sameNameMethod(); // call parent method
// do something decorate
}
}
const child = new Child('Derrick', 11);
child.parentMethod(); // call parent method
child.childMethod(); // call child method
console.log(child.name); // get parent property
console.log(child.age); // get parent property
console.log(child instanceof Child); // true
console.log(child instanceof Parent); // true
console.log(child.constructor === Child); // true
继承有几个特性:
-
child
实例拥有所有Parent
的属性和方法 -
new Child
时,Parent
的constructor
会被调用 -
child
是Child
的实例,同时也算是Parent
的实例,因此child instanceof Parent
也成立。 -
Child
可以 overrideParent
的属性和方法,也可以通过super
调用Parent
的方法。注:一旦 override 了,即便是
Parent
也会调用到 override 的,这个行为和 C# 的 virtual + override 一样。class Parent { doSomething (){ this.sameNameMethod(); } sameNameMethod() { console.log('parent'); } } class Child extends Parent { sameNameMethod() { console.log('child'); } } const child = new Child(); child.doSomething(); // 'child'
C# 还有一个 new keyword,可以做到只有
Child
调用到 override 的,而Parent
依然调用 original 的,但这个 JS 没有。
Class Init 执行顺序
function getString() {
console.log('3');
return '';
}
function getNumber() {
console.log('1');
return 0;
}
class Parent {
constructor() {
console.log('2'); // 2
}
age = getNumber(); // 1
}
class Child extends Parent {
constructor() {
super();
console.log('4'); // 4
}
name = getString(); // 3
}
const person = new Child();
Class 冷知识
class Person {
name;
age = 11;
}
const person = new Person();
console.log(Object.keys(person)); // ['name', 'age']
name 虽然没有 init value 但依然是会有 property 的哦, value = undefined。
总结
以上就是 Class 的基础知识。
如果你觉得很简单,那就对了。
因为 class 是 es6 出的语法糖,糖自然是甜甜的,真正复杂的是它的底层实现原理,下面会再讲解。
好,我们去下一 part 🚀。
Prototype
Prototype (原型链) 是 JS 的一个特性,其它语言很少见。
它有点像纯正 OOP 语言 (C#, Java) 的继承概念。
不过,只是像而已,请不要把 Prototype 和 Inheritance 划上等号,它俩是不同的机制。
好,我们来看看 Prototype 机制是如何工作的,又和继承有哪些相识之处 🚀。
What's happened?
const person = { };
console.log(person.toString()); // [object Object]
console.log(person.toNumber()); // Uncaught TypeError: person.toNumber is not a function
为什么调用 toNumber
报错了,但调用 toString
却没有报错?
person 对象就是一个空对象,哪来的 toString
方法?
再看一个更神奇的例子
const person = {};
person.__proto__ = { name: 'Derrick' };
console.log(person.name); // Derrick
__proto__
是啥?
为什么 person.name
可以拿到 person.__proto__.name
的 value?
原型链
每个对象都有一个隐藏属性叫 __proto__
。(其实在 ECMA 规范里面我们是不可以直接访问这个属性的,只是浏览器没有遵从这个规则,所以例子里才可以这样写)
这个 __proto__
指的就是 Prototype,它也是一个对象。
而 "每个对象都有一个 __proto__
" ,所以这个 __proto__
对象也有它自己的 __proto__
对象🤪。
这样一个接一个 __proto__
就构成了原型"链"。
原型链的终点
当一个对象的 __proto__
= null 时,它就是原型链的终点。
原型链查找
当我们访问 object.key
的时候,JS 引擎首先会查找 object 所有的 keys,如果没有找到这个 key,那它会去 object.__proto__
对象中继续找。
如果找到了就会返回它的值,如果找不到就沿着原型链继续找,直到结束。
Object.prototype
const person = {};
console.log(person.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(person) === Object.prototype); // 正规获取 Prototype 的方式是调用 Object.getPrototypeOf,访问私有属性 __proto__ 是不正规的做法。
console.log(person.toString()); // [object Object]
console.log(person.toNumber()); // Uncaught TypeError: person.toNumber is not a function
当我们创建对象时,对象默认的 __proto__
(prototype) 是 Object.prototype
对象,而 Object.prototype
的 __proto__
则是 null。
整条原型链是:person
→ Object.prototype
→ null
一共两个可查找的对象。
Object.prototype
对象中包含了许多方法,其中一个就是 toString
方法。
当调用 person.toString
时,JS 引擎先找 person 的 keys,看有没有 toString
方法,结果没有,于是它会继续找 person.__proto__
(也就是 Object.prototype
对象),然后就找到了 toString
方法。
当调用 person.toNumber
时,也是同样的查找路线,只不过,找到最后 Object.prototype
里并没有 toString
方法,所以就报错了。
set Prototype
const person = {};
person.__proto__ = { name: 'Derrick' };
Object.setPrototypeOf(person, { name: 'Derrick' }); // 正规写法
对象的 prototype 是可以任意替换的,调用 Object.setPrototypeOf
传入对象或 null
就可以了。
创建对象同时 set Prototype
对象默认的 prototype 是 Object.prototype
对象。
我们可以透过 Object.create
方法,在创建对象时自定义对象的 prototype。
const person = Object.create({ name: 'Derrick' });
// 等价于下面
const person = {};
Object.setPrototypeOf(person, { name: 'Derrick' });
// 创建一个没有 __proto__ 的对象
const animal = Object.create(null);
// 等价于下面
const animal = {}; // 默认 __proto__ 是 Object.prototype
Object.setPrototypeOf(animal, null);
用 Prototype 特性实现 Inheritance
Inheritance 的特色是 child 可以访问 parent 的属性和方法,我们可以利用原型链查找机制实现这个效果。
const parent = {
name: 'Derrick',
method() {
console.log('done');
},
};
const child = Object.create(parent);
console.log(child.name); // 'Derrick'
child.method(); // 'done'
小心原型链的坑
看注释理解
const parent = {
age: 11,
fullName: {
firstName: '',
lastName: '',
},
};
const child = Object.create(parent);
console.log(child.age); // read from parent
child.age = 15; // 注意: 这里不是 set property to parent 而是 add property to child
console.log(child.age); // 注意: 这个是 read from child
console.log(child.fullName.firstName); // read from parent
child.fullName.firstName = 'Derrick'; // 注意: 这里是 set property to parent, 而不是 add property to child,和上面不同哦
console.log(child.fullName.firstName); // 注意: 这里依然是 read from parent
以前 AngularJS 的 $scope 就使用了 Prototype 概念,这导致了许多人在 set value 的时候经常出现 bug。
原因就是上面这样,没搞清楚什么时候是 set to child,什么时候是 set to parent。
总之,get from prototype 不代表就是 set to prototype,因为对象属性是可以动态添加的,你以为是 set property,结果它是 add property。
Prototype 对 get all keys 的影响
const parent = {
firstName: 'Derrick',
lastName: 'Yam',
};
const child = Object.create(parent);
child.age = 20;
const allKeys = Object.keys(child);
console.log(allKeys); // ['age']
Object.keys
, values
, entries
, getOwnPropertyNames
都无法遍历出 prototype 的 key。
上面例子中,只有 child 自己本身 (Own) 的 key 才能被遍历出来。
如果我们想遍历所有原型链的 key,需要使用 for in 语法
const parent = {
firstName: 'Derrick',
lastName: 'Yam',
sayMyName() {}
};
const child = Object.create(parent);
child.age = 20;
child.doSomething = function(){ }
for (const key in child) {
const isOwnKey = child.hasOwnProperty(key);
const isPrototypeKey = !child.hasOwnProperty(key);
console.log(key, `isOwnKey: ${isOwnKey}`);
console.log(key, `isPrototypeKey: ${isPrototypeKey}`);
// age isOwnKey: true
// doSomething isOwnKey: true
// firstName isPrototypeKey: true
// lastName isPrototypeKey: true
// sayMyName isPrototypeKey: true
}
两个知识点:
-
hasOwnProperty
方法可以查看 key 是 Own 还是 from Prototype。 -
for in
和Object.keys
一样只能遍历出enumerable: true
的 key,同时 symbol key 也一样是遍历不出来的。
所以说 for in
也不是万能的,如果我们真想完完全全的找出每一个 key,需要搭配使用:
-
Object.getPrototypeOf
获取对象的 prototype,这样就可以沿着原型链找下去。
-
Object.getOwnPropertyNames
获取对象所有 keys 包括
enumerable: false
。 -
Object.getOwnPropertySymbols
获取对象所有 symbol key。
Prototype 对方法 this 的影响
const parent = {
firstName: 'Derrick',
lastName: 'Yam',
sayMyName() {
console.log(this.firstName);
}
};
const child = Object.create(parent);
child.firstName = 'Child Name';
child.sayMyName(); // 'Child Name', 此时 sayMyName 的 this 指向 child
parent.sayMyName(); // 'Derrick', 此时 sayMyName 的 this 指向 parent
注意看 child.sayMyName()
,child
本身没有 sayMyName
方法。
因此它会找 child.__proto__
(也就是 parent
) 的 sayMyName
方法来调用。
不过,sayMyName
里头的 this
却不是指向 parent
,而是指向 child
。
this
有点像是被偷龙转风了。
总结
Prototype 为对象添加了继承的能力,第一次看到或许会感觉有点绕,但了解机制后会发现它其实挺简单的。
就两个概念:
-
对象和对象关联起来后叫原型链
-
对象访问时会沿着原型链挨个挨个查找
好,以上就是 Prototype 的基础知识。
如果你觉得很简单,那就对了。
因为 Prototype 并没有破坏 Object 的基础知识,它只是扩展了一些特性而已。
真正复杂的是用 Prototype 去实现 class 底层的继承特性。
我们继续下一 part 吧 🚀。
class 和 prototype 的关系
class 和 prototype 有着紧密的关系,我们来了解一下 🚀。
instance.__proto__ & class.prototype
class Person {}
const person = new Person();
console.log(person.constructor === Person); // true
person
实例有一个 constructor
属性,它的值指向 Person
class。
console.log(Object.getOwnPropertyNames(person)); // []
不,person
实例并没有 constructor
属性。
本身没有,那就一定是在原型链中。
console.log(Object.getOwnPropertyNames(person.__proto__)); // ['constructor']
console.log(person.__proto__.constructor === Person); // true
果不其然,那 person
的 __proto__
指向的是谁呢?
答案是 Person.prototype
console.log(person.__proto__ === Person.prototype); // true
两个知识点:
-
每个
class
都会有prototype
(一个 static key) 对象,对象里有一个constructor
属性指向回class
本身。class Person {} console.log(Person.prototype.constructor === Person); // true
- 实例 (instance) 的
__proto__
指向它的class.prototype
。
shared method 的原理
class Dog {
jump = function() {}
bite() {}
}
const dog1 = new Dog();
const dog2 = new Dog();
console.log(dog1.jump === dog2.jump); // false
console.log(dog1.bite === dog2.bite); // true
bite
是 shared method,jump
则不是。
console.log(Object.getOwnPropertyNames(dog1)); // ['jump']
jump
是 dog
的 key,bite
则不是。
什么原理?
console.log(dog1.bite === Dog.prototype.bite); // true
bite
被定义在 prototype (dog1.__proto__.bite
或者说 Dog.prototype.bite
) 中,而不是在实例本身。
结论:shared method 是靠 prototype 实现的。
extends 和 prototype 的关系
class Animal {}
class Dog extends Animal {}
const lucky = new Dog();
console.log(lucky.__proto__ === Dog.prototype); // true
console.log(lucky.__proto__.__proto__ === Animal.prototype); // true
lucky
是 Dog
的实例,Dog
继承自 Animal
。
class 的继承是靠 prototype 实现的。
Dog
继承 Animal
会导致 Dog.prototype.__proto__
=== Animal.prototype
。
原型链:lucky
→ Dog.prototype
→ Animal.prototype
原型链只负责 shared method,普通 method 和 property 是在实例本身,即便是父类也会被定义到实例本身
class Animal {
jump = function() {} // non shared method
run() {} // shared method
}
class Dog extends Animal {
bite = function () { } // non shared method
walk() {} // shared method
}
const dog1 = new Dog();
console.log(Object.keys(dog1)); // ['jump', 'bite'],Animal(父类)的 jump 也在实例里,因为它不是 shared method
console.log(dog1.walk === Dog.prototype.walk); // true, 原型链路线:dog1.__proto__.walk
console.log(dog1.run === Animal.prototype.run); // true,原型链路线:dog1.__proto__.__proto__.run
instanceof 的原理
class Animal {}
class Dog extends Animal {}
const lucky = new Dog();
console.log(lucky instanceof Dog); // true
console.log(lucky.constructor === Dog); // true
console.log(lucky instanceof Animal); // true
instanceof
不是依靠 lucky.constructor
做判断,而是靠 __proto__
vs class.prototype
。
console.log(lucky.__proto__ === Dog.prototype); // true,因此 lucky instanceof Dog
console.log(lucky.__proto__.__proto__ === Animal.prototype); // true, 因此 lucky instanceof Animal
只要原型链中,其中一个对象和 class.prototype
相等,那就是 instanceof
。
特殊情况
console.log(new Number(0) instanceof Number); // true
这个合理。
console.log(Object.getPrototypeOf(0) === Number.prototype); // true
0
不是对象,但是有 prototype?
可能是因为 auto-boxing 的缘故,算合理吧。
console.log(0 instanceof Number); // false
0
的 prototype 是 Number.prototype
啊,怎么 instanceof
是 false
?
因为这里又不 auto-boxing 了...😔
Function this, apply, call, arrow function, bind
上面我们有提到 this
这个关键字被用于 class 的 constructor 还有 method 中。
这个很好理解,因为纯正 OOP 语言 (C# / Java) 也是这样用的。
this in Function
不过,在 JS,this
还可以被用于 Function 中。
function doSomething () {
console.log(this === window); // true
}
doSomething();
在浏览器环境,函数中的 this 指向 window 对象。
不过这是一个历史原因,在 'use strict'
(es5) 模式下,函数中的 this 会是 undefined
。
'use strict'; // 使用 'use strict' 模式
function doSomething () {
console.log(this === undefined); // true
}
doSomething();
但是!有例外:
'use strict';
function handleClick (event) {
console.log(event.currentTarget === this); // true
}
document.body.addEventListener('click', handleClick);
在 DOM API addEventListener
的 callback 函数中,this 指向的是 event.currentTarget
,而不是 undefined。
call & apply
它是怎么做到的呢?
答案是透过 call
方法调用函数,并传入 this
值。
'use strict';
function doSomething (param1, param2) {
console.log(this); // 'hello world'
console.log(param1); // 'param1'
console.log(param2); // 'param2'
console.log(arguments.length); // 2
}
// 透过 call 方法执行 doSomething 函数
// 参数一将成为 this 值
doSomething.call('hello world', 'param1', 'param2');
call
方法的第一个参数是 this
值,其它参数对应的是函数的参数。
另外,apply
方法也是相同的功能。
doSomething.call('hello world', 'param1', 'param2');
// 完全等价于
doSomething.apply('hello world', ['param1', 'param2']);
唯一的区别只是第二参数是采用 array。
call
和 apply
技巧常用来偷龙转风对象方法里的 this
。
const person = {
firstName: 'Derrick',
lastName: 'Yam',
getFullName() {
return this.firstName + ' ' + this.lastName;
}
};
console.log(person.getFullName()); // 'Derrick Yam'
// 借用 person.getFullName 方法,但偷龙转风 this 对象
console.log(
person.getFullName.call({ firstName: 'Alex', lastName: 'Lee' }) // 'Alex Lee'
);
类似于我们借用了某个对象的方法。
Arrow Function
Arrow Function 是 es6 推出的,它的写法有点像 C# 的 lambda 表达式。
它和普通的 Function 有两个区别:
第一个是 this
'use strict';
const that = this;
const handleClick = (event) => {
console.log(event.currentTarget === this); // false
console.log(this === that); // true
}
document.body.addEventListener('click', handleClick);
arrow function 里,没有自己的 this
。
handleClick
函数内的 this
,其实是函数外的 this
来的。
因此,call
/ apply
传入 this
也无用。
'use strict';
const that = this;
const handleClick = (param1) => {
console.log(this === that); // true 依然是 that
console.log(param1); // 'param1'
}
handleClick.call('hello world', 'param1');
第二个区别是:arrow function 里没有 arguments
对象
const handleClick = () => {
console.log(arguments); // Error: arguments is not defined
}
尝试访问 arguments
对象会直接报错。
少了 arguments
,我们可以改用 rest parameters (es6) 来获取动态的参数。
const handleClick = (...args) => {
console.log(args.length); // 2
}
handleClick('1', '2');
另外,对象方法采用 arrow function 的话,一样会没有 this
哦:
'use strict';
const that = this;
const person = {
// function old school 写法
method1: function () {
console.log(this === person); // true
},
// function modern 写法
method2() {
console.log(this === person); // true
},
// arrow function
method3: () => {
console.log(this === that); // true
}
}
person.method1();
person.method2();
person.method3();
bind
bind
方法的作用是封装 parameters。
看例子
'use strict';
function doSomething (param1, param2) {
console.log(this); // 'this'
console.log(param1); // 'param1'
console.log(param2); // 'param2'
console.log(arguments.length); // 2
}
const boundDoSomething = doSomething.bind('this', 'param1', 'param2');
boundDoSomething();
调用 doSomething.bind
不会执行 doSomething
,它会返回一个包装了 'this'
, 'param1'
, 'param2'
参数的另一个函数 -- boundDoSomething
。
调用 boundDoSomething
才会执行 doSomething
,我们无需再传入参数,因为参数之前已经 bound 进去了。
另外,可以只 bind 头一部分
const boundDoSomething = doSomething.bind('this', 'param1'); // 只 bind param1
boundDoSomething('param2'); // 补上 param2
后一部分等调用时再补上也可以。
不过,前面 bound 了的,后面无法 override
const boundDoSomething = doSomething.bind('this', undefined, 'param2'); // 尝试 param1 留空
boundDoSomething('param3'); // 尝试补上 param1。不行!它将成为 param3
注:参数和 this
都无法 override。
class 的底层原理
上面我们提到,class 只是语法糖,它的本质是 Object + Function + Prototype。
在介绍 Object、Function、Prototype 时,我刻意避开了跟 class 有关的特性,因此它们都很好理解。
不过,一旦涉及 class 的特性,它们就会变得异常复杂。
这也是为什么 es6 会推出上层的 class 作为语法糖,用以隐藏这些复杂性。
作为 JS 的学习者,搞懂这些底层特性,有助于我们理解 JS 这门语言的灵活性。
虽然在日常业务代码中几乎不会用到,但在研究前端框架 (如 Angular) 的源码时,懂这些技巧还是很有帮助的。
我们就一起来探索吧🚀。
万物皆是对象
实例化一个日期
const date = new Date();
显然 Date
是一个 class,因为它可以实例化。
那 typeof Date
会是 ... ?
console.log(typeof Date); // 'function'
是函数😨
Object.keys
我们学过
const person = { name: 'Derrick', age: 11 }
console.log(Object.keys(person)); // ['name', 'age']
Object
有 keys
方法,所以它是对象...吗?
console.log(typeof Object); // 'function'
不,它也是函数😨
其实是这样:Object
和 Date
都是 class,而 typeof class
是 'function'
。
const obj1 = {};
const obj2 = Object.create(Object.prototype);
const obj3 = new Object(); // Object 是 class,它可以实例化出对象
以上三种创建对象的方式完全是等价的。
结论:
-
class 的本质是函数
class Person {} console.log(typeof Person); // 'function'
-
函数也是一种对象
function Person (){ } // Person 函数也是一种对象,所以它可以 add/remove property Person.age = 11; console.log(Person.age); // 11 delete Person.age; console.log(Person.age); // undefined
new Function?
class 的本质是函数,难道我们 new
的是函数?
function Person () {} // Person 是函数
const person = new Person(); // new 函数
console.log(typeof person); // object
console.log(person instanceof Person); // true
没错!new
函数,会返回一个对象 (实例),即便函数没有 return。
我们可以把函数当作是 class
的 constructor
。
这个
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const person = new Person('derrick', 11);
完全等价于这个
function Person (name, age) {
this.name = name;
this.age = age
}
const person = new Person('derrick', 11);
有一个知识点 -- 函数中的 this
。
function Person (name, age) {
console.log(this); // undefined;
}
const person = Person('derrick', 11);
如果是普通函数调用,那函数里的 this
是 undefined。
如果是 new
函数,this
则是 person
实例。
static key (a.k.a static field) & block
这个
class Person {
static age = 11;
static {
console.log('run');
}
}
完全等价于
function Person() {}
Person.age = 11;
console.log('run');
因为函数也是对象,所以可以 set property for static key。
而 class block 只是一个在函数之后的执行而已。
private key
private key 是 es2022 才推出的。它不能简单的用函数去替代。
class Person {
#age = 11;
}
底层实现是这样
let _Person_age, _Person_name;
function Person() {
_Person_age.set(this, 11);
_Person_name.set(this, 'derrick');
}
_Person_age = new WeakMap();
_Person_name = new WeakMap();
其实跟函数没什么关系了,主要是靠外部变量实现的。
define shared method
class Person {
sharedMethod() {}
nonSharedMethod = function() {}
}
const person = new Person();
console.log(person.sharedMethod === Person.prototype.sharedMethod); // true
console.log(person.nonSharedMethod === Person.prototype.nonSharedMethod); // false
一个是 shared method,一个不是。
以函数来表达是这样
function Person() {
this.nonSharedMethod = function() {}
}
Person.prototype.sharedMethod = function() {}
因为实例与 class 有 prototype 关系 (person.__proto__
等于 Person.prototype
),所以能透过原型链找到 sharedMethod
来调用。
inherit 继承 (extends)
class 版本的继承
class Animal {
constructor(age) {
this.age = age;
}
jump() {
console.log('jump', this.age);
}
}
class Dog extends Animal {
constructor() {
super(11);
}
bite() {
console.log('bite');
}
}
const dog = new Dog();
dog.jump(); // 'jump' 11
dog.bite(); // bite
console.log(dog.__proto__ === Dog.prototype); // true
console.log(dog.__proto__.__proto__ === Animal.prototype); // true
函数版本的继承
function Animal(age) {
this.age = age;
}
Animal.prototype.jump = function( ) {
console.log('age', this.age);
}
function Dog () {
Animal.call(this, 11);
}
Dog.prototype.bite = function() {
console.log('bite');
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
主要做两件事:
1. 调用父类构造函数
2. 串联 prototype (Dog
to Animal
)
class / function 总结
class
的所有特性,function
都有,因为 class
的本质就是 function
。
function Animal () {} // or class Animal {}
const dog = new Animal();
console.log(typeof Animal); // 'function'
console.log(Animal.prototype.constructor === Animal); // true
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
Mixins
参考:
YouTube – TypeScript Mixin Classes - JavaScript Multiple Class Inheritance // Advanced TypeScript
以前写过相关的文章 – angular2 学习笔记 (Typescript)
TypeScript Issue – Generating type definitions for mixin classes with protected members
多继承概念 (组合 class)
我们常听人家说,多用组合,少用继承,其原因是继承不够灵活。
某一些语言是有支持多继承概念的 (比如 C++),但许多语言是没有支持的 (比如 C# / Java)。
多继承就是一个 class 同时 extends 好几个 class,也可以理解为把多个 class 组合起来。
类似这样
class ParentA {}
class ParentB {}
class ChildA extends ParentA, ParentB {}
这样 ChildA
就同时有了 ParentA
和 ParentB
的属性。
真实使用场景
查看 Angular Material 源码 (以前的,现在没有了) 会发现,里面大量使用了多继承的概念 (也可以理解为组合 class)
这些 mixinXXX 的方法里头都封装着一个 class
很明显,多继承肯定比单继承灵活,至于是否增加了复杂度,这个不一定,但有你可以选择不用,总好过没有。
JavaScript 不支持多继承
JS 没有支持多继承,但是!
JS 灵活啊,它总有它自己的办法去实现的,就像用 Function 来实现 class 那样。
动态继承 == 多继承
JS 是动态语言,因此它可以动态创建 class 和 extends,这就让它有了实现多继承的能力。
首先,来个需求
class ParentA {
parentA = 'a';
}
class ChildA extends ParentA{}
class ParentB {
parentB = 'b';
}
class ChildB extends ParentB{}
假设,我想封装 ChildA
和 ChildB
的共同属性,那么我就做一个 ChildAB
。
如果 JS 支持多继承, 那么它大概长这样
class ParentA {
parentA = 'a';
}
class ChildA extends ChildAB, ParentA {}
class ParentB {
parentB = 'b';
}
class ChildB extends ChildAB, ParentB{}
class ChildAB {
childABValue = 'c';
}
JS 要实现要靠动态继承
首先把 ChildAB
改成一个方法
function mixinsChildAB(baseClass) {
return class extends baseClass {
childABValue = 'c';
};
}
看到吗,baseClass
是参数,因此它是动态 create class + variable extends
接着
const ChildABToParentA = mixinsChildAB(ParentA);
class ChildA extends ChildABToParentA {}
现在的关系是 ChildA
≼ ChildABToParentA
≼ ParentA
接着
const ChildABToParentB = mixinsChildAB(ParentB);
class ChildB extends ChildABToParentB {}
在创建一个 class 链接到 ParentB
现在的关系是 ChildB
≼ ChildABToParentB
≼ ParentB
最后
const childA = new ChildA();
console.log(childA.childABValue); // c
console.log(childA.parentA); // a
const childB = new ChildB();
console.log(childB.childABValue); // c
console.log(childB.parentB); // b
总结
JS 没有多继承,它只是利用动态创建 class extends
的特性,实现了动态继承。
从而间接达到了类似多继承的效果。