超越-jQuery-一-

90 阅读1小时+

超越 jQuery(一)

原文:Beyond jQuery

协议:CC BY-NC-SA 4.0

一、jQuery 的压倒性魔力

多年来,业余和专业 web 开发人员都在使用 jQuery 来减轻将库或 web 应用推向市场的负担。从某种意义上说,jQuery 已经成为 web 开发不可或缺的一部分。即使在撰写本文时,jQuery 仍被绝大多数公共网站引用, 1 远远超过其他任何库。

许多开发人员似乎认为 jQuery 是一个默认需求。普遍的想法是:如果你在开发一个库或者一个 web 应用,你必须依赖 jQuery。jQuery 被视为这个神奇的黑匣子,可以解决 web 开发的所有难题。这是一个易于理解的框架,甚至允许新手快速整理他们的想法。

专业人士也倾向于对 jQuery 投入很多。毕竟这是我们新手时候用的。很舒服。我们理解。我们信任它。这些年来,它为我们提供了良好的服务。您不必(过多)考虑 DOM、浏览器错误或跨浏览器行为。jQuery 为我们解决了所有这些问题。。。不是吗?

不可否认,jQuery 确实有点神奇。事实上,它允许几乎任何技能水平的开发人员创建有用的东西。但是代价是什么呢?只考虑通过玫瑰色的 jQuery 透镜来理解 web。如果遇到 jQuery 没有正确抽象的底层行为怎么办?遇到 jQuery 的 bug 怎么办?如果您只是不能(被允许)使用 jQuery 怎么办?这类似于一个城市居民被扔进了西伯利亚的苔原。在这种情况下,你会害怕,迷失方向,准备不足。

尽管违背自己的意愿被带到异国他乡的可能性很小,但未来没有 jQuery 的可能性要大得多。如果您没有很好地掌握 DOM、web API 和 JavaScript,您最终会感觉有点像一个冷漠而困惑的都市人,试图在西伯利亚广袤无垠的陌生环境中生存。

Beyond jQuery 的一个目标是揭开这个看似无处不在的前端库的神秘面纱。摆脱对 jQuery 的盲目依赖的好处将变得显而易见。在本书的结尾,鉴于你对浏览器 API 和 JavaScript 的新理解,你将有能力作为一名 web 开发人员进一步成长。

本章探究了开发人员依赖 jQuery 的原因,以及他们继续依赖 jQuery 的原因。您将会看到为什么完全依赖一个单一的库会在您的知识中产生缺口,并阻止您作为开发人员的发展。我将讨论为什么对 web 和 JavaScript 的片面理解对开发人员来说是一个潜在的危险困境。有了这些知识,你就能更好地理解更多地依靠自己对基本面的扎实理解的好处。

为什么我们一直在使用 jQuery?

在探索我们应该如何(以及为什么)考虑从我们的工具箱中移除 jQuery 之前,我们应该首先理解 jQuery 为什么会存在。为什么这么多年来无数的 web 开发人员依赖这个库?为什么它一直是网站和应用的核心组件?为什么它继续如此普遍?为什么我们一直在使用 jQuery?我们都有自己的理由,而且确实有许多理由。最重要的是,jQuery 已经被证明提供了一个低门槛的入口。换句话说,即使是业余或偶尔的开发人员也发现,它允许他们几乎没有阻力地实现一个概念或想法。

简单

假设您正在阅读这本书,那么您已经对 jQuery 有所了解。你可能已经在某个项目中使用过它,不管它有多大。因此,让我们来探究为什么这个库对于所有技能水平的开发人员来说都如此容易使用。

最重要的是,jQuery 的 API 是直观的。想给一个元素添加一个 CSS 类?就用addClass()的方法。需要发送帖子请求吗?就用post()的方法。隐藏一个元素就像将它作为参数传递给 jQuery 的hide()方法一样简单。

jQuery 的魅力在它极其简单的 API 中显而易见。它允许那些之前对浏览器或 JavaScript 知之甚少的人创建一些有趣且有用的东西。这对那些只涉足 web 开发的人来说非常有吸引力,甚至可以说是最合适的。反过来,jQuery 的简单性对于专业的 web 开发人员来说也是一个潜在的危险。如果你还不相信这是真的,我们将在后面更深入地探讨这个理论。

社区

关于堆栈溢出(在撰写本书时),120 万个问题被标记为 JavaScript 问题,75 万个被标记为 jQuery 问题。jQuery 是栈溢出中第六受欢迎的标签。下一个最受欢迎的前端库是 AngularJS in a distant 21st place,它只有 20 万个标记问题。有趣的是,200,000 个问题被标记为 jQuery 而不是 JavaScript。在许多情况下,jQuery 并不被视为 JavaScript 库。事实上,它被视为 JavaScript 的替代品。一种无需处理底层语言或 API 就能解决浏览器问题的方法。虽然有些人可能不清楚 jQuery 与 JavaScript 的关系,但是 jQuery 并不缺少愿意并准备好为这个库提供建议的开发人员。在栈溢出的 750,000 个 jQuery 标记的问题中,550,000 个(74%)包含至少一个投票赞成的答案。

截至 2016 年年中,jQuery 仍然是公共网站中使用最多的 JavaScript 库。事实上,70%的公共网站都在某种程度上依赖于 jQuery。第二个最受欢迎的库是 Bootstrap,仅在 13%的公共网站中使用。有了这个令人印象深刻的市场份额,当然有相当一部分用户对这个主题有一些工作知识。

除了 Stack Overflow 上的 jQuery 标签和大量致力于为投资于该技术的人提供建议的不同论坛和网站之外,jQuery 的网站还有自己的活跃用户论坛。帮助是很容易找到的,你可能遇到的任何问题都可能已经得到解决并经过长时间的讨论。大型成熟社区的现实是依赖任何软件库的一个吸引人的理由。

习惯

针对 jQuery 初学者的大量示例、博客文章和论坛是那些 web 开发新手选择这个库来帮助他们的项目的原因之一。但是经验丰富的开发人员呢?为什么他们继续在项目中使用 jQuery?一个完美的、有经验的开发者曾经是一个业余爱好者。作为业余爱好者,他们很可能已经接受了 jQuery。现在,有了多个项目,jQuery 已经证明了自己。而且就算已经注意到了库中的一些瑕疵,也很好理解。

对于经验丰富的开发人员来说,jQuery 足够一致和可靠。它已经成为发展过程的一部分。一个强大的社区也是有经验的开发人员意识到的一个好处,这也是坚持使用这样一个值得信赖的工具的另一个原因。

jQuery 是我们编写任何东西的先决条件。我们无意识地引入它,部分是因为我们已经习惯性地接受了这样的训练。我们被训练成认为这是每个 web 应用的重要组成部分。习惯很难打破,尤其是那些已经产生积极结果的习惯。

软件开发可能会令人紧张和沮丧。一个典型的开发人员每天都要与无数的变量搏斗。看起来,没有什么问题是容易解决的。工具、过程或结果的一致性和可预测性是非常需要和罕见的。你能责怪 web 开发社区这么长时间依赖于像 jQuery 这样一致可靠的工具吗?

高雅

你听过有人说 DOM 很丑或者 JavaScript 有缺陷并且布满了定时炸弹吗?也许你自己也这样认为。虽然美在很大程度上是主观的,但这似乎是一个令人惊讶的普遍想法,尤其是在那些在 web 开发生涯后期继续使用 jQuery 的经验丰富的开发人员中。

原生浏览器 API 和 JavaScript 经常被认为不够优雅,这显然促使开发人员使用这个库。其思想是,没有 jQuery 的帮助,简单的问题很难解决。不相信我?问一些和你一起工作的开发人员。他们为什么使用 jQuery?期待听到创建优雅简洁的代码来应对常见问题是多么简单。正如本节前面所讨论的,API 本身是直观和优雅的。

但是 jQuery 的优雅不仅仅是一个可预测的 API。围绕 API 设计的可用性考虑进一步证明了这种优雅的说法。

以方法链接为例,它允许开发人员非常容易地将同一元素上的许多操作联系在一起,而没有重复或临时变量创建的负担。假设您想要选择一组元素,然后向所有元素添加一个类,最后向初始元素集的一个更具体的子集添加一个类。通过利用 jQuery API 提供的优雅的方法链,可以非常容易地完成所有这些工作。清单 1-1 通过向包含“字母表”类的所有元素添加一类“下划线”来演示这一点。然后,它只选择包含一类“元音”的“alphabet”元素的子元素,最后用一类“bold”对它们进行注释,同时隐藏任何本身也包含一类“a”的“元音”元素的子元素:

1  $('.alphabet').addClass('underline')
2     .find('.vowels').addClass('bold')
3        .find('.a').hide();
Listing 1-1.
jQuery method chaining

我发现许多开发人员倾向于纠结于 JavaScript 中的异步操作,比如 ajax 请求。发出这些请求并不困难,但是考虑到调用的异步性质,处理结果对一些人来说是令人沮丧的。jQuery 通过将响应处理函数与发送底层请求的函数调用完美地绑定在一起,稍微简化了这一点。清单 1-2 发送一个检索用户名的请求,提供对服务器响应结果的简单访问。

1  $.get('name/123', function(theName) {
2     console.log(theName);
3  });
Listing 1-2.jQuery GET request

Note

除非开发人员工具已打开,否则控制台对象在 Internet Explorer 9 和更低版本中不可用。此外,前面的示例不处理错误响应,只处理成功。

有了这些“美”,我们很容易忘记努力的其他属性,比如表现。在前面的例子中是否有潜在的效率地雷?是的,但是这些可能一开始很难识别。我将在后面的章节中详细讨论这一点。

害怕

jQuery 让一切变得更简单——web 开发很难。没有一些帮助,你无法开发一个可靠的 web 应用或库。如果没有 jQuery,很难保证你的应用在所有浏览器中都可以正常运行。web API 实现在不同的浏览器之间有很大的不同。反正你需要的所有好的插件都依赖于 jQuery。这些都是盲目依赖 jQuery 的常见借口,都是基于恐惧。由于对未知的恐惧,我们都依赖 jQuery。我们将 DOM 视为一个神秘且不可预测的黑匣子,布满了严重的错误。我们担心跨浏览器的实现差异。

jQuery 的创始人 John Resig 早在 2009 年就有一个著名的结论“DOM 一团糟”。 3 在 web 历史的那一刻,ie 6 和 7 几乎占据了浏览器市场的 60%。 4 考虑到这一点,很难反驳雷西格先生当时的说法。DOM 确实是一只可怕而多变的野兽,当时最流行的浏览器都有非常糟糕和有限的内置工具。如果我们把时间追溯到 2006 年 8 月,也就是 jQuery 创建和首次发布的时候,会怎么样呢?当时,Internet Explorer 的最新版本是版本 6。令人难以置信的是,IE6(及更老版本)占所有浏览器使用量的 83%。在这一点上,web API 非常不成熟,浏览器的稳定性远低于我们在当前时代的预期,而且当时的浏览器对标准的遵从也不一致。

除了不成熟的开发工具、不同的 web API 实现和不直观的 DOM 之外,浏览器肯定会有问题。与任何其他复杂的代码包类似,浏览器也不能幸免于错误。jQuery 在历史上承诺了广泛的浏览器漏洞解决方案。许多年来,在标准遵守和质量控制方面,网络似乎类似于蛮荒的西部。不难理解为什么一个旨在规范浏览器的库如此受欢迎。再也不用担心跨浏览器支持了。甚至不用担心跨浏览器测试。jQuery 将为您完成所有繁重的工作,因此您可以完全专注于开发有趣且有用的 web 应用和库。还是可以?虽然 jQuery 有望将您从浏览器中的所有问题和复杂性中解放出来,但现实却有所不同。

拐杖只是暂时的

JavaScript 库通常是有用的工具。它们有助于您构建一个有用且可靠的 web 应用,或者甚至是另一个库。它们节省你的时间和按键。它们充当你、你的代码和浏览器之间的缓冲,填补空白并规范行为。从另一个意义上来说,这些库可以起到拐杖的作用。他们帮助没有经验和没有受过教育的开发人员,实际上没有教给他们任何关于底层复杂性的东西。尽管这本书的基调有时可能会暗示不是这样,但像 jQuery 这样的库本质上并不坏。只有当你的学习没有超越图书馆时,它们才是限制性的。

jQuery 总是能节省你的时间吗?它总是让你的 web 开发体验变得更容易吗?与核心库或其插件相关的约定是直观的吗?它真的解决了你所有的问题吗,或者它也许制造了一些新的问题?您是否花时间思考过该库附带的语法和约定?有没有更好的解决方案,或者 jQuery 真的修补了开发人员经常陷入的所有可用性漏洞?大多数情况下,jQuery 的 API 赏心悦目,非常直观。当然,这并不总是正确的。图书馆中肯定有令人不愉快的部分。让我们举一个 jQuery 的优雅和必要性的例子。

jQuery 并不能完全保护你免受浏览器的困扰。这不是任何图书馆的现实目标。除此之外,jQuery 只是一个库,一个工具,一个助手。它并不意味着取代整个浏览器栈。有些问题甚至最好用 CSS 或静态 HTML 来解决。但是对于使用 jQuery 的开发人员来说,这是与浏览器交互的唯一方式。对于不知情的开发人员来说,使用 jQuery 的 API 编写最少的 HTML 并对标记进行任何调整都是完全合理的。或者,使用 jQuery 生成所有标记可能会更容易。您可以使用 jQuery 创建元素,然后轻松地将它们插入到页面中。

不在 CSS 文件中声明样式,倾向于使用$(element).css('fontWeight', 'bold')。虽然非常方便,但这是一种非常不可维护的生成内联样式的方法。对于新的开发人员来说,关注点分离的重要性可能不是很明显。jQuery 神奇的无所不包的 API 让我们很容易忽略可用的原生工具。当你盲目地依赖一个整体的抽象时,HTML、CSS 和 JavaScript 的适当角色并不总是在等式中出现。这个库不仅仅是一些人的工具,它是工具。它是 web 开发的全部和最终目标。您将会看到为什么这是一种危险的思路,尤其是对于专业的和有抱负的开发人员。

事实上,jQuery 是许多人的拐杖。它没有融合到浏览器中,只是作为一个补充。经验丰富、知识渊博的开发人员实际上可能更喜欢使用 jQuery,这当然没有错。但对其他人来说,它只是一个道具。那些刚接触 web 开发的人通常会拿起拐杖,蹒跚而行一段时间。但最终,拐杖从他们身下拔出,他们倒下了。

你是机械师,不是司机

关于堆栈溢出的一个流行问题是“在学习 jQuery 之前学习 JavaScript 是个好主意吗?”这个问题的一个特殊答案提供了一些奇怪的建议。这位投稿人在他的回答中继续说道“如果你打算使用一个像 jQuery 这样的框架,你真的不需要太专注于学习 HTML DOM 的细节。只需按照 jQuery 方式’做事,然后根据需要尽可能多地获取 HTML DOM。” 8 尽管这种想法在回答这个问题的其他人(也许是更有经验的开发人员)中并不常见。我自己也曾是一名没有经验的 web 开发新手,还记得这种思路是如何被那些进入基于浏览器的前端编码的混乱世界的人所接受的。

在我写的题为“你不需要 jQuery”的一系列博客文章中, 9 一位评论者提供了一个惊人的类比,概述了博客(和这本书)的目标之一。

“我试图指导我的同行的一件事是,你不能在浇注地基之前就在地基上竖起墙。事实上,你应该平整地面,铺设功能性管道(测试),然后在开始建造结构之前浇筑地基。这涉及到在为任何端点(浏览器)构建时理解你的工具的核心(HTML、CSS、JS),10Lawrence Francell)。

换句话说,对于一个稳定、持久的应用或库,您必须很好地理解您的工具是如何工作的。达不到这一点,你就不是开发者。事实上,你是一个图书馆集成者。

