资讯专栏INFORMATION COLUMN

深入 JavaScript 原型继承原理——babel 编译码解读

stdying / 1508人阅读

摘要:目录无继承简单的字段声明无继承简单的方法声明简单继承一层继承字段覆盖无继承静态函数无继承静态变量神秘的类无继承简单的字段声明先来看个最简单的例子,我们仅仅使用了关键字并定义了一个变量最后编译出来的代码如下。无继承静态变量还有个小例子。

在[上一篇文章][]中,我们提到 ES6 的 class 语法糖是个近乎完美的方案,并且讲解了实现继承的许多内部机制,如 prototype/__proto__/constructor 等等。这篇,我们就以实际的 babel 代码为例子,来验证上节所言不虚。此外,本文还解释了 React 组件中你需要 bind 一下类方法的原理所在。

目录

无继承——简单的 class + 字段声明

无继承——简单的 class + 方法声明

简单继承——一层继承 + 字段覆盖

无继承——静态函数

无继承——静态变量

神秘的类 arrow function

无继承——简单的 class + 字段声明

先来看个最简单的例子,我们仅仅使用了 class 关键字并定义了一个变量:

class Animal {
  constructor(name) {
    this.name = name || "Kat"
  }
}

最后 babel 编译出来的代码如下。这里笔者用的是 Babel 6 的稳定版 6.26,不同版本编译出来可能有差异,但不至于有大的结构变动。

"use strict"

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function")
  }
}

var Animal = function Animal(name) {
  _classCallCheck(this, Animal)

  this.name = name || "Kat"
}

确实十分简单,对吧。这段代码值得留意的点有两个:

一个是,使用 class 声明的 Animal 最后其实是被编译为一个函数。证明 class 跟类没关系,只是个语法糖。

另一个地方是,编译器帮我们插入了一个 _classCallCheck 函数调用,它会检查你有没有用 new Animal() 操作符来初始化这个函数。若有,则 this 会是被实例化的 Animal 对象,自然能通过 animal instanceof Animal 检查;若是直接调用函数,this 会被初始化为全局对象,自然不会是 Animal 实例,从而抛出运行时错误。这个检查,正解决了[上一篇文章][]提到的问题:如果忘记使用 new 去调用一个被设计构造函数的函数,没有任何运行时错误的毛病。

无继承——简单的 class + 方法声明

让我们再扩展一下例子,给它加两个方法。

class Animal {
  constructor(name) {
    this.name = name || "Kat"
  }

  move() {}
  getName() {
    return this.name
  }
}
"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) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps)
    if (staticProps) defineProperties(Constructor, staticProps)
    return Constructor
  }
})()

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function")
  }
}

var Animal = (function() {
  function Animal(name) {
    _classCallCheck(this, Animal)

    this.name = name || "Kat"
  }

  _createClass(Animal, [
    {
      key: "move",
      value: function move() {},
    },
    {
      key: "getName",
      value: function getName() {
        return this.name
      },
    },
  ])

  return Animal
})()

例子长了不少,但其实主要的变化只有两个:一是 Animal 被包了一层而不是直接返回;二是新增的方法 movegetName 是通过一个 _createClass() 方法来实现的。它将两个方法以 key/value 的形式作为数组传入,看起来,是要把它们设置到 Animal 的原型链上面,以便后续继承之用。

为啥 Animal 被包了一层呢,这是个好问题,但答案我们将留到后文揭晓。现在,我们先看一下这个长长的 _createClass 实现是什么:

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) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps)
    if (staticProps) defineProperties(Constructor, staticProps)
    return Constructor
  }
})()

它是个立即执行函数,执行又返回了另一个函数。说明啥,一定用了闭包,说明里面要封装些「私有」变量,那就是 defineProperties 这个函数。这很好,一是这个函数只会生成一次,二是明确了这个函数只与 _createClass 这个事情相关。

再细看这个返回的函数,接受 ConstructorprotoPropsstaticProps 三个参数。staticProps 我们暂时不会用到,回头再讲;我们传入的数组是通过 protoProps 接受的。接下来,看一下 defineProperties 做了啥事。

它将每一个传进来的 props 做了如下处理:分别设置了他们的 enumerableconfigurablewritable 属性。而传进来的 targetAnimal.prototype,相当于,这个函数最后的执行效果会是这样:

function defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    // 前面处理其实得到这样这个 descriptor 对象:
    var descriptor = {
      ...props[i],
      enumerable: false,
      configurable: true,
      writable: true,
    }
    Object.defineProperty(target, descriptor.key, descriptor)
  }
}

