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 相关文章推荐
js 提交和设置表单的值
Dec 19 Javascript
JavaScript 判断浏览器类型及版本
Feb 21 Javascript
iframe 异步加载技术及性能分析
Jul 19 Javascript
基于JQuery实现鼠标点击文本框显示隐藏提示文本
Feb 23 Javascript
js字符串引用的两种方式(必看)
Sep 18 Javascript
AngularJS实现DOM元素的显示与隐藏功能
Nov 22 Javascript
利用VUE框架,实现列表分页功能示例代码
Jan 12 Javascript
详解使用angular框架离线你的应用(pwa指南)
Jan 31 Javascript
node express使用HTML模板的方法示例
Aug 22 Javascript
layui table数据修改的回显方法
Sep 04 Javascript
vue项目强制清除页面缓存的例子
Nov 06 Javascript
在vue项目中引用Antv G2,以饼图为例讲解
Oct 28 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
优化NFR之一 --MSSQL Hello Buffer Overflow
2006/10/09 PHP
javascript,php获取函数参数对象的代码
2011/02/03 PHP
php在线解压ZIP文件的方法
2014/12/30 PHP
smarty内置函数section的用法
2015/01/22 PHP
PHP使用zlib扩展实现GZIP压缩输出的方法详解
2018/04/09 PHP
不错的一个日期输入 动态
2006/11/06 Javascript
jQuery温习篇 强大的JQuery选择器
2010/04/24 Javascript
一个基于jquery的文本框记数器
2012/09/19 Javascript
jQuery判断checkbox(复选框)是否被选中以及全选、反选实现代码
2014/02/21 Javascript
jquery基础教程之数组使用详解
2014/03/10 Javascript
常用的几段javascript代码分享
2014/03/25 Javascript
JQuery的ON()方法支持的所有事件罗列
2015/02/28 Javascript
JS+CSS实现简单滑动门(滑动菜单)效果
2015/09/19 Javascript
浅谈Node.js:Buffer模块
2016/12/05 Javascript
荐书|您有一份JavaScript书单待签收
2017/07/21 Javascript
使用NestJS开发Node.js应用的方法
2018/12/03 Javascript
javascript设计模式 ? 单例模式原理与应用实例分析
2020/04/09 Javascript
详解Node.JS模块 process
2020/08/31 Javascript
python使用PyFetion来发送短信的例子
2014/04/22 Python
Python实现抓取百度搜索结果页的网站标题信息
2015/01/22 Python
Python函数中*args和**kwargs来传递变长参数的用法
2016/01/26 Python
Python实现购物车功能的方法分析
2017/11/10 Python
Python使用pip安装报错:is not a supported wheel on this platform的解决方法
2018/01/23 Python
Python元组拆包和具名元组解析实例详解
2018/03/26 Python
使用实现pandas读取csv文件指定的前几行
2018/04/20 Python
Python使用Pandas库实现MySQL数据库的读写
2019/07/06 Python
Python如何调用外部系统命令
2019/08/07 Python
快速解决jupyter启动卡死的问题
2020/04/10 Python
Python库安装速度过慢解决方案
2020/07/14 Python
Web前端绘制0.5像素的几种方法
2017/08/11 HTML / CSS
CSS3 选择器 属性选择器介绍
2012/01/21 HTML / CSS
美国知名的家庭连锁百货商店:Boscov’s
2017/07/27 全球购物
民主评议党员自我评价材料
2014/09/18 职场文书
北京离婚协议书范文2014
2014/09/29 职场文书
四风问题专项整治工作情况报告
2014/10/28 职场文书
React自定义hook的方法
2022/06/25 Javascript