从 Prototype 开始说起(下)—— ES6 中的 class 与 extends

何为 class

众所周知,JavaScript是没有类的,class也只是语法糖,这篇文章旨在于理清我们常常挂着嘴边的语法糖,究竟指的是什么。

ES6ES5 写法对比

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    get thought() {
        console.log('Thought in head is translate to Chinese.')
        return this._thought
    }
    
    set thought(newVal) {
        this._thought = newVal
    }
    
    constructor(name) {
        this.name = name
    }
    
    static live() {
        console.log('live')
    }

    talk() {
        console.log('talk')
    }
}

这是一个很完整的写法,我们已经习惯于这么方便地写出一个类了,那么对应到 ES5 中的写法又是如何呢

function Parent(name) {
    this.name = name
    this.isAdult = true
}

Parent.nation = 'China'
Parent.live = function() {
    console.log('live')
}
Parent.prototype = {
    get thought() {
        return this._thought
    },
    set thought(newVal) {
        this._thought = newVal
    },
    talk: function() {
        console.log('talk')
    }
}

可以很清晰地看到

  • ES6Parent 类的 constructor 对应的就是 ES5 中的构造函数 Parent
  • 实例属性 nameisAdult,无论在 ES6 中采用何种写法,在 ES5 中依然都是挂在 this 下;
  • ES6 中通过关键字 static 修饰的静态属性和方法 nationlive,则都被直接挂在类 Parent 上;
  • 值得注意的是 getter 和 setter tought 和 方法 talk 是被挂在 原型对象 Parent.prototype 上的。

Babel 是如何进行编译的

我们可以通过将代码输入到 Babel 官网的 Try it out 来查看编译后的代码,这个部分我们循序渐进,一步一步来进行编译,拆解 Babel 的编译过程:

过程一

我们此时只观察 属性 相关的编译结果,
编译前:

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    constructor(name) {
        this.name = name
    }
}

编译后:

'use strict'
  // 封装后的 instanceof 操作
  function _instanceof(left, right) {
    if (
      right != null &&
      typeof Symbol !== 'undefined' &&
      right[Symbol.hasInstance]
    ) {
      return !!right[Symbol.hasInstance](left)
    } else {
      return left instanceof right
    }
  }
  // ES6 的 class,必须使用 new 操作来调用,
  // 这个方法的作用就是检查是否通过 new 操作调用,使用到了上面封装的 _instanceof 方法
  function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
      throw new TypeError('Cannot call a class as a function')
    }
  }
  // 封装后的 Object.defineProperty
  function _defineProperty(obj, key, value) {
    if (key in obj) {
      Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
      })
    } else {
      obj[key] = value
    }
    return obj
  }

  var Parent = function Parent(name) {
    // 检查是否通过 new 操作调用
    _classCallCheck(this, Parent)
    // 初始化 isAdult
    _defineProperty(this, 'isAdult', true)
    // 根据入参初始化 name
    this.name = name
  }
  // 初始化静态属性 nation
  _defineProperty(Parent, 'nation', 'China')

从编译后的代码中可以发现,Babel 为了其严谨度,封装了一些方法,其中 可能有点迷惑的是 _instanceof(left, right) 这个方法里的 Symbol.hasInsance,从 MDNECMAScript6入门 中可以知道,这个属性可以用来自定义 instanceof 操作符在某个类上的行为。这里还有一个重点关注对象 _classCallCheck(instance, Constructor) ,这个方法用来检查是否通过 new 操作调用。

过程二

编译前:

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    get thought() {
        console.log('Thought in head is translate to Chinese.')
        return this._thought
    }
    
    set thought(newVal) {
        this._thought = newVal
    }
    
    constructor(name) {
        this.name = name
    }
    
    static live() {
        console.log('live')
    }

    talk() {
        console.log('talk')
    }
}

