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

悄悄地说一个bug #7

Open
qianlongo opened this issue May 11, 2017 · 0 comments
Open

悄悄地说一个bug #7

qianlongo opened this issue May 11, 2017 · 0 comments

Comments

@qianlongo
Copy link
Owner

qianlongo commented May 11, 2017

前言

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。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant