摘要:所以的類等同于構造函數。只能存在于子類的構造方法中,這時它指代父類構造函數。指代父類的構造函數此時指向,打印出。改變了構造函數的行為,一旦發現其不是通過這種形式調用的,構造函數會忽略傳入的參數。
前言
JS基于原型的‘類’,一直被轉行前端的碼僚們大呼驚奇,但接近傳統模式使用class關鍵字定義的出現,卻使得一些前端同行深感遺憾而紛紛留言:“還我獨特的JS”、“凈搞些沒實質的東西”、“自己沒有類還非要往別家的類上靠”,甚至是“已轉行”等等。有情緒很正常,畢竟新知識意味著更多時間與精力的開銷,又不是簡單的閉眼享受。
然而歷史的軸印前行依舊,對于class可以肯定的一點是你不能對面試官說:“拜托,不是小弟不懂,僅僅是不愿意了解,您換個問題唄!”一方面雖然class只是個語法糖,但extends對繼承的改進還是不錯的。另一方面今后可能在‘類’上出現的新特性應該是由class而不是構造函數承載,誰也不確定它將來會出落得怎樣標致。因此,來來來,慢慢的喝下這碗熱氣騰騰的紅糖姜湯。
1 classECMAScript中沒有類的概念,我們的實例是基于原型由構造函數生成具有動態屬性和方法的對象。不過為了與國際接軌,描述的更為簡便和高大上,依然會使用‘類’這一詞。所以JS的類等同于構造函數。ES6的class只是個語法糖,其定義生成的對象依然構造函數。不過為了與構造函數模式區分開,我們稱其為類模式。學習class需要有構造函數和原型對象的知識,具體可以自行百度。
// ---使用構造函數 function C () { console.log("New someone."); } C.a = function () { return "a"; }; // 靜態方法 C.prototype.b = function () { return "b"; }; // 原型方法 // ---使用class class C { static a() { return "a"; } // 靜態方法 constructor() { console.log("New someone."); } // 構造方法 b() { return "b"; } // 原型方法 };1.1 與變量對比
關鍵字class類似定義函數的關鍵字function,其定義的方式有聲明式和表達式(匿名式和命名式)兩種。通過聲明式定義的變量的性質與function不同,更為類似let和const,不會提前解析,不存在變量提升,不與全局作用域掛鉤和擁有暫時性死區等。class定義生成的變量就是一個構造函數,也因此,類可以寫成立即執行的模式。
// ---聲明式 class C {} function F() {} // ---匿名表達式 let C = class {}; let F = function () {}; // ---命名表達式 let C = class CC {}; let F = function FF() {}; // ---本質是個函數 class C {} console.log(typeof C); // function console.log(Object.prototype.toString.call(C)); // [object Function] console.log(C.hasOwnProperty("prototype")); // true // ---不存在變量提升 C; // 報錯,不存在C。 class C {} // 存在提前解析和變量提升 F; // 不報錯,F已被聲明和賦值。 function F() {} // ---自執行模式 let c = new (class { })(); let f = new (function () { })();1.2 與對象對比
類內容({}里面)的形式與對象字面量相似。不過類內容里面只能定義方法不能定義屬性,方法的形式只能是函數簡寫式,方法間不用也不能用逗號分隔。方法名可以是帶括號的表達式,也可以為Symbol值。方法分為三類,構造方法(constructor方法)、原型方法(存在于構造函數的prototype屬性上)和靜態方法(存在于構造函數本身上)
class C { // 原型方法a a() { console.log("a"); } // 構造方法,每次生成實例時都會被調用并返回新實例。 constructor() {} // 靜態方法b,帶static關鍵字。 static b() { console.log("b"); } // 原型方法,帶括號的表達式 ["a" + "b"]() { console.log("ab"); } // 原型方法,使用Symbol值 [Symbol.for("s")]() { console.log("symbol s"); } } C.b(); // b let c = new C(); c.a(); // a c.ab(); // ab c[Symbol.for("s")](); // symbol s
不能直接定義屬性,并不表示類不能有原型或靜態屬性。解析class會形成一個構造函數,因此只需像為構造函數添加屬性一樣為類添加即可。更為直接也是推薦的是只使用getter函數定義只讀屬性。為什么不能直接設置屬性?是技術不成熟?是官方希望傳遞某種思想?抑或僅僅是筆者隨意拋出的一個問題?
// ---直接在C類(構造函數)上修改 class C {} C.a = "a"; C.b = function () { return "b"; }; C.prototype.c = "c"; C.prototype.d = function () { return "d"; }; let c = new C(); c.c; // c c.d(); // d // ---使用setter和getter // 定義只能獲取不能修改的原型或靜態屬性 class C { get a() { return "a"; } static get b() { return "b"; } } let c = new C(); c.a; // a c.a = "1"; // 賦值沒用,只有get沒有set無法修改。1.3 與構造函數對比
下面是使用構造函數和類實現相同功能的代碼。直觀上,class簡化了代碼,使得內容更為聚合。constructor方法體等同構造函數的函數體,如果沒有顯式定義此方法,一個空的constructor方法會被默認添加用于返回新的實例。與ES5一樣,也可以自定義返回另一個對象而不是新實例。
// ---構造函數 function C(a) { this.a = a; } // 靜態屬性和方法 C.b = "b"; C.c = function () { return "c"; }; // 原型屬性和方法 C.prototype.d = "d"; C.prototype.e = function () { return "e"; }; Object.defineProperty(C.prototype, "f", { // 只讀屬性 get() { return "f"; } }); // ---類 class C { static c() { return "c"; } constructor(a) { this.a = a; } e() { return "e"; } get f() { return "f"; } } C.b = "b"; C.prototype.d = "d";
類雖然是個函數,但只能通過new生成實例而不能直接調用。類內部所定義的全部方法是不可枚舉的,在構造函數本身和prototype上添加的屬性和方法是可枚舉的。類內部定義的方法默認是嚴格模式,無需顯式聲明。以上三點增加了類的嚴謹性,比較遺憾的是,依然還沒有直接定義私有屬性和方法的方式。
// ---能否直接調用 class C {} C(); // 報錯 function C() {} C(); // 可以 // ---是否可枚舉 class C { static a() {} // 不可枚舉 b() {} // 不可枚舉 } C.c = function () {}; // 可枚舉 C.prototype.d = function () {}; // 可枚舉 isEnumerable(C, ["a", "c"]); // a false, c true isEnumerable(C.prototype, ["b", "d"]); // b false, d true function isEnumerable(target, keys) { let obj = Object.getOwnPropertyDescriptors(target); keys.forEach(k => { console.log(k, obj[k].enumerable); }); } // ---是否為嚴格模式 class C { a() { let is = false; try { n = 1; } catch (e) { is = true; } console.log(is ? "true" : "false"); } } C.prototype.b = function () { let is = false; try { n = 1; } catch (e) { is = true; } console.log(is ? "true" : "false"); }; let c = new C(); c.a(); // true,是嚴格模式。 c.b(); // false,不是嚴格模式。
在方法前加上static關鍵字表示此方法為靜態方法,它存在于類本身,不能被實例直接訪問。靜態方法中的this指向類本身。因為處于不同對象上,靜態方法和原型方法可以重名。ES6新增了一個命令new.target,指代new后面的構造函數或class,該命令的使用有某些限制,具體請看下面示例。
// ---static class C { static a() { console.log(this === C); } a() { console.log(this instanceof C); } } let c = new C(); C.a(); // true c.a(); // true // ---new.target // 構造函數 function C() { console.log(new.target); } C.prototype.a = function () { console.log(new.target); }; let c = new C(); // 打印出C c.a(); // 在普通方法中為undefined。 // ---類 class C { constructor() { console.log(new.target); } a() { console.log(new.target); } } let c = new C(); // 打印出C c.a(); // 在普通方法中為undefined。 // ---在函數外部使用會報錯 new.target; // 報錯2 extends
ES5中的經典繼承方法是寄生組合式繼承,子類會分別繼承父類實例和原型上的屬性和方法。ES6中的繼承本質也是如此,不過實現方式有所改變,具體如下面的代碼。可以看到,原型上的繼承是使用extends關鍵字這一更接近傳統語言的形式,實例上的繼承是通過調用super完成子類this塑造。表面上看,方式更為的統一和簡潔。
class C1 { constructor(a) { this.a = a; } b() { console.log("b"); } } class C extends C1 { // 繼承原型數據 constructor() { super("a"); // 繼承實例數據 } }2.1 與構造函數對比
使用extends繼承,不僅僅會將子類的prototype屬性的原型對象(__proto__)設置為父類的prototype,還會將子類本身的原型對象(__proto__)設置為父類本身。這意味著子類不單單會繼承父類的原型數據,也會繼承父類本身擁有的靜態屬性和方法。而ES5的經典繼承只會繼承父類的原型數據。不單單是財富,連老爸的名氣也要獲得,不錯不錯。
class C1 { static get a() { console.log("a"); } static b() { console.log("b"); } } class C extends C1 { } // 等價,沒有構造方法會默認添加。 class C extends C1 { constructor(...args) { super(...args); } } let c = new C(); C.a; // a,繼承了父類的靜態屬性。 C.b(); // b,繼承了父類的靜態方法。 console.log(Object.getPrototypeOf(C) === C1); // true,C的原型對象為C1 console.log(Object.getPrototypeOf(C.prototype) === C1.prototype); // true,C的prototype屬性的原型對象為C1的prototype
ES5中的實例繼承,是先創造子類的實例對象this,再通過call或apply方法,在this上添加父類的實例屬性和方法。當然也可以選擇不繼承父類的實例數據。而ES6不同,它的設計使得實例繼承更為優秀和嚴謹。
在ES6的實例繼承中,是先調用super方法創建父類的this(依舊指向子類)和添加父類的實例數據,再通過子類的構造函數修飾this,與ES5正好相反。ES6規定在子類的constructor方法里,在使用到this之前,必須先調用super方法得到子類的this。不調用super方法,意味著子類得不到this對象。
class C1 { constructor() { console.log("C1", this instanceof C); } } class C extends C1 { constructor() { super(); // 在super()之前不能使用this,否則報錯。 console.log("C"); } } new C(); // 先打印出C1 true,再打印C。2.2 super
關鍵字super比較奇葩,在不同的環境和使用方式下,它會指代不同的東西(總的說可以指代對象或方法兩種)。而且在不顯式的指明是作為對象或方法使用時,比如console.log(super),會直接報錯。
作為函數時。super只能存在于子類的構造方法中,這時它指代父類構造函數。
作為對象時。super在靜態方法中指代父類本身,在構造方法和原型方法中指代父類的prototype屬性。不過通過super調用父類方法時,方法的this依舊指向子類。即是說,通過super調用父類的靜態方法時,該方法的this指向子類本身;調用父類的原型方法時,該方法的this指向該(子類的)實例。而且通過super對某屬性賦值時,在子類的原型方法里指代該實例,在子類的靜態方法里指代子類本身,畢竟直接在子類中通過super修改父類是很危險的。
很迷糊對吧,瘋瘋癲癲的,還是結合著代碼看吧!
class C1 { static a() { console.log(this === C); } b() { console.log(this instanceof C); } } class C extends C1 { static c() { console.log(super.a); // 此時super指向C1,打印出function a。 this.x = 2; // this等于C。 super.x = 3; // 此時super等于this,即C。 console.log(super.x); // 此時super指向C1,打印出undefined。 console.log(this.x); // 值已改為3。 super.a(); // 打印出true,a方法的this指向C。 } constructor() { super(); // 指代父類的構造函數 console.log(super.c); // 此時super指向C1.prototype,打印出function c。 this.x = 2; // this等于新實例。 super.x = 3; // 此時super等于this,即實例本身。 console.log(super.x); // 此時super指向C1.prototype,打印出undefined。 console.log(this.x); // 值已改為3。 super.b(); // 打印出true,b方法的this指向實例本身。 } }2.3 繼承原生構造函數
使用構造函數模式,構建繼承了原生數據結構(比如Array)的子類,有許多缺陷的。一方面由上文可知,原始繼承是先創建子類this,再通過父類構造函數進行修飾,因此無法獲取到父類的內部屬性(隱藏屬性)。另一方面,原生構造函數會直接忽略call或apply方法傳入的this,導致子類根本無法獲取到父類的實例屬性和方法。
function MyArray(...args) { Array.apply(this, args); } MyArray.prototype = Array.prototype; // MyArray.prototype.constructor = MyArray; let arr = new MyArray(1, 2, 3); // arr為對象,沒有儲存值。 arr.push(4, 5); // 在arr上新增了0,1和length屬性。 arr.map(d => d); // 返回數組[4, 5] arr.length = 1; // arr并沒有更新,依舊有0,1屬性,且arr[1]為5。
創建類的過程,是先構造一個屬于父類卻指向子類的this(繞口),再通過父類和子類的構造函數進行修飾。因此可以規避構造函數的問題,獲取到父類的實例屬性和方法,包括內部屬性。進而真正的創建原生數據結構的子類,從而簡單的擴展原生數據類型。另外還可以通過設置Symbol.species屬性,使得衍生對象為原生類而不是自定義子類的實例。
class MyArray extends Array { // 實現是如此的簡單 static get [Symbol.species]() { return Array; } } let arr = new MyArray(1, 2, 3); // arr為數組,儲存有1,2,3。 arr.map(d => d); // 返回數組[1, 2, 3] arr.length = 1; // arr正常更新,已包含必要的內部屬性。
需要注意的是繼承Object的子類。ES6改變了Object構造函數的行為,一旦發現其不是通過new Object()這種形式調用的,構造函數會忽略傳入的參數。由此導致Object子類無法正常初始化,但這不是個大問題。
class MyObject extends Object { static get [Symbol.species]() { return Object; } } let o = new MyObject({ id: 1 }); console.log(o.hasOwnPropoty("id")); // false,沒有被正確初始化推薦
ES6精華:Symbol
ES6精華:Promise
Async:簡潔優雅的異步之道
Generator:JS執行權的真實操作者
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/108526.html
摘要:接下來我們看下類的寫法,這個就很接近于傳統面向對象語言了。如果你想了解傳統面向對象語言,這里是一個好切入點。作為對象時,指向父類的原型對象。這些就是為將來在中支持面向對象的類機制而預留的。 在ES5中,我們經常使用方法或者對象去模擬類的使用,并基于原型實現繼承,雖然可以實現功能,但是代碼并不優雅,很多人還是傾向于用 class 來組織代碼,很多類庫、框架創造了自己的 API 來實現 c...
摘要:字節碼生成把語法樹定義的抽象的語法結構按照二進制字節碼的規則排布成字節碼,最終我們可以看到滿足虛擬機運行要求的二進制字節碼被轉換出來。上面的過程完成后,命令扮演的編譯器就將源代碼轉成了結構化的二進制字節碼。 這篇文章的素材來自周志明的《深入理解Java虛擬機》。 作為Java開發人員,一定程度了解JVM虛擬機的的運作方式非常重要,本文就一些簡單的虛擬機的相關概念和運作機制展開我自己的學...
摘要:眾多面向對象的編程思想雖不盡一致,但是無論哪種面向對象編程語言都具有以下的共通功能。原型編程以類為中心的傳統面向對象編程,是以類為基礎生成新對象。而原型模式的面向對象編程語言沒有類這樣一個概念。 什么是面向對象?這個問題往往會問到剛畢業的新手or實習生上,也是往往作為一個技術面試的開頭題。在這里我們不去談如何答(fu)好(yan)問(guo)題(qu),僅談談我所理解的面向對象。 從歷...
摘要:依賴于接口的設計模式下面列出的設計模式,尤其依賴接口工廠模式。這些私用的靜態成員可以從構造器內部訪問,這意味著所有私用函數和特權函數都能訪問它們。構造器靜態特權方法封裝之弊私用方法很難進行單元測試。 1.弱類型語言 在JavaScript中,定義變量時不必聲明其類型。但這并不意味著變量沒有類型。一個變量可以屬于幾種類型之一,這取決于其包含的數據。JavaScript中有三種原始類型:...
閱讀 1950·2021-11-22 14:44
閱讀 1682·2021-11-02 14:46
閱讀 3676·2021-10-13 09:40
閱讀 2609·2021-09-07 09:58
閱讀 1640·2021-09-03 10:28
閱讀 1670·2019-08-29 15:30
閱讀 988·2019-08-29 15:28
閱讀 1481·2019-08-26 12:20