JavaScript继承者的游戏

850 阅读10分钟

背景

春天到了,又到了交配的季节。随着湿润季节的来临,干涸的大地上,下起了瓢泼大雨,万物开始躁动。

前端切图仔车大棒童鞋也开始新的一年Flag,虽然去年flag历历在目。但依旧无法阻挡他在新的一年当中,继续树立新的flag,简直太真实了,我怀疑我受到了flag法则的监视。

又因为今年难得假期在家的时光比较长,所以在家的时候就是思考如何打败flag法则。要不先从小flag做起,比如先尝试回顾和整理自己相关知识点,获得成就感。

所以今年划水第一篇, 先从JavaScript继承讲起。

关于JavaScript的继承与原型链,无论是各种面试场还是各种blog上面都是讲烂的话题。这时候又翻出来扯淡,头也太铁了吧。

-- 做人嘛! 最重要是开(头)心(铁)就好。

JavaScript中为什么需要继承?

提到继承估计大家马上就想到的prototype 、 构造函数 和extends等实现继承,但是在开头我并不会讲这些,而是发出来问号二连用来思考。

JavaScript为什么需要继承? 继承能够帮助JavaScript解决什么问题?

带着思考,我们开始第一个demo

 // 创建⼀个构造⽅法
  function Dog() {
    this.sayHello = function () {
      console.log("炸鸡汉堡快乐水!");
    };
  }
  // 创建两个对象
  var dog1 = new Dog();
  var dog2 = new Dog();
  // 调⽤和⽐较
  dog1.sayHello();  // 输出: 炸鸡汉堡快乐水!
  dog2.sayHello();  // 输出:  炸鸡汉堡快乐水!
  console.log(dog1.sayHello === dog2.sayHello); // 输出:false

通过demo结果可以看出,两只dog打招呼输出的都是: "炸鸡汉堡快乐水". 但是最后的两者sayHello方法比较的时候,却出现了false。

说明两个对象的sayHello方法是两个独立的方法,各自在内存中占据了一份空间.

夭寿啦!单身狗为何不报团取暖,不是说好一起相亲相爱吗!不是说好一起共享种子,一起开黑吗?月黑风高秋名山一起开车吗!

所以为了避免原本每天遭受狗粮侵蚀单身汪继续团结一致,这个时候首先让我们借用第一个属性prototype来帮助稳定dog群内团结的作用。

(后文会具体解释)

  function Dog() {}
  Dog.prototype.sayHello = function () {
    console.log("炸鸡汉堡快乐水!");
  };
  // 创建两个对象
  var dog1 = new Dog();
  var dog2 = new Dog();
  // 调⽤和⽐较
  dog1.sayHello(); // 输出: 炸鸡汉堡快乐水
  dog2.sayHello(); // 输出: 炸鸡汉堡快乐水
  console.log(dog1.sayHello === dog2.sayHello); // true

通过抱团的demo可以看出,此时dog已经相亲相爱共有一个sayhello方法。

说明两个对象sayHello方法是同一个,在内存中只占据了一份空间。

此刻内存中大概图示如下:

利用prototype可以保存那些共享的数据和⽅法,实现数据复用以及减少内存占用,这也是为什么我们需要继承的原因.

原型链

众所周知JavaScript创作者吸收了百家编程语言特点,14天匆忙拼凑出来的产物. 如借鉴C语言完成基本语法,借鉴Scheme函数提升到"第一等公民(first class)"等其他语言。而继承方面则是借鉴的Self语言,基于原型(prototype)的继承机制.

原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法.

-- 摘选自《JavaScript高级程序第三版》

上文中的示例代码就是简单通过prototype完成了简单的继承, 那么接下来通过一个三代继承关系,来解释一下原型链。

    function Kars() {
        this.value = "头疼阳光"
    }
    Kars.prototype.getKarsValue = function () {
        return this.value;
    }
    function Dio() {
        this.subValue = '头疼JOJO'
    }
    Dio.prototype = new Kars();
    Dio.prototype.getDioValue = function () {
        return this.subValue;
    }
    var Ice = new Dio();
    console.log(Ice.getKarsValue())   // 头疼阳光

首先在JavaScript世界中KarsDIO开辟了两道空间, 存放自己的属性和方法。

两者区别在于DIO继承了Kars,而继承通过创建kars的实例(Kar创造了石像鬼面具), 并将实例赋给在Dio.prototype实现的。

