underscore.js源码分析第四篇,前三篇地址分别是,若是你对这个系列感兴趣,欢迎点击watch,随时关注动态。javascript
教你认清这8大杀手锏java
那些不起眼的小工具?git
(void 0)与undefined之间的小九九github
逗我呢?哥!你要说什么bug,什么bug,什么bug,我最讨厌bug。去他妹的bug。浏览器
客观别急,今天真的是要说一个bug,也许你早已知晓,也许你时常躺枪于他手,悄悄地,咱们慢慢开始。函数
for in 遍历对象属性时存在bug
工具
for in 遍历对象属性时存在bug
源码分析
for in 遍历对象属性时存在bug
this
使用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!!!
判断对象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); };
获取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 = []; // 注意这里一、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; };
该函数为下划线中的内部函数一枚,专门处理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属性为何要单独处理呢?各个看官,若是知晓,请教我啊
获取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
去判断
返回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; };
返回一个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 = _.methods = function(obj) { var names = []; for (var key in obj) { // 是函数,就装载进去 if (_.isFunction(obj[key])) names.push(key); } return names.sort(); // 最后返回通过排序的数组 };
夜深人静,悄悄地说一个bug这个鬼故事讲完了,各位good night。