jQuery源碼中的“new jQuery.fn.init()”什么意思?


所有文章搬運自我的個人主頁:sheilasun.me

引子

最近打算試試看看jQuery的源碼,剛開個頭就卡住了。無論如何都理解不了jQuery源碼入口部分中的

return new jQuery.fn.init( selector, context )

看了好多帖子都沒看懂,覺得自己很蠢,心里很苦,吃宵夜都不香了。昨晚去游泳,游完8*100后靠在池壁上喘氣,有人從我旁邊出發,水花濺起的瞬間,我突然,想通了!這大概就是回光返照 (划掉)福至心靈吧!
下面一點點地說下我對jQuery入口源碼的理解。

自執行的匿名函數

jQuery源碼最外層的結構如下:

(function(window,undefined){
    ...
})(window);

任何庫的引入都得做到不污染全局變量,得有自己的命名空間。上面的自執行匿名函數就可以做到這點,把所有庫私有的變量和方法,都包到一個私有的空間內,允許外界訪問的屬性或方法可以掛載到window上。

例如下面這段代碼:

(function(){
  var count=0;
  var addOne=function(){
    alert(count++);
  };
  window.outerAddOne=addOne; //掛到window上外界方可訪問
})();

outerAddOne();//alert "0"
console.log(count);//error
console.log(addOne);//error

內部定義的count變量以及addOne方法,外部環境下是無法訪問到的,但是在window上掛載一個方法outerAddOne,指向addOne,外界就可以訪問到了。

OK,了解了這個自執行匿名函數的作用,這里還有兩個問題。

第一,為什么要傳入window?

看了上面的outerAddOne這個例子,就會發現,不傳入window也沒什么嘛,照樣可以把方法掛到window身上啊。
兩個原因:

首先,從代碼壓縮混淆的角度考慮。

我們用線上工具來壓縮混淆下面這段示例代碼:

function say(){
  var name="naima";
  window.description="hi "+name;
}

壓完混完后瘦了一點:

function say(){var a="naima";window.description="hi "+a}

看到沒有,用a代替了name,但是window既不是聲明的局部變量也不是參數,是不會被壓縮混淆的,所以將window作為參數傳入可解決這個問題。

其次,傳入window參數,就可以不用沿着作用域鏈一層層向上查找直到頂層作用域去獲取window對象了,訪問更快了。

第二,為什么要傳入undefined?

undefined並不是JS中的關鍵字,在IE8及以下中是可以對其重新賦值的。

var undefined="new value";
alert(undefined);//alert “new value"

在參數列表中給出undefined參數,但是不傳入值,那么這個參數值就是undefined值了。

jQuery對象的構建

先看jQuery源碼中如何對jQuery賦值的:

jQuery = function( selector, context ) {
		// The jQuery object is actually just the init constructor 'enhanced'
		return new jQuery.fn.init( selector, context, rootjQuery );
	}

