原来 class 语法糖也没这么难嘛

1,308 阅读3分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

ECMAScript6 实现了 class ,实际上它是一个语法糖,但是它的出现能使 JS 编码更清晰,更接近 面向对象编程

实现原理

首先我们来看 ES6class 的实现和 ES5 构造函数的实现,两者相比较不难看出 constructor 其实就是构造方法,指向 ES5 的构造函数,那么 class 本身指向的是构造函数,换言之底层依旧是构造函数。

ES6

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  static run() {
    console.log("run");
  }
  say() {
    console.log("hello!");
  }
}

ES5

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.say = function () {
  console.log("hello!");
};

Person.run = function () {
  console.log("run");
};

babel 编译分析

通过 babel 编译器将 ES6 代码 转换成 ES5 代码之后(代码转换可以试用 babel 官方在线工具),可得到这两个关键函数 _defineProperties_createClass,现在我们来一一解析说明。

...
var Person = /*#__PURE__*/ (function () {
  "use strict";

  function Person(name, age) {
    _classCallCheck(this, Person);

    this.name = name;
    this.age = age;
  }

  _createClass(
    Person,
    [
      {
        key: "say",
        value: function say() {
          console.log("hello!");
        },
      },
    ],
    [
      {
        key: "run",
        value: function run() {
          console.log("run");
        },
      },
    ]
  );

  return Person;
})();

_createClass

_createClass 函数主要用于配置构造函数或构造函数原型上的公有函数和静态方法,并返回构造函数本身。

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

_defineProperties

_defineProperties 函数主要用于声明公有函数和静态方法的描述符,并将其挂载到当前的构造函数或构造函数原型。它接收两个参数 target() 和 props

  • target 指向当前的构造函数或构造函数原型
  • props 数组类型,指向公有函数和静态方法

在遍历数组时,我们可以看到 enumerable 默认是 false,也就是说 class 类上的内部属性默认是不可枚举的,不能使用 Object.keys 遍历,具体如下:

Object.keys(Person.prototype); // []
Object.keys(Person); // []

同时在遍历的时候还会判断当前描述符是否存在 value 值,如果存在就设置可写属性 writabletrue,反之就使用 getset 属性。在遍历的末尾,通过 Object.defineProperty 将描述符配置到当前的构造函数或构造函数原型上,至此就是 class 的基本实现了。

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);
  }
}

构造函数的区别

暂时性死区

class 不会声明提升,存在 暂时性死区,构造函数的本质是函数,函数声明会有提升作用。

// 👌
{
  const foo = new Foo();
  function Foo() {}
}

// error
{
  const foo = new Foo(); // Cannot access 'Foo' before initialization
  class Foo {}
}

严格模式

class 声明内部默认会启用 严格模式,构造函数默认 非严格模式

// 👌
{
  const foo = new Foo();
  function Foo() {
    size = 20;
  }
}

// error
{
  class Foo {
    constructor() {
      size = 20; //  size is not defined
    }
  }
  const foo = new Foo();
}

内部方法不可枚举

class 的所有方法(包括 静态方法实例方法)都 不可枚举,上文的 _defineProperties 函数方法实现中有提到,具体可参照上文,构造函数可枚举所有方法。

{
  function Foo() {}

  Foo.print = function () {};
  Foo.prototype.format = function () {};

  console.log(Object.keys(Foo)); //  [ "print" ]
  console.log(Object.keys(Foo.prototype)); //  [ "format" ]
}

{
  class Foo {
    constructor() {}

    static print() {}

    format() {}
  }
  console.log(Object.keys(Foo)); //  []
  console.log(Object.keys(Foo.prototype)); //  []
}

原型对象 prototype

class 的所有方法(包括 静态方法实例方法)都没有原型对象 prototype,因此也没有 [[construct]],不能通过 new 来调用,构造函数则支持 new 调用。

// 👌
{
  function Foo() {}
  Foo.prototype.format = function () {};

  const foo = new Foo();
  const fooFormat = new foo.format();
}

// error
{
  class Foo {
    static print() {}
    format() {}
  }

  const foo = new Foo();
  const fooFormat = new foo.format(); // foo.format is not a constructor
  const fooPrint = new foo.print(); // foo.print is not a constructor
}

new 调用

class 必须使用 new 调用,构造函数的本质是函数,支持直接调用。

// 👌
{
  function Foo() {}

  const foo = Foo();
}

// error
{
  class Foo {}

  const foo = Foo(); //  Class constructor Foo cannot be invoked without 'new'
}

类名重写

class 内部无法重写类名,构造函数可任意更改。

// 👌
{
  function Foo() {
    Foo = "yo";
  }
  const foo = new Foo();
}

// error
{
  class Foo {
    constructor() {
      // Foo = 'yo' // TypeError: Assignment to constant variable
    }
  }

  const foo = new Foo();
  Foo = "yo"; // 👌
}