underscore-analysis icon indicating copy to clipboard operation
underscore-analysis copied to clipboard

for ... in 存在的浏览器兼容问题你造吗

Open lessfish opened this issue 8 years ago • 16 comments

Why underscore

最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。

阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。

之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。

  • underscore-1.8.3 源码全文注释 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/underscore-1.8.3-analysis.js
  • underscore-1.8.3 源码解读项目地址 https://github.com/hanzichi/underscore-analysis
  • underscore-1.8.3 源码解读系列文章 https://github.com/hanzichi/underscore-analysis/issues

欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力

for ... in

今天要跟大家聊聊 for ... in 在浏览器中的兼容问题。

for ... in 大家应该都不陌生,循环只遍历可枚举属性。像 Array 和 Object 使用内置构造函数所创建的对象都会继承自 Object.prototype 和 String.prototype 的不可枚举属性,例如 String 的 indexOf() 方法或者 Object 的 toString 方法。循环将迭代对象的所有可枚举属性和从它的构造函数的 prototype 继承而来的(包括被覆盖的内建属性)。

我们举个简单的例子:

var obj = {name: 'hanzichi', age: 30};

for (var k in obj) {
  console.log(k, obj[k]);
}

// 输出
// name hanzichi
// age 30

等等,你跟我说 for ... in 这玩意有浏览器兼容性?!从来没注意过啊,好像工作中也没碰到过这样的兼容性问题啊!确实如此,for ... in 要出问题,得满足两个条件,其一是在 IE < 9 浏览器中(又是万恶的 IE!!),其二是被枚举的对象重写了某些键,比如 toString。

还是举个简单的例子:

var obj = {toString: 'hanzichi'};

for (var k in obj) {
  alert(k);
}

ok,在 chrome 中我们 alert 出了预期的 "toString",而在 IE 8 中啥都没有弹出。

我们回头看看 for ... in 的作用,循环遍历 可枚举属性,那么显然 IE 8 将 toString "内定" 成了不可枚举属性(尽管已经被重写)。那么如何判断是否在类似 IE 8 这样的环境中呢?underscore 中有个 hasEnumBug 函数就是用来做这个判断的:

// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
// IE < 9 下 不能用 for key in ... 来枚举对象的某些 key
// 比如重写了对象的 `toString` 方法,这个 key 值就不能在 IE < 9 下用 for in 枚举到
// IE < 9,{toString: null}.propertyIsEnumerable('toString') 返回 false
// IE < 9,重写的 `toString` 属性被认为不可枚举
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');

代码一目了然,用了 propertyIsEnumerable 方法。

那么哪些属性被重写之后不能用 for ... in 在 IE < 9 下枚举到呢?有如下这些:

// IE < 9 下不能用 for in 来枚举的 key 值集合
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
                    'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];

恩,应该还漏了个 constructor。

我们来看看 underscore 是怎么做的。

function collectNonEnumProps(obj, keys) {
  var nonEnumIdx = nonEnumerableProps.length;
  var constructor = obj.constructor;

  // proto 是否是继承的 prototype
  var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;

  // Constructor is a special case.
  // `constructor` 属性需要特殊处理
  // 如果 obj 有 `constructor` 这个 key
  // 并且该 key 没有在 keys 数组中
  // 存入数组
  var prop = 'constructor';
  if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);

  // nonEnumerableProps 数组中的 keys
  while (nonEnumIdx--) {
    prop = nonEnumerableProps[nonEnumIdx];
    // prop in obj 应该肯定返回 true 吧?是否不必要?
    // obj[prop] !== proto[prop] 判断该 key 是否来自于原型链
    if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
      keys.push(prop);
    }
  }
}

proto 变量保存了原型,一个对象的原型可以通过 obj.constructor.prototype 获取,但是如果重写了 constructor 很显然就无法这样获取了,则用 Object.prototype 替换。这样比如说重写了 toString,我们只需要比较 obj.toString 是否和 proto.toString 引用相同即可。个人觉得源码中的 prop in obj 判断多余了,这不肯定返回 true 吗?如果有理解错误,望指出。

