underscore.js源码分析
一、介绍
提到underscore.js大家可能不太熟悉,但是大家肯定对于lodash很熟悉。在我们日常工作中主要用lodash操作一些js操作起来比较麻烦的方法。underscore和lodash类似,他们都是对于js表现力的增强,就是说原生js在处理数据方面是比较薄弱的,像lodash和underscore对于他们有一些增强,比如数组方法的增强,函数方法的增强,对象方法的增强以及字符串方法的增强。所以像underscore,像lodash看源码无非就是一堆工具函数,会有上百个工具函数。那我们有学习的必要吗,或者说如果我们学习他的源码是不是把工具函数的源码扒下来阅读,如果我们学习源码只是学习这些东西的话,咱们大家把代码考下来,自己摸索下,问题都不大。我们今天要说的是underscore里面最精髓的一部分。我们要讲的underscore和lodash比较像,为什么要讲underscore呢,因为underscore里面有一个比较好的架构的设计,值得我们学习。所以我们的主要目标不会放在工具函数一个一个分析,更多的是关注整体架构的设计,这是我们讲underscore的原因。lodash虽然和underscore雷同,但是lodash不具备这样的架构设计。
二、underscore的编程理念
首先我们讲下从零开发一个像underscore这样的工具库要怎么做
我们知道像underscore和lodash是针对原生js的一些能力增强。原生js在处理数据的过程中提供了很薄的一层方法,处理功能是有限的。如果我想要怎么增强办呢。举个例子,我们要做一个数组去重的功能,我们翻遍脑海,发现没有找到一个专门来处理的api。最常规的方式,我们需要通过一个function去定义一个像unique这样的函数
function unique(arr) { // 逻辑 } unique([1,2,3,4,5,6,5,6,7])
我们把数组作为参数,传给一个方法,方法里面处理业务逻辑,最后返回去重的结果。
但是在underscore里面,他帮我们提供了另外一种思路。他帮我们提供了上百个类似于像unique这样的工具函数,他真正的好处是借鉴了像java里面的流式编程
流式编程是什么概念呢,举个例子,假设有这样一个数组,先去重,再过滤,再取最大值
unique([1,2,3,4,5,6,5,6,7]).filter().max()
把这个函数当作一个管道,先去重,再过滤,再取最大值,数据在管道里流动。先把数据给到unique去重,去重后在把数据流通到filter,过滤后在给他max
这个东西有点像链式调用。接触过jQuery的话肯定知道这样的写法
$('.div').css().show()
jq里面把这种方式叫做链式调用,在underscore里面把这种方式叫做流式编程。在链式调用中更多的关注的是对象,就是说当我们调用了css方法返回的是一个实例,调用其他方法返回的又是一个实例,都是通过实例去调用方法;流式编程更加关注的是数据的流通,我们的数据在上一道工序加工后怎么流通到下一道工序,然后在流通到下一道工序,更多关注的是数据。
三、undersocre的写法
在underscore里面提供了两种写法
_([1,2,3,4,5,6,5,6,7]).unique().filter().max()
_.unique([1,2,3,4,5,6,5,6,7]).filter().max()
我们先来看看这两种方式是怎么实现的,步骤模型是怎么搭建的,然后讲讲管道函数,这些数据是怎么流通的,最终在串起来,告诉大家什么是流式编程
我们简单分析下,要支持这两种方式的调用,同时支持静态方法和实例方法
_([1,2,3,4,5,6,5,6,7]) // 当作是一个函数,把数据传递给他,来开启流式编程 _.unique([1,2,3,4,5,6,5,6,7]) // 当作一个对象,通过underscore对象上的unique方法,把数据传递给他来开启流式编程
四、无new化构建
(function(root) { var _ = function() { } // 函数也是对象 root._ = _; })(this)
这个方法写在一个闭包函数里,为了避免变量全局污染。我们定义一个变量并挂载到全局。这个变量如果定义成一个对象,那么 _() 的调用就不能生效。因此需要是一个函数,在js里面,函数也是对象
我们在调用 _() 的时候,需要去创建实例。那我们扩展一个像数组去重这样一个方法的话,要找到 _ 的原型对象,在原型对象上扩展一个unique方法,给他的实例来使用
(function(root) { var _ = function() { } _.prototype.unique = { } // 函数也是对象 root._ = _; })(this)
需要做到的是当我们调用 _() 的时候,也就要创建一个实例 new _()
当调用 _() 函数的时候,是在全局环境下调用,this指向window,window不是 _ 实例,所以代码如下
(function() { var _ = function() { if (!(this instanceof _)) { return new _(); } }
_.prototype.unique = function() {}
// 函数也是对象
root._ = _;
})()
在全局引入并调用下 _()
// underscore.html console.log(_().unique)
// 可以发现在控制台有实例对象,原型上有unique方法
我们在通过对象的方式调用,所以就要在 _ 对象本身扩展unique方法
(function() { var _ = function() { if (!(this instanceof _)) { return new _(); } } _.unique = function() {
console.log(2)
} _.prototype.unique = function() {
console.log(1)
} // 函数也是对象 root._ = _; })()
五、mixin
同时执行下这两种方法
_([1,2,3,4,5,6,5,6,7]).unique()
_.unique([1,2,3,4,5,6,5,6,7])
做数组去重,如果要通过上面两种方式调用,unique方法写两遍,这样冗余程度太高了,况且underscore有上百个类似于这样的工具函数。所以要有个办法来解决这个问题
underscore中有个方法mixin,通过他来解决这个事情
mixin主要做了这么几件事
1:可枚举的属性 2:存储在数组中 3:遍历数组,给原型对象扩展
(function(root) { // ... _.functions = function(target) { var ref = []; for (var name in target) { ref.push(name); } return ref; } _.each = function(arr, callback) { for (var i=0; i<arr.length; i++) { callback(arr[i]) } } _.mixin = function(target) { _.each(_.functions(target), function(key) { target.prototype[key] = function() { console.log(1) } }) } _.mixin(_); // 函数也是对象 root._ = _; })(this)
此时通过函数和对象两种方式调用,会发现执行结果返回1 和 2。实际上应该不管我们通过函数还是对象,都应该执行对象属性上的方法,所以mixin 修改如下
_.mixin = function(target) { _.each(_.functions(target), function(key) { var func = target[key]; target.prototype[key] = function() { func(); } }) }
目前没有真实接口进行测试,修改上面的unique方法
_.unique = function(source) { var ref = []; for (var i=0; i<source.length; i++) { var target = source[i]; if (ref.indexOf(target) === -1) { ref.push(target); } } return ref; }
执行 _.unique([1,2,3,4,5,6,5,6,7]),输出结果 [1,2,3,4,5,6,7]
此时参数修改下,并不区分大小写 _.unique([1,2,3,4,5,6,5,6,7,'a','A'])
增加回调函数,来处理
_.unique([1,2,3,4,5,6,5,6,7,'a','A'], function(item) { return typeof item === 'string' ? item.toLowerCase() : item; })
同时修改下unique方法
_.unique = function(source, callback) { var ref = []; for (var i=0; i<source.length; i++) { var target = callback ? callback(source[i]) : source[i]; if (ref.indexOf(target) === -1) { ref.push(target); } } return ref; }
去重最终结果是[1,2,3,4,5,6,7,'a']
下面通过 另外一种方式调用
_.unique([1,2,3,4,5,6,5,6,7,'a','A'], function(item) { return typeof item === 'string' ? item.toLowerCase() : item; })
控制台会报错,主要问题是通过实例调用方法的时候,没有传参。下面要把数据推送给实例调用的方法,修改 _ 方法
var _ = function(data) { if (!(this instanceof _)) { return new _(data); } this._wrapper = data; }
报错没有了,但是数组没有把大小写 a 去重,所以mixin 方法中的参数加上arguments
_.mixin = function(target) { _.each(_.functions(target), function(key) { var func = target[key]; target.prototype[key] = function() { return func(this._wrapper, arguments[0]); } }) }
可以实现大小写去重,但是明显这么写代码不够健壮,我们可以通过数组合并来解决
_.mixin = function(target) { _.each(_.functions(target), function(key) { var func = target[key]; target.prototype[key] = function() { var decon = [this._wrapper]; Array.prototype.push.apply(decon, arguments); return func.apply(_, decon); } }) }