【es6系列】结合babel彻底理解class继承黑魔法

416 阅读11分钟

努力让学习成为一种习惯,自信来源于充分的准备

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

前言

js实现继承的多种方式中,我们介绍了es5中的多种继承方式。主要是借助原型链构造函数,但是写法感官上与传统的面向对象语言差异很大并且过于繁琐

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类,但需要注意的是class只是一个语法糖,底层原理实现与es5基本一致但也有所不同

那么class背后到底做了什么呢,让我们借助babel来揭开class的黑魔法

阅读完本篇文章你将get以下技能

  1. es6class实现继承的原理是什么
  2. es6es5实现继承的区别有哪些

es5与es6写法对比

在揭开面纱之前,我们先看看es5与es6的各功能对比,让大家对class有一个感性的认识

实例生成

// es5写法
function Person () {
  this.name = 'kk'
}
const p = new Person()

// es6写法
class Person {
  name = 'kk'
  constructor() {
    // this.name = 'kk' 两者写法一样
  }
}
const p = new Person()

es5es6均是使用new方式调用函数生成实例,不过两者有所区别

  1. es5函数本身直接作为构造函数,es6constructor方法作为构造函数,这里有一个比较容易理解错的点是认为es6是调用内部constructor方法,但这是错的!!,(实际babel编译的时候也并不会将constructor作为prototype的一个属性,而是直接作为入口构造函数),这么说可能有些拗口,我们借助代码理解下
// es6
// 虽然下面是true
console.log('constructor' in Person.prototype) // true

// 但是
new Person()
//! 不等同于!!!!
new Person.prototype.constructor()

// 手动修改constructor
Person.prototype.constructor = 1

new Person() // 依旧可以生成

new Person.prototype.constructor() // Uncaught TypeError: Person.prototype.constructor is not a constructor

这里其实和es5行为表现一致,constructor并不是一个安全靠谱的属性

  1. es5的构造函数可以作为普通函数调用,而es6必须使用new调用
Person() // Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
Person.prototype.constructor()// Uncaught TypeError: Class constructor Person cannot be invoked without 'new'

原型方法

// es5写法
function Person () {
  this.name = 'kk'
}
Person.prototype.getName = function () { return this.name }
const p = new Person()

// es6写法
class Person {
  name = 'kk'
  constructor() {}
  getName() { return this.name }
}
const p = new Person()

这里需要需要注意两点:

  1. 类内部包括constrcutor在内的所有方法均在其原型对象上
console.log(Object.hasOwn(Person.prototype, 'name')); // false
console.log(Object.hasOwn(Person.prototype, 'constructor')); // true
console.log(Object.hasOwn(Person.prototype, 'getName')); // true
  1. 类内部定义的方法均不可枚举,这与es5不一样
console.log(Object.keys(Person.prototype)) // []
console.log(Object.getOwnPropertyNames(Person.prototype)) // ['constructor', 'getName']

静态属性、方法

这里引用阮一峰大佬es6教程的说明:

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”,属性也是一样

class Person {
  static age = '12'
  static getAge = function () {
    return this.age
  }
}
const p = new Person()

console.log(p.age); // undefined
console.log(p.getAge); // undefined
console.log(Person.age); // '12'
console.log(Person.getAge()); // '12'

其对应es5的代码为

function Person() {}
Person.age = '12'
Person.getAge = function () { return this.age }


const p = new Person()
console.log(p.age); // undefined
console.log(p.getAge); // undefined
console.log(Person.age); // '12'
console.log(Person.getAge()); '12'

这里需要注意的是:对于class,静态方法内部的this指向本身。对于es5构造函数,则指向函数本身。区别在于class子类可以继承父类的静态属性和方法

getter与setter

class Person {
  get name() {
    return 'kk'
  }
  set name(_name) {
    console.log(_name);
  }
}

const p = new Person()
console.log(p.name); // kk
p.name = 'oo' // oo
console.log(Object.hasOwn(Person.prototype, 'name')) // true

这里es5表现与其一致

function Person() {}
Person.prototype = {
  constructor: Person,
  get name() {
    return 'kk'
  },
  set name(_name) {
    console.log(_name);
  }
}

babel是如何编译的

在前面我们列举了class的特点,并且都可以用es5的方式实现。只是有些细节会不一样

但是babel编译的时候并不会直接转化成上面这样,它会添加许多辅助函数并且以更加严谨、完善的方式帮助我们实现es6特性,接下来让我们一步一步看babel是如何实现class

ps: 为了方便理解,下面babel转化后的代码只会放与核心实现最相关的代码,并适当做一些简化

编译构造函数与实例属性

es6代码

class Person {
  constructor(name) {
    this.name = name
  }
}
const p = new Person()

babel编译

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}
var Person = function Person(name) {
  _classCallCheck(this, Person)
  this.name = name
}
var p = new Person()

