Lodash 源码中的那些迷人的细节

Lodash 源码中的那些迷人的细节

巧妙的函数实现吸引着你想去看看他的实现方法,里面会有更多奇思妙想让你欣喜若狂...

去年的时候,决心去研究 Lodash 中方法实现。执行的路数就是对照 API 文档,先用自己的想法去实现,然后再对照源码找差别 ... 当看到与作者一致的实现的时候,我会不要脸的跟自己说一句:你好聪明,哈哈 ... 如此这般,花了几个月零散的时间覆盖了部分 API, 沉淀了一些实现过程和思路,感兴趣的走传送门>>> ... 今天又去翻看了一下,看到里面有一些精彩的地方,遂挑出来与君分享。


1. length = start > end ? 0 : ((end - start) >>> 0)

在 Lodash 的代码中,会经常看到这样的代码片段,需要根据传入的 Array 的初始和结束索引求得要操作的数据元素的长度。

>>> 0 有什么用处?

在 JS 中,Array.length 是一个 32 位无符号整型数字,而通过无符号位移运算 >>> 0,就是为了确保我们得到的正确的 length 值,它总是能得到一个 32-bit unsigned ints ... 比如:

-1 >>> 0    // 4294967295

0 >>> 0     // 0

'1' >>> 0   // 1

'1x' >>> 0  // 0

null >>> 0  // 0

而且,对于异常情况的包容也可以让我们减少一些类型判断和显示的强制类型转化的操作。


因此,在有用到 Array.length 的场景,可用 >>> 0 做参数防护。


2. arr instanceof Array

我们会用 instanceof 去判断实例与类之间的关系,但是这种关系可靠吗?


arr instanceof Array 为 true 肯定 arr 就是一个数组,但是为 false 是不是就表示 arr 肯定不是数组呢?


不能!在有多个 frame 的时候就不能...

let xArray = window.frames[0].Array; // origin from iframe's window
let arr = new xArray(1, 2, 3);

arr instanceof Array; // false

Array.isArray(arr); // true

也就是,你以为的并不都是你以为的 ... 虽然我们平时不常会有多个 frame 间这种级别的类的混用,但是以防万一...

通常,在需要判断是否是数组时,可以使用 Array.isArray 或者它的 Polyfill:

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
} 


3. _.eq(value, other)

判断 value 和 other 是否(强)相等,在 ECMA 规范中对于相等其实是有明确的定义的,其中 NaN === NaN,但是在 JS 中,NaN 是不与任何值相等的,包括自身,而 Lodash 弥补了这部分的不足,那怎么判断 NaN === NaN 呢?

function eq(value, other) {
  return value === other || (value !== value && other !== other)
}

我们知道 NaN 是唯一个不与自身相等的值,因此可以通过这个特性分别得到 value 和 other 是否是 NaN,如果是,则两者相等,返回 true.


4. _.isNaN

如何判断是否是 NaN ?

通过 isNaN 全局方法?

isNaN('x')       // true

isNaN(NaN)       // true

isNaN(0)         // false

全局方法 isNaN 是有坑的,它的 NaN 定义估计真的是 not a number, 而不是值得 NaN 这个s数值,也就是除了 NaN, 对于 不能转化为数字 的情况都会返回 true ... 糟心!其实这个问题已经在 ES6 中被改善啦,Number.isNaN分分钟教isNaN 做人,Number.isNaN 只会对 NaN 返回 true, 其他情况都是 false...


5. (-0).toString() === '0'?苦恼!

-0 的 toString 竟然没有保留 - ,坑爹嘛不是...

但是,_.toString(-0) === '-0' ,如何做的?

// 一系列的前置检查和转化后...
const INFINITY = 1 / 0; // 手动 INFINITY
const res = `${value}`;

if (res === '0' && 1 / value === -INFINITY) {
  return '-0';
} else {
  return res;
}


6. JS 的世界里,谁不能转化为数字?

数字、数字字符串肯定是可以的...

对象?或者非数字的字符串?它们也能得到 NaN

Symbol !

Number('xx')          // NaN

Number(new Object())  // NaN

Number(Symbol())
// --> Uncaught TypeError: Cannot convert a Symbol value to a number

另外,Symbol 可以显示的转化为 String 和 Boolean, 但是,不能进行任何隐式转换,也就是不能对 Sybmol 进行运算,比如:

const symbol = Symbol();

1 + symbol       // TypeError

'1' + symbol     // TypeError

// 可显示转化为 布尔 和 字符串
Boolean(symbol)  // true

String(symbol)   // "Symbol()"


7. _.repeat 的优化手段

_.repeat([str = ''], [times = 1])

指定 string 重复 n 次输出,可以很简单的通过循环实现:

const repeat = (str = '', times = 1) => {
  let res = str;

  while (--times) {
    res += str;
  }

  return res;
}

repeat('6', 3); // 666

Easy ? 好像不是特别完美吧,设想 repeat('6', 4) 按照上述实现需要循环 4 次,但是其实是可以通过两次操作就把最终结果拼接出来的,就相当于 repeat(repeat('6', 2), 2) ,所以这块是有优化空间的,来看优化后的算法:

const repeat = (str = '', times = 1) => {
    let result = '';

    if (!str || times < 1) {
        return result;
    }

    do {
        if (times % 2) {
            result += str;
        }

        times = Math.floor(times / 2);

        if (times) {
            str += str;
        }
    } while (times)

    return result;
}

性能对比:repeat('hello world', 100000) 测试的 benchmark:

远远超出 ...


结束语

在 Lodash 中,函数的实现非常严谨、高效、兼容性强,以及具有一定的前瞻性,本文只拎出来一丁点儿细微的点,强力建议去撸源码 ...


顺道

阿里巴巴南京研发中心,迫切期待你的加入,详见:坐标大南京,阿里巴巴Java、前端内推

文章被以下专栏收录

    关注前端前沿技术,探寻业界深邃思想。https://qianduan.group 欢迎微信/微博搜索『前端外刊评论』,关注我们。欢迎给本专栏投稿,原作译作不限,要求:质量高!如果愿意尝试从事前端技术相关的书籍的编写或翻译工作,请私信外刊君。

    阿里巴巴南京研发中心隶属于阿里巴巴集团客户体验事业群,成立一年以来,团队规模迅速扩张,业务涵盖淘宝天猫业务维权咨询、帮助中心、阿里小蜜、人力云众包、大众评审等,岗位已包含后端技术、前端开发、产品经理和 UX 设计师等,想在南京工作或者想回江苏离爸妈近的同学们,还等什么呢,快点递交你们的简历吧!