0%

lodash源码解析:find家族

本篇分析下find家族的方法,包括findLastIndexfindLastfindKeyfindLastKey,以及这些方法引用到的一些 lodash 方法,包括keysisTypedArrayisBufferkeyskeys

其他的 find 方法

正常按官网的文档,lodashfind家族应该包括数组arrayfindIndexfindLastIndex,集合collectionfindfindLast和对象objectfindKeyfindLastKey

对比现在 fork 的4.17.19 版本的代码,固定的打包出来的版本分支中可以看到完整的方法,但是master分支中却不存在findIndex.jsfind.js两个文件以及相关的任何代码,这就很令人费解。我专门在 lodash github 的issue中提问了此问题。开发组给出的解释是,master 分支存放的是 V5 版本的 wip 代码。在打包时使用的是 lodash-cli 库,用来打包成完全体的代码。

前置知识

collection、array 和 object 的区别

ECMA262 规范

可以发现,find分别为了三类,数组、集合和对象各有自己的正向和反向两个方法。那么在lodash中,collectionarrayobject有什么区别?专门翻了 ECMA262 规范,翻译下与collection相关的定义部分。

ECMAScript 是基于对象的:基本语言和工具由对象提供,ECMAScript 程序是一个通信对象簇。在 ECMAScript 中,对象(object)是由零个或多个属性(property)组成的集合(collection),每个属性都具有决定如何使用自身的属性(attribute)ーー例如,当 property 的 Writable attribute 设置为 false 时,任何已执行 ECMAScript 代码为 property 分配其他值的尝试都将失败。属性(property)是容纳其他对象(object)、原始值(primitive values)或函数(function)的容器。原始值是下列内置类型之一的成员:Undefined, Null, Boolean, Number, BigInt, StringSymbol; 对象是内置类型 Object 的成员; 函数是可调用对象。通过属性(property)与对象(object)关联的函数称为方法(method)。

ECMAScript 定义了一组内置对象(_built-in objects_),这些对象完善了 ECMAScript 实体的定义。这些内置对象包括global对象; 还包括对运行时语义有重要意义的对象,包括 Object, Function, Boolean, Symbol和各种 Error 对象; 表示和操作数值(包括 Math, NumberDate)的对象; 文本处理对象 StringRegExp; 包括 Array 和 9 种不同种类的类型化数组(Typed Array)(其元素都由具有特定的数值数据表示)的索引集合对象; 包括 Mapset 对象的键集合;支持结构化数据(包括 JSON 对象、 ArrayBuffersharedarbufferDataView)的对象;支持控制抽象——包括生成器函数(generator)和期约(promise) 对象——的对象; 以及包括 ProxyReflect 对象在内的反射(reflection)对象。

根据 ECMA262 的解释,简单来说所有的非原始类型的值都是 collection,同时指出了对象是符合任意多个属性组成的集合,数组是是由连续数字索引做键的对象,也就是collection ⊇ object ⊇ array

underscore

接下来再看看,lodash 的爸爸underscore是怎么区分collection的:

Note: Collection functions work on arrays, objects, and array-like objects such as arguments, NodeList and similar. But it works by duck-typing, so avoid passing objects with a numeric length property. It’s also good to note that an each loop cannot be broken out of — to break, use **\.find** instead._

注意: 集合(collection)的函数可以应用到数组、对象、类数组对象(比如arguments, NodeList *和其他类似)。但是函数执行的原理是 duck-typing 的,所以应该避免传递一个具有数字格式 length 属性的对象。值得注意的是,each循环不能被打断——想要打断,应使用_.find方法代替。

总体来说还是符合规范的。

Object()方法

在本篇的代码中,使用了很多次Object(collection)方法强制转换为对象,不加new而单独使用Object()方法的原理如下:

Object 构造函数将给定的值包装为一个新对象。

  • 如果给定的值是 nullundefined, 它会创建并返回一个空对象。
  • 否则,它将返回一个和给定的值相对应的类型的对象。
  • 如果给定值是一个已经存在的对象,则会返回这个已经存在的值(相同地址)。

