玩一把JS的链式调用

时间:2023-02-19 19:10:49

  链式调用我们平常用到很多,比如jQuery中的$(ele).show().find(child).hide(),再比如angularjs中的$http.get(url).success(fn_s).error(fn_e)。但这都是已经包装好的链式调用,我们只能体会链式调用带来的方便,却不知道形成这样一条函数链的原理是什么。

  随着链式调用的普及,实现的方案也越来越多。最常见的,是jQuery直接返回this的方式,underscore的可选式的方式,和lodash惰性求值的方式。我们分别来了解,并逐个完成它们的demo。

  我们从最简单的开始,直接返回this是最常见的方式,也是所有方式的基础。我们实现一个简单的链式运算类,首先它得有个字段保留结果。

function A(num) {
this.value = num || 0; //不做传参校验了
}

  然后添加进行运算并返回this的方法。

A.prototype.add = function(a) {this.value += a; return this;}
A.prototype.reduce = function(a) {this.value -= a; return this;}

  最后为了显示正常修改两个继承的方法。

A.prototype.valueOf = function() {return this.value;}
A.prototype.toString = function() {return this.value + '';}

  进行验证。

var a = new A(2);
alert(a.add(1).reduce(2))

  这个demo应该简单到不用对任何代码进行说明,我们快速来到第二个,就是underscore中用到chain。underscore规定了两种调用方式,_.forEach(arr, fn);_.map(arr, fn);和_.chain(arr).forEach(fn).map(fn)。

  我们先实现前面一种调用方式,因为这里不是讲解underscore,所以我们只是简单实现forEach和map的功能,不对对象而仅对数组进行处理。

var _ = {};
_.forEach = function(array, fn) {
array.forEach(function(v, i, array) {
fn.apply(v, [v, i, array]);
})
};
_.map = function(array, fn) {
return array.map(function(v, i, array) {
return fn.apply(v, [v, i, array]);
})
};

  上面的代码很简单,直接调用ES5中数组原型的方法。接下来问题就来了,要实现链式调用,我们首先要做什么?我们看到第二种调用方式中,所有的操作无论是forEach还是map都是在_.chain(arr)上调用的,所以_.chain(arr)应该是返回了一个对象,这个对象上有和_上相同的方法,只是实现上传参由2个变成了1个,因为原来的第一个参数永远是_.chain中传入的参数的拷贝。

  好了,确定_.chain(arr)要返回一个对象了,那这个对象的构造函数怎么写呢?我们借用一个现成的变量来保存这个构造函数,就是_。函数也是对象,所以当_由对象变成函数,不会影响原来的逻辑,而这个函数要传入一个array,并返回一个新的对象。所以上面的代码应该改成这样。

var _ = function(array) {
this._value = Array.prototype.slice.apply(array);
}
_.forEach = function(array, fn) {
array.forEach(function(v, i, array) {
fn.apply(v, [v, i, array]);
})
};
_.map = function(array, fn) {
return array.map(function(v, i, array) {
return fn.apply(v, [v, i, array]);
})
};
_.chain = function(array) {
return new _(array);
}

  新的构造函数有了,但它生成的对象除了_value就是一片空白,我们要怎么把原本_上的方法稍加修改的移植到_生成的对象上呢?代码如下:

for(var i in _) { //首先我们要遍历_
if(i !== 'chain') { //然后要去除chain
_.prototype[i] = (function(i) { //把其他的方法都经过处理赋给_.prototype
return function() { //i是全局变量,我们要通过闭包转化为局部变量
var args = Array.prototype.slice.apply(arguments); //取出新方法的参数,其实就fn一个
args.unshift(this._value); //把_value放入参数数组的第一位
if(i === 'map') { //当方法是map的时候,需要修改_value的值
this._value = _[i].apply(this, args);
}else { //当方法是forEach的时候,不需要修改_value的值
_[i].apply(this, args);
}
return this;
}
})(i);
}
}

  最后我们模仿underscore使用value返回当前的_value。

_.prototype.value = function() {
return this._value;
}

  进行验证。

