0%

lodash源码解析:compact

分析lodash——compact,compact 的源码非常简单,并没有引用其他的函数。但是看源码之前,首先想一个问题,lodash 源码里大量出现了 a == null 或 a != null 的判断,所以值 a 什么情况下 满足 a == null 为真?什么情况下 a == undefined 为真?

宽松相等

谁与 null 宽松相等这个问题,我问了公司的资深前端们,给出了解答,答案就在 ECMAScript262 标准里:抽象相等比较算法,如下所示:

The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

  1. If Type(x) is the same as Type(y), then
    1. Return the result of performing Strict Equality Comparison x === y.
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
  6. If Type(x) is BigInt and Type(y) is String, then
    1. Let n be ! StringToBigInt(y).
    2. If n is NaN, return false.
    3. Return the result of the comparison x == n.
  7. If Type(x) is String and Type(y) is BigInt, return the result of the comparison y == x.
  8. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
  9. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
  10. If Type(x) is either String, Number, BigInt, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
  11. If Type(x) is Object and Type(y) is either String, Number, BigInt, or Symbol, return the result of the comparison ToPrimitive(x) == y.
  12. If Type(x) is BigInt and Type(y) is Number, or if Type(x) is Number and Type(y) is BigInt, then
    1. If x or y are any of NaN, +∞, or -∞, return false.
    2. If the mathematical value of x is equal to the mathematical value of y, return true; otherwise return false.
  13. Return false.

翻译一下:

在 x == y 这种比较中,x 和 y 都是值,返回 true 或者 false。这种比较执行的步骤如下所示:

  1. 如果 x 的类型与 y 的类型相同:
    1. 返回严格相等比较 x === y 的结果。
  2. 如果 x 为 null 且 y 为 undefined,返回 true。
  3. 如果 x 为 undefined 且 y 为 null,返回 true。
  4. 如果 x 为 Number 类型且 y 为 String 类型,返回比较 x == ! ToNumber(y)的结果。
  5. 如果 x 为 String 类型且 y 为 Number 类型,返回比较 ! ToNumber(x) == y 的结果。
  6. 如果 x 为 BigInt 类型且 y 为 String 类型:
    1. let n = ! StringToBigInt(y)。
    2. 如果 n 为 NaN,返回 false。
    3. 返回比较 x == n 的结果。
  7. 如果 x 为 String 类型且 y 为 BigInt 类型,返回比较 y == x 的结果。
  8. 如果 x 为 Boolean 类型,返回比较! ToNumber(x) == y 的结果。
  9. 如果 y 为 Boolean 类型,返回比较 x == ! ToNumber(y)的结果。
  10. 如果 x 为 String, Number, BigInt 或 Symbol 类型之一,且 y 为 Object 类型,返回比较 x == ToPrimitive(y)的结果。
  11. 如果 x 为 Object 类型,且 y 为 String, Number, BigInt 或 Symbol 类型之一,返回比较 ToPrimitive(x) == y 的结果。
  12. 如果 x 为 BigInt 类型且 y 为 Number 类型,或 x 为 Number 类型且 y 为 BigInt 类型:
    1. 如果 x 或者 y 为 NaN, +∞, or -∞ 之一,返回 false。
    2. 如果 x 与 y 的数学计算结果相等,返回 true;否则返回 false。
  13. 返回 false。

比较条件特别细致和繁琐,概括下:

  1. 类型相同,返回严格相等比较 x === y 的结果。
  2. null == undefined,返回 true。
  3. 数字(Number 和 BigInt)类型和 String 类型比较,转换为数字再比较。
  4. Boolean 类型转换为 Number 类型再比较
  5. Object 类型与其他原始类型比较前,先转为原始类型再比较。

所以回到最初的问题:

  1. null 为 Null 类型的唯一值,undefined 为 Undefined 类型的唯一值,所以同类型比较时执行严格相等比较,null == null 和 undefined = undefined 为真。
  2. 第二步和第三步说明了 undefined == null 和 null == undefined 为真;
  3. 其余的牵扯到 null 和 undefined 的宽松相等比较都为假。

Truthy(真值)和 Falsy (假值)

在 compact 代码中,还用到了关于真假值(Truthy、Falsy)的判断,compact 会剔除数组中的所有假值,那么什么样的值算假值?换句话说,什么样的值类型转换为 Boolean 后为假值?还是见ECMA262

直接翻译表 10: ToBoolean 转换

参数类型 结果
Undefined 返回 false。
Null 返回 false。
Boolean 返回参数。
Number 如果参数为+0, -0 或 NaN,返回 false;其余情况返回 true。
String 如果参数是空字符串(length 属性为 0),返回 false;其余情况返回 true。
Symbol 返回 true。
BigInt 如果参数为 0n,返回 false;其余情况返回 true。
Object 返回 true。

compact

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
/**
* 创建一个新数组,包含原数组中所有的非假值元素。
* 例如false, null, 0, "", undefined, 和 NaN 都是被认为是“假值”。
*
* @since 0.1.0
* @category Array
* @param {Array} array 待处理的数组
* @returns {Array} 返回过滤掉假值的新数组。
* @example
*
* compact([0, 1, false, 2, '', 3])
* // => [1, 2, 3]
*/
function compact(array) {
let resIndex = 0;
const result = [];
// 当array为undefined和null时,返回[]
if (array == null) {
return result;
}

for (const value of array) {
// false, null, 0, "", undefined, 和 NaN在if判断里都为假
if (value) {
// 新数组递增赋值
result[resIndex++] = value;
}
}
return result;
}

export default compact;

原生实现

1
2
3
4
5
// Lodash
_.compact([0, 1, false, 2, '', 3]);

// 原生实现
[0, 1, false, 2, '', 3].filter(Boolean);

原生实现的代码中直接用了Array.prototype.filter(),在后面却直接把 Boolean 当参数传进去,冷不丁看起来难理解,但只要明白下面两点就很容易想通:

  1. 首先,Boolean本身就是一个函数:

    1
    2
    3
    4
    5
    6
    Boolean(0); // false
    Boolean(1); // true
    Boolean(false); // false
    Boolean(2); // true
    Boolean(''); // false
    Boolean(3); // true
  2. Array.prototype.filter(callback)中,入参 callback 其实一个回调函数,只要函数能满足入参为element[, index[, array]]就可以,所以如下两种写法是等价的:

    1
    2
    3
    4
    5
    6
    7
    8
    // 简写,Boolean本身就是一个函数
    // 所以只要参数能对应上,也就能直接作为回调函数callback用
    b = a.filter(Boolean);

    // 使用function语法构造了一个回调函数,再把item传给Boolean
    b = a.filter(function (item) {
    return Boolean(item);
    });
👆 全文结束,棒槌时间到 👇