在非构造函数上下文中调用时, Objectnew Object()表现一致。

所以在非构造函数的上下文中执行Object(collection)时,其实是返回collection强制转化后对应类型的对象。

global

在之前的浏览器和 node.js 环境中,对于全局global的定义是不同的。最近的版本中,在 web 端全局环境下globalThisselfthis是相同的,都指向window;在node全局环境下,globalThisthis是相同的,都指向global

所以说在之后的规范中,是可以用globalThis在两个环境中通用的指向全局。

lodash 中类对象、类数组、类数组对象的定义

类对象

类对象(objectLike)的判断:

  1. value不为nullundefined
  2. typeof value 返回值为 object

也就是说functionarrayobject都算类对象

类数组

类数组(arrayLike)主要是满足两个条件:

  1. value不为nullundefined
  2. typeOf value !== 'function',也就是说可以为objectbooleannumberbigintstringsymbol
  3. 带有一个正确的数字类型的length属性。

类数组对象

类数组对象(arrayLikeObject)相当于在类数组的基础上添加一个条件:value必须为类对象。

换句话说,类数组对象就是typeof value 返回值必须为 object且带有一个正确的数字类型的length属性。

引用的内置方法

baseFindKey

eachFunc方法其实是自己选的迭代方法,用来决定使用什么样的方式来迭代collection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* `findKey`和`findLastKey`方法的基础实现,使用`eachFunc`来迭代`collection`
*
* @private
* @param {Array|Object} collection 要检查的的collection
* @param {Function} predicate 每次迭代时调用的函数
* @param {Function} eachFunc 遍历`collection`的函数
* @returns {*} 返回查到的元素或它的key,查不到则返回`undefined` ?
*/
function baseFindKey(collection, predicate, eachFunc) {
// 初始化result
let result;
// eachFunc是用于迭代的方法
eachFunc(collection, (value, key, collection) => {
// predicate是每次迭代时的断言,判断真假
if (predicate(value, key, collection)) {
result = key;
return false;
}
});
// 最后返回查到的key
return result;
}

export default baseFindKey;

root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* global globalThis, self */
import freeGlobal from './freeGlobal.js';

/** 检测自由变量 `globalThis` */
const freeGlobalThis =
typeof globalThis === 'object' &&
globalThis !== null &&
globalThis.Object == Object &&
globalThis;

/** 检测自由变量 `self`. */
const freeSelf =
typeof self === 'object' && self !== null && self.Object === Object && self;

/** 用于global对象的引用 */
// 经过测试,在Chrome84环境下 globalThis、self和this是生效的,都指向window
// 在node12环境下,globalThis、global和this是生效的,都指向global
const root =
freeGlobalThis || freeGlobal || freeSelf || Function('return this')();

export default root;

isIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/** 用作各种`Number`常量的引用 */
const MAX_SAFE_INTEGER = 9007199254740991;

/** 用于检测无符号整数值 */
const reIsUint = /^(?:0|[1-9]\d*)$/;

/**
* 检查`value`是否是一个有效的类数组对象的索引index
*
* @private
* @param {*} value 要检查的值
* @param {number} [length=MAX_SAFE_INTEGER] 有效index的上界
* @returns {boolean} 如果是一个有效的index,返回`true`;否则返回`false`
*/
function isIndex(value, length) {
// 拿到value的typeof
const type = typeof value;
// length限制如果没给,就默认MAX_SAFE_INTEGER
length = length == null ? MAX_SAFE_INTEGER : length;
// length必须为真
return (
!!length &&
// type为数字或(type为symbol且为无符号整数)
(type === 'number' || (type !== 'symbol' && reIsUint.test(value))) &&
// 并且value是0到length之间的整数
value > -1 &&
value % 1 == 0 &&
value < length
);
}

export default isIndex;

freeGlobal

