underscore 系列之链式调用
mqyqingfeng opened this issue · 24 comments
前言
本文接着上篇《underscore 系列之如何写自己的 underscore》,阅读本篇前,希望你已经阅读了上一篇。
jQuery
我们都知道 jQuery 可以链式调用,比如:
$("div").eq(0).css("width", "200px").show();
我们写个简单的 demo 模拟链式调用:
function JQuery(selector) {
this.elements = [];
var nodeLists = document.getElementsByTagName(selector);
for (var i = 0; i < nodeLists.length; i++) {
this.elements.push(nodeLists[i]);
}
return this;
}
JQuery.prototype = {
eq: function(num){
this.elements = [this.elements[num]];
return this;
},
css: function(prop, val) {
this.elements.forEach(function(el){
el.style[prop] = val;
})
return this;
},
show: function() {
this.css('display', 'block');
return this;
}
}
window.$ = function(selector){
return new JQuery(selector)
}
$("div").eq(0).css("width", "200px").show();
jQuery 之所以能实现链式调用,关键就在于通过 return this
,返回调用对象。再精简下 demo 就是:
var jQuery = {
eq: function(){
console.log('调用 eq 方法');
return this;
},
show: function(){
console.log('调用 show 方法');
return this;
}
}
jQuery.eq().show();
_.chain
在 underscore 中,默认不使用链式调用,但是如果你想使用链式调用,你可以通过 _.chain 函数实现:
_.chain([1, 2, 3, 4])
.filter(function(num) { return num % 2 == 0; })
.map(function(num) { return num * num })
.value(); // [4, 16]
我们看看 _.chain 这个方法都做了什么:
_.chain = function (obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
我们以 [1, 2, 3] 为例,_.chain([1, 2, 3]) 会返回一个对象:
{
_chain: true,
_wrapped: [1, 2, 3]
}
该对象的原型上有着 underscore 的各种方法,我们可以直接调用这些方法。
但是问题在于原型上的这些方法并没有像 jQuery 一样,返回 this ,所以如果你调用了一次方法,就无法接着调用其他方法了……
但是试想下,我们将函数的返回值作为参数再传入 _.chain
函数中,不就可以接着调用其他方法了?
写一个精简的 Demo:
var _ = function(obj) {
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
_.chain = function (obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
_.prototype.push = function(num) {
this._wrapped.push(num);
return this._wrapped
}
_.prototype.shift = function(num) {
this._wrapped.shift()
return this._wrapped
}
var res = _.chain([1, 2, 3]).push(4);
// 将上一个函数的返回值,传入 _.chain,然后再继续调用其他函数
var res2 = _.chain(res).shift();
console.log(res2); // [2, 3, 4]
然而这也太复杂了吧,难道 chain 这个过程不能是自动化的吗?如果我是开发者,我肯定希望直接写成:
_.chain([1, 2, 3]).push(4).shift()
所以我们再优化一下实现方式:
var _ = function(obj) {
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
var chainResult = function (instance, obj) {
return instance._chain ? _.chain(obj) : obj;
};
_.chain = function (obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
_.prototype.push = function(num) {
this._wrapped.push(num);
return chainResult(this, this._wrapped)
}
_.prototype.shift = function() {
this._wrapped.shift();
return chainResult(this, this._wrapped)
}
var res = _.chain([1, 2, 3]).push(4).shift();
console.log(res._wrapped);
我们在每个函数中,都用 chainResult 将函数的返回值包裹一遍,再生成一个类似以下这种形式的对象:
{
_wrapped: some value,
_chain: true
}
该对象的原型上有各种函数,而这些函数的返回值作为参数传入了 chainResult,该函数又会返回这样一个对象,函数的返回值就保存在 _wrapped 中,这样就实现了链式调用。
_.chain
链式调用原理就是这样,可是这样的话,我们需要对每个函数都进行修改呀……
幸运的是,在 underscore 中,所有的函数是挂载到 _
函数对象中的,_
.prototype 上的函数是通过 _.mixin
函数将 _
函数对象中的所有函数复制到 _.prototype
中的。
所以为了实现链式调用,我们还需要对上一篇《underscore 系列之如何写自己的 underscore》 中的 _.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(_);
// 修改后
var ArrayProto = Array.prototype;
var push = ArrayProto.push;
var chainResult = function (instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};
_.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 _;
};
_.mixin(_);
_.value
根据上面的分析过程,我们知道如果我们打印:
console.log(_.chain([1, 2, 3]).push(4).shift());
其实会打印一个对象 {_chain: true, _wrapped: [2, 3, 4] }
可是我希望获得值是 [2, 3, 4] 呀!
所以,我们还需要提供一个 value 方法,当执行 value 方法的时候,就返回当前 _wrapped 的值。
_.prototype.value = function() {
return this._wrapped;
};
此时调用方式为:
var arr = _.chain([1, 2, 3]).push(4).shift().value();
console.log(arr) // [2, 3, 4]
最终代码
结合上一篇文章,最终的 underscore 代码组织结构如下:
(function() {
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this || {};
var ArrayProto = Array.prototype;
var push = ArrayProto.push;
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
_.VERSION = '0.2';
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], obj[i], i) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], obj[i], i) === false) {
break;
}
}
}
return obj;
}
_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};
_.functions = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
/**
* 在 _.mixin(_) 前添加自己定义的方法
*/
_.reverse = function(string){
return string.split('').reverse().join('');
}
_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};
_.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 _;
};
_.mixin(_);
_.prototype.value = function () {
return this._wrapped;
};
})()
underscore 系列
underscore 系列目录地址:https://github.com/mqyqingfeng/Blog。
underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助大家阅读源码,以及写出自己的 undercore。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
您好,JQuery
的eq
方法里面是不是应该为
this.elements = [this.elements[num]];
@xxxgitone 非常感谢指出~ o( ̄▽ ̄)d
本条评论用作修改日志。
- 2017 年 11 月 27 日
修改 this.elements = [this.elements[0]];
为 this.elements = [this.elements[num]];
感谢 @xxxgitone
楼主能分享一下你是如何阅读源码的吗?
@Soyn 刚开始是因为写 JavaScript 专题系列会涉及到去重、查找数组元素、数组扁平化等等功能,所以研究了 underscore 中这些功能的实现方式,然后写 underscore 系列才开始正式读源码。
因为 underscore 它是一个功能函数库,所以首先要搞明白的就是那么多的函数,是如何组织的?这其实就是这个系列第一篇和第二篇的内容。
接下来是阅读内部函数如 cb、optimizeCb、restArgs、shallowProperty、deepGet,因为这些函数作为内部函数会被多次用到。
最后再是跟着兴趣,你想了解哪些函数的实现就去研究哪些函数的实现方式。
在具体研究一些函数的实现方式时,可以参考一些已经写过的源码分析的文章是如何解读的,可以事半功倍:
我读underscore代码的时候,第一遍是直接看源码,很多地方看的云里雾里,后面看第二遍的时候,看underscore的单元测试加源码,但是读完之后很快就忘了,和楼主相比实在是惭愧。
@Soyn 不敢当啦,因为要写文章,很多有疑问的地方都要想明白,要不然,被人问了,答错了就尴尬了……
跟着大神再看一遍underscore,昨天看到你在知乎回答的如何学习前端,我打算按照推荐的步骤开始学
@FrontToEnd 哈哈,谢谢提醒,补一下我的回答 怎样系统地自学前端?,其实这几个系列就是按照这样的思路去写的~
为什么不直接return this?
instance._chain ? _(obj).chain() : obj; 什么情况下才会返回obj??instance._chain 是肯定有值的呀
感谢感谢,十分感谢!!
@wulinsheng123 应该是当你不需要 链式调用 的时候吧。
_(obj).chain() 这里不是又创建了一次实例吗? 那么两个实例将不共享同一个属性, 按照道理应该是
instance._chain ? instance : obj
@wudao370859172 应该是因为像push,shift这些函数本身都有返回的值,return this会改变这些方法调用时的返回值的~
@Gloomysunday28 但是函数的返回值不是instance,而是obj呀。考虑代码
_.mixin({ addOne: num => num + 1}) ; _.chain(4).addOne().value()
按照你的写法会返回4而不是5
@mqyqingfeng 博主,你二版的代码
var arr = _.chain([1, 2, 3]).push(4).shift().value();
console.log(arr) // [2, 3, 4]
没有办法用这个代码进行测试
instance._chain ? _(obj).chain() : obj; 什么情况下才会返回obj??instance._chain 是肯定有值的呀
没有_chain()过的话.instance._chain是undefined
_(obj).chain() 这里不是又创建了一次实例吗? 那么两个实例将不共享同一个属性, 按照道理应该是
instance._chain ? instance : obj
你这个写法收集不了 函数返回值
问题1:我只是在最终版的闭包里加了一个push方法和console.log(.chain([1]).push(2));
就报错了。
222copy.html:97 Uncaught TypeError: Cannot read property 'push' of undefined;
哪位大神能帮忙定位一下啊。
https://codepen.io/rang1994/pen/gOLBPQR
我感觉是push方法中的this 指向的是,而_没有_wrapped属性。
问题2:还有chainResult方法中的_.chain(obj)为什么要改成 (obj).chain()。
问题3:return chainResult(this, func.apply(, args));
apply后为什么是_而不是this。
求大神解答
看了一晚上,debug了源码对自己提的三个问题有了一些了解。
1、自己写的push方法有问题,应该在参数中取array和arguments。跟_没有任何关系
2、chain方法是通过mixin方法复制到原型上的,所以var args = [this.wrapped]; func.apply(, args) 即_.chain(obj)
貌似这两个方法是等价的 _.chain(obj) 与 _(obj).chain()
3、以前看不懂,其实就是复制了构造函数上的方法到原型上,当this肯定还是指向原来的构造函数的
我感觉这个版本的链式实现有点冗余了,不停的创建了新的实例,感觉没必要,事实上push直接返回this就好了,顺便写下我的code
function _(obj) {
// eslint-disable-next-line new-cap
if (!(this instanceof _)) return new _(obj)
this._wraps = obj
}
// 遍历所有的函数 返回函数名称数组
_.functions = function (obj) {
const funcs = []
for (const key in obj) {
if (typeof obj[key] === 'function') funcs.push(key)
}
return funcs
}
_.chain = function (obj) {
const instance = _(obj)
instance._chain = true
return instance
}
_.prototype.push = function (num) {
this._wraps.push(num)
return this
}
_.prototype.shift = function () {
this._wraps.shift()
return this
}
_.prototype.value = function () {
return this._wraps
}
// 将underscore的函数放入到新的实例中去
_.mixin = function (obj) {
_.functions(obj).forEach((name) => {
const func = (_[name] = obj[name])
_.prototype[name] = function (arg) {
// 这里 context是underscore 是因为this需要访问underscore的成员变量和函数
// 如果是this的话, allarg 的参数会有问题
const allArg = [this._wraps].concat([arg])
return func.apply(_, allArg)
}
})
return _
}
_.mixin(_)
// underscore.version('yakir')
// underscore('laige').version('add').version()
const res = _.chain([1, 2, 3]).push(4).shift().push(9).value()
console.log(res)
@jxccc1998 不清楚你要问的是什么,新增的函数基本都是直接挂载 _
函数对象上的
function JQuery(selector) {
this.elements = [];
var nodeLists = document.getElementsByTagName(selector);
for (var i = 0; i < nodeLists.length; i++) {
this.elements.push(nodeLists[i]);
}
return this;
}
JQuery.prototype = {
eq: function(num){
this.elements = [this.elements[num]];
return this;
},
css: function(prop, val) {
this.elements.forEach(function(el){
el.style[prop] = val;
})
return this;
},
show: function() {
this.css('display', 'block');
return this;
}
}
@mqyqingfeng 很奇怪为啥这段代码原型上没有constructor为啥也能正常实例化