一 概覽
??這篇文章是我通過閱讀《JavaScript 高級程序設(shè)計(jì)》和《你不知道的JavaScript》中關(guān)于 繼承 模塊的一點(diǎn)心得。
二 面向?qū)ο蠡仡?/h3>
面向類編程
??你是否還記得大學(xué)里面剛學(xué)C++的時候關(guān)于面向?qū)ο蟮慕榻B呢辜纲,讓我們一塊來回顧一下吧默责。
??類的 定義:在面向?qū)ο缶幊讨械媛保愂且环N 代碼組織結(jié)構(gòu)形式,一種從真實(shí)世界到軟件設(shè)計(jì)的建模方法。
??類的 組織形式:面向?qū)ο蠡蛘呙嫦蝾惥幊虖?qiáng)調(diào) 數(shù)據(jù) 和 操作數(shù)據(jù)的行為 應(yīng)該 封裝 在一起,在正式計(jì)算機(jī)科學(xué)中我們稱為 數(shù)據(jù)結(jié)構(gòu)玻靡。
類與23種高級設(shè)計(jì)模式
??類是面向?qū)ο蟮?底層設(shè)計(jì)模式,它是面向?qū)ο?3種高級設(shè)計(jì)模式的 底層機(jī)制中贝。
??你或許還聽說過 過程化編程,一種不借助高級抽象囤捻,僅僅由 過程(函數(shù))調(diào)用 來組織代碼的編程方式。程序語言中邻寿,Java只支持面向類編程蝎土,C/C++/Php既支持過程化編程,也支持面向類編程绣否。
類的機(jī)制
??在類的設(shè)計(jì)模式中誊涯,它為我們提供了 實(shí)例化、 繼承蒜撮、多態(tài) 3種機(jī)制暴构。
??構(gòu)造器:類的實(shí)例由類的一種特殊方法構(gòu)建,這個方法的名稱通常與類名相同段磨,稱為 “構(gòu)造器(constructor)”取逾。這個方法的明確的工作,就是初始化實(shí)例所需的所有信息(狀態(tài))苹支。
??實(shí)例化:借助構(gòu)造函數(shù)菌赖,由通用類到具體對象的過程。
??繼承:子類通過 拷貝(請一定要記住這個詞)父類的屬性和方法沐序,從而使自己也能擁有這些屬性與方法的過程琉用。
??多態(tài):由繼承產(chǎn)生的堕绩,子類重寫從父類中繼承的屬性和方法,從而子類更加具體邑时。
類的繼承
(1)相對多態(tài):任何方法都可以引用位于繼承層級上更高一層的其他方法(同名或不同名)奴紧。我們說“相對”,因?yàn)槲覀儾唤^對定義我們想訪問繼承的哪一層(也就是類)晶丘,而實(shí)質(zhì)上在說“向上一層”來相對地引用黍氮。
(2)超類:在許多語言中,使用 super 關(guān)鍵字來引用 父類或祖先類浅浮。
(3)如果子類覆蓋父類的某個方法沫浆,原版的方法和覆蓋后的方法都是可以存在的,允許訪問滚秩。
(4) 不要讓多態(tài)搞糊涂专执,子類并不是鏈接到父類上,子類只是父類的一個副本郁油,類繼承的實(shí)質(zhì)是拷貝行為本股。
(5)多重繼承:子類的父類不止一個,JavaScript不支持多重繼承桐腌。
混入
原理: 子構(gòu)造函數(shù)混入父構(gòu)造函數(shù)的屬性和方法拄显。
JavaScript的復(fù)合類型以 引用 的方式傳遞,不支持拷貝行為。混入(Mixin) 以 手動拷貝 的方式模擬繼承的拷貝行為案站。
明確混入:
(1)定義:顯示的把一個對象的屬性混入另一個對象躬审。
(2)實(shí)現(xiàn)如下:
// 另一種mixin,對覆蓋不太“安全”
// 大幅簡化的`mixin(..)`示例:
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 僅拷貝非既存內(nèi)容
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
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!" );
}
} );
(3)顯示假想多態(tài):Vehicle.drive.call(this)蟆盐。因?yàn)镋S6之前盒件,JavaScript無法實(shí)現(xiàn)相對多態(tài)(inherit:drive()),所以我們明確地用名稱指出Vehicle對象舱禽,然后在它上面調(diào)用drive()函數(shù)。
(4)問題:
??A.技術(shù)上講恩沽,函數(shù)沒有被復(fù)制誊稚,只是復(fù)制了函數(shù)的引用;
??B.在每一個需要建立 假想多態(tài) 引用的函數(shù)中都需要建立手動鏈接(Vehicle.drive.call(this))罗心,維護(hù)成本高里伯。可以嘗試通過它實(shí)現(xiàn) 多重繼承渤闷。
(5)結(jié)論:明確混入復(fù)雜疾瓮、難懂、維護(hù)成本高飒箭,不推薦使用狼电。
寄生繼承:
(1)明確的mixin模式的一個變種蜒灰,在某種意義上是明確的而在某種意義上是隱含的。
(2)實(shí)現(xiàn)如下:在子構(gòu)造函數(shù)中new一個如果找函數(shù)的實(shí)例對象肩碟,在這個對象上擴(kuò)展屬性强窖、方法,最后將這個對象返回削祈。
// “傳統(tǒng)的JS類” `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();
// 現(xiàn)在, 我們修改`car`使它特化
car.wheels = 4;
// 保存一個`Vehicle::drive()`的引用
var vehDrive = car.drive;
// 覆蓋 `Vehicle::drive()`
car.drive = function() {
vehDrive.call( this );
console.log( "Rolling on all " + this.wheels + " wheels!" );
};
return car;
}
var myCar = new Car();
myCar.drive();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!
(3)問題:子函數(shù)的初始化創(chuàng)建對象丟失翅溺,改變了this綁定,不過不用new去直接創(chuàng)建髓抑。
隱式混入
(1)定義:父咙崎、子構(gòu)造函數(shù)在原有構(gòu)造函數(shù)與屬性、方法之間吨拍,添加一層函數(shù)褪猛,子構(gòu)造函數(shù)中間函數(shù)的this綁定到父構(gòu)造函數(shù)中間函數(shù)
(2)實(shí)現(xiàn)原理:利用了this的二次綁定。
(3) 實(shí)現(xiàn)如下:
var Something = {
cool: function() {
this.greeting = "Hello World";
this.count = this.count ? this.count + 1 : 1;
}
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
cool: function() {
// 隱式地將`Something`混入`Another`
Something.cool.call( this );
}
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (不會和`Something`共享狀態(tài))
(4) 問題:單純的利用this的二次綁定密末,不能實(shí)現(xiàn)相對應(yīng)用握爷。
(5) 結(jié)論:謹(jǐn)慎使用。
三 原型
prototype
prototype 定義:JavaScript中每個對象都擁有一個prototype屬性严里,它只是一個 其他對象的引用新啼。幾乎所有的對象在被創(chuàng)建時,它的這個屬性都被賦予了一個 非null 值刹碾。
類
“類”函數(shù)
代碼如下:
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
結(jié)論: 當(dāng)通過調(diào)用new Foo()創(chuàng)建實(shí)例對象時燥撞,實(shí)例對象會被鏈接到Foo.prototype指向的對象。
拷貝與鏈接
代碼如下:
function Foo() {
}
Foo.prototype.fruit = ['apple'];
// foo1的[prototype]鏈接到了 Foo.prototype
var foo1 = new Foo();
foo1.fruit.push('banana');
// foo2的[prototype]也被鏈接到了 Foo.prototype
var foo2 = new Foo();
foo2.fruit // [apple, banana]
??在面向類的語言中迷帜,可以創(chuàng)造一個類的多個拷貝物舒。在JavaScript中,我們不能創(chuàng)造一個類的多個實(shí)例戏锹,可以創(chuàng)建多個對象冠胯,它們的[prototype]鏈接指向一個共同對象。但默認(rèn)地锦针,沒有拷貝發(fā)生荠察,如此這些對象彼此間最終不會完全分離和切斷關(guān)系,而是 鏈接在一起奈搜。
??“繼承”意味著 拷貝 操作悉盆,而JavaScript不拷貝對象屬性(原生上,默認(rèn)地)馋吗。相反焕盟,JS在兩個對象間建立鏈接,一個對象實(shí)質(zhì)上可以將對屬性/函數(shù)的訪問 委托 到另一個對象上宏粤。對于描述JavaScript對象鏈接機(jī)制來說脚翘,“委托”是一個準(zhǔn)確得多的術(shù)語灼卢。
new調(diào)用和普通調(diào)用本質(zhì)相同
??JavaScript中,new在某種意義上劫持了普通函數(shù)堰怨,并將它以另一種函數(shù)調(diào)用:構(gòu)建一個對象芥玉,外加調(diào)用這個函數(shù)所做的任何事。
實(shí)例對象沒有constructor屬性
function Foo() { }
var foo1 = new Foo();
foo1.constructor === Foo // true
// 修改Foo.prototype指向的對象
Foo.prototype = {
//
}
var foo2 = new Foo();
foo2.constructor === Foo // false
??a.constructor === Foo為true意味著a上實(shí)際擁有一個.constructor屬性备图,指向Foo灿巧?不對。
??實(shí)際上揽涮,.constructor引用也 委托 到了Foo.prototype抠藕,它 恰好 有一個指向Foo的默認(rèn)屬性。
3 “原型繼承”
原型繼承分析
代碼如下:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
// 構(gòu)造函數(shù)內(nèi)部相對多態(tài)
Foo.call( this, name );
this.label = label;
}
// 這里蒋困,我們創(chuàng)建一個新的`Bar.prototype`鏈接鏈到`Foo.prototype`
Bar.prototype = Object.create( Foo.prototype );
// 注意盾似!現(xiàn)在`Bar.prototype.constructor`不存在了,
// 如果你有依賴這個屬性的習(xí)慣的話雪标,可以被手動“修復(fù)”零院。
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
核心代碼分析:
代碼1:
function Bar(para1, para2) {
Foo.call(this, para1);
//...
}
代碼1分析:構(gòu)造函數(shù)內(nèi)部初始化,利用this綁定村刨,根據(jù)父構(gòu)造函數(shù)初始化子構(gòu)造函數(shù)內(nèi)部告抄。
代碼2:
Bar.prototype = Object.create(Foo.prototype)
代碼2分析:原型初始化,將子構(gòu)造函數(shù)的[prototype]鏈接到父構(gòu)造函數(shù)的[prototype]鏈接的對象嵌牺。
誤區(qū):
Bar.prototype = Foo.prototype
這種方法是錯誤的打洼,子構(gòu)造函數(shù)會污染到父構(gòu)造函數(shù)
ES6 新方法:
Object.setPrototypeOf(Bar.prototype, Foo.prototype)
“自身”
??面向類語言中,根據(jù)實(shí)例對象查找創(chuàng)建它的類模板逆粹,稱為自誓即(或反射)。JavaScript中僻弹,如何根據(jù)實(shí)例對象阿浓,查找它的委托鏈接呢?
1 instanceOf:
代碼如下:
function Foo() {
//...
}
var a = new Foo();
a instanceOf Foo // true
代碼分析:
a: instanceOf 機(jī)制蹋绽,在實(shí)例對象(a)的原型鏈中芭毙,是否有Foo.prototype;
b: 需要用于可檢測的構(gòu)造函數(shù)(Foo);
c: 無法判斷實(shí)例對象間(比如a蟋字,b)是否通過[prototype]鏈相互關(guān)聯(lián)。
2 isPrototypeOf [[prototype]]反射:
代碼如下:
function Foo() {
// ...
}
var a = new Foo();
Foo.prototype.isPrototypeOf(a); // true
// 對象b是否在a的[[prototype]]鏈出現(xiàn)過
b.isPrototypeOf(a);
代碼分析:
a:在實(shí)例對象(a)的原型鏈中扭勉,是否有Foo.prototype鹊奖;
b:需要用于可檢測的構(gòu)造函數(shù)(Foo);
c:可以判斷對象間是否通過[prototype]鏈相互關(guān)聯(lián)涂炎。
3 getPrototypeOf 獲取原型鏈:
代碼如下:
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf(a) // 查看constructor屬性
4 proto:
代碼如下:
function Foo() {
// ...
}
var a = new Foo();
a.__proto__ === Foo.prototype // true
代碼分析:
a:proto屬性在ES6被標(biāo)準(zhǔn)化忠聚;
b:proto屬性跟 constructor屬性類似设哗,它不存在實(shí)例對象中。constructor屬性存在于 原型鏈中,proto存在于Object.prototype中两蟀。
c:proto看起來像一個屬性网梢,但實(shí)際上將它看做是一個getter/setter更合適。
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// setPrototypeOf(..) as of ES6
Object.setPrototypeOf( this, o );
return o;
}
} );
對象關(guān)聯(lián)
創(chuàng)建關(guān)聯(lián)
代碼如下:
var foo = {
printFoo: function() {
console.log('foo');
}
}
var a = Object.create(foo);
a.printFoo(); // 'foo'
代碼分析:
a赂毯、Object.create()會創(chuàng)建一個對象(a)战虏,并把它鏈接到指定對象(foo);
b党涕、相比new 調(diào)用烦感,Object.create()不會產(chǎn)生 prototype引用和 constructor引用。
關(guān)聯(lián)是否備用
代碼如下:
var anotherObject = {
cool: function() {
console.log('cool');
}
}
var bar = Object.create(anotherObject);
bar.cool(); // 'cool'
代碼分析:
a膛堤、單純的在bar無法處理屬性或方法時手趣,建立備用鏈接(anotherObject),代碼會變得很難理解和維護(hù)肥荔,這種模式應(yīng)該慎重使用绿渣;
b、ES6提供“代理”功能燕耿,它實(shí)現(xiàn)的就是“方法無法找到”時的行為中符。