jQuery源碼中的「new jQuery.fn.init()」什麼意思?

全部文章搬運自個人我的主頁:sheilasun.mecss

引子

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

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

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

自執行的匿名函數

jQuery源碼最外層的結構以下:框架

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

任何庫的引入都得作到不污染全局變量,得有本身的命名空間。上面的自執行匿名函數就能夠作到這點,把全部庫私有的變量和方法,都包到一個私有的空間內,容許外界訪問的屬性或方法能夠掛載到window上。ide

例以下面這段代碼:函數

(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,瞭解了這個自執行匿名函數的做用,這裏還有兩個問題。this

第一,爲何要傳入window?

看了上面的outerAddOne這個例子,就會發現,不傳入window也沒什麼嘛,照樣能夠把方法掛到window身上啊。
兩個緣由:prototype

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

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

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,不要,不要這樣,大家這樣講,不會有人有耐心聽下去的……咱們要像說故事同樣娓娓道來,抓住聽衆的注意力,一點點引入……」因而之後我都儘可能按照「說故事」這個思路去講,最後畢業答辯的時候,一個老師說,「爲何我以爲你像故宮導覽哈哈哈哈」…… 果真仍是沒有掌握表述的技巧啊。

相關文章
相關標籤/搜索