1. 程式人生 > >JavaScript欺騙詞法的eval、with與catch及其效能問題

JavaScript欺騙詞法的eval、with與catch及其效能問題

正常來說,執行期上下文的作用域鏈是不會改變的
JavaScript中的詞法作用域並不是一成不變的
(詞法作用域/靜態作用域: 作用域由書寫程式碼時函式宣告位置決定)
有幾種機制是可以欺騙詞法的
它們是with()、eval()還有try-catch語句的catch子句
其中with和eval我們不應該去使用(會產生很多問題)
欺騙詞法的意思就是欺騙詞法作用域
也就是說,它們在執行時改變了作用域鏈
下面我就來談談這些可以欺騙詞法的機制

eval

eval()函式接受一個字串作為引數,並且解析字串生成程式碼

var a = 123;
eval('console.log(a)'
);// 123

於是控制檯列印了123
執行了eval函式之後
js引擎並不知道這段程式碼時動態插入的,並且修改了作用域鏈
引擎還會像往常一樣查詢作用域鏈
看下面的程式碼

var a = 1;
function demo(){
    eval('var a = 2;');//欺騙詞法
    console.log(a);// 2
}
demo();

當eval函式執行時,在demo函式執行環境的最頂端作用域添加了變數a
這個區域性環境中的a“遮蔽”了全域性環境的a
最終導致程式列印2

eval()函式不僅可以修改它當前所處的作用域,甚至還可以修改全域性作用域
無論怎樣,它都可以在執行期修改詞法作用域

ES5的嚴格模式對這個函式加了一些限制,我把上面的程式碼加上區域性嚴格模式

var a = 1;
function demo(){
    'use strict';
    eval('var a = 2;');
    console.log(a);// 1
}
demo();

我們發現這回控制檯列印了1
這是因為在嚴格模式下,eval()執行時擁有自己獨立的詞法作用域(省的它給執行環境的作用域鏈搗亂)
這樣其中的宣告就無法修改它所在的作用域了

這種可以動態產生程式碼的還有兩個和它很像
定時器setTimeout()和setInterval()第一個引數可以是程式碼字串
還有函式構造器new Function()的最後一個引數同樣接受程式碼字串
和eval()一樣,不要使用這種用法,這會帶來嚴重的效能問題,這個問題一會兒再說

with

另一個不建議使用的欺騙詞法的語法就是這個with關鍵字
with通常用作重複引用某個物件的多個屬性的快捷方式
好處是可以不需要重複的引用物件本身
比如我想重複使用console物件

console.log(1);
console.info(2);
console.warn(3);

使用with關鍵字

with(console){
    log(1);
    info(2);
    warn(3);
}

列印

看起來with好像沒什麼問題,但是看下面

function demo(obj){
    with(obj){
        a = 5;
    }
}
var obj1 = {a:1};
var obj2 = {b:2};

demo(obj1);
console.log(obj1.a);// 5

demo(obj2);
console.log(obj2.a);// undefined
console.log(a);//5 -->變數a居然洩漏到了全域性環境

我們發現使用with關鍵字修改了obj1的a
但是它不僅沒有在obj2上增加a,反而產生副作用洩露到了全域性
這是因為with可以把一個物件處理為一個完全隔離的詞法作用域(放到作用域鏈的最前面)
所以在它內部產生執行a = 5;
它會向下查詢作用域鏈,但沒有找到,於是在全域性建立了一個a變數(沒有var 的宣告)

注意:雖然with產生了一個詞法作用域,但是with內部的正常var宣告不會被限制在這個塊作用域中
也就是說宣告在with外部的作用域
像這樣

function demo(){
    var obj = {};
    with(obj){
        var b = 1;
        console.log(b); // 1
    }
    console.log(b); // 1
}
demo();
console.log(b);// Uncaught ReferenceError: b is not defined

而且with關鍵字在ES5的嚴格模式乾脆就不讓用
如果你嘗試使用你會看到這樣的錯誤:

這裡寫圖片描述

catch

除了eval與with之外,try-catch語句中的catch子句同樣可以修改執行環境的作用域鏈
當try程式碼塊內發生錯誤,執行流立即跳轉到catch子句
隨後把異常物件推入一個可變物件並且放到作用域鏈最前面,這和with很像
一旦catch子句執行完畢,作用域鏈就會恢復原樣

但是和eval和with不同,try-catch還是相對有用,不用完全拋棄(雖然我沒用過)

效能

欺騙詞法會產生效能問題
js引擎在編譯階段會進行效能優化,很多優化依賴於能夠根據程式碼詞法進行靜態分析
預先確定了變數和函式的定義位置,才能快速找到識別符號
但是eval或with無法判斷識別符號位置(存在於程式碼執行過程中,無法靜態分析)
也就是說:在eval和with面前,js引擎所有的優化沒有任何意義(簡直酷炫)
既然沒意義,js引擎乾脆就不優化了
這樣就導致程式執行變慢了

對於with,它還有自己獨特的效能問題…
產生了作用域,就會導致它所在的函式的所有區域性變數處於第二個作用鏈物件
訪問代價更高了

對於try-catch語句,如果我們想要使用,可以這樣做

try{
    ...
}catch(e){
    handleError(e);
}

在catch語句中只執行了一段程式碼,委託給一個函式用於處理錯誤
這樣沒有區域性變數的訪問
作用域鏈的臨時改變就不會影響效能

總結

總結一下重點

  • 詞法作用域意味著作用域是書寫程式碼時函式宣告的位置來決定
    編譯時詞法分析階段能知道所有識別符號在哪裡及如何宣告
  • eval可以對程式碼字串進行演算,藉此在執行時修改了詞法作用域
  • with通過將一個物件引用當作作用域來處理,藉此在執行時建立了詞法作用域
  • eval在嚴格模式下會產生獨立詞法作用域,無法修改所在作用域
  • with在嚴格模式下禁止使用
  • eval與with(還有catch)可以欺騙詞法,在執行時修改作用域鏈
  • eval與with致使js引擎無法在編譯階段優化作用域查詢(無法靜態分析),導致程式變慢

說了這麼多,就是要告訴大家不要使用with關鍵字和eval函式~( ̄0 ̄)/