underscore之编写自己的underscore

原文来自冴羽的博客

自己的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function() {
var root = this;
var _ = {};

root._ = _;

//这里添加自己的方法
_.reverse = function(string) {
return string
.split("")
.reverse()
.join("");
};
})();

//使用
_.reverse("blue"); // eulb

这里将所有的方法添加到一个名为 _ 的对象上,然后将该对象挂载到全局对象上。

之所以不直接 window._ = _ 是因为这里编写的是一个工具函数,不仅要求可以运行在浏览器端,还可以运行在如 Node 环境中。

root

var root = this 这里的 this 在严格模式下是 underfined,为了避免报错我们需要做兼容。

1
2
3
var root =
(typeof window == "object" && window.window == window && window) ||
(typeof global == "object" && global.global == global && global);

事实上,除了 window 和 global 之外,还有一个叫 Web Worker 的东西

Web Worker

Web Worker 属于 HTML5 中的内容。

在 Web Worker 标准中,定义了解决客户端 JavaScript 无法多线程的问题。其中定义的“worker”是指执行代码的并行过程,不够,Web Worker 处在一个自包含的执行环境中,其无法方法 Window 对象和 Document 对象,和主线程之间的通信只能通过异步消息传递机制来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// index.js

/**
* Web Worker
* 在火狐中可以直接打开测试,在 Chrome 中需要起服务器
*/
var i = 0;

function timedCount() {
i = i + 1;

console.log('window 对象为:', typeof window)
console.log('global 对象为:', typeof global)
console.log('self 对象为:', self)
var root = (typeof window == 'object' && window.window == window && window) ||
(typeof global == 'object' && global.global == global && global);
console.log(root)
postMessage(i);
setTimeout("timedCount()", 500);
}

timedCount();

// webworker.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>

<body>
<p>Count numbers:
<output id="result"></output>
</p>
<button onclick="startWorker()">Start Worker</button>
<button onclick="stopWorker()">Stop Worker</button>
<script>
var w;
function startWorker() {
if (typeof(Worker) !== "undefined") {
if (typeof(w) == "undefined") {
w = new Worker("./index.js");
}
w.onmessage = function(event) {
document.getElementById("result").innerHTML = event.data;
}
}
else {
document.getElementById("result").innerHTML = "Sorry, your browser does not support Web Workers...";
}
}
function stopWorker() {
w.terminate();
}
</script>
</body>

</html>

在 Web Worker 中,是无法访问到 Window 对象的,所以 typeof windowtypeof global的结果都是 undefined,所以最终 root 的值为 false,然后会报错。

虽然在 Web Worker 中不能反问道 Window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象,这里就可以挂载到 self 这个对象上。

在浏览器中,除了 window 属性,我们也可以通过 self 属性直接访问到 Window 对象。

1
2
console.log(window.window === window); // true;
console.log(window.self === window); // true;

既然 self 的通用性更高,那原来的代码就可以改为下面

1
2
3
var root =
(typeof self == "object" && self.self == self && self) ||
(typeof global == "object" && global.global == global && global);

node vm

依然没完,在 node 的 vm 模块中,也就是沙盒模块,runlnContext 方法中,是不存在 window,也不存在 global 变量的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* http://www.alloyteam.com/2015/04/xiang-jie-nodejs-di-vm-mo-kuai/
*/
var vm = require("vm");
var util = require("util");

var window = {
p: 2,
vm: vm,
console: console,
require: require
};

var p = 5;

global.p = 11;

vm.createContext(window);

// global是 undefined
// vm.runInContext('p = 3;console.log(global);', window);

// 报错 window is not defined
// vm.runInContext('p = 3;console.log(window);', window);

// this 是有值的
vm.runInContext("p = 3;console.log(this);", window);

// console.log(window.p);// 被改变为3
// console.log(util.inspect(window));

我们可以通过 this 来访问到全局对象,所以代码可以写成:

1
2
3
4
var root =
(typeof self == "object" && self.self == self && self) ||
(typeof global == "object" && global.global == global && global) ||
this;

微信小程序

到此依旧没有完,在微信小程序中,window 和 global 都是 undefined,加上使用严格模式,this 为 undefined,挂载就会发生错误,所以代码要改写成:

1
2
3
4
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global)
|| this
{},

通过这些步骤下来,我们应该明白,代码的健壮性不是一蹴而就的,而是汇集了很多人的经验,考虑到了很多我们意想不到的地方。

函数对象

