1. 程式人生 > >深入理解JS中的物件(三):class 的工作原理

深入理解JS中的物件(三):class 的工作原理

**目錄** - 序言 - class 是一個特殊的函式 - class 的工作原理 - class 繼承的原型鏈關係 - 參考
**1.序言** ECMAScript 2015(ES6) 中引入的 JavaScript 類實質上是 JavaScript 現有的基於原型的繼承的語法糖。類語法(class)不會為JavaScript引入新的面向物件的繼承模型。
**2.class 是一個特殊的函式** ES6 的 class 主要提供了更多方便的語法去建立老式的構造器函式。我們可以通過 typeof 得到其型別: ```js class People { constructor(name) { this.name = name; } } console.log(typeof People) // function ``` 那 class 宣告的類到底是一個什麼樣的函式呢?我們可以通過線上工具 [ES6 to ES5](https://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=false&presets=es2015,react,stage-2&targets=&browsers=&builtIns=false&debug=false&code=) 來分析 class 背後真正的實現。
**3.class 的工作原理** 下面通過多組程式碼對比,來解析 class 宣告的類將轉化成什麼樣的函式。
**第一組:用 class 宣告一個空類** ES6的語法: ```js class People {} ``` 這裡提出兩個問題: 1.class 宣告的類與函式宣告不一樣,不會提升(即使用必須在宣告之後),這是為什麼? ```js console.log(People) // ReferenceError class People {} ``` 在瀏覽器中執行報錯,如下圖: ![ReferenceError](https://img2020.cnblogs.com/blog/898684/202005/898684-20200518212636472-719333501.png)
2.不能直接像函式呼叫一樣呼叫類`People()`,必須通過 new 呼叫類,如 `new People()`,這又是為什麼? ```js class People {} People() // TypeError ``` 在瀏覽器中執行報錯,如下圖: ![TypeError](https://img2020.cnblogs.com/blog/898684/202005/898684-20200518212718492-1302314374.png)
轉化為ES5: ```js "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 儲存函式表示式的值,而函式表示式只能在程式碼執行階段建立而且不存在於[變數物件](https://www.cnblogs.com/TomXu/archive/2012/01/16/2309728.html)中,所以如果在 class 宣告類之前使用,就相當於在給變數 People 賦值之前使用,此時使用是沒有意義的,因為其值為 undefined,直接使用反而會報錯。所以 ES6 就規定了在類宣告之前訪問類會丟擲 ReferenceError 錯誤(類沒有定義)。 對於問題2,我們可以看到 People 函式表示式中,執行了 _classCallCheck 函式,其作用就是保證 People 函式必須通過 new 呼叫。如果直接呼叫 People(),由於是嚴格模式下執行,此時的 this 為 undefined,呼叫 _instanceof 函式檢查繼承關係其返回值必然為 false,所以必然會丟擲 TypeError 錯誤。 補充:類宣告和類表示式的主體都執行在[嚴格模式](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode)下。比如,建構函式,靜態方法,原型方法,getter和setter都在嚴格模式下執行。
**第二組:給類新增公共欄位和私有欄位** ES6的語法: ```js class People { #id = 1 // 私有欄位,約定以單個的`#`字元為開頭 name = 'Tom' // 公共欄位 } ``` 轉化為ES5: ```js ... // 將類的公共欄位對映為例項物件的屬性 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 型別](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)的變數來替代類的私有欄位,宣告在函式表示式後面(也會自動檢查命名衝突),這樣就保證了類的例項物件無法直接通過屬性訪問到私有欄位(私有欄位根本就沒有在例項物件的屬性中)。 對於公共欄位,則是通過 _defineProperty 函式將類的公共欄位對映為例項物件的屬性,如果是首次設定,還會通過 Object.defineProperty 函式來進行初始化,設定屬性的可列舉性(enumerable)、可配置性(configurable)、可寫性(writable)
**第三組:給類新增建構函式與例項屬性** ES6的語法: ```js class People { #id = 1 // 私有欄位,約定以單個的`#`字元為開頭 name = 'Tom' // 公共欄位 constructor(id, name, age) { this.#id = id this.name = name this.age = age // 例項屬性 age } } ``` 轉化為ES5: ```js ... // 設定(修改)類的私有欄位 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的語法: ```js 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: ```js ... // 設定物件的屬性 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(); ``` 對比一下第三組和第四組轉化後的程式碼,可以明顯發現: 1. 類的欄位通過 _defineProperty 函式對映到例項物件(this)的屬性上。 2. 類的方法則通過 _createClass 函式對映到建構函式的原型(Constructor.prototype)的屬性上, 3. 類的靜態方也通過 _createClass 函式對映到建構函式(Constructor)的屬性上。
**第五組:類的繼承** ES6的語法: ```js // 父類(superClass) class People {} // 子類(subClass)繼承父類 class Man extends People {} ``` 轉化為ES5: ```js ... 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 函式中,實現了原型鏈和靜態屬性的繼承: ```js // 實現繼承關係 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 上的屬性: ```js // 獲得父類的建構函式 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 繼承的原型鏈關係** 例項程式碼: ```js 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中的物件(一):原型、原型鏈和建構函式](https://www.cnblogs.com/forcheng/p/12866827.html) 中所提到的建構函式的原型鏈關係,可得示例程式碼的完整原型鏈關係如下圖: ![class 繼承的原型鏈關係](https://img2020.cnblogs.com/blog/898684/202005/898684-20200518212739112-1567747734.png)
**5.參考** [類- JavaScript | MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes) [exploring-es6 - class](https://es6-org.github.io/exploring-es6/#./15.md) [為什麼說ES6的class是語法糖?](https://juejin.im/post/5c820d0e6fb9a04a0c2f3e12) [深入理解JavaScript系列(15):函式(Functions)](https://www.cnblogs.com/TomXu/archive/2012/01/30/2326372.html) [class繼承做了什麼呢?](https://juejin.im/post/5cea3379f265da1b8466c163)