爲什麼要用原生 JavaScript 代替 jQuery

隨着 JavaScript 本身的完善,越來越多的人開始喜歡使用原生 JavaScript 開發代替各種庫,其中不少人發出了用原生 JavaScript 代替 jQuery 的聲音。這並不是什麼壞事,但也不見得就是好事。如果你真的想把 jQuery 從前端依賴庫中移除掉,我建議你慎重考慮。

首先 jQuery 是一個第三方庫。庫存在的價值之一在於它能極大地簡化開發。一般情況下,第三方庫都是由原生語言特性和基礎 API 庫實現的。因此,理論上來說,任何庫第三方庫都是可以用原生語言特性代替的,問題在於是否值得?

jQuery 的作用

引用一段 jQuery 官網的話:

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.

這一段話很謙虛的介紹了 jQuery 在處理 DOM 和跨瀏覽器方面做出的貢獻。而事實上,這也正是我們選用 jQuery 的主要原因,並順帶使用了它帶來的一些工具,比如數組工具,Deferred 等。

對於我來說,最常用的功能包括

  • 在 DOM 樹中進行查詢

  • 修改 DOM 樹及 DOM 相關操作

  • 事件處理

  • Ajax

  • Deferred 和 Promise

  • 對象和數組處理

  • 還有一個一直在用卻很難在列清單時想到的——跨瀏覽器

到底是誰在替代誰?

上面提到的所有功能都能用原生代碼來實現。從本質上來說,jQuery 就是用來代替原生實現,以達到減少代碼,增強可讀性的目的的——所以,到底是用 jQuery 代替原生代碼,還是用原生代碼代替 jQuery?這個先後因果關係可否搞明白?

我看到說用 querySelectorAll() 代替 $() 的時候,不禁在想,用 jQuery 一個字符就能解決的,爲什麼要寫十六個字符?大部分瀏覽器是有實現 $(),但是寫原生代碼的時候你會考慮 $() 的瀏覽器兼容性嗎?jQuery 已經考慮了!

我看到一大堆創建 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 工具函數

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 與原生 JavaScript 的結合

有時候需要用 jQuery,有時候不需要用,該如何分辨?

jQuery 的優勢在於它的 DOM 處理、Ajax,以及跨瀏覽器。如果在項目中引入 jQuery,多半是因爲對這些功能的需求。而對於不操作 DOM,也不需要考慮跨瀏覽器(比如用於轉譯工具)的部分,則考慮儘可能的用原生 JavaScript 實現。

如此以來,一定會存在 jQuery 和原生 JavaScript 的交集,那麼,就不得不說說需要注意的地方。

jQuery 對象實現了部分數組功能的僞數組

首先要注意的一點,就是 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/mapforEach/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 Promise

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 應該是共生關係,而不是互斥關係。應該在合適的時候選用合適的方法,而不是那麼絕對的非要用誰代替誰。

本文分享自微信公衆號 - 邊城客棧(fancyidea-full)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章