在工作中经常有写一些独立的功能模块,对于如何抽象出来构建自己的工具函数库也有些思考,在阅读完资料中的介绍后,有很多醒悟的地方,代码的健壮性,并非一蹴而就,而是汇集了很多人的经验,考虑到了很多我们意想不到的地方。
自己实现
在自己的工作中,我有可能会这么做
(function(app){
var _ = {};
app._ = _;
// 在这里添加自己的方法
_.reverse = function(string){
return string.split('').reverse().join('');
}
})(window)
或者这么操作,直接指向this
(function(){
var root = this;
var _ = {};
root._ = _;
// 在这里添加自己的方法
_.reverse = function(string){
return string.split('').reverse().join('');
}
})()
整篇文章看完之后才发现,这玩意写的真垃圾,垃圾在哪些地方呢:
- 对象挂载到全局对象没有对环境做检测,使用情况有限
- 能否支持面向对象风格,而不仅是函数风格
- 到处没有考虑到JavaScript模块化下的导出方案
接下来看如何一步一步健壮吧!
全局对象选择
主要需要考虑的情况有如下几种:
- 在严格模式下,this 返回 undefined,而不是指向Window,且在 ES6 中模块脚本自动采用严格模式,不管有没有声明 use strict。
- Node环境中,全局对象不是Window,而是global
- Web Worker 属于 HTML5 中的内容,Web Worker 处在一个自包含的执行环境中,无法访问 Window 对象和 Document 对象,和主线程之间的通信业只能通过异步消息传递机制来实现。
- typeof window 和 typeof global 的结果都是 undefined
- 能通过 self 访问到 Worker 环境中的全局对象。且在浏览器中,除了 window 属性,我们也可以通过 self 属性直接访问到 Winow 对象。
- 在 node 的 vm 模块中,也就是沙盒模块,runInContext 方法中,是不存在 window,也不存在 global 变量的,但是我们却可以通过 this 访问到全局对象
- 在微信小程序中,window 和 global 都是 undefined,加上又强制使用严格模式,this 为 undefined,挂载就会发生错误,此时直接赋值为{}
最终代码:
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this ||
{};
函数对象
如果仅仅设置 _
为一个空对象,我们调用方法的时候,只能使用 _.reverse('hello')
的方式,实际上,underscore 也支持类似面向对象的方式调用,即:
_('hello').reverse(); // 'olleh'
这里underscore的实现方式让我感触很深,可以说是一个比较综合的东西了,实现步骤如下:
_
不能是字面量对象,而是一个函数,_
函数需要返回一个对象,var _ = function(obj) { // 如果不是通过new的方式调用,则强行new if (!(this instanceof _)) return new _(obj); // 将参数保存为内部属性 this._wrapped = obj; };
此时返回
_
实例,但是无法调用_
函数对象上的方法的,因为没有挂载方法到函数原型上找出
_
对象上所有函数_.functions = function(obj) { var names = []; for (var key in obj) { if (_.isFunction(obj[key])) names.push(key); } return names.sort(); };
mixin
var ArrayProto = Array.prototype; var push = ArrayProto.push; _.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 _; }; _.mixin(_);
值得注意的是:因为 _[name] = obj[name]
的缘故,我们可以给 underscore 拓展自定义的方法:
_.mixin({
addOne: function(num) {
return num + 1;
}
});
导出
直接看源码,考虑到模块化情况:
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
在这里我们回忆下在node中模块化的方案
- 每一个node.js执行文件,都自动创建一个module对象,同时,module对象会创建一个叫exports的属性,初始化的值是 {}
- exports和module.exports指向同一块内存,但require()返回的是module.exports而不是exports。
- 因此我感觉exports存在的意义?简写?
- API发生了改变?
- 一切都很美好,但是如果你不是对exports或module.exports指向的内存中做了修改,而是进行了覆盖,将其指向一块新的内存,就会打断两者之间的引用关系,由于require()返回的是module.exports,因此如果在exports上进行的上述操作,往往就会出现问题了
这就可以解释我们为什么使用exports = module.exports = _
了,为了避免后面再修改 exports 而导致不能正确输出,就写成这样,将两者保持统一。
为什么要进行一个 exports.nodeType
判断呢?哈哈,这里确实刷新我的认识了,这是因为如果你在 HTML 页面中加入一个 id 为 exports 的元素,比如
<div id="exports"></div>
就会生成一个 window.exports
全局变量,你可以直接在浏览器命令行中打印该变量。此时在浏览器中,typeof exports != 'undefined'
的判断就会生效,然后 exports._ = _
,然而在浏览器中,我们需要将 _
挂载到全局变量上呐,所以在这里,我们还需要进行一个是否是 DOM 节点的判断。