查看原文
其他

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

若川视野 若川视野 2022-05-01

前言

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

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

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

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

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

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

  2. // => [3, 2, 1]

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

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

链式调用

_.chain 函数源码:

  1. _.chain = function(obj) {

  2. var instance = _(obj);

  3. instance._chain = true;

  4. return instance;

  5. };

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

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

_ 函数对象 支持 OOP

  1. var _ = function(obj) {

  2. if (obj instanceof _) return obj;

  3. if (!(this instanceof _)) return new _(obj);

  4. this._wrapped = obj;

  5. };

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

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

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

  1. var part1 = _.chain([1, 2, 3]);

  2. var part2 = part1.reverse();

  3. var part3 = part2.value();


  4. // 没有后续part1.reverse()操作的情况下

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


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


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

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

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

  1. _.chain([1,2,3]).reverse().value()s

  1. var ArrayProto = Array.prototype;

  2. // 遍历 数组 Array.prototype 的这些方法,赋值到 _.prototype 上

  3. _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {

  4. // 这里的`method`是 reverse 函数

  5. var method = ArrayProto[name];

  6. _.prototype[name] = function() {

  7. // 这里的obj 就是数组 [1, 2, 3]

  8. var obj = this._wrapped;

  9. // arguments 是参数集合,指定reverse 的this指向为obj,参数为arguments, 并执行这个函数函数。执行后 obj 则是 [3, 2, 1]

  10. method.apply(obj, arguments);

  11. if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];

  12. // 重点在于这里 chainResult 函数。

  13. return chainResult(this, obj);

  14. };

  15. });

  1. // Helper function to continue chaining intermediate results.

  2. var chainResult = function(instance, obj) {

  3. // 如果实例中有_chain 为 true 这个属性,则返回实例 支持链式调用的实例对象 { _chain: true, this._wrapped: [3, 2, 1] },否则直接返回这个对象[3, 2, 1]。

  4. return instance._chain ? _(obj).chain() : obj;

  5. };

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

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

I don't understand the meaning of this sentence.

why delete obj[0]

基于流的编程

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

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

  1. _.prototype.value = function(){

  2. return this._wrapped;

  3. }

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

还提供了 toString的方法。

  1. _.prototype.toString = function() {

  2. return String(this._wrapped);

  3. };

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

  1. var String = function(){

  2. if(!(this instanceOf String)) return new String(obj);

  3. }

  1. var chainResult = function(instance, obj) {

  2. return instance._chain ? _(obj).chain() : obj;

  3. };

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

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

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

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

  1. _.mixin = function(obj) {

  2. // 遍历对象上的所有方法

  3. _.each(_.functions(obj), function(name) {

  4. // 比如 chain, obj['chain'] 函数,自定义的,则赋值到_[name] 上,func 就是该函数。也就是说自定义的方法,不仅_函数对象上有,而且`_.prototype`上也有

  5. var func = _[name] = obj[name];

  6. _.prototype[name] = function() {

  7. // 处理的数据对象

  8. var args = [this._wrapped];

  9. // 处理的数据对象 和 arguments 结合

  10. push.apply(args, arguments);

  11. // 链式调用 chain.apply(_, args) 参数又被加上了 _chain属性,支持链式调用。

  12. // _.chain = function(obj) {

  13. // var instance = _(obj);

  14. // instance._chain = true;

  15. // return instance;

  16. };

  17. return chainResult(this, func.apply(_, args));

  18. };

  19. });

  20. // 最终返回 _ 函数对象。

  21. return _;

  22. };


  23. _.mixin(_);

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

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

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

_.mixin 挂载自定义方法

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

  1. _.mixin({

  2. log: function(){

  3. console.log('哎呀,我被调用了');

  4. }

  5. })

  6. _.log() // 哎呀,我被调用了

  7. _().log() // 哎呀,我被调用了

_.functions(obj)

  1. _.functions = _.methods = function(obj) {

  2. var names = [];

  3. for (var key in obj) {

  4. if (_.isFunction(obj[key])) names.push(key);

  5. }

  6. return names.sort();

  7. };

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

underscore.js 究竟在 _和 _.prototype挂载了多少方法和属性

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

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

  1. var staticMethods = [];

  2. var staticProperty = [];

  3. for(var name in _){

  4. if(typeof _[name] === 'function'){

  5. staticMethods.push(name);

  6. }

  7. else{

  8. staticProperty.push(name);

  9. }

  10. }

  11. console.log(staticProperty); // ["VERSION", "templateSettings"] 两个

  12. console.log(staticMethods); // ["after", "all", "allKeys", "any", "assign", ...] 138个

  1. var prototypeMethods = [];

  2. var prototypeProperty = [];

  3. for(var name in _.prototype){

  4. if(typeof _.prototype[name] === 'function'){

  5. prototypeMethods.push(name);

  6. }

  7. else{

  8. prototypeProperty.push(name);

  9. }

  10. }

  11. console.log(prototypeProperty); // []

  12. console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 152个

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

整体架构概览

匿名函数自执行

  1. (function(){


  2. }());

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

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

root 处理

  1. var root = typeof self == 'object' && self.self === self && self ||

  2. typeof global == 'object' && global.global === global && global ||

  3. this ||

  4. {};

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

导出

  1. if (typeof exports != 'undefined' && !exports.nodeType) {

  2. if (typeof module != 'undefined' && !module.nodeType && module.exports) {

  3. exports = module.exports = _;

  4. }

  5. exports._ = _;

  6. } else {

  7. root._ = _;

  8. }

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

支持 amd 模块化规范

  1. if (typeof define == 'function' && define.amd) {

  2. define('underscore', [], function() {

  3. return _;

  4. });

  5. }

_.noConflict 防冲突函数

源码:

  1. // 暂存在 root 上, 执行noConflict时再赋值回来

  2. var previousUnderscore = root._;

  3. _.noConflict = function() {

  4. root._ = previousUnderscore;

  5. return this;

  6. };

使用:

  1. <script>

  2. var _ = '我就是我,不一样的烟火,其他可不要覆盖我呀';

  3. </script>

  4. <script src="https://unpkg.com/underscore@1.9.1/underscore.js">

  5. </script>

  6. <script>

  7. var underscore = _.noConflict();

  8. console.log(_); // '我就是我,不一样的烟火,其他可不要覆盖我呀'

  9. underscore.isArray([]) // true

  10. </script>

总结

全文根据官网提供的链式调用的例子, _.chain([1,2,3]).reverse().value();较为深入的调试和追踪代码,分析链式调用( _.chain()_(obj

).chain())、 OOP、基于流式编程、和 _.mixin(_)_.prototype挂载方法,最后整体架构分析。学习 underscore.js整体架构,利于打造属于自己的函数式编程类库。

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

  1. (function() {

  2. var root = typeof self == 'object' && self.self === self && self ||

  3. typeof global == 'object' && global.global === global && global ||

  4. this ||

  5. {};

  6. var previousUnderscore = root._;


  7. var _ = function(obj) {

  8. if (obj instanceof _) return obj;

  9. if (!(this instanceof _)) return new _(obj);

  10. this._wrapped = obj;

  11. };


  12. if (typeof exports != 'undefined' && !exports.nodeType) {

  13. if (typeof module != 'undefined' && !module.nodeType && module.exports) {

  14. exports = module.exports = _;

  15. }

  16. exports._ = _;

  17. } else {

  18. root._ = _;

  19. }

  20. _.VERSION = '1.9.1';


  21. _.chain = function(obj) {

  22. var instance = _(obj);

  23. instance._chain = true;

  24. return instance;

  25. };


  26. var chainResult = function(instance, obj) {

  27. return instance._chain ? _(obj).chain() : obj;

  28. };


  29. _.mixin = function(obj) {

  30. _.each(_.functions(obj), function(name) {

  31. var func = _[name] = obj[name];

  32. _.prototype[name] = function() {

  33. var args = [this._wrapped];

  34. push.apply(args, arguments);

  35. return chainResult(this, func.apply(_, args));

  36. };

  37. });

  38. return _;

  39. };


  40. _.mixin(_);


  41. _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {

  42. var method = ArrayProto[name];

  43. _.prototype[name] = function() {

  44. var obj = this._wrapped;

  45. method.apply(obj, arguments);

  46. if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];

  47. return chainResult(this, obj);

  48. };

  49. });


  50. _.each(['concat', 'join', 'slice'], function(name) {

  51. var method = ArrayProto[name];

  52. _.prototype[name] = function() {

  53. return chainResult(this, method.apply(this._wrapped, arguments));

  54. };

  55. });


  56. _.prototype.value = function() {

  57. return this._wrapped;

  58. };


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


  60. _.prototype.toString = function() {

  61. return String(this._wrapped);

  62. };


  63. if (typeof define == 'function' && define.amd) {

  64. define('underscore', [], function() {

  65. return _;

  66. });

  67. }

  68. }());

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

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

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客 http://lxchuan12.cn
https://github.com/lxchuan12/blog,相关源码和资源都放在这里,求个 star^_^~

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

下图是公众号二维码:若川视野,一个可能比较有趣的前端开发类公众号

往期文章

工作一年后,我有些感悟(写于2017年)

高考七年后、工作三年后的感悟

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


由于公众号限制外链,点击阅读原文,或许阅读体验更佳

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存