上兩篇介紹了原型對象和原型鏈:
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方法時,改爲調用封裝函數可以避免錯誤。