转自文章 http://www.bkjia.com/webzh/974992.html
上两篇介绍了原型对象和原型链:
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方法时,改为调用封装函数可以避免错误。