很难反驳评论者的合理建议,但是 jQuery 惊人的魔力有时似乎会蒙蔽我们。这并不意味着我们是“糟糕的”开发者。事实上,这甚至可能不是我们的错。我们注定要走阻力最小的路。这实际上是一个经过充分研究和记录的心理学理论,被称为“最小努力原则”(来自乔治·金斯利·齐夫的《人类行为和最小努力原则》(Addison-Wesley Press,1949)。

唱反调,也许我们可以用另一个例子来反驳前面引用的类比。我们大多数人可能每天都在开车。但是我们中有多少人能够诊断出典型内燃机的问题呢?有多少人会做除了换轮胎以外的事情?这些问题的答案可能是“很少”

我们真的需要成为称职的汽车修理工才能开车吗?不,当然不是。对我们许多人来说,驾驶不是一种职业。相反,这是我们所依赖的一种便利。我们没有时间去了解汽车的每一个细节,也不应该去了解。汽车的存在是为了简化我们的生活,节省我们的时间。汽车制造商不希望他们的客户是汽车修理工。出于显而易见的原因,他们设计产品时考虑的是普通人,以确保尽可能多的人可以使用他们的汽车。

我们可以把开车比作开浏览器吗?作为软件开发人员,我们真的需要理解网络的基础吗?正如你在上面的堆栈溢出回答中看到的,有些人可能会说不。但重要的是要明白我们不是驱动程序。我们是机械师和设计师,不是用户。

矮化生长

当你在没有 jQuery 和拐杖的情况下被推进一个新项目时会发生什么?如果您的能力停留在库的 API 的边缘,您的选择是有限的。你在这种抽象中投入太多,危及你超越它的能力。你真的希望你所有的项目都依赖于一段代码吗?短期来看,这似乎不是问题。展望未来,这条道路的可行性变得值得怀疑。

总的来说,软件开发和技术的前景是不断变化的。作为开发人员,我们不仅理解这一点,而且欣然接受。这使得我们的工作既有挑战性又有趣。使用 jQuery 或任何其他库都没有问题。但是通过使用这些作为拐杖,我们不再是软件开发者。我们是 jQuery 程序员。

作为一名新的开发人员,您的目标不一定要围绕敏捷性。在这个早期阶段学习基础知识是至关重要的。随着你的项目和职业的发展,通过适应你的环境——浏览器——你将自己放在一个更好的位置来做出好的决定。只有在您牢固掌握了基础知识并更好地理解了 web 开发的最基本形式之后,您才应该专注于选择和学习一种工具来加速您的开发过程。

这个建议并不是专门针对软件开发的。你还记得你第一次学数学的时候吗?你完成的所有练习都可以用计算器轻松解决。最有可能的是,你被严格禁止使用计算器(我知道我是)。为什么呢?计算器更快更准确。简单来说,在这个阶段,目标不是速度。对数学基础的透彻理解是最重要的。一旦你理解了计算器是如何执行这些任务的,你就可以在将来解决更复杂的问题时选择使用或不使用它。理解基本原理可以确保你不会被工具束缚住。

当我们的选择库逐渐变得过时,或者被人从我们身边拿走,我们盲目的依赖会阻止我们前进。这种不幸情况的例子比比皆是。在 JavaWorld 最近的一篇文章中,作者引用了“你应该使用 jQuery 的 6 个理由”原因是值得怀疑的,因为作者显然对浏览器栈缺乏基本的理解。这一点在诸如“jQuery 是 HTML5 的主要组成部分”这样的说法中尤其明显,它将 JavaScript 与文档标记规范混为一谈。这篇文章中另一个令人不安的引用是:“jQuery 页面加载速度更快。”作为开发人员,正是这种过度简化导致我们认为工作的复杂性是理所当然的。假装一个库就能驯服像浏览器这样的野兽,只会让我们陷入一场最终会失败的令人沮丧的斗争。

捷径的代价(真实故事)

接下来是一个真实的故事,一个真正的 web 开发人员走了真正的捷径(真的)。他只关注短期,关注让他的工作更容易。他主要关心的是取悦项目经理。学习基础知识对他来说是浪费时间。他的目标是尽可能快地写出代码并完成一长串功能。那个开发者就是我。

jQuery 让一切变得更简单。没有它,你无法开发一个可靠的 web 应用。如果没有 jQuery,很难保证你的应用在所有浏览器中都可以正常运行。不同浏览器之间的 DOM API 实现差别很大。反正你需要的所有好的插件都依赖于 jQuery。我相信所有这些借口,甚至更多。有些甚至一度是很好的借口。

一个新的方向,一个新的网络开发者

回到我 web 开发生涯的早期,我正从专门的服务器端工作转型过来。我被分配到 Jennings,一个基于网络的记者制作工具。我没有专业的 HTML、CSS 或 JavaScript 经验。退一步说,我缺乏前端技能。

团队中没有人对 web 开发感到满意。我们都是菜鸟,以前的后端开发人员徒劳地努力在这个新的环境中理解我们的知识。期限是严格的,目标是崇高的。看起来我们都需要一些帮助——也许是一种让我们的工作变得轻松一点的工具。没有时间学习。我们要写一个应用!

捷径和我自己发育不良的成长

我第一次接触 JavaScript 和 web 是通过 jQuery。事实上,我甚至懒得学习适当的 JavaScript。我不知道 web API 是什么样子,也不知道如何直接处理 DOM。jQuery 为我做了一切。当我后来在没有 jQuery 拐杖的情况下完成一个项目时,我知识上的这一巨大差距赶上了我。我被迫学习适当的 web 开发,并且我从未回头。

在 Jennings 之后,jQuery 是所有未来项目中的一个需求(对我来说)。这是必须的,因为我不知道任何其他方法来驯服浏览器。这在当时似乎并不罕见。事实上,它不是。在大多数应用和库中,jQuery 是一个预期的依赖项。我盲目的信仰并不是一个明显的障碍。

在某种程度上,当我搜索插件来解决项目中的常见问题时,这种盲目依赖的一些问题变得很明显。jQuery 本身是一个有用的库,但是它只解决核心的、底层的问题。如果你想支持更高级的特性,比如模态对话框,你需要自己写或者找一个已经解决了问题的插件。

自然,我专门寻找 jQuery 插件来填补项目中的漏洞。事实上,我回避任何不依赖于它的东西。我不相信任何不使用这个神奇盒子的插件。jQuery 解决了我所有的问题,让我跨浏览器开发变得很容易。我为什么要相信一个没有达到同样启蒙水平的开发人员的工作呢?

过了一会儿,很明显这些 jQuery 插件的质量低得惊人。现实是,jQuery 的低准入门槛是一把双刃剑。有时候很容易快速写出有用的东西。但是更容易的是快速编写不可维护的容易出错的代码!我发现很多插件都写得很差。我对 web 开发的新手知识使我很难整理和解决我在使用这些 jQuery 插件库时遇到的问题。挫败感袭来,我作为开发人员的基础开始出现裂缝。

但是写得很差的库中的错误和低效仅仅暴露了冰山一角。这些插件甚至 jQuery 核心中的抽象漏洞泛滥,我几乎无法理解。为什么我不能用 jQuery 触发在 jQuery 之外创建的自定义事件处理程序?jQuery 支持定制事件,为什么不能呢?这是我在从事一个既依赖 jQuery 又依赖 Prototype 的项目时遇到的一个具体问题,Prototype 是一个具有类似目标的替代 JavaScript web 框架。我天真地认为我可以使用 jQuery 轻松地触发与 Prototype 绑定的自定义事件处理程序——没那么幸运。

再以文件上传为例。有人会认为使用 jQuery 上传文件就像在请求中包含文件作为data一样简单。并非如此。如果这样做,jQuery 将尝试对文件进行 URL 编码。在令人沮丧的大量阅读和实验之后,我了解到必须将两个模糊的属性设置为false,以确保 jQuery 不会在请求发送之前试图修改文件。

开发人员盲目依赖这个库时还会遇到另一个问题:使用 jQuery 在旧浏览器中发送跨域请求是不直观的。当使用一个旨在消除 web API 差异并允许轻松管理旧浏览器的库时,这是一个令人惊讶的认识。我将在第九章中讨论所有这些以及更多内容。

jQuery 的属性处理实用函数在这个库的生命周期中发生了巨大的变化。让我们考虑一个常见的任务作为例子:确定复选框的状态。在早期版本的 jQuery 中,通过 jQuery 的正确方法是使用attr()方法。如果复选框被选中,对$(checkboxEl).attr('checked')的简单调用将返回true。否则,它将返回false。对于一个经验丰富的 JavaScript 开发人员来说,这本身就是一个奇怪的行为,但是我们将把这些细节留到第五章中。

对于专注于 jQuery 的开发人员来说,jQuery API 的这一部分变得更加糟糕。在 jQuery 的更高版本中,相同的调用将返回 checkbox 元素的checked属性的值(该值不会随着复选框的选中和取消选中而自然改变)。虽然这实际上是正确的行为,因为它正确地反映了元素的实际属性,但在重大更改后,我感到很困惑。由于对 jQuery 的过度依赖,我没有很好地掌握 HTML。我不明白为什么我后来不得不依赖 jQuery 的prop()方法来获取复选框的当前状态,尽管旧的行为或attr()方法在技术上是不正确的。

我掉进了一个陷阱,这是许多新的、偶然的和业余的 web 开发人员都会掉进的陷阱。如果我先花时间理解 JavaScript 和浏览器提供的 API,我会省去很多麻烦。事情的正确顺序是这样的:

  1. 学习 JavaScript。
  2. 学习浏览器的 API。
  3. 学习 jQuery(或任何其他跨项目可能需要的框架/库)。

许多人从第三点开始,把第一点和第二点推迟到更晚的时间(或者永远不要)。如果您不理解 jQuery 实际上为您做了什么,那么随着泄漏的抽象从木制品中出来,将会有许多令人沮丧的日子。如果你想有效地成长为一名 web 开发人员,这是一个你必须避免的陷阱——这个陷阱阻碍我作为 web 开发人员的职业生涯的时间比我希望的要长。

挑战:不允许 jQuery!

2012 年初,我开始更换 Widen Collective 中的 Java 小程序上传器, 12 Widen 的旗舰数字资产管理 SaaS 产品。在浏览器中处理 Java 变成了一场噩梦,我们渴望迁移到原生的 JavaScript/HTML 解决方案。我首先研究了 jQuery 文件上传(当时最流行的上传库, 13 ),但是由于启动和运行它需要大量的依赖项,以及缺乏内聚的文档,所以被推迟了。因此,在我的 web 开发生涯中,我第一次选择了一个非 jQuery 解决方案,一开始我有点迷茫。

我决定用这个库来替换我们的 Java applet uploader,这个库当时叫做 valums/file- uploader(由于它在 GitHub 上的位置)。它是独一无二的,因为它是完全独立的。起初我有点怀疑,因为我被训练对 jQuery 生态系统抱有很大的信心,但我对能够轻松集成插件感到惊喜。

然而,插件已经年久失修。它不再被积极维护,需要解决一些错误和调整功能,以使其为 Widen Collective 的生产做好准备。尽管所需的工作并不多,但由于我在 JavaScript、HTML 和 CSS 知识方面的巨大差距,我花了大量时间来解决这些问题。我将我的一些更改推回到一个分叉的 GitHub 存储库中。我的代码是草率的和有缺陷的,但是它是足够的。 14

我的努力显然被图书馆的创建者 Andrew Valums 注意到了,他问我是否有兴趣维护这个图书馆。尽管我在 jQuery 之外没有什么实践经验,但我抓住了这个机会并接受了。我现在是一个大型且非常流行的非 jQuery 插件的唯一维护者,该插件将被重新命名为 Fine Uploader。

当我在 2012 年年中接管大型跨浏览器文件上传库 Fine Uploader 的维护和开发时,我的第一反应是使用 jQuery 全部重写,因为那会让我的生活更轻松(我以为)。现有的用户社区非常反对将任何第三方依赖带入库中,所以我被迫使用原生 web API 和普通 JavaScript。 十五

最初,我的经验不足无疑减缓了 Fine Uploader 的发展。我被迫获得对核心概念的专家级理解。我编写了自己的小垫片来解释 web API 和 JavaScript 的跨浏览器差异。我花了大量时间阅读和实验。随着时间的推移,我成功地摆脱了对 jQuery 压迫性魔力的盲目依赖。我不需要 jQuery,你也不需要。

专注于实现,而不是魔术

jQuery 的魔力及其简化 web 应用开发的承诺非常诱人。但是我们已经讨论了如何以及为什么您可以通过首先了解您的环境而成为一名更强的开发人员。通过遵循正确的路线来学习你的交易:首先是 JavaScript、HTML、CSS 和 web API。以后再担心图书馆。

让我坦白地告诉你,对于一个以前一无所知的新手网站开发者来说,现在扮演一个聪明的“我什么都见过”的开发者,这确实是既有趣又超现实的。但是我可以非常自信地说,如果您更熟悉 web 开发的基础知识,那么您就可以更好地决定何时需要使用 jQuery,何时不需要。知识和经验给了你做出这个选择的自由,并用事实来证明它。你不会永久附属于任何图书馆。你有选择。

不要躲在工具后面——拥有自己的代码。成为 web 开发人员和教师,而不是 jQuery 开发人员和图书馆用户。说“我不再需要 jQuery 了”是一种解放。我自己能行!”而且是真心实意的。不要养成走捷径的习惯。开始走上一条你作为专业人士可以引以为豪的轨迹。不要把学习基础知识推迟到以后,因为以后永远不会发生。避免当你选择的库无法保护你免受浏览器攻击时的无助感。你不可能现实地期望在你的整个职业生涯中躲在抽象层的后面。基本面是推动你前进的基石,让你掌握自己的交易。

Footnotes 1

https://w3techs.com/technologies/history_overview/javascript_library/all/y

  2

http://w3techs.com/technologies/history_overview/javascript_library/all/y

  3

http://ejohn.org/blog/the-dom-is-a-mess/

  4

www.w3counter.com/globalstats.php?year=2009&month=1

  5

www.onestat.com/html/aboutus_pressbox44-mozilla-firefox-has-slightly-increased.html

  6

https://docs.google.com/document/d/1LPaPA30bLUB_publLIMF0RlhdnPx_ePXm7oW02iiT6o/preview?sle=true#heading=h.fumxprdxo2gn

  7

http://stackoverflow.com/questions/668642/is-it-a-good-idea-to-learn-javascript-before-learning-jquery

  8

http://stackoverflow.com/a/841292/486979

  9

http://blog.garstasio.com/you-dont-need-jquery/

  10

http://blog.garstasio.com/you-dont-need-jquery/why-not/#comment-1799026169

  11

www.javaworld.com/article/2078613/java-web-development/6-reasons-you-should-be-using-jquery.html

  12

www.widen.com/digital-asset-management-software/

  13

https://github.com/blueimp/jQuery-File-Upload

  14

https://github.com/FineUploader/fine-uploader/compare/82c8d5b0c383738ed84c771e90dbf202bd3acd68…55b3ca6e9f7a18fd3adc5ba7537124ae12b63e71

  15

https://github.com/FineUploader/fine-uploader/issues/326

二、你不再需要 jQuery 了

本章的主要目的是解释为什么像你这样的 web 开发人员在开发库或应用时应该或不应该使用 jQuery。例如,在选择基于浏览器的依赖项时,文件大小是一个常见的考虑因素。我将介绍这个属性的重要性,并确定它如何影响您使用这个库的决定。包括对文件大小参数的探索,jQuery 的有用性将被进一步分析。作为这一特殊探索工作的一部分,我将把开发人员选择使用 jQuery 的常见原因与同样的开发人员由于这一选择而可能遇到的问题进行对比。我甚至可能简要地调查和讨论其他可能用来代替 jQuery 的库,甚至推动它走向过时,尽管这将是有限的。第三方代码的焦点将集中在采用更小、更集中的库和垫片上。浏览器提供的本地功能的未来也将是讨论的焦点。

完成本章后,您将能够更好地决定 jQuery 是否应该成为您当前或未来项目的一部分。你对这样一个库的重要性的理解将变得清晰,许多常见的毫无价值的借口将被驳斥。你也将被赋予选择的权力。如果您确实希望在一个目标远大的复杂项目上获得一些帮助,jQuery 永远不是您唯一的选择。如果您真的决定放弃 jQuery,那么 web 开发的未来,就不断发展的原生浏览器工具而言,将会给你信心。本章标题中的“不再”一词有双重含义。您不再需要 jQuery,因为 web API 和 JavaScript 已经得到了充分的发展,可以省略包装器库,而采用更接近金属的方法。你不再需要 jQuery 了,因为读完这本书后,你作为一名 web 开发人员的信心和知识也会得到充分的发展。

需要与想要

需求和欲望之间的斗争并不局限于软件开发,但是在计划一个 web 项目的时候,这是一个需要特别注意的冲突。通常,当我们做出关于依赖、ide 和构建工具的决定时,我们的选择更侧重于想要而不是需要。为什么我们中的一些人选择 WebStorm 而不是 vim?当然,vim 为我们提供了开发全栈 web 应用所需的一切,但我们可能会更喜欢 WebStorm,因为它有着华丽的 UI 和出色的可用性和直观性。为什么不使用 Make 或 shell 脚本来代替 grunt 或 gulp?我们可以使用 Makefile 定义任务来自动化项目构建系统的各个方面,但是 grunt 提供了一组更直观的约定和集成,JavaScript 开发人员可以轻松掌握。

我们需要的往往被我们想要的所压倒。新的开发人员通常更有动力在每一个项目中,每一次都产生可见的进展,这是最重要的。新兴的程序员旨在证明自己,并在追求认可和自信的过程中,利用他们可以从工具中获得的任何帮助。我知道这是真的,因为我自己也曾经是一名新的开发人员,并且在我的许多同行身上观察到了同样的品质。作为一个更有经验的开发人员,我现在对我的工具集有了一个更简约的方法。我在其他一些人身上看到了同样的心态,但是很多人似乎继续把精力集中在制造代码和特性上。

有些人从熟练的理解和应用中得到满足。但大多数人似乎对采用新的尖端高级工具更感兴趣,这些工具有望比传统工具走得更远。接近金属的解决方案被认为是原始的、脆弱的和不必要的复杂。他们多年的存在被一种抽象所掩盖,这种抽象宣称比旧工具更强大,更容易使用。维护一套适度的工具对一些人来说是令人钦佩的,但通常不是目标。

将 jQuery 引入到项目中通常是一种需求或者一种没有根据的需要。它神奇的名声更多地来自于传说,而不是对需求与欲望的客观分析。事实是,这不是魔法。jQuery 虽然潜在地优雅且有用,但它只不过是 web API 的包装器和 JavaScript 的扩展。这是一种抽象、简化和方便的机制。但毫无疑问,真正的力量来自底层语言和浏览器自带的工具。虽然 jQuery 在某些方面确实很有帮助,但我们并不真的需要 jQuery。当然,许多其他抽象也是如此。尽管这一章的标题可能暗示了别的意思,但这里的目标并不是对语义吹毛求疵。

可接受使用论点的两个方面

本书的目标不是宣布 jQuery 为“不受欢迎的人”。我的意图不是挑 jQuery 的毛病,而是讲授浏览器的原生工具,并为您提供开发 web 项目的信心,而不会感到无助地依赖于库。因此,让我们坦率地讨论一下什么时候可以接受在项目中使用 jQuery 的“魔力”,什么时候不可以。让我们先把必要性放在一边,把注意力更多地放在需求上。通过正确理解 jQuery 是可接受选择的实例,您将能够在规划未来项目时做出正确的决定。

什么时候可以用?

如果您非常熟悉前端 web 开发,并且只是想编写更优雅的代码,无可否认,没有太多好的理由来避免 jQuery 成为项目依赖。这并不意味着你一定要使用它,但是如果你愿意的话,你也可以这样做。如果您也对 jQuery 感到满意,并且对 jQuery 的神奇之处非常熟悉,那么无论如何,请继续使用它。

“古老”浏览器的某些方面可能会让 jQuery,或者至少是库的某些模块变得有价值。让我们来定义一个比 Internet Explorer 9 更老的浏览器。任何不是古代浏览器的东西都可以被认为是现代浏览器。我将在下一章更多地讨论古代、现代和常青浏览器。

与现代浏览器相比,古代浏览器的 API 有很大的不同。以事件处理为例。在 Internet Explorer 8 及更早版本中,事件处理程序必须用attachEvent()方法注册,传递给attachEvent()的事件名称必须以“on”为前缀。另外,Event对象的一些重要属性和方法也是非标准的。输入元素“更改”事件不会冒泡,并且完全不支持事件捕获。

这些浏览器在 API 和功能支持方面也有很多不足之处。古代的浏览器缺乏 CSS3 选择器支持。在Array原型上缺少有用的indexOf方法。非常老的浏览器不能本地解析或创建 JSON,并且缺乏一种容易区分元素和对象的方法。这些只是古代浏览器面临的一些挑战。在某些情况下,当这些浏览器得到普遍支持时,jQuery 尤其重要。如果您处在一个不寻常且不幸的位置,需要对这样一个旧浏览器的支持,jQuery 可能是一个不错的库。

从大型遗留项目中提取 jQuery 通常没有什么好处。如果企业 web 应用已经放弃了对老式浏览器的支持,那么尝试消除不必要的基于浏览器的依赖可能会很有诱惑力。我不止一次发现自己处于这种情况。从我的经验来看,随着时间的推移,像这样的大型多用途无所不包的库往往会在一个复杂的项目中变得根深蒂固。也许对应用进行有计划的重大重写是去除这些类型的整体依赖性的一个谨慎的借口,但是如果做不到这一点,很可能会使这样的工作毫无结果。除非您的前端自动化测试套件非常全面,否则您可能会发现移除的风险远远超过将库留在原处的任何可察觉的缺点。

当为前端代码编写单元测试时——您应该总是编写测试——jQuery 是一个可接受的依赖。在测试环境中,性能和页面加载时间不是显著的因素,文件大小也不是。事实上,使用一些高级语言或抽象来编写单元测试有一些明显的好处。一个普遍的想法是,单元测试不仅应该用来测试你的代码,还应该根据预期的行为来记录它。一个优雅简洁的测试框架当然会使测试更容易维护,最重要的是,可读性更好。

最后,在一次性项目中使用一点帮助并不可耻。在 web 开发领域没有任何职业抱负的人从事一个小而简单的项目可能不会因为依赖 jQuery 来加速这个过程而放弃任何东西。如果你不是一个开发人员,并且需要启动并运行一个 WordPress 站点,jQuery 可能是一个值得注意的资产。这种情况下,汽车和司机的类比成立。在这种情况下,你是司机,不是机械师。浏览器仅仅是一种便利,而不是你交易的核心工具。

什么时候应该避免使用它?

如果您的项目只支持现代浏览器,尤其是 evergreen 浏览器,那么您可能会发现即使没有包装器库提供的便利,也很容易做到。随着浏览器的发展,web API 和 JavaScript 也在发展。随着相关规范的发展,像 jQuery 这样的库所提供的高级便利设施很快就会在现代浏览器中以本地方式呈现出来。例如,在没有 jQuery 的addClass()removeClass()hasClass()方法的情况下,添加、删除和检查 CSS 类的能力在以前是一件苦差事。但是 web 规范赶上来了,现在用add()remove()contains()方法为每个元素提供了一个原生的classList属性。这也许是 jQuery 对 web 规范产生强大影响的一个例子。随着浏览器原生 API 的不断推进,jQuery 的必要性也在降低。不要在新项目中引入多余的依赖,而是考虑依靠浏览器的力量。

当编写一个通用的可重用库时,尤其是开源库,你的直觉应该是将对第三方的依赖降到最低。您的库的依赖项也成为您的用户的依赖项。由于 jQuery 目前无处不在,您可能认为在任何导出的代码中使用它都是安全的。很可能,使用您的库的项目已经在使用 jQuery 了。但是如果他们不是呢?一个有眼光的 web 开发者会为了使用你的库而引入一个大的可传递的客户端依赖吗?也许不是。随着 web 的发展和开发人员选择摆脱这些类型的抽象,这种情况将变得更加普遍。就我个人而言,我会跳过有不必要依赖项的库,根据我从我维护的大型 JavaScript 库的用户那里收到的反馈,我不认为这种情况是唯一的。作为一名库开发人员,您的工作是解决复杂的问题,并将它们打包到一个与您正在解决的问题的大小和范围成比例的盒子中。

应用的性能可能是拒绝某些依赖项的另一个原因,尤其是像 jQuery 这样复杂的依赖项。作为一个如此成熟和受欢迎的库的用户,您自然会认为代码库的最基本和最常见的部分都经过了大量的优化。在这种情况下,效率是可以预期的。当然,也许一些更复杂和更少使用的函数有一些性能影响。但是所有基本的便利方法都应该是高性能的。不幸的是,对于 jQuery 来说,情况并不总是这样。

hide()方法为例,说明隐藏在表面之下的潜在性能问题。这看起来是一个简单而有效的操作。其实这样做比较简单。一种方法是在文档中定义一个专有的 CSS 类名,与样式display: none相关联。不使用 CSS 类,也许可以将一个隐藏的属性绑定到这个样式。在hide()上,向元素添加类或属性。在show()上,取下它。这导致了一个简单问题的简单而有效的解决方案。然而,jQuery 对一个本应简单的问题的解决方案却相当复杂且效率低下。

jQuery 实现hide()方法的一个主要性能瓶颈是由于使用了getComputedStyle(),这是一个 web API 方法,它计算元素的实际样式集,考虑 CSS 文件、<style>元素以及对元素的style属性的内联或 JavaScript 修改。在某些情况下使用getComputedStyle()是合适的,但是隐藏一个元素可能不是其中之一。在 jQuery 的hide()实现中使用这种方法会带来严重的性能问题。基准测试 1 表明,这种方法比简单地通过属性定义样式并将元素上的属性设置为隐藏要慢大约 90 倍。即使对于经验丰富的开发人员来说,这个特定的性能问题也可能是一个意想不到的问题。jQuery 中围绕 CSS 支持的使用还有其他类似的问题,这将在第七章中详细介绍。

jQuery 的hide()方法的性能问题如此严重,以至于在 3.0 版本中实现被显著简化,消除了这个特殊的性能瓶颈。尽管如此,对于任何使用 jQuery 2.x 或 1.x 的开发人员来说,问题仍然存在,3.0 中对hide()的改变如此剧烈,以至于在一个严重依赖这种方法的大型项目中,对于一些人来说,迁移到 jQuery 3.0 可能需要一点工作。这是一个很好的例子,说明盲目相信无所不包的库会让你误入歧途。

如果您想保持对代码性能的最终控制,那么在引入这种类型的库之前,您应该三思而行,以免意外遇到其他效率瓶颈。当然,一些性能问题可能更多地与您对库的使用有关,而不是其他。但是,使用 jQuery 在不知不觉中编写低效的代码仍然非常简单。请考虑下面的代码清单,它循环遍历一组包含 CSS 类“red”的元素,并删除任何包含属性值为“bar”的属性“foo”的元素:

1  $('.red').each(function() {
2     if($(this).attr('foo') === 'bar') {
3        $(this).remove();
4     }
5  });
Listing 2-1.Removing Elements with jQuery: Naïve Approach

前面的代码当然可以工作,但是它有一些值得注意的性能问题。开发新手和 jQuery 用户如果没有很好地理解 CSS 选择器和循环大量元素的含义,可能不知道有一种更简单、更有效的方法可以解决同样的问题。对于同样的问题,这里有一个性能更好、更优雅的解决方案:

1  $('.red[foo="bar"]').remove();

对于一个小文档,这两种方法的执行时间没有明显的不同。但是,如果文档包含大量 CSS 类为“red”的元素,比如说 200 个,那么第一种方法的后果是很明显的。前一种解决方案比使用复杂 CSS 选择器 2 —使用 jQuery 1.11.2 的 Chrome 42 的一行解决方案大约慢六倍。).

