目的
Underscore 是一个 JavaScript 工具库,它提供了一整套函数式编程的实用功能,但是没有扩展任何 JavaScript 内置对象。
本文主要用于梳理和研究underscore内部是如何组织和处理函数的。
通过这篇文章,我们可以:
了解underscore在函数组织方面的巧妙构思;
为自己书写函数库提供一定思路;
我们开始!
自己写个函数库
前端的小伙伴一定不会对jQuery陌生,经常使用$.xxxx的形式进行进行调用,underscore也使用_.xxxx,如果自己在ES5语法中写过自定义模块的话,就可以撸出下面一段代码:
| 1 | (function(){ | 
在Chrome浏览器中打开之后,打印出如下结果:

我们看到在全局对象下有一个_属性,属性下面挂载了自定义函数,我们不妨使用_.first(xxxx)在全局环境下直接调用。
| 1 | console.log(_.first([1,2,3,4])); | 
输出结果如下:

没问题,我们的函数库制作完成了,我们一般直接这么用,也不会有太大问题。
underscore是怎么做的?
underscore正是基于上述代码的完善,那么underscore是如何接着往下做的呢?容我娓娓道来!
对兼容性的考虑
| 1 | // Establish the root object, `window` (`self`) in the browser, `global` | 
上面是underscore1.9.1IIFE函数中的源码,对应于我们上面自己写的var root = this;。
在源码中作者也解释了:创建root对象,并且给root赋值:
浏览器端:window也可以是window.self或者直接self
服务端(node):global
WebWorker:self
虚拟机:this
underscore充分考虑了兼容性。
支持两种不同风格的函数调用
在underscore中我们可以使用两种方式调用函数:
- 函数式的调用:console.log(_.first([1,2,3,4]));
- 对象式调用:console.log(_([1,2,3,4])).first();
在underscore中,它们返回的结果都是相同的。
第一种方式没有问题,难点就是第二种方式的调用。
对象式调用的实现
解决这个问题要达到两个目的:
- _是一个函数,并且调用返回一个对象;
- 这个对象依然能够调用挂载在_对象上声明的方法。
我们来看看underscore对于_的实现:
| 1 | var _ = function(obj) { | 
不怕,我们不妨调用_([1,2,3,4]))看看他是怎么执行的!
第一步:if (obj instanceof _) return obj;传入的对象及其原型链上有_类型的对象,则返回自身。我们这里的[1,2,3,4]显然不是,跳过。
第二步:if (!(this instanceof _)) return new _(obj);,如果当前的this对象及其原型链上没有_类型的对象,那么执行new操作。调用_([1,2,3,4]))时,this为window,那么(this instanceof _)为false,所以我们执行new _([1,2,3,4])。
第三步:执行new _([1,2,3,4]),继续调用_函数,这时
obj为[1,2,3,4]
this为一个新对象,并且这个对象的__proto__指向_.prototype(对于new对象执行有疑问,请猛戳此处)
此时
(obj instanceof _)为false
(this instanceof _)为true
所以此处会执行this._wrapped = obj;,在新对象中,添加_wrapped属性,将[1,2,3,4]挂载进去。
综合上述函数实现的效果就是:
_([1,2,3,4]))<=====>new _([1,2,3,4])
然后执行如下构造函数:
| 1 | var _ = function(obj){ | 
最后得到的对象为:


我们执行如下代码:
| 1 | console.log(_([1,2,3,4])); | 
看一下打印的信息:

这表明通过_(obj)构建出来的对象确实具有两个特征:
- 下面挂载了我们传入的对象或数组
- 对象的_proto_属性指向_的prototype
到此我们已经完成了第一个问题。

接着解决第二个问题:
这个对象依然能够调用挂载在_对象上声明的方法
我们先来执行如下代码:
| 1 | _([1,2,3,4]).first(); | 
此时JavaScript执行器会先去找_([1,2,3,4])返回的对象上是否有first属性,如果没有就会顺着对象的原型链上去找first属性,直到找到并执行它。
我们发现_([1,2,3,4])返回的对象属性和原型链上都没有first!

那我们自己先在_.prototype上面加一个上去试一下:
| 1 | (function(){ | 
我们在执行打印一下:
| 1 | console.log(_([1,2,3,4])); | 
效果如下:

原型链上找到了first函数,我们可以调用first函数了。如下:
| 1 | console.log(_([1,2,3,4]).first()); | 
可惜报错了:

于是调试一下:
我们发现arr是undefined,但是我们希望arr是[1,2,3,4]。

我们马上改一下_.prototype.first的实现
| 1 | (function(){ | 
我们在执行一下代码:
| 1 | console.log(_([1,2,3,4]).first()); | 
效果如下:

我们的效果似乎已经达到了!

现在我们执行下面的代码:
| 1 | console.log(_([1,2,3,4]).first(2)); | 
调试一下:

凉凉了。

其实我们希望的是:
将
[1,2,3,4]和2以arguments的形式传入first函数
我们再来改一下:
| 1 | //_.prototype.first = function(arr,n=0){ | 
我们再执行下面代码:
| 1 | _([1,2,3,4]).first(2); | 
看一下打印的效果:

参数都已经拿到了。
我们调用函数一下first函数,我们继续改代码:
| 1 | _.prototype.first=function(){ | 
这样一来_.prototype上面的函数的实际实现都省掉了,相当于做一层代理,调用一下。
一举两得!
执行一下最初我们的代码:
| 1 | console.log(_.first([1,2,3,4])); | 

现在好像我们所有的问题都解决了。

但是似乎每声明一个函数都得在原型链上也声明一个相同名字的函数。形如下面:
| 1 | _.a = function(args){ | 
会不会觉得太恐怖了!

我们能不能改成如下这样呢?
| 1 | _.a = function(args){ | 
上面这么做好处大大的:
我们可以专注于函数库的实现,不用机械式的复写prototype上的函数。
underscore也正是这么做的!
我们看看mixin函数在underscore中的源码实现:
| 1 | // Add your own custom functions to the Underscore object. | 
有了上面的铺垫,这个代码一点都不难看懂,首先调用_.each函数,形式如下:
| 1 | _.each(arrs, function(item) { | 
我们一想就明白,我们在_对象属性上实现了自己定义的函数,那么现在要把它们挂载到_的prototype属性上面,当然先要遍历它们了。
所以我们可以猜到_.functions(obj)肯定返回的是一个数组,而且这个数组肯定是存储_对象属性上面关于我们实现的各个函数的信息。
我们看一下_.function(obj)的实现:
| 1 | _.functions = _.methods = function(obj) { | 
确实是这样的!

我们把上述实现的代码整合起来:
| 1 | (function(){ | 
我们看一下_.functions(obj)返回的打印信息:

确实是_中自定义函数的属性值。
我们再来分析一下each中callback遍历各个属性的实现逻辑。
| 1 | var func = _[name] = obj[name]; | 
第一句:func变量存储每个自定义函数
第二句: _.prototype[name]=function();在_.prototype上面也声明相同属性的函数
第三句:args变量存储_wrapped下面挂载的值
第四句:跟var args = [that].concat(Array.from(arguments));作用相似,将两边的参数结合起来
第五句:执行func变量指向的函数,执行apply函数,将上下文对象_和待传入的参数`args``传入即可。
我们再执行以下代码:
| 1 | console.log(_.first([1,2,3,4])); | 
结果如下:

Perfect!
这个函数在我们的浏览器中使用已经没有问题。
但是在Node中呢?所以下面引出新的问题。
再回归兼容性问题
我们知道在Node中,我们是这样的:
| 1 | //a.js | 
那么:
| 1 | let _ = require('./underscore.js') | 
我们也希望上述的代码能够在Node中执行。
所以root._ = _是不够的。
underscore是怎么做的呢?
如下:
| 1 | if (typeof exports != 'undefined' && !exports.nodeType) { | 
我们看到当全局属性exports不存在或者不是DOM节点时,说明它在浏览器中,所以:
root._ = _;
如果exports存在,那么就是在Node环境下,我们再来进行判断:
如果module存在,并且不是DOM节点,并且module.exports也存在的话,那么执行:
exports = module.exports = _;
在统一执行:
exports._ = _;
附录
下面是最后整合的阉割版underscore代码:
| 1 | (function(){ | 
欢迎各位大佬拍砖!同时您的点赞是我写作的动力~谢谢。