1
2
3
4
5
6
7
8
9
/** 检测Node.js中的自由变量`global`*/
// global必须是个对象,然后global不能为null,然后global.Object为Object,然后才把global返回
const freeGlobal =
typeof global === 'object' &&
global !== null &&
global.Object === Object &&
global;

export default freeGlobal;

nodeTypes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import freeGlobal from './freeGlobal.js';
// 什么是free variable(自由变量)
// https://blog.csdn.net/weixin_39408343/article/details/101532803

/** 检测自由变量`exports` */
const freeExports =
typeof exports === 'object' &&
exports !== null &&
!exports.nodeType &&
exports;

/** 检测自由变量`module`. */
const freeModule =
freeExports &&
typeof module === 'object' &&
module !== null &&
!module.nodeType &&
module;

/** 检测流行的CommonJS扩展`module.exports`. */
const moduleExports = freeModule && freeModule.exports === freeExports;

/** 检测来Node.js的自由变量`process` */
const freeProcess = moduleExports && freeGlobal.process;

/** 被用来使用更快的Node.js帮助方法 */
const nodeTypes = (() => {
try {
/* 检测Node.js v10+版本以上的公开`util.types`方法. */
/* Node.js 启用代码: DEP0103. */
const typesHelper =
freeModule && freeModule.require && freeModule.require('util').types;
// 有util.types方法就用,没有就用freeProcess
return typesHelper
? typesHelper
: /* Legacy process.binding('util') for Node.js earlier than v10. */
freeProcess && freeProcess.binding && freeProcess.binding('util');
} catch (e) {}
})();

export default nodeTypes;

getTag

获取目标的标签,之前分析过

arrayLikeKeys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import isArguments from '../isArguments.js';
import isBuffer from '../isBuffer.js';
import isIndex from './isIndex.js';
import isTypedArray from '../isTypedArray.js';

/** 用于检查是否为对象自身属性 */
const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
* 创建一个array,存放类数组对象的可枚举属性键
*
* @private
* @param {*} value 要查询的类数组对象
* @param {boolean} inherited 指定是否返回原型链上的属性键
* @returns {Array} 返回属性名组成的数组
*/
function arrayLikeKeys(value, inherited) {
// 通过以下的几种判断,可以发现类数组对象包括数组、arguments、buffer、typedArray3
const isArr = Array.isArray(value);
const isArg = !isArr && isArguments(value);
const isBuff = !isArr && !isArg && isBuffer(value);
const isType = !isArr && !isArg && !isBuff && isTypedArray(value);
// 以上四项任一项为真则skipIndexes为真
const skipIndexes = isArr || isArg || isBuff || isType;
// 初始化length和result,如果是上述几种,就会有自身的length值
const length = value.length;
const result = new Array(skipIndexes ? length : 0);
// 如果不是类数组对象就忽略了
let index = skipIndexes ? -1 : length;
while (++index < length) {
// 把indexKey插入到数组中
result[index] = `${index}`;
}
// 下面是处理非index类型的key
for (const key in value) {
// 当需要查询原型属性或者key属于自身属性时
if (
(inherited || hasOwnProperty.call(value, key)) &&
//
!(
skipIndexes &&
// 排除该情况:是类数组对象且key为length的情况
// Safari 9 has enumerable `arguments.length` in strict mode.
(key === 'length' ||
// Skip index properties.
isIndex(key, length))
)
) {
// 总体来说,就是自身的其他属性都可以push,是否添加原型属性看skipIndexes(当然都是再非length情况下)
result.push(key);
}
}
return result;
}

export default arrayLikeKeys;

baseForOwnRight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import baseForRight from './baseForRight.js';
import keys from '../keys.js';

/**
* `forOwnRight`的基础实现
*
* @private
* @param {Object} object 要迭代的`object`
* @param {Function} iteratee 每次迭代时调用的函数
* @returns {Object} 返回`object`
*/
function baseForOwnRight(object, iteratee) {
// 当object为真时返回baseForRight的返回结果
return object && baseForRight(object, iteratee, keys);
}

export default baseForOwnRight;

