1. Underscore 结构
作用域包裹
与其他第三方库一样,Underscore
也通过 立即执行函数 来包裹自己的业务逻辑。
目的:
- 避免全局污染:所有库的逻辑,库所定义和使用的变量全部被封装到了该函数的作用域中。
- 隐私保护:但凡在立即执行函数中声明的函数、变量等,除非是自己想暴露,否则绝无可能在外部获得。
// function 外面加括号,使之成为表达式
(function() {
// ... 执行逻辑
})()
2. 面向对象风格支持
_对象
underscore
有下划线的意思,所以 underscore
通过一个下划线变量 _ 来标识自身。
注意:
_是一个函数对象,之后所有的 api 都会被挂载到这个对象上,如 _.each
,_.map
等
var _ = function (obj) {
if (obj instance _) {
return obj;
}
if (!(this instanceof _)) {
return new _(obj);
}
this._wrapped = obj;
}
_()
虽然 Underscore
推崇函数式编程,但也支持面向对象风格的函数调用,仅需要通过_()
来包裹对象即可。
当我们进行如下调用时:
_([2, 3, 4])
会创建一个新的 underscore
对象(从而能够调用 underscore
提供的方法),并在 this._wrapped
中存储传入的数据。
3. mixin
// 源码
_.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 chain(this, func.apply(_, args));
};
});
return _;
};
- mixin(混入)模式是增加代码复杂度的一种广泛使用的设计模式。
- _.mixin(obj):为 underscore 对象混入 obj 具有的功能。
4. 链接式调用
- jQuery 链接式调用
$('.div').css('color', 'red').show();
- 想要实现链式调用,通常我们会在支持链式调用的函数中返回本身。
var RULES = function() {
add: function(x) { console.log(x); return this; },
mult: function(y) { console.log(y); return this; }
};
RULES.add(4).mult(5);
- 但是,这样做并不优雅,这需要我们手动地在函数中添加 return this 语句。更好的做法是我们需要创建一个通用函数,它能为我们指定的对象添加链式调用机制。
_.chain()
- _.chain(obj): 为 underscore 对象的方法添加链式调用能力。
- _.chain 源码如下:
_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
- underscore 还提供了一个帮助函数 result,该函数将会判断方法调用结果,如果该方法的调用者被标识了需要链化,则链化当前的方法执行结果。
var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};
下面开始我们的源码实现:
主要有两个文件 test.js
和 index.html
。
- 首先实现一个基本的框架和
unique
功能
test.js
(function(root) {
const _ = function() {};
_.unique = function(obj, callback) {
const res = [];
for (let i = 0; i < obj.length; i++) {
const val = callback? callback(obj[i]) : obj[i];
if (res.indexOf(val) === -1) {
res.push(val);
}
}
return res;
};
root._ = _; // 下面的 this 指向 window,所以这里是将 _ 变量挂到 window 对象下。
})(this);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>underscore</title>
</head>
<body>
<script src="./test.js"></script>
<script>
console.log(_.unique([1,2,3,4,1,2,'a','A'],(val) => {
return typeof val === 'string'? val.toLowerCase() : val;
}));
</script>
</body>
</html>
打开 console 发现输出为 [1, 2, 3, 4, "a"]
,unique 功能成功实现。
问题:如果我们想以面向风格的形式调用,该怎么实现呐?
_([1,2,3,4,1,2,'a','A']).unique((val) => {
return typeof val === 'string'? val.toLowerCase() : val;
}))
因此,我们需要在变量 _
构造函数的原型上扩展相应的方法。为了避免方法的重写,我们使用 _.mixin
方法帮我们实现静态方法的原型方法实现。
- 使用
_.mixin
方法实现静态方法的原型方法实现
test.js
(function(root) {
const toString = Object.prototype.toString;
const push = Array.prototype.push;
const _ = function(obj) {
// 如果 obj 是 _ 实例对象,则直接返回
if (obj instanceof _) {
return obj;
}
// 如果 instanceof 不是 _ 实例对象(this 一开始指向 window),则调用 new _(obj);当再次调用的时候(此时 this 指向 _ 实例对象),则下面方法不执行,
if (!(this instanceof _)) {
return new _(obj);
}
// 将值存储到实例对象的 _wrapped 属性当中。
this._wrapped = obj;
};
_.unique = function(obj, callback) {
const res = [];
for (let i = 0; i < obj.length; i++) {
const val = callback? callback(obj[i]) : obj[i];
if (res.indexOf(val) === -1) {
res.push(val);
}
}
return res;
};
// 判断对象是否为数组对象
_.isArray = function(obj) {
return toString.call(obj) === "[object Array]";
};
// 返回对象的属性列表
_.functions = function(obj) {
const res = [];
for (let key in obj) {
res.push(key);
}
return res;
};
// 循环遍历值并执行回调
_.each = function(obj, callback) {
if (_.isArray(obj)) {
for (let i = 0;i < obj.length; i++) {
callback.call(obj, obj[i], i);
}
} else {
for (let key in obj) {
callback.call(obj, key, obj[key]);
}
}
};
_.mixin = function(obj) {
_.each(_.functions(obj), (name) => {
const func = obj[name]; // 获取到对应的静态方法(例如:_.unique 方法)
// 扩展静态方法到对象原型上
_.prototype[name] = function() {
// 下面的内容是为了处理参数,具体原因可以看看 index.html 里面的调用方式。被处理的值在初始化时传入,其他参数在调用原型方法时传入。
let args = [this._wrapped];
push.apply(args, arguments);
return func.apply(this, args);
};
});
};
_.mixin(_);
root._ = _;
})(this);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>underscore</title>
</head>
<body>
<script src="./test.js"></script>
<script>
console.log(_([1,2,3,4,1,2,'a','A']).unique((val) => {
return typeof val === 'string'? val.toLowerCase() : val;
}));
</script>
</body>
</html>
打开 console 发现输出为 [1, 2, 3, 4, "a"]
,功能成功实现。
问题:如上所示,结果直接输出的是数组值;如果我们想实现方法的链式调用呐?即,在 unique 方法后再调用 map 方法呐?
因为输出结果是个数组,不可能再调用实例对应的方法,所以我们需要在调用完 unique 方法后,返回 _ 的实例对象,并且把返回的值传给 _ 来进行初始化(_([1,2,3,4,"a"]))。
_([1,2,3,4,1,2,'a','A']).unique((val) => {
return typeof val === 'string'? val.toLowerCase() : val;
})).map()
- 使用 _.chain() 方法实现链式调用
test.js
(function(root) {
const toString = Object.prototype.toString;
const push = Array.prototype.push;
const _ = function(obj) {
if (obj instanceof _) {
return obj;
}
if (!(this instanceof _)) {
return new _(obj);
}
this._wrapped = obj;
};
_.unique = function(obj, callback) {
const res = [];
for (let i = 0; i < obj.length; i++) {
const val = callback? callback(obj[i]) : obj[i];
if (res.indexOf(val) === -1) {
res.push(val);
}
}
return res;
};
_.map = function(args) {
return args;
};
_.isArray = function(obj) {
return toString.call(obj) === "[object Array]";
};
_.functions = function(obj) {
const res = [];
for (let key in obj) {
res.push(key);
}
return res;
};
_.each = function(obj, callback) {
if (_.isArray(obj)) {
for (let i = 0;i < obj.length; i++) {
callback.call(obj, obj[i], i);
}
} else {
for (let key in obj) {
callback.call(obj, key, obj[key]);
}
}
};
// 生成实例对象,并设置实例属性 _.chain 为 true
_.chain = function(obj) {
const instance = _(obj);
instance._chain = true;
return instance;
};
// 如果实例拥有 _chain 属性,则返回 _(obj).chain(),实现链式调用;否则直接返回值结束。
const result = function(instance, obj) {
// 下面的 chain 为静态方法
return instance._chain? _(obj).chain() : obj;
};
_.mixin = function(obj) {
_.each(_.functions(obj), (name) => {
const func = obj[name];
_.prototype[name] = function() {
let args = [this._wrapped];
push.apply(args, arguments);
return result(this, func.apply(this, args));
};
});
};
_.mixin(_);
root._ = _;
})(this);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>underscore</title>
</head>
<body>
<script src="./test.js"></script>
<script>
console.log(_([1,2,3,4,1,2,'a','A']).chain().unique((val) => {
return typeof val === 'string'? val.toLowerCase() : val;
}).map());
</script>
</body>
</html>
输出结果如下:
成功实现链式调用,让我们的数据流像在管道一样传输。
问题:我们的结果最终是返回一个对象,如果我们想结束链式调用返回值呐,应该怎么实现?
- 添加
_.prototype.value
方法返回值
test.js
(function(root) {
const toString = Object.prototype.toString;
const push = Array.prototype.push;
const _ = function(obj) {
if (obj instanceof _) {
return obj;
}
if (!(this instanceof _)) {
return new _(obj);
}
this._wrapped = obj;
};
_.unique = function(obj, callback) {
const res = [];
for (let i = 0; i < obj.length; i++) {
const val = callback? callback(obj[i]) : obj[i];
if (res.indexOf(val) === -1) {
res.push(val);
}
}
return res;
};
_.map = function(args) {
return args;
};
_.isArray = function(obj) {
return toString.call(obj) === "[object Array]";
};
_.functions = function(obj) {
const res = [];
for (let key in obj) {
res.push(key);
}
return res;
};
_.each = function(obj, callback) {
if (_.isArray(obj)) {
for (let i = 0;i < obj.length; i++) {
callback.call(obj, obj[i], i);
}
} else {
for (let key in obj) {
callback.call(obj, key, obj[key]);
}
}
};
_.chain = function(obj) {
const instance = _(obj);
instance._chain = true;
return instance;
};
const result = function(instance, obj) {
return instance._chain? _(obj).chain() : obj;
};
_.prototype.value = function() {
return this._wrapped;
};
_.mixin = function(obj) {
_.each(_.functions(obj), (name) => {
const func = obj[name];
_.prototype[name] = function() {
let args = [this._wrapped];
push.apply(args, arguments);
return result(this, func.apply(this, args));
};
});
};
_.mixin(_);
root._ = _;
})(this);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>underscore</title>
</head>
<body>
<script src="./test.js"></script>
<script>
console.log(_([1,2,3,4,1,2,'a','A']).chain().unique((val) => {
return typeof val === 'string'? val.toLowerCase() : val;
}).map().value());
</script>
</body>
</html>
打开 console 发现输出为 [1, 2, 3, 4, "a"]
,功能成功实现。
网友评论