之前打某湖論劍,兩道js的題,給我整懵逼了,發現以前都沒對js做過多少研究,趁著被毒打了,先研究一波js原型鏈,未雨綢繆。
首先我們研究js原型鏈,得搞明白原型是什麼,這裡借用p神的舉的一個例子:
在javascript中,我們定義一個類,需要以定義「建構函式」的方式來定義:
function Foo() {
this.bar = 1
}
new Foo()
Foo()函數的內容就是建構函式的內容,this.bar是Foo的一個屬性,學過c++的應該很容易理解,而且後面對原型鏈的利用也可以仿造c++類的思想來理解。
一個類必然有方法,我們可以在建構函式裡定義方法:
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}
在js裡,這樣定義有一個特點,就是這個方法並不是繫結在類上,而是每建立一個物件,這個方法就會定義一次,這樣就很浪費資源,我們要讓它只定義一次,就需要用到原型prototype
,這個prototype
可以認為是一個類的屬性,通過類建立的物件都將"繼承"prototype
的內容。
function Foo() {
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
那麼__proto__
是幹什麼用的?原來由類範例化的物件是無法直接存取到類的原型也就是prototype
,我們可以看到foo沒有prototype
這時__proto__
就是物件存取類原型的媒介了
以上大概就是原型的內容,讀者把原型的概念弄懂後再去看原型鏈汙染會有不一樣的結果
以上講的prototype
在js裡其實主要是用來實現繼承機制,這個繼承跟c++的繼承不太一樣,js的繼承可以改掉Object類導致原型鏈汙染,而c++的繼承是可以對父類別的方法進行進行重寫或過載,但不會直接就把父類別給改寫了,因為這個特性,這才有了原型鏈汙染的誕生。這裡舉個例子:
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
這裡Son繼承了Father的last_name
這裡通過__proto__
給Object添了個last_name
這裡推導一下:
son.__proto__ == Son.prototype
son.__proto__.__proto__ == Father.prototype
son.__proto__.__proto__.__proto__ == Object
所以後面我們new的a,它有一個last_name是繼承Object的,這裡有趣的是,改寫了Object後,Father.last__name == john,但是son.last_name == Trump,直接看Father函數,其實它的last_name並沒有變化
這就有趣了,但重新new Father,它的last_name其實也是沒變的
但我們還是成功汙染了的,Object多了個last_name屬性,此後新建的類都將繼承這個屬性,在某些地方是會造成危害的
在什麼地方我們可以使用原型鏈汙染?其實當有我們可以控制的"鍵名",並存在有賦值修改操作的地方,我們可以實現這個操作,這裡舉個js的merge函數經典例子:
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
以上是一個簡單的合併函數,我們可以看到target[key] = source[key]
,其實這個地方就存在原型鏈汙染,如果key是__proto__
的話,是不是就能修改到Object,這裡給個例子:
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
這裡我們合併成功了,但是並沒有汙染到Object,原因是__proto__
沒有被當成鍵名,而是當成原型,也就是遍歷鍵名的時候只有a,b是鍵名。
為了讓__proto__
也被當成鍵名,我們可以把o2的值設定成json的格式,遍歷json的時候,__proto__
就會被當成鍵名,從而改寫Object,例子如下:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
成功改寫Object,導致o3有了b屬性
以上就是js原型鏈的基礎內容,把基礎打好,之後遇見原型鏈的題將會成長很快。