分析Babel编译代码,深入理解ES6的类与继承

435 阅读9分钟

前言

ES2015+ 有各种新特性(语法糖),尽管有很多特性尚未纳入标准或浏览器还没有原生支持,但是 Babel 的出现让前端程序员可以不用担心兼容性问题而使用处于各种 stage 的 ES2015+ 语法。其实 class 关键字目前只是实现类的语法糖,但是可以帮助我们屏蔽掉每次实现类时的样本代码,逻辑更加清晰,并且阻止我们踩可能存在的坑,本篇文章从 ES5 的类实现到 ES6 中class 的 Babel 转码来分析类的实现与继承。

在 JavaScript 中,我们希望一个类能有以下特性:

  1. 实例的属性是绑定在每个实例上的
  2. 方法是绑定在这个类的 prototype 上的
  3. 对实例进行 Object.getPrototypeOf 能拿到 constructorprototype
  4. 实例 instanceof 构造函数返回 true
  5. 类的静态属性/方法

ES5实现类

ES5 中的类是通过 构造函数模式+原型模式 实现的。

function Animal(name) {
  this.name = name // 不共享实例属性
}

Animal.prototype.barking = function() {
    console.log(this.name + ' : ah!') // 共享方法
 }

Animal.hello = function() {
    console.log('hello animal')
}

以上几点都是实现了的,但是缺点就是封装性不好,样本代码多,这只是简陋版的实现,不过思路就是这样。

ES6的class

class Animal {
    // 构造函数
    constructor(name){
        this.name = name
    }
    // 类的实例属性
    age = 1
    // 类的实例方法
    sayAge = function(){
        console.log(this.age)
    }
    // 类的方法
    barking () {
        console.log(this.name + ' : ah!')
    }
    // getter
    get description () {
        return 'description: ' + this.name;
    }
    // 类的静态属性
    static id = 27
    // 类的静态方法
    static hello() {
        console.log('hello animal ' + this.id)
    }
}

相当简洁了,整个类的声明都在一起,接下来我们看一下 Babel 编译出来的代码:

'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.age = 1;

    this.sayAge = function () {
      console.log(this.age);
    };

    this.name = name;
  }
  // 类的实例属性

  // 类的实例方法


  _createClass(Animal, [{
    key: 'barking',

    // 类的方法
    value: function barking() {
      console.log(this.name + ' : ah!')
    }
    // getter

  }, {
    key: 'description',
    get: function get() {
      return 'description: ' + this.name;
    }
    // 类的静态属性

  }], [{
    key: 'hello',

    // 类的静态方法
    value: function hello() {
      console.log('hello animal ' + this.id);
    }
  }]);

  return Animal;
}();

Animal.id = 27;

下面开始分析:

'use strict';

使用严格模式的原因阮老师在 ECMAScript 6 入门 中有解释,这里直接贴一下:

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

先从主函数入手

  function Animal(name) {
    _classCallCheck(this, Animal);
    this.age = 1;
    this.sayAge = function () {
      console.log(this.age);
    };

    this.name = name;
  }

  _createClass(Animal, [{
    key: 'barking',
    value: function barking() {
      console.log(this.name + ' : ah!')
    }
  }, {
    key: 'description',
    get: function get() {
      return 'description: ' + this.name;
    }
  }], [{
    key: 'hello',
    value: function hello() {
      console.log('hello animal ' + this.id);
    }
  }]);

  return Animal;

先执行构造函数,首先调用 _classCallCheck用来确保类是通过 new 作为构造函数调用而不是直接调用,如果是直接调用则直接报错。

然后是在构造函数里绑定实例的属性和方法 —— age(直接写入类的定义的实例属性),sayAge(直接写入类的定义的实例方法),name (构造函数中的实例属性)。这里要注意,直接写入类的定义的实例属性/方法要先于构造函数中的实例属性/方法执行,所以如果在直接写入类的定义的实例方法中获取构造函数中定义的属性/方法,会返回 undefined

然后就是用 _createClass,接受两个参数:[绑定在类的prototype上的方法, 绑定在类上的静态方法],作用是把方法绑定在对应的对象上。

var _createClass = function () {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false; // 默认false,原型/静态方法不允许枚举
      descriptor.configurable = true; // 默认为false,设为true,否则一切属性都无法修改
      if ("value" in descriptor) descriptor.writable = true; // 默认false,设为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;
  };
}();

通过 Object.defineProperty 将各个方法绑定到 类的 prototype/类 上,Object.defineProperty 可以指定对象的属性,让类的原型方法/静态方法无法被枚举。

还有一点比较有意思的是,这里 _createClass 是 IIFE 的返回值,这样能做到不污染全局作用域,但是最后还是会有一个 _createClass,那是不是直接可以在声明一个 class 后调用 _createClass 呢?答案当然是不可以,如果你在源代码里访问或操作 _createClass,这个默认叫 _createClass 函数就会被改成 _createClass2,总之就是不让你操作到哈哈哈哈。

最后,再补上一个类的静态属性就完事大吉了:

Animal.id = 27;

但是类的静态属性也可以写成函数表达式的形式,这样的话类的静态方法就是可以枚举的了。

继承

ES5的继承

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

Animal.prototype.barking = function() {
    console.log(this.name + ' : ah!')
 }

Animal.hello = function() {
    console.log('hello animal')
}

function Cat(name, breed) {
    Animal.call(this, name) // 已经生成了指向子类实例的this,再调用父类的构造函数
    this.breed = breed
}

Cat.prototype = Object.create(Animal.prototype) // 直接拿到父类的prototype,避免多次调用父类的构造函数
Cat.prototype.barking = function(){
  var catPrototype = Object.getPrototypeOf(this) 
  var animalPrototype = Object.getPrototypeOf(catPrototype)
  animalPrototype.barking.call(this);
  console.log(this.name + ' : mew!')
}

var cat = new Cat('Tom', 'American shorthair')
console.log(cat.name) // "Tom"
console.log(cat.breed) // "American shorthair"
console.log(cat instanceof Animal) // "true"
console.log(cat instanceof Cat) // "true"
cat.barking() // "Tom : ah!" "Tom : mew!"

ES6的继承

class Animal{
  constructor(name){
    this.name = name
  }
  
  barking () {
    console.log(this.name + ' : ah!')
  }
  
  static hello () {
    console.log('hello animal')
  }
}

class Cat extends Animal{
  constructor(name, breed){
    super(name)
    // 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
    //ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
    //ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。 —— 阮老师的ES6教程
      // ↑↑↑ 这是按照ES6标准,但是目前的class只是语法糖,所以依旧是先创建一个子类的对象,在用父类的方法去加工 ↑↑↑
    this.breed = breed
  }
  
  barking(){
    super.barking()
    console.log(this.name + ' : mew!')
  }
  
  static hello () {
    super.hello()
    console.log('hello kitty')
  }
}

var cat = new Cat('Tom', 'American shorthair')
console.log(cat.name) // "Tom"
console.log(cat.breed) // "American shorthair"
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
cat.barking() // "Tom : ah!" "Tom : mew!"
Cat.hello() // "hello animal" "hello kitty"

Babel编译后的代码太长了 ,我们只需要关注继承的类比不继承的类多了那些功能即可:

var Cat = function (_Animal) {  
    _inherits(Cat, _Animal); // 子类去继承父类,子类的原型去继承父类的原型

  function Cat(name, breed) {
    _classCallCheck(this, Cat);

    var _this = _possibleConstructorReturn(this, (Cat.__proto__ || Object.getPrototypeOf(Cat)).call(this, name)); // 先生成一个父类的构造函数返回 this

    _this.breed = breed; // 再用子类的构造函数去对这个 this 添加实例
    return _this;
  }

  _createClass(Cat, [{
    key: 'barking',
    value: function barking() {
      _get(Cat.prototype.__proto__ || Object.getPrototypeOf(Cat.prototype), 'barking', this).call(this); // 调用父类原型的barking方法
      console.log(this.name + ' : mew!'); // 再执行子类的barking方法
    }
  }], [{
    key: 'hello',
    value: function hello() {
      _get(Cat.__proto__ || Object.getPrototypeOf(Cat), 'hello', this).call(this); // 调用父类的hello静态方法
      console.log('hello kitty'); // 再执行子类的barking静态方法
    }
  }]);

  return Cat;
}(Animal);

