随着 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 应该是共生关系,而不是互斥关系。应该在合适的时候选用合适的方法,而不是那么绝对的非要用谁代替谁。