如何撤销Object.defineProperty的调用?

53

Fiddle

var Assertion = function() {
    return { "dummy": "data" };    
}

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(this);
  }
});

// Insert magic here.

// This needs to be false
console.log(({}).should === undefined);

在ES5中,我有哪些选项可以撤销 defineProperty 调用?

请勿提出愚蠢的建议,例如 Object.defineProperty = function() { }

以下代码 Object.defineProperty(Object.prototype, 'should', {})

不能够正常工作

而且 Object.defineProperty(Object.prototype, 'should', { value: undefined })

会在V8引擎中抛出一个Uncaught TypeError: Cannot redefine property: defineProperty异常。

Object.defineProperty(Object.prototype, 'should', { 
    set: function() {},
    get: function() { return undefined; }
});

抛出相同的错误

delete Object.prototype.should不起作用

2个回答

55

总的来说,由于没有撤销栈或类似的东西,你通常无法撤销defineProperty调用。JavaScript引擎不会跟踪先前的属性描述符。

例如,

Object.defineProperty(Object.prototype, 'foo', {
    configurable: true,
    value: 1,
    enumerable: false
});
Object.defineProperty(Object.prototype, 'foo', {
    get: function () {
        alert('You cannot revert me');
        return 2;
    },
    enumerable: true
});
你可以删除重新配置一个属性,或者覆盖它的值。如其他答案中所述,如果你想要删除或重新配置,则需要将configurable标志设置为true。 一旦使用configurable:false定义了一个属性,就不能更改configurable标志。

要删除一个属性(这可能是你想要做的),请使用delete

Object.defineProperty(Object.prototype, 'foo', {
    configurable: true, // defaults to false
    writable: false,
    value: 1
});
delete Object.prototype.foo;
console.log(Object.prototype.hasOwnProperty('foo')); // false
重新配置需要再次使用 defineProperty 并传递不同的描述符:
Object.defineProperty(Object.prototype, 'foo', {
    configurable: true,
    get: ...
    set: ...
});
Object.defineProperty(Object.prototype, 'foo', {
    value: undefined
});
console.log({}.foo); // undefined
console.log(Object.prototype.hasOwnProperty('foo')); // true

如此示例所示,您可以使用defineProperty来在访问器(get/set)和数据(value)属性之间进行切换。


如果要覆盖,则使用简单赋值。在这种情况下,您需要将writable标志设置为true。显然,这对于访问器属性不起作用。它甚至会抛出异常:

Object.defineProperty(Object.prototype, 'foo', {
    configurable: true,
    value: 1,
    writable: true // defaults to false
});
Object.prototype.foo = undefined;
console.log(Object.prototype.foo); // undefined
console.log(Object.prototype.hasOwnProperty('foo')); // true

Object.defineProperty(Object.prototype, 'foo', {
    get: function () {
        return 1;
    },
    writable: true // JS error!
});

请注意,当您使用defineProperty时,writable默认为false,但是当您使用简单语法 o.attr = val; 定义之前不存在的属性时,writable默认为true


有人能解释一下为什么我们不能通过将新值分配给属性来覆盖或删除 accessor,例如 obj.foo = 'new value';?换句话说,为什么我们必须先删除该属性或重新配置它才能摆脱该访问器? - kapreski
1
@kapreski 因为 JavaScript 的访问器属性不支持对属性本身的重新赋值:当你试图给一个访问器属性分配一个值时,一个正确编写的 JavaScript 引擎会拦截该调用,将分配的值传递给已定义的访问器的 setter 函数(如果已定义),否则它会假定该属性根本不应被写入,并抛出错误,这是根据 ECMAScript 规范的要求。定义一个属性为访问器将删除默认的[[Get]][[Set]]行为,转而采用用户提供的行为。 - Jamie Ridding

0
如果您想撤消上次的defineProperty或所有属性,可以使用以下类:

(gist here)

class PropertyDescriptorStack {
    private readonly descriptors: PropertyDescriptor[] = [];
    constructor(private readonly target: Object, private readonly prop: string) {
        if (!target || typeof prop !== "string") { // your choice to define ""
            throw new Error("PropertySaver: no object or property");
        }
    }

    public push(props: Partial<PropertyDescriptor>): boolean {
        this.saveDescriptor(this.target, this.prop);
        try {
            Object.defineProperty(this.target, this.prop, {
                ...props,
                configurable: true,
            });
            return true;
        }
        catch (e) {
            console.error(`Error setting property ${this.prop} on ${this.target}: ${e}`);
            return false;
        }
    }

    public pop(toStart?: boolean): boolean {
        const ind = toStart ? 0 : this.descriptors.length - 1;
        const descriptor = this.descriptors[ind];
        if (!descriptor) {
            return false;
        }
        this.descriptors.splice(ind, this.descriptors.length - ind);
        try {
            Object.defineProperty(this.target, this.prop, descriptor);
            return true;
        }
        catch (e) {
            console.error(`Error resetting property ${this.prop} on ${this.target}: ${e}`);
            return false;
        }
    }

    /**
     * Saves the current descriptor of the property in the object in the descriptors stack.
     * The descriptor is taken either from the object or from the closest prototype that has this prop.
     * If none is found, a new descriptor is generated with the current value.
     * @param target
     * @param prop 
     * @returns The found descriptor
     */
    private saveDescriptor(target: object, prop: string): PropertyDescriptor {
        let ds: PropertyDescriptor | null = null;
        for (let o: any = target, ds: PropertyDescriptor = null; o; o = Object.getPrototypeOf(o)) {
            ds = Object.getOwnPropertyDescriptor(o, prop);
            if (ds) {
                break;
            }
        }
        ds = ds || {
            configurable: true,
            writable: true,
            value: target[prop],
            enumerable: true
        }
        this.descriptors.push(ds);
        return ds;

    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接