悄悄地说一个bug

前言

underscore.js源码分析第四篇,前三篇地址分别是,如果你对这个系列感兴趣,欢迎点击watch,随时关注动态。

教你认清这8大杀手锏

那些不起眼的小工具?

(void 0)与undefined之间的小九九

原文地址
源码地址

逗我呢?哥!�你要说什么bug,什么bug,什么bug,我最讨厌bug。去他妹的bug。

客观别急,今天真的是要说一个bug,也许你早已知晓,也许你时常躺枪于他手,悄悄地,我们慢慢开始。

for in 遍历对象属性时存在bug

for in 遍历对象属性时存在bug

for in 遍历对象属性时存在bug

使用for in去遍历一个对象俺们再熟悉不过了,经常干这种事,那他到底可以遍历一个对象哪些类型的属性呢? 长得帅的还是看起来美美的,瞎说,它能够遍历的是对象身上那些可枚举标志([[Enumerable]])为true的属性。

  1. 对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true
  2. 对于通过 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

  1. 可以看到我们手动地用defineProperty,给某个对象设置属性时,enumerable为false此时该属性是不可枚举的
  2. Person继承自Object构造函数,但是for in并没有枚举出Object原型上的一些方法
  3. 手动地覆盖对象原型上面的方法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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • 序 从最近的js入门系列的阅读量逐步递减,观众老爷的兴趣也不再能够接受一些细节性的地方深度挖掘,让我有了一些思考。...
    zhaolion阅读 1,578评论 5 19
  • 1.属性的简洁表示法 允许直接写入变量和函数 上面代码表明,ES6 允许在对象之中,直接写变量。这时,属性名为变量...
    雨飞飞雨阅读 1,130评论 0 3
  • 1)Computer Resources: 计算机资源,硬件。 2)kernel:内核,包围在硬件设备之上。作用:...
    廖马儿阅读 546评论 0 0