本文从 JavaScript 继承中,最核心的原型链出发,介绍了在 ES5 和 ES6 中一些实现继承的方式,及其背后的原理。
原型链
基于原型的语言
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype
属性上,而非对象实例本身。
在 JavaScript 中是在对象实例和它的构造器之间建立一个链接(它是__proto__
属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
理解对象的原型(可以通过
Object.getPrototypeOf(obj)
或者已被弃用的__proto__
属性获得)与构造函数的prototype
属性之间的区别是很重要的。**前者是每个实例上都有的属性,后者是构造函数的属性。**也就是说,Object.getPrototypeOf(new Foobar()) === Foobar.prototype
和基于类的语言的区别
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。但是在 JavaScript 是如上文说的通过引用的方式在原型链上寻找对应的方法或属性,所以这种可能更应该被称为委托。
核心图解(基础)
我们先祭出简单版本的图,并以几个关键问题阐述
- 函数的
prototype
属性指向了一个对象,这个对象是调用该 构造函数 创建的实例的 原型(所以叫实例原型) - 实例对象有一个
__proto__
属性,指向他的原型 - 每一个实例原型都有一个
constructor
属性指向关联的 构造函数
为什么有的对象有 prototype
有的没有
JavaScript 分为函数对象和普通对象,每个对象都有 __proto__
属性,但是只有函数对象才有 prototype
属性
JavaScript 是如何访问原型链上的属性或方法的
在调用继承自 Object
的 Person
的 person
上的 valueOf
方法时(person.valueOf()
),会发生以下过程:
- 浏览器首先检查,
person
对象是否具有可用的valueOf()
方法。 - 如果没有,则浏览器检查
person
对象的原型对象(即Person
构造函数的prototype
属性所指向的对象)是否具有可用的valueof()
方法。 - 如果也没有,则浏览器检查
Person()
构造函数的prototype属性所指向的对象的原型对象(即Object
构造函数的prototype属性所指向的对象)是否具有可用的valueOf()
方法。这里有这个方法,于是该方法被调用。
为什么 person.constructor === Person
?
首先,我们需要知道 实例原型(即 Person.prototype
)的 constructor
指向构造函数本身
从图里我们会发现, person
上并没有 constructor
属性,那为什么问题里的判等成立呢?——因为访问 person.``constructor
时,发现属性不存在, 则会从 person
的原型也就是 Person.prototype
中读取,这个时候能访问到,因此:person.constructor === Person.prototype.constructor
核心图解(进阶)
接下来我们换一张复杂一些的,加上了 Function
,变得复杂了起来。
原型链的顶端是什么
Object.prototype
是原型链的 root ,它指向了null
(Object.prototype === null
)
Object 和 Function
- 对于 Function 而言
Function.prototype.__proto__ === Object.prototype
这里 Function 和 Object 的联系建立了起来 —— 函数是一个对象Object.__proto__-> Function.prototype
Object 是一个函数(构造函数)Function.__proto__ === Function.prototype
这里是 Function 和其他人都不一样的地方。而查找的时候,则会从Function.prototype
查到Object.prototype
走完原型链
- 对于 Object
- 这里解释了为什么
Object.prototype.valueOf === Object.valueOf
,为什么 Object 上可以直接访问到 Object.prototype 上的属性,而当 Person 却不能直接访问Person.prototype
上的属性 ——Object.__proto__-> Function.prototype
- 这里解释了为什么
Function.prototype.__proto__->Object.prototype
鸡蛋问题
- 到底是先有鸡还是先有蛋?现有
Object
还是先有Function
?Object instanceof Function === true
Function instanceof Object === true
- 核心:
Object.prototype.__proto__ === null
,人为规定了最开始的rootPrototype
,所以可以认为是先有Object
ES5 中的继承
上文简单阐述了 JavaScript 中原型链的机制,下面我们来说说基于这种机制,如何实现继承
原型链继承
// 父
function Parent () {
this.name = 'foo';
}
// 在原型链上添加方法
Parent.prototype.getName = function () {
console.log(this.name);
return this.name
}
// 子
function Child () {
}
Child.prototype = new Parent();
const child1 = new Child();
console.log(child1.getName()) // foo
复制代码
最简单的继承
- 缺点:
- 引用类型的属性被所有实例共享
- 即,修改原型链上的引用类型属性时,会影响到所有 Child
- 在创建 Child 的实例时,不能向 Parent 传参
- 引用类型的属性被所有实例共享
借用构造函数继承
// 父
function Parent (names) {
this.names = names;
}
// 子
function Child (names) {
Parent.call(this, names);
}
const child1 = new Child([]);
child1.names.push('test');
console.log(child1.names); // ["test"]
const child2 = new Child([]);
console.log(child2.names); // []
复制代码
- 优点
- 避免了引用类型的属性被所有实例共享
- 可以在 Child 中向 Parent 传参
- 缺点:
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
组合继承【常用】
// 父
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
// 子
function Child (name, age) {
// 执行 Parent 的构造函数
Parent.call(this, name);
this.age = age;
}
// 修改 prototype 到父元素实例。以便让子元素实例通过 __proto__ 使用父元素属性
// 但是多执行了一次 Parent 的构造函数
Child.prototype = new Parent();
// 修正 constructor 以保证 child.constructor === Child
Child.prototype.constructor = Child;
const child1 = new Child('foo', '18');
child1.colors.push('black');
console.log(child1.name); // foo
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
const child2 = new Child('bar', '20');
console.log(child2.name); // bar
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
复制代码
- 缺点
- 会调用两次父构造函数,导致
prototype
和 实例上有重复的属性
- 会调用两次父构造函数,导致
原型式继承
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
复制代码
就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
- 缺点
- 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样
寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
function createObj (o) {
const clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
复制代码
- 缺点
- 跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
寄生组合式继承【常用】
核心:不使用 Child.prototype = new Parent()
,而是间接的让 Child.prototype
访问到 Parent.prototype
,少一次调用 Child
的构造函数
之前是通过构造函数,来创建 __proto__
的指向,而现在我们改进后,直接修改 Child 的 prototype
指向 Parent 的 prototype
// 代码封装
function object(o) {
// 使用一个“干净”的函数,没有执行 Parent 的构造函数
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
const prototype = object(parent.prototype);
// 修正 consturctor
prototype.constructor = child;
// 本质上约等于 child.prototype.__proto__ = parent.prototype
child.prototype = prototype;
}
// 业务代码
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 当我们使用的时候:
prototype(Child, Parent);
const child1 = new Child('foo', '18');
console.log(child1);
复制代码
- 优点
- 只调用了一次 Parent 构造函数
prototype
上没有有重复的属性- 原型链还能保持不变,能够正常使用 instanceof 和 isPrototypeOf
继承静态方法
我们前面讲的所有的继承方法,都没有实现构造函数上的静态方法继承,然而在ES6的 class
中,子类是可以继承父类的静态方法的。
我们可以通过 Object.setPrototypeOf()
方法实现静态方法的继承。
function prototype(child, parent) {
const prototype = Object.create(parent.prototype);
// 修正 consturctor
prototype.constructor = child;
// 即 child.prototype.__proto__ = parent.prototype
child.prototype = prototype;
}
// 业务代码
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 静态方法
Parent.staticFn = function () {
return "Parent";
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 继承静态方法
Object.setPrototypeOf(Child, Parent);
// 当我们使用的时候:
prototype(Child, Parent);
const child1 = new Child('foo', '18');
console.log(child1);
Child.staticFn()
复制代码
扩展 – Object.create()
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__
。 也就是说,新创建一个对象,这个对象的 __proto__
指向了一个现有对象
参数
Object.create(proto,[propertiesObject])
proto
新创建对象的原型对象。propertiesObject
【可选】需要传入一个对象,该对象的属性类型参照Object.defineProperties()
的第二个参数(即 数据描述符 –configurable
、enumerable
、value
、writable
和 访问器描述符 –get
、set
)。如果该参数被指定且不为undefined
,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。
使用场景
- 在需要实例原型,但不需要执行构造函数的时候(比如寄生组合继承)
o = new Constructor()
和o = Object.create(Constructor.prototype)
的区别就在于,后者没有执行构造函数
- 创建一个纯净的对象
- Object.create(null)
Polyfill
Object.create = (proto, propertiesObject) => {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object: ' + proto);
}
if (propertiesObject === null) throw 'TypeError'
function F () {}
F.prototype = proto
const o = new F()
// 添加属性
if (typeof propertiesObject !== 'undefined') {
Object.defineProperties(o, propertiesObject)
}
// 如果 proto 为 null,需要去除 __proto__
if (proto === null) o.__proto__ = null
// 返回新的对象
return o
}
复制代码
扩展 – Object.setPrototypeOf()
设置一个指定的对象的原型 ( 即, 内部[[Prototype]]
属性,也就是 __proto__
)到另一个对象或 null
。
和 Object.create
的微小区别
Object.create
是新创建一个对象,这个对象的__proto__
指向了一个现有对象Object.setPrototypeOf()
是 设置一个指定的对象的原型
也就是说,Object.create
会多一层对象
参数
Object.setPrototypeOf(obj, prototype)
obj
要设置其原型的对象。prototype
该对象的新原型(一个对象 或null
).
使用场景
- 继承静态方法
- 如上文
Polyfill
Object.prototype.setPrototypeOf = function(obj, proto) {
if(obj.__proto__) {
obj.__proto__ = proto;
return obj;
} else {
// 如果你想返回 prototype of Object.create(null):
const Fn = function() {
for (const key in obj) {
Object.defineProperty(this, key, {
value: obj[key],
});
}
};
Fn.prototype = proto;
return new Fn();
}
}
复制代码
ES6 中的继承 – Class 语法糖
ES6 中新增了 Class,使得实现继承得到了简化。我们先来看简单看基础
类表达式 和 类声明
实际上,类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。
- 类声明:定义类的一种方法是使用类声明。要声明一个类,你可以使用带有class关键字的类名
- 类表达式:类表达式可以命名或不命名。命名类表达式的名称是该类体的局部名称。
// 类声明
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
// 类表达式 - 匿名类
let Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
// 类表达式 - 命名类
let Rectangle = class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
复制代码
类体和方法定义
类元素拥有以下几种属性
- 构造函数:
constructor
方法是一个特殊的方法,这种方法用于创建和初始化一个由class
创建的对象- 注意:类的内部所有定义的方法,都是不可枚举的,也就是说
// class
class Person {
constructor(name) {
this.name = name;
}
}
// ES5
function Person(name) {
this.name = name;
}
复制代码
- 原型方法
- 注意:类的内部所有定义的方法,都是不可枚举的
// class
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return 'hello, I am ' + this.name;
}
}
Object.keys(Person.prototype); // []
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]
// ES5
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
return 'hello, I am ' + this.name;
};
Object.keys(Person.prototype); // ['sayHello']
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]
复制代码
- 静态方法:
static
关键字用来定义一个类的一个静态方法。调用静态方法不需要实例化该类,但不能通过一个类实例调用静态方法。类比 ES5 中定义在构造函数对象上的方法。
// class
class Person {
static sayHello() {
return 'hello';
}
}
Person.sayHello() // 'hello'
const foo = new Person();
foo.sayHello(); // TypeError: foo.sayHello is not a function
// ES5
function Person() {}
// 不在原型链上
Person.sayHello = function() {
return 'hello';
};
Person.sayHello(); // 'hello'
var foo = new Person();
kevin.sayHello(); // TypeError: foo.sayHello is not a function
复制代码
- 实例属性
- 实例的属性必须定义在类的方法里。(但是有一个 Stag 3的提案 可以直接写在类里面)
- 静态的或原型的数据属性必须定义在类定义的外面。
// class
class Person {
constructor() {
this.state = {
count: 0
};
}
}
// 新的 Field declarations
class Person {
state = {
count: 0
};
}
// ES5
function Person() {
this.state = {
count: 0
};
}
复制代码
- 静态属性
- 静态公有字段在你想要创建一个只在每个类里面只存在一份,而不会存在于你创建的每个类的实例中的属性时可以用到。静态公有字段不会在子类里重复初始化
// class
class Person {
static name = 'foo';
}
// ES5
function Person() {};
Person.name = 'foo';
复制代码
getter
和setter
- 与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
// class
class Person {
get name() {
return 'bar';
}
set name(newName) {
console.log('new name 为:' + newName)
}
}
let person = new Person();
person.name = 'foo';
// new name 为:foo
console.log(person.name);
// bar
// ES5
function Person(name) {}
Person.prototype = {
get name() {
return 'bar';
},
set name(newName) {
console.log('new name 为:' + newName)
}
}
let person = new Person();
person.name = 'foo';
// new name 为:foo
console.log(person.name);
// bar
复制代码
- 私有属性/方法
- 在属性/方法前加上
#
即可让其成为私有的,#
是名称的一部分,也用于访问和声明。 - 私有字段仅能在字段声明中预先定义,不能通过在之后赋值来创建它们
- 在属性/方法前加上
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
static #privateStaticMethod() {
return 42;
}
}
// 这个在自己实现起来很恶心,可以使用 闭包/Symbol/WeakMap 实现,暂且不详细叙述
复制代码
Babel是如何编译你的 Class 的
下文编译均来自 babel try it out
只有构造函数
// Input
class Person {
constructor(name) {
this.name = name;
}
}
// Output
"use strict";
// 在环境支持与不支持Symbol的情况做了区分
// 在 Symbol 存在的环境下,left instanceof right 实际上是 right[Symbol.hasInstance](left),同时可以在类的内部去覆写这个方法
function _instanceof(left, right) {
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
// _classCallCheck 的作用是检查 Person 是否是通过 new 的方式调用,类必须使用 new 调用,否则会报错。
// 当我们使用 var person = Person() 的形式调用的时候,this 指向 window,所以 instance instanceof Constructor 就会为 false,与 ES6 的要求一致。
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function Person(name) {
_classCallCheck(this, Person);
this.name = name;
};
复制代码
实例属性,静态属性,私有属性
// Input
class Person {
// 实例属性
foo = 'foo';
// 静态属性
static bar = 'bar';
// 私有属性
#test = 'test';
constructor(name) {
this.name = name;
}
}
// Output
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// defineProperty 修改/设置属性
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
// 通过 weakMap 实现私有变量,详见下文扩展
var _test = new WeakMap();
var Person = function Person(name) {
_classCallCheck(this, Person);
_defineProperty(this, "foo", 'foo');
_test.set(this, {
writable: true,
value: 'test'
});
this.name = name;
};
_defineProperty(Person, "bar", 'bar');
复制代码
我们可以清楚的发现:
- 实例属性通过
defineProperty
在构造函数中设置到了实例上 - 静态属性
defineProperty
在构造函数外设置到了构造函数上 - 私有属性 和 实例属性/静态属性 是独立的,通过 weakMap 实现
实例方法,静态方法,私有方法,getter/setter
// Input
class Person {
#hello = 'hello'
constructor(name) {
this.name = name;
}
sayHello() {
return 'hello, I am ' + this.#privateSayHello();
}
static onlySayHello() {
return 'hello';
}
#privateSayHello() {
return this.#hello;
}
get name() {
return 'foo';
}
set name(newName) {
console.log('new name 为:' + newName);
}
}
// Output
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// 1. 会把设置的值变成不可枚举的,符合上文规范中要求的【类的内部所有定义的方法,都是不可枚举的】
// 2. 对于 getter setter,需要设置成不可写的,进来就是没有 value 字段
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
// 将实例方法添加到原型链上,将静态方法添加到构造函数上
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
// 以下三个为获取私有属性用
function _classPrivateFieldGet(receiver, privateMap) {
var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get");
return _classApplyDescriptorGet(receiver, descriptor);
}
// 保护调用,获得 weakMap 里通过构造函数绑定的方法
function _classExtractFieldDescriptor(receiver, privateMap, action) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to " + action + " private field on non-instance");
}
return privateMap.get(receiver);
}
// 调用方法,获得私有属性的值
function _classApplyDescriptorGet(receiver, descriptor) {
if (descriptor.get) {
return descriptor.get.call(receiver);
}
return descriptor.value;
}
// 调用私有方法用
// 调用时,检查是否有这个 weakmap 在实例上,有则执行对应方法
function _classPrivateMethodGet(receiver, privateSet, fn) {
if (!privateSet.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return fn;
}
var _hello = new WeakMap();
var _privateSayHello = new WeakSet();
var Person = /*#__PURE__*/function () {
function Person(name) {
_classCallCheck(this, Person);
_privateSayHello.add(this);
// 设置私有属性的值
_hello.set(this, {
writable: true,
value: 'hello'
});
this.name = name;
}
// 第一个参数是构造函数,二个参数是实例方法,第三个参数是静态方法
_createClass(Person, [{
key: "sayHello",
value: function sayHello() {
return 'hello, I am ' + _classPrivateMethodGet(this, _privateSayHello, _privateSayHello2).call(this);
}
}, {
key: "name",
get: function get() {
return 'foo';
},
set: function set(newName) {
console.log('new name 为:' + newName);
}
}], [{
key: "onlySayHello",
value: function onlySayHello() {
return 'hello';
}
}]);
return Person;
}();
function _privateSayHello2() {
return _classPrivateFieldGet(this, _hello);
}
复制代码
这里需要注意的几点
- 在设置方法时,会把设置的值变成不可枚举的,符合上文规范中要求的【类的内部所有定义的方法,都是不可枚举的】
- 对于
getter/setter
,是不可写的 - 私有方法也是靠
weakMap
实现,不过方法被定义到了构造函数外,通过构造函数和weakMap
链接,同时需要注意私有方法和私有变量取值不一样之处
Babel是如何让你通过 Class 继承的
上面我们看了一下 Class 中各种各样的属性、方法会被编译成什么样子,接下来我们看看是如何用他们实现继承的
只有构造函数
注意,在子类的 constructor
里要使用 this
的话,必须调用一次父类的构造函数,也就是 super()
(类似 ES5 中的 Parent.call(this)
)。这是因为子类没有自己的 this
对象,而是继承父类的 this
对象,然后对其进行加工。
// Input - 寄生组合式继承
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
const child1 = new Child('foo', '18');
// Output
"use strict";
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
// 重点,继承的核心
function _inherits(subClass, superClass) {
// extend 的继承目标必须是函数或者是 null
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
// 设置子类的 __proto__ 属性指向父类,这样能让子类访问到父类上的静态方法
if (superClass) _setPrototypeOf(subClass, superClass);
}
// 工具方法,设置一个对象的 __proto__
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
// 创建一个 super,来调用父元素构造函数,主要是处理其返回类型
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
// 获取原型,也就是Parent(在之前已经完成了继承)
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
// 有 Reflect 就走高端方案
// 作为新创建对象的原型对象的 constructor 属性
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
// 没有 Reflect 的环境就当成构造函数调用一下
result = Super.apply(this, arguments);
}
// 检查返回值
return _possibleConstructorReturn(this, result);
};
}
// 用于处理构造函数返回值——规范允许在构造函数内主动返回一个 对象、方法,否则则返回构造函数内的 this
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
// 工具方法,判断 this 是不是存在
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
// 工具方法,是否支持原生的 Reflect
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {}));
return true;
} catch (e) {
return false;
}
}
// 工具方法,返回指定对象的原型
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Parent = function Parent(name) {
_classCallCheck(this, Parent);
this.name = name;
};
var Child = /*#__PURE__*/function (_Parent) {
// 继承原型链
_inherits(Child, _Parent);
// 创建一个super
var _super = _createSuper(Child);
function Child(name, age) {
var _this;
_classCallCheck(this, Child);
// 调用 super,拿到 this
_this = _super.call(this, name);
_this.age = age;
return _this;
}
return Child;
}(Parent);
var child1 = new Child('foo', '18');
复制代码
这块最核心的其实就两个:如何处理 super
和如何处理继承:
- 如何处理继承
- 防御性处理,extend 的继承目标必须是函数或者是 null
- 继承的核心和 ES5 的寄生组合式继承类似,使用
create
完成 - ES6中基于 Class 的继承,子类能继承父类的静态方法,所以需要让子类的
__proto__
指向父类
// 重点,继承的核心
function _inherits(subClass, superClass) {
// extend 的继承目标必须是函数或者是 null
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
// 设置子类的 __proto__ 属性指向父类,这样能让子类访问到父类上的静态方法
if (superClass) _setPrototypeOf(subClass, superClass);
}
复制代码
- 如何处理
super
- 本质上,
super
类似于Parent.call(this)
- 需要注意的是额外处理了返回值
- 本质上,
// 创建一个 super,来调用父元素构造函数,主要是处理其返回类型
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
// 获取原型,也就是Parent(在之前已经完成了继承)
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
// 有 Reflect 就走高端方案
// 作为新创建对象的原型对象的 constructor 属性
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
// 没有 Reflect 的环境就当成构造函数调用一下
result = Super.apply(this, arguments);
}
// 检查返回值
return _possibleConstructorReturn(this, result);
};
}
// 用于处理构造函数返回值——规范允许在构造函数内主动返回一个 对象、方法,否则则返回构造函数内的 this
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
复制代码
继承自内建对象
// Input
class Child extends Array {
constructor(value) {
super(value);
}
}
const child1 = new Child(1);
// Output
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _wrapNativeSuper(Class) {
// 基于 Map 的缓存
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
// 保护,如果没有,或者不是原生的 function 则直接返回
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
// 用父类构造函数生成了新的实例
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
// 把父类的原型方法挂上去
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
// 修正 __proto__ 指向
return _setPrototypeOf(Wrapper, Class);
};
// 所以这里返回的是父类的一个实例
return _wrapNativeSuper(Class);
}
// 工具函数,Reflect.construct 的 polyfill
function _construct(Parent, args, Class) {
if (_isNativeReflectConstruct()) {
_construct = Reflect.construct;
} else {
_construct = function _construct(Parent, args, Class) {
var a = [null];
a.push.apply(a, args);
var Constructor = Function.bind.apply(Parent, a);
var instance = new Constructor();
if (Class) _setPrototypeOf(instance, Class.prototype);
return instance;
};
}
return _construct.apply(null, arguments);
}
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _isNativeFunction(fn) {
return Function.toString.call(fn).indexOf("[native code]") !== -1;
}
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
var Child = /*#__PURE__*/function (_Array) {
_inherits(Child, _Array);
var _super = _createSuper(Child);
function Child(value) {
_classCallCheck(this, Child);
return _super.call(this, value);
}
return Child;
}( /*#__PURE__*/_wrapNativeSuper(Array));
var child1 = new Child('foo');
复制代码
这里很好的解决了一个问题,你会发现在之前 ES5 的处理中,我们没有处理继承内建对象,比如:Array
,Date
等。这是因为之前都是调用父类构造函数并使用 call
改变 this
指向子类实例实现的(Parent.call(this, foo)
)。但是对于原生构造函数来说,会有几种情况:
- 一些会忽略
apply/call
方法传入的this
,也就是说原生构造函数this
无法绑定,导致子类实例拿不到内部属性。 - 一些在底层有限制,如
Date
,如果调用对象的[[Class]]
不是Date
,则抛出错误
而现在则通过包装,调用时会返回 new 父类出来的实例,从而借助其获得内建对象上的方法:
function _wrapNativeSuper(Class) {
// 基于 Map 的缓存
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
// 保护,如果没有,或者不是原生的 function 则直接返回
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
// 用父类构造函数生成了新的实例
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
// 把父类的原型方法挂上去
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
// 修正 __proto__ 指向
return _setPrototypeOf(Wrapper, Class);
};
// 所以这里返回的是父类的一个实例
return _wrapNativeSuper(Class);
}
复制代码
小结
本文从原型链讲起,主要针对 JavaScript 中基于原型链继承的原理,ES5中各种继承的优缺点,以及 Class 语法糖本身的样子 进行了一定介绍。主要还是偏向于基础知识层面,后续会结合设计模式、TypeScript 以及有关 polyfill 进行学习