现在来看看 var _ = {}
如果仅仅设置 _ 为一个空对象,我们调用方法的时候,只能使用 _.reverse(‘hello’)的方式,实际上,underscore 也支持类似面向对象的方法调用,即:

1
_("hello").reverse(); // 'olleh'
1
2
3
4
5
6
7
8
//函数式风格
_.each([1, 2, 3], function(item) {
console.log(item);
});

_([1, 2, 3]).each(function(item) {
console.log(item);
});

既然能以 _([1,2,3])的形式可以执行,就表明 ‘_‘ 不是一个字面量对象,而是一个函数!
幸运的式,JavaScript 中 函数也是一种对象

1
2
3
4
5
6
7
8
var _ = function() {};
_.value = 1;
_.log = function() {
return this.value + 1;
};

console.log(_.value); // 1
console.log(_.log()); // 2

既然函数的通用性更高,那么就可以把自定以的函数定义在 _ 函数上!

1
2
3
4
5
6
7
8
var root =
(typeof self == "object" && self.self == self && self) ||
(typeof global == "object" && global.global == global && global) ||
this ||
{};
var _ = function() {};

root._ = _;

如何做到 _([1, 2, 3]).each(...)呢?即 _ 函数返回一个对象,这个对象,如何调用挂在 _ 函数上的方法呢?

这里先来看看 underscore 中是如何做到的

1
2
3
4
5
6
var _ = function(obj) {
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

_([1, 2, 3]);

_([1, 2, 3])的执行过程如下:

  1. 执行 this instanceof _, this 指向 window, window instanceof _为 false, !操作符取反,所以执行 new _(obj).
  2. new _(obj)中,this 指向实例对象,this instanceof _为 true,取反后,代码接着执行
  3. 执行 this._wrapped = obj,函数执行结束
  4. 总结,_([1, 2, 3])返回一个对象, 为{_wrapped: [1, 2, 3]}, 该对象的原型指向 _.prototype

我们是将方法挂载到 _ 函数对象上,并没有挂到函数的原型上,所以返回的实例是没有办法调用 _ 函数对象上的方法的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function() {
var root =
(typeof self == "object" && self.self == self && self) ||
(typeof global == "object" && global.global == global && global) ||
this ||
{};
var _ = function(obj) {
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
root._ = _;

_.log = function() {
console.log(1);
};
})();

console.log(_); // function(obj){ ... }
console.log(_(1)); // _{_wrapped : 1}
console.log(typeof _()); // object
console.log(typeof _); // function

_().log(); //_( ... ) log is not a function

我们需要一个方法将 _ 上的方法复制到 .prototype 上,这个方法就是 \.mixin.

_.functions

为了将 _ 上的方法复制到原型上,首先我们要获得 _ 上的方法,所以我们先写个 _.functions方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
_.functions = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

var class2type = {};
// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error"
.split(" ")
.map(function(item, index) {
class2type["[object " + item + "]"] = item.toLowerCase();
});
_.type = function(obj) {
// 一箭双雕
if (obj == null) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function"
? class2type[Object.prototype.toString.call(obj)] || "object"
: typeof obj;
};

_.isFunction = function(obj) {
return _.type(obj) === "function";
};

mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var ArrayProto = Array.prototype;
var push = ArrayProto.push;

_.each = function(obj, callback) {
var length,
i = 0;

if (isArrayLike(obj)) {
length = obj.length;
for (; i < length; i++) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
}
return obj;
};

_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = (_[name] = obj[name]);
// 把 _ 对象上的函数挂载到 _.prototype上
_.prototype[name] = function() {
console.log(this instanceof _);
var args = [this._wrapped];
push.apply(args, arguments);

return func.apply(_, args);
};
});
return _;
};

