js之类继承

前言

对于科班出身的同窗来说,绝大多数应该是从 过程化编程 起步,这种风格的代码之包含了过程(函数)调用,没有对基层进行抽象(面条式代码)。javascript

后来咱们开始接触到面向对象编程,进而又跟另一个被称为 的术语扯上了关系,或者能够说是面向类式编程。前端

在后来随着编程的深刻咱们开始接触到 函数式编程,这也是一种编程的选择或者习惯。java

但咱们这次只讨论一下关于类的那些事儿。说到类,咱们便会想起三大基本特性 封装继承多态,而咱们这次的主题即是 继承编程

JavaScript 中的 “类”

相较于传统语言,JavaScript 中一直在模仿类行为。直到 ES6 版本出来后才出现了一些近似类的功能,如 classextendssuper。可是这并不表明 JavaScript 实现了像传统语言同样的类,JavaScript 的核心机制是[[prototype]],而且只有对象,对象只负责定义自身的行为。像这些新定义的语法只是在原型链的基础上进行的封装(语法糖),所以搞懂[[prototype]] 才是关键。app

说了一些体外话,而后进入主题。咱们先来抛出两个小问题:函数式编程

  • 如何实现继承?
  • 继承都有那些方法,它们的利弊都是什么?

接下来咱们一个个的讲。函数

类式继承

先看一下例子:性能

function Animal() {
  this.categories = ['二哈', '英短', '龙猫'];
}

Animal.prototype.category = function() {
    console.log(this.categories);
}

var animal = function () {}
animal.prototype = new Animal();

var animal1 = new animal();
animal1.category();   // ['二哈', '英短', '龙猫'];

这是最基本的类式继承,经过使用父级的构造函数调用来为 animal.prototype 进行关联。咱们先来讲一下使用构造函数调用(new)时会自动执行的一些状况:this

  1. 建立(构造)一个新对象。
  2. 这个新对象会被执行[[Prototype]]关联,也就是说这个对新象会关联到animal.prototype对象上。
  3. 这个新对象会绑定到函数调用的this,也就是说此时的this指的是 animal
  4. 若是函数未返回其它对象,那么使用 new 关键字调用函数后会返回这个新对象(也就是 animal{})。

关于this 更细致的讨论能够参见js之thisprototype

因此当咱们执行 animal1.category() 操做的时候,由于 [[Get]] 操做的默认行为会检查原型链,animal1自身没有 categories 属性因此会到自身原型链查找,因为new Animal()操做返回的对象与Animal.prototype 自动关联而且animal.prototype 还保存着 Animal.prototype 引用,所以animal1 即可以顺利的访问到Animal 原型链及自身的属性。

咱们再来看一下例子:

function Animal() {
  this.categories = ['二哈', '英短', '龙猫'];
}

var animal = function () {}
animal.prototype = new Animal();
var animal1 = new animal();
var animal2 = new animal();

console.log( animal1.categories); // ["二哈", "英短", "龙猫"]
animal1.categories.push('柯基');
console.log( animal2.categories); // ["二哈", "英短", "龙猫", '柯基']

经过这个例子就能够很明显的看出使用类式继承的问题:

  • 若是父级的构造函数(使用new调用)里存在经过this 添加引用类型对象,当这个对象被更改时,全部子级都会受到牵连。
  • 由于是使用了父级的构造函数调用,子级对象就没法实例化本身的属性。

针对于这些问题咱们引出了另一种继承。

构造函数继承

function Animal(name) {
    this.name = name;
    this.features = ['装傻卖萌', '好吃懒作'];
}

Animal.prototype.sleep = function() {
    console.log(this.name + '正在睡觉');
}

function Dog(name, voice) {
    Animal.call(this, name);
    this.voice = voice;
}

var dog1 = new Dog('二哈', '汪汪。。');
var dog2 = new Dog('柯基', '汪汪。。');

dog1.features.push('拆家小分队');
console.log(dog1.features);   // ["装傻卖萌", "好吃懒作", "拆家小分队"]
console.log(dog2.features);   // ["装傻卖萌", "好吃懒作"]
console.log(dog1.sleep());    // TypeError: sleep is not a function

前面咱们有讲到经过构造函数调用的时候发生的状况,因为未执行原型链的关联,因此当执行完构造函数调用以后自动将 this 关联到 Dog 并为其添加属性。这段代码的核心是 Animal.call(this, name) ,这里经过显示绑定将 Animal 中的属性从新添加到 Dog 对象中。

提醒: Animal.callAnimal.apply 用法相同,都会更改当前执行上下文环境的this,这种方式称为 this显示绑定。还有一种被称为应绑定的方法: bind ,一样会更改执行上下文环境的this,但 bind 会返回执行函数的一个副本。

