在我们平常的web开发中,已经存在各种大型的专业Javascript类库(JQuery,Prototype,ExtJS)等,它们都充分利用了Javascript面向对象的思想,使得类库更加富有灵活性和健壮性,但其中最关键的是利用了Javascript的链式调用,这也就是我们今天要谈到的话题了。那什么是链式调用呢?简单的说,就是把一系列对DOM元素的操作以某种形式关联起来,使得使用很少的代码就能完成很强大的功能(Write Less, Do More)。下面我将一个示例来比较一下非链式调用和链式调用的区别所在。
//非链式调用
addEvent($('test'), 'click', function() {
setStyle(this, 'color', 'green');
show(this);
});
//链式调用
$("test").addEvent('click', function() {
$(this).setStyle('color', 'green').show();
});
大家可能看出其中的一点不同了($,addEvent等类库函数我会逐步讲到),但可能感觉不到链式调用的简洁,至少对函数调用有了的新的认识。那我们将逐步来体现链式调用的价值,就以$()函数的实现为例。大家都知道,在JS中要获取一个ID为test的元素通过这样获取: document.getElementById('test'),也许写一次感觉没什么,但写多了造成到处布满document.getElementById,你既难得写也厌烦,那我们何不把它简化一下呢,假设$('test')就代表整个意思该多好。
function $() {
var elements = [];
for(var i = 0, len = arguments.length; i < len; i++) {
var element = arguments[i];
if(typeof element === 'string') {
element = document.getElementById(element);
}
if(arguments.length === 1) {
return element;
}
elements.push(element);
}
return elements;
}
再往深处想想,如果我们现在有大堆方法都是在test这个元素上操作,试想$('test').(...???)应该是怎么组织呢?首先我们必须将$()方法作为链式调用的开头并且要返回当前元素的引用,以便我们后来的调用,采用我们前面学的闭包知识,我们对$()进行改造。
(function() {
//使用私有的方法
function _$(els) {
this.elements = [];
for(var i = 0, len = els.length; i < len; i++) {
var element = els[i];
if(typeof element === 'string') {
element = document.getElementById(element);
}
this.elements.push(element);
}
}
//提供与$()函数同样的公共接口
window.$ = function() {
return new _$(arguments);
}
})();
我们都知道JS所有的对象都会继承自它们的原型(prototype),我们就可以利用返回的引用实例以链式的方式的组织起来,代码如下:
(function() {
//使用私有的方法
function _$(els) {
this.elements = [];
for(var i = 0, len = els.length; i < len; i++) {
var element = els[i];
if(typeof element === 'string') {
element = document.getElementById(element);
}
this.elements.push(element);
}
}
_$.prototype = {
each: function(fn) {
for(var i = 0, len = this.elements.length; i < len; i++) {
fn.call(this, this.elements[i]);
}
return this;
},
setStyle: function(prop, val) {
this.each(function(el) {
el.style[prop] = val;
});
return this;
},
show: function() {
var that = this;
this.each(function(el) {
that.setStyle('display', 'block');
});
return this;
},
addEvent: function(type, fn) {
var add = function(el) {
if(window.addEventListener) {
el.addEventListener(type, fn, false);
}
else if(window.attachEvent) {
el.attachEvent('on' + type, fn);
}
};
this.each(function(el) {
add(el);
});
return this;
}
};
//提供与$()函数同样的公共接口
window.$ = function() {
return new _$(arguments);
}
})();
从以上的代码我们可以看出,在原型方法中结尾处都是return this;这保证了在调用该方法的同时可以将当前对象引用以链式的方式传递到下一方法,从而实现链式调用,如:
$(window).addEvent('load', function() {
$('test1', 'test2').show()
.setStyle('color', 'red')
.addEvent('click', function() {
$(this).setStyle('color', 'blue');
});
});
现在可以看出链式调用的简洁性了把。对于学习过JQuery的朋友来说,对这个代码非常熟悉同时也了解了JQuery代码背后的原理。到这里,我们已基本了解链式调用的原理以及实现,接下来我们试探性的设计一个JS类库的框架(可能适用于搭建企业内部结构的JS类库),了解了这种框架对你以后更好的应用JS有很大的帮助。在这里我们总结一下:大多数JS框架类库总是从以下3方面入手:
1). Events。即统一各大浏览器事件机制,使之彼此兼容。
2). DOM。即HTML中的大量元素的属性与行为管理。
3). AJAX。即统一并扩展XMLHttpRequest的异步调用机制。
利用我们今天学到的知识,利用链式调用建立一个JS简易版的类库。首先我们利用一下前面学过(面向对象的Javascript之一——初识Javascript)章节的内容。
Function.prototype.method = function(name, fn) {
this.prototype[name] = fn;
return this;
};
再次改造一下上面的代码使之看起来像JS类库:
(function() {
function _$(els) {
//...
}
//Events: addEvent, getEvent
_$.method('addEvent', function(type, fn) {
//...
}).method('getEvent', function(e) {
//...
}).
//DOM: addClass, removeClass, replaceClass, hasClass, getStyle, setStyle
method('addClass', function(className){
//...
}).method('removeClass', function(className) {
//...
}).method('replaceClass', function(oldClass, newClass) {
//...
}).method('hasClass', function(className) {
//...
}).method('getStyle', function(prop) {
//...
}).method('setStyle', function(prop, val) {
//...
}).
//AJAX: load
method('load', function(uri, method) {
//...
});
window.$ = function() {
return new _$(arguments);
}
})();
现在就把这个JS类库发布出去,可能在短期内没有什么问题发生,但突然有一天$这个函数被另一个类库给占用了,那我们所有的代码将不会再有效。为了防止与外库的冲突,我们开始想办法解决。
window.$ = function() {
return new _$(arguments);
}
//改造成以下代码
window.installHelper = function(scope, interface) {
scope[interface] = function() {
return new _$(arguments);
}
}
这样我们就可以避免冲突了,按照以下方式就可以完成调用。
intallHelper(window, '$');
下面利用这种方式去解决一个已定义好的的命名空间中。
window.com = window.com || {};
com.tms = com.tms || {};
com.tms.util = com.tms.util || {};
installHelper(com.tms.util, 'get');
(function() {
var get = com.tms.util.get;
get('test').addEvent('click', function(e) {
get(this).addClass('hello');
});
})();
并不是所有的链式调用都能解决所有问题,比如取值器和赋值器。对于赋值器很适合链式调用,取值器可能就不太好办了,下面我以一个例子来说明问题。
window.API = window.API || function() {
var name = 'Miracle';
this.setName = function(newName) {
name = newName;
return this;
};
this.getName = function() {
return name;
};
};
//调用API
var api = new API;
console.log(api.getName());//Miracle
console.log(api.setName('Miracle He').getName());//Miracle He
可以采用回调方法来返回数据,使赋值器和取值器都支持链式调用。
window.API = window.API || function() {
var name = 'Miracle';
this.setName = function(newName) {
name = newName;
return this;
};
this.getName = function(callback) {
callback(this, name);
return this;
};
};
//调用API
var api = new API;
api.getName(console.log).setName('Miracle He').getName(console.log);
//diaplay 'Miracle' and then 'Miracle He'
再次总结一下:链式调用就是让支持它的每个方法都返回对象的引用,使用链式调用可以避免多次重复使用一个对象变量以减少代码量。