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

若川大约 12 分钟

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

写于2019年8月8日

1. 前言

大家好,我是若川open in new window。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动open in new window,感兴趣的可以点此扫码加我微信 ruochuan02 参与open in new window。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》open in new window,目前是掘金关注人数(4.1k+人)第一的专栏,写有20余篇源码文章。

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

本文仓库地址open in new windowgit clone https://github.com/lxchuan12/underscore-analysis.git

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了

学习源码整体架构系列文章如下:

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

感兴趣的读者可以点击阅读。
其他源码计划中的有:expressopen in new windowvue-rotueropen in new windowreact-reduxopen in new window 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

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

本文章学习的版本是v1.9.1unpkg.com underscore 源码地址open in new window

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

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

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

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

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

2. 链式调用

_.chain 函数源码:

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

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

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

3. _ 函数对象 支持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的继承open in new window画的图)。 构造函数、原型对象和实例关系图

继续分析官方的_.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.open in new window

why delete obj[0]open in new window

4. 基于流的编程

至此就算是分析完了链式调用_.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()

5. _.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 链式调用图解

5.1 _.mixin 挂载自定义方法

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

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

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

5.3 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 原型关系图,毕竟一图胜千言。

 原型关系图
underscore.js 原型关系图

6. 整体架构概览

6.1 匿名函数自执行

(function(){

}());

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

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

6.2 root 处理

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

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

6.3 导出

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

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

6.4 支持 amd 模块化规范

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

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

7. 总结

全文根据官网提供的链式调用的例子, _.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的源码整体架构。

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

8. 推荐阅读

underscorejs.org 官网open in new window
undersercore-analysisopen in new window
underscore 系列之如何写自己的 underscoreopen in new window

笔者往期文章

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

关于

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

微信公众号 若川视野

可能比较有趣的微信公众号,长按扫码关注(回复pdf获取前端优质书籍pdf)。欢迎加笔者微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野
若川视野
欢迎扫码加我微信
拉你进源码共读群
一起学习源码