看到这里就很明白了,它就是把你定义的 movegetName 方法通过 Object.defineProperty 方法设置到 Animal.prototype 上去。前面我们说过,prototype 是用来存储公共属性的。也就是说,这两个方法在你使用继承的时候,可以被子对象通过原型链上溯访问到。也就是说,我们这个小小的例子里,声明的两个方法已经具备了继承能力了。

至于 enumerableconfigurablewritable 属性是什么东西呢,查一下语言规范就知道了。简单来说,writablefalse 时,其值不能通过 setter 改变;enumerablefalse 时,不能出现在 for-in 循环中。当然,这里是粗浅的理解,暂时不是这篇文章的重点。

简单继承——一层继承 + 字段覆盖
class Animal {
  constructor(name) {
    this.name = name || "Kat"
  }
}

class Tiger extends Animal {
  constructor(name, type) {
    super(name)
    this.type = type || "Paper"
  }
}

加一层继承和字段覆盖能看到啥东西呢?能看到继承底下的实现机制是怎么样的,以及它的 constructor__proto__ 属性将如何被正确设置。带着这两个问题,我们一起来看下编译后的源码:

"use strict"

function _possibleConstructorReturn(self, call) {
  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
}

function _inherits(subClass, superClass) {
  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: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  })
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass)
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function")
  }
}

var Animal = function Animal(name) {
  _classCallCheck(this, Animal)

  this.name = name || "Kat"
}

var Tiger = (function(_Animal) {
  _inherits(Tiger, _Animal)

  function Tiger(name, type) {
    _classCallCheck(this, Tiger)

    var _this = _possibleConstructorReturn(
      this,
      (Tiger.__proto__ || Object.getPrototypeOf(Tiger)).call(this, name)
    )

    _this.type = type || "Paper"
    return _this
  }

  return Tiger
})(Animal)

相比无继承的代码,这里主要增加了几个函数。_possibleConstructorReturn 顾名思义,可能不是很重要,回头再读。精华在 _inherits(Tiger, Animal) 这个函数,我们按顺序来读一下。

function _inherits(subClass, superClass) {
  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: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  })
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass)
}

首先是一段异常处理,简单地检查了 superClass 要么是个函数,要么得是个 null。也就是说,如果你这样写那是不行的:

const Something = "not-a-function"
class Animal extends Something {}
// Error: Super expression must either be null or a function, not string

接下来这句代码将 prototypeconstructor 一并设置到位,是精华。注意,这个地方留个问题:为什么要用 Object.create(superClass.prototype),而不是直接这么写:

function _inherits(subClass, superClass) {
  subClass.prototype = superClass && superClass.prototype
  subClass.prototype.constructor = { ... }
}

很明显,是为了避免任何对 subClass.prototype 的修改影响到 superClass.prototype。使用 Object.create(asPrototype) 出来的对象,其实上是将 subClass.prototype.__proto__ = superClass.prototype,这样 subClass 也就继承了 superClass,可以达到这样两个目的:

superClass.prototype 原型上发生的修改都能实时反映到 subClass 的实例上

subClass.prototype 上的任何修改不会影响到 superClass.prototype

最后,如果 superClass 不为空,那么将 subClass.__proto__ 设置为 superClass。这是为了继承 superClass 的静态方法和属性。如以下的例子中,Cat.TYPE 能获取到 Animal.TYPE

class Animal {
  static TYPE = "PAPER"
  static createTyping() {
    return Animal.TYPE
  }
}

class Cat extends Animal {}

console.log(Cat.TYPE)           // PAPER
console.log(Cat.createTyping()) // PAPER

至此,一个简单的继承就完成了。在使用了 extends 关键字后,实际上背后发生的事情是:

子「类」prototype 上的 __proto__ 被正确设置,指向父「类」的 prototype: subClass.prototype = { __proto__: superClass.prototype }

子「类」prototype 上的 constructor 被正确初始化,这样 instanceof 关系能得到正确结果

子「类」的 __proto__ 被指向父「类」,这样父「类」上的静态字段和方法能被子「类」继承

好,要点看完了。后面内容跟继承关系不大,但既然源码扒都扒了,我们不妨继续深入探索一些场景:

无继承——静态函数

看一个简单的代码:

class Animal {
  static create() {
    return new Animal()
  }
}

首先要知道,这个「静态」同样不是强类型类继承语言里有的「静态」的概念。所谓静态,就是说它跟实例是没关系的,而跟「类」本身有关系。比如,你可以这样调用:Animal.create(),但不能这样用:new Animal().create。什么场景下会用到这种模式呢?比如说:

工厂模式或单例模式

Object.createObject.keys 等常用方法

