早几年学习前端,大家都非常热衷于研究 jQuery 源码。我还记得当初从 jQuery 源码中学到一星半点应用技巧的时候常会有一种发自内心的惊叹,“原来 JavaScript 居然可以这样用!”
虽然随着前端的发展,另外几种前端框架的崛起,jQuery 慢慢变得不再是必须,因此大家对于 jQuery 的热情低了很多。但是许多从 jQuery 中学到的技巧用在实践中仍然非常好用。了解它有助于我们更加深入的理解 JavaScript。
这里我们把 jQuery 的实现作为一个学习案例,帮助我们进一步掌握面向对象的使用。也为有兴趣的朋友,进一步学习 jQuery源码一个铺垫,算是一个简单的抛砖引玉。
使用 jQuery 时,我们通常会这样写:
1// 声明一个 jquery 实例2$('.target')34// 获取元素的css属性5$('.target').css('width')67// 获取元素的微信信息8$('target').offset()
咦?很奇怪,和普通的对象实例不太一样,new 关键字哪里去了?$
符号又是什么?
带着这些疑问,我们来实现一个简化版的 jQuery 库。
首先,一个库就是一个单独的模块,使用自执行函数的方式模拟一个模块。
1(function() {2// do something3});
第二步,我们能够在全局直接调用 jQuery,说明 jQuery 被挂载在了全局对象上。因此当我们在模块中对外提供接口时,可以采取 window.jQuery
的方式。
1// 先声明一个构造函数2var jQuery = function() {}34// ...56window.jQuery = jQuery
在使用时,并没有用 jQuery,而是使用了 $, 其实是多加了一个赋值操作。
window.$ = window.jQuery = jQuery
我们在使用时,直接使用 $
,也就是说直接调用了构造函数 jQuery 来创建了一个实例,而没有使用 new。但是我们知道,创建一个实例 new 关键字肯定是必不可少的。因此说明 new 的操作被放在了 jQuery 方法中来实现。而 jQuery 也并不是真正的构造函数。
前面一节的学习我们知道,每一个函数都可能是任何角色,因此 jQuery 内部的实现正是利用了这一点,在具体实现时,改变了内部某些函数的 prototype 指向。先看实现代码,再来一步步分析。
10;(function (ROOT) {20// 构造函数30var jQuery = function (selector) {40// 在该方法中直接返回new过的实例,因此这里的init才是真正的构造函数50return new jQuery.fn.init(selector);60}7080jQuery.fn = jQuery.prototype = {90constructor: jQuery,10version: '1.0.0',11init: function (selector) {12var elem, selector;13elem = document.querySelector(selector);14this[0] = elem;1516// 在jquery中返回的是一个由所有原型属性方法组成的数组,我们这里做了简化,直接返回this即可17return this;18},1920// 在原型上添加一堆方法21toArray: function () { },22get: function () { },23each: function () { },24ready: function () { },25first: function () { },26slice: function () { }27// ... more28}2930// 让init方法的原型,指向jQuery的原型31jQuery.fn.init.prototype = jQuery.fn;3233ROOT.jQuery = ROOT.$ = jQuery;34})(window);
首先在 jQuery 构造函数中声明了一个 fn 属性,并将其指向了原型 jQuery.prototype
。随后在原型对象中添加了init方法。
1jQuery.fn = jQuery.prototype = {2init: function() {}3}
之后又将 init 的原型,指向了 jQuery.prototype.
jQuery.fn.init.prototype = jQuery.fn;
而在构造函数jQuery中,则返回了init的实例对象。
1var jQuery = function(selector) {2return new jQuery.fn.init(selector);3}
最后对外暴露接口时,将字符 $ 与方法 jQuery 对等起来。
ROOT.jQuery = ROOT.$ = jQuery;
因此当我们使用 $('#test')
创建一个 jQuery 实例时,实际上是调用的 jQuery('#test')
创建的一个 init 实例。这里真正的构造函数是原型中的 init 方法。
下面用图例展示下这中间的逻辑变化。
我们在使用 jQuery 时还知道,jQuery 提供了两个扩展接口来帮助我们自定义 jQuery 的方法。通常称自定义的 jQuery 方法为 jQuery 插件。那么这两个扩展方法是如何实现的呢?在上面的实现基础上我们继续添加代码,如下。
10;20(function (ROOT) {3040// 构造函数50var jQuery = function (selector) {60// 在该方法中直接返回new过的实例,因此这里的init才是真正的构造函数70return new jQuery.fn.init(selector);80}9010jQuery.fn = jQuery.prototype = {11constructor: jQuery,12version: '1.0.0',13init: function (selector) {14var elem, selector;15elem = document.querySelector(selector);16this[0] = elem;1718// 在jquery中返回的是一个由所有原型属性方法组成的数组,我们这里做了简化,直接返回this即可19return this;20},2122// 在原型上添加一堆方法23toArray: function () { },24get: function () { },25each: function () { },26ready: function () { },27first: function () { },28slice: function () { }29// ... more30}3132// 让init方法的原型,指向jQuery的原型33jQuery.fn.init.prototype = jQuery.fn;3435// 实现jQuery的两种扩展方法36jQuery.extend = jQuery.fn.extend = function (options) {37// 在jquery源码中会根据参数不同进行不同的判断,我们这里假设只有一种方式38var target = this;39var copy;4041for (name in options) {42copy = options[name];43target[name] = copy;44}45return target;46}4748// jQuery中利用上面实现的扩展机制,添加了许多方法,其中4950// 添加静态扩展方法,即工具方法51jQuery.extend({52isFunction: function () { },53type: function () { },54parseHTML: function () { },55parseJSON: function () { },56ajax: function () { }57// ...58})5960// 添加原型方法61jQuery.fn.extend({62queue: function () { },63promise: function () { },64attr: function () { },65prop: function () { },66addClass: function () { },67removeClass: function () { },68val: function () { },69css: function () { }70// ...71})7273ROOT.jQuery = ROOT.$ = jQuery;74})(window);
在上面的代码中,我们通过下面的方式简单的实现了两个扩展方法。
10jQuery.extend = jQuery.fn.extend = function (options) {2030// 在jquery源码中会根据参数不同进行很多判断,我们这里就直接走一种方式,所以就不用判断了40var target = this;50var copy;6070for (name in options) {80copy = options[name];90target[name] = copy;10}11return target;12}
要理解它的实现,首先要明确的知道内部 this 的指向。相信学习过前面的章节对于 this 的掌握已经不是问题了。传入的参数 options 对象是一个 key-value 模式的对象。我们通过 for in 遍历 options,将 key 作为新的属性,value 作为该属性对应的新方法,分别添加到 jQuery 与 jQuery.fn 中。
也就是说,当我们通过 $.extend
扩展 jQuery 时,方法被添加到了静态方法中,而当我们通过 $.fn.extend
扩展 jQuery 时,方法被添加到了原型对象中。
静态方法可以直接调用,因此常常也被称为工具方法。例如:
1$.ajax()2$.isFunction()3$.each()4//...
原型方法则必须通过声明的实例才能调用。
1$('#test').css();2$('#test').attr();34// ...