为什么 [[GetPrototypeOf]] 是 Javascript 代理的不变量?
Why is [[GetPrototypeOf]] an Invariant of a Javascript Proxy?
Javascript 代理对象的一个应用程序是通过将数据作为数组的数组以及列出字段名称和每个字段索引的对象(即一个场地图)。 (而不是每个对象中重复 属性 名称的对象数组)。
乍一看,ES6 代理似乎是在客户端使用数据的好方法(即以数组为目标,以及基于字段映射的处理程序)。
不幸的是,Javascript代理有一个概念"invariants",其中之一是:
[[GetPrototypeOf]], applied to the proxy object must return the same value as [[GetPrototypeOf]] applied to the proxy object’s target object.
换句话说,数组不可能像对象一样出现(因为数组的原型与对象的原型不同)。
一种解决方法是使包含 field/index 的对象成为目标映射,并将值嵌入到代理处理程序中。这有效,但感觉很脏。它基本上与 Proxy 文档提供的完全相反,而不是使用一个 "handler" 和很多 "targets" 它本质上是使用很多 "handlers" (每个都在围绕数组的闭包中代理代表的值)都共享相同的 "target"(即 field/index 映射)。
'use strict';
class Inflator {
constructor(fields, values) {
// typically there are additional things in the `set` trap for databinding, persisting, etc.
const handler = {
get: (fields, prop) => values[(fields[prop] || {}).index],
set: (fields, prop, value) => value === (values[fields[prop].index] = value),
};
return new Proxy(fields, handler);
}
}
// this is what the server sends
const rawData = {
fields: {
col1: {index: 0}, // value is an object because there is typically additional metadata about the field
col2: {index: 1},
col3: {index: 2},
},
rows: [
['r1c1', 'r1c2', 'r1c3'],
['r2c1', 'r2c2', 'r2c3'],
],
};
// should be pretty cheap (memory and time) to loop through and wrap each value in a proxy
const data = rawData.rows.map( (row) => new Inflator(rawData.fields, row) );
// confirm we get what we want
console.assert(data[0].col1 === 'r1c1');
console.assert(data[1].col3 === 'r2c3');
console.log(data[0]); // this output is useless (except in Stack Overflow code snippet console, where it seems to work)
console.log(Object.assign({}, data[0])); // this output is useful, but annoying to have to jump through this hoop
for (const prop in data[0]) { // confirm looping through fields works properly
console.log(prop);
}
所以:
- 因为显然可以使数组看起来像一个对象(通过在处理程序而不是目标中保存值数组);为什么这个 "invariant" 限制首先适用? Proxys 的全部意义在于让一些东西看起来像其他东西。
和
- 是否有better/more惯用的方法使数组显示为对象而不是上述方法?
您遗漏了 that note in the spec 的重要部分:
If the target object is not extensible, [[GetPrototypeOf]] applied to the proxy object must return the same value as [[GetPrototypeOf]] applied to the proxy object's target object.
(我的重点)
如果您的数组数组是可扩展的(正常情况下),您可以 return 来自 getPrototypeOf
陷阱的任何对象(或 null
):
const data = [0, 1, 2];
const proxy = new Proxy(data, {
getPrototypeOf(target) {
return Object.prototype;
},
get(target, propName, receiver) {
switch (propName) {
case "zero":
return target[0];
case "one":
return target[1];
case "two":
return target[2];
default:
return undefined;
}
}
});
console.log(Object.getPrototypeOf(proxy) === Object.prototype); // true
console.log(proxy.two); // 2
不过,关于不变量,它不仅仅是代理; JavaScript 中的所有 个对象(包括普通的和奇异的)都需要遵守 Invariants of the Essential Internal Methods section. I pinged Allen Wirfs-Brock (former editor of the specification, and editor when the invariants language was added) about it on Twitter. It turns out the invariants are primarily there to ensure that sandboxes can be implemented. Mark Miller championed the invariants with Caja and SES 中列出的某些不变量。没有不变量,显然沙箱不能依赖与完整性相关的约束,例如对象 "frozen" 或 属性 不可配置的含义。
所以回到你的代理,你可能只是让你的数组数组可扩展(我认为你正在冻结它或什么?),因为如果你不公开它,你不需要抵御其他代码修改它。但是除此之外,如果您要为此目的使用代理,那么您描述的解决方案(具有基础对象然后仅让处理程序直接访问数组数组)似乎是一种合理的方法。 (我从来没有觉得有必要。我 几乎完全按照您描述的方式减少网络使用,但我只是在收到时重新构造了对象。)
我认为除了编写 devtools mod/extension 之外,没有任何方法可以修改 devtools 为代理显示的内容。 (Node.js 过去支持对象上的 inspect
方法,当您输出对象时修改它在控制台中显示的内容,但您可以想象,当对象的 inspect
这不是为了那个目的。也许他们会用一个名为 属性 的符号重新创建它。但无论如何那将是 Node.js 特定的。)
您曾说过您希望能够在必要时使用 Object.assign({}, yourProxy)
将您的代理转换为具有相同形状的对象,但由于 [=18= 的限制,您遇到了麻烦].正如您所指出的,ownKeys
即使在可扩展对象上也有局限性:它不能对目标对象的不可配置属性撒谎。
如果你想这样做,最好只使用一个空白对象作为你的目标,并根据你的数组向它添加伪造的 "own" 属性。这可能就是您当前方法的意思。只是以防万一,或者万一有一些边缘情况你可能还没有 运行 ,这里有一个我认为至少涵盖了大部分基础的例子:
const names = ["foo", "bar"];
const data = [1, 2];
const fakeTarget = {};
const proxy = new Proxy(fakeTarget, {
// Actually set the value for a property
set(target, propName, value, receiver) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
data[index] = value;
return true;
}
}
return false;
},
// Actually get the value for a property
get(target, propName, receiver) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
return data[index];
}
}
// Possibly inherited property
return Reflect.get(fakeTarget, propName);
},
// Make sure we respond correctly to the `in` operator and default `hasOwnProperty` method
// Note that `has` is used for inherited properties, not just own
has(target, propName) {
if (typeof propName === "string" && names.includes(propName)) {
// One of our "own" properties
return true;
}
// An inherited property, perhaps?
return Reflect.has(fakeTarget, propName);
},
// Get the descriptor for a property (important for `for-in` loops and such)
getOwnPropertyDescriptor(target, propName) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
return {
writable: true,
configurable: true,
enumerable: true,
value: data[index]
};
}
}
// Only `own` properties, so don't worry about inherited ones here
return undefined;
},
// Some operations use `defineProperty` rather than `set` to set a value
defineProperty(target, propName, descriptor) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
// You can adjust these as you like, this disallows all changes
// other than value
if (!descriptor.writable ||
!descriptor.configurable ||
!descriptor.enumerable) {
return false;
}
}
data[index] = descriptor.value;
return true;
}
return false;
},
// Get the keys for the object
ownKeys() {
return names.slice();
}
});
console.log(proxy.foo); // 1
console.log("foo" in proxy); // true
console.log("xyz" in proxy); // false
console.log(proxy.hasOwnProperty("hasOwnProperty")); // false
const obj = Object.assign({}, proxy);
console.log(obj); // {foo: 1, bar: 2}
proxy.foo = 42;
const obj2 = Object.assign({}, proxy);
console.log(obj2); // {foo: 42, bar: 2}
.as-console-wrapper {
max-height: 100% !important;
}
Javascript 代理对象的一个应用程序是通过将数据作为数组的数组以及列出字段名称和每个字段索引的对象(即一个场地图)。 (而不是每个对象中重复 属性 名称的对象数组)。
乍一看,ES6 代理似乎是在客户端使用数据的好方法(即以数组为目标,以及基于字段映射的处理程序)。
不幸的是,Javascript代理有一个概念"invariants",其中之一是:
[[GetPrototypeOf]], applied to the proxy object must return the same value as [[GetPrototypeOf]] applied to the proxy object’s target object.
换句话说,数组不可能像对象一样出现(因为数组的原型与对象的原型不同)。
一种解决方法是使包含 field/index 的对象成为目标映射,并将值嵌入到代理处理程序中。这有效,但感觉很脏。它基本上与 Proxy 文档提供的完全相反,而不是使用一个 "handler" 和很多 "targets" 它本质上是使用很多 "handlers" (每个都在围绕数组的闭包中代理代表的值)都共享相同的 "target"(即 field/index 映射)。
'use strict';
class Inflator {
constructor(fields, values) {
// typically there are additional things in the `set` trap for databinding, persisting, etc.
const handler = {
get: (fields, prop) => values[(fields[prop] || {}).index],
set: (fields, prop, value) => value === (values[fields[prop].index] = value),
};
return new Proxy(fields, handler);
}
}
// this is what the server sends
const rawData = {
fields: {
col1: {index: 0}, // value is an object because there is typically additional metadata about the field
col2: {index: 1},
col3: {index: 2},
},
rows: [
['r1c1', 'r1c2', 'r1c3'],
['r2c1', 'r2c2', 'r2c3'],
],
};
// should be pretty cheap (memory and time) to loop through and wrap each value in a proxy
const data = rawData.rows.map( (row) => new Inflator(rawData.fields, row) );
// confirm we get what we want
console.assert(data[0].col1 === 'r1c1');
console.assert(data[1].col3 === 'r2c3');
console.log(data[0]); // this output is useless (except in Stack Overflow code snippet console, where it seems to work)
console.log(Object.assign({}, data[0])); // this output is useful, but annoying to have to jump through this hoop
for (const prop in data[0]) { // confirm looping through fields works properly
console.log(prop);
}
所以:
- 因为显然可以使数组看起来像一个对象(通过在处理程序而不是目标中保存值数组);为什么这个 "invariant" 限制首先适用? Proxys 的全部意义在于让一些东西看起来像其他东西。
和
- 是否有better/more惯用的方法使数组显示为对象而不是上述方法?
您遗漏了 that note in the spec 的重要部分:
If the target object is not extensible, [[GetPrototypeOf]] applied to the proxy object must return the same value as [[GetPrototypeOf]] applied to the proxy object's target object.
(我的重点)
如果您的数组数组是可扩展的(正常情况下),您可以 return 来自 getPrototypeOf
陷阱的任何对象(或 null
):
const data = [0, 1, 2];
const proxy = new Proxy(data, {
getPrototypeOf(target) {
return Object.prototype;
},
get(target, propName, receiver) {
switch (propName) {
case "zero":
return target[0];
case "one":
return target[1];
case "two":
return target[2];
default:
return undefined;
}
}
});
console.log(Object.getPrototypeOf(proxy) === Object.prototype); // true
console.log(proxy.two); // 2
不过,关于不变量,它不仅仅是代理; JavaScript 中的所有 个对象(包括普通的和奇异的)都需要遵守 Invariants of the Essential Internal Methods section. I pinged Allen Wirfs-Brock (former editor of the specification, and editor when the invariants language was added) about it on Twitter. It turns out the invariants are primarily there to ensure that sandboxes can be implemented. Mark Miller championed the invariants with Caja and SES 中列出的某些不变量。没有不变量,显然沙箱不能依赖与完整性相关的约束,例如对象 "frozen" 或 属性 不可配置的含义。
所以回到你的代理,你可能只是让你的数组数组可扩展(我认为你正在冻结它或什么?),因为如果你不公开它,你不需要抵御其他代码修改它。但是除此之外,如果您要为此目的使用代理,那么您描述的解决方案(具有基础对象然后仅让处理程序直接访问数组数组)似乎是一种合理的方法。 (我从来没有觉得有必要。我 几乎完全按照您描述的方式减少网络使用,但我只是在收到时重新构造了对象。)
我认为除了编写 devtools mod/extension 之外,没有任何方法可以修改 devtools 为代理显示的内容。 (Node.js 过去支持对象上的 inspect
方法,当您输出对象时修改它在控制台中显示的内容,但您可以想象,当对象的 inspect
这不是为了那个目的。也许他们会用一个名为 属性 的符号重新创建它。但无论如何那将是 Node.js 特定的。)
您曾说过您希望能够在必要时使用 Object.assign({}, yourProxy)
将您的代理转换为具有相同形状的对象,但由于 [=18= 的限制,您遇到了麻烦].正如您所指出的,ownKeys
即使在可扩展对象上也有局限性:它不能对目标对象的不可配置属性撒谎。
如果你想这样做,最好只使用一个空白对象作为你的目标,并根据你的数组向它添加伪造的 "own" 属性。这可能就是您当前方法的意思。只是以防万一,或者万一有一些边缘情况你可能还没有 运行 ,这里有一个我认为至少涵盖了大部分基础的例子:
const names = ["foo", "bar"];
const data = [1, 2];
const fakeTarget = {};
const proxy = new Proxy(fakeTarget, {
// Actually set the value for a property
set(target, propName, value, receiver) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
data[index] = value;
return true;
}
}
return false;
},
// Actually get the value for a property
get(target, propName, receiver) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
return data[index];
}
}
// Possibly inherited property
return Reflect.get(fakeTarget, propName);
},
// Make sure we respond correctly to the `in` operator and default `hasOwnProperty` method
// Note that `has` is used for inherited properties, not just own
has(target, propName) {
if (typeof propName === "string" && names.includes(propName)) {
// One of our "own" properties
return true;
}
// An inherited property, perhaps?
return Reflect.has(fakeTarget, propName);
},
// Get the descriptor for a property (important for `for-in` loops and such)
getOwnPropertyDescriptor(target, propName) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
return {
writable: true,
configurable: true,
enumerable: true,
value: data[index]
};
}
}
// Only `own` properties, so don't worry about inherited ones here
return undefined;
},
// Some operations use `defineProperty` rather than `set` to set a value
defineProperty(target, propName, descriptor) {
if (typeof propName === "string") {
const index = names.indexOf(propName);
if (index !== -1) {
// You can adjust these as you like, this disallows all changes
// other than value
if (!descriptor.writable ||
!descriptor.configurable ||
!descriptor.enumerable) {
return false;
}
}
data[index] = descriptor.value;
return true;
}
return false;
},
// Get the keys for the object
ownKeys() {
return names.slice();
}
});
console.log(proxy.foo); // 1
console.log("foo" in proxy); // true
console.log("xyz" in proxy); // false
console.log(proxy.hasOwnProperty("hasOwnProperty")); // false
const obj = Object.assign({}, proxy);
console.log(obj); // {foo: 1, bar: 2}
proxy.foo = 42;
const obj2 = Object.assign({}, proxy);
console.log(obj2); // {foo: 42, bar: 2}
.as-console-wrapper {
max-height: 100% !important;
}