原文: https://zhehuaxuan.github.io/...
做者:zhehuaxuan
Underscore 是一个 JavaScript 工具库,它提供了一整套函数式编程的实用功能,可是没有扩展任何 JavaScript 内置对象。javascript
本文主要梳理underscore内部的函数组织与调用逻辑的方式和思想。前端
经过这篇文章,咱们能够:java
了解underscore在函数组织方面的巧妙构思;为本身书写函数库提供必定思路;node
咱们开始!git
前端的小伙伴必定不会对jQuery陌生,常用$.xxxx
的形式进行调用,underscore使用_.xxxx
,若是本身在ES5语法中写过自定义模块的话,就能够写出下面一段代码:github
//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));
输出结果以下:浏览器
没问题,咱们的函数库制做完成了,咱们通常直接这么用,也不会有太大问题。app
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._ = _; } })();
欢迎各位大佬拍砖!同时您的点赞是我写做的动力~谢谢。