既然只有通过构造函数本身去调用,而不能通过实例来调用,期望它们被绑定到函数本身上似乎很自然。我们来看看上面这段代码将被如何编译:

"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) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps)
    if (staticProps) defineProperties(Constructor, staticProps)
    return Constructor
  }
})()

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function")
  }
}

var Animal = (function() {
  function Animal() {
    _classCallCheck(this, Animal)
  }

  _createClass(Animal, null, [
    {
      key: "create",
      value: function create() {},
    },
  ])

  return Animal
})()

熟悉的函数,熟悉的配方。与本文的第二个例子相比,仅有一个地方的不同:create 方法是作为 _createClass 方法的第三个参数被传入的,这正是我们上文提到的 staticProps 参数:

var _createClass = (function() {
  function defineProperties(target, props) { ... }

  return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps)
    if (staticProps) defineProperties(Constructor, staticProps)
    return Constructor
  }
})()

_createClass(Animal, null, [
  {
    key: "create",
    value: function create() {},
  },
])

可以看见,create 方法是直接被创建到 Animal 上的:defineProperties(Animal, [{ key: "create", value: function() {} }]),最终会将函数赋给 Animal.create。我们的猜测并没有错误。

无继承——静态变量
class Tiger {
  static TYPE = "REAL"
}

还有个小例子。如果是静态变量的话,同样因为不希望在实例对象上所使用,我们会看到编译出来的代码中它是直接被设置到函数上。代码已经很熟悉,不必再讲。

"use strict"

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function")
  }
}

var Tiger = function Tiger() {
  _classCallCheck(this, Tiger)
}

Tiger.TYPE = "REAL"

有趣的是,静态变量会不会被「子类」继承呢?这个可请读者自己做个实验,验证验证。

神秘的类 arrow function

写 React 的东西,一定遇见过这个问题:

class Button extends React.Component {
  constructor() {
    super()
    this.state = {
      isToggleOn: true,
    }
    // 画重点            
               
                                           
                       
                 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98784.html

相关文章

  • JavaScript是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换

    摘要:下面是用实现转成抽象语法树如下还支持继承以下是转换结果最终的结果还是代码,其中包含库中的一些函数。可以使用新的易于使用的类定义,但是它仍然会创建构造函数和分配原型。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 15 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是...

    PrototypeZ 评论0 收藏0
  • 原理解释 - 收藏集 - 掘金

    摘要:巧前端基础进阶全方位解读前端掘金我们在学习的过程中,由于对一些概念理解得不是很清楚,但是又想要通过一些方式把它记下来,于是就很容易草率的给这些概念定下一些方便自己记忆的有偏差的结论。 计算机程序的思维逻辑 (83) - 并发总结 - 掘金从65节到82节,我们用了18篇文章讨论并发,本节进行简要总结。 多线程开发有两个核心问题,一个是竞争,另一个是协作。竞争会出现线程安全问题,所以,本...

    AlphaGooo 评论0 收藏0
  • 原理解释 - 收藏集 - 掘金

    摘要:巧前端基础进阶全方位解读前端掘金我们在学习的过程中,由于对一些概念理解得不是很清楚,但是又想要通过一些方式把它记下来,于是就很容易草率的给这些概念定下一些方便自己记忆的有偏差的结论。 计算机程序的思维逻辑 (83) - 并发总结 - 掘金从65节到82节,我们用了18篇文章讨论并发,本节进行简要总结。 多线程开发有两个核心问题,一个是竞争,另一个是协作。竞争会出现线程安全问题,所以,本...

    forrest23 评论0 收藏0
  • 揭秘babel的魔法之class魔法处理

    摘要:年,很多人已经开始接触环境,并且早已经用在了生产当中。我们发现,关键字会被编译成构造函数,于是我们便可以通过来实现实例的生成。下一篇文章我会继续介绍如何处理子类的并会通过一段函数桥梁,使得环境下也能够继承定义的。 2017年,很多人已经开始接触ES6环境,并且早已经用在了生产当中。我们知道ES6在大部分浏览器还是跑不通的,因此我们使用了伟大的Babel来进行编译。很多人可能没有关心过,...

    wqj97 评论0 收藏0
  • JavaScript 工作原理之十五-类和继承Babel 和 TypeScript 代码转换探秘

    摘要:使用新的易用的类定义,归根结底也是要创建构造函数和修改原型。首先,它把构造函数当成单独的函数且包含类属性集。该节点还储存了指向父类的指针引用,该父类也并储存了构造函数,属性集和及父类引用,依次类推。 原文请查阅这里,略有删减,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原理的第...

    GeekGhc 评论0 收藏0

发表评论

0条评论

stdying

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<