乐趣区

利用babel彻底搞懂es6的Class类一

前言

工作中有很多用到 Class 类的地方,通常是暴露出 class 类,或者暴露 Class 类的实例,比如商品有价格、规格、名称等属性,就可以通过 new 一个 Class 类来初始化商品对象。

对 es6 有了解的朋友应该知道 es6 是 es5 的语法糖,Class 类实际上是基于 es5 的构造函数实现的生成实例对象的方法,但是 Class 类的写法更优雅,更趋近于传统的面向对象编程。

虽说平时常用 Class 类,可是也常会忽略掉细节而导致问题,出于好奇和为了彻底理解 Class 类,我利用 babel 工具降级 es6 语法,看看 Class 类的“庐山真面目”,以及理解 babel 到底做了什么,写文记录以供自己复习。

一个 Class 类 babel 前后

定义一个 Class 类

class K {constructor(name) {this.name = name;}
    // 静态方法
    static classMethod() {this.getname()
        return 'hello';
    }
    // setter
    set prop(value) {console.log('setter:'+ value);
    }
    // getter
    get prop() {return 'getter';}
    // 原型方法
    getName() {return "celeste";}
}

let k = new K("celeste")

上面的类利用 babel 降级语法后代码如下:

var K = function () {function K(name) {_classCallCheck(this, K);
    
    this.name = name;
  }

  _createClass(K, [{
    key: 'getName',
    value: function getname() {return "celeste";}
  }, {
    key: 'prop',
    set: function set(value) {console.log('setter:' + value);
    },
    get: function get() {return 'getter';}
  }], [{
    key: 'classMethod',
    value: function classMethod() {this.getName();
      return 'hello';
    }
  }]);

  return K;
}();

var k = new K("celeste");

可以发现 Class 类本质上是个自执行函数。这个函数执行完毕返回一个构造函数 K。
并且,这里定义函数不是用函数声明的形式,而是用变量声明赋值 var K这其实就是 class 类不存在变量提升的原因,因为虽然 js 函数会先扫描整个函数体语句,将所有声明的变量提升到函数的顶部,但是不会提升赋值,在 console 前变量 K 还未赋值所以打印结果是 undefined。

// 变量赋值
console.log(Bb); // undefined
var Bb = function Bb () {};
// 函数声明
console.log(Aa); // ƒ Aa () {}
function Aa () {};
_classCallCheck、_createClass 函数

看完外层,再看看里面的关键信息,主要看_classCallCheck、_createClass 他们做了什么,源码如下:

"use strict";
// 为了向前兼容,es6 语法实际上是严格模式的
// 类 or 模块中只有严格模式可用

// 判断 right 是否为 left 的构造函数
function _instanceof(left, right) {if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {return !!right[Symbol.hasInstance](left); 
    } else {return left instanceof right;} 
}

// 判断 Constructor 是否 instance 的构造函数,如果不是则抛出错误
function _classCallCheck(instance, Constructor) {if (!_instanceof(instance, Constructor)) {throw new TypeError("Cannot call a class as a function"); 
    } 
}

// 遍历 props,设置 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; 
        // descriptor 有 value 属性的话(即除了 set/get 外的原型方法),可赋值
        if ("value" in descriptor) descriptor.writable = true; 
        // 将变量 descriptor.key 定义到 target 上
        Object.defineProperty(target, descriptor.key, descriptor); 
    } 
}

// 参数分别是:构造函数、原型方法、静态方法
function _createClass(Constructor, protoProps, staticProps) { 
    // 原型方法挂载到构造函数的原型上
    if (protoProps) _defineProperties(Constructor.prototype, protoProps); 
    // 静态方法 (用了 static 关键字定义的函数) 会作为第三个参数数组里的项传进来,会直接成为构造函数下的一个属性
    if (staticProps) _defineProperties(Constructor, staticProps); 
    return Constructor; 
}

所以 constructor 事实上就是初始化了一个构造函数:


function K(name) {_classCallCheck(this, K);
    
    this.name = name;
}

_classCallCheck(this, K)的作用就是判断 K 是否为 this 的构造函数,不是的话抛出错误,确保万无一失(依据是如果 K 是个构造函数那么 this 一定是指向 K 的实例对象的)。

而_createClass 函数的作用是就是将定义在类里的方法挂载到函数的原型(针对原型方法)或者类本身(针对 static 静态方法)上:

把_createClass 函数与函数调用直观地放在一起看:// _createClass 函数
function _createClass(Constructor, protoProps, staticProps) {if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps); 
    return Constructor; 
}
  // 语法降级后自执行函数里的函数执行
  _createClass(K, [{
    key: 'getName',
    value: function getname() {return "celeste";}
  }, {
    key: 'prop',
    set: function set(value) {console.log('setter:' + value);
    },
    get: function get() {return 'getter';}
  }], [{
    key: 'classMethod',
    value: function classMethod() {this.getName();
      return 'hello';
    }
  }]);

可以看到,setter、getter 也在第二个参数数组里,他们也是原型上的方法,传参时有些许不同,value —— set/get,是为了在挂载到原型上的时候加以区分的,把他们区分开的代码就是_defineProperties 函数里的这句话:

if ("value" in descriptor) descriptor.writable = true;
一些结论:

1.类的所有方法都定义在类的 prototype 属性上面

所以类的新方法可以利用 `Object.assign` 添加在 `prototype` 对象上面
Object.assign(Person.prototype, {// add some functions ...});

2.类的内部所有定义的方法,都是不可枚举的(non-enumerable)
3.js 引擎会自动为空的类添加一个空的 constructor 方法(事实上就是会默认创建一个构造函数,将构造函数的 this 指向类的实例)
4.constructor函数可以 return Object.create(null)返回一个全新的对象,可导致实例对象不是类的实例。

下一节再看剩余的问题啦,这两天总结完毕或许会合并成一篇也可能新开一篇 …——2020/06/06 01:20

小贴士:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

babel 在线工具 https://babeljs.io/repl

退出移动版