前言
ES2015+ 有各种新特性 (语法糖), 尽管有很多特性尚未纳入标准或浏览器还没有原生支持, 但是 Babel 的出现让前端程序员可以不用担心兼容性问题而使用处于各种 stage 的 ES2015+ 语法. 其实 class 关键字目前只是实现类的语法糖, 但是可以帮助我们屏蔽掉每次实现类时的样本代码, 逻辑更加清晰, 并且阻止我们踩可能存在的坑, 本篇文章从 ES5 的类实现到 ES6 中 class 的 Babel 转码来分析类的实现与继承.
类
在 JavaScript 中, 我们希望一个类能有以下特性:
实例的属性是绑定在每个实例上的
方法是绑定在这个类的 prototype 上的
对实例进行
Object.getPrototypeOf
能拿到 constructor 的 prototype
实例 instanceof 构造函数返回 true
类的静态属性 / 方法
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 入门 http://es6.ruanyifeng.com/#docs/class 中有解释, 这里直接贴一下:
类和模块的内部, 默认就是严格模式, 所以不需要使用 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;
- }
这个函数完成了三个重要的任务:
子类的原型继承父类的原型
子类原型的构造函数指向子类
子类继承父类
至此, 子类原型已近能够访问父类原型的方法了, 子类也能够访问父类的静态方法.
再来看
- _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 的说明 https://babeljs.io/docs/usage/caveats/ :
Built-in classes such as Date, Array, DOM etc cannot be properly subclassed due to limitations in ES5 (for the https://babeljs.io/docs/plugins/transform-es2015-classes plugin). You can try to use https://github.com/loganfsmyth/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 指向 Parent,super 中的 this 指向 Child 类的实例, 所以相当于 Parent.call(this)
在子类方法中, super 指向 Parent.prototype,super 中的 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 崩溃了把我保存的内容都吞了是真的坑, 在心态崩了的情况下再重写一次真是磨练心智
来源: https://juejin.im/entry/5adc804bf265da0b981b0ac9