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 asgetComputedStyle()) -
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
No comments yet. Be the first to share your thoughts.