abstrait:從本篇開始會陪大家一起從零開始走一遍 jQuery 的奇妙旅途,在整個(gè)系列的實(shí)踐中,我們會把 jQuery 的主要功能模塊都了解和實(shí)現(xiàn)一遍。這會是一段很長的歷程,但也會很有意思 —— 作為前端領(lǐng)域的經(jīng)典之作,jQuery 里有著太多奇思妙想,如果能夠深入理解它,對于我們穩(wěn)固js基礎(chǔ)、提升前端大法技能來說大有裨益。1. 免 new 實(shí)現(xiàn)我們在使用很多插件的時(shí)候,都需要使用 new XXX() 的寫法
從本篇開始會陪大家一起從零開始走一遍 jQuery 的奇妙旅途,在整個(gè)系列的實(shí)踐中,我們會把 jQuery 的主要功能模塊都了解和實(shí)現(xiàn)一遍。
這會是一段很長的歷程,但也會很有意思 —— 作為前端領(lǐng)域的經(jīng)典之作,jQuery 里有著太多奇思妙想,如果能夠深入理解它,對于我們穩(wěn)固js基礎(chǔ)、提升前端大法技能來說大有裨益。
1. 免 new 實(shí)現(xiàn)
我們在使用很多插件的時(shí)候,都需要使用 new XXX() 的寫法來實(shí)例化一個(gè)引用:
var list = new Slip(document.getElementById('slip'), { //options });
jQuery 同樣作為一個(gè)面向?qū)ο蟮墓ぞ邘?,在我們?chuàng)建一個(gè)實(shí)例時(shí)卻無需使用 new 語法,節(jié)省了一些代碼量:
var $div = $('div'); //不需要如下寫法: //var $div = new $('div');
這種便捷的形式依賴了工廠模式,其實(shí)現(xiàn)非常簡單,把 new 封裝在庫內(nèi)即可,讓每次調(diào)用 jQuery() 時(shí)自行在內(nèi)部進(jìn)行一次實(shí)例化:
(function() { var _jQuery = window.jQuery, _$ = window.$; var version = "0.0.1", jQuery = function(selector) { console.log(document.querySelector(selector)) }; jQuery.prototype = { jquery: version, constructor: jQuery }; window.$ = window.jQuery = function(selector) { return new jQuery(selector); //notice here~ }; })();
留意這里我們走的 IIFE 形式,讓 jQuery 代碼庫形成自己的作用域,避免污染外部變量。
于是乎以上就是咱寫的第一個(gè) JQ 雛形,簡單跑一下:
<div></div> <script> var $div = $('div'); //<div></div> console.log($div.jquery); //0.0.1 </script>
別忘了后續(xù)我們還希望能通過 $.extend / $.fn.extend 來擴(kuò)展 JQ 的靜態(tài)方法和原型方法,我們把出口方法抽出來增加這個(gè) extend 的API:
function Factory(selector){ //抽出構(gòu)造函數(shù) return new jQuery(selector); } Factory.fn = jQuery.prototype; Factory.extend = Factory.fn.extend = function(){ console.log(this) }; window.$ = window.jQuery = Factory;
這樣我們也能直接通過 $.fn.jquery 來獲取當(dāng)前 JQ 版本號了。
如果希望可以通過 $.prototype 直接訪問 jQuery 的原型對象,再修改下這句代碼即可:
Factory.prototype = Factory.fn = jQuery.prototype;
2. 寫法優(yōu)化
事實(shí)上我們不太喜歡再寫多一個(gè)冗余的 Factory 構(gòu)造函數(shù)來作為 window.jQuery 的引用,也不喜歡(在模塊內(nèi)部)使用 Factory.extend() 來擴(kuò)展 JQ,它聽起來和 JQ 沒有半毛錢關(guān)系。
如果可以,直接把 jQuery 方法作為接口輸出,且在模塊內(nèi)部能以 jQuery.extend() 的形式來調(diào)用擴(kuò)展接口,這樣的形式更佳。
也就是說我們希望代碼應(yīng)該是這樣寫的:
jQuery.extend = jQuery.fn.extend = function(){ console.log(this) }; window.jQuery = window.$ = jQuery;
“直接把 jQuery 方法作為接口輸出”意味著我們要把工廠模式挪入 jQuery 方法中,顯然我們不能這樣改:
var version = "0.0.1", jQuery = function (selector) { return new jQuery(selector); };
這樣死循環(huán)了,調(diào)用棧會直接爆掉~
于是我們可以抽出一個(gè) init 方法來做初始化處理(比如簡單地注入檢索到的元素到JQ對象中),把 jQuery 方法中的內(nèi)容更改為 return new init(selector) 就行了。
保證兩個(gè)前提:
1. this 指向 jQuery 上下文
2. 其原型指向 jQuery 的原型
第一點(diǎn)很好理解,方便我們直接在 init 方法中通過對 this 的操作來處理 JQ 實(shí)例上下文,如:
//注入元素到 JQ 實(shí)例對象中 this[0] = elem; this.length = 1;
針對這點(diǎn),我們不妨把 init 作為 jQuery.prototype 的屬性方法來實(shí)現(xiàn):
var version = "0.0.1", jQuery = function(selector) { return new jQuery.fn.init() //修改點(diǎn)1 }; //方便我們使用 jQuery.fn 來引用 jQuery 原型對象 jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery }; //修改點(diǎn)2 —— init 作為原型方法,確保 this 指向正確 jQuery.fn.init = function( selector ) { if ( !selector ) { return; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } } }; jQuery.extend = jQuery.fn.extend = function(){ console.log(this) }; window.$ = window.jQuery = jQuery;
然而這時(shí)候存在一個(gè)問題 —— JQ實(shí)例對象無法訪問原型屬性/方法:
var $div = $('div'); console.log($div.jquery); //undefined
原因很簡單——我們還未實(shí)現(xiàn)上述提及的第二個(gè)前提——“init 原型指向 jQuery 的原型”
在 js 中,實(shí)例的內(nèi)部原型(__proto__)總是指向其構(gòu)造函數(shù)的原型(prototype),而經(jīng)過我們這番修改,JQ實(shí)例的構(gòu)造函數(shù)已經(jīng)變成了 jQuery.fn.init ,而其原型并非指向 jQuery 的原型,這導(dǎo)致 JQ 實(shí)例無法順其原型鏈爬取到 jQuery.prototype。
要實(shí)現(xiàn)這個(gè)條件,只需要做小小改動(dòng)——把 jQuery.fn.init 的原型指向 jQuery 的原型(jQuery.prototype / jQuery.fn)即可:
var init = jQuery.fn.init = function( selector ) { if ( !selector ) { return; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } } }; init.prototype = jQuery.fn; //修改點(diǎn)
這里貼下完整代碼:
var version = "0.0.1", jQuery = function(selector) { return new jQuery.fn.init(selector) }; jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery }; var init = jQuery.fn.init = function( selector, context, root ) { if ( !selector ) { return; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } } }; init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function(){ console.log(this) }; window.$ = window.jQuery = jQuery;
3. 鏈?zhǔn)綄懛▽?shí)現(xiàn)
JQ 里一個(gè)很大的亮點(diǎn)是,它支持鏈?zhǔn)綄懛?,調(diào)用起來非常方便:
$('div').removeClass('hide').css('width', '100px')
其實(shí)現(xiàn)其實(shí)非常簡單 —— 確保每個(gè)調(diào)用的方法尾部均返回自身即可,這里我們新增兩個(gè)實(shí)例方法做示例:
jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery, setBackground: function(){ this[0].style.background = 'yellow'; return this //返回自身引用 }, setColor: function(){ this[0].style.color = 'blue'; return this //返回自身引用 } }; var init = jQuery.fn.init = function( selector ) { if ( !selector ) { return this; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } return this; } };
鏈?zhǔn)秸{(diào)用:
<div>hello world</div> <script> var $div = $('div'); $div.setBackground().setColor(); </script>
效果如下,杠杠的:
4. 沖突處理
存在某些情況,用戶可能并不想拿 window.$ 甚至 window.jQuery 來引用 JQ 接口,或者已經(jīng)有其它庫使用了 window.$ 這個(gè)變量,如果我們粗暴地改變其引用肯定是不合理的。
so 我們來實(shí)現(xiàn) JQ 中沖突處理的靜態(tài)接口 jQuery.noConflict,這意味著在代碼段開始時(shí),就得先保存下當(dāng)前 window.$ 和 window.jQuery 兩個(gè)變量:
(function(){ var _jQuery = window.jQuery, _$ = window.$; //var version = "0.0.1"...... })()
然后是實(shí)現(xiàn) noConflict 方法,退耕還林,把保存的變量吐回去即可:
(function(){ var _jQuery = window.jQuery, _$ = window.$; //var version = "0.0.1"...... jQuery.noConflict = function( deep ) { //確保window.$沒有再次被改寫 if ( window.$ === jQuery ) { window.$ = _$; } //確保window.jQuery沒有再次被改寫 if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; //返回 jQuery 接口引用 }; window.jQuery = window.$ = jQuery; })();
deep 參數(shù)類型為 Boolean,若為真,表示要求連window.jQuery 變量都需要吐回去。
留意在尾部我們返回了 jQuery 的接口引用,這意味著我們可以以
var $$$ = jQuery.noConflict()
的形式來把它賦予新的變量。
接著在外部運(yùn)行如下代碼:
<head> <meta charset="UTF-8"> <title>DIY A JQ</title> <script> $ = 'old $'; jQuery = 'old JQ' </script> <script src="jQuery.js"></script> </head> <body> <div>hello world</div> <script> var $div = $('div'); $div.setBackground().setColor(); var $$$ = $.noConflict(true); console.log($); console.log(jQuery); console.log($$$); </script>