本文翻译自 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 moreattributes
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 */ },
});
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
> div = document.createElement("div");
> div.hasOwnProperty("isContentEditable");
true
但是在 Chrome 43 及以后的版本中,运行结果却为 false
:
> div = document.createElement("div");
> div.hasOwnProperty("isContentEditable");
false
> div = document.createElement("div");
> div.hasOwnProperty("isContentEditable");
false
这就意味着,如果想检查 HTML 元素是否定义了 isContentEditable
属性,你只能在 HTMLElement 对象的原型中查找。例如,HTMLDivElement
继承定义了 isContentEditable
属性的 HTMLElement
对象:
> HTMLElement.prototype.hasOwnProperty("isContentEditable");
true
> HTMLElement.prototype.hasOwnProperty("isContentEditable");
true
当然不局限于 hasOwnProperty
方法,我们推荐使用更加简单的 in
操作符,因为该操作符会查询整个原型链上的属性。
if ("isContentEditable" in div) {
// We have support!!
}
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}
> Object.getOwnPropertyDescriptor(div, "isContentEditable");
Object {value: "", writable: true, enumerable: true, configurable: true}
在 Chrome 43 及以后的版本中,你则会得到 undefined
:
> Object.getOwnPropertyDescriptor(div, "isContentEditable");
undefined
> Object.getOwnPropertyDescriptor(div, "isContentEditable");
undefined
这就意味着今后获取 isContentEditable
属性的属性描述符,你需要像下面代码一样在原型上查找:
> Object.getOwnPropertyDescriptor(HTMLElement.prototype, "isContentEditable");
Object {get: function, set: function, enumerable: false, configurable: false}
> 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"
}
> JSON.stringify(subscription);
{
"endpoint": "https://something",
"subscriptionId": "SomeID"
}
在 Chrome 43 及以后的版本中,定义在原型上的属性将不会被序列化,所以你会得到一个空对象:
> JSON.stringify(subscription);
{}
> 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 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);
}
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 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
> // Chrome 43 and onwards behavior
> foo();
false
Uncaught TypeError: Cannot set property isContentEditable of #<HTMLElement> which has only a getter