自己的实现
1 | (function() { |
这里将所有的方法添加到一个名为 _ 的对象上,然后将该对象挂载到全局对象上。
之所以不直接 window._ = _ 是因为这里编写的是一个工具函数,不仅要求可以运行在浏览器端,还可以运行在如 Node 环境中。
root
var root = this
这里的 this 在严格模式下是 underfined,为了避免报错我们需要做兼容。
1 | var root = |
事实上,除了 window 和 global 之外,还有一个叫 Web Worker 的东西
Web Worker
Web Worker 属于 HTML5 中的内容。
在 Web Worker 标准中,定义了解决客户端 JavaScript 无法多线程的问题。其中定义的“worker”是指执行代码的并行过程,不够,Web Worker 处在一个自包含的执行环境中,其无法方法 Window 对象和 Document 对象,和主线程之间的通信只能通过异步消息传递机制来实现。
1 | // index.js |
在 Web Worker 中,是无法访问到 Window 对象的,所以 typeof window
和typeof global
的结果都是 undefined,所以最终 root 的值为 false,然后会报错。
虽然在 Web Worker 中不能反问道 Window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象,这里就可以挂载到 self 这个对象上。
在浏览器中,除了 window 属性,我们也可以通过 self 属性直接访问到 Window 对象。
1 | console.log(window.window === window); // true; |
既然 self 的通用性更高,那原来的代码就可以改为下面
1 | var root = |
node vm
依然没完,在 node 的 vm 模块中,也就是沙盒模块,runlnContext 方法中,是不存在 window,也不存在 global 变量的
1 | /** |
我们可以通过 this 来访问到全局对象,所以代码可以写成:
1 | var root = |
微信小程序
到此依旧没有完,在微信小程序中,window 和 global 都是 undefined,加上使用严格模式,this 为 undefined,挂载就会发生错误,所以代码要改写成:
1 | var root = (typeof self == 'object' && self.self == self && self) || |
通过这些步骤下来,我们应该明白,代码的健壮性不是一蹴而就的,而是汇集了很多人的经验,考虑到了很多我们意想不到的地方。
函数对象
现在来看看 var _ = {}
如果仅仅设置 _ 为一个空对象,我们调用方法的时候,只能使用 _.reverse(‘hello’)的方式,实际上,underscore 也支持类似面向对象的方法调用,即:
1 | _("hello").reverse(); // 'olleh' |
1 | //函数式风格 |
既然能以 _([1,2,3])
的形式可以执行,就表明 ‘_‘ 不是一个字面量对象,而是一个函数!
幸运的式,JavaScript 中 函数也是一种对象
1 | var _ = function() {}; |
既然函数的通用性更高,那么就可以把自定以的函数定义在 _
函数上!
1 | var root = |
如何做到 _([1, 2, 3]).each(...)
呢?即 _ 函数返回一个对象,这个对象,如何调用挂在 _ 函数上的方法呢?
这里先来看看 underscore 中是如何做到的
1 | var _ = function(obj) { |
_([1, 2, 3])的执行过程如下:
- 执行
this instanceof _
, this 指向 window,window instanceof _
为 false, !操作符取反,所以执行new _(obj)
. new _(obj)
中,this 指向实例对象,this instanceof _
为 true,取反后,代码接着执行- 执行
this._wrapped = obj
,函数执行结束 - 总结,_([1, 2, 3])返回一个对象, 为
{_wrapped: [1, 2, 3]}
, 该对象的原型指向 _.prototype
我们是将方法挂载到 _ 函数对象上,并没有挂到函数的原型上,所以返回的实例是没有办法调用 _ 函数对象上的方法的!
1 | (function() { |
我们需要一个方法将 _ 上的方法复制到 .prototype 上,这个方法就是 \.mixin.
_.functions
为了将 _ 上的方法复制到原型上,首先我们要获得 _ 上的方法,所以我们先写个 _.functions
方法。
1 | _.functions = function(obj) { |
mixin
1 | var ArrayProto = Array.prototype; |
最终我们已经有了如下的工具函数
1 | (function() { |
至此,我们算是实现了同时支持面向对象和函数风格
导出
终于到了最后一步 root._ = _
,这里直接看源码:
1 | if (typeof exports != "undefined" && !exports.nodeType) { |
为了支持模块化,我们需要将 _ 在合适的环境中作为模块导出,但是 node.js 模块的 API 曾今发生过改变,比如在早期版本中:
1 | // add.js |
在新版本中:
1 | //add.js |
所以我们根据 exports 和 moudle 是否存在来选择不同的导出方式,那么为什么在新版本中,我们还要使用 exports = module.exports = _
呢?
这是因为在 nodeJs 中,exports 是 module.exports 的一个引用,当你使用了 module.exports = function(){},实际上覆盖了module.exports
,但是 exports 并未发生改变,为了避免后面在修改 exports 而导致不能正确输出,就写成这样,将两者保持统一。
下面是个 demo
1 | // exports 是 module.exports的一个引用 |
1 | // addOne.js |
1 | // addOne.js |
最后为什么要进行一个 exports.nodeType 判断呢? 这是因为如果你在 HTML 页面中加入一个 id 为 exports 的元素,比如:
1 | <div id="exports" /> |
就会生成一个 window.exports 全局变量,你可以直接在浏览器命令中打印该变量,这个变量对应的就是这个 dom 元素
此时在浏览器中, typeof exports != 'undefined'
的判断就会生效,然后exports._ = _
,然后在浏览器中,我们需要将 _ 挂载到全局变量上,所以在这里,我们还需要进行一个是否是 DOM 节点的判断。
源码
最终的代码如下,有了这个基本结构,你就可以自由添加需要用到的函数了
1 | (function(){ |