引用的 lodash 方法

isObject、isObjectLike、isArrayLike

之前文章分析过

isTypedArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import getTag from './.internal/getTag.js';
import nodeTypes from './.internal/nodeTypes.js';
import isObjectLike from './isObjectLike.js';

/** 用于匹配类型化数组的`toStringTag`属性值 */
const reTypedTag = /^\[object (?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)Array\]$/;

/* Node.js帮助方法引用 */
const nodeIsTypedArray = nodeTypes && nodeTypes.isTypedArray;

/**
* 检查`value`是否为类型化数组
*
* @since 3.0.0
* @category Lang
* @param {*} value 要检查的值
* @returns {boolean} 如果`value`是类型化数组则返回true,否则返回`false`
* @example
*
* isTypedArray(new Uint8Array)
* // => true
*
* isTypedArray([])
* // => false
*/
const isTypedArray = nodeIsTypedArray
? // 在node.js环境,直接用nodeTypes.isTypedArray方法
(value) => nodeIsTypedArray(value)
: // web环境中,先判断是不是类对象,是的话然后再用reTypedTag正则判断value的tag
// 符合正则才是类型化数组
(value) => isObjectLike(value) && reTypedTag.test(getTag(value));

export default isTypedArray;

keys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import arrayLikeKeys from './.internal/arrayLikeKeys.js';
import isArrayLike from './isArrayLike.js';

/**
* 创建一个数组,存放`object`的自身可枚举属性名
*
* **注意:** 非object值将被强制转化为object,详见
* [ES 规范](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
* 查看更多细节
*
* @since 0.1.0
* @category Object
* @param {Object} object 要查询的对象
* @returns {Array} 返回存放属性名的数组
* @see values, valuesIn
* @example
*
* function Foo() {
* this.a = 1
* this.b = 2
* }
*
* Foo.prototype.c = 3
*
* keys(new Foo)
* // => ['a', 'b'] (iteration order is not guaranteed)
*
* keys('hi')
* // => ['0', '1']
*/
function keys(object) {
// 如果是类数组对象,就返回arrayLikeKeys(object)
return isArrayLike(object)
? arrayLikeKeys(object)
: // 如果是不是类数组对象,就直接用Object.keys
// (其实先用Object(object)进行了强制类型转换)
Object.keys(Object(object));
}

export default keys;

find 家族

findLastIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import baseFindIndex from './.internal/baseFindIndex.js';
import toInteger from './toInteger.js';

/**
* 此方法类似`findIndex`, 区别是它是从右到左的迭代集合array中的元素。
*
* @since 2.0.0
* @category Array
* @param {Array} array 要搜索的数组
* @param {Function} predicate 每次迭代时调用的函数
* @param {number} [fromIndex=array.length-1] 开始搜索位置的索引值
* @returns {number} 返回找到元素的 索引值(index),否则返回 -1。
* @see find, findIndex, findKey, findLast, findLastKey
* @example
*
* const users = [
* { 'user': 'barney', 'active': true },
* { 'user': 'fred', 'active': false },
* { 'user': 'pebbles', 'active': false }
* ]
*
* findLastIndex(users, ({ user }) => user == 'pebbles')
* // => 2
*/
function findLastIndex(array, predicate, fromIndex) {
// 初始化length
const length = array == null ? 0 : array.length;
// 空数组返回-1
if (!length) {
return -1;
}
// 默认从最右侧开始
let index = length - 1;

// 如果设置了起始搜索位置
if (fromIndex !== undefined) {
// 先处理index为整数
index = toInteger(fromIndex);
// 把index处理为合适的值
index =
fromIndex < 0
? // 当起始搜索位置为负数时,返回`length + index`和`0`的最大值
Math.max(length + index, 0)
: // 当起始搜索位置为正数时,返回`index`和`length - 1`的最小值
Math.min(index, length - 1);
}
// 调用内置核心baseFindIndex方法
return baseFindIndex(array, predicate, index, true);
}

export default findLastIndex;

