Better

Ethan的博客,欢迎访问交流

谈谈猴子补丁和原型污染

听这个名字就很有意思,特意去查资料学习了解,发现他其实只是一个有趣的名词而已,自己的代码中经常用到,但是这么一说就显得贼专业!

动态语言与静态语言

动态语言

又称弱类型语言,动态语言是在运行时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。 例如PHP/ASP/Ruby/Python/Perl/ABAP/SQL/JavaScript/Unix Shell等等。

动态类型语言,就是类型的检查是在运行时做的,是不是合法的要到运行时才判断,例如JavaScript就没有编译错误,只有运行错误。

动态语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化。比如JavaScript便是一个典型的动态语言。

数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。

静态语言

又称强类型语言,静态语言是在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型。 例如:C++、Java、Delphi、C#等。

强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。举个例子:如果你定义了一个整型变量a,那么程序根本不可能将a当作字符串类型处理。强类型定义语言是类型安全的语言。

猴子补丁

猴子补丁的含义是指在动态语言中,不去改变源码而对功能进行追加和变更。

猴子补丁这种东西充分利用了动态语言的灵活性,可以对现有的语言Api进行追加,替换,修改Bug,甚至性能优化等等。

JavaScript中使用

由于对象共享原型,因此每一个对象都可以增加、删除或修改原型的属性。这个有争议的实践通常称为猴子补丁。

猴子补丁的吸引力在于其强大。数组缺少一个有用的方法吗?你自己就可以增加它。

Array.prototype.split=function(i){
   return [this.slice(0,i),this.slice(i)];
}

很完美,现在可以在任意的数组上调用这个方法了。但当多个库以不兼容的方式给同一个原型打猴子补丁时,另外的库使用同一个方法名给Array.prototype打猴子补丁。

Array.prototype.split=function(){
   var i=Math.floor(this.length);
   return [this.slice(0,i),this.slice(i)];
}

这样一来,使用split方法很有可能会出错,

解决办法:

  1. 如果库仅仅是将给原型打猴子补丁作为一种便利,那么可以将这些修改置于一个函数中,用户可以选择调用或忽略。
    function addArrayMethods(){
     Array.prototype.split=funciton(i){
         return [this.slice(0,i),this.slice(i)]
     }
    }
    
  2. ployfill
    if(typeof Array.prototype.map!=="function"){
     Array.prototype.map=function(f,thisArg){
        var res=[];
        for(var i=0,n=this.length;i < n;i++){
          res[i]=f.call(thisArg,this[i],i);
        }
        return res;
    }
    }
    

猴子补丁使用注意:

  • 避免使用轻率的猴子补丁
  • 记录程序库所执行的所有猴子补丁
  • 考虑通过将修改置于一个导出函数中,使猴子补丁成为可选的
  • 使用猴子补丁为缺失的标准API提供polyfills

原型污染

原型污染是指当枚举条目时,可能会导致出现一些在原型对象中不期望出现的属性和方法。

先看例子:

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

避免原型污染造成影响的解决办法:

  1. 你可以用hasOwnProperty方法,来测试属性是否来自对象而非来自原型对象。
  2. 当然更好的方式应该是仅仅将Object的直接实例作为字典,而非Array,或Object的子类

当然你可能疑惑:仍旧可以像在Array.prototype中加入猴子补丁一样,在Object.prototype中增加属性,这样不还是会导致原型污染吗?确实如此,但Object对象是JavaScript的根对象,即便技术上能够实现,你也永远不要对Object对象做任何修改。

如果你是做业务项目,上述这些已经足以让你避免原型污染问题了。不过如果你要开发通用的库,还需要考虑些额外的问题。

比如,你的库中提供has方法,能判断对像中是否有该属性(非来自原型对象的属性),你可能这么做:

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

一切都很完美,但万一有人在对象中有一个自定义的同名的hasOwnProperty属性,这将覆盖掉ES5提供的Object.hasOwnProperty。当然你会认为绝不可能有人会将一个属性起名为hasOwnProperty。但作为通用接口,你最好不做任何假设,可以用call方法改进:

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

polyfill和shim的区别

polyfill 是 shim 的一种。

Polyfill你可以理解为“腻子”,就是装修的时候,可以把缺损的地方填充抹平。

有些人就写对应的Polyfill(Polyfill有很多),帮你把这些差异化抹平,不支持的变得支持了(简单来讲,写些代码判断当前浏览器有没有这个功能,没有的话,就写一些支持的补丁代码)。

举个例子,有些旧浏览器不支持Number.isNaN方法,Polyfill就可以是这样的:

if(!Number.isNaN) {
    Number.isNaN = function(num) {
        return(num !== num);
    }
}

shim 是将不同 api 封装成一种,比如 jQuery 的 $.ajax 封装了 XMLHttpRequest 和 IE 用 ActiveXObject 方式创建 xhr 对象;

polyfill 特指 shim 成的 api 是遵循标准的,其典型做法是在IE浏览器中增加 window.XMLHttpRequest ,内部实现使用 ActiveXObject。

在实际中为了方便做对比,会特指 shim 的 api 不是遵循标准的,而是自己设计的。

大神带路



留言