所以一共就多了三个函数 _inherits_possibleConstructorReturn_get

先看_inherits

// 调用
_inherits(Cat, _Animal);

// 定义
function _inherits(subClass, 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.__proto__ = superClass.prototype
  // 子类的原型继承父类的原型
  // subClass.prototype.constructor = subClass
  // 子类的构造函数指向子类
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
    // 子类继承父类
    // subClass.__proto__ = superClass
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

这个函数完成了三个重要的任务:

  1. 子类的原型继承父类的原型
  2. 子类原型的构造函数指向子类
  3. 子类继承父类

至此,子类原型已近能够访问父类原型的方法了,子类也能够访问父类的静态方法。

再来看 _possibleConstructorReturn

//
var _this = _possibleConstructorReturn(this, (Cat.__proto__ || Object.getPrototypeOf(Cat)).call(this, name)); // 先生成一个父类的构造函数返回的this

// 两个参数,一个参数是指向子类实例的this,另一个参数是调用父类的构造函数返回的父类实例
function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  // 如果父类返回的是对象或函数,则返回父类的构造函数生成的this,否则返回self
  return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

作用是生成并返回一个调用父类的构造函数的this,再在主函数中用子类的构造函数进行加工。

再来看 _get

// 调用
_createClass(Cat, [{
    key: 'barking',
    value: function barking() {
      _get(Cat.prototype.__proto__ || Object.getPrototypeOf(Cat.prototype), 'barking', this).call(this);
      console.log(this.name + ' : mew!');
    }
  }] 
...

// 定义
var _get = function get(object, property, receiver) {
  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 { // 如果是getter
    var getter = desc.get;
    if (getter === undefined) {
      return undefined;
    }
    return getter.call(receiver);
  }
};

_get 接受三个参数,父类原型/父类,子类要 override 父类的方法,还有当前的子类实例。

但是要注意,再次强调,ES6 的 class 只是用 ES5 来实现的话就只是语法糖,因为还是无法完成原生构造函数的继承。

来自 Babel 的说明

Built-in classes such as Date, Array, DOM etc cannot be properly subclassed due to limitations in ES5 (for the es2015-classesplugin). You can try to use babel-plugin-transform-builtin-extend based on Object.setPrototypeOf and Reflect.construct, but it also has some limitations.

测试:

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
console.log(arr.length) // 理想输出:1 实际输出:0

arr.length = 0;
console.log(arr[0]) // 理想输出:undefined 实际输出:12

总结

到这里,Babel 编译的代码就分析完了,下面来看一下阮老师的ES6教程中的知识点,看看是不是能做到完全理解:

  • 在子类 constructor() 中,super 指向 Parentsuper 中的 this 指向 Child 类的实例,所以相当于Parent.call(this)

  • 在子类方法中,super 指向 Parent.prototypesuper 中的 this 指向子类的实例,所以如果有 super 调用就是 Parent.prototype.func.call(this)

  • super 在静态方法之中指向父类,而不是父类的原型对象。在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。

  • 子类的原型指向父类

    Child.proto === Parent // true
  • 子类的 prototype 的原型指向父类的原型

    Child.prototype.__proto__ = Parent.prototype // true
    //相当于
    B.prototype = Object.create(A.prototype)
  • // o1 是父类的实例,o2 是子类的实例
    o2.__proto__.__proto__ === o1.proto__ // true

吐槽

写到一半Typora崩溃了把我保存的内容都吞了是真的坑,在心态崩了的情况下再重写一次真是磨练心智 😭