We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
ES6 里面的 class 和 extends 大家想必都会用了,但对于他们的实现是否了解呢,babel 后 class 和 extends 又是如何实现的呢? 这节课将带领你深入理解 babel 编译后 class 和 extends 的实现方式。
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 构造函数上。
static
Person 类上各个属性的关系是这样的:
你是不是很好奇,为什么在 Person 类上面设置的 sayHi 和 sayHello、sayBye 三个方法,编译后被放到了不同的地方处理?
sayHi
sayHello
sayBye
从编译后的代码中可以看到 sayHello 和 sayBye 被放到了 Person 构造函数中定义,而 sayHi 用 _createClass 来处理(_createClass 将 sayHi 添加到了 Person 的原型上面)。
_createClass
曾经我也以为是 sayHello 使用了箭头函数的缘故才让它最终被绑定到了构造函数里面,后来我看到 sayBye 这种用法才知道这和箭头函数无关。
实际上 class 中定义属性还有一种写法,这种写法和 sayBye 如出一辙,在 babel 编译后会将其属性放到构造函数中,而非原型上面。
class Person { name = 'tom'; age = 23; } // 等价于 class Person { constructor() { this.name = 'tom'; this.age = 23; } }
如果我们将 name 后面的 'tom' 换成函数呢?甚至箭头函数呢?这不就是 sayBye 和 sayHello 了吗?
name
'tom'
因此,在 class 中不直接使用 = 来定义的方法,最终都会被挂载到原型上,使用 = 定义的属性和方法,最终都会被放到构造函数中。
=
Person 构造函数中调用了 _classCallCheck 函数,并将 this 和自身传入进去。 在 _classCallCheck 中通过 instanceof 来进行判断,instance 是否在 Constructor 的原型链上面,如果不在上面则抛出错误。这一步主要是为了避免直接将 Person 类当做函数来调用。 因此,在ES5中构造函数是可以当做普通函数来调用的,但在ES6中的类是无法直接当普通函数来调用的。
Person
_classCallCheck
this
instanceof
instance
Constructor
注意:为什么通过 instanceof 可以判断是否将 Person 类当函数来调用呢? 因为如果使用 new 操作符实例化 Person 的时候,那么 instance 就是当前的实例,指向 Person.prototype,instance instanceof Constructor 必然为true。反之,直接调用 Person 构造函数,那么 instance 就不会指向 Person.prototype。
new
Person.prototype
instance instanceof Constructor
我们再来看 _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 函数来给设置当前对象的属性描述符。
staticProps
protoProps
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 // 省略 _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实现的。 为了方便理解,我这里整理了一下原型链的关系:
_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,我们就重点分析这两步。
Child
age
_possibleConstructorReturn
_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
subClass.prototype
[[Prototype]]
superClass.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;而如果返回的是引用类型,那么最终实例化后的实例依然是这个引用类型(仅相当于对这个引用类型进行了扩展)。
Object.getPrototypeOf(Child).call(this, name, age))
Parent
Parent.call(this, name, age)
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。
self
super
call
注意: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 中的这些基础原理我们依然需要好好掌握。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
ES6 里面的 class 和 extends 大家想必都会用了,但对于他们的实现是否了解呢,babel 后
class
和extends
又是如何实现的呢?这节课将带领你深入理解 babel 编译后
class
和extends
的实现方式。准备工作
在开始之前,我们需要一个 babel 的环境,方便查看 babel 后的代码,这里我推荐两种方式。
本文将以 ScratchJS 转换后的代码为例进行代码分析。
1. class 实现
先从最简单的
class
开始看,下面这段代码涵盖了使用class
时所有会出现的情况(静态属性、构造函数、箭头函数)。而经过 babel 处理后的代码是这样的:
最外层的 Person 变量被赋值给了一个立即执行函数,立即执行函数里面返回的是里面的 Person 构造函数,实际上最外层的 Person 就是里面的 Person 构造函数。
在 Person 类上用
static
设置的静态属性instance,在这里也被直接挂载到了 Person 构造函数上。1.1 挂载属性方法
Person 类上各个属性的关系是这样的:
你是不是很好奇,为什么在 Person 类上面设置的
sayHi
和sayHello
、sayBye
三个方法,编译后被放到了不同的地方处理?从编译后的代码中可以看到
sayHello
和sayBye
被放到了 Person 构造函数中定义,而sayHi
用_createClass
来处理(_createClass
将sayHi
添加到了 Person 的原型上面)。曾经我也以为是
sayHello
使用了箭头函数的缘故才让它最终被绑定到了构造函数里面,后来我看到sayBye
这种用法才知道这和箭头函数无关。实际上
class
中定义属性还有一种写法,这种写法和sayBye
如出一辙,在 babel 编译后会将其属性放到构造函数中,而非原型上面。如果我们将
name
后面的'tom'
换成函数呢?甚至箭头函数呢?这不就是sayBye
和sayHello
了吗?因此,在
class
中不直接使用=
来定义的方法,最终都会被挂载到原型上,使用=
定义的属性和方法,最终都会被放到构造函数中。1.2 _classCallCheck
Person
构造函数中调用了_classCallCheck
函数,并将this
和自身传入进去。在
_classCallCheck
中通过instanceof
来进行判断,instance
是否在Constructor
的原型链上面,如果不在上面则抛出错误。这一步主要是为了避免直接将 Person 类当做函数来调用。因此,在ES5中构造函数是可以当做普通函数来调用的,但在ES6中的类是无法直接当普通函数来调用的。
1.3 _createClass
我们再来看
_createClass
函数,这个函数在 Person 原型上面添加了sayHi
方法。_createClass
函数接收三个参数,分别是Constructor
(构造函数)、protoProps(需要挂载到原型上的方法)、staticProps(需要挂载到类上的静态方法)。在接收到参数之后,
_createClass
会进行判断如果有staticProps
,则挂载到Constructor
构造函数上;如果有protoProps
,那么挂载到Constructor
原型上面。这里的挂载函数
defineProperties
是关键,它对传入的props
进行了遍历,并设置了其enumerable
(是否可枚举) 和configurable
(是否可配置)、writable
(是否可修改)等数据属性。最后使用了
Object.defineProperty
函数来给设置当前对象的属性描述符。2. extends 实现
通过上文对
Person
的分析,相信你已经知道了 ES6 中类的实现,这与ES5中的实现大同小异,接下来我们来具体看一下extends
的实现。以下面的 ES6 代码为例:
babel后的代码则是这样的:
我们可以清楚地看到,继承是通过
_inherits
实现的。为了方便理解,我这里整理了一下原型链的关系:
除去一些无关紧要的代码,最终的核心实现代码就只有这么多:
和前面的
Person
类实现有所不同的地方是,在Child
方法中增加调用了_inherits
,还有在设置name
和age
属性的时候,使用的是执行_possibleConstructorReturn
后返回的_this
,而非自身的this
,我们就重点分析这两步。2.1 _inherits
先来看_inherits函数的实现代码:
_inherits
函数接收两个参数,分别是subClass
(子构造函数)和subClass
(父构造函数),将这个函数做的事情稍微做一下梳理。subClass.prototype
的[[Prototype]]
指向superClass.prototype
的[[Prototype]]
subClass
的[[Prototype]]
指向superClass
在《深入理解类和继承》一文中,曾经提到过 ES5 中的寄生组合式继承,
extends
的实现与寄生组合式继承实则大同小异,仅仅只增加了第二步操作。2.2 _possibleConstructorReturn
在
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
;而如果返回的是引用类型,那么最终实例化后的实例依然是这个引用类型(仅相当于对这个引用类型进行了扩展)。如果没有
self
,这里就会直接抛出错误,提示super
函数还没有被调用。最后会对
call
进行判断,如果call
为引用类型,那么返回call
,否则返回self
。在
Child
方法中,最终拿到_possibleConstructorReturn
执行后的结果作为新的this
来设置构造函数里面的属性。总结
ES6 中提供的
class
和extends
本质上只是语法糖,底层的实现原理依然是构造函数和寄生组合式继承。所以对于一个合格的前端工程师来说,即使 ES6 已经到来,对于 ES5 中的这些基础原理我们依然需要好好掌握。
The text was updated successfully, but these errors were encountered: