深入理解JS中的对象(三):class 的工作原理

class 是一个特殊的函数

class 的工作原理

class 继承原型链关系

参考


1.序言

ECMAScript 2015(ES6) 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法(class)不会为JavaScript引入新的面向对象的继承模型。


2.class 是一个特殊的函数

ES6 的 class 主要提供了更多方便的语法去创建老式的构造器函数。我们可以通过 typeof 得到其类型:

class People { constructor(name) { this.name = name; } } console.log(typeof People) // function

那 class 声明的类到底是一个什么样的函数呢?我们可以通过在线工具 来分析 class 背后真正的实现。


3.class 的工作原理

下面通过多组代码对比,来解析 class 声明的类将转化成什么样的函数。


第一组:用 class 声明一个空类

ES6的语法:

class People {}

这里提出两个问题:

class 声明的类与函数声明不一样,不会提升(即使用必须在声明之后),这是为什么?

console.log(People) // ReferenceError class People {}

在浏览器中运行报错,如下图:

ReferenceError


不能直接像函数调用一样调用类People(),必须通过 new 调用类,如 new People(),这又是为什么?

class People {} People() // TypeError

在浏览器中运行报错,如下图:

TypeError


转化为ES5:

"use strict"; function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } } // 判断 Constructor.prototype 是否出现在 instance 实例对象原型链上 function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var People = function People() { // 检查是否通过 new 调用 _classCallCheck(this, People); };

针对上面提到的两个问题,我们都可以用转化后的 ES5 代码来解答:

对于问题1,我们可以看到 class 声明的类转化为的是一个函数表达式,并且用变量 People 保存函数表达式的值,而函数表达式只能在代码执行阶段创建而且不存在于变量对象中,所以如果在 class 声明类之前使用,就相当于在给变量 People 赋值之前使用,此时使用是没有意义的,因为其值为 undefined,直接使用反而会报错。所以 ES6 就规定了在类声明之前访问类会抛出 ReferenceError 错误(类没有定义)。

对于问题2,我们可以看到 People 函数表达式中,执行了 _classCallCheck 函数,其作用就是保证 People 函数必须通过 new 调用。如果直接调用 People(),由于是严格模式下执行,此时的 this 为 undefined,调用 _instanceof 函数检查继承关系其返回值必然为 false,所以必然会抛出 TypeError 错误。

补充:类声明和类表达式的主体都执行在下。比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。


第二组:给类添加公共字段和私有字段

ES6的语法:

class People { #id = 1 // 私有字段,约定以单个的`#`字符为开头 name = 'Tom' // 公共字段 }

转化为ES5:

... // 将类的公共字段映射为实例对象的属性 function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var People = function People() { _classCallCheck(this, People); // 初始化私有字段 _id.set(this, { writable: true, value: 1 }); // 将类的公共字段映射为实例对象的属性 _defineProperty(this, "name", 'Tom'); }; // 转化后的私有字段(会自动检查命名冲突) var _id = new WeakMap();

对比转化前后的代码可以看出:

对于私有字段,在使用 class 声明私有字段时,约定是以字符 '#' 为开头,转化后则将标识符中的 '#' 替换为 '_',并且单独用一个 的变量来替代类的私有字段,声明在函数表达式后面(也会自动检查命名冲突),这样就保证了类的实例对象无法直接通过属性访问到私有字段(私有字段根本就没有在实例对象的属性中)。

对于公共字段,则是通过 _defineProperty 函数将类的公共字段映射为实例对象的属性,如果是首次设置,还会通过Object.defineProperty来进行初始化,设置属性的可枚举性(enumerable)、可配置性(configurable)、可写性(writable)


第三组:给类添加构造函数与实例属性

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwppyy.html