小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
今天来跟大家聊聊Object的一些相关的工具函数,比如keys、allkeys、extends、pick、clone等函数。
keys和allkeys
_.keys({one: 1, two: 2, three: 3});
=> ["one", "two", "three"]
// allkeys
function Stooge(name) {
this.name = name;
}
Stooge.prototype.silly = true;
_.allKeys(new Stooge("Moe"));
=> ["name", "silly"]
先看keys,如何获取一个对象的key呢(不包括原型链的),直接看代码吧,思路如下:
var nativeKeys = Object.keys;
// Retrieve the names of an object's own properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`.
function keys(obj) {
// 不是一个对象,直接返回 []
if (!isObject(obj)) return [];
// Object.keys(),支持Object.keys()就优先使用
if (nativeKeys) return nativeKeys(obj);
var keys = [];
// for-in循环会遍历原型上可枚举的所有属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回
// 只遍历自身属性,不要继承来的
for (var key in obj) if (has$1(obj, key)) keys.push(key);
// Ahem, IE < 9.
// IE < 9 下,重写了 toString 等方法时的兼容性问题
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
}
思路比较简单,但是最后却调用了collectNonEnumProps函数,作用到底是什么呢?
原来是for ... in 存在的浏览器兼容问题,直接贴代码(大佬的注释),大概思路就是:
- 判断在当前环境下时候存在兼容问题,列举有问题的属性;
- 判断传入对象的constructor属性是否被修改,如果没有就用构造函数的原型对象,修改了就是Object.prototype
- 对象有constructor并且keys数组没有,就添加进去
- 循环存在bug的属性列表,依次取出,判断其属性值是否和原型对象上的值相同,不同则说明被重写了添加进去
// 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` 属性被认为不可枚举
// 据此可以判断是否在 IE < 9 浏览器环境中
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
// IE < 9 下不能用 for in 来枚举的 key 值集合
// 其实还有个 `constructor` 属性
// 个人觉得可能是 `constructor` 和其他属性不属于一类
// nonEnumerableProps[] 中都是方法
// 而 constructor 表示的是对象的构造函数
// 所以区分开来了
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
// obj 为需要遍历键值对的对象
// keys 为键数组
// 利用 JavaScript 按值传递的特点
// 传入数组作为参数,能直接改变数组的值
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
// 获取对象的原型
// 如果 obj 的 constructor 被重写
// 则 proto 变量为 Object.prototype
// 如果没有被重写
// 则为 obj.constructor.prototype
var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
// Constructor is a special case.
// `constructor` 属性需要特殊处理 (是否有必要?)
// see https://github.com/hanzichi/underscore-analysis/issues/3
// 如果 obj 有 `constructor` 这个 key
// 并且该 key 没有在 keys 数组中
// 存入 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);
}
}
}
那么allKeys的实现和这也大概相同,就直接代码吧:
// Retrieve all the enumerable property names of an object.
function allKeys(obj) {
if (!isObject(obj)) return [];
var keys = [];
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
}
和keys的区别就在于for...in的时候是否需要判断是自身属性
获取到了对象的keys那么实现values和pairs方法就很简单了
// _.values({one: 1, two: 2, three: 3}); ==> [1, 2, 3]
// Retrieve the values of an object's properties.
function values(obj) {
var _keys = keys(obj); // 获取对象自身所有的keys
var length = _keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) { // 循环获取对象的key对应的属性值
values[i] = obj[_keys[i]];
}
return values;
}
我看起pairs是和Object.entrie一样功能,但是这竟然没用Object.entrie做判断
// _.pairs({one: 1, two: 2, three: 3}); // => [["one", 1], ["two", 2], ["three", 3]]
// Convert an object into a list of `[key, value]` pairs.
// The opposite of `_.object` with one argument.
function pairs(obj) {
var _keys = keys(obj);
var length = _keys.length;
var pairs = Array(length);
for (var i = 0; i < length; i++) {
pairs[i] = [_keys[i], obj[_keys[i]]];
}
return pairs;
}
_.extend & _.extendOwn & _.defaults
和keys、allkeys类似的是,这三个的实现也都依赖了一个函数,调用函数createAssigner生成上面三个方法,只想说别人写的代码真6,那么这个函数是怎么实现的呢?
/**
* extend:将 source 对象中的所有属性简单地覆盖到 destination 对象上,并且返回 destination 对象. 复制是按顺序的, 所以后面的对象属性会把前面的对象属性覆盖掉(如果有重复)。
* extendOwn: 类似于 extend, 但只复制自己的属性覆盖到目标对象。(注:不包括继承过来的属性)
* defaults: 用 defaults 对象填充 object 中的 undefined 属性。 并且返回这个 object
*/
// An internal function for creating assigner functions.
function createAssigner(keysFunc, defaults) {
// 返回一个函数,这个函数接受 n 个参数,将第二个及以后的对象的属性 给到 第一个对象
// function(target, [source]){} 将 [source] 中的对象挨个遍历,将每个对象的属性都复制到 target
return function (obj) {
var length = arguments.length;
if (defaults) obj = Object(obj); // var defaults = createAssigner(allKeys, true);
if (length < 2 || obj == null) return obj; // 只传了1个或0个,说明没有 被合并的对象,直接返回 目标对象
// 遍历传入的参数(排除第一个,因为他是目标对象)
for (var index = 1; index < length; index++) {
// 被合并对象
var source = arguments[index],
//根据不同情况返回 source 的 keys(_.extendOwn) or allKeys
keys = keysFunc(source),
l = keys.length;
// 遍历该对象的键值对
for (var i = 0; i < l; i++) {
var key = keys[i];
// 不是 defaults(extend、extendOwn) 直接添加,会覆盖目标对象的属性值
// 或者 是 defaults 就将 目标对象的 undefiend 属性进行填充,要是目标对象有值就不做处理(不覆盖目标对象的属性值)
if (!defaults || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
}
实现了上面函数后其他就简单了,至此这三个方法就都实现了。
// Extend a given object with all the properties in passed-in object(s).
var extend = createAssigner(allKeys);
// Assigns a given object with all the own properties in the passed-in
// object(s).
// (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
var extendOwn = createAssigner(keys);
// Fill in a given object with default properties.
var defaults = createAssigner(allKeys, true);
isEqual
接下来干一件大事,就是来看看它是怎么来实现isEqual函数,来判断两个元素是否相等的,这可以说是underscore中最复杂的一个函数了
先分析一下,要是我们来实现这么一个函数,该怎么做呢,要分为哪几步呢?
在js中分为基本数据类型和引用类型,基本数据类型值相等就是相同的,但是在基本数据类型中有这么几种特殊情况,0和-0是相等的,但是他们是不同的;NaN 不等于自己;null undefined只等于自己;然后判断两个参数类型,类型不同肯定不等,要是引用类型就继续深度比较。具体就看代码注释吧:
// Perform a deep comparison to check if two objects are equal.
// 判断两者是否相同
function isEqual(a, b) {
return eq(a, b);
}
// Internal recursive comparison function for `_.isEqual`.
function eq(a, b, aStack, bStack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal).
/**
* 直接判断是否相等,
* 1. 两者相等,一个不是0,说明两者就是就是相同的 ==> 基本类型值相等(1===1)、引用类型引用地址相同(a=b={} a===b)
* 2. 如果a === 0,那么两者相等,说明 b === 0 or b === -0, 判断 1 / a === 1 / b是否相等(Infinity、-Infinity) 就知道 b 的值了
*/
if (a === b) return a !== 0 || 1 / a === 1 / b;
/** 以下就是 a !== b 的逻辑了,引用类型可能不相等,但是可以相同(属性和属性值都相同) */
// `null` or `undefined` only equal to itself (strict comparison).
// 如果两个中有一个是 `null` or `undefined`时,就说明两个不相同,因为 `null` or `undefined`只等于自己
if (a == null || b == null) return false;
// `NaN`s are equivalent, but non-reflexive.
// NaN 不等于自己,这种情况说明是 NaN,如果两个都是 NaN,咋也是相同的
if (a !== a) return b !== b;
// Exhaust primitive checks
var type = typeof a;
// 说明 a 是基本类型,b 不是对象类型(就是基础类型),两个基础类型,还不相等也不是特殊情况,那么两者不同
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
// 引用类型或基本类型的包装类型判断
return deepEq(a, b, aStack, bStack);
}
那么如何判断两个引用类型是否相同呢?
- 传入的两个参数是不是
_函数的实例,是,重新赋值,比较两者的值
// Internal recursive comparison function for `_.isEqual`.
function deepEq(a, b, aStack, bStack) {
// Unwrap any wrapped objects.
// 是否是 _ 函数的实例,是,比较两者的值
if (a instanceof _$1) a = a._wrapped;
if (b instanceof _$1) b = b._wrapped;
}
- 通过
Object.prototype.toString.call判断两者类型是否相同,不相同,则返回false(类型不同,两者肯定不同)
// Compare `[[Class]]` names.
// 通过原型链判断,两者是否是同一类型,如果不是同一类型,直接false
var className = toString.call(a);
if (className !== toString.call(b)) return false;
- 两者类型相同
- 他们是
RegExporString的时候,比较两者字符串是否相等就行了
`return '' + a === '' + b;`
- 是Number时,就将Number的包装类型转为基本类型
+a,先判断NaN,然后判断0和-0,最后就是正常情况了 - 是
DateorBoolean的时候,将他们转为number类型进行判断return +a === +b;Date类型变为时间戳,比较时间戳;Boolean类型变为0、1进行比较
// 同一类型,进行具体区分
switch (className) {
// These types are compared by value.
case '[object RegExp]':
// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return '' + a === '' + b;
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive.
// Object(NaN) is equivalent to NaN.
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a === +b;
case '[object Symbol]':
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
case '[object ArrayBuffer]':
case tagDataView:
// Coerce to typed array so we can fall through.
return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
}
到这就还剩Array 和 Object了,这里就要采取递归判断了,咱们一个一个来看。
// Internal recursive comparison function for `_.isEqual`.
function deepEq(a, b, aStack, bStack) {
var areArrays = className === '[object Array]';
//
if (!areArrays && isTypedArray$1(a)) {
var byteLength = getByteLength(a);
if (byteLength !== getByteLength(b)) return false;
if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
areArrays = true;
}
// 不是数组
if (!areArrays) {
// 如果 a 不是 object 或者 b 不是 object 则返回 false
if (typeof a != 'object' || typeof b != 'object') return false;
// Objects with different constructors are not equivalent, but `Object`s or `Array`s
// from different frames are.
// 具有不同构造函数的对象是不等价的
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor &&
isFunction$1(bCtor) && bCtor instanceof bCtor)
&& ('constructor' in a && 'constructor' in b)) {
return false;
}
}
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// 假设循环结构相等。检测循环结构的算法改编自ES 5.1第15.12.3节,抽象操作“JO”
// Initializing stack of traversed objects.
// It's done here since we only need them for objects and arrays comparison.
// 初始化遍历对象的堆栈,这是在这里完成的,因为我们只需要它们来比较对象和数组。
// 第一次调用 eq() 函数,没有传入 aStack 和 bStack 参数,之后递归调用都会传入这两个参数
aStack = aStack || [];
bStack = bStack || [];
var length = aStack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (aStack[length] === a) return bStack[length] === b;
}
// Add the first object to the stack of traversed objects.
aStack.push(a);
bStack.push(b);
// Recursively compare objects and arrays.
if (areArrays) {
// Compare array lengths to determine if a deep comparison is necessary.
length = a.length;
// 两者都是数组,数组长度都不一样,说明肯定不是相同的
if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties.
// 递归遍历每一个子项,只要有一个不一样,就是不同的
while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false;
}
} else {
// Deep compare objects. 深入对比两个对象
var _keys = keys(a), key;
length = _keys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality.
// 同理,两者都是对象,对象的属性个数不一样,说明肯定不是相同的
if (keys(b).length !== length) return false;
while (length--) {
// Deep compare each member
key = _keys[length];
if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
}
// Remove the first object from the stack of traversed objects.
// 与 aStack.push(a) 对应 此时 aStack 栈顶元素正是 a 而代码走到此步 a 和 b isEqual 确认 所以 a,b 两个元素可以出栈
aStack.pop();
bStack.pop();
return true;
}
至此,isEqual函数到此结束,实现思路还是很清楚的,也比较容易理解,大家可以仔细看看。
create
_.create(prototype, props)创建具有给定原型的新对象,可选附加 props 作为 own 的属性。基本上,和 Object.create 一样,但是没有所有的属性描述符。
// An internal function for creating a new object that inherits from another.
function baseCreate(prototype) {
if (!isObject(prototype)) return {};
if (nativeCreate) return nativeCreate(prototype);
var Ctor = ctor();
Ctor.prototype = prototype;
var result = new Ctor;
Ctor.prototype = null;
return result;
}
// Creates an object that inherits from the given prototype object.
// If additional properties are provided then they will be added to the
// created object.
function create(prototype, props) {
var result = baseCreate(prototype);
if (props) extendOwn(result, props);
return result;
}
Objects相关的方法基本都介绍完了,其他的多比较简单直接,大家哟兴趣可以直接去看,在这里就不多说了,接下来就准备去看 Array 扩展方法相关代码