本文目录
1
概念与实现
估计很多同学对链式调用相当熟悉了,可以直接跳过第一节。
2
辅助函数
为了链式调用,让我们写代码时,多写几行this,问题倒是不大。但有的情况是,代码是别人写的,并且对方并没有考虑到所谓i的链式调用。
3
案例
这个辅助函数虽然很简单,但其应用相当广。
4
后记
1.概念与实现
可链式调用的英文是chainable,我们先看看非chainable是什么样的:
<script> var object = { doSomething1: function(){ console.log('doSomething1...') }, doSomething2: function(){ console.log('doSomething2...') }, doSomething3: function(){ console.log('doSomething3...') } } object.doSomething1(); object.doSomething2(); object.doSomething3(); </script>
代码中的最后三行,每次调用都要重复写对象的名称object。
而chainable的代码:
object.doSomething1().doSomething2().doSomething3();
实现的原理,亦很简单,每个方法最后都返回 this:
<script> var object = { doSomething1: function(){ console.log('doSomething1...') return this; }, doSomething2: function(){ console.log('doSomething2...') return this; }, doSomething3: function(){ console.log('doSomething3...') return this; } } object.doSomething1().doSomething2().doSomething3(); </script>
然而代码都写在一行,是不利于阅读和断点调试的,开发版本中可以格式化一下:
object
.doSomething1()
.doSomething2()
.doSomething3();
说到格式化,如果链的节点返回不是同一个对象,建议使用如下的格式化方式:
object1
.doSomething1()
.doSomething2()
.getObject2()
.doSomethingElse1();
.doSomethingElse2();
尤其是jquery的代码:
$('.container') .find('p') .eq(0) .removeClass('red') .css('color', 'blue');
上面的格式,因人而异吧,有人觉得好看,有人觉得难看。
2.辅助函数
为了链式调用,让我们写代码时,多写几行this,问题倒是不大。但有的情况是,代码是别人写的,并且对方并没有考虑到所谓i的链式调用。难道我们只有放弃使用这种优雅的方式了吗?解决方案是有的。
比如有如下的第三方代码:
function Person(name,age){ this.name = name; this.age = age; this.count++; } Person.prototype = { sayName: function(){ console.log(this.name); }, sayAge: function(){ console.log(this.age); }, getCount: function(){ return this.count; }, count: 0 }; var p = new Person('xxx',18); p.sayName(); p.sayAge(); var count = p.getCount(); console.log(count);
上面 Person 类明显是不支持链式调用的。我们希望的使用方式是:
var p = new Person('xxx', 18); var count = p .sayName() .sayAge() .getCount(); console.log(count);
要做到链式调用,需要该相应的方法返回this,比如上面的 sayName 方法。
不改变源代码的基础上,如何让它返回 this 呢?添加一层包裹函数就能做到,比如:
var old = Person.prototype.sayName; Person.prototype.sayName = function() { var result = old.apply(this, arguments); return result === undefined ? this : result; };
先执行原方法,根据其返回值,来判断该方法是否支持链式调用。原sayName是没有返回值的,因此新sayName变成支持链式调用的。
如果把sayName改成 getCount ,那么新的getCount跟原先做的事情是一样的,仍不支持链式调用的,这是我们想要的结果。接下来封装个辅助函数:
function chainablize(constructor) { var prototype = constructor.prototype; for(var method in prototype) { try { if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') { (function(method) { var old = prototype[method]; prototype[method] = function() { var result = old.apply(this, arguments); return result === void 0 ? this : result; }; })(method); } } catch(e) {} } }
其中使用了 try-catch ,是因为一些原生对象中的某些属性,使用 typeof 时会报错。
测试案例如下:
function Person(name, age) { this.name = name; this.age = age; this.count++; } Person.prototype = { sayName: function() { console.log(this.name); }, sayAge: function() { console.log(this.age); }, getCount: function() { return this.count; }, count: 0 }; function chainablize(constructor) { var prototype = constructor.prototype; for(var method in prototype) { try { if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') { (function(method) { var old = prototype[method]; prototype[method] = function() { var result = old.apply(this, arguments); return result === void 0 ? this : result; }; })(method); } } catch(e) {} } } chainablize(Person); var p = new Person('xxx', 18); var count = p .sayName() .sayAge() .getCount(); console.log(count);
现在考虑一个问题,如果Person实例是如下的方式使用呢?
var p = new Person('xxx', 18); p.name = 'yyy'; p.age = 20; p.sayName(); p.sayAge();
对象属性的赋值操作是很常见,如果赋值操作也能支持链式调用的就好了。所以有必要发明如下的api:
p.prop({ name: 'yyy', age: 20 });
该api是我们主观添加进去的,第三方的代码可能不提供。应该长成如下的样子:
Person.prototype.prop = function(object) { for(var property in object) { this[property] = object[property]; } return this; }
然后我们把它也添加到 chainablize 函数里:
function chainablize(constructor) { var prototype = constructor.prototype; for(var method in prototype) { try { if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') { (function(method) { var old = prototype[method]; prototype[method] = function() { var result = old.apply(this, arguments); return result === void 0 ? this : result; }; })(method); } } catch(e) {} } if('prop' in prototype) return; prototype.prop = function(object) { for(var property in object) { this[property] = object[property]; } return this; } }
最终测试案例如下:
function Person(name, age) { this.name = name; this.age = age; this.count++; } Person.prototype = { sayName: function() { console.log(this.name); }, sayAge: function() { console.log(this.age); }, getCount: function() { return this.count; }, count: 0 }; function chainablize(constructor) { var prototype = constructor.prototype; for(var method in prototype) { try { if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') { (function(method) { var old = prototype[method]; prototype[method] = function() { var result = old.apply(this, arguments); return result === void 0 ? this : result; }; })(method); } } catch(e) {} } if('prop' in prototype) return; prototype.prop = function(object) { for(var property in object) { this[property] = object[property]; } return this; } } chainablize(Person); var p = new Person('xxx', 18); p .prop({ name: 'yyy', age: 20 }) .sayName() .sayAge();
3.案例
这个辅助函数虽然很简单,但其应用相当广。
-
3.1 实现canvas链式调用
非链式调用:
<canvas id="canvas" width="600" height="400"></canvas> <script> // 代码来自于《canvas核心技术 图形、动画与游戏开发》一书的第2章 var context = document.getElementById('canvas').getContext('2d'); function drawGrid(context, color, stepx, stepy) { context.strokeStyle = color; context.lineWidth = 0.5; for(var i = stepx + 0.5; i < context.canvas.width; i += stepx) { context.beginPath(); context.moveTo(i, 0); context.lineTo(i, context.canvas.height); context.stroke(); } for(var i = stepy + 0.5; i < context.canvas.height; i += stepy) { context.beginPath(); context.moveTo(0, i); context.lineTo(context.canvas.width, i); context.stroke(); } } drawGrid(context, 'lightgray', 10, 10); </script> <style> body { background: #eeeeee; } #canvas { background: #ffffff; cursor: pointer; margin-left: 10px; margin-top: 10px; box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5); } </style>
链式调用:
<canvas id="canvas" width="600" height="400"></canvas> <script> // 辅助函数 function chainablize(constructor) { var prototype = constructor.prototype; for(var method in prototype) { try { if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') { (function(method) { var old = prototype[method]; prototype[method] = function() { var result = old.apply(this, arguments); return result === void 0 ? this : result; }; })(method); } } catch(e) {} } if('prop' in prototype) return; prototype.prop = function(object) { for(var property in object) { this[property] = object[property]; } return this; } } </script> <script> // 使其canvas的context相关api可以链式调用 chainablize(CanvasRenderingContext2D); var context = document.getElementById('canvas').getContext('2d'); function drawGrid(context, color, stepx, stepy) { context.prop({ strokeStyle: color, lineWidth: 0.5 }); for(var i = stepx + 0.5; i < context.canvas.width; i += stepx) { context .beginPath() .moveTo(i, 0) .lineTo(i, context.canvas.height) .stroke(); } for(var i = stepy + 0.5; i < context.canvas.height; i += stepy) { context .beginPath() .moveTo(0, i) .lineTo(context.canvas.width, i) .stroke(); } } drawGrid(context, 'lightgray', 10, 10); </script> <style> body { background: #eeeeee; } #canvas { background: #ffffff; cursor: pointer; margin-left: 10px; margin-top: 10px; box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5); } </style>
-
3.2 实现dom链式操作api
-
3.2.1 非链式调用
<script> var button = document.createElement('button'); button.innerHTML = 'click me'; button.setAttribute('data-msg', 'hello'); button.addEventListener('click', function() { alert(this.getAttribute('data-msg')); }); button.style.color = 'white'; button.style.setProperty('background-color', 'blue'); button.style.setProperty('margin', '100px 100px'); document.body.appendChild(button); </script>
3.2.2 链式调用
<script> // 辅助函数 function chainablize(constructor) { var prototype = constructor.prototype; for(var method in prototype) { try { if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') { (function(method) { var old = prototype[method]; prototype[method] = function() { var result = old.apply(this, arguments); return result === void 0 ? this : result; }; })(method); } } catch(e) {} } if('prop' in prototype) return; prototype.prop = function(object) { for(var property in object) { this[property] = object[property]; } return this; } } chainablize(CSSStyleDeclaration); chainablize(DOMTokenList); chainablize(Node); chainablize(Element); var button = document.createElement('button'); button .prop({ innerHTML: 'click me' }) .setAttribute('data-msg', 'hello') .addEventListener('click', function() { alert(this.getAttribute('data-msg')); }, false); button.style .prop({ color: 'white' }) .setProperty('background-color', 'blue') .setProperty('margin', '100px 100px'); document.body.appendChild(button); </script>
4.后记
其实本文的思想来源是一个库(chainvas)。库是有点老,不过作者是《css揭秘》的作者。可以把本文当成其源码分析,其源码没多少,不到200行,看完本文后,照着敲一边应该没问题了。话说 chainablize 函数确实修改了内置原型,毕竟为每个方法包裹了一层函数。但与一般的“猴子补丁”不一样,没有改变原有方法的行为,只是让其尽可能的返回this罢了。而新增prop确实是“猴子补丁”,可以删除。