我們會(huì)發(fā)現(xiàn) obj 已經(jīng)有幾個(gè)屬性(方法)了吁讨。那么問題來了:valueOf / toString / constructor 是怎么來第练?我們并沒有給 obj.valueOf 賦值呀雳刺。在理解繼承之前季惯,需要知道 js 的三個(gè)東西:
1仗考、什么是 JS 原型鏈
2、this 的值到底是什么
3请敦、JS 的 new 到底是干什么的
你可能遇到過這樣的 JS 面試題:
我們知道 JS 有對象,比如
var obj = {name:"obj"};
我們通過控制臺(tái)把 obj 打印出來:
我們會(huì)發(fā)現(xiàn) obj 已經(jīng)有幾個(gè)屬性(方法)了储玫。那么問題來了:valueOf / toString / constructor 是怎么來侍筛?我們并沒有給 obj.valueOf 賦值呀。
看如下示意圖
我們發(fā)現(xiàn)控制臺(tái)打出來的結(jié)果是:
1撒穷、obj 本身有一個(gè)屬性 name (這是我們給它加的)
2匣椰、obj 還有一個(gè)屬性叫做proto(它是一個(gè)對象)
3、obj 還有一個(gè)屬性端礼,包括 valueOf, toString, constructor 等
4禽笑、obj.proto其實(shí)也有一個(gè)叫做proto的屬性(console.log 沒有顯示)入录,值為 null
現(xiàn)在回到我們的問題:obj 為什么會(huì)擁有 valueOf / toString / constructor 這幾個(gè)屬性?
答案: 這跟?proto有關(guān) 佳镜。
當(dāng)我們「讀取」 obj.toString 時(shí)僚稿,JS 引擎會(huì)做下面的事情:
1、看看 obj 對象本身有沒有 toString 屬性蟀伸。沒有就走到下一步蚀同。
2、看看 obj.proto對象有沒有 toString 屬性啊掏, 發(fā)現(xiàn) obj.proto有 toString 屬性蠢络, 于是找到了,所以 obj.toString 實(shí)際就是第 2 步中找到的 obj.proto.toString脖律。
3谢肾、如果 obj.proto沒有,那么瀏覽器會(huì)繼續(xù)查看 obj.proto.proto
4小泉、如果 obj.proto.proto也沒有芦疏,那么瀏覽器會(huì)繼續(xù)查看 obj.proto.proto.proto
5、直到找到 toString 或者proto為 null微姊。
上面的過程酸茴,就是「讀」屬性的「搜索過程」。而這個(gè)「搜索過程」兢交,是連著由?proto?組成的鏈子一直走的薪捍。這個(gè)鏈子,就叫做「原型鏈」配喳。
共享原型鏈
現(xiàn)在我們還有另一個(gè)對象
var????obj2 =? ? {name:"obj2"}
那么 obj.toString 和 obj2.toString 其實(shí)是同一東西酪穿, 也就是 obj2.proto.toString。
說白了晴裹,我們改其中的一個(gè)proto.toString 被济,那么另外一個(gè)其實(shí)也會(huì)變!
差異化
如果我們想讓 obj.toString 和 obj2.toString 的行為不同怎么做呢?
直接賦值就好了:
obj.toString = function () {?
????return "新的 toString 方法";
};
小結(jié)
????[讀]屬性時(shí)會(huì)沿著原型鏈搜索
????[新增]屬性時(shí)不會(huì)去看原型鏈
2. this 的值到底是什么
你可能遇到過這樣的 JS 面試題:
var obj = {
? ? foo:function(){console.log(this);? ??
}};
var bar = obj.foo;
obj.foo();// 打印出的 this 是 obj
bar();// 打印出的 this 是 window
函數(shù)調(diào)用
JS(ES5)里面有三種函數(shù)調(diào)用形式:
1涧团、func(p1, p2);
2只磷、obj.child.method(p1, p2);
3、func.call(context, p1, p2);// 先不講 apply
前面兩種都是語法糖泌绣,可以等價(jià)地變?yōu)?call 形式:
func(p1,p2)等價(jià)于?func.call(undefined, p1, p2);
obj.child.method(p1, p2) 等價(jià)于 obj.child.method.call(obj.child, p1, p2);
this 是你 call 一個(gè)函數(shù)時(shí)傳的 context钮追,先看 func(p1, p2) 中的 this 如何確定:
按理說打印出來的 this 應(yīng)該就是 undefined 了吧,但是瀏覽器里有一條規(guī)則:
????如果你傳的 context 就 null 或者 undefined阿迈,那么 window 對象就是默認(rèn)的 context(嚴(yán)格模式下默認(rèn) context 是 undefined)
因此上面的打印結(jié)果是 window元媚。如果你希望這里的 this 不是 window,很簡單:
func.call(obj)// 那么里面的 this 就是 obj 對象了
回到題目:
[ ] 語法
我們可以把 arr0想象為 arr.0( ),雖然后者的語法錯(cuò)了惠毁,但是形式與轉(zhuǎn)換代碼里的 obj.child.method(p1, p2) 對應(yīng)上了犹芹,于是就可以愉快的轉(zhuǎn)換了:
arr[0]();????假想為 arr.0()????然后轉(zhuǎn)換為 arr.0.call(arr)????那么里面的 this 就是 arr 了?
小結(jié):
????this 就是你 call 一個(gè)函數(shù)時(shí),傳入的第一個(gè)參數(shù)鞠绰。
????如果你的函數(shù)調(diào)用不是 call 形式腰埂, 請將其轉(zhuǎn)換為 call 形式
3. JS 的 new 到底是干什么的?
先看一下問題
用一個(gè)函數(shù)把這兩部分聯(lián)系起來:
JS 之父看到大家都這么搞蜈膨,覺得何必呢屿笼,我給你們個(gè)糖吃,于是 JS 之父創(chuàng)建了 new 關(guān)鍵字翁巍,可以讓我們少寫幾行代碼:
只要你在士兵前面使用 new 關(guān)鍵字驴一,那么可以少做四件事情:
1.不用創(chuàng)建臨時(shí)對象,因?yàn)?new 會(huì)幫你做(你使用「this」就可以訪問到臨時(shí)對象)灶壶;
2.不用綁定原型肝断,因?yàn)?new 會(huì)幫你做(new 為了知道原型在哪,所以指定原型的名字 prototype);
3.不用 return 臨時(shí)對象驰凛,因?yàn)?new 會(huì)幫你做胸懈;
4.不要給原型想名字了,因?yàn)?new 指定名字為 prototype恰响。
注意 constructor 屬性
new 操作為了記錄「臨時(shí)對象是由哪個(gè)函數(shù)創(chuàng)建的」趣钱,所以預(yù)先給「士兵.prototype」加了一個(gè) constructor 屬性:
士兵.prototype = {
? ? constructor: 士兵
};
如果你重新對「士兵.prototype」賦值,那么這個(gè) constructor 屬性就沒了胚宦,所以你應(yīng)該這么寫:
或者你也可以自己給 constructor 重新賦值:
四首有、繼承
繼承的本質(zhì)就是上面的講的原型鏈
1)借助構(gòu)造函數(shù)實(shí)現(xiàn)繼承
打印結(jié)果
這個(gè)主要是借用 call 來改變 this 的指向,通過 call 調(diào)用 Parent 枢劝,此時(shí) Parent 中的 this 是指 Child1井联。有個(gè)缺點(diǎn),從打印結(jié)果看出 Child1 并沒有 say 方法您旁,所以這種只能繼承父類的實(shí)例屬性和方法低矮,不能繼承原型屬性/方法。
2)借助原型鏈實(shí)現(xiàn)繼承
要共享莫些屬性被冒,需要 對象.proto?= 父親對象的.prototype,但實(shí)際上我們是不能直接 操作proto坪仇,這時(shí)我們可以借用 new 來做公浪,所以
Child2.prototype = new Parent2(); <=> Child2.prototype.proto?= Parent2.prototype; 這樣我們借助 new 這個(gè)語法糖,就可以實(shí)現(xiàn)原型鏈繼承妓灌。但這里有個(gè)總是跃洛,如打印結(jié)果率触,我們給 s1.play 新增一個(gè)值 ,s2 也跟著改了汇竭。所以這個(gè)是原型鏈繼承的缺點(diǎn)葱蝗,原因是 s1.pro?和 s2.pro指向同一個(gè)地址即 父類的 prototype穴张。
3)組合方式實(shí)現(xiàn)繼承
將 1 和 2 兩種方式組合起來,就可以解決 1 和 2 存在問題两曼,這種方式為組合繼承皂甘。這種方式有點(diǎn)缺點(diǎn)就是我實(shí)例一個(gè)對象的時(shí), 父類 new 了兩次悼凑,一次是 var s3 = new Child3()對應(yīng) Child3.prototype = new Parent3()還要 new 一次偿枕。
4)組合繼承的優(yōu)化 1
這邊主要為 Child4.prototype = Parent4.prototype, 因?yàn)槲覀兺ㄟ^構(gòu)造函數(shù)就可以拿到所有屬性和實(shí)例的方法户辫,那么現(xiàn)在我想繼承父類的原型對象渐夸,所以你直接賦值給我就行,不用在去 new 一次父類渔欢。其實(shí)這種方法還是有問題的墓塌,如果我在控制臺(tái)打印以下兩句:
從打印可以看出,此時(shí)我是沒有辦法區(qū)分一個(gè)對象 是直接 由它的子類實(shí)例化還是父類呢奥额?我們還有一個(gè)方法判斷來判斷對象是否是類的實(shí)例苫幢,那就是用 constructor,我在控制臺(tái)打印以下內(nèi)容:
咦,你會(huì)發(fā)現(xiàn)它指向的是父類 披坏,這顯然不是我們想要的結(jié)果态坦, 上面講過我們 prototype 里面有一個(gè) constructor, 而我們此時(shí)子類的 prototype 指向是 父類的 prototye ,而父類 prototype 里面的 contructor 當(dāng)然是父類自己的,這個(gè)就是產(chǎn)生該問題的原因棒拂。
組合繼承的優(yōu)化 2
這里主要使用Object.create()伞梯,它的作用是將對象繼承到proto屬性上。舉個(gè)例子:
那大家可能說這樣解決了嗎帚屉,其實(shí)沒有解決,因?yàn)檫@時(shí) Child5.prototype 還是沒有自己的 constructor,它要找的話還是向自己的原型對象上找最后還是找到 Parent5.prototype, constructor 還是 Parent5 ,所以要給 Child5.prototype 寫自己的 constructor:
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;