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 相关文章推荐
JavaScript中的Screen屏幕对象
Jan 16 Javascript
JSON 和 JavaScript eval使用说明
Jun 13 Javascript
jQuery Ajax方法调用 Asp.Net WebService 的详细实例代码
Apr 27 Javascript
JavaScript 匿名函数(anonymous function)与闭包(closure)
Oct 04 Javascript
js日期相关函数总结分享
Oct 15 Javascript
Js调用Java方法并互相传参的简单实例
Aug 11 Javascript
jquery实现ajax提交表单信息的简单方法(推荐)
Aug 24 Javascript
AngularJS执行流程详解
Feb 17 Javascript
JavaScript中立即执行函数实例详解
Nov 04 Javascript
create-react-app构建项目慢的解决方法
Mar 14 Javascript
使用jquery的cookie实现登录页记住用户名和密码的方法
Mar 13 jQuery
Angular8路由守卫原理和使用方法
Aug 29 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
PHP5中的时间相差8小时的解决办法
2008/03/28 PHP
php cookie 登录验证示例代码
2009/03/16 PHP
解析php函数method_exists()与is_callable()的区别
2013/06/21 PHP
叫你如何修改Nginx与PHP的文件上传大小限制
2014/09/10 PHP
php执行多个存储过程的方法【基于thinkPHP】
2016/11/08 PHP
thinkphp关于简单的权限判定方法
2017/04/03 PHP
Mac下php 5升级到php 7的步骤详解
2017/04/26 PHP
Laravel5.4框架中视图共享数据的方法详解
2019/09/05 PHP
在JavaScript中使用inline函数的问题
2007/03/08 Javascript
jQuery入门介绍之基础知识
2015/01/13 Javascript
javascript实现汉字转拼音代码分享
2015/04/20 Javascript
js基于cookie记录来宾姓名的方法
2016/07/19 Javascript
Javascript OOP之面向对象
2016/07/31 Javascript
vue 2.0组件与v-model详解
2017/03/27 Javascript
AngularJS中table表格基本操作示例
2017/10/10 Javascript
浅谈React Native Flexbox布局(小结)
2018/01/08 Javascript
ajax前台后台跨域请求处理方式
2018/02/08 Javascript
vue 的keep-alive缓存功能的实现
2018/03/22 Javascript
一次Webpack配置文件的分离实战记录
2018/11/30 Javascript
vue router动态路由设置参数可选问题
2019/08/21 Javascript
layui之数据表格--与后台交互获取数据的方法
2019/09/29 Javascript
微信小程序如何实现radio单选框单击打勾和取消
2020/01/21 Javascript
js实现简单贪吃蛇游戏
2020/05/15 Javascript
使用Vue-scroller页面input框不能触发滑动的问题及解决方法
2020/08/08 Javascript
用Python写的图片蜘蛛人代码
2012/08/27 Python
Python程序设计入门(2)变量类型简介
2014/06/16 Python
Python中使用gzip模块压缩文件的简单教程
2015/04/08 Python
Python cookbook(数据结构与算法)从任意长度的可迭代对象中分解元素操作示例
2018/02/13 Python
关于Pycharm无法debug问题的总结
2019/01/19 Python
pandas实现DataFrame显示最大行列,不省略显示实例
2019/12/26 Python
基于 HTML5 的 WebGL 3D 版俄罗斯方块的示例代码
2018/05/28 HTML / CSS
沃尔玛加拿大:Walmart.ca
2020/03/02 全球购物
团员学习总结的自我评价范文
2013/10/14 职场文书
土地租赁协议书
2015/01/29 职场文书
2015年全国爱耳日活动总结
2015/02/27 职场文书
任命书怎么写
2015/03/02 职场文书