隨着 JavaScript 自己的完善,愈來愈多的人開始喜歡使用原生 JavaScript 開發代替各類庫,其中很多人發出了用原生 JavaScript 代替 jQuery 的聲音。這並非什麼壞事,但也不見得就是好事。若是你真的想把 jQuery 從前端依賴庫中移除掉,我建議你慎重考慮。javascript
首先 jQuery 是一個第三方庫。庫存在的價值之一在於它能極大地簡化開發。通常狀況下,第三方庫都是由原生語言特性和基礎 API 庫實現的。所以,理論上來講,任何庫第三方庫都是能夠用原生語言特性代替的,問題在於是否值得?前端
引用一段 jQuery 官網的話:java
jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation, and Ajax much simpler with an easy-to-use API that works across a multitude of browsers.git
這一段話很謙虛的介紹了 jQuery 在處理 DOM 和跨瀏覽器方面作出的貢獻。而事實上,這也正是咱們選用 jQuery 的主要緣由,並順帶使用了它帶來的一些工具,好比數組工具,Deferred 等。github
對於我來講,最經常使用的功能包括編程
上面提到的全部功能都能用原生代碼來實現。從本質上來講,jQuery 就是用來代替原生實現,以達到減小代碼,加強可讀性的目的的——因此,究竟是用 jQuery 代替原生代碼,仍是用原生代碼代替 jQuery?這個前後因果關係能否搞明白?數組
我看到說用 querySelectorAll()
代替 $()
的時候,不由在想,用 jQuery 一個字符就能解決的,爲何要寫十六個字符?大部分瀏覽器是有實現 $()
,可是寫原生代碼的時候你會考慮 $()
的瀏覽器兼容性嗎?jQuery 已經考慮了!promise
我看到一大堆建立 DOM 結構的原生 JavaScript 代碼的時候,不由在想,用 jQuery 只須要一個方法鏈就解決了,我甚至能夠用和 HTML 結構相似的代碼(包含縮進),好比瀏覽器
// 建立一個 ul 列表並加在 #container 中 $("<ul>").append( $("<li>").append( $("<a>").attr("href", "#").text("first")), $("<li>").append( $("<a>").attr("href", "#").text("second")), $("<li>").append( $("<a>").attr("href", "#").text("third")) ).appendTo($("#container"));
這段代碼用 document.createElement()
來實現徹底沒有問題,只不過代碼量要大得多,並且會出現大量重複(或相似)的代碼。固然是能夠把這些重複代碼提取出來寫成函數的……不過 jQuery 已經作了。數據結構
注,拼 HTML 的方法實在弱爆了,既容易出錯,又不易閱讀。若是有 ES6 的字符串模板以後,用它來寫 HTML 也是個不錯的主意。
就 DOM 操做這一部分來講,jQuery 仍然是一個很是好用的工具。這是 jQuery 替代了原生 JavaScript,之前如此,如今仍然如此。
jQuery 2006 年被髮明出來的時候,尚未 ES5(2011年6月發佈)。即便在 ES5 發佈以後很長一段時間裏,也不是全部瀏覽器都支持。所以在這一時期,除 DOM 操做外,jQuery 的巨大貢獻在於解決跨瀏覽器的問題,以及提供了方便的對象和數組操做工具,好比 each()
、index()
和 filter
等。
現在 ECMAScript 剛剛發佈了 2017 的標準,瀏覽器標準混亂的問題也已經獲得了很好的解決,前端界還出現了 Babel 這樣的轉譯工具和 TypeScript 之類的新語言。因此如今你們都儘可放心的使用各類新的語言特性,哪怕 ECMAScript 的相關標準還在制定中。在這一時期,jQuery 提供的大量工具方法都已經有了原生替代品——在使用上差異不大的狀況下,確實寧願用原生實現。
事實上,jQuery 也在極盡量地採用原生實現,以提升執行效率。jQuery 沒有放棄這些已有原生實現的工具函數/方法,主要仍是由於向下兼容,以及一如既往的提供瀏覽器兼容性——畢竟不是每個使用 jQuery 的開發者都會使用轉譯工具。
那麼,對於 JavaScript 開發者而言,jQuery 確實有不少工具方法能夠被原生 JavaScript 函數/方法替代。好比
$.parseJSON()
能夠用 JSON.parse()
替代,並且 JSON.stringify()
還彌補了 jQuery 沒有 $.toJSON()
的不足;$.extend()
的部分功能能夠由 Object.assign()
替代`$.fn
的一些數據處理工具方法,好比 each()
、index()
等均可以用 Array.prototype
中相應的工具方法替代,好比 forEach()
、indexOf()
等。$.Deferred()
和 jQuery Promise 在某些狀況下能夠用原生 Promise 替代。它們在沒有 ES6 以前也算是個不錯的 Promise 實現。......
$.fn
就是jQuery.prototype
,也就是 jQuery 對象的原型。因此在其上定義的方法就是 jQuery 對象的方法。
這些工具方法在原生 JavaScript 中已經逐漸補充完善,但它們仍然只是在某些狀況下能夠被替代……由於 jQuery 對象是一個特有的數據結構,針對 jQuery 自身建立的工具方法在做用於 jQuery 對象的時候會有一些針對性的實現——既然 DOM 操做仍然不能把 jQuery 拋開,那這些方法也就不可能被徹底替換掉。
有時候須要用 jQuery,有時候不須要用,該如何分辨?
jQuery 的優點在於它的 DOM 處理、Ajax,以及跨瀏覽器。若是在項目中引入 jQuery,多半是由於對這些功能的需求。而對於不操做 DOM,也不須要考慮跨瀏覽器(好比用於轉譯工具)的部分,則考慮儘量的用原生 JavaScript 實現。
如此以來,必定會存在 jQuery 和原生 JavaScript 的交集,那麼,就不得不說說須要注意的地方。
首先要注意的一點,就是 jQuery 對象是一個僞數組,它是對原生數組或僞數組(好比 DOM 節點列表)的封裝。
若是要得到某個元素,能夠用 []
運算符或 get(index)
方法;若是要得到包含全部元素的數組,可使用 toArray()
方法,或者經過 ES6 中引入的 Array.from()
來轉換。
// 將普通數組轉換成 jQuery 對象 const jo = $([1, 2, 3]); jo instanceof jQuery; // true Array.isArray(jo); // false // 從 jQuery 對象獲取元素值 const a1 = jo[0]; // 1 const a2 = jo.get(1); // 2 // 將 jQuery 對象轉換成普通數組 const arr1 = jo.toArray(); // [1, 2, 3] Array.isArray(arr1); // true const arr2 = Array.from(jo); // [1, 2, 3] Array.isArray(arr2); // true
each/map
和 forEach/map
回調函數的參數順序jQuery 定義在 $.fn
上的 each()
和 map()
方法與定義在 Array.prototype
上的原生方法 forEach()
和 map()
對應,它們的參數都是回調函數,但它們的回調函數定義有一些細節上的差異。
$.fn.each()
的回調定義以下:
Function(Integer index, Element element )
回調的第一個參數是數組元素所在的位置(序號,從 0
開始),第二個參數是元素自己。
而 Array.prototype.forEach()
的回調定義是
Function(currentValue, index, array)
回調的第一個參數是數組元素自己,第二個參數纔是元素全部的位置(序號)。並且這個回調有第三個參數,即整個數組的引用。
請特別注意這兩個回調定義的第一個參數和第二個參數,所表示的意義正好交換,這在混用 jQuery 和原生代碼的時候很容易發生失誤。
對於 $.fn.map()
和 Array.prototype.map()
的回調也是如此,並且因爲這兩個方法同名,發生失誤的機率會更大。
each()/map()
中的 this
$.fn.each()
和 $.fn.map()
回調中常常會使用 this
,這個 this
指向的就是當前數組元素。正是由於有這個便利,因此 jQuery 在定義回請販時候沒有把元素自己做爲第一個參數,而是把序號做爲第一個參數。
不過 ES6 帶來了箭頭函數。箭頭函數最多見的做用就是用於回調。箭頭函數中的 this
與箭頭函數定義的上下文相關,而不像普通函數中的 this
是與調用者相關。
如今問題來了,若是把箭頭函數做爲 $.fn.each()
或 $.fn.map()
的回調,須要特別注意 this
的使用——箭頭函數中的 this
再也不是元素自己。鑑於這個問題,建議若非必要,仍然使用函數表達式做爲 $.fn.each()
和 $.fn.map()
的回調,以保持原有的 jQuery 編程習慣。實在須要使用箭頭函數來引用上下文 this
的狀況下,千萬記得用其回調定義的第二個參數做爲元素引用,而不是 this
。
// 將全部輸入控制的 name 設置爲其 id $(":input").each((index, input) => { // const $input = $(this) 這是錯誤的!!! const $input = $(input); $input.prop("name", $input.prop("id")); });
$.fn.map()
返回的並非數組與 Array.prototype.map()
不一樣,$.fn.map()
返回的不是數組,而是 jQuery 對象,是僞數組。若是須要獲得原生數組,能夠採用 toArray()
或 Array.from()
輸出。
const codes = $([97, 98, 99]); const chars = codes.map(function() { return String.fromCharCode(this); }); // ["a", "b", "c"] chars instanceof jQuery; // true Array.isArray(chars); // false const chars2 = chars.toArray(); Array.isArray(chars2); // true
jQuery 是經過 $.Deferred()
來實現的 Promise 功能。在 ES6 之前,若是引用了 jQuery,基本上不須要再專門引用一個 Promise 庫,jQuery 已經實現了 Promise 的基本功能。
不過 jQuery Promise 雖然實現了 then()
,卻沒有實現 catch()
,因此它不能兼容原生的 Promise,不過用於 co 或者 ES2017 的 async/await
毫無壓力。
// 模擬異步操做 function mock(value, ms = 200) { const d = $.Deferred(); setTimeout(() => { d.resolve(value); }, ms); return d.promise(); }
// co 實現 co(function* () { const r1 = yield mock(["first"]); const r2 = yield mock([...r1, "second"]); const r3 = yield mock([...r2, "third"]); console.log(r1, r2, r3); }); // ['first'] // ['first', 'second'] // ['first', 'second', 'third']
// async/await 實現,須要 Chrome 55 以上版本測試 (async () => { const r1 = await mock(["first"]); const r2 = await mock([...r1, "second"]); const r3 = await mock([...r2, "third"]); console.log(r1, r2, r3); })(); // ['first'] // ['first', 'second'] // ['first', 'second', 'third']
雖然 jQuery 的 Promise 沒有 catch()
,可是提供了 fail
事件處理,這個事件在 Deferred reject()
的時候觸發。相應的還有 done
事件,在 Deferred resovle()
的時候觸發,以及 always
事件,不論什麼狀況都會觸發。
與一次性的 then()
不一樣,事件能夠註冊多個處理函數,在事件觸發的時候,相應的處理函數會依次執行。另外,事件不具有傳遞性,因此 fail()
不能在寫在 then()
鏈的最後。
總的來講,在大量操做 DOM 的前端代碼中使用 jQuery 能夠帶來極大的便利,也使 DOM 操做的相關代碼更易讀。另外一方面,原生 JavaScript 帶來的新特性確實能夠替代 jQuery 的部分工具函數/方法,以下降項目對 jQuery 的依賴程序。
jQuery 和原生 JavaScript 應該是共生關係,而不是互斥關係。應該在合適的時候選用合適的方法,而不是那麼絕對的非要用誰代替誰。