而对于重写了 constructor 的情况,underscore 用 hasOwnProperty 进行判断。

对于重写了以上几种属性的情况,underscore 确实能够获取其在 IE < 9 中的键,但是爱钻牛角尖的楼主也十分不解,constructor 真的有必要和其他属性分开来检测吗?

对于 toString 这样的属性被重写,underscore 的判断非常好,如果没有被重写,那么对象的 toString 方法肯定是继承于原型链的,判断对象的 toString 方法是否和原型链上的一致即可,但是用 hasOwnProperty 能判断吗?楼主觉得也是可以的,hasOwnProperty 方法用来判断对象的 key 是否是自有属性,即是否来自于原型链,如果被重写了,那么应该会返回 true,否则 false。

而被重写的 constructor 能否用 obj[prop] !== proto[prop] 来判断呢?楼主觉得也是可以的,如果没有被重写,那么 obj.constructor === obj.constructor.prototype.constructor 返回 true,如果被重写,obj.constructor === Object.prototype.constructor 返回 false。

关于这点,楼主也是百思不得其解,但是很显然 constructor 属性和其他属性是有明显区别的,从代码理解角度来看,也是 underscore 这样处理比较容易接受。如果是楼主理解有出入的地方,还望指出!

最后,小结下,对于 for ... in 在 IE < 9 下的兼容问题,楼主感觉并没有那么重要,毕竟谁会没事去重写这些属性呢!所以,知道有这么一回事就可以了。

最后的最后,给出这部分源码位置,有兴趣的同学可以看下 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L904-L946

lessfish avatar May 22 '16 12:05 lessfish

I don't understand why you use two different conditions to check if an object's property is being overwritten. Also in this case, I think obj.constructor === obj.constructor.prototype.constructor will always return true and obj.constructor === Object.prototype.constructor will always return false, overwritten or not.

yaodingyd avatar Aug 09 '16 14:08 yaodingyd

这个 我跟楼主一样 看的也是一脸懵逼

daijinma avatar Aug 19 '16 01:08 daijinma

prop in obj && obj[prop] !== proto[prop] 应该是 obj 的 constructor 被重写时候的判断吧。

var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; 这只是预防 obj 被直接量重写 constructor 的指向变化。

fegg avatar Sep 12 '16 11:09 fegg

而被重写的 constructor 能否用 obj[prop] !== proto[prop] 来判断呢?楼主觉得也是可以的,如果没有被重写,那么 obj.constructor === obj.constructor.prototype.constructor 返回 true,如果被重写,obj.constructor === Object.prototype.constructor 返回 false。

楼主,你感觉下面的这种情况有意义吗?但是这种情况用的场景还是挺多的,就是显示声明一个对象的constructor。这个时候,严谨的说是更改constructor的指向

var obj = {constructor:Object}
//这时候
obj.constructor === Object.prototype.constructor   //true

zhaosaisai avatar Nov 04 '16 08:11 zhaosaisai

what '造' mean in Chinese ?

image72 avatar Nov 05 '16 08:11 image72

@image72 means "知道"

lessfish avatar Nov 05 '16 08:11 lessfish

关于prop in obj,应该是为了避免一种特殊情况,就是obj的原型链是null的情况。(如果不要这个判断的话,会是以下结果)

var o = {}
var keys = []
collectNonEnumProps(o, keys)
// keys = []

o.__proto__ = null
// or Object.setPrototypeOf(o, null)
collectNonEnumProps(o, keys)
// keys = ["toLocaleString", "hasOwnProperty", "propertyIsEnumerable", "toString", "isPrototypeOf", "valueOf"]

tsanie avatar Jun 22 '17 05:06 tsanie

_.has(obj, prop) 与 (prop in obj) 有什么区别呢?

seaskymonster avatar Sep 07 '17 22:09 seaskymonster

@tsanie 你说的是对的。 把collectNonEnumProps方法通过“_”导出成公有方法以后,执行以下代码: var obj = Object.create(null); var keys = []; _.collectNonEnumProps(obj, keys);

