老早之前,读过jQuery的“吐温”动画(Tween Animation)的模块源码,不经意间,也顺便拜读jQuery如何创立不可思议的$对象的,这里简单列出以下core.js源代码:

// Define a local copy of jQuery
jQuery = function( selector, context ) {
	// The jQuery object is actually just the init constructor 'enhanced'
	// Need init if jQuery is called (just allow error to be thrown if not included)
	return new jQuery.fn.init( selector, context );
}

初读jQuery源码,可能大多数的同学都会和我一样摸不到头脑:

  • 为了实现$(...)的选择器功能,jQuery通过new关键字创建对象,就一定要把new隐藏封装到jQuery函数体内部吗?
  • 而且new个啥,他竟然new一个jQuery.fn.init这个玩意,这是啥?

于是我们读到了jQuery.fn:

jQuery.fn = jQuery.prototype;

等等,这不是耍我吗,这玩意就是“原型对象”?!这里其实是jQuery扩展原型插件的公共接口,为了以后第三方插件开发人员方便所提供的,还记得吗?我们平日看到很多jQuery插件库有用到$.fn.extend(...),如果你写个prototype多费劲啊,jQuery在API细节处理上,应该算是良苦用心了。

OK,接下来,我们可能会找到init.js定义处,我们在init定义结束之处,惊然发现如下一句代码:

// Give the init function the jQuery prototype for later instantiation
init.prototype = jQuery.fn;

搞半天,我们new的init“构造器”的原型引用竟然指向的就是jQuery.fn(jQuery.prototype),最后时刻,可能有点无奈,更有甚者(不排除极端),可能立马破口大骂:“TM这不是耍我吗,装什么大神?”,但是更多的人会问道:“为啥这样,有啥好处?”

这里,虽说我没有像某些同学那样那么极端,但是这些日子以来,总是对jQuery对象构造这块的设计十分疑惑,光想想,如果自己设计jQuery,肯定会使用直接让用户自己用new关键字得了,取名字叫JQuery(大写开头)即可,这样大家都知道自己根据JQuery这个类生成了对应的实例即可,这样至少不会产生那么多困惑啊。

那么,我们的API代码,很可能就像这样:

var myJQDomObj = new JQuery("#test_dom_id");

这样的代码,看起来多清楚,根据类构造一个实例,多么类Java,不是么?可是,你想想John Resig是做过数据挖掘的程序员,Java、以及那些类C++的面向对象语言难道他不清楚么?今天,我才明白,作为一名“大师”级人物,他考虑到的事情更多,参考以下例子

/**
 * 构造一个类,叫做GoodMan
 */
function GoodMan(name) {
	this.name = name;
}
/**
 * 添加原型公共方法,叫做say
 */
GoodMan.prototype.say = function () {
	return "I'm a good man!!!";
};

很多自诩为js高手的人,大概对此一点都不感冒吧,分分钟就能说出这句代码的意图——无非就是创建一个类似Java的Class呗,那么接下来,我们很顺其自然地调用它。

var me = new GoodMan("Joo Wu");
console.log(me.say()); // I'm a good man!!!

是的,我是一个好人。可是像这样呢?

var you = GoodMan("Jooooooooo");

此刻,这个假如这个程序员本身技术不咋地,而且喜欢睡觉,他写着写着,一头趴在键盘上,把o键按住了,更糟糕的时,他还把new关键字给忘了,Oh, shit,这个程序还能执行吗?

答案是肯定的,肯定能正确执行上面那句代码,可是当你真正想起要调用you对象时,你不经意会发现”you is undefined!“,是的,你未被定义,你都没出生呢,哪里能说话呢?可能,等到你调用you的时候,控制台报错了,你却老早就不知道你是在哪被初始化的了。还有一点,全局window,被你莫名其妙地挂上了一个全局变量,叫做name,其值为Jooooooooo。

console.log(you.say()) // Error ...
console.log(name) // Jooooooooo

js是动态脚本语言,因为语法太像Java而导致很多编程思维,甚至代码规范都想套用Java的,这种做法确实是一件值得鼓舞的事情,毕竟大家的编程水平参差不齐,有一套良好的规范十分重要。但是必须注意的是,作为一位专业的前端开发人员,如果不去了解本质,而是一味地套用别人的模板,可能一辈子在编程技术方面很难有进展。正因为John Resig考虑到了这些,我认为他为了让jQuery封装得更简单易用,他设计了这个优雅的解决方案,为了无论用户调用哪种API,jQuery为了保证高度一致性,他采用了这种”环行构造器“的方式。
下面,我们改进一下我们的GoodMan。

function GoodMan(name) {
	/*这里无论GoodMan是否带是通过new关键产生实例,init都会巧妙地指向正确的this指针!!!(神奇过头了,谁TM想得到啊)*/
	return new GoodMan.prototype.init(name);
}

GoodMan.prototype = {
	init: function(name) {
    	this.name = name;
    },
    say: function() {
    	return "I'm a good man!!!";
    },
    myFriend: function() {
    	return GoodMan("You");
    }
};

GoodMan.prototype.init.prototype = GoodMan.prototype;

好了,这下我们那位技术不咋地,还爱参瞌睡的程序员可以这样构造他的GoodMan的实例了。

var you = GoodMan("Lili");
var me = new GoodMan("Rosan");
console.log(you.say() === me.say()); // true

此刻,你或许有恍然大悟的感觉了,要知道,设计一份通用世界的API多么不容易,如此简单优雅,你还会骂Shit吗?当然,为了级联式编程,这样设计jQuery,既为用户省略了一大堆本该要写的new关键字,也让用户一句代码,做更多事情。注意到我在方法里面加了一个myFriend函数,那接下来就有意思了。

GoodMan("Joo Wu")
.myFriend()
.myFriend()
.myFriend().say() // "I'm a good man!!!"

此时多年Java经验,并且强迫症很严重的同学会立马站起来,说:”你看jQuery,首字母小写,就不是构造函数了,他不能算一个类,而且你这里的GoodMan开头大写,是一个类,你这样设计API,谁知道你是类还是对象啊???“,其实,我应该这样回答,js优美的特性决定它的灵活,并且没有任何一个官方文档强烈要求我们必须把函数和构造器、甚至对象区分开来,我们面向函数编程,函数才是我们的一等对象,原型继承、属性扩展,或许只有Java的反射才能做到,所以JavaScript!==Java,请不要过分类比。

很多时候,我们崇拜大神,不对,是大师,仅仅是因为我们半知半解那些人的代码,最终因为fork的人数居多便习以为然,往往只有时间和个人积累,到了某一天才会猛然明白一些原理。

最后,我们推崇John Resig,感谢他创造了CSS选择器,感谢他基于选择器的算法创建神一般的jQuery,无论从架构设计角度,还是从jQuery细节处理方面,都是十分优雅、符合人性的,这些对问题的思维方式都需要我们深入研究、好好学习。