談談Javascript中的delete操做符

你以爲下列代碼中,哪些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:本文所表述的內容均由規範演譯而來,而非經過實驗進行推理,代碼示例僅用來證實文中闡述內容的正確性,若有疑惑歡迎留言討論。this

delete究竟在作啥?

參見EMCA 262-5 第11.4.1小節:lua

The production UnaryExpression : delete UnaryExpression is evaluated as follows:code

  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 Accessorsip

對於以下代碼(在全局做用域下):作用域

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均不可刪除

- 完 -

相關文章
相關標籤/搜索