如果您知道在 API 中应该避免哪些方法,以及何时应该避免,那么您仍然可以使用 jQuery 编写高性能的代码。浏览器的 API 也是如此,但是库经常提供一种虚假的安全感。直接使用 web API 的代码在意图上更加明确和具体。另一方面,jQuery 提供了一个更高级的、看似神奇的 API,它掩盖了实现的许多细节,并掩盖了潜在的性能折衷。我们经常不想忽略抽象的便利,但是你必须这么做。如果您想编写可靠且高效的代码,您不仅要理解 jQuery 本身(如果您选择使用它),还要理解 jQuery 如何利用 web API。在这里,盲目的信仰会以多种形式出现问题。

可能需要避免 jQuery 的另一个考虑因素是页面加载时间。对 jQuery 的ready()方法的过度依赖就是一个例子。只有在文档中的所有元素都被加载到页面上之后,ready()方法才会执行传递的函数。这对实际的页面加载时间没有显著的影响,但是它确实影响了感知的页面加载时间。通常,任何由 jQuery 的ready()方法执行的代码都被导入到文档的顶部(通常在<head>元素中)。如果所有脚本都在文档顶部加载,这可能会导致页面呈现明显延迟,因为脚本必须在元素之前加载和执行。只要有可能,推荐的方法是将所有脚本加载到文档的底部。这使得页面加载速度更快,因为文档元素比其他任何东西都要先加载。如果你遵循这个惯例,使用$.ready()就没有必要了。jQuery 的ready()方法被广泛使用,甚至经常出现在 jQuery 学习网站的示例代码中。这是另一个例子,您最好理解所有可能的选项(比如在页面底部加载脚本),而不是盲目依赖 jQuery 提供的便利方法,比如ready()

与页面加载时间有点关系的是文件大小。我指的是为了在页面加载时完全呈现,页面必须加载的任何资源的大小(以字节为单位)。反对依赖 jQuery 等库的一个常见理由是文件大小。现实情况是,带宽是有限的,浏览器在页面加载时下载的所有客户端依赖项都会消耗一部分带宽。如果您的用户都有一个 60 Mbps 的下行管道,那么您的应用下载的脚本可能不会对页面加载时间产生任何明显的影响。但是如果你的用户没有那么幸运呢?如果他们只能访问最大下行速率为 6 Mbps 的 DSL 呢?如果你的目标是移动设备呢?在这种情况下,下行带宽可能不会超过 4 Mbps。在发展中国家,您的用户可能只能访问 Edge,其峰值速度约为 400 Kbps。你考虑到你所有的用户了吗?

jQuery 的大小对您和您的用户来说可能重要,也可能不重要。如果您决定从 CDN 加载 jQuery,那么完全避免往返的可能性更大。因为这个库非常受欢迎,所以很多用户可能已经在浏览器中缓存了来自另一个应用的 jQuery。但这肯定不能保证。大量使用中的 jQuery 版本使得项目所依赖的特定版本不太可能被大多数用户缓存。生产运行时依赖第三方服务器也有潜在的缺点。如果该服务器遇到技术问题,即使您控制下的所有服务器都按预期运行,您的应用也可能因此瘫痪。

如果您自己或通过私有 CDN 托管 jQuery,那么您可以更好地控制如何提供服务,以及从哪里提供服务(考虑用户的位置)。或者,您可能担心单个 HTTP 请求的开销,并选择将 jQuery 与所有其他页面资源结合起来作为对单个请求的响应。结合 GZIP 压缩,这是一个不错的策略。但是当您的用户群依赖于极低带宽的连接时,保持您的资源列表较小仍然是最重要的。如果第一页的加载花费了大量的时间,你可能会失去一个潜在的客户。

公平地说,我应该提到 jQuery 1.8 在项目源代码中公开了一个构建任务,它允许 jQuery 被“定制”构建,排除了特定项目可能不需要的任何模块。这可能否定了文件大小参数。但是一个问题仍然存在:新的和没有经验的开发人员真的知道他们需要 jQuery 的哪些部分吗?大多数开发人员知道创建 jQuery 定制版本的能力吗?这两个问题的答案很可能都是“不”。不幸的是,创建定制构建的能力隐藏在 jQuery 源代码树的构建文件中。为了使用它,您必须下载整个存储库,安装构建 jQuery 所需的开发依赖项,搜索构建文件或浏览项目 GitHub 存储库中的 README.md 以获得说明,并使用他们的 grunt 构建工具运行任务,排除任何不需要的模块。有了所有这些步骤,大多数依赖 jQuery 的开发人员不太可能使用面向用户的下载页面上提供的完整构建文件以外的任何东西。

所有这些都揭示了在开发流行的单体库时所做的妥协。毫无疑问,从一开始,jQuery 的开发就投入了大量的精力和细节。但是它可能没有考虑到你或者你的边缘情况,甚至你的目标。是不是既要方便又要快捷?这两个目标可能相互矛盾,这取决于您的工作流和库的预期用途。你想要无缝的文件上传还是最小的占用空间?这两个都不是 jQuery 的目标。jQuery 和其他拥有庞大用户群的大型库一样,必须非常小心地关注特性和工作流的最大公约数。

应该使用其他库吗?

本书的一个目标是推动你去除对 jQuery 的依赖。但是我提供给单片包装器的唯一选择是直接连接到浏览器的本机 API。这当然是一个令人钦佩的目标,从某种意义上说,浏览器是我们开发所有前端项目所需要的。但实际上,我们可能需要更多的帮助,以减少在整合过于复杂的东西时不可避免的焦虑。假设我们的目标是现代浏览器,考虑到网络的当前状态,这是合理的。但是,即使是“现代的”浏览器,如果有一个进化了的 API,也可能对我们的项目所需要的一些强大的特性提供不一致的支持。如果有某种方法可以在所有现代浏览器上一致地使用现代 web 和 JavaScript 特性,而不用包装整个堆栈就好了。。。。

大包装纸上的小垫片

web 开发中有一个概念描述了一种非常特殊的库,即回归库。这是一个新名词,你可能从未听说过,因为这是我自己创造的。回归库是大型包装库的合理替代品。虽然它们通常很小(尽管不总是如此),但它们真正的吸引力从名字上就很明显——它们是递减的。尽管大多数库随着时间的推移在大小和特性集方面都有所发展,但退步的库会退化。回归库的最终目标是消失,被浏览器的原生 API 完全取代。

回归库通常被称为垫片或聚合填充。他们不提供任何新的 API。他们的工作是在不兼容的浏览器中临时填补标准化 API 的缺失实现。这些库让我们专注于本地工具。没有任何抽象会模糊我们的理解,隐藏网络的真实本质。Polyfill 代码通常构造为只有在浏览器不包含匹配的本机实现时才使用。如果浏览器包含适当的本机代码,则库直接委托给浏览器。

常用的聚合填充的一个例子是道格拉斯·克洛克福特的 json2 库。 3 它为浏览器中的JSON对象贡献了一个实现,该实现不包含它们自己的本地实现。正如所料,json2.js 的 API 与 ECMAScript 5 规范中标准化的 JSON API 是一对一的匹配。 4 该规范描述了一个 JavaScript 对象,该对象包含将 JSON 字符串转换成 JavaScript 对象以及将 JavaScript 对象转换回 JSON 字符串的方法。当序列化和反序列化数据作为与 JSON 感知端点通信的一部分时,这些方法非常有用和重要。Json2.js 确保这个 API 在没有实现这个特定 ECMAScript 5 规范的旧浏览器中可用(比如 Internet Explorer 7)。

还有不少其他流行的 shims 比如 webcomponents.js,和 fetch。它们的名字很好地表明了它们负责修补的本机 API。目前,这两种聚合填充都有助于实现前沿规范。Webcomponents.js 为没有完全实现 Webcomponents 浏览器规范的浏览器(指目前除 Chrome 以外的所有浏览器)贡献了补丁。Fetch 允许开发者使用 WHATWG 创建的新 fetch 规范, 5 最终取代了XMLHttpRequest。其中一些将在本书的后面部分探讨,比如下一章。

编写您自己的垫片

当我们想要在我们的项目中使用 web API 和 JavaScript 的一些令人兴奋的新的前沿特性,并保持对各种浏览器的支持,并确保我们的依赖关系的足迹尽可能小且暂时时,我们转向回归库。但是聚合填充物到底是什么样子的呢?如何着手创建这样一个图书馆?当您发现自己需要在一个较旧的浏览器中使用一个常见且有用的本机 API 方法,而没有现成的 polyfill 可供您使用时,这些问题比学术问题更实际。创建自己的聚合填充可能没有您想象的那么复杂。不相信我?让我们现在就创建一个。

以 JavaScript Array s 中可用的find()方法为例,它是 ECMAScript 2015 规范的一部分。Array.find返回数组中满足给定条件的条目。虽然这听起来相当有用,但所有版本的 Internet Explorer 都缺少浏览器支持。但是我们可以通过编写自己的填充程序在所有浏览器中使用这种方法,如清单 2-2 所示。

 1  if (!Array.prototype.find) {
 2    Array.prototype.find =
 3      function(callback, ctx) {
 4        for (var i = 0; i < this.length; i++) {
 5          var el = this[i];
 6          if (callback.call(ctx, el, i, this)) {
 7            return this[i];
 8          }
 9        }
10      };
11  }
Listing 2-2.Conditionally Creating an Array.prototype.find Shim

