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


Posted in Javascript onJanuary 19, 2019

本文全面讲述了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程序设计有所帮助。

Javascript 相关文章推荐
jquery 图片预加载 自动等比例缩放插件
Dec 25 Javascript
JavaScript 判断浏览器是否支持SVG的代码
Mar 21 Javascript
JS批量操作CSS属性详细解析
Dec 16 Javascript
重写document.write实现无阻塞加载js广告(补充)
Dec 12 Javascript
js控制div弹出层实现方法
May 11 Javascript
谈谈因Vue.js引发关于getter和setter的思考
Dec 02 Javascript
基于JavaScript实现飘落星星特效
Aug 10 Javascript
原生JavaScript实现Ajax异步请求
Nov 19 Javascript
让bootstrap的carousel支持滑动滚屏的实现代码
Nov 27 Javascript
浅谈小程序 setData学问多
Feb 20 Javascript
教你使用vue-cli快速构建的小说阅读器
May 13 Javascript
vue项目中锚点定位替代方式
Nov 13 Javascript
js嵌套的数组扁平化:将多维数组变成一维数组以及push()与concat()区别的讲解
Jan 19 #Javascript
js的各种数据类型判断的介绍
Jan 19 #Javascript
JavaScript实现与使用发布/订阅模式详解
Jan 19 #Javascript
Vuex中的State使用介绍
Jan 19 #Javascript
为什么要使用Vuex的介绍
Jan 19 #Javascript
Vue核心概念Getter的使用方法
Jan 18 #Javascript
Vue唯一可以更改vuex实例中state数据状态的属性对象Mutation的讲解
Jan 18 #Javascript
You might like
Linux安装配置php环境的方法
2016/01/14 PHP
thinkphp3.x中cookie方法的用法分析
2016/05/19 PHP
XML的代替者----JSON
2007/07/21 Javascript
jquery 屏蔽一个区域内的所有元素,禁止输入
2009/10/22 Javascript
jquery对dom的操作常用方法整理
2013/06/25 Javascript
网站繁简切换的JS遇到页面卡死的解决方法
2014/03/12 Javascript
浅谈JavaScript中的分支结构
2016/07/01 Javascript
浅谈JavaScript中的this指针和引用知识
2016/08/05 Javascript
jstree创建无限分级树的方法【基于ajax动态创建子节点】
2016/10/25 Javascript
JavaScript如何一次性展示几万条数据
2017/03/30 Javascript
Angular2安装angular-cli
2017/05/21 Javascript
AngularJS中filter的使用实例详解
2017/08/25 Javascript
vue单页面打包文件大?首次加载慢?nginx带你飞,从7.5M到1.3M蜕变过程(推荐)
2018/01/16 Javascript
解决Vue2.x父组件与子组件之间的双向绑定问题
2018/03/06 Javascript
vue-cli 3.x配置跨域代理的实现方法
2019/04/12 Javascript
Vue.js实现tab切换效果
2019/07/24 Javascript
[01:29:17]RNG vs Liquid 2019国际邀请赛淘汰赛 败者组 BO3 第二场 8.23
2019/09/05 DOTA
python网络编程之TCP通信实例和socketserver框架使用例子
2014/04/25 Python
介绍Python的Django框架中的静态资源管理器django-pipeline
2015/04/25 Python
Python 12306抢火车票脚本 Python京东抢手机脚本
2018/02/06 Python
Python实现的登录验证系统完整案例【基于搭建的MVC框架】
2019/04/12 Python
python线程安全及多进程多线程实现方法详解
2019/09/27 Python
Python @property及getter setter原理详解
2020/03/31 Python
六种酷炫Python运行进度条效果的实现代码
2020/07/17 Python
使用python把xmind转换成excel测试用例的实现代码
2020/10/12 Python
Flask处理Web表单的实现方法
2021/01/31 Python
图库照片、免版税图片、矢量艺术、视频片段:Depositphotos
2019/08/02 全球购物
大学生护理专业自荐信
2013/10/03 职场文书
行政办公员自我评价分享
2013/12/14 职场文书
无传销社区工作方案
2014/05/13 职场文书
反邪教警示教育方案
2014/05/13 职场文书
好媳妇事迹材料
2014/12/24 职场文书
幼儿园工作总结2015
2015/04/01 职场文书
无房证明样本
2015/06/17 职场文书
换届选举主持词
2015/07/03 职场文书
上个世纪50年代的可穿戴技术:无线电帽子
2022/02/18 无线电