基礎知識
嚴格意義上來講,在Js
中是沒有類這一概念的。
我們可以運用前面章節提到的構造函數來模擬出類這一概念,並且可以通過原型對象的繼承來完美的實現實例對象方法復用。
但是這樣十分的麻煩,我們需要將實例對象需要用到的公共方法來存放到構造函數的原型對象中,而使用class
語法糖整個過程就變得非常簡單。
聲明定義
使用class
來定義類,類中定義的函數稱為方法,不需要用關鍵字function
也不需要用逗號進行分割。
<script>"use strict"; class User { f1() { console.log("運行了f1..."); } // 不需要逗號分隔 f2() { console.log("運行了f2..."); } } let u1 = new User(); u1.f1(); // 方法調用 u1.f2(); // 方法調用 </script>
構造函數
使用 constructor
構造函數傳遞參數,該函數會在new
時自動執行。
<script>"use strict"; class User { constructor(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } show(){ console.log(`姓名:${this.name},年齡:${this.age},性別:${this.gender}`); } } let u1 = new User("雲崖",18,"男"); u1.show(); // 執行方法 </script>
構造函數不是必須定義的,當沒有定義構造函數時。
它會自動去查找原型鏈,相當於如下代碼所示。
constructor(...args) {
super(...args);
}
原理分析
不管是使用構造函數還是class
語法糖,其原理都是一樣的。
但是構造函數中的方法應該存放進原型對象,這一步需要我們手動去做,使用class
語法糖的結構后則會自動將方法放入原型對象。
<script>"use strict"; class U1 { constructor(name) { this.name = name; } show() { console.log("U1 show"); } } console.log(typeof U1); // function class只是語法糖,內部還是函數。 </script>
<script>"use strict"; class U1 { constructor(name) { this.name = name; } show() { console.log("U1 show"); } } let u1 = new U1("class語法糖"); console.dir(u1); // ============ 兩種操作一模一樣 class自動將方法寫入原型對象中 function U2(name) { this.name = name; } U2.prototype.show = function () { console.log("U2 show"); } let u2 = new U2("構造函數"); console.dir(u2); </script>
遍歷差異
雖說class
定義的類歸根結底還是函數,但是與我們手動創建構造函數還是有一些優化措施的。
class
中定義的方法不能被遍歷出來,而構造函數原型對象中的方法卻可以被遍歷出來。
<script>"use strict"; class U1 { constructor(name) { this.name = name; } show() { console.log("U1 show"); } } let u1 = new U1("class語法糖"); for (let key in u1) { console.log(key); // name Ps:show不會被遍歷出來 } // ============ 兩種操作一模一樣 class自動將方法寫入原型對象中 function U2(name) { this.name = name; } U2.prototype.show = function () { console.log("U2 show"); } let u2 = new U2("構造函數"); for (let key in u2) { console.log(key); // name show } </script>
嚴格模式
在class
中, 默認使用strict
嚴格模式執行
<script>// "use strict"; 取消嚴格模式 class U1 { constructor(name) { this.name = name; } show() { (function () { console.log(this); // class中用嚴格模式執行代碼,所以this指向為undefined }()) } } let u1 = new U1("class語法糖"); u1.show(); // ============ 兩種操作一模一樣 class自動將方法寫入原型對象中 function U2(name) { this.name = name; } U2.prototype.show = function () { (function () { console.log(this); // 構造函數中方法中的函數this非嚴格模式下指向為window }()) } let u2 = new U2("構造函數"); u2.show(); </script>
靜態訪問
靜態屬性
靜態屬性即為類自己單獨設置屬性,而不是為生成的對象設置,請使用static
進行聲明。
<script>"use strict"; class User { static username = "用戶類"; // 如果不加 static,將會變成實例屬性 constructor(username) { this.username = username; } } let u1 = new User("雲崖"); console.log(u1.username); // 雲崖 console.log(User.username); // 用戶類 </script>
實現原理也非常簡單。
<script> "use strict"; function User(username) { this.username = username; } User.username = "用戶類"; let u1 = new User("雲崖"); console.log(u1.username); // 雲崖 console.log(User.username); // 用戶類 </script>
靜態方法
靜態方法即為類自己單獨設置方法,而不是為生成的對象設置,請使用static
進行聲明。
<script> "use strict"; class User { static show(){ console.log("類的方法..."); } } let u1 = new User(); User.show(); u1.show(); // 拋出異常 </script>
實現原理也非常簡單。
<script> "use strict"; function User() { } // 構造函數 User.show = function () { console.log( "類的方法..."); }; let u1 = new User(); User.show(); u1.show(); // 拋出異常 </script>
訪問器
使用訪問器可以對對象的屬性進行訪問控制,下面是使用訪問器對私有屬性進行管理。
語法介紹
使用訪問器可以管控屬性,有效的防止屬性隨意修改
訪問器就是在函數前加上
get/set
修飾,操作屬性時不需要加函數的擴號,直接用函數名
<script> "use strict"; class User { constructor(username){ this.username = username; } get name(){ // 訪問name時返回username return this.username; } set name(value){ // 設置name其實是給username做設置 this.username = value; } } let u1 = new User("雲崖"); console.log(u1.name); </script>
屬性保護
當外部嘗試修改某一屬性時,可以使用訪問器來進行驗證。
<script> "use strict"; class User { constructor(username){ this.username = username; } get name(){ // 訪問name時返回username return this.username; } set name(value){ // 設置name其實是給username做設置 if(typeof value == String){ this.username = value; } throw Error("value type error,must string"); } } let u1 = new User("雲崖"); u1.name = 18; // value type error,must string console.log(u1.name); </script>
私有封裝
私有封裝是指內部可以任意調用,外部只能通過訪問的接口才能進行調用。
public
public
指不受保護的屬性,在類的內部與外部都可以訪問到
<script> "use strict"; class User { constructor(username){ this.username = username; } show(){ return this.username; // 內部可以訪問 } } let u1 = new User("雲崖"); console.log(u1.username); // 外部也可以訪問 console.log(u1.show()); </script>
protected
protected
是受保護的屬性修釋,不允許外部直接操作,但可以繼承后在類內部訪問,下面將介紹protected
三種封裝方法。
命名保護
將屬性定義為以 _
開始,來告訴使用者這是一個私有屬性,請不要在外部使用。
外部修改私有屬性時可以使用訪問器
setter
操作但這只是提示,就像吸煙時煙盒上的吸煙有害健康,但還是可以抽
<script> "use strict"; class User { constructor(username){ this._username = username; } show(){ return this._username; // 內部可以訪問 } } let u1 = new User("雲崖"); // console.log(u1._username); // 外部也可以訪問,但是如果你是專業人員看到 _開頭就知道不該在外部拿他。 console.log(u1.show()); </script>
Symbol
由於我們的代碼都是在一個模塊中進行封裝的,所以使用Symbol()
來進行私有封裝非常方便。
除非使用者打開源代碼找到變量名key
,否則他只能通過提供的接口來拿到數據。
<script> "use strict"; let key = Symbol(); class User { constructor(username){ this[key] ={username}; } show(){ return this[key].username; // 內部可以訪問 } } let u1 = new User("雲崖"); console.log(u1); // { Symbol(): {username: "雲崖"}} 外部拿不到。只能通過接口來拿 console.log(u1.show()); </script>
WeakMap
WeakMap
是一組鍵/值對的集,下面利用WeakMap
類型特性定義私有屬性
<script> "use strict"; let key = new WeakMap(); class User { constructor(username) { key.set(this, { // 以當前對象作為鍵來存儲。 username, }) } show() { return key.get(this).username; // 內部可以訪問 } } let u1 = new User("雲崖"); console.log(u1); // {} 干干凈凈,啥都拿不到。 console.log(u1.show()); // 雲崖 </script>
private
private
指絕對私有屬性,只在當前類可以訪問到,並且不允許繼承的子類使用。
為屬性或方法名前加
#
為聲明為私有屬性私有屬性只能在聲明的類中使用
<script> "use strict"; class User { #username; // 對於實例的私有屬性來說,必須先定義一下。方法則不用。 constructor(username) { this.#check(username); this.#username = username; } show() { return this.#username; // 內部可以訪問 } #check = (username) => { // 驗證類的私有方法。 私有方法格式必須如此。 if (typeof username != "string") { throw Error("type error,value type must string.") } } } let u1 = new User("雲崖"); console.log(u1); // {#username: "雲崖", #check: ƒ} console.log(u1.show()); // 只能通過接口訪問 // console.log(u1.#check); // 外部不能訪問,拋出異常。 // console.log(u1.#username); // 外部不可以訪問,拋出異常 </script>
繼承特性
class
內部也是采用原型繼承,這與構造函數如出一轍。
如果你不了解原型繼承,那么可以看一下我前面的一篇文章,解釋的非常清楚了。
這里就是介紹一些class
語法糖如何使用繼承。
extends
以下示例將展示使用extends
語法實現原型繼承。
需要注意的是,只要繼承的子類中有構造函數,就一定要使用
super
方法。
沒使用super引發異常
<script> "use strict"; class A { constructor(name) { this.name = name; } show() { console.log(`${this.name}運行A類的show...`); } } class B extends A { // 繼承A的原型對象 constructor(name) { this.name = name; // Uncaught ReferenceError: // Must call super constructor in derived // class before accessing 'this' or returning from derived constructor // 意思是必須用super } } let b1 = new B("實例b1"); b1.show(); </script>
正確示范
<script> "use strict"; class A { constructor(name) { this.name = name; } show() { console.log(`${this.name}運行A類的show...`); } } class B extends A { // 繼承A的原型對象 constructor(name) { super(name); // 必須使用super,這里用父類的構造函數進行構造。 } } let b1 = new B("實例b1"); b1.show(); // 實例b1運行A類的show... </script>
原理如下
<script> "use strict"; function A(name) { this.name = name; } A.prototype = { constructor: A, show() { console.log(`${this.name}運行A類的show...`); }, }; function B(name){ A.call(this,name); } Object.setPrototypeOf(B.prototype,A.prototype); // B原型對象繼承與A原型對象 let b1 = new B("實例b1"); b1.show(); // 實例b1運行A類的show... </script>
super
表示從當前原型中執行方法。
super
一直指向當前對象在構造函數中,
super
一定要放在this
聲明的前面
super
只能在類或對象的方法中使用,而不能在函數中使用,換而言之,不要使用function
來表示這是一個函數!
正常調用構造函數示范
<script> "use strict"; class User { constructor(name) { this.name = name; } show() { console.log(`姓名:${this.name},年齡:${this.age},性別:${this.gender}`); // this指向始終為當前對象 } } class Admin extends User { // 繼承User的原型對象 constructor(name,age,gender) { super(name); // 使用父類的構造函數進行構造name,其他參數由自身構造。必須放在this上面 this.age = age; this.gender = gender; } func() { super.show(); // 使用super調用的同時會將當前對象this進行傳遞。 } } let a1 = new Admin("雲崖",18,"男"); a1.func(); // 姓名:雲崖,年齡:18,性別:男 </script>
super調用其他方法的示范
<script> "use strict"; let obj_1 = { show(){ console.log("執行了show..."); }, }; let obj_2 = { __proto__:obj_1, run(){ super.show(); // 正常執行 }, }; obj_2.run(); // 執行了show... </script>
錯誤示范,不能用function
進行聲明
<script> "use strict"; let obj_1 = { show() { console.log("執行了show..."); }, }; let obj_2 = { __proto__: obj_1, run = function () { super.show(); // 'super' keyword unexpected here }, }; obj_2.run(); // 異常,super不能在function中執行 </script>
靜態繼承
類自身的屬性以及方法都能被繼承下來。
<script> "use strict"; class A { static description = "類A"; static show(){ return ("類A的方法"); } } class B extends A { // 繼承A的原型對象 } console.log(B.description); // 類A console.log(B.show()); // 類A的方法 </script>
方法覆寫
當父類原型對象中有一個方法與子類原型對象中方法同名,子類的實例對象將調用子類的原型對象中的方法。
這是由原型鏈屬性順序查找引起的。
<script> "use strict"; class A { show() { console.log("A-->show"); } } class B extends A { // 繼承A的原型對象 show() { console.log("B-->show"); } } let b1 = new B(); b1.show() // B-->show </script>
MixIn機制
由於Js
不支持多繼承,所以想添加功能必須在某一個原型對象上不斷的增加功能,這勢必會讓其本來的原型顯得混亂不堪。
這種時候就可以使用Mixin
機制來實現。
注意:Minin
類應該當做工具箱來使用,而不應該作為其他類的父類被繼承下來。

<script> "use strict"; class Vehicle { // 交通工具類 constructor(name) { this.name = name; } whistle() { console.log(`${this.name}在鳴笛`); // 公用方法放父類中 } } class Aircraft extends Vehicle { constructor(name) { super(name); } } class Car extends Vehicle { constructor(name) { super(name); } } let Flyable_Mixin = { // 飛行器的功能 fly() { console.log(`${this.name}在飛`); }, outer() { console.log("其他功能..."); }, }; Object.assign(Aircraft.prototype, Flyable_Mixin); //給飛機添加上飛機Mixin的功能 let Car_Mixin = { // 汽車的功能 reversing() { console.log(`${this.name}正在倒車入庫`); }, outer() { console.log("其他功能..."); }, }; Object.assign(Car.prototype, Car_Mixin); //給汽車添加汽車Mixin的功能 let c1 = new Car("法拉利"); let a1 = new Aircraft("波音747"); c1.whistle(); // 法拉利在鳴笛 c1.reversing(); // 法拉利正在倒車入庫 a1.whistle(); // 波音747在鳴笛 a1.fly(); // 波音747在飛 </script>
原型檢測
檢測方法
使用instanceof
檢測構造函數的 prototype
屬性是否出現在某個實例對象的原型鏈上
使用isPrototypeOf
檢測一個對象是否是另一個對象的原型鏈中
<script> "use strict"; class User { } let u1 = new User(); console.log(u1 instanceof User); // true u1的原型鏈中包含User的原型對象嗎? console.log(User.prototype.isPrototypeOf(u1)); // true User的原型對象在u1的原型鏈中嗎? </script>
instanceof原理
遞歸不斷向上檢測原型對象。
function checkPrototype(obj, constructor) { if (!obj.__proto__) return false; if (obj.__proto__ == constructor.prototype) return true; return checkPrototype(obj.__proto__, constructor); }