Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
LazyDuke opened this issue Nov 17, 2019 · 0 comments
Open
Labels
blog blog

Comments

@LazyDuke
Copy link
Owner

何为 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')

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__ 属性,因此同时存在两条继承链。

编译过程

同样的,我们将代码输入到 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 原型相关的内容。

参考资料

@LazyDuke LazyDuke added the blog blog label Nov 17, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blog blog
Projects
None yet
Development

No branches or pull requests

1 participant