JavaScript | 继承

455 阅读7分钟

前言

本文首发于笔者的个人博客,欢迎围观。

如果在阅读过程发现文章有表达不当的地方,希望各位不吝指教。

本文章将从【实现继承的方式 / 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的实例。

缺点

  1. 原型中包含的引用值会在所有实例间共享
  2. 创建 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的值并未发生改变,并不是因为person1person2有独立的 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高级程序设计》

文章

深入JavaScript | 冴羽

前端开发核心知识进阶 | LucasHC