当前位置 : 主页 > 网络编程 > JavaScript >

15分钟深入了解JS继承分类、原理与用法

来源:互联网 收集:自由互联 发布时间:2021-06-05
本文全面讲述了JS继承分类、原理与用法。分享给大家供大家参考,具体如下: 许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实

本文全面讲述了JS继承分类、原理与用法。分享给大家供大家参考,具体如下:

许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于 ECMAScript 中的函数没有签名,所以在 JS 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。所以,下面所要说的原型链继承借用构造函数继承组合继承原型式继承寄生式继承寄生组合式继承都属于实现继承。

最后的最后,我会解释 ES6 中的 extend 语法利用的是寄生组合式继承。

1. 原型链继承

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现原型链继承有一种基本模式,其代码大致如下:

function SuperType(){
  this.property = true;
}
SuperType.prototype.getSuperValue = function(){
  return this.property;
};
function SubType(){
  this.subproperty = false;
}
SubType.prototype = new SuperType();    // 敲黑板!这是重点:继承了 SuperType
SubType.prototype.getSubValue = function (){
  return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());    // true

原型链继承的一个本质是重写原型对象,代之以一个新类型的实例;给原型添加方法的代码一定要放在替换原型的语句之后;在通过原型链实现继承时,不能使用对象字面量创建原型方法。

实例属性在实例化后,会挂载在实例对象下面,因此称之为实例属性。上面的代码中 SubType.prototype = new SuperType(); ,执行完这条语句后,原 SuperType 的实例属性 property 就挂载在了 SubType.prototype 对象下面。这其实是个隐患,具体原因后面会讲到。

每次去查找属性或方法的时候,在找不到属性或方法的情况下,搜索过程总是要一环一环的前行到原型链末端才会停下来。

所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。由此可知,所有函数的默认原型都是 object 的实例,因此函数的默认原型都会包含一个内部指针,指向 Object.prototype 。

缺点:

  1. 最主要的问题来自包含引用类型值的原型。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
  2. 在创建子类型的实例时,不能向超类型的构造函数传递参数。

* 题外话:确定原型与实例的关系的两种方式

  1. 第一种方式是使用 instanceOf 操作符,只要用这个操作符来测试实例的原型链中是否出现过某构造函数。如果有,则就会返回 true ;如果无,则就会返回 false 。以下为示例代码:
    alert(instance instanceof Object);   //true
    alert(instance instanceof SuperType);  //true
    alert(instance instanceof SubType);   //true
    
    
  1. 第二种方式是使用 isPrototypeOf() 方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生出来的实例的原型。以下为示例代码:
alert(Object.prototype.isPrototypeOf(instance));    //true
alert(SuperType.prototype.isPrototypeOf(instance));   //true
alert(SubType.prototype.isPrototypeOf(instance));    //true

2. 借用构造函数继承

借用构造函数继承,也叫伪造对象或经典继承。其基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。其继承代码大致如下:

function SuperType(){
  this.colors = [ "red", "blue", "green"];
}
function SubType(){
  SuperType.call(this);    // 敲黑板!注意了这里继承了 SuperType
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    // "red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors);    // "red,blue,green"

通过使用 call() 方法(或 apply() 方法也可以),我们实际上是在(未来将要)新创建的子类的实例环境下调用父类构造函数。

为了确保超类构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

优点:可以在子类型构造函数中向超类型构造函数传递参数。

缺点:

  1. 方法都在构造函数中定义,每次实例化,都是新创建一个方法对象,因此函数根本做不到复用;
  2. 使用这种模式定义自定义类型,超类型的原型中定义的方法,对子类型而言是不可见。

3. 组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。其继承代码大致如下:

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};
function SubType(name, age){
  SuperType.call(this, name);   // 继承属性
  this.age = age;         // 先继承,后定义新的自定义属性
}
SubType.prototype = new SuperType();    // 继承方法
Object.defineProperty( SubType.prototype, "constructor", {   // 先继承,后定义新的自定义属性
  enumerable: false,   // 申明该数据属性——constructor不可枚举
  value: SubType
});
SubType.prototype.sayAge = function(){   // 先继承,后定义新的自定义方法
  alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors);    // "red, blue, green, black"
instance1.sayName();      // "Nicholas"
instance1.sayAge();       // 29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors);    // "red, blue, green"
instance2.sayName();      // "Greg";
instance2.sayAge();       // 27

优点:

  1. 融合了原型链继承和借用构造函数继承的优点,避免了他们的缺陷;
  2. instanceOf()isPrototypeOf() 也能够用于识别基于组合继承创建的对象。

缺点:

在实现继承的时候,无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型的原型最终会包含超类型对象的全部实例属性,但我们不得不在定义子类型构造函数时重写这些属性,因为子类型的原型中最好不要有引用类型值。但这在实际中,就造成了内存的浪费。

4. 原型式继承

原型式继承所秉承的思想是:在不必创建自定义类型的情况下,借助原型链,基于已有的对象创建新对象。这其中会用到 Object.create() 方法,让我们先来看看该方法的原理代码吧:

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}

从本质上讲,object() 对传入其中的对象执行了一次浅复制。