我就是被new jQuery.fn.init()這里弄暈了,先在這里暫停,回想一下平常我是如何使用jQuery的($即對應‘jQuery'):

$('body').css('background','red');
$.parseJSON('{}');

要實現這兩種調用,$('body')應該是一個實例對象,css是每個實例共享的方法,是原型上的方法。而$則是一個類,parseJSON則是類的靜態方法。
接下來,我們試着往這個結果上靠。

如何不用new關鍵字得到jQuery對象

回想一下平常我都是怎么構建實例對象的,通常我會這樣寫一個Prince類:

function Prince(name){
  this.name=name;
  this.body="human";
}

Prince.prototype.change=function(){
  this.body="frog";
};

然后我會這樣去獲取一個Prince實例對象:

var prince=new Prince("Harry");
prince.change();

如果我年紀大了忘記用new關鍵字了,程序就報錯了:

var a=Prince('harry');
a.change();//error,"Cannot read property 'change' of undefined"

除了調用方法會出錯之外,window還被掛載了兩個變量上去,何其無辜。

但是獲取jQuery對象(以下簡稱JQ對象)用new和不用new都可以,返回的是一樣樣的。

console.log($('*').length);//14
console.log(new $('*').length);//14

為了做到這點,我們很容易想到需要在構造函數內部返回對象。引用下我在另一篇博文JavaScript中的普通函數與構造函數里寫的:

構造函數有return值怎么辦?
構造函數里沒有顯式調用return時,默認是返回this對象,也就是新創建的實例對象。
當構造函數里調用return時,分兩種情況:
1.return的是五種簡單數據類型:String,Number,Boolean,Null,Undefined。
這種情況下,忽視return值,依然返回this對象。
2.return的是Object
這種情況下,不再返回this對象,而是返回return語句的返回值。

所以我們應該在jQuery構造函數內部去返回一個對象,這樣就可以不用new的方式去創建JQ對象了,其實這時候,構造函數就相當於一個工廠函數了。
那么核心問題來了。

該返回什么樣的對象?對於這個對象有何要求?

這個對象必須可以調用jQuery.prototype上的方法。

我們使用或自己寫jQuery插件的時候會經常遇到$.fn這個對象,很多插件都是通過擴展這個對象來實現的。
$.fn其實對應着jQuery.prototype,$和fn分別是jQuery和prototype的簡寫方式,只要我們把方法擴展到這個原型對象身上,通過$()獲取的JQ對象都是可以訪問到方法的。
例如:

$.fn.greeting=function(){alert('hi')};
$('body').greeting();//alert 'hi'

所以,工廠函數內部返回的對象一定要可以調用jQuery.prototype上的方法。

是時候看John Resig到底是怎么做的啦。

jQuery源碼

jQuery = function( selector, context ) {
	return new jQuery.fn.init( selector, context, rootjQuery );
},
jQuery.fn = jQuery.prototype = { //fn即對應prototype
    constructor: jQuery,
    init: function( selector, context, rootjQuery ) {
        ...
        return this;
    }
    ...
}
jQuery.fn.init.prototype = jQuery.fn;

在chrome里調試時候添加JQ對象的watch,會看到類似如下的結果:

$('*'): n.fn.init[14]

看到上面這段源碼,原因就很明顯了,其實我們所說的JQ對象根本就是init函數的實例對象,而init則是jQuery原型上的一個對象,它本身是沒有什么方法的,全靠從jQuery原型上拿。

"jQuery.fn.init.prototype = jQuery.fn"這句很重要,它將init的原型指向jQuery的原型,所以JQ對象才可以訪問‘css'、'show'、'hide'這些寫在jQuery.fn上的方法。

我們可能會有疑問,為何要從init這繞這么一大圈來訪問jQuery的原型,而不是直接返回一個jQuery實例直接通過這個實例來訪問自身原型?比如說代碼可以寫成這樣:

jQuery = function( selector, context ) {
	    return new jQuery();
} 

問題很明顯,這樣做只會大家一起死,死在循環里。

好,那我接受init的存在,但是我這樣寫難道不可以嗎?

jQuery = function( selector, context ) {
    	return jQuery.fn.init();//不同點在於去掉了new關鍵字
}

讓我們做點動作來證明加上new是有用的。

jQuery = function( selector, context ) {
	return jQuery.fn.init();
},
jQuery.fn = jQuery.prototype = {
    init: function() {
            this.name='sheila';
            return this;
    },
    anotherName:'sunwukong'
};
var jq=jQuery();
console.log(jq.anotherName);//"sunwukong"
console.log(jq.name);//"sheila"

上面這段代碼是為了說明this的作用域問題,其不僅能訪問init函數內部,還能向上一層到fn對象。我聽人家說,做框架的,作用域要獨立才好呢。
給它加上new關鍵字:

...
return new jQuery.fn.init();
...

console.log(jq.anotherName);//undefined
console.log(jq.name);//"sheila"

這樣this的作用域就獨立出來了。

經博友評論提醒,加不加new還牽涉到一個更重要的問題:返回的對象究竟是誰。不加new的情況下,'jQuery.fn.init()'相當於調用方法,this指向的以及最后返回的都是同一個jQuery.fn對象,$('body')和$('p')就沒有區分了。顯然,這是不合理的。而加了new,就是每次用構造函數實例化了一個新對象,彼此都是不同的。

有任何不妥之處或錯誤歡迎各位指出,不勝感激~

題外話

經常看別人的博客,有些表述方式實在獨特而有趣,每每讀來都覺妙趣橫生,啞然失笑。不禁心生羡慕,技術過硬,知識面廣還寫得一手好文章,贊!
想起在學校時每次我們做presentation,上台第一句,“大家好,我今天講的題目是……”,然后幻燈片一頁頁划過去,“歷史背景”,“研究現狀”,“我使用的方法”……導師都聽得一臉崩潰,“nonono,不要,不要這樣,你們這樣講,不會有人有耐心聽下去的……我們要像說故事一樣娓娓道來,抓住聽眾的注意力,一點點引入……”於是以后我都盡量按照“說故事”這個思路去講,最后畢業答辯的時候,一個老師說,“為什么我覺得你像故宮導覽哈哈哈哈”……
果然還是沒有掌握表述的技巧啊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM