前言
本文首发于笔者的个人博客,欢迎围观。
如果在阅读过程发现文章有表达不当的地方,希望各位不吝指教。
本文章将从【实现继承的方式 / ES5的继承实现】以及【ES6的继承实现】两方面分析继承。
实现继承的方式 / ES5的继承实现
ES5 的继承方式可以大致分为一下六种:原型链继承、盗用构造函数继承、组合继承;原型式继承、寄生式继承、寄生组合式继承。
原型链继承
分析
回顾 原型&原型链 中构造函数、原型、实例间的关系,我们知道:构造函数的 prototype 属性以及实例的 __proto__ 属性指向原型对象,原型对象的 constructor 属性指向构造函数。
当一个函数的原型对象成为另一个函数的实例对象时,eg.Child.prototype = new Parent(),说明这个原型存在 __proto__ 属性指向另一个原型,而这另一个原型本身也存在 constructor 属性指向其构造函数。这就在两个函数之间生成了一条原型链。
instance.__proto__ => Child.prototype
Child.prototype.__proto__ => Parent.prototype
Parent.prototype.constructor => Parent()
function Parent() {
this.ParentProperty = true
}
Parent.prototype.getParentProp = function() {
return this.ParentProperty
}
function Child() {
this.ChildProperty = false
}
// 实现继承:原型是另一个类型的实例
Child.prototype = new Parent()
const instance = new Child()
console.log(instance.getParentProp()) //true
关键要点
Child.prototype = new Parent()
这样的实现,不同的Child实例的__proto__会引用同一Parent的实例。
缺点
- 原型中包含的引用值会在所有实例间共享
- 创建
Child实例时,不能向Parent()传参
function Parent() {
this.example = ['aki', 'enjoy']
}
function Child() {}
Child.prototype = new Parent()
let instance1 = new Child()
instance1.example.push('coding')
let instance2 = new Child()
console.log(instance2.example) // ['aki', 'enjoy', 'coding']
盗用构造函数继承
这种继承的出现是为了解决上述原型链继承的缺点。
分析
因为函数就是在特定上下文中的简单对象,所以可以使用 apply() 和 call() 以新创建的对象,为上下文执行构造函数。
function Parent() {
this.example = ['aki', 'enjoy']
}
function Child() {
// 实现继承:以新创建的对象为上下文执行构造函数
Parent.call(this)
}
let instance1 = new Child()
instance1.example.push('coding')
let instance2 = new Child()
console.log(instance2.example) // ['aki', 'enjoy']
关键要点
function Child(args) {
//...
Parent.call(this, args)
}
优缺点
优点是解决了原型链继承带来的问题。
引用原型中的属性值不再被所有实例共享,并且可以在子类构造函数中向父类构造函数传参。
function Parent(name) {
this.name = name
}
function Child() {
// 继承并传参
Parent.call(this, 'aki')
this.age = 20
}
let instance = new Child()
console.log(instance.name) // 'aki'
console.log(instance.age) // 20
缺点:必须在构造函数中定义方法。这也就造成了盗用构造函数继承的缺点——每次继承时都会创建一次方法,因此函数无法重用。
只实现了实例属性继承,Parent 原型的方法在 Child 实例中并不可用。
组合继承
分析
组合继承结合了原型链继承以及盗用构造函数继承的优点:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。
function Parent(name) {
this.name = name
this.example = ['moon', 'and']
}
Parent.prototype.sayName() = function() {
console.log(this.name)
}
function Child(name, age) {
// 盗用构造函数继承实例属性
Parent.call(this, name)
this.age = age
}
// 原型链继承方法
Child.prototype = new Parent()
let instance1 = new Child('aki', 20)
instance1.example.push('sunrise')
// instance1各类属性
console.log(instance1.name) // 'aki'
console.log(instance1.age) // 20
console.log(instance1.example) // ['moon', 'and', 'sunrise']
instance1.sayName() // 'aki'
let instance2 = new Child('moon', 25)
console.log(instance2.example) // ['moon', 'and']
关键要点
function Child(args1, args2) {
//...
this.args2 = args2
Parent.call(this, args1)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
优缺点
融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
它的问题在于 Child 实例会存在 Parent 的实例属性,同时 Child.__proto__ 也会存在相同的Parent实例属性,且所有的 Child 实例的 __proto__ 指向同一内存地址。
原型式继承
分析
本质是 ES6 的 Object.create 的模拟实现。
该继承方式是为了实现“即使不自定义函数也能够通过原型实现对象之间的信息共享”。
function objCreate(o) {
fucntion F() {}
F.prototype = o
return new F()
}
objCreate()函数中新建了一个临时构造函数F,将该临时构造函数的原型指向传入的o,最后返回F的实例。
本质上,objCreate() 是对传入的对象进行一次浅拷贝。
let person = {
name: 'aki',
eg: ['moon', 'and']
}
let person1 = objCreate(person)
person1.name = 'person1'
person1.eg.push('sunrise')
let person2 = objCreate(person)
person2.eg.push('kk')
console.log(person2.name) // 'aki'
console.log(person.eg) // ['moon', 'and', 'sunrise', 'kk']
在该例子中,实际上克隆了两个person。
注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值。
缺点
原型式继承会产生和原型链继承一样的问题:包含引用类型的属性值始终都会共享相应的值。
寄生式继承
分析
创建一个实现继承的函数,以某种方式增强对象,并返回这个对象。
function createObj (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
缺点
该方法的缺点和盗用构造函数相同:每次创建对象都会创建一遍方法,函数无法复用。
寄生式组合继承
分析
我们先来回顾一下组合继承:
function Parent(name) {
this.name = name
this.example = ['moon', 'and']
}
Parent.prototype.sayName() = function() {
console.log(this.name)
}
function Child(name, age) {
Parent.call(this, name) // 第一次调用Parent()
this.age = age
}
Child.prototype = new Parent() // 第二次调用Parent()
let instance1 = new Child('aki', 20)
从上面的注释可以看到,子类每进行一次实例化,Parent()始终会被调用两次,造成了效率问题。
为了避免重复调用父类构造函数,不使用 Child.prototype = new Parent(),而是间接地让
Child.prototype 访问到 Parent.prototype。
function inherit(Child, Parent) {
let prototype = objCreate(Parent.prototype) // 创建对象
prototype.constructor = Child // 增强对象(重写原型默认丢失constructor)
Child.prototype = prototype // 赋值对象
}
function Parent(name) {
this.name = name
this.example = ['moon', 'and']
}
Parent.prototype.sayName() = function() {
console.log(this.name)
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
inherit(Child, Parent)
let instance1 = new Child('aki', 20)
优点
这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
ES6 继承实现
ES6 class
随着class的推出,可以使用直接使用 extends关键字。
用法
// 包含一个实例属性的父类
class Parent {
constructor() {
this.type = 'person'
}
}
// 子类继承
class Student extends Person {
constructor() {
super()
}
}
var student1 = new Student()
student1.type // "person"
student1 instanceof Student // true
student1 instanceof Person // true
student1.hasOwnProperty('type') // true
原理剖析
经过Babel编译后,我们的代码发生了以下变化
// 编译前
class Parent {
constructor() {
this.type = 'person'
}
}
================================
// 编译后
var Person = function Person() {
_classCallCheck(this, Person)
this.type = 'person'
}
// 编译前
class Student extends Person {
constructor(){
super()
}
}
==================================================================
// 编译后
var Student = (function(_Person) {
_inherits(Student, _Person);
// 将会返回这个函数作为完整的 Student 构造函数
function Student() {
// 使用检测
_classCallCheck(this, Student);
// _get 的返回值可以先理解为父类构造函数
_get(Object.getPrototypeOf(Student.prototype), 'constructor', this).call(this);
}
return Student;
})(Person);
// _x为Student.prototype.__proto__
// _x2为'constructor'
// _x3为this
var _get = function get(_x, _x2, _x3) {
var _again = true;
_function: while (_again) {
var object = _x,
property = _x2,
receiver = _x3;
_again = false;
// Student.prototype.__proto__为null的处理
if (object === null) object = Function.prototype;
// 以下是为了完整复制父类原型链上的属性,包括属性特性的描述符
var desc = Object.getOwnPropertyDescriptor(object, property);
if (desc === undefined) {
var parent = Object.getPrototypeOf(object);
if (parent === null) {
return undefined;
} else {
_x = parent;
_x2 = property;
_x3 = receiver;
_again = true;
desc = parent = undefined;
continue _function;
}
} else if ('value' in desc) {
return desc.value;
} else {
var getter = desc.get;
if (getter === undefined) {
return undefined;
}
return getter.call(receiver);
}
}
};
function _inherits(subClass, superClass) {
// superClass 需要为函数类型,否则会报错
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass);
}
// Object.create 第二个参数是为了修复子类的 constructor
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
// Object.setPrototypeOf 是否存在做了一个判断,否则使用 __proto__
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
我们对子类继承进行拆分分析:
var Student = (function(_Person) {
_inherits(Student, _Person);
function Student() {
_classCallCheck(this, Student);
_get(Object.getPrototypeOf(Student.prototype), 'constructor', this).call(this);
}
return Student;
})(Person);
定义了 Student 构造函数,它是一个自执行函数,接受父类构造函数为参数。
在该函数中,实现对父类原型链属性的继承。
上面 _inherits 方法的本质其实就是让 Student 子类继承 Person 父类原型链上的方法。它的实现原理可以归结为一句话:
Student.prototype = Object.create(Person.prototype);
Object.setPrototypeOf(Student, Person)
是不是这就非常熟悉了。注意,Object.create 接收了第二个参数,这顺带实现了对 Student 的 constructor 修复。
以上通过 _inherits 实现了对父类原型链上属性的继承,那么对于父类的实例属性(就是 constructor 定义的属性)的继承,也可以归结为一句话:
Person.call(this);
我们看到 Babel 将 class extends 编译成了 ES5 组合模式的继承,这才是 JavaScript 面向对象的实质。
参阅
书籍
《JavaScript高级程序设计》