JS基於原型的‘類’,一直被轉行前端的碼僚們大呼驚奇,但接近傳統模式使用class
關鍵字定義的出現,卻使得一些前端同行深感遺憾而紛紛留言:「還我獨特的JS」、「淨搞些沒實質的東西」、「本身沒有類還非要往別家的類上靠」,甚至是「已轉行」等等。有情緒很正常,畢竟新知識意味着更多時間與精力的開銷,又不是簡單的閉眼享受。前端
然而歷史的軸印前行依舊,對於class
能夠確定的一點是你不能對面試官說:「拜託,不是小弟不懂,僅僅是不肯意瞭解,您換個問題唄!」一方面雖然class
只是個語法糖,但extends
對繼承的改進仍是不錯的。另外一方面從此可能在‘類’上出現的新特性應該是由class
而不是構造函數承載,誰也不肯定它未來會出落得怎樣標緻。所以,來來來,慢慢的喝下這碗熱氣騰騰的紅糖薑湯。面試
ECMAScript中沒有類的概念,咱們的實例是基於原型由構造函數生成具備動態屬性和方法的對象。不過爲了與國際接軌,描述的更爲簡便和高大上,依然會使用‘類’這一詞。因此JS的類等同於構造函數。ES6的class
只是個語法糖,其定義生成的對象依然構造函數。不過爲了與構造函數模式區分開,咱們稱其爲類模式。學習class
須要有構造函數和原型對象的知識,具體能夠自行百度。segmentfault
// ---使用構造函數 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'; } // 原型方法 };
關鍵字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 () { })();
類內容({}
裏面)的形式與對象字面量類似。不過類內容裏面只能定義方法不能定義屬性,方法的形式只能是函數簡寫式,方法間不用也不能用逗號分隔。方法名能夠是帶括號的表達式,也能夠爲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
函數定義只讀屬性。爲何不能直接設置屬性?是技術不成熟?是官方但願傳遞某種思想?抑或僅僅是筆者隨意拋出的一個問題?app
// ---直接在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沒法修改。
下面是使用構造函數和類實現相同功能的代碼。直觀上,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; // 報錯
ES5中的經典繼承方法是寄生組合式繼承,子類會分別繼承父類實例和原型上的屬性和方法。ES6中的繼承本質也是如此,不過實現方式有所改變,具體以下面的代碼。能夠看到,原型上的繼承是使用extends
關鍵字這一更接近傳統語言的形式,實例上的繼承是經過調用super
完成子類this
塑造。表面上看,方式更爲的統一和簡潔。this
class C1 { constructor(a) { this.a = a; } b() { console.log('b'); } } class C extends C1 { // 繼承原型數據 constructor() { super('a'); // 繼承實例數據 } }
使用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。
關鍵字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指向實例自己。 } }
使用構造函數模式,構建繼承了原生數據結構(好比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執行權的真實操做者