阅读 570

深入理解 Class 和 extends 原理

前言

ES6 里面的 class 和 extends 大家想必都会用了,但对于他们的实现是否了解呢,babel 后 classextends 又是如何实现的呢? 这节课将带领你深入理解 babel 编译后 classextends 的实现方式。

注意:本文涉及到 立即执行函数( IIFE )、instanceofObject.defineProperty,如果还未接触过,建议先点击链接学习。

准备工作

在开始之前,我们需要一个 babel 的环境,方便查看 babel 后的代码,这里我推荐两种方式。

  1. chrome 插件 —— ScratchJS,可以设置 babel 来转换代码,通过点击 Toggle output 就能看到 babel 后的代码。
  2. babel 官网推荐的在线编译工具 试一试,可以实时看到转换前后的代码。

本文将以 ScratchJS 转换后的代码为例进行代码分析。

1. class 实现

先从最简单的 class 开始看,下面这段代码涵盖了使用 class 时所有会出现的情况(静态属性、构造函数、箭头函数)。

class Person {
    static instance = null;
    static getInstance() {
        return super.instance;
    }
	constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sayHi() {
        console.log('hi');
    }
    sayHello = () => {
        console.log('hello');
    }
    sayBye = function() {
        console.log('bye');
    }
}
复制代码

而经过 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 Person = function () {
  function Person(name, age) {
    _classCallCheck(this, Person);

    this.sayHello = function () {
      console.log('hello');
    };
    
    this.sayBye = function () {
      console.log('bye');
    };
    
    this.name = name;
    this.age = age;
  }

  _createClass(Person, [{
    key: 'sayHi',
    value: function sayHi() {
      console.log('hi');
    }
  }]);

  return Person;
}();

Person.instance = null;
复制代码

最外层的 Person 变量被赋值给了一个立即执行函数,立即执行函数里面返回的是里面的 Person 构造函数,实际上最外层的 Person 就是里面的 Person 构造函数。

在 Person 类上用 static 设置的静态属性instance,在这里也被直接挂载到了 Person 构造函数上。

1.1 挂载属性方法

Person 类上各个属性的关系是这样的:

image_1dmjbel2cfvdls41h2e1hcmpn39.png-30.9kB

你是不是很好奇,为什么在 Person 类上面设置的 sayHisayHellosayBye 三个方法,编译后被放到了不同的地方处理?

从编译后的代码中可以看到 sayHellosayBye 被放到了 Person 构造函数中定义,而 sayHi_createClass 来处理(_createClasssayHi 添加到了 Person 的原型上面)。

曾经我也以为是 sayHello 使用了箭头函数的缘故才让它最终被绑定到了构造函数里面,后来我看到 sayBye 这种用法才知道这和箭头函数无关。

实际上 class 中定义属性还有一种写法,这种写法和 sayBye 如出一辙,在 babel 编译后会将其属性放到构造函数中,而非原型上面。

class Person {
    name = 'tom';
    age = 23;
}
// 等价于
class Person {
    constructor() {
        this.name = 'tom';
        this.age = 23;
    }
}
复制代码

如果我们将 name 后面的 'tom' 换成函数呢?甚至箭头函数呢?这不就是 sayByesayHello 了吗?

因此,在 class 中不直接使用 = 来定义的方法,最终都会被挂载到原型上,使用 = 定义的属性和方法,最终都会被放到构造函数中。

1.2 _classCallCheck

Person 构造函数中调用了 _classCallCheck 函数,并将 this 和自身传入进去。 在 _classCallCheck 中通过 instanceof 来进行判断,instance 是否在 Constructor 的原型链上面,如果不在上面则抛出错误。这一步主要是为了避免直接将 Person 类当做函数来调用。 因此,在ES5中构造函数是可以当做普通函数来调用的,但在ES6中的类是无法直接当普通函数来调用的。

注意:为什么通过 instanceof 可以判断是否将 Person 类当函数来调用呢? 因为如果使用 new 操作符实例化 Person 的时候,那么 instance 就是当前的实例,指向 Person.prototypeinstance instanceof Constructor 必然为true。反之,直接调用 Person 构造函数,那么 instance 就不会指向 Person.prototype

1.3 _createClass

我们再来看 _createClass 函数,这个函数在 Person 原型上面添加了 sayHi 方法。

// 创建原型方法
_createClass(Person, [{
    key: 'sayHi',
    value: function sayHi() {
      console.log('hi');
    }
}]);
    
// _createClass也是一个立即执行函数
var _createClass = function () { 
    // 将props属性挂载到目标target上面
    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; 
            // 通过defineProperty来挂载属性
            Object.defineProperty(target, descriptor.key, descriptor); 
        } 
    } 
    // 这个才是“真正的”_createClass
    return function (Constructor, protoProps, staticProps) { 
        // 如果传入了需要挂载的原型方法
        if (protoProps) 
            defineProperties(Constructor.prototype, protoProps); 
        // 如果传入了需要挂载的静态方法
        if (staticProps) 
            defineProperties(Constructor, staticProps); 
        return Constructor; 
    }; 
}();
复制代码

