談談eval另外一面

以前在祼看ECMA262-5,在說到eval的地方,死活看不明白爲何會有一節專門扯到Direct Call to Evaljavascript

A direct call to the eval function is one that is expressed as a CallExpression that meets the following two conditions:html

The Reference that is the result of evaluating the MemberExpression in the CallExpression has an environment record as its base value and its reference name is "eval".java

The result of calling the abstract operation GetValue with that Reference as the argument is the standard built-in function defined in 15.1.2.1.express

當時以爲寫規範的那羣傢伙必定是有毛病,衆所周知,eval無非是接收一個字符串,把這個字符串當作代碼解釋執行。瀏覽器

這個問題我一直不明白,而後就放着。閉包

直到有一天.....函數

關於閉包

某一天,小夥伴們討論到說關於閉包變量的問題時,Y君指出,若是一個函數沒有引用到其所處閉包的a變量,那這個a變量所指向的空間將被釋放。工具

function getClosure() {
    var a = 1;
    var b = 2;

    return function inner() {
      debugger;
      console.log(b);
    }
}
var func = getClosure();
func();

正如上面代碼所示,當打開Chrome調試工具時到達debugger語句時,咱們只能訪問到b變量。試圖訪問a是會拋出ReferenceError的。性能

可是這真的能證實a引用已經被釋放了嗎?若是沒被釋放的話,還不讓在調試的時候訪問,這個設計就只能說是(嗶~)坑爹了,Y君補充道。測試

因而我寫了以下代碼作了測試:

function getClosure() {
    var a = 1;
    var b = 2;

    return function inner(val) {
      debugger;
      console.log(eval(val));
    }
}

var func = getClosure();
func('a');
func('b');

在閉包中使用eval,這樣引擎就不知道我在閉包中的會引用到什麼變量了。這一次到達debugger語句時,驚奇地發現,ab均可以直接引用到了。難道是由於eval的緣由?引擎在發現閉包中有eval以後,就不會回收閉包中的垃圾了?(由於它無從得知哪些變量會在未來被引用到)。

但若是引擎不知道未來會不會執行到eval呢?

function getClosure(){
    var a = 1;
    var b = 2;

    return function inner(func, val) {
        debugger;
        console.log(func(val));
    }
}

var f = getClosure();

f(eval, a);

eval當作函數傳進去,而後讓這個eval函數解釋執行傳入的val指向的變量。遺憾的是,到在debugger處,沒法訪問到a和b,執行到console.log(func(val))拋了異常,表示找不到引用。

因而我纔回過神,彷佛以前閱讀過蛇精病般的什麼direct call to eval的東西,跟這個相關?

回到eval

在規範中指出,進調用eval函數時:

  • 若是是直接調用eval函數的話,將當前的this指向詞法環境以及變量環境當作新的執行上下文的this指向,詞法環境以及變量環境
  • 若是不是直接調用的話,則新的執行上下文就至關於全局執行上下文。
    >更準確地說,則是以global對象全局詞法環境以及全局變量環境當作新的執行上下文的this指向,詞法環境以及變量環境

那什麼是直接調用呢?參照篇首。簡言之,有以下限制:

a. BaseValue必須是一個Environment Record
b. Reference Name必須是"eval"

以下例子:

var a = eval;
a('hello'); //非直接調用,由於Reference Name是'a'而不是'eval'

var f = {eval: eval};
f.eval("hello"); //非直接調用,由於BaseValue是f變量,而不是Environment Record

eval('hello'); //直接調用

function test() {
    var eval = window.eval,
        x    = window.eval;
    eval('hello'); //直接調用
    x('hello'); //非直接調用
}

看到規律了吧?只要是祼的eval調用就是direct call,前面不要有宿主對象,且函數名必定要是eval字符串。

再回到閉包

瞭解了什麼是直接eval調用後,若是我弄出這樣的代碼呢?

function getClosure() {
    var a = 1;
    var b = 2;
    var eval = function(x){
      return x;
    }

    return function inner(val) {
      debugger;
      console.log(eval(val));
    }
}

var func = getClosure();
func('a');
func('b');

getClosure中給一eval賦成一個徹底不相干的函數,進入到debugger時,能不能訪問到ab這兩個變量呢?思考一下。

最後

從eval能夠管窺出來,ES5的規範幾乎是從引擎現實者的角度來考慮問題。若是上下文中沒有eval的話,那麼閉包中的變量將被很好的釋放掉(並不徹底正確,參見A surprising JavaScript memory leak found at Meteor),由於引擎能夠檢測出哪些變量會被引用到,而哪些不會。

對於ES3的引擎而言,如IE6,IE7, IE8,不管閉包中是否包含eval,它們都不會釋放掉那些再也引用不到的變量。緣由在於,ES3規範中沒有對非直接eval調用進行規範,以下代碼在IE6,7,8能經過,而在現代瀏覽器中會報錯:

function getClosure(){
    var a = 1;
    var b = 2;

    return function (func, val) {
        alert(func(val));
    }
}

var f = getClosure();
f(eval, 'a');
f(eval, 'b');

結論

  1. 不要在閉包中用eval,許多垃圾將得不到回收。
  2. 不要採用eval當函數名,引擎會誤認爲那是一個direct call to eval的,而後內存得不到釋放。
  3. 從理論上而言,ES5比ES3要有更好的性能,ES5更多地考慮到了內存管理的問題。
相關文章
相關標籤/搜索