5
前言

工作中有很多用到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


CelesteW
11 声望4 粉丝

不想当产品经理的画师不是好程序员(ˉ▽ ̄~)