编译后:

 'use strict'
  // 封装后的 instanceof 操作
  function _instanceof(left, right) {
    // .....
  }
  // ES6 的 class,必须使用 new 操作来调用,
  // 这个方法的作用就是检查是否通过 new 操作调用,使用到了上面封装的 _instanceof 方法
  function _classCallCheck(instance, Constructor) {
    // ......
  }
  // 封装 Object.defineProperty 来添加属性
  function _defineProperties(target, props) {
    // 遍历 props
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i]
      // enumerable 默认为 false
      descriptor.enumerable = descriptor.enumerable || false
      descriptor.configurable = true
      if ('value' in descriptor) descriptor.writable = true
      Object.defineProperty(target, descriptor.key, descriptor)
    }
  }
  // 为 Constructor 添加原型属性或者静态属性并返回
  function _createClass(Constructor, protoProps, staticProps) {
    // 如果是原型属性,添加到原型对象上
    if (protoProps) _defineProperties(Constructor.prototype, protoProps)
    // 如果是静态属性,添加到构造函数上
    if (staticProps) _defineProperties(Constructor, staticProps)
    return Constructor
  }
  // 封装后的 Object.defineProperty
  function _defineProperty(obj, key, value) {
    // ......
  }

  var Parent =
    /*#__PURE__*/
    (function() {
      // 添加 getter/setter
      _createClass(Parent, [
        {
          key: 'thought',
          get: function get() {
            console.log('Thought in head is translate to Chinese.')
            return this._thought
          },
          set: function set(newVal) {
            this._thought = newVal
          }
        }
      ])

      function Parent(name) {
        // 检查是否通过 new 操作调用
        _classCallCheck(this, Parent)
        // 初始化 isAdult
        _defineProperty(this, 'isAdult', true)
        // 根据入参初始化 name
        this.name = name
      }
      // 添加 talk 和 live 方法
      _createClass(
        Parent,
        [
          {
            key: 'talk',
            value: function talk() {
              console.log('talk')
            }
          }
        ],
        [
          {
            key: 'live',
            value: function live() {
              console.log('live')
            }
          }
        ]
      )

      return Parent
    })()
  // 初始化静态属性 nation
  _defineProperty(Parent, 'nation', 'China')

与过程一相比,编译后的代码, Babel 多生成了一个 _defineProperties(target, props)_createClass(Constructor, protoProps, staticProps) 的辅助函数,这两个主要用来添加原型属性和静态属性,并且通过 Object.defineProperty 的方法,对数据描述符存取描述符都可以进行控制。
值得注意的是,ES6 中的 class 里的所有方法都是不可遍历的(enumerable: false),这里有一个小细节: 如果有使用 TypeScript,在设置 compileOptions 中的 target 时,如果设置为 es5,那么会发现编译后的 方法可以通过 Object.keys() 遍历到,而设置为es6时就无法被遍历。

总结

Babel 通过 AST 抽象语法树分析,然后添加以下

  • _instanceof(left, right) // 封装后的 instanceof 操作
  • _classCallCheck(instance, Constructor) // 检查是否通过 new 操作调用
  • _defineProperties(target, props) // 封装 Object.defineProperty 来添加属性
  • _createClass(Constructor, protoProps, staticProps) // 为 Constructor 添加原型属性或者静态属性并返回
  • _defineProperty(obj, key, value) // // 封装后的 Object.defineProperty

五个辅助函数,来为 Parent 构造函数添加属性和方法,转换 名为 class 的语法糖为 ES5 的代码。

何为 extends

既然 ES6 没有类,那又应该如何实现继承呢,相信聪明的你已经知道了,其实和 class 一样,extends 也是语法糖,接下来我们一步一步接着把这层语法糖也拆开。

ES5 的 寄生组合式继承

从 Prototype 开始说起(上)—— 图解 ES5 继承相关 这里知道,相对完美的继承实现是 寄生组合式继承,为了方便阅读,这里再次附上源码和示意例图:

