摘要:賦值操作符會導致查詢。中有兩個機制可以欺騙詞法作用域和。如果是作為賦值表達式的一部分的話,那它就是一個函數表達式,如果被包含在一個函數體內,或者位于程序的最頂部的話,那它就是一個函數聲明。但函數不是唯一的作用域單元。
本文僅整理自己所學做為筆記,如有錯誤請指正。
作用域作用域是一套規則,用于確定在何處以及如何查找變量(標識符)。如果查找的目的是對變量進行賦值,那么就會使用 LHS 查詢;如果目的是獲取變量的值,就會使用 RHS 查詢。賦值操作符會導致 LHS 查詢。 = 操作符或調用函數時傳入參數的操作都會導致關聯作用域的賦值操作。
JavaScript 引擎首先會在代碼執行前對其進行編譯,在這個過程中,像 var a = 2 這樣的聲明會被分解成兩個獨立的步驟:
首先, var a 在其作用域中聲明新變量。這會在最開始的階段,也就是代碼執行前進行。
接下來, a = 2 會查詢(LHS 查詢)變量 a 并對其進行賦值。
LHS 和 RHS 查詢都會在當前執行作用域中開始,如果有需要(也就是說它們沒有找到所需的標識符),就會向上級作用域繼續查找目標標識符,這樣每次上升一級作用域(一層樓),最后抵達全局作用域(頂層),無論找到或沒找到都將停止。
不成功的 RHS 引用會導致拋出 ReferenceError 異常。不成功的 LHS 引用會導致自動隱式地創建一個全局變量(非嚴格模式下),該變量使用 LHS 引用的目標作為標識符,或者拋出 ReferenceError 異常(嚴格模式下)。
詞法作用域詞法作用域意味著作用域是由書寫代碼時函數聲明的位置來決定的。編譯的詞法分析階段基本能夠知道全部標識符在哪里以及是如何聲明的,從而能夠預測在執行過程中如何對它們進行查找。
JavaScript 中有兩個機制可以“欺騙”詞法作用域: eval(..) 和 with 。前者可以對一段包含一個或多個聲明的“代碼”字符串進行演算,并借此來修改已經存在的詞法作用域(在運行時)。后者本質上是通過將一個對象的引用當作作用域來處理,將對象的屬性當作作用域中的標識符來處理,從而創建了一個新的詞法作用域(同樣是在運行時)。
這兩個機制的副作用是引擎無法在編譯時對作用域查找進行優化,因為引擎只能謹慎地認為這樣的優化是無效的。使用這其中任何一個機制都將導致代碼運行變慢。不要使用它們。
因為 JavaScript 采用的是詞法作用域,函數的作用域在函數定義的時候就決定了。
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope();//local scope
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()();//local scope
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } var foo = checkscope(); foo();//local scope函數表達式和函數聲明
函數聲明:function 函數名稱 (參數:可選){ 函數體 }
函數表達式:function 函數名稱(可選)(參數:可選){ 函數體 }
辨別:
如果不聲明函數名稱,它肯定是表達式。
如果function foo(){}是作為賦值表達式的一部分的話,那它就是一個函數表達式,如果function foo(){}被包含在一個函數體內,或者位于程序的最頂部的話,那它就是一個函數聲明。
被括號括住的(function foo(){}),他是表達式的原因是因為括號 ()是一個分組操作符,它的內部只能包含表達式
function foo(){} // 聲明,因為它是程序的一部分 (function(){ function bar(){} // 聲明,因為它是函數體的一部分 })(); var bar = function foo(){}; // 表達式,因為它是賦值表達式的一部分 new function bar(){}; // 表達式,因為它是new表達式 (function foo(){}); // 表達式:包含在分組操作符內 try { (var x = 5); // 分組操作符,只能包含表達式而不能包含語句:這里的var就是語句 } catch(err) { // SyntaxError }
函數聲明在條件語句內雖然可以用,但是沒有被標準化,最好使用函數表達式
函數聲明會覆蓋變量聲明,但不會覆蓋變量賦值
function value(){ return 1; } var value; alert(typeof value); //"function"函數作用域和塊作用域
函數是 JavaScript 中最常見的作用域單元。本質上,聲明在一個函數內部的變量或函數會在所處的作用域中“隱藏”起來,這是有意為之的良好軟件的設計原則。但函數不是唯一的作用域單元。
塊作用域指的是變量和函數不僅可以屬于所處的作用域,也可以屬于某個代碼塊(通常指 { .. } 內部)。
從 ES3 開始, try/catch 結構在 catch 分句中具有塊作用域。
在 ES6 中引入了 let 關鍵字( var 關鍵字的表親),用來在任意代碼塊中聲明變量。
if(..) { let a = 2; } 會聲明一個劫持了 if 的 { .. } 塊的變量,并且將變量添加到這個塊中。
有些人認為塊作用域不應該完全作為函數作用域的替代方案。兩種功能應該同時存在,開發者可以并且也應該根據需要選擇使用何種作用域,創造可讀、可維護的優良代碼。
提升我們習慣將 var a = 2; 看作一個聲明,而實際上 JavaScript 引擎并不這么認為。它將 var a和 a = 2 當作兩個多帶帶的聲明,第一個是編譯階段的任務,而第二個則是執行階段的任務。
這意味著無論作用域中的聲明出現在什么地方,都將在代碼本身被執行前首先進行處理。可以將這個過程形象地想象成所有的聲明(變量和函數)都會被“移動”到各自作用域的最頂端,這個過程被稱為提升。
聲明本身會被提升,而包括函數表達式的賦值在內的賦值操作并不會提升。
要注意避免重復聲明,特別是當普通的 var 聲明和函數聲明混合在一起的時候,否則會引
起很多危險的問題!
var a; if (!("a" in window)) { a = 1; } alert(a);作用域閉包
通常,程序員會錯誤的認為,只有匿名函數才是閉包。其實并非如此,正如我們所看到的 —— 正是因為作用域鏈,使得所有的函數都是閉包(與函數類型無關: 匿名函數,FE,NFE,FD都是閉包), 這里只有一類函數除外,那就是通過Function構造器創建的函數,因為其[[Scope]]只包含全局對象。 為了更好的澄清該問題,我們對ECMAScript中的閉包作兩個定義(即兩種閉包):
ECMAScript中,閉包指的是:
從理論角度:所有的函數。因為它們都在創建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因為函數中訪問全局變量就相當于是在訪問自由變量,這個時候使用最外層的作用域。
從實踐角度:以下函數才算是閉包:
即使創建它的上下文已經銷毀,它仍然存在(比如,內部函數從父函數中返回)
在代碼中引用了自由變量
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, j*1000 ); })( i ); } for (var i=1; i<=5; i++) { let j = i; // 是的,閉包的塊作用域! setTimeout( function timer() { console.log( j ); }, j*1000 ); } for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0]();//3 data[1]();//3 data[2]();//3模塊
模塊有兩個主要特征:(1)為創建內部作用域而調用了一個包裝函數;(2)包裝函數的返回
值必須至少包括一個對內部函數的引用,這樣就會創建涵蓋整個包裝函數內部作用域的閉
包。
var MyModules = (function Manager() { var modules = {}; function define(name, deps, impl) { for (var i=0; i未來模塊機制 //bar.js function hello(who) { return "Let me introduce: " + who; } export hello;//foo.js // 僅從 "bar" 模塊導入 hello() import hello from "bar"; var hungry = "hippo"; function awesome() { console.log( hello( hungry ).toUpperCase() ); } export awesome;baz.js // 導入完整的 "foo" 和 "bar" 模塊 module foo from "foo"; module bar from "bar"; console.log( bar.hello( "rhino" ) ); // Let me introduce: rhino foo.awesome(); // LET ME INTRODUCE: HIPPO塊作用域替代方案Google 維護著一個名為 Traceur 的項目,該項目正是用來將 ES6 代碼轉換成兼容 ES6 之前的環境(大部分是 ES5,但不是全部)。TC39 委員會依賴這個工具(也有其他工具)來測試他們指定的語義化相關的功能。
{ try { throw undefined; } catch (a) { a = 2; console.log( a ); } } console.log( a )上下文 EC(執行環境或者執行上下文,Execution Context)EC={ VO:{/* 函數中的arguments對象, 參數, 內部的變量以及函數聲明 */}, this:{}, Scope:{ /* VO以及所有父執行上下文中的VO */} }ECS(執行環境棧Execution Context Stack)//ECS=[Window] A(//ECS=[Window,A] B(//ECS=[Window,A,B] //run B ) //ECS=[Window,A] ) //ECS=[Window]VO(變量對象,Variable Object)var a = 10; function test(x) { var b = 20; }; test(30); /* VO(globalContext) a: 10, test: VO(test functionContext) x: 30 b: 20 */AO(活動對象,Active Object)function test(a, b) { var c = 10; function d() {} var e = function _e() {}; (function x() {}); } test(10); /* AO(test) = { a: 10, b: undefined, c: undefined, d:scope chain(作用域鏈)和[[scope]]屬性e: undefined }; */ Scope = AO|VO + [[Scope]]例子var x = 10; function foo() { var y = 20; function bar() { var z = 30; alert(x + y + z); } bar(); } foo(); // 60全局上下文的變量對象是:
globalContext.VO === Global = { x: 10 foo:}; 在“foo”創建時,“foo”的[[scope]]屬性是:
foo.[[Scope]] = [ globalContext.VO ];在“foo”激活時(進入上下文),“foo”上下文的活動對象是:
fooContext.AO = { y: 20, bar:}; “foo”上下文的作用域鏈為:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.: fooContext.Scope = [ fooContext.AO, globalContext.VO ];內部函數“bar”創建時,其[[scope]]為:
bar.[[Scope]] = [ fooContext.AO, globalContext.VO ];在“bar”激活時,“bar”上下文的活動對象為:
barContext.AO = { z: 30 };“bar”上下文的作用域鏈為:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.: barContext.Scope = [ barContext.AO, fooContext.AO, globalContext.VO ];對“x”、“y”、“z”的標識符解析如下:
- "x" -- barContext.AO // not found -- fooContext.AO // not found globalContext.VO // found - 10 - "y" -- barContext.AO // not found fooContext.AO // found - 20 - "z" barContext.AO // found - 30參考資料:
深入理解JavaScript系列(16):閉包(Closures)
JavaScript深入之執行上下文棧
JavaScript深入之詞法作用域和動態作用域
你不懂JS:作用域與閉包
變量對象(Variable object)
深入理解JavaScript系列(14):作用域鏈(Scope Chain)
let 是否會聲明提升?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/99319.html
摘要:賦值操作符會導致查詢。中有兩個機制可以欺騙詞法作用域和。如果是作為賦值表達式的一部分的話,那它就是一個函數表達式,如果被包含在一個函數體內,或者位于程序的最頂部的話,那它就是一個函數聲明。但函數不是唯一的作用域單元。 本文僅整理自己所學做為筆記,如有錯誤請指正。 作用域 作用域是一套規則,用于確定在何處以及如何查找變量(標識符)。如果查找的目的是對變量進行賦值,那么就會使用 LHS 查...
摘要:捕獲所有參數綁定當一個函數用作構造函數時使用關鍵字,它的被綁定到正在構造的新對象。使用來調用函數,或者說發生構造函數調用時,會自動執行下面的操作你不知道的創建或者說構造一個全新的對象。在箭頭函數中,與封閉詞法上下文的保持一致。 this 實際上是在函數被調用時發生的綁定,它指向什么完全取決于函數的調用位置(也就是函數的調用方法)。 四條規則:(你不知道的JS) 1. 默認綁定 func...
摘要:捕獲所有參數綁定當一個函數用作構造函數時使用關鍵字,它的被綁定到正在構造的新對象。使用來調用函數,或者說發生構造函數調用時,會自動執行下面的操作你不知道的創建或者說構造一個全新的對象。在箭頭函數中,與封閉詞法上下文的保持一致。 this 實際上是在函數被調用時發生的綁定,它指向什么完全取決于函數的調用位置(也就是函數的調用方法)。 四條規則:(你不知道的JS) 1. 默認綁定 func...
閱讀 1664·2021-11-16 11:44
閱讀 2402·2021-10-11 11:07
閱讀 4062·2021-10-09 09:41
閱讀 669·2021-09-22 15:52
閱讀 3193·2021-09-09 09:33
閱讀 2710·2019-08-30 15:55
閱讀 2289·2019-08-30 15:55
閱讀 843·2019-08-30 15:55