这里我们可以看到,babel直接将class内部的constructor方法用于构造函数。其中_classCallCheck函数检测this是不是其构造函数的实例,如果不是则抛出错误。因为class只能通过new方式调用

注意,如果这里采用在构造函数外面定义实例属性的方式,babel编辑的结果会稍微有些不同

es6代码

class Person {
  name = 'kk'
  constructor() {}
}
const p = new Person()

babel编译

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
}

function _classCallCheck(instance, Constructor) {...}
var Person = function Person(name) {
  _classCallCheck(this, Person)
  _defineProperty(this, 'name', 'kk')
}
var p = new Person()

两者其实本质差不多

编译原型方法,静态属性、方法、getter/setter

es6代码:

class Person {
  static age = 1
  static getAge = function () {
    return this.age
  }
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
  get habbit() {
    return 'basketball'
  }
  set habbit(_habbit) {
    console.log(_habbit)
  }
}
const p = new Person()

babel编译:

function _classCallCheck(instance, Constructor) {...}
function _defineProperty(obj, key, value) {...}

var Person = function () {
  function Person(name) {
    _classCallCheck(this, Person);
    this.name = name;
  }
  return _createClass(Person, [{
    key: "getName",
    value: function getName() {
      return this.name;
    }
  }, {
    key: "habbit",
    get: function get() {
      return 'basketball';
    },
    set: function set(_habbit) {
      console.log(_habbit);
    }
  }]);
}();
_defineProperty(Person, "age", 1);
_defineProperty(Person, "getAge", function () {
  return this.age;
});
var p = new Person();

这里我们可以看到对于静态属性、方法,babel利用_defineProperty函数直接将其定义在函数本身上面, 另外新增了一个_createClass函数,第一个参数是构造函数,第二个参数是需要定义在原型上的属性、方法,我们来看看_createClass函数内部

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)
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
  if (staticProps) _defineProperties(Constructor, staticProps)
  Object.defineProperty(Constructor, 'prototype', { writable: false })
  return Constructor
}

可以看出,babel将class内部的getter、setter、方法统一放到protoProps里,并依次设置到构造函数的原型对象上。静态属性和方法统一放到staticProps里,并依次设置到了构造函数上(上面我们发现,针对静态属性、方法。babel直接放到了最外面处理而没有传入_createClass函数中,但其实两者本质都是一样的),最后返回构造函数

这里其实有点像寄生式继承:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象,但这里只是增强,不涉及原型链的操作,对于babel有专门处理继承、原型相关操作的函数,这个下面会讲到

_defineProperties方法很好理解。运用Object.defineProperty设置属性,class内部定义的方法默认都是不可枚举的。如果有value,设置writable为true,否则就使用gettersetter

继承

es6

class Parent {
    constructor(name) {
        this.name = name;
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
}
var child1 = new Child('kk', '18');

babel编译

function _createClass(Constructor, protoProps, staticProps) {...}
function _classCallCheck(instance, Constructor) {...}

var Parent = _createClass(function Parent(name) {
  _classCallCheck(this, Parent)
  this.name = name
})
var Child = (function (_Parent) {
  function Child(name, age) {
    var _this
    _classCallCheck(this, Child)
    _this = _callSuper(this, Child, [name])
    _this.age = age
    return _this
  }
  _inherits(Child, _Parent)
  return _createClass(Child)
})(Parent)
var child1 = new Child('kk', '18')

这里有两个新增的函数:_inherits_callSuper,我们来具体看下

_inherits

function _setPrototypeOf(o, p) {
  _setPrototypeOf = Object.setPrototypeOf
    ? Object.setPrototypeOf.bind()
    : function _setPrototypeOf(o, p) {
        o.__proto__ = p
        return o
      }
  return _setPrototypeOf(o, p)
}

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 }
  })
  Object.defineProperty(subClass, 'prototype', { writable: false })
  if (superClass) _setPrototypeOf(subClass, superClass)
}

extends继承目标必须是一个function或者null image.png

类本质还是构造函数,因此它同时具有[[prototype]]prototype

从babel编译的代码我们可以得出以下关系

// 这里用非标准的方式,更加直观
console.log(Child.__proto__ === Parent) // true
console.log(Child.prototype.__proto__ === Parent.prototype) // true

其中class继承的原型链相比于es5的原型链多了一条关系:Child.__proto__ -> Parent

es5

image.png

es6

image.png

为什么会多一条这样的关系呢,我们来看段代码

// es6 class
class A {
  static a = 1
  static test = function () { return this.a }
}
class B extends A {}
console.log(B.a) // 1
console.log(B.test()) //1

es5 寄生组合继承

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

function inheritPrototype(o, options) {
   const t = createObj(o)
   Object.defineProperties(t, options)
   return t
}

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

Parent.a = 1
parent.test = function () {
  return this.a
}

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

