從本篇開始會陪你們一塊兒從零開始走一遍 jQuery 的奇妙旅途,在整個系列的實踐中,咱們會把 jQuery 的主要功能模塊都瞭解和實現一遍。css
這會是一段很長的歷程,但也會頗有意思 —— 做爲前端領域的經典之做,jQuery 裏有着太多奇思妙想,若是可以深刻理解它,對於咱們穩固js基礎、提高前端大法技能來講大有裨益。前端
另外,本系列的相關代碼都可以從 個人github 上獲取到。jquery
1. 免 new 實現git
咱們在使用不少插件的時候,都須要使用 new XXX() 的寫法來實例化一個引用:github
var list = new Slip(document.getElementById('slip'), { //options });
jQuery 一樣做爲一個面向對象的工具庫,在咱們建立一個實例時卻無需使用 new 語法,節省了一些代碼量:api
var $div = $('div'); //不須要以下寫法: //var $div = new $('div');
這種便捷的形式依賴了工廠模式,其實現很是簡單,把 new 封裝在庫內便可,讓每次調用 jQuery() 時自行在內部進行一次實例化:ide
(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 代碼庫造成本身的做用域,避免污染外部變量。模塊化
因而乎以上就是咱寫的第一個 JQ 雛形,簡單跑一下:函數
<div></div> <script> var $div = $('div'); //<div></div> console.log($div.jquery); //0.0.1 </script>
別忘了後續咱們還但願能經過 $.extend / $.fn.extend 來擴展 JQ 的靜態方法和原型方法,咱們把出口方法抽出來增長這個 extend 的API:工具
function Factory(selector){ //抽出構造函數 return new jQuery(selector); } Factory.fn = jQuery.prototype; Factory.extend = Factory.fn.extend = function(){ console.log(this) }; window.$ = window.jQuery = Factory;
這樣咱們也能直接經過 $.fn.jquery 來獲取當前 JQ 版本號了。
若是但願能夠經過 $.prototype 直接訪問 jQuery 的原型對象,再修改下這句代碼便可:
Factory.prototype = Factory.fn = jQuery.prototype;
2. 寫法優化
事實上咱們不太喜歡再寫多一個冗餘的 Factory 構造函數來做爲 window.jQuery 的引用,也不喜歡(在模塊內部)使用 Factory.extend() 來擴展 JQ,它聽起來和 JQ 沒有半毛錢關係。
若是能夠,直接把 jQuery 方法做爲接口輸出,且在模塊內部能以 jQuery.extend() 的形式來調用擴展接口,這樣的形式更佳。
也就是說咱們但願代碼應該是這樣寫的:
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); };
這樣死循環了,調用棧會直接爆掉~
因而咱們能夠抽出一個 init 方法來作初始化處理(好比簡單地注入檢索到的元素到JQ對象中),把 jQuery 方法中的內容更改成 return new init(selector) 就好了。
保證兩個前提:
1. this 指向 jQuery 上下文 2. 其原型指向 jQuery 的原型
第一點很好理解,方便咱們直接在 init 方法中經過對 this 的操做來處理 JQ 實例上下文,如:
//注入元素到 JQ 實例對象中 this[0] = elem; this.length = 1;
針對這點,咱們不妨把 init 做爲 jQuery.prototype 的屬性方法來實現:
var version = "0.0.1", jQuery = function(selector) { return new jQuery.fn.init() //修改點1 }; //方便咱們使用 jQuery.fn 來引用 jQuery 原型對象 jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery }; //修改點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;
然而這時候存在一個問題 —— JQ實例對象沒法訪問原型屬性/方法:
var $div = $('div'); console.log($div.jquery); //undefined
緣由很簡單——咱們還未實現上述說起的第二個前提——「init 原型指向 jQuery 的原型」
在 js 中,實例的內部原型(__proto__)老是指向其構造函數的原型(prototype),而通過咱們這番修改,JQ實例的構造函數已經變成了 jQuery.fn.init ,而其原型並不是指向 jQuery 的原型,這致使 JQ 實例沒法順其原型鏈爬取到 jQuery.prototype。
要實現這個條件,只須要作小小改動——把 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; //修改點
這裏貼下完整代碼:
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. 鏈式寫法實現
JQ 裏一個很大的亮點是,它支持鏈式寫法,調用起來很是方便:
$('div').removeClass('hide').css('width', '100px')
其實現其實很是簡單 —— 確保每一個調用的方法尾部均返回自身便可,這裏咱們新增兩個實例方法作示例:
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; } };
鏈式調用:
<div>hello world</div> <script> var $div = $('div'); $div.setBackground().setColor(); </script>
效果以下,槓槓的:
4. 衝突處理
存在某些狀況,用戶可能並不想拿 window.$ 甚至 window.jQuery 來引用 JQ 接口,或者已經有其它庫使用了 window.$ 這個變量,若是咱們粗暴地改變其引用確定是不合理的。
so 咱們來實現 JQ 中衝突處理的靜態接口 jQuery.noConflict,這意味着在代碼段開始時,就得先保存下當前 window.$ 和 window.jQuery 兩個變量:
(function(){ var _jQuery = window.jQuery, _$ = window.$; //var version = "0.0.1"...... })()
而後是實現 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 參數類型爲 Boolean,若爲真,表示要求連window.jQuery 變量都須要吐回去。
留意在尾部咱們返回了 jQuery 的接口引用,這意味着咱們能夠以
var $$$ = jQuery.noConflict()
的形式來把它賦予新的變量。
接着在外部運行以下代碼:
<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>
輸出以下:
第一篇就寫到這裏,相關的代碼能夠從 個人github 上下載到。
下次咱們會試着實現模塊化的寫法,並與時俱進,改用 ES6解構賦值語法 + Rollup 來進行打包以減小可能存在的冗餘代碼段。
共勉~