使用原型鏈來構(gòu)建項目架構(gòu)
原型和原型鏈
一點(diǎn)廢話
? 現(xiàn)在網(wǎng)上關(guān)于原型鏈這一部分的博客大多是從類繼承模型開始說的。因為 javascript 的繼承模型區(qū)別于很多傳統(tǒng)語言的類繼承称近,它使用的是 prototype 原型模型模擬出繼承的效果吟秩。但是我是做IC前端設(shè)計出身的红碑,想我這種本來就對與類繼承不熟悉的人說這個區(qū)別意義也不大。我之前僅僅接觸過面向過程的編程寓免,從JAVA胸梆,pathon之類的高級語言過渡到j(luò)avascript上面自然而然就會有提到兩者的區(qū)別。對于沒有接觸過的面向?qū)ο缶幊痰娜藖碚f的話最好先能夠?qū)ο笫玻约懊嫦驅(qū)ο蟮木幊逃忠粋€大致的了解蝙叛。因為只有先對“對象”在一個項目里面的意義有所了解俺祠,才會明白我們?yōu)槭裁葱枰玫接迷玩溁蛘咂渌氖裁捶绞絹順?gòu)建項目架構(gòu)公给。到但是如果我要開始從對象開始說的話,這篇文章就會過長蜘渣,所以我假設(shè)正在讀這篇文章的你有著還不錯的邏輯思維能力淌铐,但是對于高級語言這點(diǎn)破事又不太清楚。那么蔫缸,我們開始吧腿准。
原型鏈
? 如果你之前有過編程經(jīng)驗。那么拾碌,我相信你一定知道我們會把一些反復(fù)用到的功能封裝成一個函數(shù)⊥麓校現(xiàn)在,除了函數(shù)以外校翔,在面向?qū)ο蟮恼Z言里面弟跑,還引入了對象這個概念。對于 javascript 來說除了 null 和 undefined 以外都是對象防症。每一個對象都會有對應(yīng)的屬性或者方法孟辑。那么,當(dāng)我們訪問對象的屬性的時候蔫敲,如果這個對象不具有這種屬性饲嗽,那么編譯器就會順著原型鏈向上尋找,一個對象除了會擁有自己的屬性以外奈嘿,還會繼承來自原型鏈上層的父級對象的屬性貌虾,所以編譯器會一直順著原型鏈向上尋找,直到找到那個屬性或者到達(dá)原型鏈末尾裙犹。但是在實(shí)際操作中這樣做會實(shí)際上非常消耗資源尽狠,因為這需要遍歷整個原型鏈,所以很多時候還是使用hasOwnProperty 方法伯诬,這是官方欽定的唯一一種不需要遍歷整個原型鏈就能對屬性進(jìn)行處理的方法晚唇。
上面說的這種依次向上尋找的尋找屬性的方法就是 javascript 中的繼承方式
//// 假定有一個對象 o, 其自身的屬性(own properties)有 a 和 b:
// {a: 1, b: 2}
// o 的原型 o.[[Prototype]]有屬性 b 和 c:
// {b: 3, c: 4}
// 最后, o.[[Prototype]].[[Prototype]] 是 null.
// 這就是原型鏈的末尾,即 null盗似,
// 根據(jù)定義哩陕,null 沒有[[Prototype]].
// 綜上,整個原型鏈如下:
// {a:1, b:2} ---> {b:3, c:4} ---> null
console.log(o.a); // 1
// a是o的自身屬性嗎?是的悍及,該屬性的值為1
console.log(o.b); // 2
// b是o的自身屬性嗎闽瓢?是的,該屬性的值為2
// o.[[Prototype]]上還有一個'b'屬性,但是它不會被訪問到.這種情況稱為"屬性遮蔽 (property shadowing)".
console.log(o.c); // 4
// c是o的自身屬性嗎心赶?不是扣讼,那看看o.[[Prototype]]上有沒有.
// c是o.[[Prototype]]的自身屬性嗎?是的,該屬性的值為4
console.log(o.d); // undefined
// d是o的自身屬性嗎缨叫?不是,那看看o.[[Prototype]]上有沒有.
// d是o.[[Prototype]]的自身屬性嗎椭符?不是,那看看o.[[Prototype]].[[Prototype]]上有沒有.
// o.[[Prototype]].[[Prototype]]為null耻姥,停止搜索销钝,
// 沒有d屬性,返回undefined
上面提到的是關(guān)于原型鏈?zhǔn)且环N理想化的模型琐簇。便于理解蒸健,在通常的情況下,每個函數(shù)都有一個原型屬性 prototype 指向自己的原型婉商,而由這個函數(shù)創(chuàng)建的對象也有一個 _proto_ 屬性指向這個原型似忧。上面說的有點(diǎn)繞,我從另一個角度來說明原型鏈盒原型吧丈秩。先從下面這張圖開始說
我們可以看到這里面有 _proto_ 和 prototype 這兩個東西盯捌。如果你有使用過chrome 的develop tools 或者vscode進(jìn)行單步調(diào)試的經(jīng)驗的話你會經(jīng)常看到 *_proto_* 這個字段癣籽。那么挽唉,這個到底是什么意思呢
_proto_ 和 protptype 兩個有什么關(guān)系與區(qū)別呢?
先看 _proto_ 開始說起
每一個JS對象一定對應(yīng)一個原型對象筷狼,并且從原型對象那里繼承屬性和方法瓶籽。(重點(diǎn))
這話不是我說的,看下面
Every JavaScript object has a second JavaScript object (or null ,
but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.
看完了以后再看下面這個例子
var one = {x: 1};
var two = new Object();
one._proto_ === Object.protopyte //true
two._proto_ === Object.prototype //true
one.toSting === one._protpto_.toString //true
在上面兩個例子里面埂材,one 和 two 兩個對象的原型對象全等于對自己當(dāng)前對象的 _proto_ 屬性塑顺。更進(jìn)一步來說,one 的自己的繼承來自己原型的方法的時候也可以用 _proto_ 來找到俏险。但是這也引入了一個新問題严拒。為什么one 和 two 的原型對象就是Object.protopyte,還有 Object.protopyte 究竟是個啥竖独?
上面這個問題先放一下裤唠,等我講完了prototype再放到一起看
首先,prototype 和 _proto_ 的第一個區(qū)別就在于:每一個對象都會有一個 _proto_ 屬性來標(biāo)示自己所繼承的原型莹痢。但是函數(shù)才會有 prototype 屬性种蘸。當(dāng)我們創(chuàng)建函數(shù)的時候墓赴,JS 會為這個函數(shù)追加一個 prototype 屬性。當(dāng)我們嘗試把這個函數(shù)當(dāng)成一個構(gòu)造函數(shù)來調(diào)用的時候航瞭,那么 JS 就會創(chuàng)建這個構(gòu)造函數(shù)的實(shí)例诫硕,這個事例會繼承構(gòu)造函數(shù) prototype 的所有屬性和方法。同時實(shí)例會通過 _proto_ 指向構(gòu)造函數(shù)的 prototype 刊侯。
于是 JS 就是這樣通過 _proto_ 和 prototype 來實(shí)現(xiàn)原型鏈章办。
構(gòu)造函數(shù)就是通過 prototype 來保存要共享給實(shí)例的屬性和方法。
對象的 _proto_ 總是指向自己的構(gòu)造函數(shù)的 prototype 滨彻。 大概可以這樣描述一下
obj._proto_._proto_ === Constructor.prototype
之類的藕届。
最后再補(bǔ)充一下,原型鏈的頂端就是 Object.prototype 疮绷。因為在之前的圖片里面 Object.prototype 的上層 _proto_ 指向的是 null
君與this與apply與call那點(diǎn)破事
在討論完原型鏈之后翰舌,還有一個繞不開的問題就是 this 的指向問題,還有 apply 和 call 到底是咋回事的問題冬骚。
this
最開始接觸到 this 是在廖雪峰的教程里面,當(dāng)時只記得 this 有設(shè)計缺陷啥的懂算,理解也不是特別深刻只冻。我現(xiàn)在常識對之前的問題祖宗一個總結(jié),嘗試用一種簡單的方式并且全面的把這個部分講清楚计技。
如果你嘗試尋找網(wǎng)上關(guān)于this的資料喜德,那么很大的概率最終會找到這么一篇文章 ,說真的看了這篇文章之后我完全不知道怎么往下寫。畢竟這篇文章說的太好了垮媒。這篇文章分別在全局對象舍悯,函數(shù),原型睡雇,方法中 this 具體的指向問題萌衬。堪稱教科書級別的文章它抱。但是我覺得還是有必要自己的方法去描述一下這個概念秕豫。
就像從零開始寫一個 cpu 那樣,我嘗試使用一種增量模型來描述 this 這個概念观蓄。
-
“ this 指向當(dāng)前對象”
我們姑且先這樣記住混移,然后按照每個特出情況再對這句話進(jìn)行一點(diǎn)修改,讓最后的表述最接近真實(shí)的 this .(會不會有點(diǎn)小題大做了侮穿?)那么在瀏覽器宿主的全局環(huán)境下歌径,this 所指向的對象就是對象 window
console.log(this === Window) // true
-
在函數(shù)中使用 this 的時候,不使用 new 關(guān)鍵字聲明函數(shù)的時候 this 仍然指向 window亲茅,當(dāng)使用 new 的時候指向這個函數(shù)回铛。
//普通的聲明 foo = "bar"; function xx(){ this.foo = "fuck"; } console.log(this.foo); //bar new xx(); console.log(this.foo); //bar console.log(new xx().foo) //fuck
是不是看的有點(diǎn)暈金矛?為什么單獨(dú)new 沒變化,log出去就改變了this的值呢勺届?我們得先知道new的時候編譯器干了啥
英文好的讀 這個 驶俊。下面的你就不用看了,英文不好的接著看免姿。下面是不負(fù)責(zé)任的翻譯:
new干了5件事
- 創(chuàng)造一個對象
- 把構(gòu)造函數(shù)的 prototype 拷貝到 實(shí)例對象的 _proto_ 中饼酿。
- 讓 this 這個變量指向新創(chuàng)建的對象
- 使用新創(chuàng)見的對象執(zhí)行構(gòu)造函數(shù)
- 最后返回新創(chuàng)建的對象
下面是兩個例子
ObjMaker = function() {this.a = 'first';}; // 我叫 ObjMaker 是一個普通的構(gòu)造函數(shù) ObjMaker.prototype.b = 'second'; // 和所有的函數(shù)一樣,我有很多可以改變的原型屬性(prototype property)胚膊,現(xiàn)在我被增加了一個叫 b 的屬性故俐。 // 與此同時,我還是一個對象.那么作為一個對象紊婉,我也會有很多不可訪問的內(nèi)部屬性([[prototype]] property) obj1 = new ObjMaker(); // 現(xiàn)在我作為一個構(gòu)造函數(shù)構(gòu)造了一個叫obj1的對象 // 發(fā)生了三件事 // 1. 一個全新的叫obj1的空對象背創(chuàng)建了药版。類似于這樣 obj1 = {} // 2. obj1 的[[prototype]] 屬性被設(shè)置為ObjMaker.prototype(如果ObjMaker.prototype在此之后又作為構(gòu)造 函數(shù)生成了一個新對象,obj1 的 [[prototype]] 不會改變但是你可以通過修改ObjMaker.porototype來添加原型) // 3. 執(zhí)行這個函數(shù)
附上new的執(zhí)行代碼
function New(func) { var res = {}; if (func.prototype !== null) { res.__proto__ = func.prototype; } var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } return res; }
上面雖然 new 了一個新的函數(shù)喻犁,但是這個構(gòu)造函數(shù)并沒有指向找到合適的對象返回出去槽片,而在調(diào)用console.log的時候相當(dāng)于生成了一個新的對象,然后把這個沒有名字的新生成的對象打印出去了肢础。
說到這还栓,基本上new的東西就講完了,如果想有更近一步的了解传轰,你可以去看一下廖雪峰的教程 剩盒。(我還是覺得了廖雪峰的教程不是特別適合完全的新手看。)new 就先說到這慨蛙,我們接著看this相關(guān)的內(nèi)容
-
在原型中指向當(dāng)前對象辽聊,但是在原型鏈中原型鏈底層函數(shù)中對this的操作會覆蓋上層的值
function Thing1() { } Thing1.prototype.foo = "bar"; function Thing2() { this.foo = "foo"; } Thing2.prototype = new Thing1(); function Thing3() { } Thing3.prototype = new Thing2(); var thing = new Thing3(); console.log(thing.foo); //logs "foo" //原型鏈 thing -> Thing3 -> Thing2 -> Thing1 //每一次new 操作都會讓 this 跳轉(zhuǎn)一次,當(dāng)編譯器在查找方法或者屬性的時候會依次向上尋找期贫, //當(dāng)編譯器在thing2這層找到foo屬性的時候就停止查找跟匆,這樣就屏蔽了Thing1這一層的foo屬性
實(shí)際上上面這個例子也是 JS 在模擬經(jīng)典的對象繼承
apply與call
終于講到 apply 了。其實(shí)這兩個在有了上面的基礎(chǔ)以后唯灵,一句話就能說清楚贾铝。apply 和 call 就是用于改變函數(shù)內(nèi)部 this 的指向。當(dāng)某個對象埠帕,比如A對象有 a 方法垢揩,B對象有 b 方法,那么如果想讓A對象也具有b方法敛瓷,那么使用apply或者call就可以完成這樣一個操作叁巨。
call和apply都是Function.prototype 下面的一個方法,所有的函數(shù)都具備這兩種方法呐籽,這兩種方法都具有相同的效果锋勺,唯一的區(qū)別在于調(diào)用方式不同蚀瘸。我們用一個簡單的例子來說明問題。
function ghost(){};//我們定義一個鬼兵
ghost.prototype = {
EMP : function(){
//emp code here
},
HasSnipe : 0庶橱,
Say : function(){
console.log("Waiting on you");
}
} //這是一個升級了EMP但是沒有升級狙擊贮勃,有一句臺詞的的鬼兵。
var nova = new ghost;//諾娃是一個光榮的帝國鬼兵苏章,具有鬼兵的技能寂嘉。
var Scott = {};//Scott是一個新兵蛋子,他剛接受了帝國鬼兵的訓(xùn)練枫绅,可以使用技能泉孩,但是系統(tǒng)并沒有為他分配武器。
//然而在偵查到一個小隊滿能量的哨兵并淋,于是諾娃的武器給Scott了寓搬,
//命令Scott去使用EMP去清空那些哨兵的能量與護(hù)盾。
//在系統(tǒng)層面县耽,我們這么做
Scott.EMP.call(nova);
然后諾娃掏出了她的大吊大刀把那些燒餅干掉了句喷,就是這樣。
實(shí)際上在這個例子已經(jīng)很清楚了酬诀。更進(jìn)一步的描述在于這里
下面抄一個正式一點(diǎn)的例子脏嚷,為那些不玩SC的同學(xué)們準(zhǔn)備的
function Man() {}
Man.prototype = {
valet: false,
wakeUp: function(event) {
console.log(this.valet + "? Some breakfase, please.");
}
};
//get "undefined? Some breakfast, please
var button = document.getElementById('morning');
button.addEventListener(
"click",
wooster.wakeUp,
false
);
//使用apply來改變 wakeUp 的上下文環(huán)境,即 wakeUp 中的this
var button = document.getElementById('morning2');
button.addEventListener(
"click",
function() {
Man.prototype.wakeUp.apply(wooster, arguments);
},
false
);
擴(kuò)展閱讀
http://www.cnblogs.com/TomXu/archive/2012/01/05/2305453.html
https://segmentfault.com/a/1190000002634958
https://developer.mozilla.org/en/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
http://bonsaiden.github.io/JavaScript-Garden/zh/#object.prototype