Child.prototype = inheritPrototype(Parent.prototype, {
  constructor: {
    value: Child,
    configurable: true,
    writable: true,
    enumerable: false
  }
})

console.log(Child.a) // undefined
console.log(Child.test) // undefined

对于class,子类可以继承父类的静态属性、方法。而es5的继承模式则不支持。这正是Child.__proto__ -> Parent这条链路的作用!

_callSuper

这块的逻辑相对比较复杂,也是es6es5继承最大的不同点,为了方便阅读,我把babel编译调用_callSuper的例子复制过来

var Parent = _createClass(function Parent(name) {
  this.name = name
})
var Child = (function (_Parent) {
  function Child(name, age) {
    var _this
    _this = _callSuper(this, Child, [name])
    _this.age = age
    return _this
  }
  _inherits(Child, _Parent);
  return _createClass(Child);
})(Parent)
// 这样代码有点绕。简化下
function _callSuper(t, o, e) {
  return (
    (o = _getPrototypeOf(o)),
    _possibleConstructorReturn(
      t,
      _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)
    )
  )
}

// 上面代码相当于,注意这里的childThis并不是最终Child构造函数里的this
// 这里可以认为传进去了一个空对象{}
function _callSuper(childThis,Child, [name]) {
   return _possibleConstructorReturn(
       // 这里就相当于childThis, Parent.apply(childThis, [name])
       childThis, Child.__proto__.apply(childThis, [name])
   )
}

我们接着看_possibleConstructorReturn干了啥

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === 'object' || typeof call === 'function')) {
    return call
  } else if (call !== void 0) {
    throw new TypeError('Derived constructors may only return object or undefined')
  }
  return _assertThisInitialized(self)
}

由上面逻辑我们可以知道:父类构造函数执行的返回值是关键

我们用代码验证下构造函数各种类型返回值对应实例的结果

// 显示返回对象
class A {
  constructor() {
    this.a = 2
    return {
       a: 1
    }
  }
}
const a = new A()
console.log(a) // {a: 1}
console.log(a instanceof A) // false
// 返回null
class A {
  constructor() {
    this.a = 1
    return null
  }
}
const a = new A()
console.log(a) // {a: 1}
console.log(a instanceof A) // true
// 返回函数
class A {
  constructor() {
    this.a = 1
    return function() {}
  }
}
const a = new A()
console.log(a) // ƒ () {}
console.log(a instanceof A) // false
// 不返回即返回undefined
class A {
  constructor() {
    this.a = 1
  }
}
const a = new A()
console.log(a) // {a:1}
console.log(a instanceof A) // true
// 返回基本类型
class A {
  constructor() {
    this.a = 1
    return 1
  }
}
const a = new A()
console.log(a) // {a:1}
console.log(a instanceof A) // true

由上面例子可以得出结论:当Parent.call(childThis)的值是一个有值对象的时候,返回该对象。其他情况都返回childThis

这里有个关键的顺序点:

function Child(name, age) {
    var _this
    _this = _callSuper(this, Child, [name])
    _this.age = age
    return _this
}

我们可以发现,子类实际的this由父类构造函数按其执行规则执行后的返回结果来进行初始化,这也说明:在子类构造函数里,我们不能在super函数调用之前使用this。因为此时this还没有初始化

我们看以下代码

class Parent {
    constructor(name) {
        this.name = name;
    }
}
class Child extends Parent {
    constructor(name, age) {}
}
var child1 = new Child('kk', '18')

babel编译结果:

var Parent = /*#__PURE__*/_createClass(function Parent(name) {
  _classCallCheck(this, Parent);
  this.name = name;
});
var Child = /*#__PURE__*/function (_Parent) {
  function Child(name, age) {

    var _this;
    _classCallCheck(this, Child);
    // _this为undefined, 会抛出异常
    return _assertThisInitialized(_this);
  }
  _inherits(Child, _Parent);
  return _createClass(Child);
}(Parent);
var child1 = new Child('kk', '18');

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

我们可以发现,没有调用super(), _this没有初始化。子类构造函数的this为undefined,后会抛出异常

最后

这里我们对开头的两个问题进行归纳总结下

es6中class实现继承的原理是什么

es6class实现继承的原理与es5一样,都是基于原型链构造函数

es6与es5实现继承的区别有哪些

es6继承的原型链对比es5多了一条直接基于构造函数本身的继承。这使得子类可以访问父类上的静态属性静态方法

es5的继承机制是先生成子类实例对象,然后再将父类的属性添加进去。即实例在前,继承在后

es6的继承机制则是先将父类的属性和方法,加到一个空的对象上面,然后再对其进行加工,添加子类自己的实例属性和方法,最后作为自己的实例对象。即“继承在前,实例在后

到这里,就是本篇文章的全部内容了

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

如果你有疑问或者出入,评论区告诉我,我们一起讨论

参考文档

ECMAScript6入门-class

ES6 系列之 Babel 是如何编译 Class 的