ECMAScript 5 想通过 Object.create() 方法规范化原型式继承。这个方法接受两个参数:一参是被用来作为新对象原型的一个对象;二参为可选,一个为新对象定义额外属性的对象,这个参数的格式与 Object.defineProperties() 的二参格式相同。以下为原型式继承的示例代码:

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
  name: {
    value: "Greg"
  }
});
anotherPerson.friends.push("Rob");
alert(anotherPerson.name);   //"Greg"
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

缺点:所有实例始终都会共享源对象中的引用类型属性值。

5. 寄生式继承

寄生式(parasitic)继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。下面来看看,寄生式继承的示例代码:

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function createAnother(original){
  var clone = object(original);  // 通过调用函数创建一个新对象
  clone.sayHi = function(){    // 以某种方式来增强这个对象
    alert("hi");
  };
  return clone;          // 返回这个对象
}

该继承方式其实就是将原型式继承放入函数内,并在其内部增强对象,再返回而已。就相当于原型式继承寄生于函数中,故而得名寄生式继承。

前面示范继承模式时使用的 object() 函数不是必需的;任何能够返回新对象的函数都适用于此模式。

缺点:不能做到函数复用,效率低下。

6. 寄生组合式继承(推荐)

寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。以下为寄生组合式继承的实例代码:

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function inheritPrototype(subType, superType){
  var prototype = object(superType.prototype);    //创建对象
  prototype.constructor = subType;          //增强对象
  subType.prototype = prototype;           //指定对象
}
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};
function SubType(name, age){
  SuperType.call(this, name);     // 继承属性
  this.age = age;
}
inheritPrototype(SubType, SuperType);    // 继承原型方法
SubType.prototype.sayAge = function(){
  alert(this.age);
};

优点:

  1. 只调用一次超类型构造函数;
  2. 避免了在子类原型上创建不必要的、多余的属性,节省内存空间;
  3. 原型链还能正常保持不变,也就意味着能正常使用 instanceOf 和 isPrototypeOf() 进行对象识别。

寄生组合式继承是最理想的继承方式。

7. ES6 中的 extend 继承

来看看 ES6 中 extend 如何实现继承的示例代码:这一块的内容解释,我阅读的是这篇文章,欲知原文,请戳这里~

class Child extends Parent{
  name ='qinliang';
  sex = "male";
  static hobby = "pingpong";   //static variable
  constructor(location){
    super(location);
  }
  sayHello (name){
    super.sayHello(name);    //super调用父类方法
  }
}

我们再来看看 babel 编译过后的代码中的 _inherit() 方法:

function _inherits(subClass, superClass) {
  //SuperClass必须是一个函数,同时非null
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
  }
  subClass.prototype = Object.create(   // 寄生组合式继承
    superClass && superClass.prototype,   //原型上的方法、属性全部被继承过来了
    {
      constructor: {   // 并且定义了新属性,这里是重写了constructor属性
        value: subClass,
        enumerable: false,   // 并实现了该属性的不可枚举
        writable: true,
        configurable: true
      }
    }
  );
  if (superClass)   // 实现类中静态变量的继承
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

从这里我们就可以很明显的看出 ES6 中的 extend 语法,在内部实现继承时,使用的是寄生组合式继承。

下面我们来看看编译过后,除了 _inherit() 方法外的其他编译结果代码:

"use strict";
var _createClass = function () {    // 利用原型模式创建自定义类型
  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);
    }
  }
  return function (Constructor, protoProps, staticProps) {
    // 设置Constructor的原型属性到prototype中
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    // 设置Constructor的static类型属性
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();
var _get = function get(object, property, receiver) {  // 调用子类的方法之前会先调用父类的方法
  // 默认从Function.prototype中获取方法
  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 {    // 继续获取父类原型中指定的方法
      return get(parent, property, receiver);
    }
  } else if ("value" in desc) {
    return desc.value;   // 返回获取到的值
  } else {
    var getter = desc.get;   // 获取原型的getter方法
    if (getter === undefined) {
      return undefined;
    }
    return getter.call(receiver);    // 接着调用getter方法,并传入this对象
  }
};
function _classCallCheck(instance, Constructor) {    // 保证了我们的实例对象是特定的类型
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
// 在子类的构造函数中调用父类的构造函数
function _possibleConstructorReturn(self, call) {    // 一参为子类的this,二参为父类的构造函数
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
var Child = function (_Parent) {
  _inherits(Child, _Parent);
  function Child(location) {   // static variable
    _classCallCheck(this, Child);    // 检测this指向问题
    // 调用父类的构造函数,并传入子类调用时候的参数,生成父类的this或者子类自己的this
    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, location));
    _this.name = 'qinliang';
    _this.sex = "male";
    return _this;
  }
  _createClass(Child, [{   //更新Child类型的原型
    key: "sayHello",
    value: function sayHello(name) {
      // super调用父类方法,将调用子类的super.sayHello时候传入的参数传到父类中
      _get(Child.prototype.__proto__ || Object.getPrototypeOf(Child.prototype), "sayHello", this).call(this, name);
    }
  }]);
  return Child;
}(Parent);
Child.hobby = "pingpong";

从我的注释中就可以看出 _possibleConstructorReturn() 函数,其实就是寄生组合式继承中唯一一次调用超类型构造函数,从而对子类型构造函数进行实例化环境的初始化。从这点,我们可以更加确定的 ES6 中的 extend 使用的是寄生组合式继承。

更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

网友评论