如果(且仅当)浏览器没有本地实现Array.prototype.find,前面的代码将注册一个实现。因此,有了这个垫片,你可以在任何浏览器中使用 ECMAScript 2015 规范中提出的Array.prototype.find,甚至是 Internet Explorer 6!本质上,shim 就像一个本机实现一样,将迭代数组中的所有项,直到找到满足传递的谓词函数的项,或者直到用完了要检查的元素。对于数组中的每个元素,调用传递的谓词函数,传递当前数组元素、当前数组索引,最后传递整个数组。请注意,ctx参数是可选的,它允许调用代码指定谓词函数要使用的替换值(也称为上下文)。如果省略了这个上下文参数,那么传递的谓词函数的实际上下文将是“全局对象”,如果这段代码在浏览器中执行,那么它恰好是window对象。如果谓词函数返回“真”值,则数组元素满足谓词函数。shim 将返回满足谓词的元素,如果没有元素是令人满意的,则返回 undefined。

使用我们的 shim,清单 2-3 中的函数返回数组中名称属性为“foobar”的元素。这恰好是数组中的第三个元素。

 1  function findFoo() {
 2    return [
 3      {name: 'one'},
 4      {name: 'two'},
 5      {name: 'foobar'},
 6      {name: 'four'}
 7    ].find(function(el) {
 8      return el.name === 'foobar';
 9    });
10  }
Listing 2-3.Using the Array.prototype.find Shim

最后一句话

jQuery 不是 web 开发未来的一部分,但其他大型且当前流行的库或 JavaScript 框架也不是。图书馆来来去去;浏览器的 API 和 JavaScript 会比它们都长寿。web 的未来,以及您作为 web 开发人员的职业生涯,都被编入了 web 和 ECMAScript 规范中。这些规范正在迅速发展——它们正在迅速赶上库。常见问题的本地解决方案通常会提高性能并增加便利性。

使用 jQuery 这样的包装器库并没有什么错。然而,您不仅要正确理解 jQuery 本身所依赖的代码,还要正确理解您选择使用它的原因。您并不真的需要 jQuery,但是如果您仍然想使用它,一定要注意在哪些情况下使用它是可以接受的,以及在哪些情况下您可能想考虑放弃这个特定的抽象。

Footnotes 1

http://jsperf.com/jquery-hide-vs-set-attr

  2

http://jsperf.com/jquery-loop-vs-complex-selector

  3

https://github.com/douglascrockford/JSON-js

  4

www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf

  5

https://fetch.spec.whatwg.org

  6

http://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.prototype.find

三、理解 Web API 和“普通”JavaScript

在我们进一步探索浏览器、JavaScript 和 jQuery 的奥秘之前,有一些重要的概念和术语需要讨论。如果您的计划是更好地理解浏览器中所有可用的不同本机工具,那么您必须了解这些工具的历史以及它们之间的相互关系。

浏览器可以分为几个不同的类别。在本书和其他地方,你会听到很多用来描述这些类别的术语,比如现代浏览器和常青树浏览器——这些将在本章中详细讨论。仔细观察这些分类的必要性将揭示为什么一些分组是移动的目标,并且潜在地具有可疑的重要性。除了基于浏览器的 JavaScript 之外,由于 Node.js,您甚至可以了解如何在浏览器之外使用这种语言,比如在服务器上。

web API 和 JavaScript 语言都是本书讨论的主题。在介绍这两个项目的复杂语法和用法之前,您需要清楚它们的作用和重要性。我将详细介绍 web API 和 JavaScript 的定义,以及这两个基本概念之间的关系。本章的另一个重要目标是说明这两种技术是如何受到标准化的影响的。制定这些标准的组织将被详细介绍。完成本章后,您将会非常熟悉构成本机浏览器堆栈的各种规范。

关于浏览器你需要知道的一切

最初(1990 年),现代互联网的创始人之一蒂姆·伯纳斯·李开发了第一个网络浏览器 Nexus。1993 年,第一个完全图形化的浏览器 Mosaic 紧随其后。1994 年和 1995 年,分别发布了网景 Navigator 和微软 Internet Explorer 1.0。到 20 世纪 90 年代中期,Netscape 和 Explorer 几乎占据了所有常用的浏览器。它们都提供了一套快速增长的专有特性,使自己与众不同,但也有利于极性而不是标准化。这在一定程度上导致了后来工具(如 jQuery)的流行,这些工具允许 web 开发人员更有效地针对多种浏览器。虽然 Nexus、Mosaic 和当时的其他类似浏览器相对短暂,并且不受用户和开发者的青睐,但 Netscape 和 Explorer 开创了一个浏览器激烈竞争的时代。

以今天的标准来看,早期的网络是缺乏创意和原始的。在浏览器端,网络上只有静态内容。信息从服务器一次加载一整页,即使只需要更新页面的一小部分。单击锚链接时,HTTP GET 请求被发送到服务器,服务器用下一页的内容作出响应——页眉、正文、页脚等等。这不利于提供出色的用户体验,也不利于充分利用当时非常有限的带宽。然后在 20 世纪 90 年代后期出现了 Java 小程序和 Flash,它们允许开发人员创建动态的浏览器内应用。然而,这两种技术都需要在浏览器上安装第三方软件。早在万维网联盟制定官方标准之前,微软就允许开发人员创建一个页面,通过向服务器发送请求,返回一个文档的片段,可以部分更新该页面。然后,这个片段用于替换现有内容或创建附加内容,而不改变页面的其余部分。这就是通常所说的 ajax 请求,它的发明为本地浏览器带来了动态内容创建。微软对这一概念的实现是在 20 世纪 90 年代末,Flash 和 Java 小程序出现后不久引入的。尽管 Ajax 请求最初是在 1999 年左右引入的,但直到 2002 年包含在 Mozilla Firefox 浏览器中,它们才出现在其他地方,并且直到 2006 年它们仍然是非标准的。这是一个网络标准化远远落后于对更现代功能的渴望的时代。

尽管 Internet Explorer 及其专有功能在相当长一段时间内主导了浏览器市场,但显着的竞争在 21 世纪初到来。Mozilla Firefox 是微软产品的第一个可行对手。免费开源浏览器的推出让微软完全措手不及,并开创了一个新的网络时代。Firefox 的 Gecko 引擎是第一个挑战微软 Trident 的引擎。Gecko 发布几年后,苹果开发了 WebKit 渲染引擎来支持其 Safari 浏览器。WebKit 最初是 Konqueror 浏览器使用的 KHTML 渲染引擎的一个分支,Konqueror 浏览器是一种用于 Linux 桌面的浏览器。不久之后,谷歌开发了自己的浏览器 Chrome,也使用了苹果的 WebKit 引擎。后来,谷歌创建了自己的渲染引擎——Blink——它本身是 WebKit 的一个分支,类似于苹果最初对 KDE KHTML 引擎的分支。有趣的是:Opera 是一个小众浏览器,大部分时间都依赖于自己开发的“Presto”渲染引擎,在 2013 年转向了 Chrome 的“Blink”引擎。虽然 Firefox 最初在 Windows 上占据了相当大的市场份额,Safari 在 OS X 上也是如此,但 Chrome 在推出后不久就开始上升到跨操作系统的主导地位。Chrome 的成功可以归功于它的快速发展和对 web 标准化的重大影响。这是一个正式规范开始匹配并影响浏览器开发的时代。有了成熟的标准和相对可靠的浏览器质量保证,像 jQuery 这样的库开始变得不那么重要了。

我刚刚概述的历史描述了许多不同的移动和桌面浏览器,像大多数东西一样,可以以许多不同的方式进行分类。在本书的上下文中,将使用一组合理的类别来说明它们的现代性、可移植性和可更新性。在下面几节中,您将熟悉一些更常见的浏览器类别。我还将评论所有当前可用的浏览器的状态,并提供一些在考虑这些类别的浏览器时要考虑的警告。

古代浏览器

古代浏览器,也称为遗留浏览器,通常被认为是微软的 Internet Explorer 的旧版本。在撰写本书时,2016 年年中,古老的浏览器是比 Internet Explorer 9 更老的浏览器。Explorer 7 通常被认为是最古老的浏览器,无论出于何种目的,即使是 Internet Explorer 10 和更低版本也不再受微软支持,只有在大量用户无法升级的情况下,您的新 web 应用才会支持它。IE6、Mosaic、Netscape 和其他类似的浏览器不仅过时,而且大部分都没有使用过。它们不属于任何一套当前使用的浏览器,所以在本书未来的讨论中我们不会考虑它们。截至 2016 年 6 月,令人欣慰的是,在整个被测网络中,古代浏览器仅占当前使用浏览器的 1%左右。 1

老式浏览器是所有浏览器类别中最不受欢迎的。它们有许多缺点,这使得它们很难开发和支持,并且通常被认为与更现代的选择相比非常慢。他们对 DOM API 和其他相关 web APIs 的支持是原始的。由于当时缺乏现代规范,它们支持有限的 JavaScript 便利方法。它们中的许多,尤其是 Internet Explorer 6,充斥着显著而严重的布局错误。由于这些原因,古老的浏览器已经失宠,取而代之的是更加稳定、高效和方便的选择。

但是当使用年龄作为关键属性对浏览器进行分类时,我们必须小心。古代的浏览器代表了一个移动的目标。几年后,今天的新浏览器可能会被认为是古老的浏览器。在不久的将来,当一个古老的浏览器的市场份额实际下降到 0 时,它将被视为死亡。这种类型的分类不是特别实用,但是对于将那些在任何情况下都可以不用 jQuery 合理处理的浏览器与那些不能的浏览器分开来说,这种分类可以说是足够有效的。然而,未来的章节将探索其他更有效的技术,通过程序性特征检测的实践来区分有能力和无能力的浏览器。

现代浏览器

这个现代的形容词可以用来描述所有比那些被认为古老的浏览器更新的浏览器。在撰写本文时,现代浏览器都是比 Internet Explorer 9 更新的浏览器,包括 Internet Explorer 9。这个列表还包括 Chrome、Firefox 和 Safari 版本 6+。但是这种现代分类法与古代分类法有一个共同的特点。这个类别的后缘是一个移动的目标。现在现代的东西,几年后可能就是古代的了。与古代相似,现代只是在阅读这本书时用作上下文。在描述浏览器对一段代码的支持时,我会经常用到这些术语。

与古老的浏览器相比,现代浏览器更容易使用,这是因为它们拥有相对先进的开发工具、web API 和 JavaScript 支持以及稳定性。它们说明了一组无需像 jQuery 这样的包装器库的帮助就可以很好地解决的浏览器。以编程方式识别这类浏览器的冲动非常强烈。如果我们可以轻松地确定我们是在处理 Internet Explorer 9 还是 8,或者 Safari 8 还是 5,那么也许我们可以定义两个离散的代码路径——一个用于古代浏览器,另一个用于现代浏览器。但是你要忍住这种冲动。

这种主要基于年龄的对浏览器的笼统分类是没有意义的。更糟糕的是识别这一类别的浏览器,并根据浏览器的用户代理标识字符串做出所有的代码路径决策。正确的方法是测试浏览器的 API 实现是否有特定的功能,然后为该特定的功能选择合适的代码路径。我想明确这一点,以确保我迄今提出的分类在适当的背景下被看到,而不是被提升到比它们应得的更高的重要性水平。

Note

用户代理字符串是标识特定类型和版本的 web 浏览器的一系列字符。您可以通过 JavaScript 检查 navigator 对象的 userAgent 属性来获取浏览器的 UA 字符串。

常青树浏览器

还有第三类浏览器,它是永恒的,并且在不断发展。常青树浏览器是网络的未来。属于这一类的浏览器无需任何用户干预就能自行更新。虽然它们都有版本,但大多数使用 evergreen 浏览器的人可能不知道他们浏览器的当前版本号(除了 Internet Explorer 10+和 Safari)。更新是完全透明的,即使是重大版本升级。这允许浏览器无缝地发展其 web API 和 JavaScript 支持。用户通常没有(容易地)保留在旧版本上的选项。这使得这些浏览器能够更快地实现新的 web 规范,并确保整个用户群是最新的。整个生态系统都赢了。

目前,Chrome、Firefox、Safari、Opera、Internet Explorer 10+和 Microsoft Edge 被认为是常青树浏览器。这个概念非常流行,并占据了今天大多数可用的浏览器。将一个浏览器版本与一组特定的操作系统版本或“服务包”捆绑在一起的模式,随着 Internet Explorer 9 和 Windows Vista 的出现,大部分已经消亡。微软最新重新设计的浏览器名为 Microsoft Edge,更像其他传统的 evergreen 浏览器,因为与微软早期开发的产品相比,版本号没有那么突出。随着 evergreen 浏览器接管 web,我们可以比以往任何时候都更快地利用快速发展的规范、安全改进和错误修复。在这个世界上,需要一个库来填补浏览器的空白变得不那么重要了。

移动浏览器

至少在美国,桌面浏览器仍然占网络流量的大部分。然而,目前的测量显示,移动浏览器的使用正在上升,而桌面流量正在下降。截至 2016 年 6 月,移动/平板设备约占网络流量的 42%, 2 ,其余大部分来自桌面浏览器。移动设备使用的稳步上升,以及“移动第一”的一贯(也是重要的)口号,?? 揭示了移动浏览器与常青树浏览器一样是网络未来的重要组成部分。可以说,移动网络仍处于起步阶段,但它不能也不应该被忽视。

在某些方面,许多移动浏览器也是非常常青的浏览器,因为它们中的一部分会自动更新,无需用户干预。就像在桌面上一样,这种行为允许移动浏览快速发展,并确保用户始终拥有他们选择的最新版本的浏览器。但是自动更新浏览器的好处在历史上是与物理设备的能力联系在一起的。例如,运行 Android 2.x 的旧手机可能无法处理 4.x 版本,从而无法使用最新版本的移动 Chrome。同样的问题也存在于其他移动平台上,比如停留在过时的 iOS 版本上的老款苹果设备。

移动领域在很大程度上被运行 iOS 的苹果 iPhone 和 iPad 以及众多运行谷歌 Android 操作系统的其他设备所主导。尽管微软已经开始对他们的 Windows Phone 操作系统有所了解。对于本书的目的(以及大多数其他内容)来说,Research in Motion 的黑莓操作系统无关紧要,因为它在移动网络流量中所占的份额很小,而且还在不断下降。不管使用什么移动设备,请记住,本书中用于替换 jQuery 的代码和技术将同样适用于所有现代/常青树浏览器,因此在这种情况下,除了性能考虑之外,移动和桌面之间的区别并不特别重要。

尽管移动网络越来越普及,但目前的情况并不乐观。移动浏览器带来了独特的挑战。与桌面系统相比,外形和电池方面的考虑使我们代码的性能结果更加明显。尤其是移动浏览器,相对于更大尺寸的浏览器来说,还不太成熟。伴随这种不成熟而来的是各种浏览器之间规范支持的不一致性。将手机浏览器与桌面浏览器进行比较时,这一点往往更加明显。Chrome 和 Safari 是在移动和桌面设备上都存在的浏览器的两个例子。虽然这些浏览器可能在多个平台上共享相同的名称,但它们的目标各不相同,从而导致不同的体验。

在某些情况下,由于非常独特的移动性问题,比如数据使用,web 规范的公共部分表现不同。以 HTML5 <video >元素上的autoplay布尔属性为例,它将确保相关视频在加载后立即开始播放。 5 桌面浏览器都支持这个标准特性,但是在移动端的支持有点不同。运行在 iOS 上的 Safari 不会观察到这一属性,以确保自动播放的视频不会对用户有限(且相对昂贵)的移动数据消耗产生不利影响。 6 还有其他类似的例子,移动浏览器的独特环境可能会导致意想不到的实施差距。在编写“移动优先”的库和应用时,必须考虑这一现实。

非浏览器

这一节可能看起来有点不合适,但是为了完整起见,我认为至少涉及另一个 JavaScript 蓬勃发展的环境是很重要的。由于 Node.js 的存在,两端都使用 JavaScript 的全栈软件开发成为可能,node . js 是一种使用 Chrome JavaScript 引擎的服务器端运行时。 7 服务器端 JavaScript 开发不包含任何类型的 web API,原因显而易见。因此,尽管贯穿本书的基于 web 的讨论(有很多)并不适用于 Node.js 世界,但许多纯 JavaScript 领域确实超越了浏览器。

如果您不太清楚特定于浏览器的 JavaScript 和语言规范中规定的 JavaScript(以及可在浏览器之外使用的 JavaScript)之间的区别,稍后当我比较、对比和定义 web API 和 JavaScript 语言时,会对此进行更多的讨论。这里重要的一点是理解本书的一些内容实际上适用于基于服务器的 JavaScript 开发。在很大程度上,服务器上的 JavaScript 为您提供了相对最新的语言规范支持,这意味着本书中大多数非特定于浏览器的示例也可以在服务器上使用。

这个 Web API 是什么,为什么它很重要?

我在整本书中都提到了 web API,并将继续这样做。但是术语 web API 不是一个标准术语,甚至不是一个常用术语,所以有必要做一些解释来消除任何潜在的歧义。简单地说,web API 指的是所有 JavaScript 方法和对象,这些方法和对象专门允许开发人员以编程方式处理和操作浏览器。这个通用的浏览器 API 由两个不同的部分组成:一个是文档对象模型(DOM ) API,它是附加到 HTML 文档节点的一组方法和属性,第二个是仅在浏览器环境中可用但不直接与 HTML 相关的其他方法和函数的集合。如果这些简洁的定义仍然有点模糊,没必要担心。我保证在本节的后面会更详细地解释 DOM 和非 DOM APIs。

我希望到现在为止,您对术语 web API 以及它与浏览器的关系已经比较熟悉了。但仍有一个问题:你为什么要在乎?除了它是本书大部分章节和概念的关键属性之外,它也是 web 开发人员可用的最重要的工具。web API 提供了为最终用户甚至其他开发人员创建特别定制的动态体验所必需的一切。它在不断地快速发展,因此 web 注定最终会取代已安装的应用。作为一名专业开发人员,您对 web API 的理解(或缺乏理解)将对您有效设计和开发丰富复杂的 web 应用和库的能力产生重大影响。这本书的目标是灌输这种现实,并教你如何不仅很好地利用 web API 来代替 jQuery,而且更好地理解浏览器环境,以便你可以更有效地使用包装了这种原生 API 的库。