var a = [1, 2, 3];
_.forEach(a, function(v){console.log(v);})
alert(_.map(a, function(v){return ++v;}))
alert(_.chain(a).map(function(v){return ++v;}).forEach(function(v){console.log(v);}).value())

  以上是underscore中用到的链式调用的简化版,应该不难理解。那最复杂的来了,lodash惰性调用又是怎样的呢?首先我来解释下什么是惰性调用,比如上面的_.chain(arr).forEach(fn).map(fn).value(),当执行到chain(arr)的时候,返回了一个对象,执行到forEach的时候开始轮询,轮询完再返回这个对象,执行到map的时候再次开始轮询,轮询完又返回这个对象,最后执行到value,返回对象中_value的值。其中每一步都是独立的,依次进行的。而惰性调用就是,执行到forEach的时候不执行轮询的操作,而是把这个操作塞进队列,执行到map的时候,再把map的操作塞进队列。那什么时候执行呢?当某个特定的操作塞进队列的时候开始执行之前队列中所有的操作,比如当value被调用时,开始执行forEach、map和value。

  惰性调用有什么好处呢,为什么把一堆操作塞在一起反倒是更优秀的方案的?我们看传统的链式操作都是这样的格式,obj.job1().job2().job3(),没错整个函数链都是job链,如果这时候有一个简单的需求,比如连续执行100遍job1-3,那么我们就要写100遍,或者用for把整个链条断开100次。所以传统链式操作的缺点很明显,函数链中都是job,不存在controller。而一旦加上controller,比如上面的需求我们用简单的惰性调用来实现,那就是obj.loop(100).job1().job2().job3().end().done()。其中loop是声明开启100次循环,end是结束当前这次循环,done是开始执行任务的标志,代码多么简单!

  现在我们实现一下惰性链式调用,由于lodash就是underscore的威力加强版,大体架构都差不多,而上面已经有underscore的基本链式实现,所以我们脱离lodash和underscore的其他代码,仅仅实现一个类似的惰性调用的demo。

  首先我们要有一个构造函数,生成可供链式调用的对象。之前提到的,任何controller或者job的调用都是把它塞入任务队列,那么这个构造函数自然要有一个队列属性。有了队列,肯定要有索引指明当前执行的任务,所以要有队列索引。那么这个构造函数暂时就这样了

function Task() {
this.queen = [];
  this.queenIndex = 0;
}

  如果我们要实现loop,那么还要有个loop的总次数和当前loop的次数,而如果一次loop结束,我们要回到任务队列哪里呢?所以还要有个属性记录loop开始的地方。构造函数最终的形态如此:

function Task() {
this.queen = [];
this.queenIndex = 0;
this.loopCount = 0;
this.loopIndex = 0;
this.loopStart = 0;
}

  现在我们开始实现controller和job,比如上面这个例子中说到的:job()、loop()、end()、done()。它们应该都包含两种形态,一种是本来的业务逻辑,比如job的业务就是do something,而loop的控制逻辑就是记录loopCount和loopStart,end的控制逻辑就是loopIndex+1和检查loopIndex看是否需要回到loopStart的位置再次遍历。而另一种形态是不管业务逻辑是什么,把业务逻辑对应的代码统一塞进任务队列,这种形态可以称之为第一种形态的包装器。

  如果我们最终的调用格式是new Task().loop(100).job().end().done(),那么方法链上的方法肯定是包装器,这些方法自然应该放在Task.prototype上,那第一种形态的方法何去何从呢?那就放在Task.prototype.__proto__上吧。我们这样写

var _task_proto = {
loop: function(num) {
this.loopStart = this.queenIndex;
this.loopCount = num;
},
job: function(str) {
console.log(str);
},
end: function() {
this.loopIndex++;
if(this.loopIndex < this.loopCount) {
this.queenIndex = this.loopStart;
}else {
this.loopIndex = 0;
}
},
done: function() {
console.log('done');
}
};
Task.prototype.__proto__ = _task_proto;

  然后在遍历_task_proto在Task.prototype上生成包装器,并让每个包装器返回this以供链式调用(看见没,其实每一种链式调用的方式都要这么做)

  for(var i in _task_proto) {
(function(i) {
var raw = Task.prototype[i];
Task.prototype[i] = function() {
this.queen.push({
name: i,
fn: raw,
args: arguments
}); //保存具体的实现方法、名字和参数到任务队列
    return this;
};
})(i);
}

  现在问题来了,我们什么时候开始执行具体的任务,又怎样让任务有条不紊的执行和跳转呢?这时候我们要在Task.prototype上定义一个新的方法,这个方法专门用来控制任务的执行的,因为任务队列是依次执行并由索引定位的,跟迭代器有那么一点相像,我们定义这个新的方法叫next

Task.prototype.next = function() {
var task = this.queen[this.queenIndex]; //取出新的任务
task.fn.apply(this, task.args); //执行任务中指向的具体的实现方法,并传入之前保存的参数
if(task.name !== 'done') {
this.queenIndex++;
this.next(); //如果没执行完,任务索引+1并再次调用next
}else {
this.queen = [];
this.queenIndex = 0; //如果执行完了,清空任务队列,重置任务索引
}
}

  添加了next,我们需要在done的包装器上加点东西以便让任务队列开始执行,修改之前生成包装器的代码

  for(var i in _task_proto) {
(function(i) {
var raw = Task.prototype[i];
Task.prototype[i] = function() {
this.queen.push({
name: i,
fn: raw,
args: arguments
}); //保存具体的实现方法、名字和参数到任务队列
if(i === 'done') {
this.next();
}
return this;
};
})(i);
}

  最后我们进行验证。

var t = new Task();
console.log('1')
t.job('fuck').loop(3).job('world').end().loop(3).job('world').end().job('!').done();
console.log('2')
t.job('fuck').loop(3).job('world').job('!').end().done();
console.log('3')
t.job('fuck').loop(3).job('world').job('!').end().job('!');

  好了,链式调用玩到这里了。这几个demo尤其是惰性调用稍加改造后,功能可以大大加强,但是这里就不再讨论了。

玩一把JS的链式调用的更多相关文章

  1. JS实现链式调用 a&lpar;&rpar;&period;b&lpar;&rpar;&period;c&lpar;&rpar;

    function a() { this.b = function () { console.log('111') return this } this.c = function () { consol ...

  2. 如何写 JS 的链式调用 ---》JS 设计模式《----方法的链式调用

    1.以$ 函数为例.通常返回一个HTML元素或一个元素集合. 代码如下: function $(){ var elements = []; ;i<arguments.length;i++){ v ...

  3. js实现方法的链式调用

    假如这里有三个方法:person.unmerried();person.process();person.married();在jQuery中通常的写法是:person.unmerried().pro ...

  4. js简单实现链式调用

    链式调用实现原理:对象中的方法执行后返回对象自身即可以实现链式操作.说白了就是每一次调用方法返回的是同一个对象才可以链式调用. js简单实现链式调用demo Object.prototype.show ...

  5. js链式调用

    我们都很熟悉jQuery了,只能jQuery中一种非常牛逼的写法叫链式操作 * $('#div').css('background','#ccc').removeClass('box').stop() ...

  6. js原生设计模式——2面向对象编程之js原生的链式调用

    技巧点:对象方法中返回当前对象就可以链式调用了,即方法中写return this; <!DOCTYPE html><html lang="en"><h ...

  7. 《javascript设计模式》笔记之第六章:方法的链式调用

    这一章要实现的就是jQuery的那种链式调用,例子: $(this).setStyle('color', 'green').show(); 一:调用链的结构: 首先我们来看一下最简单的$()函数的实现 ...

  8. js实现链式操作

    前言:前不久阿里远程面试时问了我一个问题,如下: function Person(){}; var person = new Person(); //实现person.set(10).get()返回2 ...

  9. 【Java】子类的链式调用

    记录最近在项目设计中遇到的一个小问题. 前提:有这样两个POJO类,它们都可以通过链式调用的方式来设置其属性值,其中一个类继承了另一个类. 问题:通过链式调用,子类对象访问父类方法后,如何使返回对象仍 ...

随机推荐

  1. Android四大组件之—— BroadcastReceiver的使用

    BroadcastReceiver又名广播接收者.既然它用于接收广播,那一定就有人负责发送. Android系统中的广播: 在现实生活中,我们都知道广播是什么,用来做什么.例如公园里的广播,主要通知游 ...

  2. R语言实现 广义加性模型 Generalized Additive Models&lpar;GAM&rpar; 入门

    转载请说明. R语言官网:http://www.r-project.org/ R语言软件下载:http://ftp.ctex.org/mirrors/CRAN/         注:下载时点击 ins ...

  3. Java程序调用javascript等脚本的实现方法

    public static void main(String[] args) throws FileNotFoundException, ScriptException, NoSuchMethodEx ...

  4. Android 开发必备知识:我和 Gradle 有个约会

    腾讯Bugly特约作者:霍丙乾 0.讲个故事 0.1 Ant,我还真以为你是只蚂蚁 真正开始近距离接触编程其实是在2012年,年底的时候带我的大哥说,咱们这个 app 发布的时候手动构建耗时太久,研究 ...

  5. How to install Node&period;js on Linux

    How to install Node.js on Linux Posted on November 13, 2015 by Dan Nanni Leave a comment Question: H ...

  6. Bias&sol;variance tradeoff

    线性回归中有欠拟合与过拟合,例如下图: 则会形成欠拟合, 则会形成过拟合. 尽管五次多项式会精确的预测训练集中的样本点,但在预测训练集中没有的数据,则不能很好的预测,也就是说有较大的泛化误差,上面的右 ...

  7. Android WebView播放视频flash&lpar;判断是否安装flash插件&rpar;

    Android WebView播放flash(判断是否安装flash插件)  最近帮一个同学做一个项目,断断续续的一些知识点记录一下.一个页面中有一个WebView,用来播放swf,如果系统中未安装f ...

  8. MySQL中Checkpoint技术

    个人读书笔记,详情参考<MySQL技术内幕 Innodb存储引擎> 1,checkpoint产生的背景数据库在发生增删查改操作的时候,都是先在buffer pool中完成的,为了提高事物操 ...

  9. NATS—消息通信模型

    消息通信模型 NATS的消息通信是这样的:应用程序的数据被编码为一条消息,并通过发布者发送出去:订阅者接收到消息,进行解码,再处理.订阅者处理NATS消息可以是同步的或异步的. * 异步处理  异步处 ...

  10. 一千个不用Null的理由

    原文链接:http://www.importnew.com/27378.html 原文出处: xrzs 港真,Null 貌似在哪里都是个头疼的问题,比如 Java 里让人头疼的 NullPointer ...