Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lessfish opened this issue May 22, 2016 · 16 comments
Open

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

lessfish opened this issue May 22, 2016 · 16 comments

Comments

@lessfish
Copy link
Owner

lessfish commented May 22, 2016

Why underscore

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

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

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

欢迎围观~ (如果有兴趣,欢迎 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

@yaodingyd
Copy link

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.

@daijinma
Copy link

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

@fegg
Copy link

fegg commented Sep 12, 2016

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

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

@zhaosaisai
Copy link

而被重写的 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

@image72
Copy link

image72 commented Nov 5, 2016

what '造' mean in Chinese ?

@lessfish
Copy link
Owner Author

lessfish commented Nov 5, 2016

@image72 means "知道"

@tsanie
Copy link

tsanie commented Jun 22, 2017

关于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"]

@seaskymonster
Copy link

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

@rookiePrgrmer
Copy link

rookiePrgrmer commented Sep 30, 2017

@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更合适。

@aswind7
Copy link

aswind7 commented Nov 3, 2017

@seaskymonster

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

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

可以去看 mdn 详解。

@wy1009
Copy link

wy1009 commented Nov 27, 2017

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

@lingxia
Copy link

lingxia commented Nov 30, 2017

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

@yinguangyao
Copy link

其实我还是不理解为什么不全部用_.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来判断反而靠谱的多。

@wangliang1124
Copy link

wangliang1124 commented Mar 22, 2018

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

@269378737
Copy link

269378737 commented Jul 26, 2018

@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);
            }
        }
    }