DOM API

DOM 是一组用于表示 HTML 文档的方法和对象。这些表示通常(但不仅限于)使用 web 上最常见的语言:JavaScript 来表达。DOM 提供了镜像文档中元素的 JavaScript 对象。它允许创建、定位、操作和描述元素。这种语言绑定在所有 HTML 元素中公开了许多潜在的控制点。例如,DOM API 在 DOM 的Element接口上定义了一个className属性。这个特定的属性允许编程读取和更改任何元素的 CSS class属性。所有其他 HMTL 元素,比如锚点(HTMLAnchorElement)、<div>元素(HTMLDivElement)和<span>元素(HTMLSpanElement),都继承自Element接口,因此也在它们的 JavaScript 对象表示中包含了className属性。

上一个className示例中展示的元素层次结构是需要理解的重要内容。特定元素上可用的公共属性和方法通常继承自更常见的元素类型。EventTarget类型位于链的顶端,所有其他 HTML 节点的表示都继承自它。EventTarget类型定义了注册事件处理程序的方法,所有其他 HTML 项目都会继承这些方法。一个Node是一个EventTarget的子类型,所有其他元素也从它继承而来。这个Node接口提供了克隆 HTML 项目和定位兄弟节点的方法,以及其他行为。Node的子类型包括ElementCharacterData. Element对象,如您所料,所有节点都可以用标准化的 HTML 标签来表示,例如<div><span>. CharacterData项可以是文档中的文本或注释。

除了对每个元素的控制之外,还可以使用 JavaScript 对整个文档进行操作。事实上,文档有一个特殊的表示,恰当地命名为Document接口。一个Document对象继承自Node(如果您还记得的话,它继承自基类——EventTarget)。例如,文档包含允许检查与标记关联的所有样式表的属性。许多重要的方法也是可用的,比如一种便于创建新的 HTML 元素的方法。请注意,没有任何东西是从Document继承的。出于本书讨论的目的,浏览器的document对象上的所有属性和方法也可以被认为是 DOM 规范的一部分。图 3-1 显示了 DOM 元素的层次结构。

img/A430213_1_En_3_Fig1_HTML.jpg)

图 3-1。

DOM element hierarchy

所有这些类型、行为、属性、方法和关系都是标准 DOM 规范的一部分。 8 这个标准最初是由万维网联盟(W3C)在 1998 年创建的,称为 DOM Level 1。传统上有两种处理 DOM 的特定标准途径:DOM Core 和 DOM HTML。正如规范摘要所指出的,DOM Core 是“一个平台和语言中立的接口,允许程序和脚本动态地访问和更新文档的内容、结构和风格。”在撰写本文时,最新的此类标准(2004 年末成为推荐标准)是 DOM Level 3 Core,【10】,它定义了元素的新属性,比如可以用来读取或设置节点文本的textContent属性。

DOM HTML 规范摘要听起来类似于 DOM Core,但实际上有点不同。它声称该规范是“一个平台和语言中立的接口,允许程序和脚本动态访问和更新 HTML 4.01 和 XHTML 1.0 文档的内容和结构”(强调由我添加)。换句话说,正如您所料,DOM 核心规范定义了所有文档共有的核心功能,DOM HTML 规范对这一核心规范进行了一点扩展,提供了一个更加特定于 HTML 的 API。DOM HTML 规范定义了元素的公共属性,比如idclassNametitle。在撰写本书时,最新的 DOM HTML 标准是 DOM Level 2 HTML,它在 2003 年末成为一个推荐标准。 11

