JavaScript的链式调用

老姚
老姚 发布于 2016-12-26 19:27:25 浏览:1623 类型:原创 - 随笔 分类:JavaScript - 我也来说说系列 二维码: 作者原创 版权保护
本文结构
1.概念与实现
2.辅助函数
3.案例
4.后记
估计很多同学对链式调用相当熟悉了,可以直接跳过第一节。

1.概念与实现
可链式调用的英文是chainable,我们先看看非chainable是什么样的:
var object = {
        doSomething1: function() {
                console.log('doSomething1...');
        },
        doSomething2: function() {
                console.log('doSomething2...');
        },
        doSomething3: function() {
                console.log('doSomething3...');
        }
};
object.doSomething1();
object.doSomething2();
object.doSomething3();
代码中的最后三行,每次调用都要重复写对象的名称object。
而chainable的代码:
object.doSomething1().doSomething2().doSomething3();
实现的原理,亦很简单,每个方法最后都返回this:
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();
然而代码都写在一行,是不利于阅读和断点调试的,开发版本中可以格式化一下:
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.案例
这个辅助函数虽然很简单,但其应用相当广。
一、实现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>

二、实现dom链式操作api
1.非链式调用
<body></body>
<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>

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;
	}
}
</script>
<body></body>
<script>
	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确实是“猴子补丁”,可以删除。

本文完。
z
给个赞 31 人点赞
收藏 19 人收藏
评论 已有 8 条评论;以下用户言论只代表其个人观点,不代表 前端网(QDFuns) 的观点或立场。
登录 以后才能发表评论
最新评论
极乐网
极乐网2016-12-29 11:44:038F
好滴~ //@老姚:转吧,遇到错别字请修改。 //@极乐网:可以转载了吗?老姚~哈 //@老姚emoticon //@极乐网:圣诞节不是过完了吗?竟然没有人点赞!我坐个沙发~
举报 支持 (0) 回复 (0)
老姚
老姚2016-12-28 21:30:077F
转吧,遇到错别字请修改。 //@极乐网:可以转载了吗?老姚~哈 //@老姚emoticon //@极乐网:圣诞节不是过完了吗?竟然没有人点赞!我坐个沙发~
举报 支持 (0) 回复 (1)
极乐网
极乐网2016-12-28 18:32:456F
可以转载了吗?老姚~哈 //@老姚emoticon //@极乐网:圣诞节不是过完了吗?竟然没有人点赞!我坐个沙发~
举报 支持 (0) 回复 (1)
陈陈陈大文
陈陈陈大文2016-12-28 15:37:275F
先收藏,搬完砖再看。
举报 支持 (0) 回复 (0)
吴大树
吴大树2016-12-28 09:14:014F
学习了!emoticon
举报 支持 (0) 回复 (0)
良民
良民2016-12-27 14:00:063F
前排出售瓜子花生小板凳
举报 支持 (0) 回复 (0)
老姚
老姚2016-12-27 10:04:232F
emoticon //@极乐网:圣诞节不是过完了吗?竟然没有人点赞!我坐个沙发~
举报 支持 (0) 回复 (1)
极乐网
极乐网2016-12-26 22:58:221F
圣诞节不是过完了吗?竟然没有人点赞!我坐个沙发~
举报 支持 (0) 回复 (1)
老姚 老姚 作者

与自己为敌,与自己为友,一边深挖思想,一边埋葬自己。

作者最新