乐趣区

跟underscore一起学如何写函数库

原文:https://zhehuaxuan.github.io/… 作者:zhehuaxuan
目的
Underscore 是一个 JavaScript 工具库,它提供了一整套函数式编程的实用功能,但是没有扩展任何 JavaScript 内置对象。
本文主要梳理 underscore 内部的函数组织与调用逻辑的方式和思想。
通过这篇文章,我们可以:
了解 underscore 在函数组织方面的巧妙构思;为自己书写函数库提供一定思路;

我们开始!
自己写个函数库
前端的小伙伴一定不会对 jQuery 陌生,经常使用 $.xxxx 的形式进行调用,underscore 使用_.xxxx,如果自己在 ES5 语法中写过自定义模块的话,就可以写出下面一段代码:
//IIFE 函数
(function(){
// 获取全局对象
var root = this;
// 定义对象
var _ = {};
// 定义和实现函数
_.first = function(arr,n){
var n = n || 0;
if(n==0) return arr[0];
return arr.slice(0,n);
}
// 绑定在全局变量上面
root._ = _;
})();
console.log(this);
在 Chrome 浏览器中打开之后,打印出如下结果:

我们看到在全局对象下有一个_属性,属性下面挂载了自定义函数。我们不妨使用_.first(xxxx)在全局环境下直接调用。
console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
输出结果如下:

没问题,我们的函数库制作完成了,我们一般直接这么用,也不会有太大问题。
underscore 是怎么做的?
underscore 正是基于上述代码进行完善,那么 underscore 是如何接着往下做的呢?容我娓娓道来!
对兼容性的考虑
首先是对兼容性的考虑,工具库当然需要考虑各种运行环境。
// Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self`
// instead of `window` for `WebWorker` support.
var root = typeof self == ‘object’ && self.self === self && self ||
typeof global == ‘object’ && global.global === global && global ||
this ||
{};
上面是 underscore1.9.1 在 IIFE 函数中的源码,对应于我们上面自己写的 var root = this;。
在源码中作者也作了解释:创建 root 对象,并且给 root 赋值。怎么赋值呢?
浏览器端:window 也可以是 window.self 或者直接 self 服务端(node):global
WebWorker:self
虚拟机:this

underscore 充分考虑了兼容性,使得 root 指向对局对象。
支持两种不同风格的函数调用
在 underscore 中我们可以使用以下两种方式调用:

函数式的调用:console.log(_.first([1,2,3,4]));

对象式调用:console.log(_([1,2,3,4])).first();

在 underscore 中,它们返回的结果都是相同的。
第一种方式我们现在就没有问题,难点就是第二种方式的实现。
对象式调用的实现
解决这个问题要达到两个目的:

_是一个函数,并且调用返回一个对象;
这个对象依然能够调用挂载在_对象上声明的方法。

我们来看看 underscore 对于_的实现:
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

不怕,我们不妨调用_([1,2,3,4]))看看他是怎么执行的!
第一步:if (obj instanceof _) return obj; 传入的对象及其原型链上有_类型的对象,则返回自身。我们这里的 [1,2,3,4] 显然不是,跳过。
第二步:if (!(this instanceof _)) return new _(obj);,如果当前的 this 对象及其原型链上没有_类型的对象,那么执行 new 操作。调用_([1,2,3,4]))时,this 为 window,那么 (this instanceof _) 为 false,所以我们执行 new _([1,2,3,4])。
第三步:执行 new _([1,2,3,4]),继续调用_函数,这时

obj 为[1,2,3,4]this 为一个新对象,并且这个对象的__proto__指向_.prototype(对于 new 对象执行有疑问,请猛戳此处)

此时
(obj instanceof _)为 false(this instanceof _)为 true

所以此处会执行 this._wrapped = obj;,在新对象中,添加_wrapped 属性,将 [1,2,3,4] 挂载进去。
综合上述函数实现的效果就是:
_([1,2,3,4]))<=====>new _([1,2,3,4])
然后执行如下构造函数:
var _ = function(obj){
this._wrapped = obj
}
最后得到的对象为:

我们执行如下代码:
console.log(_([1,2,3,4]));
console.log(_.prototype);
console.log(_([1,2,3,4]).__proto__ == _.prototype);
看一下打印的信息:

这表明通过_(obj)构建出来的对象确实具有两个特征:

下面挂载了我们传入的对象 / 数组
对象的_proto_属性指向_的 prototype

到此我们已经完成了第一个问题。

