7

最近开始读源码了,遵循大多数过来人的建议,underscore.js是最适合新手阅读的一个js库,所以我就迫不及待地下载了underscore.js,1.8.3版本。

IIFE(立即执行函数)

与其他第三方库一样,underscore.js也是通过IIFE来包裹自己的业务逻辑。

(function(){
    ...
}.call(this))

通过传入this(浏览器环境中其实就是window对象)来改变函数的作用域。(function(){}.call(this))等同于(function(){}()),就变成熟知的IIFE的写法了。

关于IIFEthis,附上两篇文章,有助于更好地认识相关问题。

详解javascript立即执行函数表达式(IIFE)
如何理解JavaScript中的函数调用和“this”

暴露一个全局对象_

(function(){
    var root = this;
    
    // 定义underscore对象
    var _ = function (obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
    };
    
    // 把underscore对象暴露到全局作用域
    if (typeof exports !== 'undefined') {
        if (typeof module !== 'undefined' && module.exports) {
          exports = module.exports = _;
        }
        exports._ = _;
     } else {
        root._ = _;
     }
}.call(this));

可以看到代码中的_被定义成了一个函数(暂时先不管函数里面的代码是什么意思),函数也是对象,所以可以在_对象上添加一些方法。比如:

(function(){
    ...
    
    _.sayHello = function(name) {
        console.log('Hello, ' + name + '!');
    };
}.call(this));

// 此时就可以在我们自己的js代码中调用这个方法了
_.sayHello('underscore'); // 'Hello, underscore!'

这也是underscore.js大部分情况下的调用方式,_.funcName(arg1, arg2),文档中也是以这种方式调用的。

看,我们好像也写出了一个类underscore.js库呢,好开森啊,哈哈~~~

面向对象编程(OOP)风格

在函数式编程(FP)范式中,函数是拿来就用的,即便是对象,也只是函数的一个参数:

var arr = [1, 2, 3];
_.map(arr, function(item) {
    return item * 2;
});

而在OOP中,函数一般是隶属于某个对象的方法:

var arr = [1, 2, 3];
arr.map(function(item) {
    return item * 2;
});

underscore.js是推崇函数式编程的,但是也提供了OOP风格的函数调用方式,仅需要通过_()来包裹一下对象即可:

var arr = [1, 2, 3];

// FP风格
_.map(arr, function(item) {
    return itme * 2;
});

// OOP风格
_(arr).map(function(item) {
    return item * 2;
});

通过_()构造出一个实例对象,下面来分析一下这个构造过程:

var _ = function(obj) {
    if (obj instanceof _) return obj;
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
};

_([1, 2]);
// 当执行这句代码时,函数中的this指向window
// obj instanceof _ === false, this instanceof _ === false
// 所以执行 return new _(obj)
// 在new的时候,this指向了构造函数_的实例,所以 this instanceof _ === true
// this._wrapped = obj; 表示构造实例有一个属性_wrapped,值为obj(供后面_.mixin方法调用)

// 当obj已经是一个实例的时候,_(obj)直接返回这个实例: _(_([1,2])) == _([1,2])
// 自此,构造过程就结束了
// 这也就是所谓的 无new调用的构造函数

_的构造实例怎么调用_.func这些方法呢?构造实例本身没有这些方法,那么这些方法应该存在原型链上,也就是_.prototype上。_又是如何把自身的方法挂载到_.prototype上面呢?

思路:遍历_的属性,如果某个属性的类型是function,就把该函数挂载到_.prototype上:

_.mixin = function(obj) {
    // _.functions(obj) 返回obj中所有的方法
    _.each(_.functions(obj), function(name) { 
       _.prototype[name] = function() {
          var args = [this._wrapped];
          // _(args1).func(args2)  == _.func(args1, args2)
          // 右侧func的参数比左侧func的参数多一个,也就是this._wrapped
          push.apply(args, arguments); 
          return obj[name].apply(_, args); // 方法调用结果
       };
   });
};
_.mixin(_); // 把 _ 对象上的方法都挂载到其原型上

链式调用

想要实现链式调用,通常我们会在支持链式调用的函数中返回对象本身:

var car = {
    run: function() {
        console.log('begin run');
        return this;
    },
    stop: function() {
        console.log('stopped');
    }
};
car.run().stop();
// => begin run
// => stopped

underscore.js中链式调用的实现:

// 开启一个链式调用
_.chain = function(obj) {
   var instance = _(obj); // 获得一个underscore实例
   instance._chain = true; // 标识当前实例支持链式调用
   return instance;
};
// Helper函数,用来判断 实例调用某一方法之后的返回结果是否支持继续链式调用
var result = function(instance, obj) {
   return instance._chain ? _(obj).chain() : obj;
};

// 此时,我们要把上面的_.mixin函数更新一下了
_.mixin = function(obj) {
    _.each(_.functions(obj), function(name) { 
       _.prototype[name] = function() {
          var args = [this._wrapped];
          push.apply(args, arguments); 
          return result(this, obj[name].apply(_, args)); // this指向underscore实例
       };
   });
};

// 由于链式调用的结果被转化为一个带有_chain属性的underscore实例对象,
// {_wrapped: obj, _chain: true}
// 所以想要获取链式调用的结果时,需要有个取值过程
_.prototype.value = function() {
   return this._wrapped; // 返回被包裹的对象
};

// 链式调用demo
var testArr = [1, 2, 3];
_.chain(testArr)
    .map(function(item) {
        return item * 5;
    })
    .each(function(item) {
        console.log(item);
    })
    .first()
    .value(); // 5
// _(testArr).chain().map().each()... 这种方式也可以

Mixin

mixin(混入)模式是增加代码复用度的一个广泛使用的设计模式:向一个对象混入一系列方法,使之具备更强大的能力,这一系列方法又包裹在一个称之为mixin的对象中,这样,其他对象也能够通过该mixin进行扩展。

clipboard.png

underscore.js也允许用户扩充_的功能,只需要在_.mixin函数上加上一行代码:

// 完整版
_.mixin = function(obj) {
    _.each(_.functions(obj), function(name) { 
        // 当obj是自定义对象时,
        // obj的方法被扩充到underscore的方法集合中以及_.prototype上
       var func = _[name] = obj[name];
       _.prototype[name] = function() {
          var args = [this._wrapped];
          push.apply(args, arguments); 
          return result(this, func.apply(_, args)); 
       };
   });
};

// demo
_.mixin({
    add5: function(num) {
        return num + 5;
    }
});
_.add5(5); // 10
_([1,2,3]).chain().map(function(item) {
    return item * 10;
}).first().add5().value(); // 15

理解了以上所说的内容,underscore.js的整体代码架构就掌握的差不多了,接下去就可以一个一个方法的去看具体代码细节。才刚读了这些内容,我就感觉收货颇多,特别是读到Mixin模式的时候,感觉像发现了新大陆似的,接下来会去再看一些其他类型的设计模式。

第一次读源码,认识不足或分析不到位的地方,还请路过的大大给以指正,谢谢!

以下两个链接的内容对于我理清underscore.js的代码结构起了很大帮助,非常感谢原作者。


无名小贝勒
5.7k 声望324 粉丝

引用和评论

0 条评论