Get to know MDN better
此页面由社区从英文翻译而来。了解更多并加入 MDN Web Docs 社区。
现在,我们将探讨常见的跨浏览器 JavaScript 问题以及如何解决它们。这包括使用浏览器开发者工具来跟踪和修复问题,使用 polyfill 和库来解决问题,在较旧的浏览器中使用现代 JavaScript 特性等信息。
| 熟练使用 HTML、CSS 和 JavaScript 语言,了解跨浏览器测试的核心概念。 |
| 可以分析一些常见的 JavaScript 跨浏览器问题,使用合适的工具和技术修复它们。 |
历史上,JavaScript 一直困扰于跨浏览器兼容性问题——在 1990 年代,当时主要的浏览器选择(Internet Explorer 和 Netscape)以不同的语言风格实现了脚本编程(Netscape 有 JavaScript,IE 有 JScript,还提供了 VBScript),虽然 JavaScript 和 JScript 在某种程度上是兼容的(都基于 ECMAScript 规范),但它们通常是以互相冲突、不兼容的方式实现的,给开发人员带来了许多噩梦。
这种不兼容性问题一直持续到 2000 年代初期,因为旧浏览器仍在使用,仍需要支持。例如,创建 XMLHttpRequest 对象的代码必须特别处理 Internet Explorer 6:
这也是像 jQuery 这样的库出现的主要原因之一——它们可以抽象掉浏览器实现的差异,这样开发人员就可以只使用例如 jQuery.ajax() 这样的,背后会处理这些差异的函数。
自那时以来,情况已经大大改善;现代浏览器可以很好地支持“经典 JavaScript 特性”,随着对旧浏览器的支持需求减少,使用这种代码的需求也减少了(但请注意它们并没有完全消失)。
如今,大多数跨浏览器 JavaScript 问题出现在:
我们将在下面探讨所有这些问题及更多问题。
正如我们在关于 HTML/CSS 的前一篇文章中所说,你应该先确保你的代码能够正常工作,然后再去集中解决跨浏览器问题。如果你还不熟悉如何查找 JavaScript 代码的错误的基本内容,你应该在继续学习之前学习这篇文章。你要注意一些常见的 JavaScript 问题,比如说:
例如,在 bad-for-loop.html 中(见源代码),我们使用一个用 var 定义的变量循环了 10 次,每次都创建一个段落并给它添加 onclick 事件处理器。当点击时,我们希望每个段落都能显示一个包含其编号(创建时的 i 值)的警告信息。但是,它们都报告 i 为 11,因为 for 循环在调用嵌套函数之前就完成了所有的迭代。
最简单的解决方案是用 let 而不是 var 来声明迭代变量,这样与函数相关的 i 的值对每个迭代都是唯一的。请参阅 good-for-loop.html(也可以参阅源代码)以查看能正常工作的版本。
备注:JavaScript 代码中的 Bug:JavaScript 开发人员的 10 个最常见错误对这些常见错误及更多内容有很好的讨论。
就像 HTML 和 CSS 所说的一样,使用 linter 可以确保编写出质量更高,错误更少的 JavaScript 代码,它可以指出错误,也可以标记出关于不良做法的警告等,并且可以自定义为更严格或更宽松的错误/警告报告。我们推荐的 JavaScript/ECMAScript linter 包括 JSHint 和 ESLint;它们能以多种方式使用,其中一些我们将在下面详细介绍。
JSHint 主页提供了一个在线 linter,它可以让你在左侧输入 JavaScript 代码,并在右侧提供包含指标、警告和错误的输出。
每次把代码复制并粘贴到网页上以检查其有效性并不方便,你真正需要的是能融入你的标准工作流程的 linter,而且麻烦最少。许多代码编辑器都有 linter 插件,例如,请参见 JSHint 安装页面的“文本编辑器和 IDE 的插件”部分。
浏览器开发工具有许多有用的特性,可以帮助调试 JavaScript。首先,JavaScript 控制台会报告代码中的错误。
下载示例 fetch-broken 到本地(也可以查看源代码)。
如果查看控制台,你会看到一条错误消息。确切的语句会因浏览器而异,但大致会是:“Uncaught TypeError: heroes is not iterable”(未捕获的类型错误:heroes 不可迭代),并且引用的行号是 25。如果我们查看源代码,相关的代码部分如下:
所以当我们尝试使用 jsonObj(正如你所料,它应该是一个 JSON 对象)时,代码就会崩溃。这个 JSON 对象应该通过以下 fetch() 调用从外部的 .json 文件中获取:
但是它失败了。
你可能已经知道这段代码有什么问题,但让我们更深入地探讨一下,以了解你该如何调查这个问题。首先,有一个 Console API,借助它 JavaScript 代码可以与浏览器的 JavaScript 控制台进行交互。它有许多可用的特性,但最常用的是 console.log(),它可以将自定义消息打印到控制台。
尝试添加一个 console.log() 调用来记录 fetch() 的返回值,像这样:
在浏览器中刷新页面。这次,在错误消息之前,你会看到一条新的消息记录在控制台上:
Response value: [object Promise]console.log() 的输出显示,fetch() 的返回值不是 JSON 数据,而是一个 Promise。fetch() 函数是异步的:它返回一个 Promise,只有在实际从网络接收到响应时,这个 Promise 才会兑现。在我们使用响应之前,我们必须等待 Promise 被兑现。
我们可以通过将使用响应的代码放在返回的 Promise 的 then() 方法中来实现这一点,像这样:
总之,每当有东西不工作,并且某个值在代码的某个点上不是它应该是的样子时,你都可以使用 console.log() 来打印出它,了解发生了什么。
不幸的是,我们仍然有同样的错误,问题并没有消失。现在让我们使用浏览器开发工具的一个更复杂的特性来调查一下这个问题,它在 Firefox 中被称为 JavaScript 调试器。
备注:其他浏览器中也有类似的工具;Chrome 中的来源面板、Safari 中的调试器(见 Safari Web 开发工具)等。
在 Firefox 中,调试器标签页大致如图所示:
这类工具的主要特点是能够为代码添加断点,这些是代码执行停止的点,在这一点上,你可以检查环境的当前状态,了解正在发生什么。
让我们开始工作吧。现在错误是在第 26 行抛出的。在中间的面板上点击第 26 行,给它添加一个断点(你会看到一个蓝色的箭头出现在它的上方)。现在刷新页面(Cmd/Ctrl + R),浏览器将在第 26 行暂停执行代码。在这一点上,右侧将更新显示一些非常有用的信息。
这里面有一些非常有用的信息。
showHeroes() 的参数是 fetch() promise 兑现得到的值。所以这个 promise 不是 JSON 格式的:它是一个 Response 对象。还需要额外的步骤来将响应内容转换为 JSON 对象。
我们希望你自己尝试修复这个问题。作为起点,请查看 Response 对象的文档。如果遇到困难,可以在 https://github.com/mdn/learning-area/blob/main/tools-testing/cross-browser-testing/javascript/fetch-fixed 找到修复后的源代码。
备注:调试器标签页还有许多其他有用的特性,我们没有在这里讨论,比如条件断点和观察表达式。更多信息,请参见调试器页面。
随着应用程序变得越来越复杂,你可能会开始使用更多的 JavaScript。这时,你可能会遇到性能问题(尤其是在较慢的设备上)。性能优化是一个广泛的话题,我们无法在此详尽讨论,但以下是一些快速优化建议:
备注:阿迪—奥斯曼尼的编写快速、内存效率高的 JavaScript 代码包含了大量细节和一些有助于提高 JavaScript 性能的出色建议。
在本节中,我们将研究一些比较常见的跨浏览器的 JavaScript 问题。我们将把它分为:
在之前的文章中,我们描述了由于 HTML 和 CSS 的语言性质,处理 HTML 和 CSS 错误和不被识别特性的一些方法。然而,JavaScript 不像 HTML 和 CSS 那么宽容,如果 JavaScript 引擎遇到错误或不被识别的语法(例如在使用新的不受支持的 特性时),它通常会抛出错误。
有几种处理新特性支持的策略可以探索,让我们来探讨一下最常见的几种。
备注:这些策略并不是相互独立的——当然,你可以根据需要将它们结合使用。例如,你可以使用特性检测来确定是否支持某项特性;如果不支持,你可以运行代码来加载 polyfill 或库来处理缺乏支持的情况。
特性检测的核心理念在于,通过执行测试来确认当前浏览器是否支持某个 JavaScript 特性,进而有条件地执行代码。这样做可以确保无论浏览器是否支持该特性,都能提供一个可接受的用户体验。以地理位置 API 为例——该 API 能够访问网络浏览器所在设备的位置数据,它主要通过全局 Navigator 对象的 geolocation 属性来使用。因此,你可以采用如下方法来检测浏览器是否支持地理位置特性:
你也可以为 CSS 特性编写这样的测试,例如通过测试 element.style.property 的存在(比如 paragraph.style.transform !== undefined)。如果你希望在支持某个 CSS 特性的情况下应用样式,可以直接使用 @supports 规则(也称为特性查询)。例如,要检查浏览器是否支持 CSS 容器查询,可以这样做:
最后一点,不要将特性检测与浏览器嗅探(检测访问站点的特定浏览器)混淆——这是一种应该被全面反对的做法。有关更多详细信息,请参见后文的不要嗅探浏览器。
备注:特性检测将在本模块的专门文章中详细介绍。
JavaScript 库本质上是一组第三方代码,你可以将其集成到你的网页中,它们提供了大量现成的功能,从而为你节省宝贵的时间。许多 JavaScript 库的诞生,可能源于其开发者编写了一系列常用的实用函数以便节省未来项目的开发时间,并决定将它们开源,因为这些功能对其他人来说也可能非常有用。
JavaScript 库往往有几个主要的种类(有些库包含其中的一个以上的目的):
在选择使用一个库时,要确保它能在你想支持的一系列浏览器中工作,并对你的实现进行彻底的测试。还要确保这个库是受欢迎的,得到良好的支持,并且不可能在下周就被淘汰。与其他开发者交谈,了解他们的建议,看看该库在 GitHub(或其他存放它的地方)上有多少活动和贡献者,等等。
库的基本用法往往包括下载库的文件(JavaScript,可能还有一些 CSS 或其他依赖项)并将其附加到你的页面上(例如通过 <script> 元素),尽管这些库通常还有许多其他用法选择,例如将其作为 Bower 组件安装,或通过 Webpack 模块捆绑器将其作为依赖项。你需要阅读这些库的单独安装页面以获得更多信息。
对于那些想要使用现代 JavaScript 特性的开发者来说,另一个选择是将采用 ECMAScript 6/ECMAScript 2015 特性的代码转换成能够在旧版浏览器上运行的版本。
备注:这个过程被称为“转译”。这并不是将代码编译到更低级别以便在计算机上运行(像 C 语言代码那样);而是将代码转换为另一种同等抽象级别的语法,使其可以以类似的方式运行,尽管细节上有所不同(在这个例子中,是将一种 JavaScript 风格转换为另一种风格)。
Babel.js 是一种常见的转译器,但还有其他转译器。
在历史上,开发人员曾使用浏览器嗅探代码来检测用户使用的浏览器,并为该浏览器提供适当的代码。
所有浏览器都有一个用户代理字符串,它标识了浏览器的身份(版本、名称、操作系统等)。许多开发人员实现的浏览器嗅探代码非常糟糕,而且没有进行维护。这导致支持的浏览器无法访问网站,即使它们可以轻松渲染网页。这种情况变得极其普遍,以至于浏览器开始在其用户代理字符串中隐藏真实身份(或声称它们都是浏览器),以规避嗅探代码。浏览器还实现了允许用户更改 JavaScript 查询得到的用户代理字符串的功能。这些都使浏览器嗅探变得更加容易出错,最终变得毫无意义。
Aaron Andersen 撰写的浏览器用户代理字符串的历史中讲述了这段有用且有趣的浏览器探测历史。使用特性检测(以及 CSS @supports 进行 CSS 特性检测)来检测是否支持某个特性要可靠地多。这样,当新的浏览器版本出现时,你就不需要更改代码。
在上一篇文章中,我们详细讨论了处理 CSS 前缀的内容。新的 JavaScript 实现有时也会使用前缀,但与 CSS 使用连字符不同,JavaScript 采用驼峰命名。例如,如果一个新的 jshint API 对象名为 Object,不同的浏览器厂商会使用以下前缀:
以下是一个例子,来自我们的 violent-theremin 演示(参见源代码),它结合使用了 Canvas API 和 Web Audio API,创造了一个既有趣(又喧闹)的绘画工具:
关于 Web Audio API,在 Chrome/Opera 中使用该 API 的主要方式是通过带有 webkit 前缀的版本(尽管现在它们也支持无前缀版本)。一种简单的方法是为带前缀的对象创建一个新版本,并将其设置为等同于无前缀版本,或者是带有前缀的版本(具体取决于用户当前使用的浏览器支持哪一个)。
接下来,我们使用这个新对象来操作 API,而不是原始对象。在这个例子中,我们创建了一个修改版的 AudioContext 构造函数,然后用它来创建新的音频上下文实例,用于我们的 Web Audio 编程。
这种模式几乎可以应用于所有带前缀的 JavaScript 特性。JavaScript 库/ployfill 也采用这种代码,以最大程度地屏蔽为开发者屏蔽浏览器之间的细节差异。
同样,前缀特性本不应在生产环境网站中使用——它们可能会在没有任何警告的情况下被更改或删除,从而导致跨浏览器兼容性问题。如果你仍然需要使用带前缀的特性,请确保你使用的是正确的特性。你可以在 MDN 的参考页面和 caniuse.com 等网站上查询哪些浏览器需要对不同的 JavaScript/API 特性使用前缀。如果你不确定,也可以通过在浏览器中进行测试来获取相关信息。
例如,尝试进入你的浏览器的开发者控制台,开始输入
如果你的浏览器支持这一特性,它将带有自动完成提示。
在使用 JavaScript 的过程中,你还会遇到很多其他问题;最重要的是知道如何在网上找到答案。请参考 HTML 和 CSS 文章的寻找帮助部分,了解我们的最佳建议。
所以,这就是 JavaScript 的基本情况。听起来简单吗?可能并不完全如此,但希望这篇文章至少为你提供了一个开始的地方,以及一些关于如何解决你可能遇到的 JavaScript 相关问题的思路。