还有其他相关的标准,比如选择器 API 12 ,正如你所料,它涵盖了选择元素。例如,querySelectorquerySelectorAll方法在DocumentElement接口上都有定义,以允许使用选择器规范中定义的 CSS 选择器字符串选择文档中的元素(目前在第 4 级。 13 另一个相关的规范是 UI 事件规范, 14 定义了原生 DOM 事件,比如鼠标和键盘事件。DOM4 规范试图集合所有这些标准以及更多标准。 15

然后是最广为人知的标准 HTML5,它在 2014 年末成为了一个推荐标准。 16 它是最新的 DOM 规范之一,继承了 DOM4 的目标以及许多其他与 DOM 无关的规范(我将在下一节谈到)。在 DOM 的上下文中,HTML5 定义了新元素(如<section><footer><header>)、新属性(如placeholderrequired)和新元素方法和属性(如图像元素的naturalWidthnaturalHeight属性)。当然,这只是一小部分变化。W3C 维护了一个文档,该文档相当详细地描述了 HTML5 带来的变化。 17 目前,最新进行中的规范是 HTML 5.2,也是由万维网联盟策划的。HTML 5.1 和 5.2 为 DOM 带来了一些更新的元素。这些新元素中最值得注意的是<图片>、 18 ,它允许指定多个图像源,并向浏览器提示要加载哪个图像。例如,图片来源可以与浏览器窗口大小或像素密度相关联。

简而言之,DOM API 提供了一种使用 JavaScript 读取、更新、遍历和创建文档元素的方法。元素本身及其属性也是由这个规范族定义的。web 中的真正力量部分是由 DOM APIs 定义的。没有它,动态 web 应用就不会以当前的形式存在。唯一的选择可能是嵌入式 Flash 或 Java 小程序——由于现代 DOM API 的强大功能,这两种技术很快就会过时。让我们明确另一件事:jQuery 构建在 DOM API 之上。没有 DOM API,jQuery 也不会存在。jQuery 主要是 DOM API 的包装器,提供了一定程度的抽象。Beyond jQuery 的很大一部分致力于在有或没有 jQuery 帮助的情况下使用 DOM APIs。

其他一切(非 DOM)

除了 DOM API 之外,还有另一组特定于浏览器的 API,它们组成了附加到浏览器的window对象的所有属性。浏览器“窗口”包含一个 HTML 文档和document对象(由 DOM API 定义)。这个窗口可以通过 JavaScript window对象——一个全局变量——以编程方式访问。尽管 DOM API 定义了附加到document对象的所有东西,但是附加到window对象的所有东西都是由大量其他规范定义的。例如,文件 API19定义了一组用于在浏览器中读取、写入和标识文件的方法和属性,由两个接口表示:BlobFile。两个接口定义都可以在window对象上获得。

绑定到Window接口的另一个众所周知的 API 规范是XMLHttpRequest20 ,它定义了一组用于通过 HTTP 与服务器异步通信的方法和属性。除了新的 DOM API 特性,HTML5 标准还定义了一大堆与window相关的属性。一个例子是History接口、 21 ,其提供对浏览器历史的编程访问。这在window上显示为一个history对象。还有一个例子是Storage接口, 22 ,它包括在window上表示为sessionStoragelocalStorage,用于管理浏览器中少量数据的临时存储。

虽然上一节中讨论的关于 DOM 的 HTML 5.1 规范也在非 DOM APIs 的发展中发挥了作用,但它的作用比 HTML 5 标准小得多。在 W3C HTML 5.1 规范的当前版本中,最值得注意的非 DOM 参考是由另一个标准组织 WHATWG 起草的fetch API、 23 。这就给我们带来了一个关于这个相对较新的现象的简短讨论:相互竞争的网络标准。一方面,我们有 W3C,它从 1994 年就开始为网络制定标准。它是由万维网的发明者蒂姆·伯纳斯·李领导的。我们今天使用和喜爱的 web 规范的起源是由 W3C 正式标准化的。W3C 出现十年后,网络超文本应用技术工作组(WHATWG)成立了。

WHATWG 提倡一种“生活标准”,一种与版本号或“级别”无关的标准。例如,它们没有 HTML5 或 HTML 5.1 规范,只是有一个随时更新的 HTML 规范。该组织自己也起草了一些原创的新标准,比如之前提到的Fetch API,以及Notifications API、 25 使 web 应用能够向用户显示通知。根据 FAQ 页面,该小组是出于对“W3C 对 XHTML 的指导、对 HTML 缺乏兴趣以及明显无视现实世界作者的需求”的不满而创建的。WHATWG 看起来确实是 W3C 的健康制衡力量,显然促进了网络的更快发展,这当然是件好事。

JavaScript:不太优雅的 jQuery 版本?

引入 jQuery 的一个常见原因是为了弥补底层语言 JavaScript 本身的不足。这是最无聊的借口之一。仅仅为了稍微好一点的遍历对象属性和数组元素的方法,而引入像 jQuery 这样的第三方依赖有点过分。事实上,有了forEachObject.keys()的存在,这是完全没有必要的,这两个在现代浏览器中都有。或者也许你认为$.inArray()是一个重要的实用功能。事实是,自从 Internet Explorer 9——Array.prototype.indexOf成为语言的一部分以来,最优雅的解决方案是使用“普通的”JavaScript。当然,在本书中还会有更多明显的例子。

在前端开发人员中,尤其是那些对 web 开发了解有限的人,在编写客户端应用时,通常认为有两种“语言”可供选择:jQuery 或 JavaScript。对于经验丰富的 web 开发人员来说,这组选项中的缺陷是显而易见的。这两种“语言”实际上只有一种是语言。事实上,JavaScript 是一种标准化的语言,而 jQuery 只是提供了一组实用方法,旨在使 JavaScript 在各种浏览器中解决常见问题变得更容易、更优雅。jQuery 只不过是 web API 包装方法的集合。

在开发 web 应用时,JavaScript 无处不在且不可避免,随着 Node.js 的出现,JavaScript 现在也是服务器上的一个可行选项。在接下来的部分中,我将解释 JavaScript 作为一种语言在 web 开发环境中的重要性。Beyond jQuery 没有一个明确的目标来深入研究语言语法和核心概念,如继承和范围,尽管如果与 jQuery 提供的抽象层有明确的联系,这些形式的语言细节可能会在整本书中不时出现。相反,您将了解 JavaScript 与 web API 的联系。类似于我们之前对 web API 的讨论,该语言的历史和标准化也将被探究。

Note

实际上,JavaScript 在技术上是可以避免的,尤其是在 WebAssembly 出现之后,但是这个标准还处于起步阶段。如果你用编译成 WebAssembly 的非传统前端语言编写,假设 WebAssembly 是可靠的(目前情况并非如此),那么你很可能不会受到 JavaScript 的攻击。但除此之外,它仍然是相当重要和不可避免的。

语言与 Web API

JavaScript 是 web API 不可或缺的组成部分。以 DOM 为例。虽然浏览器 DOM 通常用 C 或 C++实现,并打包为布局引擎(如 Safari 的 WebKit 和 Chrome 的 Blink),但 DOM 最常见的操作是使用 JavaScript。例如,考虑一下使用 DOM 元素属性。为此,DOM Level 1 中描述了三种属性相关的方法:getAttribute、、 27 、、setAttribute、、 28removeAttribute29 另外 DOM Level 2 提供了hasAttribute30 这四个方法都是在Element接口中定义的,在 JavaScript 中有相应的(也是众所周知的)实现。给定任何 HTML 元素,您都可以在 JavaScript 中读取和操作它的属性,就像这些规范中定义的那样。第五章将会包含更多关于属性的细节。

除了 DOM 之外,当与 web API 中不依赖于 DOM 的部分进行交互时,也使用 JavaScript,例如 web 消息传递 API, 31 ,它是 W3C html 5 规范的一部分。Web 消息传递 API 为不同的浏览上下文提供了一种通过消息传递相互通信的方式。这为不同领域的两个iframe之间的通信,甚至是浏览器主 UI 线程和 Web Worker 线程之间的通信打开了一个简单的途径。 32 该规范定义了一个MessageEvent接口, 33 ,允许客户端监听传递的消息。在 JavaScript 中,这个事件对象在所有现代浏览器中实现,并允许开发人员使用在windowdocumentelement对象上可用的addEventListener方法来监听消息。这些对象从EventTarget接口获得这个方法,你可能还记得本章前面的内容,它是顶级接口,许多其他本地浏览器对象都是从这个接口继承而来的。我将在第九章中更详细地介绍事件处理。作为第八章的一部分,我们会更详细地介绍 Web 消息 API。

尽管 JavaScript 是使用特定于浏览器的本地 API 的关键,但作为一种语言,它不能与 web 规范本身相混淆。JavaScript 用于在浏览器中与这些 web 规范的实现进行交互,但是语言本身有自己的规范:ECMAScript, 34 ,我将在下一节对此进行更多的讨论。请注意,它并不依赖于 web,尽管它可以在所有的 Web 浏览器中实现。在某些方面,web API 建立在 JavaScript API 提供的基础之上。Array s、 35 Object s、 36 Function s、 37 以及布尔和字符串 38 等原语都在 JavaScript 规范中定义,在浏览器中(以及其他环境中)可用。ECMAScript 规范的这些核心元素被进一步定义为具有额外的属性。例如,Array s 包含一个检索特定项目索引的方法,实现为indexOf【39】Function接口包含apply 40call 41 方法,这些方法可以很容易地调用具有备用上下文(值为this ) 42 的函数以及传递参数。第十二章包含了大量与特定于 JavaScript 的实用函数相关的细节,将它们与 jQuery 的高级包装方法进行了比较。

历史和标准化

JavaScript 的故事从 Brendan Eich 开始,他在 1995 年作为 Netscape 的员工,在十天内开发了该语言的第一个工作版本。一种在 Netscape Navigator 中运行的脚本语言将被创建,并被要求“像 Java 一样”。艾希被任命承担这一愿景,并使之成为现实。结果是混合了 C,Self, 43 和 Scheme, 44 和一点 Java 的味道。45JavaScript 化身的更多细节可以在 Brendan Eich 的博客中找到。 46

在正式的标准化过程建立之前,JavaScript 实际上是一种专有语言,只被 Netscape 在其旗舰浏览器中使用。但在 Netscape Navigator 中实现该语言后不久,微软创建了自己的实现 JScript,它首先在 Internet Explorer 3 中引入。JScript 和 Netscape 的 JavaScript 在名称上很相似。微软选择这个名字是为了避免 Java 商标的拥有者可能引起的任何商标纠纷, 47 当时是 Sun Microsystems。

在 JScript 出现后不久,一个正式的语言规范就被起草并在后来被采用。但是缺乏标准化,即使是在相对较短的时间内,已经对网络造成了明显的影响。1996 年末,Netscape 与欧洲计算机制造商协会(ECMA)接洽,以创建一个正式的语言规范。这在一定程度上是由 Netscape Navigator 和 Microsoft Internet Explorer 3 之间的语言实现差异引起的。第一个规范于 1997 年 6 月完成,命名为 ECMA-262,也称为 ECMAScript。该规范目前由 ECMA 技术委员会 39(也称为 TC39)制定,该委员会是一个受委托发展和维护该语言的个人团体。TC39 的成员包括重量级人物,如道格拉斯·克洛克福特、布伦丹·艾奇和耶胡达·卡茨(他也是 jQuery 基金会的成员)。

ECMAScript 语言规范的第一个版本发布于 1997 年,名为“ECMAScript- 262,第一版”在撰写本文时,第 7 版刚刚完成。尽管在语言的整个生命周期中,新规范的发布是不一致的,但从第 6 版开始,每年发布更新规范的概念似乎越来越流行。为了证明这一目标,该规范的第 6 版也被命名为“ECMAScript 2015”,第 7 版被命名为“ECMAScript 2016”,并假设第 8 版将是“ECMAScript 2017”,以此类推。一组“和谐”规范也是讨论的焦点,这个术语的关键似乎与规范的一般顺序有关。版本 4 和更早的版本是不和谐的,而版本 5 和更早的版本是和谐的,这一时期的规范被命名为“和谐”应该注意的是,规范的第 4 版实际上从未完成。相反,它被整合到 ECMAScript 5 中。和谐, 49 在这个上下文中,可能指的是目标和需求,比如“成为更好的写作语言”,以及“保持语言对于临时开发人员的愉悦”语言规范的和谐运动的另一个目标是“建立在 ES5 严格模式的基础上,以避免过多的模式。”换句话说,简化和可用性给语言带来了和谐。在撰写本文时,ECMAScript 3 的浏览器支持包括现有的所有浏览器。所有“现代”浏览器都完全支持 ECMAScript 5。ECMAScript 2015 目前在所有浏览器的大多数当前版本中都有不错的支持。ECMAScript 2016 目前的支持是不稳定的,但当然会随着时间的推移而改善。

Footnotes 1

https://www.w3counter.com/globalstats.php?year=2016&month=6

  2

http://gs.statcounter.com/#all-comparison-ww-monthly-201404-201606

  3

http://stratechery.com/2015/mobile-first/

  4

www.gartner.com/newsroom/id/2944819

  5

www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay

  6

https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html

  7

https://code.google.com/p/v8/

  8

www.w3.org/DOM/DOMTR

  9

www.w3.org/TR/1998/REC-DOM-Level-1-19981001/

  10

www.w3.org/TR/DOM-Level-3-Core/

  11

www.w3.org/TR/DOM-Level-2-HTML/

  12

http://dev.w3.org/2006/webapi/selectors-api2/

  13

http://dev.w3.org/csswg/selectors-4/

  14

https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html

  15

www.w3.org/TR/domcore/

  16

www.w3.org/TR/html5/

  17

www.w3.org/TR/html5-diff/

  18

www.w3.org/html/wg/drafts/html/master/semantics.html#the-picture-element

  19

www.w3.org/TR/FileAPI/

  20

https://xhr.spec.whatwg.org/

  21

www.w3.org/TR/html5/browsers.html#the-history-interface

  22

www.w3.org/TR/html5/browsers.html#the-history-interface

  23

https://fetch.spec.whatwg.org

  24

www.w3.org/Consortium/facts#history

  25

https://notifications.spec.whatwg.org

  26

https://wiki.whatwg.org/wiki/FAQ#What_is_the_WHATWG.3F

  27

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-getAttribute

  28

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-setAttribute

  29

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-removeAttribute

  30

www.w3.org/TR/DOM-Level-2-Core/core.html#ID-ElHasAttr

  31

www.w3.org/TR/DOM-Level-2-Core/core.html#ID-ElHasAttr

  32

www.w3.org/TR/workers/

  33

www.w3.org/TR/webmessaging/#the-messageevent-interfaces

  34

www.ecmascript.org

  35

www.ecma-international.org/ecma-262/5.1/#sec-15.4

  36

www.ecma-international.org/ecma-262/5.1/#sec-15.2

  37

www.ecma-international.org/ecma-262/5.1/#sec-15.3

  38

www.ecma-international.org/ecma-262/5.1/#sec-4.3.2

  39

www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.14

  40

www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.3

  41

www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.4

  42

www.ecma-international.org/ecma-262/5.1/#sec-10.3

  43

http://handbook.selflanguage.org/4.5/intro.html

  44

www.scheme.com/tspl4/intro.html

  45

www.oracle.com/technetwork/topics/newtojava/downloads/index.html

  46

https://brendaneich.com/2008/04/popularity/

  47

http://yuiblog.com/blog/2007/01/24/video-crockford-tjpl/

  48

www.ecma-international.org/memento/TC39.htm

  49

http://wiki.ecmascript.org/doku.php?id=harmony%3Aharmony

四、查找 HTML 元素

您遇到过多少次这样的项目:仅仅使用 jQuery 来执行看似微不足道的元素选择?你写了多少次$('#myElement')或者$('.myElement')?如果您的大多数(或所有)项目都依赖于 jQuery,那么您可能没有意识到这样一个事实,即您不需要 jQuery 来选择元素!借助普通的 ole boring web API,这项任务相当简单。与 jQuery 非常相似,有大量的例子(如本书)展示了如何恰当地利用浏览器的能力来快速选择文档中的任何元素。所有这些都不是什么秘密,但是 jQuery 的普及性让许多人相信找到元素的唯一合理的方法是借助万能的美元符号。没有什么比这更偏离事实了,当你继续读下去,所有这些都会变得清晰。

这里描述的所有选择元素的方法在所有现代浏览器中都受支持。事实上,许多在古代浏览器中也受支持。换句话说,除非您正在支持一个老化的遗留 web 应用,否则即使是最复杂的用于选择元素的本地解决方案也可以在没有任何库的帮助下使用。对于那些仍然依赖于过去浏览器的旧应用来说呢?您可以用几行代码轻松复制任何缺少的本机方法。事实上,我将提供一些简单直观的解决方案来帮助您填补古代浏览器中的重要空白。

尽管 jQuery 确实为您节省了一些击键次数,但是您将会牺牲性能。这种类型的高级抽象比直接依赖原生 API 要慢,这是可以理解的。那么,如果您希望既有 jQuery 的便利性,又没有巨大的依赖性和潜在的性能问题,该怎么办呢?很简单,在 web API 周围创建你自己的非常薄的包装器来节省你的一些击键。在我们结束本章之前,我将用一些示例代码来探索这种可能性。

核心元素选择器

在关于在文档中查找 HTML 元素这一主题的第一部分中,我讨论了通过使用一些更传统的元素属性来选择元素,比如 ID、类和标记名。在这里,我通过例子比较了 jQuery 中的元素选择和“普通”JavaScript,这些例子利用各种 web API 规范中的功能直接与 DOM 交互。完成本节之后,您将有必要的信心和理解来使用最常见的方法在 DOM 中选择元素——完全不依赖 jQuery。

本能冲动

W3C HTML4 规范 1id属性定义为在文档内定义的所有 id 中必须是唯一的。规范的这一部分继续描述它的主要用途, 2 比如元素选择和使用锚链接导航到页面的其他部分。DOM Level 1 规范定义了HTMLElement接口、 3 ,所有其他元素都从该接口继承。在这个接口中定义了id属性,它直接连接到在标记中相应元素上定义的id属性。

例如,考虑以下标记:

1  <div id="my-element-id"></div>

<div>元素的id属性也可以通过元素的 JavaScript 表示来访问。这是由元素对象的id属性公开的:

1  // `theDiv` is the <div> from our sample HMTL above

2  theDiv.id === 'my-element-id'; // returns true

框架

在 jQuery-land 中,获取<div>元素对象的句柄类似于清单 4-1 。

1  // returns a jQuery object with 1 element -

2  // the <div> from our sample HMTL above

3  var result = $('#my-element-id');
4
5  // assuming our element has been found in the document

6  result.is('#my-element-id'); // returns true

Listing 4-1.Select by ID: jQuery

在 jQuery 示例中,我们使用 ID 选择器字符串,它最初是在 W3C CSS1 规范中定义的。 4 这次选择尝试返回的 jQuery 对象是一个伪数组(第十二章对此有更详细的讨论)。这个伪数组包含文档中这个元素的对象表示。

Web API

在没有 jQuery 帮助的情况下选择完全相同的元素非常容易,事实上实现这一点的代码看起来非常相似。使用 web API 通过 ID 选择元素有两种不同的方式。第一个这样的方法涉及到使用在Document接口上定义的getElementById方法,它首先在 DOM Level 2 Core 规范中正式化。 5 现有的所有浏览器都支持这种方法(最早在 Internet Explorer 5.5 中实现):

1  // returns the matching HTMLElement - the <div> from our sample

2  var result = document.getElementById('my-element-id');
3
4  // assuming our element has been found in the document

5  result.id === 'my-element-id'; // returns true

第二种方法利用了querySelector方法,该方法首先在 W3C 选择器 API Level 1 规范中的文档和元素接口上定义。 6 记住定义了id属性的HTMLElement接口继承自Element接口,所以Element s 也有一个id属性。所有现代浏览器都有querySelector方法,包括 Internet Explorer 8。在清单 4-2 中,您将开始注意到本地方法和 jQuery 快捷方式之间的一些明显的相似之处。

1  // returns the matching HTMLElement - the <div> from our sample

2  var result = document.querySelector('#my-element-id');
3
4  // assuming our element has been found in the document

5  result.id === 'my-element-id'; // returns true

Listing 4-2.Select by ID: Web API, Modern Browsers and Internet Explorer 8

Performance Note

querySelectorgetElementById、、、稍慢,但是随着浏览器 JavaScript 引擎的发展,这种性能差距正在缩小。

班级

与 IDs 的关注点相反,类属性并不唯一地标识文档中的元素。相反,传统上使用类在应用的上下文中将元素作为一个整体进行语义分组。虽然 id 当然可以用于通过 CSS 设计元素的样式,但是这个角色通常与类属性联系在一起。元素也可以被分配多个类名,而它们只限于一个 ID(原因很明显)。HTML 4.01 规范对类属性的角色进行了更详细的描述。 8 第五章更详细地讨论了元素属性的使用。

一般来说,有效的 CSS 类不区分大小写,只能包含字母数字字符或连字符或下划线,并且不能以一个数字或两个连字符或一个连字符和一个数字开头。这些规则也适用于 id,以及用于通过 CSS 定位元素的其他元素属性。你可以阅读 CSS 2.1 规范中 CSS 选择器的所有允许值。 9

例如,考虑以下标记:

1  <span class="some-class"></span>

通过对象的className属性上的元素的 JavaScript 表示,也可以访问span元素的class属性。注意这里的不一致性——属性名是class,而对应的Element属性是className。这是因为class是许多语言中的保留字,例如 JavaScript(甚至晚于 ECMAScript 5.1 版规范), 10 这就是为什么在元素的 JavaScript 表示中存在备用名称。例如:

1  // `elementObject` is the <span> in our sample markup above

2  elementObject.className === 'some-class'; // returns true

框架

在 jQuery 中按类选择元素看起来非常类似于用来选择 ID 的方法。事实上,jQuery 中的所有元素选择都遵循相同的模式:

1  // Returns a jQuery object with 0 elements (element not found)

2  // or all elements with the 'some-class' class attribute.

3  var result = $('.some-class');
4
5  // assuming our element has been found in the document

6  result.is('.some-class'); // returns true

如果文档中碰巧有三个不同元素的类名为some-class,那么result jQuery 对象将有三个条目,每个条目对应一个匹配。

Web API

与 IDs 一样,使用 web API 通过类名选择元素有几种不同的方法。我将演示其中的两个——都可以在现代浏览器和 Internet Explorer 8 中使用(最后一个例子)。清单 4-3 是性能最好的,但是清单 4-4 显然是最优雅的。

1  // Returns an HTMLCollection containing all matching elements,

2  // which is empty if there are no matches.

3  var result = anyElement.getElementsByClassName('some-class');
4
5  // assuming our element has been found in the document

6  result[0].className === 'some-class'; // returns true

Listing 4-3.Select by Class: Web API, Modern Browsers

getElementByIdgetElementsByClassName之间第一个值得注意的区别是,后者返回一个类似数组的对象,包含所有匹配的元素,而不是单个元素。记住,一个文档可以包含许多共享相同类名的元素。您可能还会注意到另一个差异,在提供的简单示例中,这个差异可能不是特别明显。getElementsByClassName方法在Document界面上可用,就像getElementById一样。但是,在 W3C DOM4 规范中,它也被定义为元素接口上的方法。 11 这意味着在文档中指定一个元素来查找类名匹配时,可以将查询限制在特定的元素子集。当在特定元素上执行时,只检查后代元素的匹配。这允许更集中和更有效的 DOM 遍历。

getElementsByClassName方法的返回值是一个 HTMLCollection,这是一个伪数组,它提供了有序的数字属性(0,1,2,.。。),每个匹配元素一个,还有一个length属性和一些其他方法(用处有限)。一件HTMLCollection最显著的特点是它是一件活的收藏品。也就是说,它会自动更新,以匹配它所代表的 DOM 中的底层元素。例如,如果包含在返回的HTMLCollection中的一个元素从 DOM 中移除,那么它也将从范围内的任何HTMLCollection中移除。注意,getElementsByClassName 是在 W3C DOM4 规范中定义的。

第二种方法,如清单 4-4 所示,通过类名选择元素,涉及到前面演示的querySelector的一个表亲。

1  // Returns a NodeList containing all matching elements,

2  // which is empty if there are no matches.

3  var result = anyElement.querySelectorAll('.some-class');
4
5  // assuming our element has been found in the document

6  result[0].className === 'some-class'; // returns true

Listing 4-4.Select by Class: Web API, Modern Browsers and Internet Explorer 8

getElementsByClassNamequerySelectorAll在一个类似数组的对象中返回所有匹配。不过,差异也就到此为止了。例如,querySelectorAll返回一个NodeList对象,这是一个首先在 W3C DOM Level 3 核心规范中正式定义的接口。 12 NodeListHTMLCollection有一个重要的区别:它不是一个“活”的系列。因此,如果一个包含在NodeList中的匹配元素从 DOM 中移除,它不会从任何NodeList中移除。

根据 W3C 选择器 API,querySelectorAllDocument接口和Element接口上都是可用的(就像getElementsByClassName)。13querySelector方法也可以在查找具有特定类名的元素时使用,它将只返回第一个匹配的元素,这在某些情况下实际上可能是可取的。无论哪种情况,都必须传递 CSS 选择器字符串。在查找类名时,我们必须包含一个“.”前缀,它最早是在 CSS 1 规范中描述的, 14 ,尽管更多的细节包含在后来的 CSS 2.1 规范中。 15 虽然getElementsByClassName在 IE8 中不可用,但是您也可以通过简单地将 CSS 类选择器字符串传递给querySelectorAll方法,在浏览器中通过类名定位元素。

如果您必须支持 Internet Explorer 7 或更早的版本,通过类名选择元素的方法可能有点麻烦。因为这种类型的支持正在迅速失宠,所以我选择忽略丑陋且低效的遗留解决方案(jQuery 无论如何都必须依赖它)。你可以看看我维护的一个库是如何解决 16 支持古代浏览器的问题的。

元素标签

任何元素最基本的属性就是它的名字。早在 1993 年,由蒂姆·伯纳斯·李部分起草的 IETF HTML 规范中, 17 一个有效的元素/标签名可以“由一个字母后跟多达 33 个字母、数字、句点或连字符组成”该规范继续说“名称不区分大小写”在这个文档中还定义了少量的元素,比如锚标记(<a>)、段落标记(<p>)和用于提供联系信息的<address>元素。自从这个第一个规范以来,已经添加了更多的元素,例如在相对较新的 HTML5 规范中添加的<video> 18<audio>19

尽管定制元素没有被浏览器明确禁止,但是在 Web 组件规范出现之前,没有什么动机去创建它们。 20 Web Components 是一个规范集合,其中一个是自定义元素规范, 21 详细说明了创建新的HTMLElement的方法,这些新的HTMLElement具有自己的 API 和属性,甚至是现有元素的扩展——例如 ajax 表单自定义元素, 22 扩展并添加了原生<form>的功能。

为了设置我们的示例,考虑以下非常简单的 HTML 块:

1  <code>System.out.println("Hello world!");</code>

如果给你一个元素引用,你可以通过tagName属性很容易地确定元素的名称,这个属性是在元素接口上定义的,是 DOM Level 1 Core 的一部分: 23

1  // `elementObject` is the <code> element from our above HTML

2  elementObject.tagName === 'code'; // returns true

框架

可以预见的是,通过将 CSS 元素选择器传递到$jQuery函数中,可以方便地通过 jQuery 选择元素:

1  // Returns a jQuery object with 0 elements (element not found)

2  // or all elements with a matching tag name.

3  var result = $('code');
4
5  // assuming our element has been found in the document

6  result.is('code'); // returns true

这里没什么神奇的。事实上,元素名称选择器字符串的语法是在第一个 CSS 规范中定义的。jQuery 只是为本地方法提供了一个简单的别名,这些本地方法可以通过标签名来选择元素,接下来将对此进行探讨。

Web API

让我们从通过直接与本地 web API 交互来快速查看通过标记名选择元素的传统方法开始:

1  // Returns a HTMLCollection containing all matching elements,

2  // which is empty if there are no matches.

3  var result = anyElement.getElementsByTagName('code');
4
5  // assuming our element has been found in the document

6  result[0].tagName === 'code'; // returns true

前面的方法早在 DOM Level 1 Core 就已经有了,而且和getElementsByClass- Name一样,在文档接口 25 和元素接口上都有。 26 所以,这种方法在现有的所有浏览器上都是可用的。

如你所料,更“现代”的方法包括querySelectorquerySelectorAll:

 1  // Returns a NodeList containing all matching elements,

 2  // which is empty if there are no matches.

 3  var result = anyElement.querySelectorAll('code');
 4
 5  // assuming our element has been found in the document

 6  result[0].tagName === 'code'; // returns true

 7
 8  // -OR-

 9
10  // ...you can use this if you know there is only one <code>

11  // element to find, or if you only care about the first.

12  // Returns true.

13  anyElement.querySelector('code').tagName === 'code';

目前在getElementsByTagNamequerySelectorAll(tagName)之间存在潜在的显著性能差异。 27 使用querySelectorAll的性能后果显然是由于getElementsByTagName返回 DOM 中匹配元素的动态集合(一个 HTMLCollection),而querySelectorAll返回静态集合(一个 NodeList)。后者需要遍历 DOM 中的所有元素,而前者返回缓存的匹配元素,然后在访问列表时查询文档更新。 28 这种性能差异类似于getElementsByClassNamequery- SelectorAll(classSelector)出于同样的原因。

伪类

虽然在 CSS 规范的最新版本中,伪类的流行和数量有了很大的增长,但是伪类从 CSS 规范的最早版本就已经存在了。 29 伪类是向选择器字符串或元素组添加状态的关键字。例如,锚选择器字符串上的:visited伪类将指向用户已经访问过的任何链接。另一个例子是,:focus伪类将把被确定为具有焦点的元素作为目标,比如用户当前正在交互的文本输入字段。在下面的例子中,我们将使用后者,因为出于隐私考虑,浏览器会阻止 JavaScript 中的程序选择器访问被访问的链接。 30

为了设置我们的示例,让我们创建一个带有几个文本输入的简单表单,并假设用户点击了(或跳转到)最后一个文本输入(名为“company”)。这最后一个输入将是“聚焦”的输入:

1  <form>
2     <label>Full Name
3        <input name="full-name">
4     </label>
5     <label>Company
6        <input name="company">
7     </label>
8  </form>

框架

假设我们想使用 jQuery 选择当前关注的输入:

1  // Return value will be a jQuery object containing the

2  // "company" input element

3  var focusedInputs = $('INPUT:focus');

前面同样是一个标准化的 CSS 选择器字符串。我们使用了一个带有伪类修饰符的标签名选择器。jQuery 并没有为我们做什么特别的事情。事实上,它只是简单地直接委托给 web API。

Web API

请考虑以下几点:

1  // Return value will be the "company" text input field element

2  var companyInput = document.querySelector('INPUT:focus');

这段代码避免了所有与通过 jQuery 过滤调用相关的开销。如果我们使用 jQuery(就像我们在前面的例子中所做的那样),querySelectorAll将被 jQuery 的选择器代码用完全相同的选择器字符串在内部调用。由于一次只能有一个元素有焦点,querySelectorquerySelectorAll更合适。它也更快一点,因为同样的原因,任何一种getElementsBy方法都比querySelectorAll快。

根据元素之间的关系选择元素

有了一些在头脑中记忆犹新的选择元素的基本方法,我们就可以通过元素选择器进入下一步了。以下部分介绍了如何根据元素与其他元素的关系来选择元素。我们将研究定位子元素和子元素、子元素的父元素以及其他元素的兄弟元素。DOM 被组织成树状结构。考虑到这一点,能够在考虑关系的情况下导航这种节点层次结构通常是有利的。正如我们在核心选择器一节中已经看到的那样,在没有 jQuery 的情况下,根据元素之间的关系查找元素相当简单,而且性能更好。

父母和孩子

从我们对 DOM API 的讨论中可以看出,ElementNode的一种特定类型。如果一个NodeElement是一个“叶”节点,那么它可以没有孩子。否则,它将有一个或多个直接子级。但是文档中的每个NodeElement都只有一个直接父级。嗯,差不多了。这个规则有两个例外:一个出现在<html>标签(HTMLHtmlElement ) 31 中,它是文档中的根Element,因此没有父Element(尽管它有一个父Node : document)。这就把我们带到了第二个异常,即既没有父对象Node也没有父对象Elementdocument对象(Document)32。是根Node

清单 4-5 显示了一个简单的 HMTL 片段。

1  <div>
2     <a href="http://fineuploader.com">
3        <span>Go to Fine Uploader</span>
4     </a>
5     <p>Some text</p>
6     Some other text
7  </div>

Listing 4-5.Example Markup for Parent/Children Traversal Examples

在下面的代码示例中,将区分目标子/父Node s 和Element s。如果这种区分还不清楚,首先要理解清单 4-5 是由Element类型对象组成的,例如<div><a><span><p>。这些Element也是Node,因为Element接口是Node接口的子类型。但是片段的“转到精细上传器”、“一些文本”和“一些其他文本”部分不是Element,但是它们是Node,更具体地说,它们是Text项。Text接口 33CharacterData接口 34 的一个子类型,它本身实现了Node接口。

框架

jQuery 的 API 包括一个parent方法。为了简单起见,我们假设“当前 jQuery 对象”只表示一个元素。当在这个对象上调用parent方法时,产生的 jQuery 对象将包含父对象Element,或者在极少数情况下,包含不是Element的父对象Node。参见清单 4-6 。

 1  // Assuming $a is a reference to the anchor in our example HTML,

 2  // $result will contain the <div> above it.

 3  var $result = $a.parent();
 4
 5  // Assuming $span is a reference to the <span> in our example HTML,

 6  // the first parent() call references the <a> element, and the

 7  // $result will contain the <div> root element.

 8  var $result = $span.parent().parent();
 9
10  // Assuming someText is a reference to the "Some text" Text node,

11  // the result will contain the <p> element in our example HTML.

12  // Note: selecting a text node requires locating the node in the result of

13  // using the `contents()` method, as illustrated in the next code block.

14  var $result = $someText.parent();
Listing 4-6.
Get Parent Element/Node:

jQuery

为了定位子元素,jQuery 提供了一个children()方法,该方法将返回给定元素的所有直接子元素Element。您还可以使用 CSS 2.1 W3C 规范中标准化的子选择器选择给定引用元素的子元素。 35 但是由于children()只会返回Element s,所以我们必须使用 jQuery 的contents() API 方法来获取任何不同时属于Element s 的Node s,比如Text节点。同样,为了保持简单,清单 4-7 假设我们示例中的引用 jQuery 对象只引用 DOM 中的一个特定元素。

 1  // Assuming $div is a jQuery object containing the <div> in our example HTML,

 2  // $result will contain 2 elements: <a> and <p>.

 3  var $result = $div.children();
 4
 5  // $result contains the <p> element in the sample markup

 6  var $result = $('DIV > P');
 7
 8  // Again, assuming $div refers to the <div> in our example markup,

 9  // $result will contain 3 nodes: <a>, <p>, and "Some other text".

10  var $result = $div.contents();
11
12  // Assuming $a refers to the <a> element in our example markup,

13  // $result will contains 1 element: <span>.

14  var $result = $a.children();
15
16  // This returns the exact same elements as the previous example.

17  var $result = $('A > *')
Listing 4-7.Get Child Elements and/or Child Nodes: jQuery

Web API

在大多数情况下,不使用 jQuery 来定位元素/节点的父元素很简单。DOM Level 2 Core 是第一个在Node接口上定义一个parentNode属性的规范, 36 正如您所料,它被设置为引用元素的父元素Node。当然,这个值可以是一个Element或任何其他类型的节点。后来,在随后的 W3C DOM4 规范中,一个parentElement属性被添加到了Node接口中。 37 这个属性永远是一个Element。如果引用Node的父级是除Element之外的某种类型的Node,那么parentElement将是null。但是大多数情况下,parentElementparentNode会是相同的,除非参考节点是<html>,在这种情况下parentNode会是documentparentElement当然会是null。一般来说,特别是由于广泛的浏览器支持,parentNode属性是最好的选择,但是parentElement几乎一样安全。参见清单 4-8 。

 1  // Assuming "a" is the <a> element in our HTML example,

 2  // "result" will be the the <div> above it.

 3  var result = a.parentNode;
 4
 5  // Assuming "span" is the <span> element in our HTML example,

 6  // the first parentNode is the <a>, while "result" is the <div>

 7  // at the root of our example markup.

 8  var result = span.parentNode.parentNode;
 9
10  // Assuming "someText" is the "Some text" Text node in our HTML example,

11  // "result" will be the the <p> that contains it.

12  var result = someText.parentNode;
Listing 4-8.
Get Parent Element/Node:

Web API

使用 web API 有许多不同的方法来定位元素的直接子元素。接下来我将演示两种这样的方法,并简要讨论第三种方法。在所有现代浏览器中定位元素子元素的最简单也是最常见的方法是使用ParentNode接口上的children属性。 38 ParentNode被定义为由ElementDocument接口共同实现,尽管它通常只在Element接口上实现。它适用于可能有孩子的Node。它最初是在 W3C DOM4 规范 39 中定义的,只在现代浏览器中可用。ParentNode.children返回一个HTMLCollection中引用Node的所有子节点,您可能还记得本章前面的内容,它代表了一个Element的“活动”集合:

1  // Assuming "div" is an Element object containing the <div> in our example HTML,

2  // result will contain an HTMLCollection holding 2 elements: <a> and <p>.

3  var result = div.children;

第二种用于定位孩子Element s 的方法包括使用querySelectorAll和 CSS 2 子选择器。这种方法允许我们支持 Internet Explorer 8,以及所有现代浏览器。记住,querySelectorAll返回一个NodeList,它不同于HTMLCollection,因为它是元素的“静态”集合。在这种情况下,集合包含父节点Node的所有Element子节点:

1  // The result will contain a NodeList holding 2 elements: <a> and <p>

2  // from our HTML fragment above.

3  var result = document.querySelectorAll('DIV > *');
4
5  // The result will be all <p> children of the <div>, which, in this case

6  // is only one element: <p>Some text</p>.

7  var result = document.querySelectorAll('DIV > P');

使用 web API 选择孩子的第三个选项涉及到了Node接口上的childNodes属性。 41 这个属性是在最初的 W3C DOM Level 1 核心规范中声明的。 42 结果是所有浏览器都支持,甚至是古代的。属性childNodes将显示所有子节点Node,甚至TextComment节点。您可以简单地通过迭代结果来过滤掉集合中的非Element对象,忽略任何具有不等于1nodeType属性 43 的对象。这个nodeType属性也是在最初的Node接口规范中定义的:

1  // Assuming "div" is an Element object containing the <div> in

2  // our example HTML, result will contain a NodeList

3  // holding 3 Nodes: <a>, <p>, and "Some other text".

4  var result = div.childNodes;

给定一个父节点Node,你也可以分别通过恰当命名的firstChildlastChild属性定位第一个和最后一个子节点。这两个属性在最初的Node接口规范中就已经存在,并且它们引用子Node s,所以第一个或最后一个子元素可能是Text NodeHTMLDivElementfirstChild属性可以作为第四种方法的一部分,用来获取父Node的子对象。这种方法将在下面的兄弟元素选择一节中讨论。

同科

如果它们共享同一个直系父代,那么它们就是兄弟姐妹。它们可能是相邻的兄弟姐妹(彼此紧挨着)或“一般的”兄弟姐妹(不一定紧挨着)。有多种方法可以在兄弟Node中查找和导航。虽然我将介绍如何使用 jQuery 来实现这一点以供参考,但是您将会看到在没有 jQuery 的情况下实现这一点是多么容易。清单 4-9 将被用作所有演示代码的参考点。

1  <div id="parent">
2     <a href="https://github.com/rnicholus">GitHub</a>
3     <span>Span text</span>
4     <p>Paragraph text</p>
5     <div>Div text</div>
6     Text node
7  </div>
Listing 4-9.Working with Siblings: Markup for Following Demos

框架

为了找到给定Element的所有兄弟Element,jQuery 提供了一个siblings方法作为其 API 的一部分。对于遍历给定Element的兄弟节点,也有next()prev()方法。为了简单起见,我将简单回顾一下我们是如何使用 jQuery 来查找和遍历给定元素的兄弟元素的,从清单 4-10 开始。

 1  // $result will be a jQuery object that contains <a>, <span>, <p>,

 2  // and <div> elements inside of the #parent <div>.

 3  var $result = $('SPAN').siblings();
 4
 5  // $result will be a jQuery object that contains the <a> element

 6  // that precedes the <span>.

 7  var $result = $('SPAN').prev();
 8
 9  // The first next() refers to the <p>, and the 2nd next()

10  // refers to the <div>Div text</div> element, which is also

11  // the element contained in the jQuery $result object.

12  var $result = $('SPAN').next().next();
13
14  // The first next() refers to the <p>, and the 2nd next()

15  // refers to the <div>Div text</div> element. The final next()

16  // does not reference any element, since the final Node in the

17  // fragment is a Text Node, and not an element. So, the $result

18  // is an empty jQuery object.

19  var $result = $('SPAN').next().next().next();
Listing 4-10.Find and Traverse Through Siblings: jQuery

您还可以在 jQuery 中使用 CSS 同级选择器,我们将在下一节中对此进行探讨。jQuery 实际上允许标准化的 W3C CSS 选择器字符串用于这一操作和其他操作。

Web API

为了反映 jQuery API 提供的行为,我将讨论以下与兄弟遍历和发现相关的主题:

  1. 定位特定ElementNode的所有兄弟。
  2. 浏览特定ElementNode的前面和后面的兄弟。
  3. 使用 CSS 选择器定位一个Element的普通兄弟和相邻兄弟。
  4. 使用Node接口上的兄弟属性定位子节点。

定位另一个Element的所有兄弟Element的最简单方法是使用 CSS3 通用兄弟选择器。 44 这种方法可以追溯到 Internet Explorer 8,并为您提供所有同级ElementNodeList,W3C CSS2 规范定义了一个“相邻”同级选择器, 45 ,它只选择与引用元素之后出现的选择器相匹配的第一个Element。清单 4-11 展示了这里描述的两个兄弟选择器。

 1  // "result" contains a NodeList of all siblings that occur after the <span>

 2  // in our example HMTL at the start of this section. These siblings are

 3  // the <p> and the <div> elements.

 4  var result = document.querySelectorAll('#parent > SPAN ∼ *');
 5
 6  // Another general sibling selector that specifically targets any

 7  // subsequent siblings of the <span> that are <div>s. In our case,

 8  // there is only one such element - <div>Div text</div>. The

 9  // "result" variable is a NodeList containing this one element.

10  var result = document.querySelectorAll('#parent > SPAN ∼ DIV');
11
12  // This is an adjacent sibling selector in action. It will target

13  // the first sibling after the <span>. So, "result", is the same

14  // as in the previous general sibling selector example.

15  var result = document.querySelector('#parent > SPAN + *');
Listing 4-11.
Find Siblings Using

CSS Selectors

: Web API, Modern Browsers, and Internet Explorer 8

您会注意到,通用同级选择器(∾)不选择引用元素之前的任何元素,只选择引用元素之后的元素。如果您确实需要考虑引用元素之前的任何兄弟元素,您将需要使用首先在 W3C DOM Level 1 Core46中定义的Node.previousSibling属性,或者使用首先在 W3C 元素遍历规范中定义的ElementTraversal接口 47 的一部分previousElementSibling属性。 48

ElementTraversal是由任何也实现了Element接口的对象实现的接口。简单地说,所有 DOM 元素都有一个previousElementSibling属性。清单 4-12 展示了这一点。

 1  // Find all siblings that follow the <span> in our example HTML

 2  var allSiblings = document.querySelectorAll('#parent > SPAN ∼ *');
 3
 4  // Converts the allSiblings NodeList into an Array.

 5  allSiblings = [].slice.call(allSiblings);
 6
 7  var currentElement = document.querySelector('#parent > SPAN');
 8
 9  // This loop executes until we run out of previous siblings,

10  // starting with the sibling before the <span>. Each sibling

11  // is added to the allSiblings array. After this loop is complete,

12  // the allSiblings array will contain all siblings of the <span>

13  // (before and after).

14  do {
15     currentElement = currentElement.previousElementSibling;
16     currentElement && allSiblings.unshift(currentElement);
17  } while (currentElement);
Listing 4-12.
Find Both Preceding and

Subsequent

Siblings of a Reference Element: Web API, Modern Browsers

Note

另一种方法是选择引用元素的父元素,然后收集其子元素,忽略引用元素。本节中的代码是专门为演示一些标准 CSS 选择器和元素属性而创建的。

对于 Internet Explorer 8 支持,您必须使用Node.previousSibling而不是Element.previousElementSibling。这是因为在 9 以前的任何版本的 Explorer 中都不支持元素遍历规范。该属性返回任何Node,因此如果您只想接受Element s,您将需要确保添加一个nodeType属性检查。参见清单 4-13 。

 1  var allSiblings = document.querySelectorAll('#parent > SPAN ∼ *');
 2
 3  // Converts the allSiblings NodeList into an Array.
 4  var allSiblings = [].slice.call(allSiblings);
 5
 6  var currentElement = document.querySelector('#parent > SPAN');
 7
 8  do {
 9    currentElement = currentElement.previousSibling;
10    // This differs from the previous example in that we must

11    // exclude non-Element Nodes by examining the nodeType property.

12    if (currentElement && currentElement.nodeType === 1) {
13      allSiblings.unshift(currentElement);
14    }
15  } while (currentElement);
Listing 4-13.
Find Both Preceding and Subsequent Siblings of a Reference Element: Web API, Modern Browsers, and Internet Explorer

8

web API 还在Node接口上公开了一个nextSibling属性,在ElementTraversal接口上公开了一个nextElementSibling属性。 49 如清单 4-14 所示,浏览器对这些属性的支持与它们“以前的”表亲相同。

 1  // The first nextSibling refers to the <p>, and the 2nd nextSibling

 2  // refers to the <div>Div text</div> element. The final nextSibling

 3  // refers to the "Text node" Text Node, since nextSibling targets

 4  // any type of Node. So, the result is this Text Node.

 5  var result = document.querySelector('SPAN')
 6                  .nextSibling.nextSibling.nextSibling;
 7
 8  // Same as the above example, but the final nextElementSibling returns null,

 9  // since the last Node in the example markup is not an Element. There are only

10  // 2 Element siblings following the <span>. Note that nextElementSibling

11  // is not available in ancient browsers.

12  var result = document.querySelector('SPAN')
13                  .nextElementSibling.nextElementSibling.nextElementSibling;
Listing 4-14.Traverse Through All Subsequent Siblings:  Web API, Modern Browsers, and Internet Explorer 8

除了上一节概述的使用 web API 选择孩子的方法之外,还有另一个这样的选项,在任何浏览器中只选择父NodeElement孩子。这包括获取父元素NodefirstChild,定位第一个子元素的兄弟元素Node,然后使用每个NodenextSibling属性继续遍历所有兄弟元素,直到没有剩余的兄弟元素。最后,为了排除所有非元素的兄弟元素(比如Text Node s),只需检查每个NodenodeType属性,如果Node更具体地说是一个Element,那么这个属性的值就是1。这就是 jQuery 实现其children方法的方式,至少在库的 1.x 版本中是这样。这种实现选择可能是因为Node接口上的所有这些属性都有广泛的浏览器支持,甚至在古老的浏览器中。然而,现代浏览器支持更简单的方法,所以刚刚描述的路径实际上仅从学术或历史角度相关。

祖先和后代

为了说明祖先/后代Node的关系,让我们从一个简短的 HTML 片段开始:

 1  <body>
 2     <div>
 3        <span>random text</span>
 4           <ul>
 5              <li>
 6                 <span>item 1</span>
 7              </li>
 8              <li>
 9                 <a href="#some-content">item 2</a>
10              </li>
11           </ul>
12     </div>
13  </body>

一个元素的祖先是在 DOM 中出现在它之前的任何元素。也就是说,它的父母、其父母的父母(或祖父母)、其父母的父母的父母(曾祖父母),等等。在前面的 HTML 片段中,锚元素的祖先包括它的直接父元素(<li>),以及<ul><div>,最后是<body>元素。相反,一个元素的后代包括它的子元素、子元素的子元素等等。在前面的标记中,<ul>元素有四个后代:两个<li>元素、<span><a>

框架

jQuery 的 API 提供了一种方法来检索元素的所有祖先- parents():

1  // Using our HTML example, $result is a jQuery object that

2  // contains the following elements: <li>, <ul>,

3  // <div>, and <body>

4  var $result = $('A').parents();

但是,如果您只想检索匹配特定条件的第一个祖先,该怎么办呢?在我们的例子中,假设我们只寻找也是一个<div><a>的第一个祖先。为此,我们将使用 jQuery 的closest()方法。jQuery 通过强力实现closest()——通过检查引用Node的每个父节点:

1  // Using our HTML example, $result is a jQuery object that

2  // contains the <div> element.

3  var $result = $('A').closest('DIV');

为了定位后代,您可以使用 jQuery 的find()方法:

1  // Using our HTML example, $result is a jQuery object that

2  // contains the following elements: both <li>s, the <span>,

3  // and the <a>.

4  var $result = $('UL').find('*');
5
6  // $result is a jQuery object that contains the <span>

7  // under the first <li>.

8  var $result = $('UL').find('SPAN');

Web API

原生 web 不提供返回元素所有祖先的单一 API 方法。如果你的项目需要这样做,你可以利用Node.parentNode属性 50Node.parentElement,通过一个简单的循环来累积这些Node。记住,后者只针对一种特定类型的Node:一种Element。这通常是我们想要的,所以我们将在例子中使用parentElement。参见清单 4-15 。

 1  // When this code is complete, "ancestors" will contain all

 2  // ancestors of the anchor element: <li>, <ul>,

 3  // <div>, and <body>

 4  var currentNode = document.getElementsByTagName('A')[0],
 5      ancestors = [];
 6
 7  while (currentNode.parentElement) {
 8     ancestors.push(currentNode.parentElement);
 9     currentNode = currentNode.parentElement;
10  }
Listing 4-15.
Retrieve All Element Ancestors:

Web API

, Any Browser

我们已经知道,jQuery 提供了一种方法,允许我们轻松地找到元素的第一个匹配祖先,closest。web API 在Element接口上有类似的方法,也叫closest. Element.closest() 51 是 WHATWG DOM“生活标准”的一部分。 52 这个方法的行为和 jQuery 的closest()完全一样。截至 2016 年年中,任何版本的 Internet Explorer 和 Microsoft Edge 都不支持这种方法,但 Chrome、Firefox 和 Safari 9 支持这种方法。在下一个例子中,我将演示如何使用 web API 的closest()方法,我甚至为没有本机支持的浏览器提供了一个简单的后备。让我们再次使用我们的示例标记,并尝试定位<a>的最近祖先,即<div>。参见清单 4-16 和 4-17 。

Note

你可能还记得第三章的内容,WHATWG 开发了一套网络规范,与传统的 W3C 规范略有不同。

1  function closest(referenceEl, closestSelector) {
 2    // use Element.closest if it is supported

 3    if (referenceEl.closest) {
 4        return referenceEl.closest(closestSelector);
 5    }
 6
 7    // ...otherwise use brute force (like jQuery)

 8    // To find a match for our closestSelector, we must use the

 9    // Element.matches method, which is still vendor-prefixed

10    // in some browsers.

11    var matches = Element.prototype.matches ||
12          Element.prototype.msMatchesSelector ||
13          Element.prototype.webkitMatchesSelector,
14
15        currentEl = referenceEl;
16
17    while (currentEl) {
18      if (matches.call(currentEl, closestSelector)) {
19        return currentEl;
20      }
21      currentEl = currentEl.parentElement;
22    }
23
24    return null;
25  }
26
27  // "result" is the <div> that exists before the <a>

28  var result = document.querySelector('A').closest('DIV');
Listing 4-16.

Retrieve Closest Element Ancestor

: Web API, All Modern Browsers Except IE and Edge

 1  function closest(referenceEl, closestSelector) {
 2    // use Element.closest if it is supported

 3    if (referenceEl.closest) {
 4        return referenceEl.closest(closestSelector);
 5    }
 6
 7    // ...otherwise use brute force (like jQuery)

 8
 9    // To find a match for our closestSelector, we must use the

10    // Element.matches method, which is still vendor-prefixed

11    // in some browsers.

12    var matches = Element.prototype.matches ||
13          Element.prototype.msMatchesSelector ||
14          Element.prototype.webkitMatchesSelector,
15
16        currentEl = referenceEl;
17
18    while (currentEl) {
19      if (matches.call(currentEl, closestSelector)) {
20        return currentEl;
21      }
22      currentEl = currentEl.parentElement;
23    }
24
25    return null;
26  }
27
28  // "result" is the <div> that exists before the <a>

29  var result = closest(document.querySelector('A'), 'DIV');

Listing 4-17.Retrieve Closest Element Ancestor: Web API, All Modern Browsers

请注意,跨浏览器解决方案使用了Element.matches、、 53 、,这也是 WHATWG 在其 DOM living 规范中定义的。如果调用该方法的元素与传递的 CSS 选择器匹配,该方法将返回true。一些浏览器,即 IE 和 Safari,仍然实现与旧版本规范一致的命名约定以及特定于供应商的前缀。我已经在我的例子中说明了这些。

前面的解决方案可能不够优雅,但它更好地利用了浏览器的固有功能。jQuery 的closest()函数总是使用最原始的蛮力方式,即使浏览器原生支持Element.closest

使用 web API 查找后代就像使用 jQuery 一样简单(清单 4-18 )。

1  // Using our HTML example, result is a NodeList that

2  // contains the following elements: the two <li>s, <span>,

3  // and <a>.

4  var result = document.querySelectorAll('UL *');
5
6  // "result" is a NodeList that contains the <span>

7  // under the first <li>.

8  var result = document.querySelectorAll('UL SPAN');
Listing 4-18.

Retrieve Element Descendants

: Web API, Modern Browsers, and Internet Explorer 8

掌握高级元素选择

下面是一些更高级的方法,用于选择更具体的元素或元素组。虽然 jQuery 提供了 API 方法来处理每种场景,但是您会看到现代 web 规范也提供了相同的支持,这意味着对于这些示例,在现代浏览器中不需要 jQuery。这里的 Web API 解决方案将主要涉及各种 CSS3 选择器的使用, 54 也可以从 jQuery 中使用。

所有现代浏览器都支持本节中的所有本地示例。在某些情况下,我还会谈到如何在古老的浏览器中使用 web API 来实现相同的目标。如果您发现自己需要以下一些选择器来支持一个古老的浏览器,那么在理解如何使用浏览器的本地工具来解决这个问题之后,您可能会放弃引入 jQuery。或者不是,但至少该解决方案将揭示 jQuery 的内部工作原理,如果您坚持将其作为核心工具集的一部分,这仍然是有益的。

排除元素

尽管排除集合中特定匹配的能力是 jQuery API 的一部分,但是我们还将看到如何使用另一个适当命名的伪类来获得相同的结果。在我们深入研究代码之前,让我们考虑下面的 HTML 片段:

1  <ul role="menu">
2     <li>choice 1</li>
3     <li class="active">choice 2</li>
4     <li>choice 3</li>
5  </ul>

想象这是某种菜单,有三个项目可供选择。第二个项目“选项 2”当前被选中。如果您想方便地收集所有未选中的菜单项,该怎么办?

框架

jQuery 的 API 提供了一个not()方法,该方法将从原始元素集中删除任何匹配选择器的元素:

1  // $result is a jQuery object that contains all

2  // `<li>`s that are not "active" (the first and last).

3  var $result = $('UL LI').not('.active');

虽然前面的例子是惯用的 jQuery,但是您不必使用not()函数。相反,您可以使用 CSS3 选择器,这将在下面讨论。

Web API

现代浏览器的原生解决方案可以说和 jQuery 的一样优雅,当然也一样简单。下面,我们使用 W3C CSS3 否定伪类 55 来定位非活动列表项。没有库开销,所以这当然比 jQuery 的实现更有性能:

1  // "result" is a NodeList that contains all

2  // `<li>`s that are not "active" (the first and last).

3  var result = document.querySelectorAll('UL LI:not(.active)');

但是如果我们仍然需要支持 Internet Explorer 8,不幸的是它不支持否定伪类选择器呢?好吧,这个解决方案并不优雅,但是如果我们需要一个快速的解决方案并且不想引入一个大的库,这个解决方案也不是特别困难:

 1  var allItems = document.querySelectorAll('UL LI'),
 2      result = [];
 3
 4  // "result" will be an Array that contains all

 5  // `<li>`s that are not "active" (the first and last).

 6  for (var i = 0; i < allItems.length; i++) {
 7     if (allItems[i].className !== 'active') {
 8        result.push(allItems[i]);
 9     }
10  }

前面的解决方案仍然比 jQuery 实现的not()方法性能更好。 56

多重选择器

假设您想要选择几组不同的元素。考虑下面的 HTML 片段:

1  <div id="link-container">
2     <a href="https://github.com/rnicholus">GitHub</a>
3  </div>
4  <ol>
5     <li>one</li>
6     <li>two</li>
7  </ol>
8  <span class="my-name">Ray Nicholus</span>

如果您想选择“link-container”和“my-name”元素以及有序列表,该怎么办?我们还假设您想要在没有循环的情况下完成这一任务——只需一行简单的代码。

框架

jQuery 允许您通过提供一个长的逗号分隔的 CSS 选择器字符串来选择多个不相关的元素:

1  // $result is a jQuery object that contains 3 elements -

2  // the <div>, <ol> and the <span> from this section's

3  // HTML fragment.

4  var $result = $('#link-container, .my-name, OL');

Web API

使用 web API,不使用 jQuery 也可以获得完全相同的结果。该解决方案看起来与 jQuery 解决方案极其相似。在这种情况下以及许多其他情况下,jQuery 只是 web API 的一个非常薄的包装器。jQuery 完全支持并充分利用 CSS 规范。选择多个不相关的元素组的能力一直是 CSS 规范的一部分。由于 jQuery 支持标准的 CSS 选择器字符串,所以 jQuery 方法看起来与本地路径几乎相同:

1  // "result" is a NodeList that contains 3 elements -

2  // the <div>, <ol> and the <span> from this section's

3  // HTML fragment.

4  var result = document.querySelectorAll('#link-container, .my-name, OL');

元素类别和修饰符

jQuery 的 API 提供了不少自己专有的 CSS 伪类选择器,比如:button:submit:password。事实上,jQuery 的文档建议不要使用这些非标准选择器,因为事实上还有更高性能的选择——标准化的 CSS 选择器。例如,:button伪类的 jQuery API 文档包含以下警告:

  • 因为:button 是一个 jQuery 扩展,不是 CSS 规范的一部分,所以使用:button 的查询无法利用原生 DOM querySelectorAll()方法提供的性能提升。

我将演示如何使用querySelectorAll模拟 jQuery 自己的一些伪类的行为。这些解决方案(如清单 4-19 和 4-20 所示)将比使用非标准的 jQuery 选择器更高效。我们将从:button:submit:password:file开始。

1  // "result" will contain a NodeList of all <button> and

2  // <input type="button"> elements in the document, just like

3  // jQuery's :button pseudo-class.

4  var result = document.querySelectorAll('BUTTON, INPUT[type="button"]');
1  // "result" will contain a NodeList of all <button type="submit"> and

2  // <input type="submit"> elements in the document, just like

3  // jQuery's :submit pseudo-class.

4  var result = document.querySelectorAll(
5          'BUTTON[type="submit"], INPUT[type="submit"]'
6      );
Listing 4-19.Implementing jQuery’s :button Pseudo-class: Web API, Modern Browsers, and Internet Explorer 8

清单 4-21 和 4-22 中的原生解决方案有点冗长,但并不特别复杂,而且肯定比 jQuery 的:submit性能更好。你可以看到相同的性能差异between jQuery’s :button选择器和本机解决方案更多: 58

1  // "result" will contain a NodeList of all <input type="password">

2  // elements in the document, just like jQuery's :password pseudo-class.

3  var result = document.querySelectorAll('INPUT[type="password"]');
Listing 4-21.Implementing jQuery’s :password Pseudo-class: Web API, Modern Browsers, and Internet Explorer 8

1  // "result" will contain a NodeList of all <input type="file">

2  // elements in the document, just as jQuery's :file pseudo-class.

3  var result = document.querySelectorAll('INPUT[type="file"]');
Listing 4-22.

Implementing jQuery’s

:file Pseudo-class: Web API, Modern Browsers, and Internet Explorer 8

甚至这个相当简单的原生 CSS 选择器也比 jQuery 的非标准:file伪类快得多。 59 性能损失真的值得你在代码里省几个字符吗?

jQuery 还提供了一个非标准的:first伪类选择器。如您所料,它会过滤查询结果集中除第一个匹配之外的所有匹配。考虑以下标记:

1  <div>one</div>
2  <div>two</div>
3  <div>three</div>

假设我们想要选择这个片段中的第一个<div>。使用 jQuery,我们的代码看起来会像这样:

1  // $result is a jQuery object containing

2  // the first <div> in our example markup.

3  var $result = $('DIV:first');
4
5  // same as above, but perhaps more idiomatic jQuery.

6  var $result = $('DIV').first();

与 jQuery 的原始实现相比,本机解决方案出奇地简单,而且性能异常出色:

1  // result is the first <div> in our example markup.

2  var result = document.querySelector('DIV');

由于querySelector返回选择器字符串的第一个匹配,这实际上是 jQuery 的:first伪类或first() API 方法的一个非常优雅的替代方法。在 jQuery 的武库中,您会发现许多其他专有的 CSS 选择器,它们在 web API 中有直接的替代方法。

$(选择器)的简单替换

在本章中,您已经看到了许多元素选择方法,包括将 CSS 选择器字符串传递给jQuery函数(别名为$)。本地解决方案通常包含传递给querySelectorquerySelectorAll的相同选择器字符串。在所有条件相同的情况下,假设我们只使用有效的 CSS 选择器字符串,我们可以用一个本地解决方案来替换 jQuery 函数,这个本地解决方案不仅易于连接,而且比 jQuery 性能更高。

如果我们只关注选择器支持,并且只需要对现代浏览器的支持,我们可以通过完全放弃 jQuery 并用一个令人惊讶的简洁的本地替代来代替它,如清单 4-23 所示。

1  window.$ = function(selector) {
2     return document.querySelectorAll(selector);
3  };
4
5  // examples that use our replacement

6  $('.some-class');
7  $('#some-id');
8  $('.some-parent > .some-child');
9  $('UL LI:not(.active)');
Listing 4-23.Native Replacement for jQuery Function: All Modern Browsers, Internet Explorer 8 for CSS2 Selectors

当对比使用 jQuery 和 web API 选择这些相同的元素时,一些更复杂的选择器中看到的性能优势存在的原因与前面描述的相同。让我们看看上面代码中的子选择器。我们的原生解决方案无疑比 jQuery、 61 更快,并且两者之间的语法完全相同。在这里,我们没有因为放弃 jQuery 而放弃任何东西,并且获得了性能和更精简的页面——这是本章值得注意的主题。

Footnotes 1

www.w3.org/TR/REC-html40/cover.html

  2

www.w3.org/TR/REC-html40/struct/global.html#adef-id

  3

www.w3.org/TR/REC-DOM-Level-1/level-one-html.html#ID-58190037

  4

www.w3.org/TR/REC-CSS1/#id-as-selector

  5

www.w3.org/TR/DOM-Level-2-Core/core.html#ID-getElBId

  6

www.w3.org/TR/selectors-api/#queryselector

  7

http://jsperf.com/getelementbyid-vs-queryselector/11

  8

www.w3.org/TR/html401/struct/global.html#adef-class

  9

www.w3.org/TR/CSS21/syndata.html#characters

  10

www.ecma-international.org/ecma-262/5.1/#sec-7.6.1.2

  11

www.w3.org/TR/2015/WD-dom-20150428/#dom-document-getelementsbyclassname

  12

www.w3.org/TR/DOM-Level-3-Core/core.html#ID-536297177

  13

www.w3.org/TR/2007/WD-selectors-api-20071221/#documentselector

  14

www.w3.org/TR/REC-CSS1/#class-as-selector

  15

www.w3.org/TR/CSS21/selector.html#class-html

  16

https://github.com/FineUploader/fine-uploader/blob/5.2.1/client/js/util.js#L107

  17

www.w3.org/MarkUp/draft-ietf-iiir-html-01.txt

  18

www.w3.org/TR/html5/embedded-content-0.html#the-video-element

  19

www.w3.org/TR/html5/embedded-content-0.html#the-audio-element

  20

www.w3.org/wiki/WebComponents/

  21

www.w3.org/TR/custom-elements/

  22

https://github.com/rnicholus/ajax-form

  23

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-1950641247

  24

www.w3.org/TR/REC-CSS1/#basic-concepts

  25

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#i-Document

  26

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-745549614

  27

https://jsperf.com/queryselectorall-vs-getelementsbytagname

  28

www.nczonline.net/blog/2010/09/28/why-is-getelementsbytagname-faster-that-queryselectorall/

  29

www.w3.org/TR/CSS1/#anchor-pseudo-classes

  30

www.w3.org/TR/selectors-api/#privacy

  31

www.w3.org/TR/html5/semantics.html#the-html-element

  32

www.w3.org/TR/html5/dom.html#the-document-object

  33

www.w3.org/TR/DOM-Level-3-Core/core.html#ID-1312295772

  34

www.w3.org/TR/DOM-Level-3-Core/core.html#ID-FF21A306

  35

http://www.w3.org/TR/CSS21/selector.html#child-selectors

  36

www.w3.org/TR/DOM-Level-2-Core/core.html#ID-1060184317

  37

www.w3.org/TR/2015/WD-dom-20150428/#node

  38

www.w3.org/TR/2015/WD-dom-20150428/#parentnode

  39

www.w3.org/TR/2015/WD-dom-20150428/

  40

http://www.w3.org/TR/CSS21/selector.html#child-selectors

  41

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-1950641247

  42

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html

  43

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-1950641247

  44

www.w3.org/TR/css3-selectors/#general-sibling-combinators

  45

www.w3.org/TR/CSS21/selector.html#adjacent-selectors

  46

www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-1950641247

  47

www.w3.org/TR/ElementTraversal/#attribute-previousElementSibling

  48

www.w3.org/TR/ElementTraversal/

  49

www.w3.org/TR/ElementTraversal/#attribute-nextElementSibling

  50

www.w3.org/TR/DOM-Level-2-Core/core.html#ID-1060184317

  51

https://dom.spec.whatwg.org/#dom-element-closestselectors

  52

https://dom.spec.whatwg.org

  53

https://dom.spec.whatwg.org/#dom-element-matchesselectors

  54

www.w3.org/TR/css3-selectors/

  55

www.w3.org/TR/css3-selectors/#negation

  56

http://jsperf.com/jquery-not-vs-looping-through-results1

  57

www.w3.org/TR/REC-CSS1/#grouping

  58

http://jsperf.com/jquery-submit-vs-queryselectorall

  59

http://jsperf.com/jquery-file-vs-queryselectorall

  60

http://jsperf.com/jquery-first-vs-queryselector

  61

http://jsperf.com/jquery-select-children-vs-native-replacement