深⼊理解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 {}
这⾥提出两个问题:
1.class 声明的类与函数声明不⼀样,不会提升(即使⽤必须在声明之后),这是为什么?
console.log(People) // ReferenceError
class People {}
在浏览器中运⾏报错,如下图:
2.不能直接像函数调⽤⼀样调⽤类People(),必须通过 new 调⽤类,如new People(),这⼜是为什么?
class People {}
People() // 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;
}
}js arguments
// 判断 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)。
第三组:给类添加构造函数与实例属性
ES6的语法:
class People {
#id = 1      // 私有字段,约定以单个的`#`字符为开头
name = 'Tom' // 公共字段
constructor(id, name, age) {
this.#id = id
this.name = name
this.age =  age // 实例属性 age
}
}
转化为ES5:
...
// 设置(修改)类的私有字段
function _classPrivateFieldSet(receiver, privateMap, value) {
var descriptor = (receiver);
if (!descriptor) {
throw new TypeError("attempted to set private field on non-instance");
}
if (descriptor.set) {
descriptor.set.call(receiver, value);
} else {
if (!descriptor.writable) {
throw new TypeError("attempted to set read only private field");
}
descriptor.value = value;
}
return value;
}
var People = function People(id, name, age) {
_classCallCheck(this, People);
_id.set(this, {
writable: true,
value: 1
});
_defineProperty(this, "name", 'Tom');
// constructor 从这开始执⾏
_classPrivateFieldSet(this, _id, id);
this.name = name;
this.age = age;
};
var _id = new WeakMap();
对⽐转化前后的代码可以看出:
类的构造函数(constructor)⾥⾯的代码的执⾏时机是在字段定义(字段映射为实例对象的属性)之后。⽽对私有字段的赋值(修改)是专门通过 _classPrivateFieldSet 函数来实现的。
第四组:给类添加原型⽅法和静态⽅法
ES6的语法:
class People {
#id = 1
name = 'Tom'
constructor(id, name, age) {
this.#id = id
this.name = name
this.age =  age
}
// 原型⽅法
getName() { return this.name }
// 静态⽅法
static sayHello() { console.log('hello') }
}
转化为ES5:
...
// 设置对象的属性
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
// 将类的⽅法映射到构造函数的原型(Constructor.prototype)的属性上
// 将类的静态⽅法映射到构造函数(Constructor)的属性上
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var People = function () {
function People(id, name, age) {
// ...
}
// 设置类的⽅法和静态⽅法
_createClass(People, [{
key: "getName",
value: function getName() {
return this.name;
}
}], [{
key: "sayHello",
value: function sayHello() {
console.log('hello');
}
}]);
return People;
}();
var _id = new WeakMap();
对⽐⼀下第三组和第四组转化后的代码,可以明显发现:
1. 类的字段通过 _defineProperty 函数映射到实例对象(this)的属性上。
2. 类的⽅法则通过 _createClass 函数映射到构造函数的原型(Constructor.prototype)的属性上,
3. 类的静态⽅也通过 _createClass 函数映射到构造函数(Constructor)的属性上。
第五组:类的继承
ES6的语法:
// ⽗类(superClass)
class People {}
// ⼦类(subClass)继承⽗类
class Man extends People {}
转化为ES5:
.
..
var People = function People() {
_classCallCheck(this, People);
};
var Man = function (_People) {
// Man 继承 _People
_inherits(Man, _People);
// 获取 Man 的⽗类的构造函数
var _super = _createSuper(Man);
function Man() {
_classCallCheck(this, Man);
/
/ 实现了⽗类构造函数的调⽤, ⼦类的 this 继承⽗类的 this 上的属性
return _super.apply(this, arguments);
}
return Man;
}(People);
在 _inherits 函数中,实现了原型链和静态属性的继承:
// 实现继承关系
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); }
// ate(proto, propertiesObject) ⽅法
// 创建⼀个新对象,使⽤ proto 来提供新创建的对象的__proto__
// 将 propertiesObject 的属性添加到新创建对象的不可枚举(默认)属性(即其⾃⾝定义的属性,⽽不是其原型链上的枚举属性)
subClass.prototype = ate(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } });
if (superClass) _setPrototypeOf(subClass, superClass);
}
// 设置对象 o 的原型(即 __proto__ 属性)为 p
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; };
return _setPrototypeOf(o, p);
}
1.通过ate函数调⽤可知:
(1)subClass.prototype.__proto__ === superClass.prototype,相当于实现了原型链的继承
(2)structor === subClass,表明 subClass 构造函数的显⽰原型对象(prototype)的 constructor 属性指向原构造函数2.通过调⽤_setPrototypeOf(subClass, superClass)可知:
(1)subClass.__proto__ === superClass,相当于实现了静态属性的继承
在 Man 构造函数中,通过调⽤其⽗类的构造函数(_super),实现了⼦类的 this 继承⽗类的 this 上的属性:
// 获得⽗类的构造函数
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function () {
var Super = _getPrototypeOf(Derived), result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = struct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
// 判断 call 的类型,返回合适的 Constructor
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; }
return _assertThisInitialized(self);
}
// 断⾔ selft 是否初始化
function _assertThisInitialized(self) {
if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); }
return self;
}
// 判断是否能否使⽤ Reflect
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !struct) return false;
if (struct.sham) return false;
if (typeof Proxy === "function") return true;
try {
String.struct(Date, [], function () {}));
return true;
} catch (e) {
return false;
}
}
// 获取 o 对象的原型(__proto__)
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? PrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || PrototypeOf(o); };
return _getPrototypeOf(o);
}
从上述可知 class 继承的实现主要包含三部分:
原型链的继承
静态属性的继承
通过调⽤⽗类的构造函数,获得⽗类的构造函数 this 上的属性
4.class 继承的原型链关系
实例代码:
class People {
constructor(name) {
this.name = name
}
}
class Man extends People {
constructor(name, sex) {
super(name)
this.sex = sex
}
}
var man = new Man('Tom', 'M')
根据上⾯分析所知道的类(class)的继承的实现原理,并结合中所提到的构造函数的原型链关系,可得⽰例代码的完整原型链关系如下图:
5.参考

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。