接着解决第二个问题:
这个对象依然能够调用挂载在_对象上声明的方法
我们先来执行如下代码:
_([1,2,3,4]).first();
此时 JavaScript 执行器会先去找_([1,2,3,4])返回的对象上是否有 first 属性,如果没有就会顺着对象的原型链上去找 first 属性,直到找到并执行它。
我们发现_([1,2,3,4])返回的对象属性和原型链上都没有 first!

那我们自己先在_.prototype 上面加一个 first 属性上去试试:
(function(){
// 定义
var root = typeof self == ‘object’ && self.self === self && self ||
typeof global == ‘object’ && global.global === global && global ||
this ||
{};

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

_.first = function(arr,n){
var n = n || 0;
if(n==0) return arr[0];
return arr.slice(0,n);
}
_.prototype.first = function(arr,n){
var n = n || 0;
if(n==0) return arr[0];
return arr.slice(0,n);
}
root._ = _;
})();
我们在执行打印一下:
console.log(_([1,2,3,4]));
效果如下:

原型链上找到了 first 函数,我们可以调用 first 函数了。如下:
console.log(_([1,2,3,4]).first());
可惜报错了:

于是调试一下:
我们发现 arr 是 undefined,但是我们希望 arr 是[1,2,3,4]。

我们马上改一下_.prototype.first 的实现
(function(){

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

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

_.first = function(arr,n){
var n = n || 0;
if(n==0) return arr[0];
return arr.slice(0,n);
}
_.prototype.first = function(arr,n=0){
arr = this._wrapped;
if(n==0) return arr[0];
return arr.slice(0,n);
}
root._ = _;
})();
我们在执行一下代码:
console.log(_([1,2,3,4]).first());
效果如下:

我们的效果似乎已经达到了!

现在我们执行下面的代码:
console.log(_([1,2,3,4]).first(2));
调试一下:

凉凉了。

其实我们希望的是:
将 [1,2,3,4] 和 2 以 arguments 的形式传入 first 函数
我们再来改一下:
//_.prototype.first = function(arr,n=0){
// arr = this._wrapped;
// if(n==0) return arr[0];
// return arr.slice(0,n);
//}
_.prototype.first=function(){
/**
* 搜集待传入的参数
*/
var that = this._wrapped;
var args = [that].concat(Array.from(arguments));
console.log(args);
}
我们再执行下面代码:
_([1,2,3,4]).first(2);
看一下打印的效果:

参数都已经拿到了。
我们调用函数一下 first 函数,我们继续改代码:
_.prototype.first=function(){
/**
* 搜集待传入的参数
*/
var that = this._wrapped;
var args = [that].concat(Array.from(arguments));
/**
* 调用在_属性上的 first 函数
*/
return _.first(…args);
}
这样一来_.prototype 上面的函数的实现都省掉了,相当于做一层代理;而且我们不用再维护两套代码,一旦修改实现,两边都要改。
一举两得!
执行一下最初我们的代码:
console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));

现在好像我们所有的问题都解决了。

但是似乎还是怪怪的。我们每声明一个函数都得在原型链上也声明一个同名函数。形如下面:
_.a = function(args){
// a 的实现
}
_.prototype.a = function(){
// 调用_.a(args)
}
_.b = function(args){
// b 的实现
}
_.prototype.b = function(){
// 调用_.b(args)
}
_.c = function(args){
// c 的实现
}
_.prototype.c = function(){
// 调用_.c(args)
}
.
.
.
1000 个函数之后 …
会不会觉得太恐怖了!

我们能不能改成如下这样呢?
_.a = function(args){
// a 的实现
}
_.b = function(args){
// b 的实现
}
_.c = function(args){
// c 的实现
}
1000 个函数之后 …
_.mixin = function(){
// 将_属性中声明的函数都挂载在_prototype 上面
}
_.mixin(_);
上面这么做好处大大的:
我们可以专注于函数库的实现,不用机械式的复写 prototype 上的函数。
underscore 也正是这么做的!
我们看看 mixin 函数在 underscore 中的源码实现:
// Add your own custom functions to the Underscore object.
_.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 _;
};

// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
有了上面的铺垫,这个代码一点都不难看懂,首先调用_.each 函数,形式如下:
_.each(arrs, function(item) {
// 遍历 arrs 数组中的每一个元素
}
我们一想就明白,我们在_对象属性上实现了自定义函数,那么现在要把它们挂载到—_.prototype 属性上面,当然先要遍历它们了。
我们可以猜到_.functions(obj)肯定返回的是一个数组,而且这个数组肯定是存储_对象属性上面关于我们实现的各个函数的信息。
我们看一下_.function(obj)的实现:
_.functions = _.methods = function(obj) {
var names = [];
/**
** 遍历对象中的属性
**/
for (var key in obj) {
// 如果属性值是函数,那么存入 names 数组中
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
确实是这样的!

我们把上述实现的代码整合起来:
(function(){
/**
* 保证兼容性
*/
var root = typeof self == ‘object’ && self.self === self && self ||
typeof global == ‘object’ && global.global === global && global ||
this ||
{};

/**
* 在调用_(obj)时,让其执行 new _(obj), 并将 obj 挂载在_wrapped 属性之下
*/
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

// 自己实现的 first 函数
_.first = function(arr,n){
var n = n || 0;
if(n==0) return arr[0];
return arr.slice(0,n);
}

// 判断是否是函数
_.isFunction = function(obj) {
return typeof obj == ‘function’ || false;
};

// 遍历生成数组存储_对象的函数值属性
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

// 自己实现的遍历数组的函数
_.each = function(arrs,callback){
for(let i=0;i<arrs.length;i++){
callback(arrs[i]);
}
}

var ArrayProto = Array.prototype;
var push = ArrayProto.push;

//underscore 实现的 mixin 函数
_.mixin = function(obj) {
console.log(_.functions(obj)); // 我们打印一下_.functions(_)到底存储了什么?
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return func.apply(_, args);
};
});
return _;
};

// 执行 minxin 函数
_.mixin(_);
root._ = _;
})();
我们看一下_.functions(obj)返回的打印信息:

确实是_中自定义函数的属性值。
我们再来分析一下 each 中 callback 遍历各个属性的实现逻辑。
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return func.apply(_, args);
};
第一句:func 变量存储每个自定义函数
第二句:_.prototype[name]=function(); 在_.prototype 上面声明相同属性的函数
第三句:args 变量存储_wrapped 下面挂载的值
第四句:跟 var args = [that].concat(Array.from(arguments)); 作用相似,将两边的参数结合起来
第五句:执行 func 变量指向的函数,执行 apply 函数,将上下文对象_和待传入的参数 args` 传入即可。
我们再执行以下代码:
console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
结果如下:

Perfect!
这个函数在我们的浏览器中使用已经没有问题。
但是在 Node 中呢?又引出新的问题。
再回归兼容性问题
我们知道在 Node 中,我们是这样的:
//a.js
let a = 1;
module.exports = a;
//index.js
let b = require(‘./a.js’);
console.log(b) // 打印 1
那么:
let _ = require(‘./underscore.js’)
_([1,2,3,4]).first(2);
我们也希望上述的代码能够在 Node 中执行。
所以 root._ = _是不够的。
underscore 是怎么做的呢?
如下:
if (typeof exports != ‘undefined’ && !exports.nodeType) {
if (typeof module != ‘undefined’ && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
我们看到当全局属性 exports 不存在或者不是 DOM 节点时,说明它在浏览器中,所以:
root._ = _;
如果 exports 存在,那么就是在 Node 环境下,我们再来进行判断:
如果 module 存在,并且不是 DOM 节点,并且 module.exports 也存在的话,那么执行:
exports = module.exports = _;
在统一执行:
exports._ = _;
附录
下面是最后整合的阉割版 underscore 代码:
(function(){
/**
* 保证兼容性
*/
var root = typeof self == ‘object’ && self.self === self && self ||
typeof global == ‘object’ && global.global === global && global ||
this ||
{};

/**
* 在调用_(obj)时,让其执行 new _(obj), 并将 obj 挂载在_wrapped 属性之下
*/
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

// 自己实现的 first 函数
_.first = function(arr,n){
var n = n || 0;
if(n==0) return arr[0];
return arr.slice(0,n);
}

// 判断是否是函数
_.isFunction = function(obj) {
return typeof obj == ‘function’ || false;
};

// 遍历生成数组存储_对象的函数值属性
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

// 自己实现的遍历数组的函数
_.each = function(arrs,callback){
for(let i=0;i<arrs.length;i++){
callback(arrs[i]);
}
}

var ArrayProto = Array.prototype;
var push = ArrayProto.push;

//underscore 实现的 mixin 函数
_.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 func.apply(_, args);
};
});
return _;
};

// 执行 minxin 函数
_.mixin(_);
if (typeof exports != ‘undefined’ && !exports.nodeType) {
if (typeof module != ‘undefined’ && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
})();
欢迎各位大佬拍砖!同时您的点赞是我写作的动力~ 谢谢。

退出移动版