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

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

(0)

相关推荐

  • JavaScript是如何实现继承的(六种方式)

    前言:大多OO语言都支持两种继承方式: 接口继承和实现继承 ,而ECMAScript中无法实现接口继承,ECMAScript只支持实现继承,而且其实现继承主要是依靠 原型链 来实现. 1.原型链 基本思想:利用原型让一个引用类型继承另外一个引用类型的属性和方法. 构造函数,原型,实例之间的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针. 原型链实现继承例子: function SuperType() { this.property

  • 实例介绍JavaScript中多种组合继承

    1. 组合继承:又叫伪经典继承,是指将原型链和借用构造函数技术组合在一块的一种继承方式. 下面来看一个例子: function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { alert(this.name); } function SubType(nam

  • 彻底理解js面向对象之继承

    说道这个继承,了解object-oriented的朋友都知道,大多oo语言都有两种,一种是接口继承(只继承方法签名):一种是实现继承(继承实际的方法) 奈何js中没有签名,因而只有实现继承,而且靠的是原型链实现的.下面正式的说一说js中继承那点事儿 1.原型链 原型链:实现继承的主要方法,利用原型让一个引用类型继承另一个引用类型的属性和方法. 回顾:构造函数,原型,实例三者的关系 每一个构造函数都有一个原型对象(Person.prototype):原型对象都包含指向构造函数的指针(constru

  • JavaScript寄生组合式继承原理与用法分析

    本文实例讲述了JavaScript寄生组合式继承.分享给大家供大家参考,具体如下: 寄生组合式继承 寄生组合式继承,就是通过伪造对象来继承属性,通过原型链的混成形式来继承方法. 这种技术的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,因为我们所需要的只是超类型原型的一个副本而已(这一点正是组合继承的问题,并且正是JavaScript这种原型式语言的强大之处,直接copy对象-原型对象).本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型. function

  • 利用JS实现文字的聚合动画效果

    前言 所谓文字聚合效果,原理就是将容器分为若干的小块,然后每个小块设置 background-poisition ,最后添加css3动画就可以了,去掉注释,也就仅仅20行的代码. 先来看看效果图: js代码如下: //c为列数,r为行数,把box划分成多少个小块 var box = document.querySelector('.boxWrap1'),c=4,r=8; //每个小块的宽高 var w = box.offsetWidth/c,h = box.offsetHeight/r; //循

  • JavaScript常见继承模式实例小结

    本文实例总结了JavaScript常见继承模式.分享给大家供大家参考,具体如下: JavaScript中并没有传统的面向对象语言中的类的概念,但是却实现了特殊的继承机制. (阅读此文您首先需要知道原型的知识) 先来说说第一种继承方式,原型链继承. 一. 原型链继承 所谓原型链继承,就是让父类的一个实例作为子类的原型. 即 : parentInstance = new Parent(); child.prototype = parentInstance; 这样,在创建子类的实例时,子类实例的__p

  • js中继承的几种用法总结(apply,call,prototype)

    一,js中对象继承 js中有三种继承方式 1.js原型(prototype)实现继承 复制代码 代码如下: <SPAN style="BACKGROUND-COLOR: #ffffff"><SPAN style="FONT-SIZE: 18px"><html>  <body>  <script type="text/javascript">      function Person(na

  • JavaScript继承与聚合实例详解

    本文实例讲述了JavaScript继承与聚合.分享给大家供大家参考,具体如下: 一.继承 第一种方式:类与被继承类直接耦合度高 1. 首先,准备一个可以被继承的类(父类),例如 //创建一个人员类 function Person(name) {//现在Person里面的域是由Person里面的 来this来控制的 this.name=name; } 2. 然后,有个需要继承父类的子类 function Teacher(name,books) { Person.call(this,name);//

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

    本文全面讲述了JS继承分类.原理与用法.分享给大家供大家参考,具体如下: 许多 OO 语言都支持两种继承方式:接口继承和实现继承.接口继承只继承方法签名,而实现继承则继承实际的方法.由于 ECMAScript 中的函数没有签名,所以在 JS 中无法实现接口继承.ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的.所以,下面所要说的原型链继承.借用构造函数继承.组合继承.原型式继承.寄生式继承和寄生组合式继承都属于实现继承. 最后的最后,我会解释 ES6 中的 exten

  • JS回调函数原理与用法详解【附PHP回调函数】

    本文实例讲述了JS回调函数原理与用法.分享给大家供大家参考,具体如下: JS回调函数 何为回调函数,官方解释:当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数.但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务.这个被传入的.后又被调用的函数就称为回调函数(callback function). 通常将一个函数B传入另一个函数A,并且在需要的时候再调用函数A. 说白

  • Node.js assert断言原理与用法分析

    本文实例讲述了Node.js assert断言原理与用法.分享给大家供大家参考,具体如下: node.js官方API中文版 http://nodeapi.ucdok.com/#/api/assert.html assert 模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误. class : assert - assert.fail(actual, expected, message, operator) - assert(value, message), assert.ok

  • js回调函数原理与用法案例分析

    本文实例讲述了js回调函数原理与用法.分享给大家供大家参考,具体如下: 回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数.回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应. 函数指针是指向函数的指针变量. 因此"函数指针"本身首先应是指针变量,只不过该指针变量指向函数. 函数指针有两个用途:调用函数和做函数的参数. 作用:

  • JS严格模式原理与用法实例分析

    本文实例讲述了JS严格模式原理与用法.分享给大家供大家参考,具体如下: 使用 "use strict" 指令 "use strict" 指令在 JavaScript 1.8.5 (ECMAScript5) 中新增. 它不是一条语句,但是是一个字面量表达式,在 JavaScript 旧版本中会被忽略. 支持严格模式的浏览器: Internet Explorer 10 +. Firefox  4+ Chrome  13+. Safari 5.1+. Opera 12+.

  • Vue js with语句原理及用法解析

    vue源码中编译部分有下面一段代码,里面用到了with: export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(th

  • JS变量提升原理与用法实例浅析

    本文实例讲述了JS变量提升.分享给大家供大家参考,具体如下: 该篇介绍什么是变量提升,写给像我一样的JS新手看的 简单来说变量提升就是 JS会把var变量的声明自动提升到作用域的顶部,即使你不想这样 一个例子: (局部变量与全局变量同名时 , 局部变量覆盖全局变量) var a="全局变量"; function test() { document.writeln(a); var a="局部变量"; document.writeln(a); } test(); 上例的

  • 手把手15分钟搭一个企业级脚手架

    1 写在前面的话 搭一个脚手架,考验了你的 nodejs 水平.工程化能力.以及工具服务的设计能力,是前端进阶不可或缺的过程 笔者在开发 cli 的过程中,调研流行的 cli 并形成最佳实践,本文旨在用最短的篇幅实现主要功能,揭露核心原理,同时提供 demo 仓库与大家学习探讨. 通篇阅读大约需要 10 分钟,基于本教程自己撸一个 cli 大约需要花费 15 分钟 2 脚手架的雏形 其实脚手架的初衷,就是提供一个最佳实践的基础模板,因此模板拷贝是其核心功能 几年前我曾写过一个极简的脚手架,大该干

  • 15 分钟掌握vue-next函数式api(小结)

    写在前面 在分享 vue-next 各个子模块的实现之前,我觉的有必要比较全面的整理下 vue-next 中提出的函数式 api,了解这些的话,无论是对于源码的阅读,还是当正式版发布时开始学习,应该都会有起到一定的辅助作用. 类似的东西在网上有很多,只是会比较零碎,同时有些也相对过时了,当然当前整理的这些也有可能会过时,毕竟代码还处于 pre-alpha 的阶段,但其中的设计思想应该是不会改变了. 往期文章 15 分钟掌握 vue-next 响应式原理 vue-next/runtime-core

  • 15分钟学会vue项目改造成SSR(小白教程)

    15分钟学会vue项目改造成SSR Ps:网上看了好多服务器渲染的例子,基本都是从0开始的,用Nuxt或者vue官网推荐的ssr方案(vue-server-renderer),但是我们在开发过程中基本上是已经有了现有的项目了,我们所要做的是对现有项目的SSR改造.那么这里,跟我一起对一个vue-cil2.0生成的项目进行SSR改造 关于这篇文章的案例源代码我放在我的github上面,有兴趣的同学,也可以去我的github查看我之前写的博客.博客 一.改造技术的分析对比. 一般来说,我们做seo有

随机推荐