JavaScript中作用域相關的那些點

longwenli7 7年前發布 | 10K 次閱讀 JavaScript開發 JavaScript

本文為《你不知道的JavaScript(上卷)》中關于作用域相關的知識點的總結。

作用域

賦值操作

變量的賦值操作實際上有兩個動作,首先編譯器會在當前作用域中聲明一個變量(如果之前沒有聲明過),然后在運行時引擎會在作用域中查找該變量,如果能夠找到就對它進行賦值。

看完這段話,我相信你一定想到了前端入門時,一定會接觸的一個名詞“變量提升”。

舉個最簡單的例子:

alert(a); // undefined
var a = 12;

有同樣作用的是 function ,例如:

alert(func); // function func(){}
function func() {};

但是函數表達式不會提升:

foo(); // TypeError

var foo = function bar() {
    ...
}

注意:僅有 var 和 function 這兩個關鍵字才可以變量提升。

ES6 中新增的 let 以及 const 關鍵字不可以進行變量提升,我們可以嘗試一下:

// 1. let
alert(a); // Uncaught ReferenceError: a is not defined
let a = 'abc';

// 2. const
alert(b); // Uncaught ReferenceError: b is not defined
const b = 123;

LHS以及RHS

在運行時引擎會在作用域中查找該變量

引擎對變量所做的查找分為 LHS查詢 以及 RHS查詢 , L 和 R 分別代表一個賦值操作的左側以及右側。

我們可以簡單的記憶:

當變量出現在賦值操作的左側時進行 LHS查詢 ,出現在賦值操作的右側時進行 RHS查詢 .

注意:作用域查找會在找到第一個匹配的標識符時停止

作用域嵌套

作用域是根據名稱查找變量的 一套規則

作用域嵌套的定義如下:

當一個塊或者函數嵌套在另一個塊或函數中時,就發生了作用域的嵌套。

理解作用域嵌套這一機制,我們就可以理解變量查找的順序:

  1. 在當前作用域查找變量。如果沒有,則進行下一步

  2. 判斷是否是全局作用域。如果是,則停止查找過程;如果不是,則進行下一步

  3. 進入當前作用域的外層作用域,并進行第一步

形象一點,我們可以把作用域查找想象成在大樓中找人。

第一層代表當前作用域,大樓的頂層代表全局作用域。

首先在當前樓層查找,如果沒有找到,則上一樓進行查找,一直到找到這個人或者找完整個大樓依然沒有找到為止。

異常報錯的種類

如果能將 LHS 以及 RHS 進行很好的區分,那我們就能夠很好的理解瀏覽器所拋出的各種異常。

下舉幾種特別常見的報錯:

  • ReferenceError :

    RHS
    LHS
    
  • TypeError :

    1. RHS 找到該變量值,但嘗試對這個變量的值進行不合理的操作(例如,引用 null 或者 undefined 類型的值中的屬性)

詞法作用域

詞法作用域完全由寫代碼期間函數所聲明的位置來定義

欺騙詞法作用域

注意:欺騙詞法作用域會導致性能下降

eval

eval() 是一個危險的函數, 他執行的代碼擁有著執行者的權利。如果你運行eval()伴隨著字符串,那么你的代碼可能被惡意方(不懷好意的人)影響, 通過在使用方的機器上使用惡意代碼,可能讓你失去在網頁或者擴展程序上的權限。更重要的是,第三方代碼可以看到作用域在某一個eval()被調用的時候,這有可能導致一些不同方式的攻擊。相似的Function就是不容易被攻擊的。

with

根據你所傳遞給它的對象憑空創建了一個 全新的詞法作用域

性能問題

欺騙詞法作用域會導致性能下降,其原因在于 編譯階段的性能優化不起作用

JavaScript引擎會在編譯階段進行數項的性能優化。其中的某些優化依賴于能夠根據代碼的詞法進行靜態分析,并預先確定所有變量和函數的定義位置,才能在執行的過程中快速找到標識符。

但是,編譯到含有 eval 和 with 的代碼時,編譯器無法知道 eval 或者 with 會接受什么代碼,自然無法做代碼優化。

函數作用域以及塊作用域

函數作用域:屬于這個函數的全部變量都可以在整個函數的范圍內使用及復用(事實上在嵌套的作用域中也可以使用)。

隱藏組件內部實現

開發者最主要是利用函數作用域實現隱藏組件或者API的內部實現,最小限度的暴露必要內容。