最终我们已经有了如下的工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
(function() {
var root =
(typeof self == "object" && self.self == self && self) ||
(typeof global == "object" && global.global == global && global) ||
this ||
{};
var _ = function(obj) {
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

root._ = _;

// 获得 _ 对象上的所有 function
_.functions = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

// 判断数据类型
_.type = (function() {
var class2type = {};
// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error"
.split(" ")
.map(function(item, index) {
class2type["[object " + item + "]"] = item.toLowerCase();
});

return function(obj) {
// 一箭双雕
if (obj == null) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function"
? class2type[Object.prototype.toString.call(obj)] || "object"
: typeof obj;
};
})();

// 判断是否是个函数
_.isFunction = function(obj) {
return _.type(obj) === "function";
};

// 判断是否是个数组或类数组
_.isArrayLike = function(obj) {
// obj 必须有length属性
var length = !!obj && "length" in obj && obj.length;
var typeRes = _.type(obj);

//排除掉函数和window对象
if (typeRes === "function" || _.isWindow(obj)) {
return false;
}

return (
typeRes === "array" ||
length === 0 ||
(typeof length === "number" && length > 0 && length - 1 in obj)
);
};

// 判断是否是一个window对象
_.isWindow = function(obj) {
return obj != null && obj === obj.window;
};

// 遍历对象或数组
_.each = function(obj, callback) {
var length,
i = 0;

if (_.isArrayLike(obj)) {
length = obj.length;
for (; i < length; i++) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
}

return obj;
};

// 将 _ 对象上的 函数都 挂载到 _.prototype 上
_.mixin = (function() {
var ArrayProto = Array.prototype;
var push = ArrayProto.push;
return function(obj) {
_.each(_.functions(obj), function(index, name) {
var func = (_[name] = obj[name]);
// 把 _ 对象上的函数挂载到 _.prototype上
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);

return func.apply(_, args);
};
});

return _;
};
})();
})();

至此,我们算是实现了同时支持面向对象和函数风格

导出

终于到了最后一步 root._ = _,这里直接看源码:

1
2
3
4
5
6
7
8
if (typeof exports != "undefined" && !exports.nodeType) {
if (typeof module != "undefined" && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}

为了支持模块化,我们需要将 _ 在合适的环境中作为模块导出,但是 node.js 模块的 API 曾今发生过改变,比如在早期版本中:

1
2
3
4
5
6
7
8
// add.js
exports.addOne = function(num) {
return num + 1;
};

// index.js
var add = require("./add");
add.addOne(2);

在新版本中:

1
2
3
4
5
6
7
//add.js
module.exports = function(num) {
return num + 1;
};
// index.js
var addOne = require("./add.js");
addOne(2);

所以我们根据 exports 和 moudle 是否存在来选择不同的导出方式,那么为什么在新版本中,我们还要使用 exports = module.exports = _ 呢?

这是因为在 nodeJs 中,exports 是 module.exports 的一个引用,当你使用了 module.exports = function(){},实际上覆盖了module.exports,但是 exports 并未发生改变,为了避免后面在修改 exports 而导致不能正确输出,就写成这样,将两者保持统一。

下面是个 demo

1
2
3
4
5
6
7
// exports 是 module.exports的一个引用
module.exports.num = "1";
console.log(exports.num); // 1

exports.num = "2";

console.log(module.exports.num); //2
1
2
3
4
5
6
7
8
9
10
11
12
// addOne.js
module.exports = function(num) {
return num + 1;
};

exports.num = "1";

// result.js 中引入 addOne.js
var addOne = require("./addOne.js");

console.log(addOne(1)); //2
console.log(addOne.num); // undefined
1
2
3
4
5
6
7
8
9
10
11
12
// addOne.js
exports = module.exports = function(num) {
return num + 1;
};

exports.num = "3";

// result.js 中引入 addOne.js
var addOne = require("./addOne.js");

console.log(addOne(1)); //2
console.log(addOne.num); // 3

最后为什么要进行一个 exports.nodeType 判断呢? 这是因为如果你在 HTML 页面中加入一个 id 为 exports 的元素,比如:

1
<div id="exports" />

就会生成一个 window.exports 全局变量,你可以直接在浏览器命令中打印该变量,这个变量对应的就是这个 dom 元素

此时在浏览器中, typeof exports != 'undefined'的判断就会生效,然后exports._ = _,然后在浏览器中,我们需要将 _ 挂载到全局变量上,所以在这里,我们还需要进行一个是否是 DOM 节点的判断。

源码

最终的代码如下,有了这个基本结构,你就可以自由添加需要用到的函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
(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;
}

var push = ArrayProto.push;

var ArrayProto = Array.prototype;

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

}else {
root._ = _;
}

_VERSION = '0.1'l

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

var isArrayLike = function(collection) {
var length = collection.length;
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

_.each = function(obj, callback) {
var length , i = 0;

if(isArrayLike(obj)) {
length = obj.length;
for(; i < length; i ++) {
if(callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
} else {
for(i in obj) {
if(callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
}
return obj;
}

_.isFunction = function(obj) {
return typeof obj == 'function' || false;
}

_.function = function(obj) {
var name = [];
for(var key in obj) {
if(_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

/**
* 在 _.mixin(_) 前添加自己定义的方法
*/

_.reverse = function(string) {
return string.split('').reverse().join('');
}


_.mixin = function(obj) {
_.each(_.function(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(_);
})()