JavaScript进阶之一篇读懂Underscore.js的作用及源码

2,752 阅读13分钟

官网对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为我们提供了这样的一个实用工具包。

该文章的目录:

  1. Underscore安装;
  2. 从下划线 "_ " 开始解剖;
  3. 通过解析 _each() 方法深入 Underscore 函数库;
  4. 研究 Underscore 函数库中的 _mixin() 如何运转的;
  5. _mixin() 的自定义拓展;
  6. Underscore 函数库常用实用功能;

1. Underscore安装


​ Underscore.js 是一个 Javascript 功能类库,不依赖于环境,可以加载到HTML中在浏览器运行,也可以直接在 Node.js 的环境中使用。

官网地址:www.html.cn/doc/undersc…

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 库,希望大家不断去探索新的领域,加强自己的技术。