1

你觉得下列代码中,哪些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:

  1. Let ref be the result of evaluating UnaryExpression.
  2. If Type(ref) is not Reference, return true.
  3. If IsUnresolvableReference(ref) then,

    • If IsStrictReference(ref) is true, throw a SyntaxError exception.
    • Else, return true.
  4. 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.
  5. 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代码块里,进入withcatch后,会创建新的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细节了,我总结了一下。

  1. 几乎所有的binding都是不可删除的。函数的参数,变量声明,函数声明,catch变量,arguments均不可删除
  2. 例外是eval环境下的变量声明和函数声明是可删除的

详细的可参见:

故有:

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
  • 对于Environment Record而言,取决于CreateBinding时是否指定了这个Binding是一个可删除了,除了eval中的变量声明和函数声明是可删除的外,其它所有binding均不可删除

- 完 -


ssnau
1.5k 声望98 粉丝

负能量职业打码师