如果把“prop in obj”注掉的话,keys = ["hasOwnProperty", "propertyIsEnumerable", "toString", "isPrototypeOf", "valueOf"], 打开注释的话,keys=[]。

这里发现少了toLocalString,是因为Object.prototype中也没有这个属性。

不过这里用Object.create(null)举例可能不太合适,毕竟这是ES5的方法,但collectNonEnumProps方法时专门为ES3准备的,可能obj.__proto__ = null更合适。

rookiePrgrmer avatar Sep 30 '17 07:09 rookiePrgrmer

@seaskymonster

_.has的实现:
	_.has = function(obj, key) {
		return obj != null && hasOwnProperty.call(obj, key);
	};

hasOwnProperty 会获取 obj对象 本身的 可迭代和不可迭代的属性, 不会获取原型上面的任何属性; for ... in 会获取obj对象本身和 原型上面的可迭代的属性, 不会获取不可迭代的属性。

可以去看 mdn 详解。

aswind7 avatar Nov 03 '17 06:11 aswind7

@2json 请问那么为什么不能全部用hasOwnProperty的方法判断呢?也就是不单独处理constructor,而是将所有属性都像constructor一样处理。实际上,我根本就不明白为什么单单要把constructor单独处理……

wy1009 avatar Nov 27 '17 13:11 wy1009

你好,请问下_.keys和_.allKeys方法的主要区别在哪里?虽然从代码和代码注释上可以看到,.allKeys遍历所有的属性名,而.keys只遍历自有属性(包括被重写的继承来的属性),但是我没有找到两个方法返回结果不同的例子。所有尝试的结果两者返回的都是一样的,可以帮忙举个返回结果不同的例子吗?谢谢

lingxia avatar Nov 30 '17 12:11 lingxia

其实我还是不理解为什么不全部用_.has来判断,还有确实像上面所说可能会出现这种情况:

var obj = {constructor:Object}
//这时候
obj.constructor === Object.prototype.constructor   //true

但是其他方法也一样会出现这种情况,比如:

var toString = Object.prototype.toString
var obj = {toString: toString}

我认为这一样是没法用prop in obj && obj[prop] !== proto[prop]这句来判断的,因为一定会返回false。如果用_.has来判断反而靠谱的多。

yinguangyao avatar Mar 14 '18 15:03 yinguangyao

发现另外一个bug: 假如obj的constructor重写,并且constructor.ptototype重写toString var Func = function() {}; Func.prototype.toString = null; var obj = { constructor: Func }; // 返回值是 =>是["constructor", "toString"] 问题出在这句 obj[prop] !== proto[prop]

wangliang1124 avatar Mar 22 '18 05:03 wangliang1124

@tsanie 你说的这个我测试出来有些不同,下面是我的测试结果

var o = {}
var keys = []
collectNonEnumProps(o, keys)
//  keys = ['toString']

o.__proto__ = null
collectNonEnumProps(o, keys)
// keys = []

另外我看的 underscorecollectNonEnumProps 方法代码如下:

function collectNonEnumProps(obj, keys) {
        var nonEnumIdx = nonEnumerableProps.length;
        var proto = typeof obj.constructor === 'function' ? FuncProto : ObjProto;

        while (nonEnumIdx--) {
            var prop = nonEnumerableProps[nonEnumIdx];
           //  if判断我觉得可以改成 if (_.has(obj, prop) && !_.contains(keys, prop)) { }
            if (prop === 'constructor' ? _.has(obj, prop) : prop in obj &&
                obj[prop] !== proto[prop] && !_.contains(keys, prop)) {  
                keys.push(prop);
            }
        }
    }

269378737 avatar Jul 26 '18 05:07 269378737

楼主,我在IE9以下的浏览器中测试发现,for..in遍历对象属性时,toString是可以alert出来的啊,你是怎么测试的呢?

var obj = {
    toString: null
}


for(key in obj) {
    alert(key)
}

Luobinf avatar Sep 22 '20 07:09 Luobinf