findLast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import findLastIndex from './findLastIndex.js';
import isArrayLike from './isArrayLike.js';

/**
* 此方法类似`find`,但是在collection中从右向左迭代
* collection、array和object的区别
* https://blog.csdn.net/Soaring_Tiger/article/details/48180511
* http://underscorejs.org/#collections
* https://stackoverflow.com/questions/23921647/lo-dash-difference-between-array-and-collection
* @since 2.0.0
* @category Collection
* @param {Array|Object} collection 要检查的集合
* @param {Function} predicate 每次迭代调用的函数
* @param {number} [fromIndex=collection.length-1] 开始搜索位置处的索引
* @returns {*} 返回匹配的元素,或者`undefined`
* @see find, findIndex, findKey, findLastIndex, findLastKey
* @example
*
* findLast([1, 2, 3, 4], n => n % 2 == 1)
* // => 3
*/
function findLast(collection, predicate, fromIndex) {
// 初始化一个迭代器和可迭代对象
// Object(collection)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/Object
let iteratee;
const iterable = Object(collection);
// 如果collection是类数组对象
if (!isArrayLike(collection)) {
// collection为key组成的数组
collection = Object.keys(collection);
// 改造predicate来适应类数组
iteratee = predicate;
predicate = (key) => iteratee(iterable[key], key, iterable);
}
// 处理到这一步,就可以按照标准的数组来看待了,先查到需要返回元素的索引值
const index = findLastIndex(collection, predicate, fromIndex);
// 是数组就直接拿index,类数组对象就拿key,然后从可迭代对象iterable中返回结果
return index > -1
? iterable[iteratee ? collection[index] : index]
: undefined;
}

export default findLast;

findKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 该方法类似`find`。但是它返回最先被 predicate(断言函数) 判断为真值元素的 key,而不是元素本身。
*
* @since 1.1.0
* @category Object
* @param {Object} object 要检查的对象
* @param {Function} predicate 每次迭代调用的函数
* @returns {string|undefined} 返回匹配元素的key,或者`undefined`
* @see find, findIndex, findLast, findLastIndex, findLastKey
* @example
*
* const users = {
* 'barney': { 'age': 36, 'active': true },
* 'fred': { 'age': 40, 'active': false },
* 'pebbles': { 'age': 1, 'active': true }
* }
*
* findKey(users, ({ age }) => age < 40)
* // => 'barney' (iteration order is not guaranteed)
*/
function findKey(object, predicate) {
// 初始化result为undefined
let result;
// 当object为null或者undefined时,返回undefined
if (object == null) {
return result;
}
// 用some方法来迭代,其实用find也可以,方便随时停止
Object.keys(object).some((key) => {
// 拿到键对应的值
const value = object[key];
// 如果断言函数返回真
if (predicate(value, key, object)) {
// 则把key赋值给result
result = key;
// 返回true,停止迭代
return true;
}
});
// 最后返回key
return result;
}

export default findKey;

findLastKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import baseFindKey from './.internal/baseFindKey.js';
import baseForOwnRight from './.internal/baseForOwnRight.js';

/**
* 该方法类似`findKey`,但是按照相反的顺序从collection中迭代元素
*
* @since 2.0.0
* @category Object
* @param {Object} object 要检查的对象
* @param {Function} predicate 每次迭代调用的函数
* @returns {string|undefined} 返回匹配元素的key,或者undefined
* @see find, findIndex, findKey, findLast, findLastIndex
* @example
*
* const users = {
* 'barney': { 'age': 36, 'active': true },
* 'fred': { 'age': 40, 'active': false },
* 'pebbles': { 'age': 1, 'active': true }
* }
*
* findLastKey(users, ({ age }) => age < 40)
* // => returns 'pebbles' assuming `findKey` returns 'barney'
*/
function findLastKey(object, predicate) {
// 直接调用baseFindKey
return baseFindKey(object, predicate, baseForOwnRight);
}

export default findLastKey;
👆 全文结束,棒槌时间到 👇