function createObject(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function Parent(name) {
    this.name = name
}

function Child(name) {
    Parent.call(this, name)
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

var child = new Child('child')

QQ20191117-204128.png

ES6ES5 写法对比

如果参考上面的继承实现,我们可以轻松地写出两种版本的继承形式

class Child extends Parent {
    constructor(name, age) {
        super(name); // 调用父类的 constructor(name)
        this.age = age;
    }
}
function Child (name, age) {
    Parent.call(this, name)
    this.age = age
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

Babel 是如何进行编译的

一些细节

  • 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。
    也正是因为这个原因,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。
  • ES6 中,父类的静态方法,可以被子类继承。class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。
    QQ20191117-204140@2x.png

编译过程

同样的,我们将代码输入到 Babel 官网的 Try it out 来查看编译后的代码:

'use strict'
  // 封装后的 typeof
  function _typeof(obj) {
    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)
  }
  // 调用父类的 constructor(),并返回子类的 this
  function _possibleConstructorReturn(self, call) {
    if (
      call &&
      (_typeof(call) === 'object' || typeof call === 'function')
    ) {
      return call
    }
    return _assertThisInitialized(self)
  }
  // 检查 子类的 super() 是否被调用
  function _assertThisInitialized(self) {
    if (self === void 0) {
      throw new ReferenceError(
        "this hasn't been initialised - super() hasn't been called"
      )
    }
    return self
  }
  // 封装后的 getPrototypeOf
  function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf
      ? Object.getPrototypeOf
      : function _getPrototypeOf(o) {
          return o.__proto__ || Object.getPrototypeOf(o)
        }
    return _getPrototypeOf(o)
  }
  // 实现继承的辅助函数
  function _inherits(subClass, superClass) {
    if (typeof superClass !== 'function' && superClass !== null) {
      throw new TypeError(
        'Super expression must either be null or a function'
      )
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
      constructor: { value: subClass, writable: true, configurable: true }
    })
    if (superClass) _setPrototypeOf(subClass, superClass)
  }
  // 封装后的 setPrototypeOf
  function _setPrototypeOf(o, p) {
    _setPrototypeOf =
      Object.setPrototypeOf ||
      function _setPrototypeOf(o, p) {
        o.__proto__ = p
        return o
      }
    return _setPrototypeOf(o, p)
  }
  // 检查是否通过 new 操作调用
  function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
      throw new TypeError('Cannot call a class as a function')
    }
  }

  var Child =
    /*#__PURE__*/
    (function(_Parent) {
      // 继承操作
      _inherits(Child, _Parent)

      function Child(name, age) {
        var _this

        _classCallCheck(this, Child)
        // 调用父类的 constructor(),并返回子类的 this
        _this = _possibleConstructorReturn(
          this,
          _getPrototypeOf(Child).call(this, name)
        )
        // 根据入参初始化子类自己的属性
        _this.age = age
        return _this
      }

      return Child
    })(Parent)

_inherits(subClass, superClass)

我们来细看一下这个实现继承的辅助函数的细节:

function _inherits(subClass, superClass) {
    // 1. 检查 extends 的继承目标(即父类),必须是函数或者是 null
    if (typeof superClass !== 'function' && superClass !== null) {
      throw new TypeError(
        'Super expression must either be null or a function'
      )
    }
    // 2. 类似于 ES5 的寄生组合式继承,使用 Object.create,
    //    设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
    subClass.prototype = Object.create(superClass && superClass.prototype, {
      constructor: { value: subClass, writable: true, configurable: true }
    })
    // 3. 设置子类的 __proto__ 属性指向父类
    if (superClass) _setPrototypeOf(subClass, superClass)
  }

这个方法主要分为3步,其中第2步,通过寄生组合式继承在实现继承的同时,新增了一个名为 constructor 的不可枚举的属性;第3步实现了上文说的第二条原型链,从而达到静态方法也能被继承的效果。

_possibleConstructorReturn(self, call)

这个辅助函数主要是用来实现 super() 的效果,对应到寄生组合式继承上则是借用构造函数继承的部分,有所不同的是,该方法返回一个 this 并赋给子类的 this。具体细节可以在 ES6 系列之 Babel 是如何编译 Class 的(下) 查看。

总结

class 一样,Babel 通过 AST 抽象语法树分析,然后添加一组辅助函数,在我看来可以分为两类,第一类:

  • _typeof(obj) // 封装后的 typeof
  • _getPrototypeOf(o) // 封装后的 getPrototypeOf
  • _setPrototypeOf(o, p) // 封装后的 setPrototypeOf

这种为了健壮性的功能辅助函数
第二类:

  • _assertThisInitialized(self) // 检查 子类的 super() 是否被调用
  • _possibleConstructorReturn(self, call) // 调用父类的 constructor(),并返回子类的 this
  • _classCallCheck(instance, Constructor) // 检查是否通过 new 操作调用
  • _inherits(subClass, superClass) // 实现继承的辅助函数

这种为了实现主要功能的流程辅助函数,从而实现更完善的寄生组合式继承

后记

从 Prototype 开始说起 一共分为两篇,从两个角度来讲述 JavaScript 原型相关的内容。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容