@gyyin
2019-10-07T16:10:29.000000Z
字数 7639
阅读 556
慕课专栏
前面我们已经深入讲解过 JavaScript 中的类与继承,也举了不少 ES5 中实现的例子,那么 ES6 中 class 和 extends 又是如何实现的呢?
这节课将带领你深入理解 babel 编译后 class 和 extends 的实现方式。
注意:本文涉及到 立即执行函数( IIFE )、instanceof、Object.defineProperty,如果还未接触过,建议先点击链接学习。
在开始之前,我们需要一个 babel 的环境,方便查看 babel 后的代码,这里我推荐两种方式。
本文将以 ScratchJS 转换后的代码为例进行代码分析。
先从最简单的 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 构造函数上。
Person 类上各个属性的关系是这样的:

你是不是很好奇,为什么在 Person 类上面设置的 sayHi 和 sayHello、sayBye 三个方法,编译后被放到了不同的地方处理?
从编译后的代码中可以看到 sayHello 和 sayBye 被放到了 Person 构造函数中定义,而 sayHi 用 _createClass 来处理(_createClass 将 sayHi 添加到了 Person 的原型上面)。
曾经我也以为是 sayHello 使用了箭头函数的缘故才让它最终被绑定到了构造函数里面,后来我看到 sayBye 这种用法才知道这和箭头函数无关。
实际上 class 中定义属性还有一种写法,这种写法和 sayBye 如出一辙,在 babel 编译后会将其属性放到构造函数中,而非原型上面。
class Person {name = 'tom';age = 23;}// 等价于class Person {constructor() {this.name = 'tom';this.age = 23;}}
如果我们将 name 后面的 'tom' 换成函数呢?甚至箭头函数呢?这不就是 sayBye 和 sayHello 了吗?
因此,在 class 中不直接使用 = 来定义的方法,最终都会被挂载到原型上,使用 = 定义的属性和方法,最终都会被放到构造函数中。
Person 构造函数中调用了 _classCallCheck 函数,并将 this 和自身传入进去。
在 _classCallCheck 中通过 instanceof 来进行判断,instance 是否在 Constructor 的原型链上面,如果不在上面则抛出错误。这一步主要是为了避免直接将 Person 类当做函数来调用。
因此,在ES5中构造函数是可以当做普通函数来调用的,但在ES6中的类是无法直接当普通函数来调用的。
注意:为什么通过
instanceof可以判断是否将 Person 类当函数来调用呢?
因为如果使用new操作符实例化 Person 的时候,那么instance就是当前的实例,指向Person.prototype,instance instanceof Constructor必然为true。反之,直接调用 Person 构造函数,那么 instance 就不会指向Person.prototype。
我们再来看 _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);}}// 这个才是“真正的”_createClassreturn 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 函数来给设置当前对象的属性描述符。
通过上文对 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// 省略 _classCallCheckfunction _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实现的。
为了方便理解,我这里整理了一下原型链的关系:

除去一些无关紧要的代码,最终的核心实现代码就只有这么多:
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,还有在设置 name 和 age 属性的时候,使用的是执行 _possibleConstructorReturn 后返回的 _this,而非自身的 this,我们就重点分析这两步。
先来看_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 (父构造函数),将这个函数做的事情稍微做一下梳理。
subClass.prototype 的 [[Prototype]]指向 superClass.prototype 的 [[Prototype]]subClass 的 [[Prototype]] 指向 superClass在《深入理解类和继承》一文中,曾经提到过 ES5 中的寄生组合式继承,extends 的实现与寄生组合式继承实则大同小异,仅仅只增加了第二步操作。
在 Child 中调用了 _possibleConstructorReturn 函数,将 this 和 Object.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 中提供的 class 和 extends 本质上只是语法糖,底层的实现原理依然是构造函数和寄生组合式继承。
所以对于一个合格的前端工程师来说,即使 ES6 已经到来,对于 ES5 中的这些基础原理我们依然需要好好掌握。