你觉得下列代码中,哪些delete
操作能成功?人肉判断一下,不要放进浏览器里执行。
// #1
a = "hello world";
delete a;
// #2
var b = "hello world";
delete b;
// #3
x = {};
Object.defineProperties(x, {
"p1": {
value: 'hello',
configurable: true
},
"p2": {
value: "world",
configurable: false
}
});
console.log(delete x.p1);
console.log(delete x.p2);
// #4
function f() {
console.log("hello f");
}
delete f;
// #5
with({t:'try'}) {
console.log(t);
delete t;
console.log(t); // print what?
}
// #6
try {
throw "hello";
} catch (e) {
console.log(e);
delete e;
console.log(e);// print what?
}
// #7
function test(a, b, c) {
delete a;
console.log(a);
delete arguments;
console.log(arguments);
}
test(1,2,3);
// #8
eval('var v = "ttt"');
delete v;
console.log(v);
// #9
y = {a:'bye'};
function f() {
return y.a;
}
delete f();
如果上述代码都在strict
模式下执行呢,又有哪些操作是成功的呢?如果不清楚的话,往下看。
PS:本文所表述的内容均由规范演译而来,而非通过实验进行推理,代码示例仅用来证明文中阐述内容的正确性,如有疑惑欢迎留言讨论。
delete究竟在做啥?
参见EMCA 262-5 第11.4.1小节:
The production UnaryExpression : delete UnaryExpression is evaluated as follows:
- Let ref be the result of evaluating UnaryExpression.
- If Type(ref) is not Reference, return true.
- If IsUnresolvableReference(ref) then,
- If IsStrictReference(ref) is true, throw a SyntaxError exception.
- Else, return true.
- If IsPropertyReference(ref) is true, then
- Return the result of calling the [[Delete]] internal method on
ToObject(GetBase(ref)) providing GetReferencedName(ref) and
IsStrictReference(ref) as the arguments.- Else, ref is a Reference to an Environment Record binding, so
- If IsStrictReference(ref) is true, throw a SyntaxError exception.
- Let bindings be GetBase(ref).
- Return the result of calling the DeleteBinding concrete method of bindings, providing GetReferencedName(ref) as the argument.
要读懂上面这一堆术语诸如Type(ref), Reference, GetBase(ref),DeleteBinding似乎还是需要花费点力气的,没关系,慢慢来。
什么是Reference?
在ECMA规范中,Reference是一个抽象的概念,由三个部分组成,可以理解为:
{
base: undefined | Object | Boolean | String | Number | environment record, //这个表示Reference的基
refName: string, //在ECMA中,常以Reference Name表示
isStrict: boolean //是否是一个strict的reference,如果在strict mode下执行的话,对所有的Reference这个值都是true。而在普通mode下,则需要分情况讨论了
}
什么时候会创建一个Reference呢?有两种情况:
- 解析变量(GetIdentifierReference )
- 访问对象属性(Property Accessors)
对于如下代码(在全局作用域下):
var jake= 'string';
delete jake;
在delete表达式中,对jake
变量的解析便可得到这样的一个Reference
:
{
base: GLOBAL, //base是全局对象,在浏览器环境下就是window对象
refName: 'jake', //Reference Name就是字符串jake
isStrict: false
}
而对于如下代码:
var man = {
name: 'delta',
age: 24
};
console.log(man.age);
在console.log(man.age)
语句中,对man.age
的解析便可得到如下的Reference
。
{
base: man,
refName: 'age',
isStrict: false
}
So Easy,那什么情况下会有IsUnresolvableReference(ref)
为true
的情况呢?当且仅当一个Reference的Base值为undefined
时,才会有IsUnresolvableReference(ref)
为true。
delete abcd;
在解析abcd
变量时,会查找当前环境记录(Environment Record)是否有一个叫abcd
这样的绑定(Binding),如果有,则当前环境记录则为Base值,否则再从当前词法环境(Lexical Environment)的父环境(parent Lexical Environment)的环境记录中查找,直到undefined
。故对于解析abcd
而言,得到的*Reference`为:
{
base: undefined,
refName: 'abcd',
isStrict: false
}
上述所有Reference的isStrict
属性在strict mode
下均为true
。
回到delete的定义,可以看到:
If Type(ref) is not Reference, return true.
If IsUnresolvableReference(ref) then,
- If IsStrictReference(ref) is true, throw a SyntaxError exception.
- Else, return true.
这就很好理解了,可以得出如下结论(在普通mode下):
delete abcdefg; //不会报错,而且还返回true
delete "abcde"; //"abcde"是一个值,不是Reference,返回true
Property Reference
什么时候会有IsPropertyReference(ref)
为true呢?这很好理解,仅当一个Reference
的Base值为一个Object或一个JS原生类型如string, boolean, number时,它才会为true
.
回到delete的定义:
If IsPropertyReference(ref) is true, then
+ Return the result of calling the [[Delete]] internal method on ToObject(GetBase(ref)) providing GetReferencedName(ref) and IsStrictReference(ref) as the arguments.
因此有:
a = {};
delete a.p; //结果是true
delete "hello".p //结果也是true
y = {a:'bye'};
function f() {
return y.a;
}
delete f(); //结果是true,因为f()的结果是一个值,不是Reference
重点在于[[Delete]]这个内部方法,如果一个属性的Configurable
为false,那么:
- 在普通mode下,属性不会被删除,返回true
- 在strict mode下,抛出Type Error异常
如果一个属性的Configurable
为true的话,那么delete操作就能成功去除相应的属性。
继续
回到delete的定义,最后一段:
Else, ref is a Reference to an Environment Record binding, so
- If IsStrictReference(ref) is true, throw a SyntaxError exception.
- Let bindings be GetBase(ref).
- Return the result of calling the DeleteBinding concrete method of
bindings, providing GetReferencedName(ref) as the argument.
如果一个reference是一个Environment Record binding的话,但Environment Record是什么?而Environment Record binding又是什么?
这要从执行上下文(Execution Context)说起。
Execution Context
对于一个特定的执行上下文,它有如下构成:
{
LexicalEnvironment: {},
VariableEnvironment: {},
ThisBinding: {}
}
ThisBinding很好理解,就是一个特定执行上下文的this
值。而LexicalEnvironment和VariableEnvironment又是什么?这两个都是Lexical Environment,(摔,术语越来越多了)。
一个Lexical Environment由两部分组成:
{
EnvironmentRecord: {}, //一个Environment Record
OuterLexicalEnvironment: outer //指向它外层的词法环境
}
那环境记录(Environment Record)是什么呢?
Environment Record
Environment Record
分为两种,一种是Object Environment Record
,另一种是Declarative Environment Record
。 从概念上来讲,这两者区别不大,它们都实现了相同的接口。唯一区别就是Object Environment Record
是一个用户可访问到的Javascript Object。而Declarative Environment Record
无法在JS代码中访问到。一个Environment Record
上会有一系列的绑定(binding),如果把Environment Record
当做一个对象的话,那么它上面的绑定(binding)就可以认为是它的属性了。
//对于一个函数
function hello(b, c) {
var a = 10;
}
hello();//执行它会进入一个新的Execution Context
//它有一个Environment Record
er = {
a: undefined,
b: undefined,
c: undefined,
arguments: `List of args`
}
//它有一个Lexical Environment
le = {
EnvironmentRecord: er,
OuterLexicalEnvironment: GLOBAL
}
//而它的Execution Context为:
EC = {
LexicalEnvironment: le,
VariableEnvironment: le, //VariableEnvironment和LexicalEnvironment指向同一个Lexical Environment
ThisBinding: GLOBAL
}
其实对于任意Execution Context(简称EC),99%的情况你都可以认为它的LexicalEnvironment和VariableEnvironment都指向同一个Lexical Environment。但为什么还区分出这两个呢?
- 对于一个EC的VariableEnvironment,一量创建它的指向不会改变,永远是指向同一个Lexical Environment
- 对于一个LexicalEnvironment,可能会根据代码的控制流改变,如进入了
with
代码块里或是catch
代码块里,进入with
和catch
后,会创建新的LexicalEnvironment(简称LE),然后将当前的LE当做新的LE的parent,最后将EC.LexicalEnvironment指向新的LE
一旦了解了Execution Context, Lexical Environment, Environment Record这些概念,回到delete定义:
- Let bindings be GetBase(ref).
- Return the result of calling the DeleteBinding concrete method of
bindings, providing GetReferencedName(ref) as the argument.
通过GetBase(ref)取得它的Environment Record,然后调用相应的DeleteBinding的内部方法来删除binding。那么DeleteBinding又有什么玄机呢?
DeleteBinding
DeleteBinding的操作可理解为:
- 对于Object Environment Record,调用其内部的[[Delete]]方法。
- 对于Declarative Environment Record,当且仅且在创建这个Binding时指定了它是可删除的,才可以从当前Record中删掉这个binding
首先看简单的Object Environment Record情况:
a = "ttt";
delete a;
console.log(a); //报错,因为GLOBAL是一个Object Environment Record(简称OER),而a属性是可删除的
var t = {a:'ccc'}
with(t) {
delete a;
console.log(a); //报错,因为当前的Environment Record是一个指向t的OER,而其a属性是可删除的
}
对于其它情况,我们就需要充分理解Create Binding细节了,我总结了一下。
- 几乎所有的binding都是不可删除的。函数的参数,变量声明,函数声明,catch变量,arguments均不可删除
- 例外是eval环境下的变量声明和函数声明是可删除的
详细的可参见:
- 10.5 Declaration Binding Instantiation 变量声明绑定
- 12.14 The try Statement try..catch时的变量绑定
故有:
var a = "cccc";
delete a; //没用的
function s(){
}
delete s; //没用
function f(a,b){
//均没用
delete a;
delete b;
delete f;
delete arguments;
}
try {
throw "hello";
} catch(e) {
delete e; //没用
}
eval("var a = 'ccc'; delete a; console.log(a)");//能删掉,最后的console.log会报错
总结
- 对于对象属性而言,
delete a.b
,取决于configurable
属性。- Object Environment Record 上的binding也取决于其configurable属性,然而一个OER的binding的创建方式有两种,一种是用户代码自己赋上去,如
a = 123
,另一种是引擎采用CreateBinding来创建,如在全局作用域下的var x = 123
,就会在GLOBAL对象上创建一个configurable为false的binding
- Object Environment Record 上的binding也取决于其configurable属性,然而一个OER的binding的创建方式有两种,一种是用户代码自己赋上去,如
- 对于Environment Record而言,取决于CreateBinding时是否指定了这个Binding是一个可删除了,除了eval中的变量声明和函数声明是可删除的外,其它所有binding均不可删除
- 完 -
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。