返回介绍

第一部分 类型和语法

第二部分 异步和性能

A.4 原生原型

发布于 2023-05-24 16:38:21 字数 3725 浏览 0 评论 0 收藏 0

一个广为人知的 JavaScript 的最佳实践是:不要扩展原生原型。

如果向 Array.prototype 中加入新的方法和属性,假设它们确实有用,设计和命名都很得当,那它最后很有可能会被加入到 JavaScript 规范当中。这样一来你所做的扩展就会与之冲突。

我自己就曾遇到过这样一个例子。

当时我正在为一些网站开发一个嵌入式构件,该构件基于 jQuery(基本上所有的框架都会犯这样的错误)。基本上它在所有的网站上都可以运行,但是在某个网站上却彻底无法运行。

经过差不多一个星期的分析调试之后,我发现这个网站有一段遗留代码,如下:

// Netscape 4没有Array.push
Array.prototype.push = function(item) {
  this[this.length-1] = item;
};

除了注释以外(谁还会关心 Netscape 4 呢?),上述代码似乎没有问题,是吧?

问题在于 Array.prototype.push 随后被加入到了规范中,并且和这段代码不兼容。标准的 push(..) 可以一次加入多个值。而这段代码中的 push 方法则只会处理第一个值。

几乎所有 JavaScript 框架的代码都使用 push(..) 来处理多个值。我的问题则是 CSS 选择器引擎(CSS selector)。可想而知其他很多地方也会有这样的问题。

最初编写这个方法的开发人员将其命名为 push 没有问题,但是并未预见到需要处理多个值的情况。这相当于挖了一个坑,而大约 10 年之后,我无意间掉了进去。

从中我们可以吸取几个教训。

首先,不要扩展原生方法,除非你确信代码在运行环境中不会有冲突。如果对此你并非 100% 确定,那么进行扩展是非常危险的。这需要你自己仔细权衡利弊。

其次,在扩展原生方法时需要加入判断条件(因为你可能无意中覆盖了原来的方法)。对于前面的例子,下面的处理方式要更好一些:

if (!Array.prototype.push) {
  // Netscape 4没有Array.push
  Array.prototype.push = function(item) {
    this[this.length-1] = item;
  };
}

其中,if 语句用来确保当 JavaScript 运行环境中没有 push() 方法时才将扩展加入。这应该可以解决我的问题。但它并非万全之策,并且存在着一定的隐患。

如果网站代码中的 push(..) 原本就不打算处理多个值的情况,那么标准的 push(..) 出台后会导致代码运行出错。

如果在 if 判断前引入了其他第三方的 push(..) 方法,并且该方法的功能不同,也会导致代码运行出错。

这里突出了一个不太为 JavaScript 开发人员注意的问题:在各种第三方代码混合运行的环境中,是否应该只使用现有的原生方法?

答案是否定的,但是实际上不太行得通。通常你无法重新定义所有会用到的原生方法,同时确保它们的安全。即使可以,这种做法也是一种浪费。

那么是否应该既检测原生方法是否存在,又要测试它能否执行我们想要的功能?如果测试没通过,是不是意味着代码要停止执行?

// 不要信任 Array.prototype.push
(function(){
  if (Array.prototype.push) {
    var a = [];
    a.push(1,2);
    if (a[0] === 1 && a[1] === 2) {
      // 测试通过,可以放心使用!
      return;
    }
  }

  throw Error(
    "Array#push() is missing/broken!"
  );
})();

理论上说这个方法不错,但实际上不可能为每个原生函数都做这样的测试。

那应该怎么办呢?我们是否应该逐一做测试?还是假设一切没问题,等出现问题时再处理?

这里没有标准答案。实际上,只要我们自己不去扩展原生原型,就不会遇到这类问题。

如果你和第三方代码都遵循以上原则,那么你的程序是安全的。否则就要更加谨慎小心地对待程序,以防任何可能出现的类似问题。

针对各种运行环境做单元和回归测试能够早点发现问题,却不能够完全杜绝问题。

shim/polyfill

通常来说,在老版本的(不符合规范的)运行环境中扩展原生方法是唯一安全的,因为环境不太可能发生变化——支持新规范的新版本浏览器会完全替代老版本浏览器,而非在老版本上做扩展。

如果能够预见哪些方法会在将来成为新的标准,如 Array.prototype.foobar ,那么就可以完全放心地使用当前的扩展版本,不是吗?

if (!Array.prototype.foobar) {
  // 幼稚
  Array.prototype.foobar = function() {
    this.push( "foo", "bar" );
  };
}

如果规范中已经定义了 Array.prototype.foobar ,并且其功能和上面的代码类似,那就没有什么问题。这种情况一般称为 polyfill(或者 shim)。

polyfill 能有效地为不符合最新规范的老版本浏览器填补缺失的功能,让你能够通过可靠的代码来支持所有你想要支持的运行环境。

ES5-Shim(https://github.com/es-shims/es5-shim )是一个完整的 shim/polyfill 集合,能够为你的项目提供 ES5 基本规范支持。同样,ES6-Shim(https://github.com/es-shims/es6-shim )提供了对 ES6 基本规范的支持。虽然我们可以通过 shim/polyfill 来填补新的 API,但是无法填补新的语法。可以使用 Traceur(https://github.com/google/traceur-compiler/wiki/GettingStarted )这样的工具来实现新旧语法之间的转换。

对于将来可能成为标准的功能,按照大部分人赞同的方式来预先实现能和将来的标准兼容的 polyfill,我们称为 prollyfill(probably fill)。

真正的问题在于一些标准功能无法被完整地 polyfill/prollyfill。

JavaScript 社区存在这样的争 论,即是否可以对一个功能做不完整的 polyfill(将无法 polyfill 的部分文档化),或者不做则已,要做就要达到 100% 符合规范。

很多开发人员可以接受一些不完整的 polyfill(如 Object.create(..) ),因为缺失的部分也不会被用到。

一些人认为在 polyfill/shim 中的 if 判断里需要加入兼容性测试,并且只在被测试的功能不存在或者未通过测试时才将其替换。这也是区别 shim(有兼容性测试)和 polyfill(检查功能是否存在)的方式。

对此并没有一个绝对正确的答案。即便在老版本的运行环境中使用了“安全”的做法,对原生功能进行扩展也无法做到 100% 安全。依赖第三方代码中的原生功能也是如此,因为这些功能有可能被扩展了。

因此,在处理这些情况的时候需要格外小心,要编写健壮的代码,并且写好文档。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文