比如對于一些組件的開發,大家習慣于利用立即執行函數 (function() {})() 進行內部實現的封裝。

規避沖突

利用函數作用域將變量保持在私有、無沖突的作用域中,這樣可以有效規避掉所有的沖突。

舉個例子, underscore 這個庫里面有跟原生js一樣的方法 map ,那怎么區分這兩個方法呢?通過將 map 當做一個屬性掛載在 underscore 上面,這樣可以避免兩者的沖突。

立即執行函數表達式

形式如下:

(function() {...})()
(function() {...})()

上面兩種形式沒有區別,可依個人興趣隨意使用。

立即執行函數表達式的一種進階用法就是把它們當做函數調用并傳遞參數進去。

各種類庫常見的用法是:

(function(global) {
    ...
})(window)

塊作用域

塊作用域目前在 ES6 中有如下體現:

  1. let

  2. const

  3. with :用 with 從對象創建出的作用域僅在 with 聲明而非外部作用域中有效。

  4. try/catch : catch 分句會創建一個塊作用域,其中聲明的變量僅在 catch 內部有效。

例如:

for (let i; i < 4; i ++) {
    ...
}

console.log(i) // Uncaught ReferenceError: i is not defined
try {
    undefined();
} catch (err) {
    console.log(err);
}

console.log(err); // Uncaught ReferenceError: err is not defined

提升

函數優先

先來看下面的代碼:

foo(); // 1
var foo;

function foo() {
    console.log(1);
}

foo = function() {
    console.log(2);
}

上面的例子說明:

函數會被首先提升,然后才是變量

上面的代碼實際等于:

function foo() {
    console.log(1);
}

foo(); // 1

var foo;

foo = function() {
    console.log(2);
}

作用域閉包

知乎上面有關于閉包的問題: 什么是閉包?

其中寸志老師的解釋我認為是比較好的。

對于閉包,《你不知道的JavaScript(上卷)》這本書的解釋是:

當函數可以記住并訪問所在的詞法作用域時,就產生了閉包。

我們實際上來理解閉包時,需要特別注意是兩個點: 函數 和 作用域 。

簡單的來說,就是函數以及作用域的結合,注意,作用域必須是封閉的,其主要的表現形式就是函數中返回一個函數。

閉包在類庫、組件封裝中有太多的示例了,本文就不拓展了。

塊作用域與閉包的結合

首先看一個單純的閉包的代碼:

for (var i = 0; i <= 5; i++) {
    (function() {
       var j = i;
       setTimeout(function timer(){
           console.log(j);
       }, j * 1000) 
    })()
}

這段代碼就是在每次循環的時候創建一個新的封閉作用域,保存當次循環的i值。

再看一下下面的代碼:

for (let i = 0; i <= 5; i++) {
    setTimeout(function timer(){
        console.log(i);
    }, i*1000)
}

利用let創建塊作用域,當塊作用域與閉包結合之后,我們可以減少創建新的封閉作用域這一操作( var j = i );

that's cool!

模塊

模塊這一利器,在以前封裝插件用的非常多,示例如下:

var foo = (function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];

    function doSomething() {
        console.log(something)
    }

    function doAnother() {
        console.log(another.join("!"));
    }

    return  {
        doSomething: doSomething,
        doAnother: doAnother
    }
})()

模塊模式必備條件如下:

  1. 必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會創建一個新的莫模塊實例)。

  2. 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態。

當然,說到模塊,我們不得不提到 CMD 、 AMD 、 ES6 module 等模塊機制了。

我這里簡單提一下兩者的區別:

  • AMD:

    • early executing(提前執行)

    • 推薦依賴前置

    • 示例: requireJs

  • CMD:

    • as lazy as possible(延遲執行)

    • 推薦依賴就近

    • 示例: seaJs

繼續聊一下 ES6 的模塊機制( import 、 export )。

import 可以將一個模塊中的一個或多個API導入到當前的作用域中,并分別綁定在一個變量上。

export 會將當前模塊的一個標識符(變量、函數)導出為公共API。

Github 有很多基于 es6 實現的代碼功能,請自行查閱。

動態詞法作用域

動態作用域鏈是基于調用棧的,而不是代碼中的作用域嵌套。

對于 JavaScript ,不存在動態作用域。如果一定要找一個點與動態詞法作用域扯上關系的話,那就是 this 值了。 

好了,作用域相關的點整理完了,如果有遺漏,歡迎指正~

 

來自:https://segmentfault.com/a/1190000008501331

 

 本文由用戶 longwenli7 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!