javascript - 为什么 [[GetPrototypeOf]] 是 Javascript 代理的不变量?

标签 javascript arrays proxy es6-proxy

Javascript 代理对象的一个​​应用是通过将数据作为数组的数组以及列出字段名称和每个字段索引的对象(即字段映射)通过网络发送来减少网络流量。 (而不是在每个对象中重复属性名称的对象数组)。

乍一看,似乎 ES6 代理是在客户端使用数据的好方法(即以数组为目标,以及基于字段映射的处理程序)。

不幸的是,Javascript Proxy 有一个“不变量”的概念,其中之一是:

[[GetPrototypeOf]], applied to the proxy object must return the same value as [[GetPrototypeOf]] applied to the proxy object’s target object.

换句话说,数组不可能像对象一样出现(因为数组的原型(prototype)与对象的原型(prototype)不同)。

解决方法是使包含字段/索引映射的对象成为目标,并将值嵌入到代理处理程序中。这有效,但感觉很脏。它基本上与代理文档所呈现的完全相反,而不是使用一个具有大量“目标”的“处理程序”,它本质上是使用大量“处理程序”(每个处理程序都在代理所代表的值数组周围的闭包中)所有共享相同的“目标”(即字段/索引映射)。

'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);
}

所以:

  1. 因为显然可以使数组看起来像一个对象(通过在处理程序而不是目标中保存值数组);为什么这个“不变”限制首先适用? Proxy 的全部意义在于让某些东西看起来像其他东西。

  1. 是否有比上述方法更好/更惯用的方法来使数组显示为对象?

最佳答案

您遗漏了 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.

(我的重点)

如果您的数组数组是可扩展的(正常情况下),您可以从 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 中列出的某些不变量。部分。我向 Allen Wirfs-Brock(规范的前编辑,以及添加不变量语言时的编辑)询问了它 on Twitter .事实证明,不变量主要是为了确保可以实现沙箱。 Mark Miller 用 Caja 支持不变量和 SES心里。如果没有不变量,显然沙箱不能依赖与完整性相关的约束,例如对象“卡住”或属性不可配置的含义。

所以回到你的代理,你可能只是让你的数组数组可扩展(我认为你正在卡住它或什么?),因为如果你不公开它,你不需要防御修改它的其他代码。但是除此之外,如果您要为此目的使用代理,那么您描述的解决方案(具有基础对象然后仅让处理程序直接访问数组数组)似乎是一种合理的方法。 (我从来没有觉得有必要。我需要几乎完全按照您描述的方式减少网络使用,但我只是在收到时重新构造了对象。)

除了编写 devtools mod/扩展之外,我认为没有任何方法可以修改 devtools 为代理显示的内容。 (Node.js 过去支持对象的 inspect 方法,当您输出对象时修改它在控制台中显示的内容,但您可以想象,当对象的 inspect 并不是为了这个目的。也许他们会用一个以符号命名的属性重新创建它。但这无论如何都是 Node.js 特定的。)


您说过您希望能够在必要时使用 Object.assign({}, yourProxy) 将您的代理转换为具有相同形状的对象,并且您拥有由于 ownKeys 的限制而引起的麻烦。正如您所指出的,ownKeys 甚至对可扩展对象也有限制:它不能说谎目标对象的不可配置属性。

如果你想这样做,你可能最好只使用一个空白对象作为你的目标,并根据你的数组向它添加假的“自己的”属性。这可能就是您当前方法的意思。以防万一,或者以防万一您可能还没有遇到过一些边缘情况,这是一个我认为至少涵盖了大部分基础的示例:

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 - 为什么 [[GetPrototypeOf]] 是 Javascript 代理的不变量?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57554305/

相关文章:

javascript - GridStack-设置静态网格

javascript - 查找已排序旋转数组中的最小元素

arrays - 在 Swift 3 中从字典中的数组中检索随机项

java - Tomcat 6 - Java 代理隧道在 Windows 上失败 - 错误 - 407

java - 代理错误 502 : The proxy server received an invalid response from an upstream server

javascript - 是否可以使用 sort() 以这种特定方式对对象数组进行排序?

javascript - 我如何找到每个 <ul></ul> 中的最后一个 <li></li> (前端、html、css、jquery)

python - Numpy "double"-广播 - 有可能吗?

Python selenium - 与主机、端口、用户名、密码的代理连接

javascript - Angular Material 数据表导出到excel