确定了继承关系之后,之后Dio又在自己prototype添加了一个方法。这样继承Dio的实例对象(Dio鲜血改造),也会继承dio传达的意志:

头疼JOJO. 并把JOJO作为目标。

这个例子中的实例以及构造函数和原型之间的关系如图:

通过这个图可以看出Ice本身是没有getKarValue这个属性方法的,但是他会沿着原型链一步一步向上走。

小贴士:

  • 所有引用类型默认都继承Object,而继承也是通过原型链实现的.
  • 此图仅供参考,更加详细原型链说明例如[[protype]]protypeconstructor请移步《JavaScript高级程序第三版》

原型链的弊端

前面讲了原型链的优点,现在要给原型链泼冷水了。明人不说暗话,这回直接先上demo3

  function SuperMenu() {
    this.foods = ["香辣鸡腿堡", "老北京鸡肉卷", "香辣鸡翅"];
  }
  function SubMenu() {}
  SubMenu.prototype = new SuperMenu();
  var order1 = new SubMenu();
  
  // order1口渴需要添加一瓶快乐水
  order1.foods.push("快乐水");
  console.log(order1.foods); 
  // 输出: 【"香辣鸡腿堡", "老北京鸡肉卷", "香辣鸡翅""快乐水"】
  
  //  order2正常套餐
  var order2 = new SubMenu();
  console.log(order2.foods); 
  // 输出: 【"香辣鸡腿堡", "老北京鸡肉卷", "香辣鸡翅", "快乐水"

按照原本我预想,我的两份订单里面。一份是有快乐水,一份是没有快乐水的,这里却是出现两份一样订单。

这一切的背后原因,得从js运行机制说起。这里灵魂画师车大棒再次出手了:

因为数组是引用类型,导致代码当中引用类型值的原型属性会被所有实例共享。 修改order1.food, 也会导致order2.food也会跟着一起变化。

借用构造函数

虽说引用类型的值在原型中会导致共享一类的问题,但是古语有云:

只要思想不滑坡,方法总比问题多。
--《来自于劳动人民的智慧》

开发人员借用构造函数 (constructor stealing) 的技术, 别称伪造对象经典继承

接下来展示借助构造函数解决共享问题的demo4:

 function SuperMenu() {
    this.foods = ["香辣鸡腿堡", "老北京鸡肉卷", "香辣鸡翅"];
  }
  function SubMenu() {
    // 继承SuperMenu
    SuperMenu.call(this);
  }

  // order1口渴需要添加一瓶快乐水
  var order1 = new SubMenu();
  order1.foods.push("快乐水");
  console.log(order1.foods);
  // 输出: 【"香辣鸡腿堡", "老北京鸡肉卷", "香辣鸡翅", "快乐水"】

  //  order2正常套餐
  var order2 = new SubMenu();
  console.log(order2.foods);
  // 输出: 【"香辣鸡腿堡", "老北京鸡肉卷", "香辣鸡翅"

多种组合模式继承

原型链继承和构造函数都是各有优点,也各有相应的弊端。所以在大家很少单独使用原型链或构造函数函数来实现继承,更多是将两者结合起来使用。

目前常用的组合模式有:

  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

这里也就不一一罗列出来了,感兴趣童鞋也可以移步到《JavaScript高级程序第三版》。毕竟我要快乐游戏了! 这里我就拿日常开发使用最多的,也最推崇的【寄生组合继承】。

  /**
   * @method extend
   * @param {function} r the object to modify.
   * @param {function} s the object to inherit.
   */
  function Extend(r, s) {
    var sp = s.prototype;
    var rp = Object.create(sp);
    rp.constructor = r;
    r.prototype = rp;
  }
  function KFC(food) {
    this.food = food;
  }
  KFC.prototype.getFood = function () {
    console.log(this.food)
  }

  function Mcdonalds(food, newFood) {
    KFC.call(this, food);
    this.newFood = newFood;
  }

  Extend(Mcdonalds, KFC);

  Mcdonalds.prototype.getNewFood = function () {
      console.log(this.newFood)
  }
  
  var dog = new Mcdonalds("老北京鸡肉卷", "5G炸鸡");
  dog.getFood(); // "老北京鸡肉卷"
  dog.getNewFood();  // "5G炸鸡"

以上就是通过YUI库源码学习进行简单完成的寄生组合继承小例子,感兴趣的童鞋可以移步

YUI中关于寄生组合的源码地址

这里就懒画相应的图例

《JavaScript高级程序设计》中对寄生组合式继承的评价是这样的:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

ES6语法糖extends继承

伴随着jQuery、YUI、underscore/lodash、Backbone 等框架和库中都提供了类似扩展( extend )的功能,从而实现JavaScript继承。

因此在 ES6 中,新增加了 extends 的语法糖,在定义子类的时候可以直接继承父类,这样统一了继承的方式,让大家不再被各种各样的继承方式困扰。

  class KFC {
    constructor(food) {
      this.food = food;
    }
    getFood() {
      console.log(this.food);
    }
  }

  class Mcdonalds extends KFC {
    constructor(food, newFood) {
      super(food);
      this.newFood = newFood;
    }
    getNewFood() {
      console.log(this.newFood);
    }
  }

  const dog = new Mcdonalds("老北京鸡肉卷", "5G炸鸡");
  dog.getFood(); // "老北京鸡肉卷"
  dog.getNewFood(); // "5G炸鸡"

看到有的童鞋可能通过对比可以,发现ES6的extends与前面寄生组合继承中Extend都能够完成了同样的一件事情。 那么问题来了,ES6语法糖extends是否和前面寄生组合继承中Extend代码结构一样呢? --放可达鸭发现事情不妙的表情

bable编译下的extends

事实胜于雄辩,为了验证猜想是否正确。 这里我们借用babel 官网推荐的在线编译工具 传送门,通过对比验证转换前的代码,来验证我们的猜想是否正确。

在线babel转换之后的完整代码:

(想看提取之后extends代码片段,可以跳过此节直接往下翻。)

"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _createSuper(Derived) { return function () { var Super = _getPrototypeOf(Derived), result; if (_isNativeReflectConstruct()) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }

function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }

function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }

var KFC = /*#__PURE__*/function () {
  function KFC(food) {
    this.food = food;
  }

  var _proto = KFC.prototype;

  _proto.getFood = function getFood() {
    console.log(this.food);
  };

  return KFC;
}();

var Mcdonalds = /*#__PURE__*/function (_KFC) {
  _inheritsLoose(Mcdonalds, _KFC);

  var _super = _createSuper(Mcdonalds);

  function Mcdonalds(food, newFood) {
    var _this;

    _this = _KFC.call(this, food) || this;
    _this.newFood = newFood;
    return _this;
  }

  var _proto2 = Mcdonalds.prototype;

  _proto2.getNewFood = function getNewFood() {
    console.log(this.newFood);
  };

  return Mcdonalds;
}(KFC);

var dog = new Mcdonalds("老北京鸡肉卷", "5G炸鸡");
dog.getFood(); // "老北京鸡肉卷"

dog.getNewFood(); // "5G炸鸡"

提取babel转换之后extends的代码片段:

babel转换之后的代码很长一届,其中还有关于class的转换。为了方便阅读与验证,我们提取和整理extends相关的代码片段。

  [......]
  function _inheritsLoose(subClass, superClass) {
    subClass.prototype = Object.create(superClass.prototype);
    subClass.prototype.constructor = subClass;
    subClass.__proto__ = superClass;
  }
  var Mcdonalds = /*#__PURE__*/ (function (_KFC) {
    _inheritsLoose(Mcdonalds, _KFC);

    var _super = _createSuper(Mcdonalds);

    function Mcdonalds(food, newFood) {
      var _this;

      _this = _KFC.call(this, food) || this;
      _this.newFood = newFood;
      return _this;
    }

    var _proto2 = Mcdonalds.prototype;

    _proto2.getNewFood = function getNewFood() {
      console.log(this.newFood);
    };

    return Mcdonalds;
  })(KFC);

通过提取的extends的代码可以看出,其工作原理与寄生组合继承的工作原理几乎一致。

两者比较明显的区别一行代码: subClass.__proto__ = superClass; es6中extends会将置subClass 的 [[Prototype]] 指向 superClass, 通俗点就是说,假设某个子类想要继承其父类的属性,extends除了会走一遍寄生组合继承的流程之外。 还会将子类的 [[Prototype]] (__proto__) 指向到到其父类上面

感兴趣童靴这里可以好好研究一下,这个知识点面试题经常考到😃

小结

新年flag第一篇终于开张了,累的一匹。不说了,我要开心的去买快乐肥宅套餐,冲冲!

我是车大棒! 我的flag已经开张了

你们flag的开张了嘛?

参考资料

《JavaScript高级程序设计设计(第三版)》
《ECMAScript 6 入门教程》--阮一峰
《web前端修炼指南》 --sh22n