- 基本語法
- 繼承
- 私有屬性與方法、靜態屬性與方法
- 修飾器(Decorator)
一、基本語法
1 class Grammar{ 2 constructor(name,age){ //定義對象自身的方法和屬性 3 this.name = name, 4 this.age = age 5 } 6 // 在原型上定義只讀屬性 7 get inva(){ 8 return "JS"; 9 } 10 //在原型上定義可讀寫屬性 11 set skill(val){ 12 this._skill = val; 13 } 14 get skill(){ 15 return this._skill; 16 } 17 action(){ //定義原型上的方法 18 console.log("使用" + this.inva + "實現web項目。"); 19 } 20 }
將ES6的class類示例用ES5語法實現:
1 function Obj(name,age){ 2 this.name = name; 3 this.age = age; 4 } 5 Object.defineProperty(Obj.prototype,"inva",{ 6 get:function(){ 7 return "JS"; 8 }, 9 configurable:true, 10 enumerable:false 11 }); 12 Object.defineProperty(Obj.prototype,"skill",{ 13 set:function(val){ 14 this._skill = val; 15 }, 16 get:function(){ 17 return this._skill; 18 }, 19 configurable:true, 20 enumerable:false 21 }); 22 Object.defineProperty(Obj.prototype,"saction",{ 23 value:function action(){ 24 console.log("使用" + this.inva + "實現web項目。"); 25 }, 26 writable:true, 27 configurable:true, 28 enumerable:false 29 });
1.1Class簡單說明:
Class聲明的類本質上還是一個函數:
typeof Grammar; //"function" Grammar === Grammar.prototype.constructor; //true
類雖然是函數,但是不能直接被調用執行,必須使用new指令執行構造行為:
Grammar();//Class constructor Grammar cannot be invoked without 'new'
constructor方法是類的默認方法,一個類必須有constructor方法,如果沒有顯式的定義,會隱式的添加一個空的constructor方法,不會報錯:
1 class ObjFun{ 2 get a(){ 3 return "這是一個沒有顯式定義constructor方法的類"; 4 } 5 } 6 var objFun = new ObjFun(); 7 console.log(objFun.a);//這是一個沒有顯式定義constructor方法的類
Class聲明的類不存在變量提升,所以在類前面調用會報錯:
1 new Foo(); //Cannot access 'Foo' before initialization 2 class Foo{}
二、繼承
1 class InheritGrammar extends Grammar{ 2 constructor(name,age,color){ 3 super(name,age); //調用父類的constructor 4 this.color = color; 5 } 6 introduce(){ 7 this.action(); 8 console.log("我的顏色是"+this.color); 9 } 10 } 11 var inGrammar = new InheritGrammar("他鄉踏雪",18,"blur"); 12 inGrammar.introduce();//使用JS實現web項目 我的顏色是blur
在控制台展開inGrammar實例:
1 InheritGrammar {name: "他鄉踏雪", age: 18, color: "blur"}
將ES6的class繼承語法使用ES5的語法實現:
1 function InheritObj(name,age,color){ 2 var InSuper = new Obj(name,age); //這行代碼可以替換(沒有差異):var InSuper = new Grammar(name,age); 3 for(var key in InSuper){ 4 this[key] = InSuper[key]; 5 } 6 this.color = color; 7 } 8 InheritObj.prototype.introduce = function introduce(){ 9 this.action(); 10 console.log("我的顏色是"+this.color); 11 } 12 var InObjPro = Object.getOwnPropertyNames(Obj.prototype); //將Obj.prototype替換成Grammar沒有區別 13 for(var time of InObjPro){ 14 if(time !== "constructor"){ 15 var attrObj = Object.getOwnPropertyDescriptor(Obj.prototype,time);//將Obj.prototype替換成Grammar沒有區別 16 Object.defineProperty(InheritObj.prototype,time,attrObj); 17 } 18 }
測試代碼:
1 var grammar = new Grammar("他鄉踏雪",18); 2 var obj = new Obj("他鄉踏雪",18); 3 4 var inGrammar = new InheritGrammar("他鄉踏雪",18,"blur"); 5 var inObj = new InheritObj("他鄉踏雪",18,"blur"); 6 7 console.log(grammar); 8 console.log(obj); 9 console.log("............................."); 10 console.log(inGrammar); 11 console.log(inObj);
ES6的class實現可定有瀏覽器內部更優的實現方案,使用ES5的語法實現只是模仿,但ES6語法並不改變JS的語言特性,所以使用ES5模仿實現的class機制不會有差異。ES5實現class的機制其核心就是在屬性描述符,核心ES5的API就是:(**重點**)
1 Object.getOwnPropertyNames(Obj.prototype);//獲取對象的屬性名稱列表 2 Object.getOwnPropertyDescriptor(Obj.prototype,time);//獲取屬性描述符對象 3 Object.defineProperty(InheritObj.prototype,time,attrObj);//給對象的屬性綁定屬性描述符對象
三、私有屬性與方法、靜態屬性與方法
2.1靜態屬性與方法
在class語法中除了引入繼承的特性,還實現了靜態屬性和靜態方法的特性,在ES提案中的私有屬性和方法也有比較多的支持者。目前,私有屬性還是基於現有的語法特性,然后開發者們按照約定熟成的方式來實現,但並不能完全實現屬性和方法的私有性。
1 //使用static指令實現靜態屬性和靜態方法 2 class Foo{ 3 static a ; 4 static b = 10; 5 static c = function c(){ 6 console.log(this.a + this.b); 7 } 8 } 9 class Fun extends Foo{ 10 constructor(){ 11 super() 12 } 13 } 14 Foo.a = 18; 15 Foo.c();//28 16 Fun.a = 10; 17 Fun.c();//20 18 Foo.c();//28
通過上面的示例可以看到,在JavaScript中的class機制同樣實現了類的靜態方法特性,被正常的繼承,其實靜態方法被繼承相比原型屬性和方法的繼承還要簡單,因為static聲明的靜態屬性是可以枚舉的,不需要復雜的獲取實行描述符。所以,如果父級類的靜態屬性是引用值類型的話,就需要小心使用,因為class的繼承機制僅僅只是將父級的引用值引用賦給了子類,所以它們可以共同修改這個引用值。
1 class Foo{ 2 static e = [1,2,3]; 3 } 4 class Fun extends Foo{ 5 constructor(){ 6 super() 7 } 8 } 9 console.log(Foo.e); //[1,2,3] 10 Fun.e.push(4); 11 console.log(Fun.e);//[1,2,3,4] 12 console.log(Foo.e);//[1,2,3,4]
這也是JavaScript類機制與像Java那些語言的根本區別,Java的繼承是完全重新復制一份,互不干擾。但是如果要在JavaScript中實現深拷貝顯然在性能上是個很大的損失。
一個額外的問題,直接在class中寫靜態屬性並賦值的方式,是ES7的寫法,在老版本的瀏覽器中不能識別(報錯),我是用最新的chrome瀏覽器已經能識別。但是,目前來講瀏覽器能不能識別並不重要,因為我們在使用ES6還是ES7的時候都會使用編譯工具進行降級,在babel中我使用的7.5.5版本都不支持編譯示例中的靜態屬性的語法,在編譯的時候回報以下錯誤:
Support for the experimental syntax 'classProperties' isn't currently enabled (2:11): 1 | class Foo{ > 2 | static e = [1,2,3]; | ^ 3 | static bar(){ 4 | return "bar"; 5 | }
但同時,在后面babel也提示了能編譯這個語法的插件:
Add @babel/plugin-proposal-class-properties (https://git.io/vb4SL) to the 'plugins' section of your Babel config to enable transformation.
下載插件:
npm install @babel/plugin-proposal-class-properties --save-dev
然后在(.babel文件中配置,這種模式任選一種配置,這里我選擇了寬松模式):
1 //嚴格模式 2 { 3 "plugins": ["@babel/plugin-proposal-class-properties"] 4 } 5 //寬松模式(我測代碼時使用了這個) 6 { 7 "plugins": [ 8 ["@babel/plugin-proposal-class-properties", { "loose": true }] 9 ] 10 }
其實,bable編譯后的代碼非常的簡單暴力:
Foo.e = [1, 2, 3];
2.2私有屬性和方法:
這里有一篇作為參考:https://www.oschina.net/news/100766/tc39-approval-emcascript
關於私有方法的TC39提案:https://github.com/tc39/proposal-private-methods
TC39官網: https://tc39.es/,2019年1月通過的方案
私有屬性示例:(使用同靜態屬性和方法的轉碼器插件:@babel/plugin-proposal-class-properties,安裝和使用詳細查看前面靜態屬性和方法的相關插件安裝內容)
1 class Foo{ 2 #x = 10; 3 static poor(numVal){ 4 //這是會報錯的,因為靜態方法的this指向Foo,而私有屬性的this指向實例對象 5 // 所以靜態方法中不能使用私有屬性 6 console.log(this.#x - numVal); 7 } 8 constructor(x = 0){ 9 this.num = this.#x; //公有屬性可以獲取私有屬性的值 10 } 11 set x(value){ 12 this.#x +=value; //這里可以理解為公有方法內部給私有屬性寫入值 13 } 14 get x(){ 15 return this.#x; //這里可以理解為公有方法內部讀取私有屬性的值 16 } 17 sum(){ 18 console.log(this.num + this.#x); //這里可以理解為公有方法讀取私有屬性的值 19 } 20 } 21 class Fun extends Foo{ 22 constructor(){ 23 super() 24 } 25 y(){ 26 // 子類方法不能直接使用父類的私有屬性,babel插件無法編譯 27 // return this.#x; 28 } 29 } 30 var foo = new Foo(); 31 var fun = new Fun(); 32 console.log(fun.x);//10:這里可以理解為子類的實例對象,可以通過父類的公有方法獲取父類的私有屬性值 33 fun.x = 10; // 10+10: 這里會發生屬性賦值遮蔽效果,如若是數組push就可以修改到真正的私有屬性值 34 console.log(foo.x);//10: 類的實例可以通過類的公有方法獲取類的私有屬性值 35 console.log(fun.x);//20: 類的實例可以通過類的公有方法獲取類的私有屬性值 36 console.log(fun.num);//10:這個不能說明什么,因為是原始值類型賦值,然讀取被賦值的公有屬性(但是這個賦值發生在實例對象構造時期) 37 foo.x = 10;//10+10:這里可以理解為類的實例調用類的公有方法,給類的私有屬性賦值 38 console.log(fun.x);//20:調用父類的方法讀取父類的私有屬性值 39 foo.sum();//10 + 20:這里同樣是調用父類的方法打印父類的公有屬性與父類的私有屬性的數值之和
關於私有屬性的語法:
- 聲明私有屬性直接使用#開頭作為屬性名,聲明不能在私有屬性前加this,但是使用時必須使用this調用
- 不能在子類調用父類的私有屬性,但是可以在子類定義與父類同名的私有屬性,並且互不干擾,但是我知道你不會這么做
- 不能在constructor中定義私有屬性和方法
- 私有屬性語法同樣需要@babel/plugin-proposal-class-properties插件才能編譯(ES7語法)
- 私有方法語法需要@babel/plugin-proposal-private-methods插件才能編譯
關於私有屬性和方法一直是困擾的問題,什么是私有,私有與靜態的區別是什么?私有與公有又有什么區別?
其實可以這么開描述私有屬性和方法:它不是靜態的,也不是公有的屬性和方法。這就好像哲學一樣,我們可以用“關於數的學科”來描述數學,但是我們不能用“關於哲的學科”來描述哲學一樣,私有屬性和方法不能用類的私有屬性和方法來理解它。
在程序中,靜態屬性和方法中的“靜態”描述的是屬性和方法相對於類是靜止的,通俗的說法就是類在那里出現,靜態屬性和方法就在那里出現。所以靜態屬性和方法永遠是被類名用(.)或者([key])的方式引用。
而公有屬性和方法意思就是只有屬於這個類的所有成員都能擁有,並非公共所有,而是公開所有,每個類的成員動能擁有的屬性和方法。
注意,不管是靜態的屬性和方法,還是公有的屬性和方法,都是相對類而言,相對類靜止、相對類公開所有。靜態就是相對類而言,類在那里出現,靜態屬性和方法才可以在那里出現;公有就是相對類是公開的,公開屬性和方法只要是歸屬於類的對象就可以擁有,這個擁有是對象擁有一個獨立的公開的屬性和方法。
最后,私有同樣也是相對類而言,與私有對立的就是公有,公有屬性和方法有一個非常重要的隱式特性,就是對象擁有這個屬性和方法的所有權限的意思,實例對象可以任意的將這個屬性和方法賦值給別的對象,可以任意的讀寫屬性和方法。而相對公有屬性的私有屬性,就是屬於類的不公開所有的屬性和方法(不能繼承),也就是說類的實例對象沒有私有屬性和方法的所有權,類的實例對象不能對私有屬性和方法進行讀寫。但是類的實例對象繼承了類的公有方法,類在定義公有方法時,類自己可以將私有屬性和方法用在任意自己公開的方法中。
所以,實例對象可以調用類定義的公開方法獲取和修改私有屬性,可以調用類的公開方法執行私有方法。實例對象一定要通過類的公有方法才能使用私有屬性和方法。
私有方法:
1 class Foo{ 2 #a(){ 3 console.log("我是一個私有方法"); 4 } 5 n(){ 6 console.log("我是一個公有方法,我可以調用一個私有方法"); 7 this.#a() 8 } 9 } 10 11 var foo = new Foo(); 12 foo.n(); 13 foo.a();//foo.a is not a function 14 foo.#a();//這樣導致無法編譯
使用私有方法時,babel編譯器還需要一個的插件:
npm install @babel/plugin-proposal-private-methods --save-dev
然后,再.babel文件中配置:
1 { 2 "presets":[ 3 "@babel/preset-env" 4 ], 5 "plugins": [ 6 ["@babel/plugin-proposal-class-properties", { "loose": true }], 7 ["@babel/plugin-proposal-private-methods", { "loose": true }] //引入支持編譯私有方法的插件 8 ] 9 }
詳細可以了解:https://babeljs.io/docs/en/babel-plugin-proposal-private-methods#via-cli
1 class Foo{ 2 #a(){ 3 console.log("我是一個私有方法"); 4 } 5 n(){ 6 console.log("我是一個公有方法,我可以調用一個私有方法"); 7 this.#a(); 8 } 9 } 10 class Fun extends Foo{ 11 constructor(){ 12 super() 13 } 14 m(){ 15 this.n(); 16 // this.#a();//無法編譯,子類不能直接調用父類的方法 17 } 18 } 19 var foo = new Foo(); 20 var fun = new Fun(); 21 foo.n(); 22 fun.m();//子類可以調用包含私有方法的父類公有方法
四、修飾器(Decorator)
修飾器是面向切面編程思想的,用來修改類的行為的API。ES7引入這項功能,Babel轉碼器已經支持。在了解和使用修飾器之前,先來看一個案例需求和模擬實現:
需求一:模擬搜索引擎,輸入搜索內容,點擊搜索按鈕實現搜索功能(不需要實際實現,只需要觸發觸發點擊事件,打印出“向+urlA+發送請求+data")。
需求二:不改變搜所引擎基本功能(即不修改第一條需求的代碼),當搜索功能被觸發時,同時模擬實現記錄搜索關鍵字的搜索次數(只需要將搜索數據發送到另一個url,即打印出“向+urlB+發送請求+data”)。
使用ES5的js語法基於面向切面的編程思想,實現以上需求:
1 <input type="text" name="" id="inp"> 2 <button id="but">搜索</button> 3 <script> 4 var inpDom = document.getElementById("inp"); 5 var butDom = document.getElementById("but"); 6 7 var keyValue = ""; 8 inpDom.oninput = function(){ 9 keyValue = this.value; 10 } 11 12 var requestFun = dealFun(getContent); 13 butDom.onclick = function(){ 14 requestFun(keyValue); 15 } 16 17 //模擬實現的搜索請求功能 18 function getContent(data){ 19 var url = "urlA" 20 console.log("向" + url + "發送請求搜索信息,數據:" + data); 21 } 22 23 //模擬實現的關鍵字記錄請求功能,並應用面向切面編程思想代理搜索請求功能 24 function dealFun(func){ 25 return function(data){ 26 var url = "urlB"; 27 console.log("向" + url + "發送請求記錄關鍵字,數據:" + data); 28 return func.apply(this,arguments); 29 } 30 } 31 </script>
測試效果:
1 向urlB發送請求記錄關鍵字,數據:TC39 2 向urlA發送請求搜索信息,數據:CT39
使用ES7修飾器基於面向切面編程思想,實現模擬案例需求:
1 let inpDom = document.getElementById("inp"); 2 let butDom = document.getElementById("but"); 3 4 //實現搜索功能的類 5 class Search{ 6 constructor(){ 7 this.keyValue = ""; 8 } 9 #url = 'urlA'; 10 @dealFun 11 getContent(){ 12 console.log("向" + this.#url + "發送請求搜了信息,數據:" + this.keyValue); 13 } 14 } 15 //模擬實現關鍵字記錄請求功能,基於ES7修飾器應用面向切面編程實現 16 function dealFun(proto,key,descriptor){ 17 let dealGetContent = descriptor.value; 18 descriptor.value = function(){ 19 let urlDeal = "rulB"; 20 console.log("向" + urlDeal + "發送請求記錄關鍵字,數據:" + this.keyValue) 21 return dealGetContent.apply(this,arguments); 22 } 23 } 24 //實例化搜索對象 25 let oS = new Search(); 26 27 inpDom.oninput = function(){ 28 oS.keyValue = this.value; 29 } 30 butDom.onclick = function(){ 31 oS.getContent(); 32 }
測試效果:
向rulB發送請求記錄關鍵字,數據:TC39
向urlA發送請求搜了信息,數據:TC39
ES7修飾器(Decorator)轉碼插件:
npm install --save-dev @babel/plugin-proposal-decorators
在.babelrc中配置裝飾器:
1 { 2 "plugins": [ 3 ["@babel/plugin-proposal-decorators", { "legacy": true }], 4 ["@babel/plugin-proposal-class-properties", { "loose" : true }] 5 ] 6 }
修飾器配置手冊:https://babeljs.io/docs/en/babel-plugin-proposal-decorators#legacy
一個修飾器問題:https://github.com/WarnerHooh/babel-plugin-parameter-decorator/issues/1
什么是修飾器?裝飾器有什么功能?
修飾器就是基於現有的屬性、方法、類,生成或替換一個全新的功能。可以給現有的屬性重新配置屬性描述符或重新賦值,甚至可以使用一個全新的值替換。由於類沒有屬性的描述符,但可以給類添加靜態方法,給類重新賦值甚至繼承和被繼承等操作。先了解一些有必要理解的語法:
- 修飾器使用“@”開頭命名,然后取非“@”部分的名稱在類的外面生一個函數。
- 修飾器設置在需要裝飾的類、屬性、方法的上方,獨立一行。
- 修飾器可以是方法,也可以是函數執行后放回的函數,函數執行返回函數就是將修飾器作為函數執行一樣放到被裝飾者的前面,可以傳入參數,例如:@fun(data),這個修飾器執行需要放回一個方法用來裝飾被裝飾者。
- 修飾器不能裝飾私有方法和屬性,可以修飾靜態屬性方法和公開屬性方法。
修飾器與屬性:
1 class Search{ 2 @dealAttr 3 static attr = 10; 4 } 5 function dealAttr(proto,key,descriptor){ 6 // proto:屬性所屬對象--靜態屬性指向類;公有屬性指向構造方法constructor 7 // key:屬性名稱 8 // descriptor:修飾對象--屬性描述符+initializer; 9 // 可以給initializer配置一個函數,函數返回值會作用於屬性的初始值 10 descriptor.value = 20;//通過屬性描述符value定義屬性初始值 11 }
修飾器與方法:
1 @dealSearch 2 class Search{ 3 @dealFun 4 getContent(){ 5 console.log("向" + this.#url + "發送請求搜了信息,數據:" + this.keyValue); 6 } 7 } 8 function dealFun(proto,key,descriptor){ 9 // proto:方法所屬對象--公有屬性指向構造方法constructor,靜態屬性指向類; 10 // key:方法名稱 11 // descriptor:修飾對象--屬性描述符 12 descriptor.valuer = function(){}//將方法指向一個全新的函數 13 }
修飾器與類:
1 @decorator 2 class A{} 3 function decorator(proto){ 4 //proto 指向類本身 5 return newClass; 6 } 7 //同等與 8 A = decorator(A) || A;
修飾器的表示方式:
@decorator //裝飾器對應的函數作用於裝飾對象 @decorator() //裝飾器對應的函數執行后返回的函數作用於裝飾對象