Skip to main content

Strange Behavior of delete

Free2017-03-05#JS#js elete#javascript delete#delete var#js delete规则

What delete can and cannot delete

1. Problem Background

The scenario is like this:

'use strict';

var F = function() {
    this.arr = [1, 2, 3, 4, 5, 6, 7];

    var self = this;
    Object.defineProperty(self, 'value', {
        get: function() {
            if (!self._value) {
                self._value = self.doStuff();
            }
            return self._value;
        },
        set: function(value) {
            return self._value = value;
        }
    })
}
F.prototype.doStuff = function() {
    return this.arr.reduce(function(acc, value) {return acc + value}, 0);
};

Instances of F own a value property, but we don't want to initialize the property value when new (because this value may not be used, and the calculation cost is relatively high, or may not be calculable when new), so naturally think of implementing "on-demand calculation" through defining getter:

var f = new F();
// At this point f has value property, but don't know what the value is yet
// Only calculate initial value when first accessing this property (through doStuff)
f.value

var tmpF = new F()
// If value property is not accessed, never need to calculate its initial value

This can avoid doing unnecessary expensive operations in advance, such as:

  • DOM queries

  • layout (such as getComputedStyle())

  • Deep traversal

Of course, directly adding a getValue() can also achieve the desired effect, but getter is more friendly to users, external parties completely don't know if the value is pre-calculated or calculated on the spot

delete's strange behavior is divided into 2 parts:

// 1.delete property defined with defineProperty reports error
// Uncaught TypeError: Cannot delete property 'value' of #<F>
delete f.value

// 2.After adding placeholder initial value, can delete normally
// Change F's value definition part to
var self = this;
self.value = null;  // Placeholder, avoid delete error
Object.defineProperty(self, 'value', {/*...*/});

2. Cause Analysis

delete Error

Remember delete operator's rule is: successful delete returns true, otherwise returns false

Whether successfully deleted or not, shouldn't report error. Actually error is because strict mode is on:

Third, strict mode makes attempts to delete undeletable properties throw (where before the attempt would simply have no effect):

(From Strict mode - JavaScript | MDN)

In strict mode, if can't delete then error. But already added value property through defineProperty(), why can't delete? It's configurable causing trouble:

configurable

true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.

Defaults to false.

This thing actually defaults to false, checked and found several others also default to false:

configurable    Defaults to false.
enumerable      Defaults to false.
writable        Defaults to false.

value, get, set Defaults to undefined.

Because defining descriptor changes property's read/write method, !writable is somewhat reasonable, !enumerable is a bit aggressive, and !configurable is a bit excessive. But rules are rules, so strange behavior 1 is reasonable

Placeholder Initial Value

Guess if property already exists, defineProperty() will be more conservative, consider the original descriptor's feelings:

var obj = {};
obj.value = null;
var _des = Object.getOwnPropertyDescriptor(obj, 'value');
Object.defineProperty(obj, 'value', {
    get: function() {},
    set: function() {}
});
var des = Object.getOwnPropertyDescriptor(obj, 'value');
console.log(_des);
console.log(des);

Result is as follows:

// _des
{
    configurable: true,
    enumerable: true,
    value: null,
    writable: true
}
// des
{
    configurable: true,
    enumerable: true,
    get: (),
    set: ()
}

Found after defineProperty(), configurable and enumerable remain unchanged, so can delete after adding placeholder value. Additionally writable is gone, because after defining getter/setter whether writable depends on getter/setter's specific implementation, can't tell at a glance (such as setter discarding new value, or getter returning unchanged value, effect is both unwritable)

3.delete Rules

Since encountered delete problem, might as well look at a bit more

delete var

Generally think delete can't delete variables declared by var, can delete properties. Actually not entirely correct, for example:

var x = 1;
delete x === false

// Can delete var declared variable
eval('var evalX = 1');
delete evalX === true
// Properties not necessarily deletable
var arr = [];
delete arr.length === false
var F = function() {};
delete F.prototype === false
// DOM, BOM objects have even more disobedient ones

At least from form perspective, delete can't delete var declared variables is incorrect. As for why evalX can be deleted, is quite interesting, need to understand several things: execution context, variable object/activation object, eval environment's particularity

Execution Context

Execution context is divided into 3 types: Global environment (such as environment enclosed by script tag), Function environment (such as onclick attribute value's execution environment, execution context created by function call) and eval environment (eval passed code's execution environment)

Variable Object/Activation Object

Each execution context corresponds to a variable object, variables and functions declared in source code exist as variable object's properties, so things declared in global scope become global's properties, for example:

var p = 'value';
function f() {}
window.p === p
window.f === f

If it's Function execution context, variable object is generally not global, called activation object, each time entering Function execution environment, create an activation object, besides variables and functions declared in function body, each formal parameter and arguments object also exist as activation object's properties, although cannot directly verify

Note: Variable object and activation object are abstract internal mechanisms, used to maintain variable scope, isolate environments, etc., cannot directly access, even in Global environment variable object seems like global, this global is also not entirely internal variable object (only have intersection on property access)

P.S. Variable object and activation object such "mysterious" things don't need to be too serious, what they are and what relationship doesn't matter, just understand their function

eval Environment's Particularity

Properties and functions declared in eval execution environment will exist as calling environment's (that is upper layer execution environment) variable object's properties, this is different from other two environments, of course, also cannot directly verify (cannot directly access variable object)

Variable object's properties all have some internal characteristics, such as visible configurable, enumerable, writable (of course internal division may be more detailed, whether can delete may just be part of configurable)

Follows rule is: variables and functions created through declaration carry an undeletable innate talent, while variables and functions created through explicit or implicit property assignment don't have this talent

Some built-in object properties also carry undeletable talent, for example:

var arr = [];
delete arr.length === false
void function(arg) {console.log(delete arg === false);}(1);

Because variables and functions created through property assignment don't have undeletable talent, so variables and functions created through assignment can be deleted, for example:

x = 1;
delete x === true
window.a = 1
delete window.a === true

And things created by global variable declaration that also get added to global cannot be deleted:

var y = 2;
delete window.y === false

Just because creation method is different, and talent is given at creation time

Additionally, there's an interesting attempt, since eval directly takes outer layer's variable object, and things declared in eval environment don't have undeletable talent, then combine the two, can it override and force delete?

var x = 1;

/* Can't delete, `x` has DontDelete */

delete x; // false
typeof x; // "number"

eval('function x(){}');

/* `x` property now references function, and should have no DontDelete */

typeof x; // "function"
delete x; // should be `true`
typeof x; // should be "undefined"

Result is still can't delete after override, properties added internally through declaration method on variable object, seems to prohibit modifying descriptor, above x value although overridden, but undeletable talent still remains

4. Summary

New properties defined through defineProperty(), their descriptor default several properties are all false, that is non-enumerable, non-modifiable descriptor, non-deletable, for example:

var obj = {};
Object.defineProperty(obj, 'a', {configurable: true, value: 10});
Object.defineProperty(obj, 'a', {configurable: true, value: 100});
delete obj.a === true

Object.defineProperty(obj, 'b', {value: 11});
delete obj.b === false
// Error, doesn't allow modifying descriptor
// Uncaught TypeError: Cannot redefine property: b
Object.defineProperty(obj, 'b', {value: 110});

Additionally, delete operator's simple rules are as follows:

  • If operand is not a reference, directly return true

  • If variable object/activation object doesn't have this property, return true

  • If property exists, but has undeletable talent, return false

  • Otherwise, delete property, return true

So:

delete 1 === true

Primitive values are true at first step anyway, whether deleted or not nobody knows

Reference Materials

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment