js原型鏈汙染詳解

2023-02-22 15:01:09

前言

之前打某湖論劍,兩道js的題,給我整懵逼了,發現以前都沒對js做過多少研究,趁著被毒打了,先研究一波js原型鏈,未雨綢繆。

基礎

protype

首先我們研究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__

那麼__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原型鏈的基礎內容,把基礎打好,之後遇見原型鏈的題將會成長很快。