你不知道的JavaScript——this全面解析(下)
混入
JavaScript和傳統的面向物件程式語言有很大差異,實際上它沒有類,只有物件,那麼自然也就不存在繼承、多型這些東西。
混入是在JS中實現傳統面向物件模式的一個辦法。
function mixin(parentObj,childObj){
for(var key in parentObj){
if(!(key in childObj))
childObj[key] = parentObj[key];
}
return childObj;
}
如上,混入就是賦予子元素所有在它身上沒有的他爹身上的屬性,這不就是繼承嘛。
看個例子:
var Vehicle = { engines: 1, ignition: function() { console.log("Turning on my engine."); }, drive: function() { this.ignition(); console.log("Steering and moving forward"); } }; var Car = mixin(Vehicle,{ wheels: 4, drive: function(){ Vehicle.drive.call(this); console.log("Rolling on all "+this.wheels+" wheels!"); } }); Car.drive(); /* Turning on my engine. Steering and moving forward Rolling on all 4 wheels! */
分析,Car通過mixin
,獲得了Vehicle
的engine
屬性(值)和ignition
方法(引用),而drive
則保留自己的,實現了子類重寫父類方法,並且子類新增了一個wheels
屬性。
Vehicle.drive.call(this)
就是一個多型模式,因為父類和子類都有drive
的定義,這裡是使用了父類的drive
方法,並將自己作為上下文繫結進去,如果不使用call而是直接呼叫,那就是在Vehicle
物件的上下文中呼叫。顯然對這個示例沒什麼影響,但是這不是我們所期望的。
寄生繼承
寄生繼承更像是委託設計模式。
// 定義Vehicle物件 function Vehicle(){ this.engines = 1; } Vehicle.prototype.ignition = function(){ console.log("Turning on my engine."); } Vehicle.prototype.drive = function(){ this.ignition(); console.log("Steering and moving forward!"); } // 定義Car物件 function Car(){ // Car是一個Vehicle var car = new Vehicle(); // 新增屬性 car.wheels = 4; // 保留父類方法 var vehDrive = car.drive; // 重寫父類方法 car.drive = function(){ // 呼叫父類方法 vehDrive.call(this); console.log("Rolling on all " + this.wheels + " wheels!"); } // 返回物件 return car; } var myCar = new Car(); myCar.drive();
只是個人覺得這樣寫挺醜。
原型
前面提到了JS中沒有類,只有物件,也介紹了兩種用來在JS中描述類之間結構關係的辦法,那JS有沒有啥自帶的辦法??
好像每個JS物件都有toString
方法,valueOf
方法......這是典型的繼承操作嘛,所以JS肯定有類似的東西,那就是Prototype
——原型。
JS中的每個物件都有一個__proto__
屬性,它也是一個物件(後文稱它proto物件),一般這個物件不會為空,這個物件描述了JS中物件之間的繼承關係。
通過Object.create(obj)
可以顯式的把一個物件obj
作為一個新物件的proto物件。
var parent = { a: 10 };
var child = Object.create(parent);
當我們檢視這個child
物件時,你會清楚的看到parent
物件成為了這個物件的proto物件,這時你就可以訪問child.a
了,當訪問一個物件中並不存在的屬性時,就會去檢查proto物件中是否有相應的屬性。
而且child
的proto物件中也有一個proto物件,這是再上一級的父物件,這一層一層的proto物件就構成了JS中的原型鏈,原型鏈的末尾是Object.prototype
,也就是這裡定義了toString
、valueOf
這些方法。訪問屬性時會沿著原型鏈向上查詢。當使用for in
、key in value
時也會檢查原型鏈。
顯然這個例子中再上一級的proto物件就是Object.prototype
了,因為沒有其他的繼承關係了。
引用!引用!
注意,原型鏈並不複製物件,而是直接儲存引用。
可以看到,child.__proto__
與parent
根本就是同一個物件。
再看賦值操作
obj.a = 10;
- 先檢查obj中是否有a這個屬性,如果有直接修改賦值。
- 檢查obj的原型鏈中是否有a這個屬性,如果有且
writable == true
的話,在obj中建立a屬性並賦值 - 如果obj的原型鏈中有這個屬性,並且
writable == false
的話,不會在obj中建立新屬性,如果執行在非嚴格模式,忽略該條語句,如果在嚴格模式,TypeError - 如果obj的原型鏈中這個a屬性有setter的話,呼叫setter,忽略是否可寫
- 都不滿足,說明在obj中和它的原型鏈中都不存在a屬性,那麼在obj中建立a屬性並賦值
我們把它和傳統的面向物件做一個類比。
- 相當於子類中特有的屬性
- 相當於父類中有這個屬性,但子類要覆蓋或重寫
- 相當於父類中有這個屬性,但父類已經為這個屬性設定了一個很高的訪問級別(即不允許寫),那麼子類自然無法低於這個級別
- 沒什麼可以類比的,如果非要類比,可以想成,父類對一個屬性的實際儲存方式已經規劃好了,所有的子類必須要遵守這個規定,所以要呼叫原型鏈當中的
setter
- 也相當於子類中特有的屬性
隱式遮蔽
var parent = { a: 10 };
var child = Object.create(parent);
child.a++;
console.log(child);
console.log(child.__proto__);
該程式碼有個坑,child.a
應該讀取到的是parent
中的a
屬性,那麼對它進行自增,應該操作的是parent
裡面的a
,預期的輸出結果應該是:
{}
{ a: 11 }
但實際的輸出結果是:
{ a: 11 }
{ a: 10 }
因為自增操作實際上是會被轉換成如下程式碼:
child.a = child.a + 1;
所以,根據物件賦值的規則,會在child中新建一個a
,並且把11賦給它,而parent中的a
並未受影響。
跳出面向物件的模式
上面運用了大量的面向物件的類比來介紹原型鏈,其實不過是方便理解罷了,但實際上JS中並沒有類似繼承、類、多型這些概念,它只有物件。同時,本書的作者也對社群中的一些通用的過於面向物件風
的術語嗤之以鼻。
再次重申,JS中沒有類,只有物件,剛剛你看到的不過是通過一系列物件的組合引用,使用一種類似委託的技術創造出了原型鏈模式,來方便的在物件間複用邏輯,也就是定義一個物件時,只考慮它特有的屬性和方法,其他的屬性和方法留空,然後再使用別的物件填滿。後面我們不會再討論繼承、多型、類這些術語了,忘了它們吧。
類函式
下面是被濫用了很多年的用JS中的函式模擬傳統面向物件的“類”特性的寫法,至今還能在一些成熟的框架中看到影子。
function People(name){
this.name = name;
}
People.prototype.eat = function(){
console.log(this.name + ",eating...");
}
People.prototype.drink = function(){
console.log(this.name + ",drinking...");
}
var p1 = new People("張三");
p1.eat();
p1.drink();
前面說了,JS中物件都有一個proto屬性,而函式在JS中也是一個特殊的物件,它自然也有proto屬性,並且當你使用new
關鍵字時,會返回一個物件(見上一篇博文),JS會自動將這個函式的proto屬性作為返回物件的proto屬性。所以p1.__proto__ === People.prototype
是成立的。
這裡已經完全是在模仿傳統面向物件中的類的思路了,提供一套模板方法和屬性,每當例項化一個物件時,就將這些模板方法和屬性複製進去。雖然這裡沒有發生複製,但是整體的效果已經一致了。
累了...明天再寫...