那既然这两种都不能实现一个完整的继承过程,咱们能够结合一下这两种思想,使用构造函数将父级的公有属性与子级的公有属性进行合并,同时要将父级原型链上属性也进行合并(注意子级自已的公有属性要后执行)。注意:不要直接执行父级的构造函数调用,由于使用 call 已经执行了调用了构造函数。再使用 new 操做至关于执行了两遍重复的操做。

原型式继承

最先提出的这一方式的是美国的道格拉斯·克罗克福德(Douglas Crockford),世界著名的前端大师,同时也是JSON 的创立者。他提出的这个方案:

function inheritObject(proto) {
    function F() {};
    F.prototype = proto;
    return new F();
}

这段代码使用了一个一次性函数,经过改写它的 .prototy 将它指向想要关联的对象,而后再使用 new 操做构造一个新对象进行关联。

function Animal(name) {
    this.name = name;
}

Animal.prototype.sleep = function() {
    console.log(this.name + '正在睡觉。');
}

function Dog(name, voice) {
    Animal.call(this, name);
    this.voice = voice;
}

Dog.prototype = inheritObject(Animal.prototype);

Dog.prototype.yell = function() {
    console.log(this.name + ': ' +  this.voice);
}

var dog = new Dog('二哈', '汪汪。。');

dog.sleep();   // 二哈正在睡觉。 
dog.yell();    // 二哈: 汪汪。。

须要注意一点:通过 inheritObject 后已经没有 Dog.prototype.constructor 属性了,由于Dog.prototype 指向的是 Animal.prototype ,因此若是还须要这个属性,须要手动修复它:

Dog.prototype.constructor = Dog

寄生组合式继承

所以便出现了更加理想的继承方式:

function inheritPrototype(subClass, superClass) {
    var f = inheritObject(superClass.prototype);
    f.constructor = subClass;
    subClass.prototype = f;
}

function Animal(name) {
    this.name = name;
}

Animal.prototype.sleep = function() {
    console.log(this.name + '正在睡觉。');
}

function Dog(name, voice) {
    Animal.call(this, name);
    this.voice = voice;
}
// 不考虑 construstor 指向的时候:
// Dog.prototype = inheritObject(Animal.prototype); 
// 或者 Dog.prototype = Object.create(Animal.prototype)

// 考虑 construstor 指向的时候:
// 使用Object.create后手动修复construstor: Dog.prototype.constructor = Animal
inheritPrototype(Dog, Animal);
// 或者 Object.setPrototypeOf(Dog.prototype, Animal.prototype)

Dog.prototype.yell = function() {
    console.log(this.name + '饿了: ' +  this.voice);
}

var dog = new Dog('二哈', '汪汪。。');

dog.sleep();   // 二哈正在睡觉。 
dog.yell();    // 二哈饿了: 汪汪。。

随着这种方式的深刻,后来ES5便出现了 Object.create 这个方法,固然这个方法内部还有不少附加功能,可是核心倒是如此。可是这样会致使 constructor 指向错误,进而咱们引出了 inheritPrototype() 方法修复其 constructor 指向的问题。一样,ES6以后出现了Object.setPrototypeOf(subProto, superProto) ,这个方法实际上跟咱们本身写的 inheritPrototype() 是相似的。

若是不考虑 constructor 指向错误问题及轻微性能损失(被丢弃F对象会在适当时机被GC回收掉),使用 Object.create是彻底没问题的。

此外,Obeject.create 会建立一个拥有空原型连的对象,这个对象没有原型链,没法进行进行委托。这种特殊的空对象特别适合作为字典结构来存储数据。所以,该对象没法使用 instanceof 关键字,而且在使用for..in遍历对象的时候,使用 Object.prototype.hasOwnProperty.call 来避免类型错误。

多继承

对于多继承来说,咱们能够换个思路。咱们刚刚将到,单一方式最完善的继承是寄生组合式,其实多继承彻底能够照这个思路将多个类的公有属性经过 call 或着 apply 的从新绑定功能将属性拷贝到自身,而对于原型链上的属性,则可使用原型继承(须要将其它类的原型进行混入)。

function SuperClass() {
    this.name = "SuperClass"
}

SuperClass.prototype.superMethod = function () {
    // ..
}

function OtherSuperClass() {
    this.otherName = "OtherSuperClass"
}

OtherSuperClass.prototype.otherSuperMethod = function () {
    // ..
}

function MyClass() {
    SuperClass.call(this);
    OtherSuperClass.call(this);
}

// 混入原型对象

Object.setPrototypeOf(SuperClass.prototype, OtherSuperClass.prototype)
Object.setPrototypeOf(MyClass.prototype, SuperClass.prototype)

MyClass.prototype.myMethod = function() {
    console.log('myMethod')
};

var myClass = new MyClass();

console.log(myClass);  // MyClass {name: "SuperClass", otherName: "OtherSuperClass"}
相关文章
相关标签/搜索