class 是 ES6 模仿面嚮對象語言(C++, Java)提出的定義類的方法。形式相似 C++ 和 Java (各取所長), 下面例子展現了 class 是如何定義構造函數、對象屬性和對象動/靜態方法的:javascript
class Point{ constructor(x, y){ //定義構造函數 this.x = x; //定義屬性x this.y = y; //定義屬性y } //這裏沒有逗號 toString(){ //定義動態方法,不須要 function 關鍵字 return `(${this.x},${this.y})`; } static show(){ //利用 static 關鍵字定義靜態方法 console.log("Static function!"); } } var p = new Point(1,4); console.log(p+""); //(1,4) console.log(typeof Point); //"function" console.log(Point.prototype.constructor === Point); //true console.log(Point.prototype.constructor === p.constructor); //true Point.show(); //"Static function!"
至關於傳統寫法:java
function Point(x, y){ this.x = x; this.y = y; } Point.prototype.toString = function(){ return `(${this.x},${this.y})`; } Point.show = function(){ console.log("Static function!"); } var p = new Point(1,4); console.log(p+""); //(1,4)
這裏不難看出,class 的類名就是 ES5 中的構造函數名,靜態方法就定義在其上,而類的本質依然是個函數。而 class 中除了 constructor 是定義的構造函數之外,其餘的方法都定義在類的 prototype 上,這都和 ES5 是一致的,這就意味着,ES5 中原有的那些方法均可以用, 包括但不限於:segmentfault
Object.keys()
, Object.assign()
等等但有些細節仍是有區別的,好比:數組
class Point{ constructor(x, y){ //定義構造函數 this.x = x; //定義屬性x this.y = y; //定義屬性y } //這裏沒有逗號 toString(){ //定義動態方法,不須要 function 關鍵字 return `(${this.x},${this.y})`; } getX(){ return this.x; } getY(){ return this.y; } } var p = new Point(1,4); var keys = Object.keys(Point.prototype); var ownKeys = Object.getOwnPropertyNames(Point.prototype); console.log(keys); //[] console.log(ownKeys); //["constructor", "toString", "getX", "getY"] console.log(p.hasOwnProperty("toString")); //false console.log(p.__proto__.hasOwnProperty("toString")); //true
//ES5 function Point(x, y){ this.x = x; this.y = y; } Point.prototype = { toString(){ return `(${this.x},${this.y})`; }, getX(){ return this.x; }, getY(){ return this.y; } } var p = new Point(1,4); var keys = Object.keys(Point.prototype); var ownKeys = Object.getOwnPropertyNames(Point.prototype); console.log(keys); //["toString", "getX", "getY"] console.log(ownKeys); //["toString", "getX", "getY"] console.log(p.hasOwnProperty("toString")); //false console.log(p.__proto__.hasOwnProperty("toString")); //true
這個例子說明,class 中定義的動態方法是不可枚舉的,而且 constructor 也是其自有方法中的一個。babel
使用 class 注意一下幾點:app
"use strict
。關於嚴格模式能夠看:Javascript基礎(2) - 嚴格模式特色 TypeError: Class constructor Point cannot be invoked without 'new'
class Point{ constructor(x,y){ return [x, y]; } } new Point() instanceof Point; //false
new Point(); //ReferenceError: Point is not defined class Point{}
這個和麪向對象不同了,js 中函數能夠有函數聲明形式和函數表達式2種方式定義,那麼 class 同樣有第二種2種定義方式:class 表達式函數
var className1 = class innerName{ //... }; let className2 = class innerName{ //... }; const className3 = class innerName{ //... };
class 表達式由不少特性和 ES5 同樣:this
ES5 中有當即執行函數,相似的,這裏也有當即執行類:prototype
var p = new class { constructor(x, y){ this.x = x; this.y = y; } toString(){ return `(${this.x},${this.y})`; } }(1,5); //當即生成一個對象 console.log(p+""); //(1,5)
getter 和 setter 使用方式和 ES5 同樣, 這裏很少說了,舉個例子一看就懂:設計
class Person{ constructor(name, age, tel){ this.name = name; this.age = age; this.tel = tel; this._self = {}; } get id(){ return this._self.id; } set id(str){ if(this._self.id){ throw new TypeError("Id is read-only"); } else { this._self.id = str; } } } var p = new Person("Bob", 18, "13211223344"); console.log(p.id); //undefined p.id = '30010219900101009X'; console.log(p.id); //'30010219900101009X' var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'id'); console.log('set' in descriptor); //true console.log('get' in descriptor); //true p.id = '110'; //TypeError: Id is read-only
Generator 用法也和 ES6 Generator 部分同樣:
class Person{ constructor(name, age, tel){ this.name = name; this.age = age; this.tel = tel; this._self = {}; } *[Symbol.iterator](){ var keys = Object.keys(this); keys = keys.filter(function(item){ if(/^_/.test(item)) return false; else return true; }); for(let item of keys){ yield this[item]; } } get id(){ return this._self.id; } set id(str){ if(this._self.id){ throw new TypeError("Id is read-only"); } else { this._self.id = str; } } } var p = new Person("Bob", 18, "13211223344"); p.id = '30010219900101009X'; for(let info of p){ console.log(info); //依次輸出: "Bob", 18, "13211223344" }
這裏咱們只重點講繼承,關於多態沒有新的修改,和 ES5 中同樣,在函數內判斷參數便可。關於多態能夠閱讀Javascript對象、類與原型鏈中關於多態重構
的部分。
此外,class 繼承屬於 ES5 中多種繼承方式的共享原型,關於共享原型也在上面這篇文章中講解過。
class 實現繼承能夠簡單的經過 extends 關鍵字實現, 而使用 super 關鍵字調用父類方法:
//定義 '有色點'' 繼承自 '點' class ColorPoint extends Point{ //這裏延用了上面定義的 Point 類 constructor(x, y, color){ super(x, y); //利用 super 函數調用父類的構造函數 this.color = color; } toString(){ return `${super.toString()},${this.color}`; //利用 super 調用父類的動態方法 } } var cp = new ColorPoint(1, 5, '#ff0000'); console.log(cp+""); //(1,5),#ff0000 ColorPoint.show(); //"Static function!" 靜態方法一樣被繼承了 cp instanceof ColorPoint; //true cp instanceof Point; //true
使用 extends 繼承的時候須要注意一下幾點:
constructor(...args){ super(...args); }
Object.setPrototypeOf
, obj.__proto__
等)var obj = { toString(){ return `MyObj ${super.toString()}`; } } console.log(obj+""); //MyObj [object Object]
prototype
和 __proto__
在 class 的繼承中
__proto__
指向其父類__proto__
指向其父類的 prototypeclass Point{ constructor(x, y){ this.x = x; this.y = y; } } class ColorPoint extends Point{ constructor(x, y, color){ super(x, y); this.color = color; } } ColorPoint.__proto__ === Point; //true ColorPoint.prototype.__proto__ === Point.prototype; //true
其等價的 ES5 是這樣的:
function Point(){ this.x = x; this.y = y; } function ColorPoint(){ this.x = x; this.y = y; this.color = color; } Object.setPrototypeOf(ColorPoint.prototype, Point.prototype); //繼承動態方法屬性 Object.setPrototypeOf(ColorPoint, Point); //繼承靜態方法屬性 ColorPoint.__proto__ === Point; //true ColorPoint.prototype.__proto__ === Point.prototype; //true
這裏咱們應該理解一下3種繼承的 prototype 和 __proto__
:
class A{} A.__proto__ === Function.prototype; //true A.prototype.__proto__ === Object.prototype; //true
class A extends Object{} A.__proto__ === Object; //true A.prototype.__proto__ === Object.prototype; //true
class A extends null{} A.__proto__ === Function.prototype; //true A.prototype.__proto__ === undefined; //true
判斷類的繼承關係:
class A{} class B extends A{} Object.getPrototypeOf(B) === A; //true
子類的實例的 __proto__
的 __proto__
指向其父類實例的 __proto__
class A{} class B extends A{} var a = new A(); var b = new B(); B.__proto__.__proto__ === A.__proto__; //true
所以,能夠經過修改子類實例的 __proto__.__proto__
改變父類實例的行爲。建議:
此外存取器和 Generator 函數均可以很理想的被繼承:
class Person{ constructor(name, age, tel){ this.name = name; this.age = age; this.tel = tel; this._self = {}; } *[Symbol.iterator](){ var keys = Object.keys(this); keys = keys.filter(function(item){ if(/^_/.test(item)) return false; else return true; }); for(let item of keys){ yield this[item]; } } get id(){ return this._self.id; } set id(str){ if(this._self.id){ throw new TypeError("Id is read-only"); } else { this._self.id = str; } } } class Coder extends Person{ constructor(name, age, tel, lang){ super(name, age, tel); this.lang = lang; } } var c = new Coder("Bob", 18, "13211223344", "javascript"); c.id = '30010219900101009X'; for(let info of c){ console.log(info); //依次輸出: "Bob", 18, "13211223344", "javascript" } console.log(c.id); //'30010219900101009X' c.id = "110"; //TypeError: Id is read-only
多繼承指的是一個新的類繼承自已有的多個類,JavaScript 沒有提供多繼承的方式,因此咱們使用 Mixin 模式手動實現:
function mix(...mixins){ class Mix{} for(let mixin of mixins){ copyProperties(Mix, mixin); //繼承靜態方法屬性 copyProperties(Mix.prototype, mixin.prototype); //繼承動態方法屬性 } return Mix; function copyProperties(target, source){ for(let key of Reflect.ownKeys(source)){ if(key !== 'constructor' && key !== "prototype" && key !== "name"){ if(Object(source[key]) === source[key]){ target[key] = {}; copyProperties(target[key], source[key]); //遞歸實現深拷貝 } else { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } } } } //使用方法: class MultiClass extends mix(superClass1, superClass2, /*...*/){ //... }
因爲 mixin 模式使用了拷貝構造,構造出的子類的父類是 mix 函數返回的 class, 所以 prototype 和 __proto__
與任一 superClass 都沒有直接的聯繫,instanceof 判斷其屬於 mix 函數返回類的實例,一樣和任一 superClass 都沒有關係。能夠這麼說:咱們爲了實現功能破壞了理論應該具備的原型鏈。
在 ES5 中,原生構造函數是不能繼承的,包括: Boolean(), Number(), Date(), String(), Object(), Error(), Function(), RegExp()等,好比咱們這樣實現:
function SubArray(){} Object.setPrototypeOf(SubArray.prototype, Array.prototype); //繼承動態方法 Object.setPrototypeOf(SubArray, Array); //繼承靜態方法 var arr = new SubArray(); arr.push(5); arr[1] = 10; console.log(arr.length); //1 應該是2 arr.length = 0; console.log(arr); //[0:5,1:10] 應該爲空
很明顯這已經不是那個咱們熟悉的數組了!咱們能夠用 class 試試:
class SubArray extends Array{} var arr = new SubArray(); arr.push(5); arr[1] = 10; console.log(arr.length); //2 arr.length = 0; console.log(arr); //[]
仍是熟悉的味道,對吧!這就和以前提到的繼承差別有關了,子類沒有本身的 this,須要藉助 super 調用父類構造函數後加工獲得從父類獲得的 this,子類構造函數必須調用 super 函數。而 ES5 中先生成子類的 this,而後把父類的 this 中的屬性方法拷貝過來,咱們都知道,有的屬性是不可枚舉的,而有的屬性是 Symbol 名的,這些屬性不能很好的完成拷貝,就會致使問題,好比 Array 構造函數的內部屬性 [[DefineOwnProperty]]
。
利用這個特性,咱們能夠定義本身的合適的類, 好比一個新的錯誤類:
class ExtendableError extends Error{ constructor(message){ super(message); this.stack = new Error().stack; this.name = this.constructor.name; } } throw new ExtendableError("test new Error"); //ExtendableError: test new Error
爲什麼靜態屬性須要單獨寫,而靜態方法直接簡單帶過。由於這是個兼容性問題,目前 ES6 的靜態方法用 static 關鍵字,可是靜態屬性和 ES5 同樣,須要單獨定義:
class A{} A.staticProperty = "staticProperty"; console.log(A.staticProperty); //"staticProperty"
不過 ES7 提出能夠在 class 內部實現定義,惋惜目前不支持,可是還好有 babel 支持:
class A{ static staticProperty = "staticProperty"; //ES7 靜態屬性 instanceProperty = 18; //ES7 實例屬性 } console.log(A.staticProperty); //"staticProperty" console.log(new A().instanceProperty); //18
new 原本是個關鍵字,但 ES6 給它添加了屬性——target
。該屬性只能在構造函數中使用,用來判斷構造函數是否做爲構造函數調用的, 若是構造函數被 new 調用返回構造函數自己,不然返回 undefined:
function Person(){ if(new.target){ console.log("constructor has called"); } else { console.log("function has called"); } } new Person(); //"constructor has called" Person(); //"function has called"
這樣咱們能夠實現一個構造函數,只能使用 new 調用:
function Person(name){ if(new.target === Person){ this.name = name; } else { throw new TypeError("constructor must be called by 'new'"); } } new Person('Bob'); //"constructor has called" Person(); //TypeError: constructor must be called by 'new' Person.call({}); //TypeError: constructor must be called by 'new'
這裏須要注意:父類構造函數中的 new.target 會在調用子類構造函數時返回子類,所以使用了該屬性的類不該該被實例化,只用於繼承,相似於 C++ 中的抽象類。
class Person{ constructor(name){ if(new.target === Person){ this.name = name; } else { throw new TypeError("constructor must be called by 'new'"); } } } class Coder extends Person{} new Coder('Bob'); //TypeError: constructor must be called by 'new' 這不是咱們想要的
//抽象類實現 class Person{ constructor(name){ if(new.target === Person){ throw new TypeError("This class cannot be instantiated"); } this.name = name; } } class Coder extends Person{} var c = new Coder('Bob'); console.log(c.name); //'Bob' new Person('Bob'); //TypeError: This class cannot be instantiated
關於抽象類這裏解釋一下,要一個類不能實例化只能繼承用什麼用?
在繼承中產生歧義的緣由有多是繼承類繼承了基類屢次,從而產生了多個拷貝,即不止一次的經過多個路徑繼承類在內存中建立了基類成員的多份拷貝。抽象類的基本原則是在內存中只有基類成員的一份拷貝。舉個例子,一個類叫"動物",另有多各種繼承自動物,好比"胎生動物"、"卵生動物",又有多個類繼承自哺乳動物, 好比"人", "貓", "狗",這個例子好像複雜了,不過很明顯,被實例化的必定是一個個體,好比"人", "貓", "狗"。而"胎生動物",不該該被實例化爲一個個體,它僅僅是人類在知識領域,爲了分類世間萬物而抽象的一個分類。可是面向對象設計要求咱們把共性放在一塊兒以減小代碼,所以就有了抽象類。因此胎生動物都會運動,均可以發出聲音,這些就應該是共性放在"胎生動物"類中,而因此動物都會呼吸,會新陳代謝,這些共性就放在動物裏面,這樣咱們就不須要在"人", "貓", "狗"這樣的具體類中一遍遍的實現這些共有的方法和屬性。