学习underscorejs整体架构打造属于自己的函数式编程类库

7次阅读

共计 8247 个字符,预计需要花费 21 分钟才能阅读完成。

前言

上一篇文章写了jQuery 整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库

虽然看过挺多 underscorejs 分析类的文章,但总感觉少点什么。这也许就是 纸上得来终觉浅,绝知此事要躬行 吧。于是决定自己写一篇学习 underscorejs 整体架构的文章。

本文章学习的版本是 v1.9.1
unpkg.com 源码地址:https://unpkg.com/underscore@…

虽然很多人都没用过underscorejs,但看下官方文档都应该知道如何使用。

从一个官方文档 _.chain 简单例子看起:

_.chain([1, 2, 3]).reverse().value();
// => [3, 2, 1]

看例子中可以看出,这是支持链式调用。

读者也可以顺着文章思路,自行打开下载源码进行调试,这样印象更加深刻。

链式调用

_.chain 函数源码:

_.chain = function(obj) {var instance = _(obj);
    instance._chain = true;
    return instance;
};

这个函数比较简单,就是传递 obj 调用 _()。但返回值变量竟然是instance 实例对象。添加属性 _chain 赋值为 true,并返回intance 对象。但再看例子,实例对象竟然可以调用 reverse 方法,再调用 value 方法。猜测支持OOP(面向对象)调用。

带着问题,笔者看了下定义 _ 函数对象的代码。

_ 函数对象 支持OOP

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

如果参数 obj 已经是 _ 的实例了,则返回 obj
如果 this 不是 _ 的实例,则手动 new _(obj);
再次 new 调用时,把 obj 对象赋值给 _wrapped 这个属性。
也就是说最后得到的实例对象是这样的结构
`{

_wrapped: '参数 obj',

}`
它的原型_(obj).__proto___.prototype;

如果对这块不熟悉的读者,可以看下以下这张图 (之前写面试官问:JS 的继承 画的图)。

继续分析官方的 _.chain 例子。这个例子拆开,写成三步。

var part1 = _.chain([1, 2, 3]);
var part2 = part1.reverse();
var part3 = part2.value();

// 没有后续 part1.reverse()操作的情况下
console.log(part1); // {__wrapped: [1, 2, 3], _chain: true}

console.log(part2); // {__wrapped: [3, 2, 1], _chain: true}

console.log(part3); // [3, 2, 1]

思考问题:reverse本是 Array.prototype 上的方法呀。为啥支持链式调用呢。
搜索reverse,可以看到如下这段代码:

并将例子代入这段代码可得(怎么有种高中做数学题的既视感 ^_^):

