原型污染和猴子补丁 Prototype Pollution and Monkey-Patching,monkeypatching

转自文章 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方法时,改为调用封装函数可以避免错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值