原型污染和猴子補丁 Prototype Pollution and Monkey-Patching

上兩篇介紹了原型對象和原型鏈:

JavaScript對象創建模式:http://blog.csdn.net/hongse_zxl/article/details/44595809

深入理解JavaScript的原型對象 :http://blog.csdn.net/hongse_zxl/article/details/44622997

原型對象是JavaScript模擬類並實現繼承的靈魂。這一篇介紹兩個典型的問題:原型污染和猴子補丁

原型污染 Prototype Pollution

先看個例子:

function Person() { }               //先定義個空函數(空函數也有對應的原型對象)

//原型對象中聲明兩個方法,一個count,一個otherFunc
Person.prototype.count = function() {  //count方法統計原型對象中有多少個屬性和方法
    var i = 0;
    for (var prop in this) { i++; }
    return i;
};
Person.prototype.otherFunc  = function() { };  //隨便定義個空方法,起名叫otherFunc

var p = new Person();
p.name = "Jack";      //爲對象添加兩個屬性name和age
p.age = 32;

alert(p.count());     //4

有了前兩篇的基礎,應該能明白爲何最後結果爲4,而不是2。對象p有兩個屬性name和age,而Person是個空函數,預想應該返回2纔對。但實際結果返回了4,枚舉時將對象屬性(name,age)和原型對象中的方法(count,otherFunc)都算進去了。這就是原型污染。

原型污染是指當枚舉條目時,可能會導致出現一些在原型對象中不期望出現的屬性和方法。

上面這個例子只是拋磚引玉引出原型污染的概念,並不具備太多現實意義,一個更現實的例子:

var book = new Array();
book.name = "Love in the Time of Cholera";  //《霍亂時期的愛情》看完後整個人生都在裏面
book.author = "Garcia Marquez";             //加西亞馬爾克斯著。另推薦《百年孤獨》,永遠的馬孔多
book.date = "1985";

alert(book.name);   //Love in the Time of Cholera


定義個Array對象,用於管理書本。結果很正確,看似沒什麼問題,但這個代碼很脆弱,一不小心就會遇到原型污染的問題:

//爲Array增加兩個方法,first和last(猴子補丁後面會介紹)
Array.prototype.first = function() {  //獲取第一個
    return this[0]; 
};
Array.prototype.last = function() {   //獲取最後一個
    return this[this.length-1];
};

var bookAttributes = [];  //定義個book的屬性的數組
for (var v in book) {     //將上面創建的Array對象book中屬性一個個取出來,加入數組中
    bookAttributes.push(v);
}
alert(bookAttributes);    //name,author,date,first,last
我們定義了個book對象,裏面有name書名,author作者,date出版日這3個屬性。通過枚舉將3個屬性加入到bookAttributes數組中後,發現不僅這3個屬性,連Array的原型對象中的方法也被加入到了數組中了,這不是我們希望看到的

你可以用hasOwnProperty方法,來測試屬性是否來自對象而非來自原型對象:

var bookAttributes = [];
for (var v in book) { 
    if(dict.hasOwnProperty(v)){  //爲每個屬性加上hasOwnProperty的測試
        bookAttributes.push(v);  //只有對象自身的屬性纔會被加入數組
    }
}
alert(bookAttributes);    //name,author,date
當然更好的方式應該是僅僅將Object的直接實例作爲字典,而非Array,或Object的子類(如上述Person,函數本身也是Object):

var book = {};      //等價於var book = new Object(),不是new Array() 
book.name = "Love in the Time of Cholera";
book.author = "Garcia Marquez";
book.date = "1985";

var bookAttributes = [];
for (var v in book) { 
    bookAttributes.push(v);
}

alert(bookAttributes);     // name,author,date 這樣就避免了原型污染

當然你可能疑惑:仍舊可以像在Array.prototype中加入猴子補丁一樣,在Object.prototype中增加屬性,這樣不還是會導致原型污染嗎?確實如此,但Object對象是JavaScript的根對象,即便技術上能夠實現,你也永遠不要對Object對象做任何修改。

如果你是做業務項目,上述這些已經足以讓你避免原型污染問題了。不過如果你要開發通用的庫,還需要考慮些額外的問題。

比如,你的庫中提供has方法,能判斷對像中是否有該屬性(非來自原型對象的屬性),你可能這麼做:

function Book(elements) {
    this.elements = elements || {};
}
Book.prototype.has = function(key) {
    return this.elements.hasOwnProperty(key);
};

var b = new Book({
    name : "Love in the Time of Cholera",
    author : "García Márquez",
    date : "1985"
});
alert(b.has("author"));  //true
alert(b.has("has"));     //false
你在Book的原型對象中添加了has方法,判斷傳入的屬性是否是對象自身的屬性,如果是,返回true,如果不是(比如來自原型對象的屬性)則返回false。結果表明author來自對象,因此返回了true,而has來自原型對象,因此返回了false。

一切都很完美,但萬一有人在對象中有一個自定義的同名的hasOwnProperty屬性,這將覆蓋掉ES5提供的Object.hasOwnProperty。當然你會認爲絕不可能有人會將一個屬性起名爲hasOwnProperty。但作爲通用接口,你最好不做任何假設,可以用call方法改進:

Book.prototype.has = function(key) {
    return {}.hasOwnProperty.call(this.elements, key);
};
運行結果和改進前一樣,沒有任何區別,但現在就算有人在對象中定義了同名的hasOwnProperty屬性,has方法內仍舊會正確調用ES5提供的Object.hasOwnProperty方法。

猴子補丁 Monkey-Patching

猴子補丁的吸引力在於方便,數組缺少一個有用的方法?加一個就是了:

Array.prototype.split = function(i) { 
    return [this.slice(0, i), this.slice(i)];
};
環境太舊,不支持ES5中Array的新方法如forEach,map,filter?加上就是了:

if (typeof Array.prototype.map !== "function") {  //確保如存在的話,它不被覆蓋
    Array.prototype.map = function(f, thisArg) {
        var result = [];
        for (var i = 0, n = this.length; i < n; i++) {
            result[i] = f.call(thisArg, this[i], i);
        }
        return result;
    };
}
但是當多個庫給同一原型打猴子補丁時會出現問題,如項目中依賴的另一個庫也有個Array的split方法,但和上面的實現不同:

Array.prototype.split = function() {
    var i = Math.floor(this.length / 2);
    return [this.slice(0, i), this.slice(i)];
};
現在對Array調用split方法會有50%的機率出錯,這取決於哪個庫哪個版本先被加載(假設它們之間沒有依賴的先後順序)被調用。
解決方案是,將想要的版本封裝起來:
function addArrayMethods() {
    Array.prototype.split = function(i) {
        return [this.slice(0, i), this.slice(i)];
    };
};
需要調用split方法時,改爲調用封裝函數可以避免錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章