如何像数组解构一样覆盖 ES class 的对象解构

How to override object destructuring for ES class just like array destructuring

TL;DR

如何制作{...myCls}return{...myCls.instanceVar}

实际问题

我正在尝试实现 *[Symbol.iterator]() { yield this.internalObj; } 的自定义版本,以便我的 class 的对象传播对 myClass.instanceVar 执行对象传播操作。 具体来说,我想让 {...(new MyClass('a', 'b'}))} return {...(new MyClass('a', 'b'})).innerVal}。不过,好像we cannot override object-spread logic, we can only override array-spread logic.

例如,这是创建 Array 包装器

的简单 class
class MyArray {
    innerArray = [];

    getItem(index) {
        return (index < this.innerArray.length) ? this.innerArray[index] : null;
    }

    setItem(index, val) {
        const innerArrayLength = this.innerArray.length;

        return (index < innerArrayLength)
            ?
                this.innerArray[index] = val
            :
                this.innerArray.push(val)
        ;
    }

    removeItem(index) {
        return this.innerArray.splice(index, 1);
    }

    clear() {
        this.innerArray = [];
    }

    get length() {
        return this.innerArray.length;
    }

    *[Symbol.iterator]() {
        return yield* this.innerArray;
    }
}

// Usage:
let myArr = new MyArray()   // undefined
myArr.setItem(0, 'a')   // 1
myArr.setItem(10, 'b')   // 2
console.log([...myArr])   // (2) [ 0 => "a", 1 => "b" ]

但是,我想要的是一种使用 object class 实例变量而不是 array class实例变量。

例如,当我尝试实现 StorageMock class

时会发生这种情况
class StorageMock {
    storage = {};

    setItem(key, val) {
        this.storage[key] = val;
    }

    getItem(key) {
        return (key in this.storage) ? this.storage[key] : null;
    }

    removeItem(key) {
        delete this.storage[key];
    }

    clear() {
        this.storage = {};
    }

    get length() {
        return Object.keys(this.storage).length;
    }

    key(index) {
        return Object.keys(this.storage)[index] || null;
    }

    *[Symbol.iterator]() {
        return yield* Object.entries(this.storage).map(([ k, v ]) => ({ [k]: v }));
    }
}

let myStore = new StorageMock()    // undefined
myStore.setItem('a', 'hello');    // undefined
myStore.setItem('b', 'world');    // undefined
console.log({...myStore});   // { storage: { a: "hello", b: "world" } }  <== PROBLEM

// Doing the same with localStorage prints out:
// { a: "hello", b: "world" }
// instead of
// { storage: { a: "hello", b: "world" } }

在这种情况下,Storage API 会在传播 (local|session)Storage 时传播存储条目,但创建一个特殊的 StorageMock class 不会。

重点是我做不到 {...storageMockInstance} === {...(storageMockInstance.storage)}。那么如何覆盖 ES class 的 object 扩展语法?

References/attempts

我尝试了 Object.create(), Object.definePropert(y|ies)(), variants of the in operator (all of which have relevant access-ability defined here), all depending on the for...in syntax defininition from the generic-spreading-syntax proposal. But all I've found is that only "standard" destructuring can be used according to references 1, 2, and 3 的各种组合。

但是必须有一种方法可以通过 ESNext classes 来做到这一点。 None 我尝试实现使用实际本机 class 功能的能力 而不是 通过 AMD module syntax. It doesn't seem reasonable that I couldn't override these fields in a way similar to how other languages do so. i.e. If I could only override how the JS for..in loop works in the same way that Python allows overriding it, then I could spread the inner variable through a forIn() method just like toString() or toJSON().

可用的功能
备注

请不要用 @babel/polyfillcore-jsbabel-jest 回答这个问题。它不仅适用于 (local|session)Storage,而且还只是一个高级问题的问题。

TL;DR

你不能。

除非你作弊。但可能不值得。

实际答案

术语“数组解构”可能有点误导。它实际上 总是 从获取对象的迭代器开始,并从那里提取值,直到满足所有绑定。它不是,实际上只应该用在数组上。

const obj = { 
  *[Symbol.iterator]() { 
    yield 1; 
    yield 2; 
    yield 3; 
    yield 4; 
  } 
};

//1. take iterator
//2. take first three values
const [a, b, c] = obj;
//1. take iterator (do not continue the old one)
//2. take the first value 
const [x] = obj;

console.log(a, b, c, x); // 1, 2, 3, 1

然而,对象解构没有类似的机制。当使用 {...x} 时,执行抽象操作 CopyDataProperties。顾名思义,它将复制属性,而不是调用某种机制来获取要复制的数据。

将被复制的属性是

  1. 自己的 - 不是来自原型。
  2. A data property as opposed to an accessor property(由getterand/orsetter定义的属性)。
  3. 可枚举。
  4. 不是抽象操作的 excludedNames 参数的一部分。注意:这仅在将 spread 与 rest 目标一起使用时才相关,例如 const {foo, bar, ...rest} = obj;

可以做的是对运行时说谎关于这些事情中的每一个。这可以使用 Proxy 完成,您需要更改以下内容:

  1. ownKeys trap 切换用于解构的键。
  2. getOwnPropertyDescriptor trap 以确保属性可枚举。
  3. get trap 为 属性.
  4. 赋值

这可以像这样作为一个函数来完成,例如,这将使一个对象的行为就像您正在使用其 属性 值之一一样:

const obj = {
  a: 1,  
  b: 2, 
  c: 3, 
  myProp: {
    d: 4, 
    e: 5, 
    f: 6
  }
};

const x = { ...lieAboutSpread("myProp", obj) };
console.log(x);

function lieAboutSpread(prop, obj) {
  //this will be the false target for spreading
  const newTarget = obj[prop];
  
  const handler = {
    // return the false target's keys
    ownKeys() {
      return Reflect.ownKeys(newTarget);
    },
    // return the false target's property descriptors
    getOwnPropertyDescriptor(target, property) {
      return Reflect.getOwnPropertyDescriptor(newTarget, property);
    },
    // return the false target's values
    get(target, property, receiver) { 
      return Reflect.get(newTarget, property, receiver);
    }
  }
  
  return new Proxy(obj, handler);
}

所以,这是可能。然而,我不确定简单地做 { ...obj.myProp } 是否有那么大的好处。此外,上述函数可以以完全不“说谎”的方式重写。但是变得极度无聊:

const obj = {
  a: 1,  
  b: 2, 
  c: 3, 
  myProp: {
    d: 4, 
    e: 5, 
    f: 6
  }
};

const x = { ...lieAboutSpread("myProp", obj) };
console.log(x);

function lieAboutSpread(prop, obj) {
  //this will be the target for spreading
  return obj[prop];
}

在我看来,这凸显了为什么人为掩盖对象的方式是一种矫枉过正。