原型污染和猴子补丁 Prototype Pollution and Monkey

上两篇介绍了原型对象和原型链:

JavaScript对象创建模式:

深入理解JavaScript的原型对象 :

原型对象是JavaScript模拟类并实现继承的灵魂。这一篇介绍两个典型的问题:原型污染和猴子补丁

原型污染 Prototype Pollution

先看个例子:

function Person() { }//先定义个空函数(空函数也有对应的原型对象)//原型对象中声明两个方法,一个count,一个otherFuncPerson.prototype.count = function() { //count方法统计原型对象中有多少个属性和方法var i = 0;for (var prop in this) { i++; }return i;};Person.prototype.otherFunc = function() { }; //随便定义个空方法,起名叫otherFuncvar p = new Person();p.name = "Jack";//为对象添加两个属性name和agep.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")); //truealert(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方法时,改为调用封装函数可以避免错误。

告诉自己,我这次失败了,

原型污染和猴子补丁 Prototype Pollution and Monkey

相关文章:

你感兴趣的文章:

标签云: