官网对Underscore.js的介绍:
Underscore一个JavaScript实用库,提供了一整套函数式编程的实用功能,但是没有扩展任何JavaScript内置对象。它是这个问题的答案:“如果我在一个空白的HTML页面前坐下, 并希望立即开始工作, 我需要什么?”它弥补了部分jQuery没有实现的功能,同时又是Backbone.js必不可少的部分。
Underscore提供了100多个函数,包括常用的: map, filter, invoke...当然还有更多专业的辅助函数,如:函数绑定, JavaScript模板功能,创建快速索引, 强类型相等测试, 等等。
前言:
Underscore封装了我们开发过程中常用的JavaScript对象操作方法,用来提高开发效率。它本身与官网说到的“Backbone.js”没有任何的关系,因此我们可以完全不必理会“Backbone.js”的概念来学习它,或将它单独运用到任何一个页面。
另外,Underscore还可以在Node.js环境上运行,这表示我们在开发项目使用到的vue.js、react.js、angular.js等这些前端框架时会遇到我们如何快速使用JavaScript对集合对象做遍历,做一些特殊处理,如果你的JavaScript基础很扎实的话,你完全可以自己实现这些功能,但对于大部分人来说,这些基础功能应该是由底层API支持的,就像SDK一样,Underscore为我们提供了这样的一个实用工具包。
该文章的目录:
- Underscore安装;
- 从下划线 "_ " 开始解剖;
- 通过解析 _each() 方法深入 Underscore 函数库;
- 研究 Underscore 函数库中的 _mixin() 如何运转的;
- _mixin() 的自定义拓展;
- Underscore 函数库常用实用功能;
1. Underscore安装
Underscore.js 是一个 Javascript 功能类库,不依赖于环境,可以加载到HTML中在浏览器运行,也可以直接在 Node.js 的环境中使用。
GitHub仓库:github.com/jashkenas/u…
有注释的源码:www.html.cn/doc/undersc…
node(vue项目)环境安装:npm install vue-underscore
Underscore 有100多个的函数,下面我们开始通过实用的方法来了解源码。
2.从下划线 "_" 开始解刨
新建一个testUnderscore.js文件,测试 Underscore 对集合的支持
js 代码:
~ vi testUnderscore.js
// 引入并加载 Underscore 库
<script src="./js/underscore.js"></script>
// node环境
var _ = require("underscore")._;
解析一下为什么加载的是 Underscore 中下划线 "_" 呢?看源码。
// 避免全局污染,所以 Underscore 利用了自执行函数原理来执行自己想要的函数
(function(){
// 将 this 赋值给局部变量 root
// root 的值, 客户端为 `window`, 服务端(node) 中为 `exports`
var root = this;
// 声明一个局部变量 '_'
// "_" 其实是一个构造函数
// 支持无 new 调用的构造函数
// 将传入的参数(实际要操作的数据)赋值给 this._wrapped 属性
// OOP 调用时,_ 相当于一个构造函数
// each 等方法都在该构造函数的原型链上
// _([1,2,3]).each(alert)
// _([1,2,3]) 相当于无 new 构造了一个新的对象
// 调用了该对象的 each 方法,该方法在对象构造函数的原型链上
var _ = function(obj){
// 以下均针对 OOP 形式的调用
// 如果非 OOP 的形式的调用,不会进入该函数内部
// 如果 obj 已经是 '_' 函数的实例,则直接返回 obj
if(obj instanceof _)
return obj;
// 如果不是 '_' 函数的实例
// 则调用 new 运算符,返回实例化的对象
if(!(this instanceof _))
return new _(obj);
// 将 obj 赋值给 this._wrapped 属性
this._wrapped = obj;
};
// 将上面定义的 '_' 局部变量赋值给全局对象中的 '_' 属性
// 即客户端中 window._ = _
// 服务端(node)中 exports._ = _
// 同时在服务端向后兼容老的 require() API
// 这样暴露给全局后便可以在全局环境中使用 '_' 变量(方法)
if(typeof exports !== 'undefined'){
if(typeof module !== 'undefined' && module.exports){
exports = module.exports = _;
}
exports._ = _ ;
}else {
root._ = _;
}
})()
由此可以看出我们在引入 Underscore 库的时候,就可以在全局对象中随意调用 _ 该对象中的方法
3. 通过解析 _each() 方法深入 Underscore 函数库
_each()作用:对集合循环操作
注意:接下来我们调用的方法都是 Underscore 的调用形式跟其他方式不要混淆
js 代码:
// 函数式风格
_.each([1,2,3],function(ele,index){
console.log(index + ":" + ele);
})
// 0:1
// 1:2
// 2:3
Underscore 库源码:
// 与 ES5 中 Array.prototype.forEach 使用方法类似
// 遍历数组或者对象的每个元素
// 第一个参数为数组(包括类数组)或者对象
// 第二个参数为迭代方法,对数组或者对象每个元素都执行该方法
// 该方法又能传递三个参数,分别为 (item,index,arry) ((value,key,obj) for objtect)
// 与 ES5 中 Array.prototype.forEach 方法传参格式一致
// 第三个参数(可省略)确定第二个参数 iteratee 函数中的(可能有的)this 指向
// 即 iteratee 中出现的(如果有)所有 this 都指向 context
// notice: 不要传入一个带有 key 类型为 number 的对象
// notice:_.each 方法不能 return 跳出循环 (同样,Array.prototype.forEach 也不行)
_.each = _.forEach = function(obj,iteratee,context){
// 根据 context 确定不同的迭代函数
interatee = optimizeCb(interatee,context);
var i , length ;
// 如果是类数组
// 默认不会传入类似 {length:10} 这样的数据
if(isArrayLike(obj)){
//遍历
for(i = 0 ; length = obj.length ; i < length ; i++){ interatee(obj[i],i,obj);
}
}else {
// 如果 obj 是对象
// 获取对象的所有 key 值
var keys = _.keys(obj);
// 如果是对象,则遍历处理 values 值
for(i = 0 , length = keys.length ; i < length ; i++){ interatee(obj[keys[i]], keys[i], obj);//(value, key, obj)
}
}
// 返回 obj 参数
// 供链式调用(Returns the list for chaining)
// 应该仅 OOP 调用有效
return obj;
}
上面就是 _each() 方法的基础源码,首先我们要调用 optimizeCb() 来确定不同的迭代函数,从而让this的指向发生改变,接下来就是通过 isArrayLike() 方法来校验参数 obj 是否是一个数组,如果不是的话我们就调用 _.keys() 获取key值得形式来遍历参数 obj
解析 Underscore 函数库中的 optimizeCb() 源码
// Underscore 内部方法
// 根据 this 指向(context 参数)
// 以及 argCount 参数
// 二次操作返回一些回调、迭代方法
var optimizeCb = function(func, context, argCount){
// 如果没有指定 this 指向 ,则返回原函数
if(context === void 0) return func;
switch (argCount === null ? 3 : argCount){
case 1: return function(value){
return func.call(context, value);
};
case 2: return function(value, other){
return func.call(context, value, other);
};
// 如果有指定this,但没有传入 argCount 参数
// 则执行一下 case
case 3: return function(value, index, collection){
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection){
return func.call(context,accumulator, value, index, collection);
}
}
return function(){
return func.apply(context,argumnets)
}
}
解析 Underscore 函数库中的 isArrayLike() 源码
// 一个数组,元素都是对象
// 根据指定的 key 值
// 返回一个数组,元素都是指定 key 值的 value 值
// void 0 返回undefined
var property = function(key){
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};
// Math.pow(2,53) - 1 是 JavaScript 中能精确表示的最大数字
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
// getLength 函数
// 该函数传入一个参数,返回参数的length 属性值
// 用来获取 array 以及 arrayLike 元素的 length 属性值
var getLength = property('length');
// 判断是否是 ArrayLike Object
// 类数组,即拥有 length 属性并且 length 属性值为 Number 类型的元素
// 包括数组、 arguments 、 HTML Collection 以及 NodeList 等等
// 包括类似 {length:10} 这样的对象
// 包括字符串、函数等
var isArrayLike = function(collection) {
// 返回参数 collection 的 length 属性值
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
}
4. 研究 Underscore 函数库中的 _mixin() 如何运转的
上面调用 _each() 方法是函数式风格,我们可以直接就调用 Underscore 库上的 _each() 方法,下面我们来看一下另外一种调用方式风格。
js 代码:
// 面向对象风格 OOP 模式
_([1,2,3]).each(
function(item){
console.log(item)
}
)
执行 _([1,2,3,]) 是怎么做到返回一个对象,而这个对象又能调用 _ 原型上的方法呢?其实在上面加载 Underscore 库的时候我们就看到了 _ 对应的源码,我把源码涉及的部分贴出来。
Underscore 源码:
var _ = function(obj){
if(!(this instanceof _ )) return new _(obj)
this._wrapped = obj
}
// 这就是调用 _([1,2,3]) 的时候涉及 Underscore 第一部分源码,我们来解析一下其中的思想
// 第一步:当执行 _([1,2,3]),因为此时没有 new,所以 this 等于 window,不是_的实例,返回 new _(obj);
// 第二步:这时 this 是 _ 的实例,执行 this._wrapped = [1,2,3];
// 第三步:因为这是构造函数,所以会返回 return this;这个 this 就是实例,实例的 __proto__ 指向其构造函数的prototype,所以我们就可以使用 _ 的原型上的方法了
这也是我们为什么使用构造函数创建对象的原因,构造函数原型上的方法实例都可以使用,可是我们可以从上面看到 Underscore 的源码对 _each = function(){} " 这样写的,这些方法并没有挂载在原型上, _([1,2,3]) 依然会报错,所以我们使用了 _mixin() 方法把这些方法挂在到原型上,我们来看一下 Underscore 是如何实现 _mixin()方法的
Underscore 源码:
// 缓存变量,便于压缩代码
// 此处 【压缩】指的是压缩到 min.js 版本
// 而不是 gzip 压缩
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// 缓存变量,便于压缩代码
// 同时可减少在原型链中的查找次数(提高代码效率)
var
push = ArrayProto.push,
slice = ArrayProto.slice,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// 可向 Underscore 函数库 拓展自己的方法
// obj 参数必须是一个对象(JavaScript 中一切皆对象)
// 且自己的方法定义在 obj 的属性上
// 如 obj.myFunc = function() {...}
// 形如 {myFunc: function() {...}}
// 之后便可使用如下:_.myFunc(..) 或者 OOP _(..).myFunc(..)
_.mixin = function(obj) {
// 遍历 obj 的 key,将方法挂载到 Underscore 上
// 其实是将方法浅拷贝到 _.prototype 上
_.each(_.functions(obj),function(name){
// 直接把方法挂载到 _[name] 上
// 调用类似 _.myFunc([1,2,3], ..)
var func = _[name] = obj[name];
// 浅拷贝
// 将 name 方法挂载到 _ 对象的原型链上,使之能 OOP 调用
_.prototype[name] = function(){
// 第一个参数
var args = [this._wrapped];
// argunments 为 name 方法需要的其他参数
push.apply(args,arguments);
// 执行 func 方法
// 支持链式操作
return result(this, func.apply(_, args));
}
})
}
// 将前面定义的 Underscore 方法添加给包装过的对象
// 即添加到 _.prototype 中
// 使 Underscore 支持面向对象形式的调用
_.mixin(_);
通过调用 _mixin() 方法把 _ 该对象上的属性都绑定在 _.prototype 上从而达到支持 OOP 调用 (注:OOP 就是 JavaScript 中的面向对象过程)
接下来我们看一下 _mixin() 函数中的 _.functions() 方法是做什么用的
// 传入一个对象
// 遍历该对象的键值对(包括 own properties 以及原型链上的)
// 如果某个 value 的类型是方法(function),则将该 key 存入数组
// 将该数组排序后返回
_.functions= _.methods = function(obj) {
// 返回的数组
var names = [];
// if IE < 9
// 且对象重写了 `nonEnumerableProps` 数组中的某些方法
// 那么这些方法名是不会被返回来的
// 可见放弃了 IE < 9 可能对 `toString` 等方法的重写支持
for(var key in obj){
// 如果某个 key 对应的 value 值类型是函数
// 则将这个 key 值存入数组
if(_.isFunction(obj[key])) names.push(key);
}
// 返回排序后的数组
return names.sort()
}
// _.isFunction 在 old v8 ,IE 11 和 Safari 8 的兼容
if(typeof /./ != 'function' && typeof Int8Array != 'object'){
_.isFunction = function(obj) {
return typeof obj == 'function' || false
}
}
5. _mixin() 方法的自定义拓展
从上面 _ mixin(obj) 源码我们可以了解到, _[name] = obj[name] 把 obj 上的属性绑定在 _ 的原型链上,从而我们可以自定义一些方法挂载到 _ 原型链上,通过 OOP 方式调用。
js 代码:
// 自定义方法挂载到 _.prototype 原型链上
_.mixin({
addNum:function(num) {
return num + 1
}
})
// 调用自己自定义的方法
console.log(_(1).addNum()); //2
6. Underscore 函数库常用实用功能
接下来我们说一下 Underscore 函数库到底有哪么些常用、实用的功能
功能目录
- 集合部分: 数组或对象
- 数组部分
- 对象部分
- 链式语法
6.1 集合部分: 数组或对象
- map: 对集合以map方式遍历,产生一个新数组
js 代码:
var arr = _.map([1,2,3], function(ele, idx) {
return ele * 3
})
console.log(arr); // [3,6,9]
-
reduce:集合元素合并集的到memo
js 代码:
var num = _.reduce([1,2,3],function(memo, num) {
return memo + num
},0);
console.log(num); // 6
-
filter:过滤集合中符合条件的元素。注:find:只返回第一个
js 代码:
var arr = _.filter([1,2,3,4,5,6], function(num) {
return num % 2 == 0;
})
console.log(arr); // [2,4,6]
-
reject:过滤集合中不符合条件的元素
js 代码:
var arr = _.reject([1,2,3,4,5,6],function(num) {
return num % 2 == 0;
});
console.log(arr); // [1,3,5]
-
where:遍历list,返回新的对象数组
js 代码:
var list = [
{title:"AAA",year:2020},
{title:"BBB",year:2021} ];
var new_list = _.where(list,{year:2020})
console.log(new_list); // [ {title:"AAA",year:2020}]
-
contains:判断元素是否在list中
js 代码:
console.log(_.contains([1,2,3,], 3)) // true
-
pluck:提取一个集合里指定的属性值
js 代码:
var users = [
{name:"李四",age:18},
{name:"zhangsan",age:16}
];
console.log(_.pluck(users,"age") ); // [18,16]
6.2 数组部分
-
first、last、initial、rest:数组的元素操作
js 代码:
var nums = [5,4,3,2,1];
// 获取数组集合中第一个元素
console.log(_.first(nums)); // 5
// 获取数组集合中最后一个元素
console.log(_.last(nums)); // 1
// 去除数组集合中最后一个元素
console.log(_.initial(nums)); // [5,4,3,2]
// 去除数组集合中第一个元素
console.log(_.rest(nums)); // [4,3,2,1]
- compact:数组去除为 'false' 的值 (0 , false , '' , ....)
js 代码:
console.log(_.compact([0, 1, false, 2, '', 3]) ); => [ 1, 2, 3 ]
-
flatten:将一个嵌套多层的数组(嵌套可以是任何层数)转换为只有一层的数组
js 代码:
console.log(_.flatten([1, [2], [3, [[4]]]]) ); => [ 1, 2, 3, 4 ]
-
without: 去掉指定元素
js 代码:
console.log(_.without([1, 2, 1, 0, 3, 1, 4], 0,1 ) ); => [ 2, 3, 4 ]
-
object: 把数组转换成对象
js 代码:
console.log(_.object(['moe', 'larry', 'curly'], [30, 40, 50]) );
// { moe: 30, larry: 40, curly: 50 }
6.3 对象部分
-
keys、values、paris、invert: 取属性名,取属性值,把对象转换成 [key,value] 数组,对调键值
js 代码:
var obj = {one: 1, two: 2, three: 3};
// 获取对象中的 key 值
console.log(_.keys(obj)); // ['one', 'two', 'three']
// 获取对象中的 value 属性值
console.log(_.values(obj)); // [1, 2, 3]
// 合并属性值转成数组
console.log(_.paris(obj)); // [ [ 'one', 1 ], [ 'two', 2 ], [ 'three', 3 ] ]
// key 跟 value 值互换
console.log(_.invert(obj)); // { '1': 'one', '2': 'two', '3': 'three' }
-
defaults: 复制对象的所有属性到目标对象上,跳过已有属性
js 代码:
var iceCream = {flavor : "chocolate"};
console.log(_.defaults(iceCream, {flavor : "vanilla", sprinkles : "lots"}));
// { flavor: 'chocolate', sprinkles: 'lots' }
6.4 链式语法
-
chain:返回一个封装的对象,在封装的对象上调用方法会返回封装的对象本身, 直到value() 方法调用为止。
js 代码:
var stooges = [
{name : 'curly', age : 25},
{name : 'moe', age : 21},
{name : 'larry', age : 23}
];
var youngest = _.chain(stooges)
.sortBy(function(stooge){ return stooge.age; })
.map(function(stooge){ return stooge.name + ' is ' + stooge.age; })
.first()
.value();
console.log(youngest);
// 'moe is 21'
-
对一个对象使用 chain 方法,会把这个对象封装,并让以后每次方法的调用结束后都返回这个封装的对象, 当您完成了计算,,可以使用 value 函数来取得最终的值。 以下是一个同时使用了 map/flatten/reduce 的链式语法例子,目的是计算一首歌的歌词里每一个单词出现的次数。
js 代码:
var lyrics = [
{line : 1, words : "I'm a lumberjack and I'm okay"},
{line : 2, words : "I sleep all night and I work all day"},
{line : 3, words : "He's a lumberjack and he's okay"},
{line : 4, words : "He sleeps all night and he works all day"}
];
console.log(
_.chain(lyrics)
.map(function(line) { return line.words.split(' '); })
.flatten()
.reduce(function(counts, word) {
counts[word] = (counts[word] || 0) + 1;
return counts;
}, {})
.value()
);
/* { 'I\'m': 2,
a: 2,
lumberjack: 2,
and: 4,
okay: 2,
I: 2,
sleep: 1,
all: 4,
night: 2,
work: 1,
day: 2,
'He\'s': 1,
'he\'s': 1,
He: 1,
sleeps: 1,
he: 1,
works: 1 }
*/
总结:
上面我们介绍了 Underscore 函数库是如何从 _ 对象的赋值封装,再到 _each() 一步一步解剖 Underscore 函数库通过 _mixin( _ ) 对 _ 对象上属性绑定到 _.prototype 原型链上,实现 OOP 模式的调用,这只是初步的架构和常用的方法,其实还有很多方法以及作用和源码,我这边就不一一细说,大家可以自己去研究一下,这个 Underscore 函数库真的很适合作为前端程序员研究源码的第一篇 js 库,希望大家不断去探索新的领域,加强自己的技术。