目錄
- 序言
- 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 聲明的類到底是一個什么樣的函數呢?我們可以通過在線工具 ES6 to ES5 來分析 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;
}
}
// 判斷 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 聲明私有字段時,約定是以字符 '#' 為開頭,轉化后則將標識符中的 '#' 替換為 '_',並且單獨用一個 WeakMap 類型的變量來替代類的私有字段,聲明在函數表達式后面(也會自動檢查命名沖突),這樣就保證了類的實例對象無法直接通過屬性訪問到私有字段(私有字段根本就沒有在實例對象的屬性中)。
對於公共字段,則是通過 _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 = privateMap.get(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];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
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();
對比一下第三組和第四組轉化后的代碼,可以明顯發現:
-
類的字段通過 _defineProperty 函數映射到實例對象(this)的屬性上。
-
類的方法則通過 _createClass 函數映射到構造函數的原型(Constructor.prototype)的屬性上,
-
類的靜態方也通過 _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"); }
// Object.create(proto, propertiesObject) 方法
// 創建一個新對象,使用 proto 來提供新創建的對象的__proto__
// 將 propertiesObject 的屬性添加到新創建對象的不可枚舉(默認)屬性(即其自身定義的屬性,而不是其原型鏈上的枚舉屬性)
subClass.prototype = Object.create(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.通過 Object.create
函數調用可知:
(1)subClass.prototype.__proto__ === superClass.prototype
,相當於實現了原型鏈的繼承
(2)subClass.prototype.constructor === 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 = Reflect.construct(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" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Date.prototype.toString.call(Reflect.construct(Date, [], function () {}));
return true;
} catch (e) {
return false;
}
}
// 獲取 o 對象的原型(__proto__)
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(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)的繼承的實現原理,並結合 深入理解JS中的對象(一):原型、原型鏈和構造函數 中所提到的構造函數的原型鏈關系,可得示例代碼的完整原型鏈關系如下圖:
5.參考