_.chain([1,2,3]).reverse().value()
var ArrayProto = Array.prototype;
// 遍历 数组 Array.prototype 的这些方法,赋值到 _.prototype 上
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
    // 这里的 `method` 是 reverse 函数
    var method = ArrayProto[name];
    _.prototype[name] = function() {// 这里的 obj 就是数组 [1, 2, 3]
    var obj = this._wrapped;
    // arguments  是参数集合,指定 reverse 的 this 指向为 obj,参数为 arguments,并执行这个函数函数。执行后 obj 则是 [3, 2, 1]
    method.apply(obj, arguments);
    if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
    // 重点在于这里 chainResult 函数。return chainResult(this, obj);
    };
});
// Helper function to continue chaining intermediate results.
var chainResult = function(instance, obj) {// 如果实例中有_chain 为 true 这个属性,则返回实例 支持链式调用的实例对象  { _chain: true, this._wrapped: [3, 2, 1] },否则直接返回这个对象[3, 2, 1]。return instance._chain ? _(obj).chain() : obj;};

if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
提一下上面源码中的这一句,看到这句是百思不得其解。于是赶紧在 github 中搜索这句加上 "" 双引号。表示全部搜索。

搜索到两个在官方库中的ISSUE,大概意思就是兼容 IE 低版本的写法。有兴趣的可以点击去看看。

I don’t understand the meaning of this sentence.

[why delete obj[0]](https://github.com/jashkenas/…

基于流的编程

至此就算是分析完了链式调用 _.chain()_ 函数对象。这种把数据存储在实例对象 {_wrapped: '', _chain: true} 中,_chain 判断是否支持链式调用,来传递给下一个函数处理。这种做法叫做 基于流的编程

最后数据处理完,要返回这个数据怎么办呢。underscore提供了一个 value 的方法。

_.prototype.value = function(){return this._wrapped;}

顺便提供了几个别名。toJSONvalueOf
_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

还提供了 toString的方法。

_.prototype.toString = function() {return String(this._wrapped);
};

这里的 String()new String() 效果是一样的。
可以猜测内部实现和 _函数对象类似。

var String = function(){if(!(this instanceOf String)) return new String(obj);
}
var chainResult = function(instance, obj) {return instance._chain ? _(obj).chain() : obj;};

细心的读者会发现 chainResult 函数中的_(obj).chain(),是怎么实现实现链式调用的呢。

_(obj) 是返回的实例对象 {_wrapped: obj} 呀。怎么会有 chain() 方法,肯定有地方挂载了这个方法到 _.prototype 上或者其他操作,这就是_.mixin()

_.mixin 挂载所有的静态方法到 _.prototype,也可以挂载自定义的方法

_.mixin 混入。但侵入性太强,经常容易出现覆盖之类的问题。记得之前 Reactmixin功能,Vue也有 mixin 功能。但版本迭代更新后基本都是慢慢的都不推荐或者不支持mixin

_.mixin = function(obj) {
    // 遍历对象上的所有方法
    _.each(_.functions(obj), function(name) {// 比如 chain, obj['chain'] 函数,自定义的,则赋值到_[name] 上,func 就是该函数。也就是说自定义的方法,不仅_函数对象上有,而且 `_.prototype` 上也有
    var func = _[name] = obj[name];
    _.prototype[name] = function() {
        // 处理的数据对象
        var args = [this._wrapped];
        // 处理的数据对象 和 arguments 结合
        push.apply(args, arguments);
        // 链式调用  chain.apply(_, args) 参数又被加上了 _chain 属性,支持链式调用。// _.chain = function(obj) {//    var instance = _(obj);
        //    instance._chain = true;
        //    return instance;
        };
        return chainResult(this, func.apply(_, args));
    };
    });
    // 最终返回 _ 函数对象。return _;
};

_.mixin(_);

_mixin(_) 把静态方法挂载到了 _.prototype 上,也就是 _.prototype.chain 方法 也就是 _.chain方法。

所以 _.chain(obj)_(obj).chain()效果一样,都能实现链式调用。

关于上述的链式调用,笔者画了一张图,所谓一图胜千言。

_.mixin 挂载自定义方法

挂载自定义方法:
举个例子:

_.mixin({log: function(){console.log('哎呀,我被调用了');
    }
})
_.log() // 哎呀,我被调用了
_().log() // 哎呀,我被调用了

_.functions(obj)

_.functions = _.methods = function(obj) {var names = [];
    for (var key in obj) {if (_.isFunction(obj[key])) names.push(key);
    }
    return names.sort();};

_.functions_.methods 两个方法,遍历对象上的方法,放入一个数组,并且排序。返回排序后的数组。

underscorejs 究竟在 __.prototype挂载了多少方法和属性

再来看下 underscorejs 究竟挂载在 _函数对象 上有多少静态方法和属性,和挂载 _.prototype 上有多少方法和属性。

使用 for in 循环一试遍知。看如下代码:

var staticMethods = [];
var staticProperty = [];
for(var name in _){if(typeof _[name] === 'function'){staticMethods.push(name);
    }
    else{staticProperty.push(name);
    }
}
console.log(staticProperty); // ["VERSION", "templateSettings"] 两个
console.log(staticMethods); // ["after", "all", "allKeys", "any", "assign", ...] 138 个
var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){if(typeof _.prototype[name] === 'function'){prototypeMethods.push(name);
    }
    else{prototypeProperty.push(name);
    }
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 152 个

根据这些,笔者又画了一张图underscorejs 原型关系图,毕竟一图胜千言。

整体架构概览

匿名函数自执行

(function(){}());

这样保证不污染外界环境,同时隔离外界环境,不是外界影响内部环境。

外界访问不到里面的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。
匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。
关于自执行函数不是很了解的读者可以参看这篇文章。
[[译] JavaScript:立即执行函数表达式(IIFE)](https://segmentfault.com/a/11…

root 处理

var root = typeof self == 'object' && self.self === self && self ||
    typeof global == 'object' && global.global === global && global ||
    this ||
    {};

支持 浏览器环境 nodeWeb Workernode vm 微信小程序

导出

if (typeof exports != 'undefined' && !exports.nodeType) {if (typeof module != 'undefined' && !module.nodeType && module.exports) {exports = module.exports = _;}
    exports._ = _;
} else {root._ = _;}

关于 root 处理导出 的这两段代码的解释,推荐看这篇文章冴羽:underscore 系列之如何写自己的 underscore,讲得真的太好了。笔者在此就不赘述了。
总之,underscorejs作者对这些处理也不是一蹴而就的,也是慢慢积累,和其他人提 ISSUE 之后不断改进的。

支持 amd 模块化规范

if (typeof define == 'function' && define.amd) {define('underscore', [], function() {return _;});
}

_.noConflict 防冲突函数

源码:

// 暂存在 root 上,执行 noConflict 时再赋值回来
var previousUnderscore = root._;
_.noConflict = function() {
    root._ = previousUnderscore;
    return this;
};

使用:

<script>
var _ = '我就是我,不一样的烟火,其他可不要覆盖我呀';
</script>
<script src="https://unpkg.com/underscore@1.9.1/underscore.js">
</script>
<script>
var underscore = _.noConflict();
console.log(_); // '我就是我,不一样的烟火,其他可不要覆盖我呀'
underscore.isArray([]) // true
</script>

总结

全文根据官网提供的链式调用的例子,_.chain([1, 2, 3]).reverse().value();较为深入的调试和追踪代码,分析链式调用(_.chain()_(obj).chain())、OOP、基于流式编程、和 _.mixin(_)_.prototype挂载方法,最后整体架构分析。学习 Underscorejs 整体架构,利于打造属于自己的函数式编程类库。

文章分析的源码整体结构。

(function() {
    var root = typeof self == 'object' && self.self === self && self ||
        typeof global == 'object' && global.global === global && global ||
        this ||
        {};
    var previousUnderscore = root._;

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

    if (typeof exports != 'undefined' && !exports.nodeType) {if (typeof module != 'undefined' && !module.nodeType && module.exports) {exports = module.exports = _;}
      exports._ = _;
    } else {root._ = _;}
    _.VERSION = '1.9.1';

    _.chain = function(obj) {var instance = _(obj);
      instance._chain = true;
      return instance;
    };

    var chainResult = function(instance, obj) {return instance._chain ? _(obj).chain() : obj;};

    _.mixin = function(obj) {_.each(_.functions(obj), function(name) {var func = _[name] = obj[name];
        _.prototype[name] = function() {var args = [this._wrapped];
          push.apply(args, arguments);
          return chainResult(this, func.apply(_, args));
        };
      });
      return _;
    };

    _.mixin(_);

    _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {var method = ArrayProto[name];
      _.prototype[name] = function() {
        var obj = this._wrapped;
        method.apply(obj, arguments);
        if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
        return chainResult(this, obj);
      };
    });

    _.each(['concat', 'join', 'slice'], function(name) {var method = ArrayProto[name];
      _.prototype[name] = function() {return chainResult(this, method.apply(this._wrapped, arguments));
      };
    });

    _.prototype.value = function() {return this._wrapped;};

    _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

    _.prototype.toString = function() {return String(this._wrapped);
    };

    if (typeof define == 'function' && define.amd) {define('underscore', [], function() {return _;});
    }
}());

下一篇文章可能是学习 lodash 的源码整体架构。

读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

推荐阅读

underscorejs.org 官网

undersercore-analysis

underscore 系列之如何写自己的 underscore

笔者往期文章

学习 jQuery 源码整体架构,打造属于自己的 js 类库

面试官问:JS 的继承

面试官问:JS 的 this 指向

面试官问:能否模拟实现 JS 的 call 和 apply 方法

面试官问:能否模拟实现 JS 的 bind 方法

面试官问:能否模拟实现 JS 的 new 操作符

前端使用 puppeteer 爬虫生成《React.js 小书》PDF 并合并

关于

作者:常以 若川 为名混迹于江湖。前端路上 | PPT 爱好者 | 所知甚少,唯善学。

个人博客

掘金专栏,欢迎关注~

segmentfault前端视野专栏,开通了 前端视野 专栏,欢迎关注~

知乎前端视野专栏,开通了 前端视野 专栏,欢迎关注~

github blog,相关源码和资源都放在这里,求个star^_^~

微信交流群

添加微信 lxchuan12,注明来源。拉您进【前端视野交流群

正文完
 0