学习 underscore 源码总体架构,打造属于本身的函数式编程类库

前言

你好,我是若川。这是学习源码总体架构第二篇。总体架构这词语好像有点大,姑且就算是源码总体结构吧,主要就是学习是代码总体结构,不深究其余不是主线的具体函数的实现。本篇文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。前端

学习源码总体架构系列文章以下:vue

1.学习 jQuery 源码总体架构,打造属于本身的 js 类库
2.学习 underscore 源码总体架构,打造属于本身的函数式编程类库
3.学习 lodash 源码总体架构,打造属于本身的函数式编程类库
4.学习 sentry 源码总体架构,打造属于本身的前端异常监控SDK
5.学习 vuex 源码总体架构,打造属于本身的状态管理库
6.学习 axios 源码总体架构,打造属于本身的请求库
7.学习 koa 源码的总体架构,浅析koa洋葱模型原理和co原理
8.学习 redux 源码总体架构,深刻理解 redux 及其中间件原理node

感兴趣的读者能够点击阅读。ios

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

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

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

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

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

看例子中能够看出,这是支持链式调用。编程

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

链式调用

_.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]

基于流的编程

至此就算是分析完了链式调用_.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()效果同样,都能实现链式调用。

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

underscore.js 链式调用图解
underscore.js 链式调用图解

_.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 两个方法,遍历对象上的方法,放入一个数组,而且排序。返回排序后的数组。

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

再来看下underscore.js究竟挂载在_函数对象上有多少静态方法和属性,和挂载_.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个 复制代码

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

原型关系图
uunderscore.js 原型关系图

总体架构概览

匿名函数自执行

(function(){
 }()); 复制代码

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

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

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,讲得真的太好了。笔者在此就不赘述了。 总之,underscore.js做者对这些处理也不是一蹴而就的,也是慢慢积累,和其余人提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挂载方法,最后总体架构分析。学习underscore.js总体架构,利于打造属于本身的函数式编程类库。

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

(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的源码总体架构。学习 lodash 源码总体架构,打造属于本身的函数式编程类库

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

推荐阅读

underscorejs.org 官网
undersercore-analysis
underscore 系列之如何写本身的 underscore

笔者往期文章

面试官问:JS的继承
面试官问:JS的this指向
面试官问:可否模拟实现JS的call和apply方法
面试官问:可否模拟实现JS的bind方法
面试官问:可否模拟实现JS的new操做符
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

关于

做者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,惟善学。
若川的博客,用vuepress重构了,阅读体验可能好些
掘金专栏,欢迎关注~
segmentfault前端视野专栏,开通了前端视野专栏,欢迎关注~
知乎前端视野专栏,开通了前端视野专栏,欢迎关注~
语雀前端视野专栏,新增语雀专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star^_^~

微信公众号 若川视野

可能比较有趣的微信公众号,长按扫码关注。也能够加微信 ruochuan12,注明来源,拉您进【前端视野交流群】。 若川视野

本文使用 mdnice 排版

相关文章
相关标签/搜索