_createClass 函数接收三个参数,分别是 Constructor (构造函数)、protoProps(需要挂载到原型上的方法)、staticProps(需要挂载到类上的静态方法)。 在接收到参数之后,_createClass 会进行判断如果有 staticProps,则挂载到 Constructor 构造函数上;如果有 protoProps ,那么挂载到 Constructor 原型上面。 这里的挂载函数 defineProperties 是关键,它对传入的 props 进行了遍历,并设置了其 enumerable(是否可枚举) 和 configurable(是否可配置)、writable(是否可修改)等数据属性。 最后使用了 Object.defineProperty 函数来给设置当前对象的属性描述符。

2. extends 实现

通过上文对 Person 的分析,相信你已经知道了 ES6 中类的实现,这与ES5中的实现大同小异,接下来我们来具体看一下 extends 的实现。 以下面的 ES6 代码为例:

class Child extends Parent {
  constructor(name, age) {
    super(name, age);
    this.name = name;
    this.age = age;
  }
  getName() {
    return this.name;
  }
}

class Parent {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  getName() {
    return this.name;
  }
  getAge() {
    return this.age;
  }
}
复制代码

babel后的代码则是这样的:

"use strict";

// 省略 _createClass
// 省略 _classCallCheck

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

var Child = function (_Parent) {
  _inherits(Child, _Parent);

  function Child(name, age) {
    _classCallCheck(this, Child);

    var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));

    _this.name = name;
    _this.age = age;
    return _this;
  }

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

  return Child;
}(Parent);

// 省略 Parent(类似上面的 Person 代码)
复制代码

我们可以清楚地看到,继承是通过_inherits实现的。 为了方便理解,我这里整理了一下原型链的关系:

image_1dmec296p60q11bp1f8c1rid1rc52a.png-43.1kB

除去一些无关紧要的代码,最终的核心实现代码就只有这么多:

var Child = function (_Parent) {

  _inherits(Child, _Parent);

  function Child(name, age) {

    var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));

    _this.name = name;
    _this.age = age;
    return _this;
  }

  return Child;
}(Parent);
复制代码

和前面的 Person 类实现有所不同的地方是,在 Child 方法中增加调用了 _inherits,还有在设置 nameage 属性的时候,使用的是执行 _possibleConstructorReturn 后返回的 _this,而非自身的 this,我们就重点分析这两步。

2.1 _inherits

先来看_inherits函数的实现代码:

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 设置为 superClass.prototype 的实例
    subClass.prototype = Object.create(superClass && superClass.prototype, { 
        constructor: { 
            value: subClass, 
            enumerable: false, 
            writable: true, 
            configurable: true 
        } 
    }); 
    // 将 subClass 设置为 superClass 的实例(优先使用 Object.setPrototypeOf)
    if (superClass) 
        Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}
复制代码

_inherits 函数接收两个参数,分别是 subClass (子构造函数)和 subClass (父构造函数),将这个函数做的事情稍微做一下梳理。

  1. 设置 subClass.prototype[[Prototype]]指向 superClass.prototype[[Prototype]]
  2. 设置 subClass[[Prototype]] 指向 superClass

在《深入理解类和继承》一文中,曾经提到过 ES5 中的寄生组合式继承,extends 的实现与寄生组合式继承实则大同小异,仅仅只增加了第二步操作。

2.2 _possibleConstructorReturn

Child 中调用了 _possibleConstructorReturn 函数,将 thisObject.getPrototypeOf(Child).call(this, name, age)) 传了进去。 这个 this 我们很容易理解,就是构造函数的 this,但后面这么长的一串又是什么意思呢? 刚刚在 _inherits 中设置了 Child[[Prototype]] 指向了 Parent,因此可以将后面这串代码简化为 Parent.call(this, name, age)。 这样你是不是就很熟悉了?这不就是组合继承中的执行一遍父构造函数吗? 那么 Parent.call(this, name, age) 执行后返回了什么呢? 正常情况下,应该会返回 undefined,但不排除 Parent 构造函数中直接返回一个对象或者函数的可能性。 *** 小课堂:** 在构造函数中,如果什么也没有返回或者返回了原始值,那么默认会返回当前的 this;而如果返回的是引用类型,那么最终实例化后的实例依然是这个引用类型(仅相当于对这个引用类型进行了扩展)。

const obj = {};
function Parent(name) {
    this.name = name;
    return obj;
}
const p = new Parent('tom');
obj.name; // 'tom'
p === obj; // true
复制代码

如果没有 self,这里就会直接抛出错误,提示 super 函数还没有被调用。 最后会对 call 进行判断,如果 call 为引用类型,那么返回 call,否则返回 self

注意:call 就是 Parent.call(this, name, age) 执行后返回的结果。

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; 
}
复制代码

Child 方法中,最终拿到 _possibleConstructorReturn 执行后的结果作为新的 this 来设置构造函数里面的属性。

思考题:如果直接用 this,而不是 _this,会出现什么问题?

总结

ES6 中提供的 classextends 本质上只是语法糖,底层的实现原理依然是构造函数和寄生组合式继承。 所以对于一个合格的前端工程师来说,即使 ES6 已经到来,对于 ES5 中的这些基础原理我们依然需要好好掌握。

文章分类
前端
文章标签