Chrome: DOM attributes now on the prototype

Chrome:我们正在把 DOM 属性迁移到原型上

Sunday, September 13, 2015

本文翻译自 HTML5Rocks《DOM attributes now on the prototype》。翻译时间:2015 年 9 月 14 日;原文更新时间:2015 年 4 月 14 日。

最近 Chrome 团队对外宣布,“我们正在把 DOM 属性迁移到原型上”。Chrome 43 版本(2015 年 4 月发布的 beta 版本)完成了此次迁移,至此 Chrome 与 Web IDL 规范以及其他诸如 IE 和 FireFox 浏览器的实现保持一致(目前基于 Webkit 内核的早期浏览器并不兼容规范,但是 Safari 却与规范兼容的)。

说明:本文所使用的两个单词 Attribute 和 Property 可以相互替换 (doray:我把这两个单词均翻译为属性) ,不过按照 ECMAScript 规范定义, Property 可以拥有任意多个 Attribute (doray:Property 和 Attribute 是完全不同的两个概念 而 JavaScript 语言中的 Property 则为 “WebIDL 所定义的 Attribute”。另外本文所说的 Attribute 并不是指诸如 HTML 图片元素的 class Attribute 。

An ECMAScript object is a collection of properties each with zero or more attributes that determine how each property can be used. ECMAScript Language Specification

The property has attributes { [[Get]]: G, [[Set]]: S, [[Enumerable]]: true, [[Configurable]]: configurable } Web IDL (Second Edition)

新的变更带来了很多积极的影响:

  • 与规范保持兼容,提高了跨 Web 终端的兼容性 (IE 和 Firefox 已经这么做了)
  • 允许在每个 DOM 对象上创建统一的 getter/setter
  • 提高 DOM 编程的可自定义能力(hackability)。比如你可以实现 pollyfill 来填补浏览器所缺失的功能,也可以实现一个 JavaScript 库来覆写浏览器 DOM 属性的默认行为

举个例子,假设 W3C 规范规定了一个新功能,称之为 isSuperContentEditable 而 Chrome 浏览器并不支持,但是现在我们可以为此实现一个 polyfill 或者使用第三方库来模拟。如果你是 JavaScript 库开发者,你可能会自然而然地像下面的代码一样利用 prototype 来创建 polyfill:

Object.defineProperty(HTMLDivElement.prototype, "isSuperContentEditable", {
  get: function() { return true; },
  set: function() { /* some logic to set it up */ },
});

在此之前,为了维持 DOM 属性的一致性,你不得不在每个 HTMLDivElement 对象中创建一个新属性,而这种做法非常低效。

此次更新对 Web 平台的一致性、性能以及标准化进程产生了重要影响,不过也给开发者带来了一些困扰。如果你希望利用上述的更新结果,由于 Chrome 与 WebKit 的历史兼容问题,我们建议你检查网站的兼容性并阅读以下的变更内容。

总结变更内容

在 DOM 对象上使用 hasOwnProperty 将会得到 false

开发者有时会使用 hasOwnProperty 来检查 DOM 对象中是否存在某个属性,然而按照 ECMAScript 5.1 Section#15.7.4.2 的规定,上述用法是无效的,因为 DOM 属性已经作为原型链上的属性而 hasOwnProperty 方法只在当前对象中查询属性是否被定义。

对于 Chrome 42 以及较早版本,以下代码的运行结果为 true

> div = document.createElement("div");
> div.hasOwnProperty("isContentEditable");

true

但是在 Chrome 43 及以后的版本中,运行结果却为 false

> div = document.createElement("div");
> div.hasOwnProperty("isContentEditable");

false

这就意味着,如果想检查 HTML 元素是否定义了 isContentEditable 属性,你只能在 HTMLElement 对象的原型中查找。例如,HTMLDivElement 继承定义了 isContentEditable 属性的 HTMLElement 对象:

> HTMLElement.prototype.hasOwnProperty("isContentEditable");

true

当然不局限于 hasOwnProperty 方法,我们推荐使用更加简单的 in 操作符,因为该操作符会查询整个原型链上的属性。

if ("isContentEditable" in div) {
  // We have support!!
}

在 DOM 对象上使用 Object.getOwnPropertyDescriptor 将不会得到属性的描述符

如果你的网站需要从某个 DOM 对象中获取某个属性节点的属性描述符,你需要在原型上查找。

在 Chrome 42 及较早版本中,如果你想获取属性描述符,你可能会这么写:

> Object.getOwnPropertyDescriptor(div, "isContentEditable");

Object {value: "", writable: true, enumerable: true, configurable: true}

在 Chrome 43 及以后的版本中,你则会得到 undefined

> Object.getOwnPropertyDescriptor(div, "isContentEditable");

undefined

这就意味着今后获取 isContentEditable 属性的属性描述符,你需要像下面代码一样在原型上查找:

> Object.getOwnPropertyDescriptor(HTMLElement.prototype, "isContentEditable");

Object {get: function, set: function, enumerable: false, configurable: false}

JSON.stringify 将不会序列化 DOM 的属性节点

JSON.stringify 将不会序列化原型上的 DOM 属性。例如,如果你尝试把推送通知 API 的 PushSubscription 对象序列化,上述的变化将影响到你的网站。

在 Chrome 42 及较早的版本中,以下做法是有效的:

> JSON.stringify(subscription);

{
  "endpoint": "https://something",
  "subscriptionId": "SomeID"
}

在 Chrome 43 及以后的版本中,定义在原型上的属性将不会被序列化,所以你会得到一个空对象:

> JSON.stringify(subscription);

{}

所以你就需要自定义序列化方法,比如你可以这样做:

function stringifyDOMObject(object) {
  function deepCopy(src) {
    if (typeof src != "object") {
      return src;
    }
    var dst = Array.isArray(src) ? [] : {};
    for (var property in src) {
      dst[property] = deepCopy(src[property]);
    }
    return dst;
  }
  return JSON.stringify(deepCopy(object));
}

var s = stringifyDOMObject(domObject);

在严格模式下对只读属性执行写操作将抛出异常

当你启动严格模式时,对只读属性执行写操作将抛出异常。例如以下这个例子:

function foo() {
  "use strict";
  var d = document.createElement("div");
  console.log(d.isContentEditable);
  d.isContentEditable = 1;
  console.log(d.isContentEditable);
}

对于 Chrome 42 以及较早版本,虽然 isContentEditable 不会发生值变化,但是以上方法还会继续执行且程序将正常地运行下去。

// Chrome 42 and earlier behavior
> foo();

false // isContentEditable
false // isContentEditable (after writing to read-only property)

如今在 Chrome 43 及以后的版本中,以上做法将抛出异常:

// Chrome 43 and onwards behavior
> foo();

false
Uncaught TypeError: Cannot set property isContentEditable of #<HTMLElement> which has only a getter

参考资料