编写 JavaScript Polyfills






4.83/5 (4投票s)
JavaScript 有许多不同的实现,它们在支持的功能方面存在分歧。编写 polyfills 可以弥合实现之间的差距,让你可以像所有浏览器都支持该功能一样进行编码。
引言
JavaScript 以其跨浏览器兼容性问题而闻名。Internet Explorer、Mozilla Firefox、Google Chrome、Safari 和 Opera 都拥有自己的专有功能以及它们自己的标准功能子集。不同的浏览器以自己的方式实现每个功能,这可能会让试图让一切都在所有浏览器中正常工作的 Web 开发人员感到头疼。
幸运的是,JavaScript 具有足够的灵活性和可扩展性来弥合浏览器之间的一些差距。这些桥梁被称为 Polyfills。
我们将创建几个简单的 polyfills 来了解它们的工作原理。在本文中,我将假设您是一名对 JavaScript 工作原理有基本了解的开发人员。
什么是 Polyfill?
Polyfill 是一段代码,它实现了你期望浏览器原生支持的功能。
Polyfills 通常模拟一个提供回退功能的较新 API,以便在旧浏览器中运行。Polyfills 还可以使不同浏览器实现方式不同的功能在每个浏览器中以相同的方式工作。
Polyfill 的完整程度各不相同
- 完美 polyfills 是完全实现功能的 polyfills,没有任何副作用。例如 JSON 库,它在不支持全局
JSON
对象的旧浏览器中实现了JSON.stringify
和JSON.parse
。 - 通用 polyfills 是几乎完全实现功能的 polyfills。它们可能有一些小的功能缺失,一些边缘情况无法正确工作,或者可能有一些稍微令人恼火的副作用。大多数 polyfills 都属于此类。
- 部分 polyfills 实现某些功能,但存在大量缺失或损坏的功能。例如 ES5 shim,它为 ECMAScript 3 引擎实现了许多 ECMAScript 5 功能,但缺失了几个关键功能。
- 回退 polyfills 是根本不实现新功能的 polyfills,它们只是确保为旧浏览器提供优雅的回退行为。例如 Web Worker 回退。在 HTML5 中,使用 web worker,你可以在多个线程中执行 JavaScript 代码,但在旧浏览器中没有已知的方法可以做到这一点。此回退只是将所有多线程代码在单个线程中运行,这允许代码在旧浏览器中运行,但代码运行时浏览器将冻结。
Polyfills 通常有两个基本组成部分:功能检测和功能实现。
特性检测
首先,你必须测试浏览器是否已经实现了给定功能。如果已经实现,你就不想重新实现任何已有的东西,你应该立即停止。否则,如果浏览器确实缺少该功能,你就可以进行下一步。
在覆盖 polyfills 中,会省略此步骤,在这种情况下,你将覆盖任何现有行为,而是使用你的实现。这种方法可以确保所有浏览器都按照你的预期运行,但它的缺点是可能会由于覆盖功能的本机实现而产生不必要的开销。
功能实现
这是 polyfill 的核心,你将实际实现缺失的功能。
第一个 Polyfill 示例:添加原型方法
在 ECMAScript 5 中,添加了许多新的 Array 原型方法,它们强调了函数式编程方法来操作数组。例如 filter
方法,它接受一个函数并返回一个数组,该数组仅包含原始数组中函数返回 true
的值。例如,你可以过滤数组以仅包含偶数值。
var isEven = function(n) {
return n % 2 === 0;
};
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter(isEven);
// returns [2, 4, 6, 8, 10]
filter
方法还有一个可选的第二个参数,用于绑定到函数 this
的值。例如,你可以将一个对象作为 this
绑定到传递给 filter
的函数。
var fruits = {
banana: "yellow",
strawberry: "red",
pumpkin: "orange",
apple: "red"
};
var isRedFruit = function(name) {
return this[name] === "red";
};
["pumpkin", "strawberry", "apple", "banana", "strawberry"].filter(isRedFruit, fruits);
// returns ["strawberry", "apple", "strawberry"]
由于这是 ES5 中添加的功能,因此旧浏览器(如 Internet Explorer 8 及更早版本)不支持 filter
方法。幸运的是,这个功能很容易创建 polyfill。
首先,我们必须进行功能检测,以查看 filter
方法是否已受支持。在这种情况下,我们只需要检查 Array
原型中是否存在名为 filter
的函数。如果没有,我们可以创建它。
if(typeof Array.prototype.filter !== "function") {
Array.prototype.filter = function() {
// implementation goes here
};
}
现在我们可以编写实现。注意它对边缘情况的检查。
Array.prototype.filter = function(fn, thisp) {
if (this === null) throw new TypeError;
if (typeof fn !== "function") throw new TypeError;
var result = [];
for (var i = 0; i < this.length; i++) {
if (i in this) {
var val = this[i];
if (fn.call(thisp, val, i, this)) {
result.push(val);
}
}
}
return result;
};
事实证明,这只是一个通用 polyfill,而不是一个完美的 polyfill,因为它通过将 "filter"
作为可枚举属性添加到每个数组中,会导致 for..in
循环出现意外行为。
var arr = [0, 1, 2];
for(var i in arr) {
console.log(i);
}
//LOG: 0
//LOG: 1
//LOG: 2
//LOG: filter
第二个 Polyfill 示例:Blink 标签支持
这更多的是一个有趣的教学示例,而不是真正实用的东西——展示如何做到这一点甚至可能具有积极的危害。blink 标签是一个非标准元素,会导致其内容闪烁。幸运的是,唯一支持此标签的现代浏览器是 Mozilla Firefox 和 Opera。
不幸的是,没有已知直接的方法可以检测浏览器是否支持 blink
标签(但如果你想出了一个,我很想听听)。这意味着我们将不得不编写一个覆盖 polyfill,它将覆盖浏览器对 blink 标签的任何内置行为。
在第一步不是功能检测的情况下,第一步将是替换所有 blink
标签,用另一个不会导致任何浏览器闪烁且不会与现有样式冲突的标签替换。在这里,我将用 blinky
标签替换所有 blink
标签。
(function replaceBlinks() {
var blinks = document.getElementsByTagName("blink");
while (blinks.length) {
var blink = blinks[0];
var blinky = document.createElement("blinky");
blinky.innerHTML = blink.innerHTML;
blink.parentNode.insertBefore(blinky, blink);
blink.parentNode.removeChild(blink);
}
})();
好了,我们可以实现实际的闪烁行为了。
(function blink(visible) {
var blinkies = document.getElementsByTagName("blinky"),
visibility = visible ? "visible" : "hidden";
for (var i = 0; i < blinkies.length; i++) {
blinkies[i].style.visibility = visibility;
}
setTimeout(function() {
blink(!visible);
}, 500);
})(true);
好了!我们已经创建了一个 polyfill,可以在每个浏览器中运行 blink 标签。你可以在此演示页面上查看它的实际效果。
这也不是一个完美的 polyfill,而是一个通用的 polyfill。原因有几个
- 对
blink
的任何 CSS 样式都不会有任何效果。如果你将样式应用于blinky
,它们可能会生效。 - 这覆盖了已支持
blink
的浏览器的实现,因此, - 这不会保留浏览器定义的闪烁超时。例如,在 Firefox 中,闪烁可见时间为四分之三秒,不可见时间为四分之一秒。Opera 可能有不同的超时定义。无论用户代理已经决定了什么,我们的实现每半秒在可见和不可见之间切换。
这就是全部,伙计们!
如果你有兴趣了解更多 polyfills,你应该看看由 Modernizr 社区维护的这个 wiki。
历史
- 2012年4月24日:初始版本