前言
underscore.js源码分析第四篇,前三篇地址分别是,如果你对这个系列感兴趣,欢迎点击watch,随时关注动态。
逗我呢?哥!你要说什么bug,什么bug,什么bug,我最讨厌bug。去他妹的bug。
客观别急,今天真的是要说一个bug,也许你早已知晓,也许你时常躺枪于他手,悄悄地,我们慢慢开始。
for in 遍历对象属性时存在bug
for in 遍历对象属性时存在bug
for in 遍历对象属性时存在bug
使用for in
去遍历一个对象俺们再熟悉不过了,经常干这种事,那他到底可以遍历一个对象哪些类型的属性呢? 长得帅的
还是看起来美美的
,瞎说,它能够遍历的是对象身上那些可枚举标志([[Enumerable]])为true
的属性。
- 对于通过直接的赋值和属性初始化的属性,该标识值默认为即为
true
- 对于通过 Object.defineProperty 等定义的属性,该标识值默认为 false
举个例子哪些属性可以被枚举
let Person = function (name, sex) {
this.name = name
this.sex = sex
}
Person.prototype = {
constructor: Person,
showName () {
console.log(this.name)
},
showSex () {
console.log(this.sex)
}
}
Person.wrap = {
sayHi () {
console.log('hi')
}
}
var p1 = new Person('qianlongo', 'sex')
p1.sayBye = () => {
console.log('bye')
}
p1.toString = () => {
console.log('string')
}
Object.defineProperty(p1, 'info', {
enumerable: false,
configurable: false,
writable: false,
value: 'feDev'
});Ï
for (var key in p1) {
console.log(key)
}
// name
// sex
// sayBye
// constructor
// showName
// showSex
// toString复制代码
- 可以看到我们手动地用defineProperty,给某个对象设置属性时,enumerable为false此时该属性是不可枚举的
- Person继承自Object构造函数,但是
for in
并没有枚举出Object原型上的一些方法 - 手动地覆盖对象原型上面的方法
toString
也是可枚举的
如何判断一个对象的属性是可枚举的
方式其实很简单,使用原生js提供的
Object.propertyIsEnumerable
来判断
let obj = {
name: 'qianlongo'
}
let obj2 = {
name: 'qianlongo2',
toString () {
return this.name
}
}
obj.propertyIsEnumerable('name') // true
obj.propertyIsEnumerable('toString') // false
obj2.propertyIsEnumerable('name') // true
obj2.propertyIsEnumerable('toString') // true复制代码
为什么obj判断toString为不可枚举属性,而obj2就是可枚举的了呢?原因很简单,obj2将toString
重写了,而一个对象自身直接赋值的属性是可被枚举的
说了这么多,接下来我们来看一下下划线中涉及到遍历的部分对象方法,come on!!!
_.has(object, key)
判断对象obejct是否包含key属性
平时你可能经常这样去判断一个对象是否包含某个属性
if (obj && obj.key) {
// xxx
}复制代码
但是这样做有缺陷,比如某个属性其对应的值为0,null,false,''空字符串呢?这样明明obj有以下对应的属性,却因为属性值为假而通过不了验证
let obj = {
name: '',
sex: 0,
handsomeBoy: false,
timer: null
}复制代码
所以我们可以采用下划线中的这种方式
源码
var hasOwnProperty = ObjProto.hasOwnProperty;
_.has = function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
};复制代码
_.keys(object)
获取object对象所有的属性名称。
使用示例
let obj = {
name: 'qianlongo',
sex: 'boy'
}
let keys = _.keys(obj)
// ["name", "sex"]复制代码
源码
_.keys = function(obj) {
// 如果obj不是object类型直接返回空数组
if (!_.isObject(obj)) return [];
// 如果浏览器支持原生的keys方法,则使用原生的keys
if (nativeKeys) return nativeKeys(obj);
var keys = [];
// 注意这里1、for in会遍历原型上的键,所以用_.has来确保读取的只是对象本身的属性
for (var key in obj) if (_.has(obj, key)) keys.push(key);
// Ahem, IE < 9.
// 这里主要处理ie9以下的浏览器的bug,会将对象上一些本该枚举的属性认为不可枚举,详细可以看collectNonEnumProps分析
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};复制代码
collectNonEnumProps函数分析
该函数为下划线中的内部函数一枚,专门处理ie9以下的枚举bug问题,
for in
到底有啥bug,终于可以说出来了。
简单地说就是如果对象将其原型上的类似toString
的方法覆盖了的话,那么我们认为toString
就是可枚举的了,但是在ie9以下的浏览器中还是认为是不可以枚举的,又是万恶的ie
源码
// 判断浏览器是否存在枚举bug,如果有,在取反操作前会返回false
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
// 所有需要处理的可能存在枚举问题的属性
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
// 处理ie9以下的一个枚举bug
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
// 读取obj的原型
var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
// 这里我有个疑问,对于constructor属性为什么要单独处理?
// Constructor is a special case.
var prop = 'constructor';
if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
// nonEnumerableProps中的属性出现在obj中,并且和原型中的同名方法不等,再者keys中不存在该属性,就添加进去
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
keys.push(prop);
}
}
}复制代码
代码看起来并不复杂,但是有一个小疑问,对于constructor属性为什么要单独处理呢?各个看官,如果知晓,请教我啊
_.allKeys(object)
获取object中所有的属性,包括原型上的。
举个简单的例子说明
let Person = function (name, sex) {
this.name = name
this.sex = sex
}
Person.prototype = {
constructor: Person,
showName () {
console.log(this.name)
}
}
let p = new Person('qianlongo', 'boy')
_.keys(p)
// ["name", "sex"] 只包括自身的属性
_.allKeys(p)
// ["name", "sex", "constructor", "showName"] 还包括原型上的属性复制代码
接下来看下源码是怎么干的
源码
// 获取对象obj的所有的键
// 与keys不同,这里包括继承来的key
// Retrieve all the property names of an object.
_.allKeys = function(obj) {
if (!_.isObject(obj)) return [];
var keys = [];
// 直接读遍历取到的key,包括原型上的
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys); // 同样处理一下有枚举问题的浏览器
return keys;
};复制代码
可以看到和_.keys的唯一的不同就在于遍历obj的时候有没有用hasOwnProperty
去判断
_.values()
返回object对象所有的属性值。
使用案例
let obj = {
name: 'qianlongo',
sex: 'boy'
}
_.values(obj)
// ["qianlongo", "boy"]复制代码
源码
// Retrieve the values of an object's properties.
_.values = function(obj) {
// 用到了前面已经写好的keys函数,所以values认为获取的属性值,不包括原型
var keys = _.keys(obj);
var length = keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values;
};复制代码
_.invert(object)
返回一个object副本,使其键(keys)和值(values)对换。
使用案例
let obj = {
name: 'qianlongo',
secName: 'qianlongo',
age: 100
}
_.invert(obj)
// {100: "age", qianlongo: "secName"}复制代码
注意哟,如果对象中有些属性值是相等的,那么翻转过来的对象其key取最后一个
源码
_.invert = function(obj) {
var result = {};
// 所以也只是取对象本身的属性
var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
// 值为key,key为值,如果有值相等,后面的覆盖前面的
result[obj[keys[i]]] = keys[i];
}
return result;
};复制代码
_.functions(object)
返回一个对象里所有的方法名, 而且是已经排序的(注意这里包括原型上的属性)
源码
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
// 是函数,就装载进去
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort(); // 最后返回经过排序的数组
};复制代码
结尾
夜深人静,悄悄地说一个bug这个鬼故事讲完了,各位good night。