摘要:聲明是模仿面向對象語言提出的定義類的方法。抽象類的基本原則是在內存中只有基類成員的一份拷貝。但是面向對象設計要求我們把共性放在一起以減少代碼,因此就有了抽象類。
class聲明
class 是 ES6 模仿面向對象語言(C++, Java)提出的定義類的方法。形式類似 C++ 和 Java (各取所長), 下面例子展示了 class 是如何定義構造函數、對象屬性和對象動/靜態方法的:
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!"
相當于傳統寫法:
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 中原有的那些方法都可以用, 包括但不限于:
Object.keys(), Object.assign() 等等
而且 class 也同樣支持表達式做屬性名,比如 Symbol
ES5 函數具有的屬性/方法:length、name、apply、call、bind、arguments 等等
但有些細節還是有區別的,比如:
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 也是其自有方法中的一個。
使用 class 注意一下幾點:
class 中默認是嚴格模式,即使不寫"use strict。關于嚴格模式可以看:Javascript基礎(2) - 嚴格模式特點
同名 class 不可重復聲明
class 相當于 object 而不是 map,不具有 map 屬性,也不具有默認的 Iterator。
constructor 方法在 class 中是必須的,如果沒有認為指定,系統會默認生成一個空的 constructor
調用 class 定義的類必須有 new 關鍵字,像普通函數那樣調用會報錯。ES5 不限制這一點。
TypeError: Class constructor Point cannot be invoked without "new"
constructor 方法默認返回值為 this,可以認為修改返回其他的值,但這會導致一系列奇怪的問題:
class Point{ constructor(x,y){ return [x, y]; } } new Point() instanceof Point; //false
class 聲明類不存在變量提升
new Point(); //ReferenceError: Point is not defined class Point{}class 表達式
這個和面向對象不一樣了,js 中函數可以有函數聲明形式和函數表達式2種方式定義,那么 class 一樣有第二種2種定義方式:class 表達式
var className1 = class innerName{ //... }; let className2 = class innerName{ //... }; const className3 = class innerName{ //... };
class 表達式由很多特性和 ES5 一樣:
和函數表達式類似,這里的innerName可以省略,而且innerName只有類內部可見,實際的類名是賦值號前面的 className。
這樣定義的類的作用域,由其所在位置和聲明關鍵字(var, let, const)決定
const申明的類是個常量,不能修改。
其變量聲明存在提升,但初始化不提升
class 表達式也不能和 class 申明重名
ES5 中有立即執行函數,類似的,這里也有立即執行類:
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 和 Generator 方法
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" }class 的繼承
這里我們只重點講繼承,關于多態沒有新的修改,和 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 繼承的時候需要注意一下幾點:
super 不能多帶帶使用,不能訪問父類屬性,只能方法父類方法和構造函數(super本身)
子類沒有自己的 this,需要借助 super 調用父類構造函數后加工得到從父類得到的 this,子類構造函數必須調用 super 函數。這一點和 ES5 完全不同。
子類如果沒有手動定義構造函數,會自動生成一個構造函數,如下:
constructor(...args){ super(...args); }
子類中使用 this 關鍵字之前,必須先調用 super 構造函數
由于繼承屬于共享原型的方式,所以不要在實例對象上修改原型(Object.setPrototypeOf, obj.__proto__等)
super 也可以用在普通是對象字面量中:
var obj = { toString(){ return `MyObj ${super.toString()}`; } } console.log(obj+""); //MyObj [object Object]prototype 和 __proto__
在 class 的繼承中
子類的 __proto__ 指向其父類
子類 prototype 的 __proto__ 指向其父類的 prototype
class 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
繼承自 Object
class A extends Object{} A.__proto__ === Object; //true A.prototype.__proto__ === Object.prototype; //true
繼承自 null
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__ 改變父類實例的行為。建議:
總是用 class 取代需要 prototype 的操作。因為 class 的寫法更簡潔,更易于理解。
使用 extends 實現繼承,因為這樣更簡單,不會有破壞 instanceof 運算的危險。
此外存取器和 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); //18new.target 屬性
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
關于抽象類這里解釋一下,要一個類不能實例化只能繼承用什么用?
在繼承中產生歧義的原因有可能是繼承類繼承了基類多次,從而產生了多個拷貝,即不止一次的通過多個路徑繼承類在內存中創建了基類成員的多份拷貝。抽象類的基本原則是在內存中只有基類成員的一份拷貝。舉個例子,一個類叫"動物",另有多各類繼承自動物,比如"胎生動物"、"卵生動物",又有多個類繼承自哺乳動物, 比如"人", "貓", "狗",這個例子好像復雜了,不過很明顯,被實例化的一定是一個個體,比如"人", "貓", "狗"。而"胎生動物",不應該被實例化為一個個體,它僅僅是人類在知識領域,為了分類世間萬物而抽象的一個分類。但是面向對象設計要求我們把共性放在一起以減少代碼,因此就有了抽象類。所以胎生動物都會運動,都可以發出聲音,這些就應該是共性放在"胎生動物"類中,而所以動物都會呼吸,會新陳代謝,這些共性就放在動物里面,這樣我們就不需要在"人", "貓", "狗"這樣的具體類中一遍遍的實現這些共有的方法和屬性。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97440.html
摘要:中沒有類的概念,因此它的對象和基于類的語言中的對象有所不同。生成對象的傳統方法是通過構造函數來實現的上述這種方式因為和中聲明方法的形式一樣,所以對象和方法的區分并不明顯,很容易造成混淆。要求,子類的構造函數必須執行一次函數,否則就會報錯。 面向對象的語言有一個標志,那就是他們都有類的概念,通過類可以創建任意多個具有相同屬性和方法的對象。 ECMAScript5中沒有類的概念,因此它的對...
摘要:修飾器修飾器是提出的一個提案,用來修改類的行為。目前需要才可以使用。其執行格式如下是修飾器名,即函數名相當于修飾器函數接受個參數,依次是目標函數屬性名可忽略該屬性的描述對象可忽略。 修飾器 修飾器是 ES7 提出的一個提案,用來修改類的行為。目前需要 babel 才可以使用。它最大的特點是:可以在編譯期運行代碼!其本質也就是在編譯器執行的函數。其執行格式如下: @decorator ...
摘要:一簡介與的關系是的規格,是的一種實現另外的方言還有和轉碼器命令行環境安裝直接運行代碼命令將轉換成命令瀏覽器環境加入,代碼用環境安裝,,根目錄建立文件加載為的一個鉤子設置完文件后,在應用入口加入若有使用,等全局對象及上方法安裝 一、ECMAScript6 簡介 (1) 與JavaScript的關系 ES是JS的規格,JS是ES的一種實現(另外的ECMAScript方言還有Jscript和...
摘要:基本類型是一種解決命名沖突的工具。這樣,就有了個基本類型和個復雜類型使用需要注意以下幾點和一樣不具有構造函數,不能用調用。判斷對象是否某個構造函數的實例,運算符會調用它是一個數組對象屬性。即,當存在時,以此為構造函數構建對象。 Symbol基本類型 Symbol 是一種解決命名沖突的工具。試想我們以前定義一個對象方法的時候總是要檢查是否已存在同名變量: if(String && Str...
摘要:數值類型擴展類型新增了如下特性支持二進制和八進制二進制用或開頭八進制用或開頭新加方法判斷一個數字是否有限方法判斷一個變量是否。值得注意的是如果將非數值傳入這兩個函數一律返回。對于無法轉換為數值的參數返回符合函數。 數值類型擴展 Number 類型新增了如下特性: 支持二進制和八進制 二進制用 0b 或 0B 開頭, 八進制用 0o 或 0O 開頭: Number(0b1101); ...
閱讀 2964·2023-04-26 02:04
閱讀 1278·2021-11-04 16:07
閱讀 3699·2021-09-22 15:09
閱讀 678·2019-08-30 15:54
閱讀 1899·2019-08-29 14:11
閱讀 2525·2019-08-26 12:19
閱讀 2255·2019-08-26 12:00
閱讀 752·2019-08-26 10:27