免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
從 JavaScript 作用域說開去


目錄


1.靜態(tài)作用域與動態(tài)作用域

2.變量的作用域

3.JavaScript 中變量的作用域

4.JavaScript 欺騙作用域

5.JavaScript 執(zhí)行上下文

6.JavaScript 中的作用域鏈

7.JavaScript 中的閉包

8.JavaScript 中的模塊


靜態(tài)作用域與動態(tài)作用域


在電腦程序設(shè)計(jì)中,作用域(scope,或譯作有效范圍)是名字(name)與實(shí)體(entity)的綁定(binding)保持有效的那部分計(jì)算機(jī)程序。不同的編程語言可能有不同的作用域和名字解析。而同一語言內(nèi)也可能存在多種作用域,隨實(shí)體的類型變化而不同。作用域類別影響變量的綁定方式,根據(jù)語言使用靜態(tài)作用域還是動態(tài)作用域變量的取值可能會有不同的結(jié)果。


  • 包含標(biāo)識符的宣告或定義;

  • 包含語句和/或表達(dá)式,定義或部分關(guān)于可運(yùn)行的算法;

  • 嵌套嵌套或被嵌套嵌套。


原文中此處為鏈接,暫不支持采集

原文中此處為鏈接,暫不支持采集


原文中此處為鏈接,暫不支持采集

原文中此處為鏈接,暫不支持采集


作用域又分為兩種,靜態(tài)作用域和動態(tài)作用域。


靜態(tài)作用域又叫做詞法作用域,采用詞法作用域的變量叫詞法變量。詞法變量有一個在編譯時靜態(tài)確定的作用域。詞法變量的作用域可以是一個函數(shù)或一段代碼,該變量在這段代碼區(qū)域內(nèi)可見(visibility);在這段區(qū)域以外該變量不可見(或無法訪問)。詞法作用域里,取變量的值時,會檢查函數(shù)定義時的文本環(huán)境,捕捉函數(shù)定義時對該變量的綁定。


function f() {

    function g() {

  }

}


靜態(tài)(詞法)作用域,就是可以無須執(zhí)行程序而只從程序源碼的角度,就可以看出程序是如何工作的。從上面的例子中可以肯定,函數(shù) g 是被函數(shù) f 包圍在內(nèi)部。


大多數(shù)現(xiàn)在程序設(shè)計(jì)語言都是采用靜態(tài)作用域規(guī)則,如C/C 、C#、Python、Java、JavaScript……


相反,采用動態(tài)作用域的變量叫做動態(tài)變量。只要程序正在執(zhí)行定義了動態(tài)變量的代碼段,那么在這段時間內(nèi),該變量一直存在;代碼段執(zhí)行結(jié)束,該變量便消失。這意味著如果有個函數(shù)f,里面調(diào)用了函數(shù)g,那么在執(zhí)行g(shù)的時候,f里的所有局部變量都會被g訪問到。而在靜態(tài)作用域的情況下,g不能訪問f的變量。動態(tài)作用域里,取變量的值時,會由內(nèi)向外逐層檢查函數(shù)的調(diào)用鏈,并打印第一次遇到的那個綁定的值。顯然,最外層的綁定即是全局狀態(tài)下的那個值。


function g() {

}


function f() {

   g();

}


當(dāng)我們調(diào)用f(),它會調(diào)用g()。在執(zhí)行期間,g被f調(diào)用代表了一種動態(tài)的關(guān)系。


采用動態(tài)作用域的語言有Pascal、Emacs Lisp、Common Lisp(兼有靜態(tài)作用域)、Perl(兼有靜態(tài)作用域)。C/C 是靜態(tài)作用域語言,但在宏中用到的名字,也是動態(tài)作用域。

變量的作用域


1. 變量的作用域


變量的作用域是指變量在何處可以被訪問到。比如:


function foo(){

    var bar;

}


這里的 bar 的直接作用域是函數(shù)作用域foo();


2. 詞法作用域


JavaScript 中的變量都是有靜態(tài)(詞法)作用域的,因此一個程序的靜態(tài)結(jié)構(gòu)就決定了一個變量的作用域,這個作用域不會被函數(shù)的位置改變而改變。


3. 嵌套作用域


如果一個變量的直接作用域中嵌套了多個作用域,那么這個變量在所有的這些作用域中都可以被訪問:


function foo (arg) {

    function bar() {

        console.log( 'arg:' arg );

    }

    bar();

}


console.log(foo('hello'));   // arg:hello


arg的直接作用域是foo(),但是它同樣可以在嵌套的作用域bar()中被訪問,foo()是外部的作用域,bar()是內(nèi)部作用域。


4. 覆蓋的作用域


如果在一個作用域中聲明了一個與外層作用域同名的變量,那么這個內(nèi)部作用域以及內(nèi)部的所有作用域中將會訪問不到外面的變量。并且內(nèi)部的變量的變化也不會影響到外面的變量,當(dāng)變量離開內(nèi)部的作用域以后,外部變量又可以被訪問了。


var x = 'global';


function f() {

   var x = 'local';

   console.log(x);   // local

}


f();

console.log(x);  // global


這就是覆蓋的作用域。


JavaScript 中變量的作用域


大多數(shù)的主流語言都是有塊級作用域的,變量在最近的代碼塊中,Objective-C 和 Swift 都是塊級作用域的。但是在 JavaScript 中的變量是函數(shù)級作用域的。不過在最新的 ES6 中加入了 let 和 const 關(guān)鍵字以后,就變相支持了塊級作用域。到了 ES6 以后支持塊級作用域的有以下幾個:


with 語句


用 with 從對象中創(chuàng)建出的作用域僅在 with 聲明中而非外 部作用域中有效。


try/catch 語句


JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會創(chuàng)建一個塊作用域,其中聲明的變量僅在 catch 內(nèi)部有效。


let 關(guān)鍵字


let關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是{ .. }內(nèi)部)。換句話說,let 為其聲明的變量隱式地了所在的塊作用域。


const 關(guān)鍵字


除了 let 以外,ES6 還引入了 const,同樣可以用來創(chuàng)建塊作用域變量,但其值是固定的 (常量)。之后任何試圖修改值的操作都會引起錯誤。


這里就需要注意變量和函數(shù)提升的問題了,這個問題在前一篇文章里面詳細(xì)的說過了,這里不再贅述了。


不過這里還有一個坑,如果賦值給了一個未定義的變量,會產(chǎn)生一個全局變量。


在非嚴(yán)格模式下,不通過 var 關(guān)鍵字直接給一個變量賦值,會產(chǎn)生一個全局的變量


function func() { x = 123; }

func();

x

<123


不過在嚴(yán)格模式下,這里會直接報(bào)錯。


function func() { 'use strict'; x = 123; }

func();

<ReferenceError: x is not defined


在 ES5 中,經(jīng)常會通過引入一個新的作用域來限制變量的生命周期,通過 IIFE(Immediately-invoked function expression,立即執(zhí)行的函數(shù)表達(dá)式)來引入新的作用域。


通過 IIFE ,我們可以


  • 避免全局變量,隱藏全局作用域的變量。

  • 創(chuàng)建新的環(huán)境,避免共享。

  • 保持全局的數(shù)據(jù)對于構(gòu)造器的數(shù)據(jù)相對獨(dú)立。

  • 將全局的數(shù)據(jù)附加到單例對象上。

  • 將全局?jǐn)?shù)據(jù)附加到方法中。

JavaScript 欺騙作用域


(1). with 語句


with 語句被很多人都認(rèn)為是 JavaScript 里面的糟粕( Bad Parts )。起初它被設(shè)計(jì)出來的目的是好的,但是它導(dǎo)致的問題多于它解決的問題。


with 起初設(shè)計(jì)出來是為了避免冗余的對象調(diào)用。


舉個例子:


foo.a.b.c = 888;

foo.a.b.d = 'halfrost';


這時候用 with 語句就可以縮短調(diào)用:


with (foo.a.b) {

      c = 888;

      d = 'halfrost';

}


但是這種特性卻帶來了很多問題:


function myLog( errorMsg , parameters) {

  with (parameters) {

    console.log('errorMsg:' errorMsg);

  }

}


myLog('error',{});

<errorMsg:error


myLog('error',{ errorMsg:'stackoverflow' }); 

<errorMsg:stackoverflow


可以看到輸出就出現(xiàn)問題了,由于 with 語句,覆蓋掉了第一個入?yún)ⅰMㄟ^閱讀代碼,有時候是不能分辨出這些問題,它也會隨著程序的運(yùn)行,導(dǎo)致發(fā)生不多的變化,這種對未來的不確定性就很容易出現(xiàn)

bug。


with 會導(dǎo)致3個問題:


  • 性能問題

變量查找會變慢,因?yàn)閷ο笫桥R時性的插入到作用域鏈中的。


  • 代碼不確定性

@Brendan Eich 解釋,廢棄 with 的根本原因不是因?yàn)樾阅軉栴},原因是因?yàn)椤皐ith 可能會違背當(dāng)前的代碼上下文,使得程序的解析(例如安全性)變得困難而繁瑣”。


  • 代碼壓縮工具不會壓縮 with 語句中的變量名


所以在嚴(yán)格模式下,已經(jīng)嚴(yán)格禁止使用 with 語句。


Uncaught SyntaxError: Strict mode code may not include a with statement


如果還是想避免使用 with 語句,有兩種方法:


  1. 用一個臨時變量替代傳進(jìn) with 語句的對象。

  2. 如果不想引入臨時變量,可以使用 IIFE 。


(function () {

  var a = foo.a.b;

  console.log('Hello' a.c a.d);

}());


或者


(function (bar) {

  console.log('Hello' bar.c bar.d);

}(foo.a.b));


(2). eval 函數(shù)


eval 函數(shù)傳遞一個字符串給 JavaScript 編譯器,并且執(zhí)行其結(jié)果。


eval(str)


它是 JavaScript 中被濫用的最多的特性之一。


var a = 12;

eval('a 5')

<17


eval 函數(shù)以及它的親戚( Function 、setTimeout、setInterval)都提供了訪問 JavaScript 編譯器的機(jī)會。


Function() 構(gòu)造函數(shù)的形式比 eval() 函數(shù)好一點(diǎn)的地方在于,它令入?yún)⒏忧逦?/span>


new Function( param1, ...... , paramN, funcBody )



var f = new Function( 'x', 'y' , 'return x y' );

f(3,4)

<7


用 Function() 的方式至少不用使用間接的 eval() 調(diào)用來確保所執(zhí)行的代碼除了其自己的作用域只能訪問全局的變量。


在 Weex 的代碼中,就還存在著 eval() 的代碼,不過 Weex 團(tuán)隊(duì)在注釋里面承諾會改掉??偟膩碚f,最好應(yīng)該避免使用 eval() 和 new Function() 這些動態(tài)執(zhí)行代碼的方法。動態(tài)執(zhí)行代碼相對會比較慢,并且還存在安全隱患。


再說說另外兩個親戚,setTimeout、setInterval 函數(shù),它們也能接受字符串參數(shù)或者函數(shù)參數(shù)。當(dāng)傳遞的是字符串參數(shù)時,setTimeout、setInterval 會像 eval 那樣去處理。同樣也需要避免使用這兩個函數(shù)的時候使用字符串傳參數(shù)。


eval 函數(shù)帶來的問題總結(jié)如下:


  1. 函數(shù)變成了字符串,可讀性差,存在安全隱患。

  2. 函數(shù)需要運(yùn)行編譯器,即使只是為了執(zhí)行一個微不足道的賦值語句。這使得執(zhí)行速度變慢。

  3. 讓 JSLint 失效,讓它檢測問題的能力大打折扣。


JavaScript 執(zhí)行上下文



這個事情要從 JavaScript 源代碼如何被運(yùn)行開始說起。


我們都知道 JavaScript 是腳本語言,它只有 runtime,沒有編譯型語言的 buildTime,那它是如何被各大瀏覽器運(yùn)行起來的呢?


JavaScript 代碼是被各個瀏覽器引擎編譯和運(yùn)行起來的。JavaScript 引擎的代碼解析和執(zhí)行過程的目標(biāo)就是在最短時間內(nèi)編譯出最優(yōu)化的代碼。JavaScript 引擎還需要負(fù)責(zé)管理內(nèi)存,負(fù)責(zé)垃圾回收,與宿主語言的交互等。流行的引擎有以下幾種:


蘋果公司的 JavaScriptCore (JSC) 引擎,Mozilla 公司的 SpiderMonkey,微軟 Internet Explorer 的 Chakra (JScript引擎),Microsoft Edge 的 Chakra (JavaScript引擎) ,谷歌 Chrome 的 V8。



其中 V8 引擎是最著名的開源的引擎,它和前面那幾個引擎有一個最大的區(qū)別是:主流引擎都是基于字節(jié)碼的實(shí)現(xiàn),V8 的做法非常極致,直接跳過了字節(jié)碼這一層,直接把 JS 編譯成機(jī)器碼。所以 V8 是沒有解釋器的。(但是這都是歷史,V8 現(xiàn)在最新版是有解釋器的)



在2017年5月1號之后, Chrome 的 V8 引擎的v8 5.9 發(fā)布了,其中的 Ignition 字節(jié)碼解釋器將默認(rèn)啟動 :V8 Release 5.9 。v8 自此回到了字節(jié)碼的懷抱。


V8 在有了字節(jié)碼以后,消除 Cranshaft 這個舊的編譯器,并讓新的 Turbofan 直接從字節(jié)碼來優(yōu)化代碼,并當(dāng)需要進(jìn)行反優(yōu)化的時候直接反優(yōu)化到字節(jié)碼,而不需要再考慮 JS 源代碼。去掉 Cranshaft 以后,就成了 Turbofan Ignition 的組合了。



Ignition TurboFan 的組合,就是字節(jié)碼解釋器 JIT 編譯器的黃金組合。這一黃金組合在很多 JS 引擎中都有所使用,例如微軟的 Chakra,它首先解釋執(zhí)行字節(jié)碼,然后觀察執(zhí)行情況,如果發(fā)現(xiàn)熱點(diǎn)代碼,那么后臺的 JIT 就把字節(jié)碼編譯成高效代碼,之后便只執(zhí)行高效代碼而不再解釋執(zhí)行字節(jié)碼。蘋果公司的 SquirrelFish Extreme 也引入了 JIT。SpiderMonkey 更是如此,所有 JS 代碼最初都是被解釋器解釋執(zhí)行的,解釋器同時收集執(zhí)行信息,當(dāng)它發(fā)現(xiàn)代碼變熱了之后,JaegerMonkey、IonMonkey 等 JIT 便登場,來編譯生成高效的機(jī)器碼。


總結(jié)一下:


JavaScript 代碼會先被引擎編譯,轉(zhuǎn)化成能被解釋器識別的字節(jié)碼。



源碼會被詞法分析,語法分析,生成 AST 抽象語法樹。



AST 抽象語法樹又會被字節(jié)碼生成器進(jìn)行多次優(yōu)化,最終生成了中間態(tài)的字節(jié)碼。這時的字節(jié)碼就可以被解釋器執(zhí)行了。


這樣,JavaScript 代碼就可以被引擎跑起來了。


JavaScript 在運(yùn)行過程中涉及到的作用域有3種:


  1. 全局作用域(Global Scope)JavaScript 代碼開始運(yùn)行的默認(rèn)環(huán)境

  2. 局部作用域(Local Scpoe)代碼進(jìn)入一個 JavaScript 函數(shù)

  3. Eval 作用域 使用 eval() 執(zhí)行代碼


當(dāng) JavaScript 代碼執(zhí)行的時候,引擎會創(chuàng)建不同的執(zhí)行上下文,這些執(zhí)行上下文就構(gòu)成了一個執(zhí)行上下文棧(Execution context stack,ECS)。


全局執(zhí)行上下文永遠(yuǎn)都在棧底,當(dāng)前正在執(zhí)行的函數(shù)在棧頂。



當(dāng) JavaScript 引擎遇到一個函數(shù)執(zhí)行的時候,就會創(chuàng)建一個執(zhí)行上下文,并且壓入執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行完畢的時候,就會將函數(shù)的執(zhí)行上下文從棧中彈出。


對于每個執(zhí)行上下文都有三個重要的屬性,變量對象(Variable object,VO),作用域鏈(Scope chain)和this。這三個屬性跟代碼運(yùn)行的行為有很重要的關(guān)系。


變量對象 VO 是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域。它是一個與上下文相關(guān)的特殊對象,其中存儲了在上下文中定義的變量和函數(shù)聲明。也就是說,一般 VO 中會包含以下信息:


  1. 創(chuàng)建 arguments object

  2. 查找函數(shù)聲明(Function declaration)

  3. 查找變量聲明(Variable declaration)



上圖也解釋了,為何函數(shù)提升優(yōu)先級會在變量提升前面。


這里還會牽扯到活動對象(Activation object):


只有全局上下文的變量對象允許通過 VO 的屬性名稱間接訪問。在函數(shù)執(zhí)行上下文中,VO 是不能直接訪問的,此時由活動對象(Activation Object, 縮寫為AO)扮演 VO 的角色?;顒訉ο笫窃谶M(jìn)入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的 arguments 屬性初始化。



Arguments Objects 是函數(shù)上下文里的激活對象 AO 中的內(nèi)部對象,它包括下列屬性:


  1. callee:指向當(dāng)前函數(shù)的引用

  2. length: 真正傳遞的參數(shù)的個數(shù)

  3. properties-indexes:就是函數(shù)的參數(shù)值(按參數(shù)列表從左到右排列)


JavaScript 解釋器創(chuàng)建執(zhí)行上下文的時候,會經(jīng)歷兩個階段:


  • 創(chuàng)建階段(當(dāng)函數(shù)被調(diào)用,但是開始執(zhí)行函數(shù)內(nèi)部代碼之前)

創(chuàng)建 Scope chain,創(chuàng)建 VO/AO(variables, functions and arguments),設(shè)置 this 的值。


  • 激活 / 代碼執(zhí)行階段

設(shè)置變量的值,函數(shù)的引用,然后解釋/執(zhí)行代碼。


VO 和 AO 的區(qū)別就在執(zhí)行上下文的這兩個生命周期里面。



VO 和 AO 的關(guān)系可以理解為,VO 在不同的 Execution Context 中會有不同的表現(xiàn):當(dāng)在 Global Execution Context 中,直接使用的 VO;但是,在函數(shù) Execution Context 中,AO 就會被創(chuàng)建。


JavaScript 中的作用域鏈


在 JavaScript 中有兩種變量傳遞的方式


1. 通過調(diào)用函數(shù),執(zhí)行上下文的棧傳遞變量。


函數(shù)每調(diào)用一次,就需要給它的參數(shù)和變量準(zhǔn)備新的存儲空間,就會創(chuàng)建一個新的環(huán)境將(變量和參數(shù)的)標(biāo)識符合變量做映射。對于遞歸的情況,執(zhí)行上下文,即通過環(huán)境的引用是在棧中進(jìn)行管理的。這里的棧對應(yīng)了調(diào)用棧。


JavaScript 引擎會以堆棧的方式來處理它們,這個堆棧,我們稱其為函數(shù)調(diào)用棧(call stack)。棧底永遠(yuǎn)都是全局上下文,而棧頂就是當(dāng)前正在執(zhí)行的上下文。


這里舉個例子:比如用遞歸的方式計(jì)算n的階乘。


2. 作用域鏈


在 JavaScript 中有一個內(nèi)部屬性 [[ Scope ]] 來記錄函數(shù)的作用域。在函數(shù)調(diào)用的時候,JavaScript 會為這個函數(shù)所在的新作用域創(chuàng)建一個環(huán)境,這個環(huán)境有一個外層域,它通過 [[ Scope ]] 創(chuàng)建并指向了外部作用域的環(huán)境。因此在 JavaScript 中存在一個作用域鏈,它以當(dāng)前作用域?yàn)槠瘘c(diǎn),連接了外部的作用域,每個作用域鏈最終會在全局環(huán)境里終結(jié)。全局作用域的外部作用域指向了null。


作用域鏈,是由當(dāng)前環(huán)境與上層環(huán)境的一系列變量對象組成,它保證了當(dāng)前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問。


作用域是一套規(guī)則,是在 JavaScript 引擎編譯的時候確定的。

作用域鏈?zhǔn)窃趫?zhí)行上下文的創(chuàng)建階段創(chuàng)建的,這是在 JavaScript 引擎解釋執(zhí)行階段確定的。


function myFunc( myParam ) {

    var myVar = 123;

    return myFloat;

}

var myFloat = 2.0;  // 1

myFunc('ab');       // 2


當(dāng)程序運(yùn)行到標(biāo)志 1 的時候:



函數(shù) myFunc 通過 [[ Scope]] 連接著它的作用域,全局作用域。


當(dāng)程序運(yùn)行到標(biāo)志 2 的時候,JavaScript 會創(chuàng)建一個新的作用域用來管理參數(shù)和本地變量。



由于外層作用域鏈,使得 myFunC 可以訪問到外層的 myFloat 。


這就是 Javascript 語言特有的'作用域鏈'結(jié)構(gòu)(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。


作用域鏈?zhǔn)潜WC對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問。作用域鏈的前端始終是當(dāng)前執(zhí)行的代碼所在環(huán)境的變量對象。而前面我們已經(jīng)講了變量對象的創(chuàng)建過程。作用域鏈的下一個變量對象來自包含環(huán)境即外部環(huán)境,這樣,一直延續(xù)到全局執(zhí)行環(huán)境;全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。


JavaScript 中的閉包


當(dāng)函數(shù)可以記住并訪問所在的詞法作用域,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行,這時就產(chǎn)生了閉包。


接下來看看大家對閉包的定義是什么樣的:


MDN 對閉包的定義:


閉包是指那些能夠訪問獨(dú)立(自由)變量的函數(shù)(變量在本地使用,但定義在一個封閉的作用域中)。換句話說,這些函數(shù)可以「記憶」它被創(chuàng)建時候的環(huán)境。


《JavaScript 權(quán)威指南(第6版)》對閉包的定義:


函數(shù)對象可以通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)體內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi),這種特性在計(jì)算機(jī)科學(xué)文獻(xiàn)中稱為閉包。


《JavaScript 高級程序設(shè)計(jì)(第3版)》對閉包的定義:


閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。


最后是阮一峰老師對閉包的解釋:


由于在 Javascript 語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,因此可以把閉包簡單理解成定義在一個函數(shù)內(nèi)部的函數(shù)。它的最大用處有兩個,一個是前面提到的可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量的值始終保持在內(nèi)存中。


再來對比看看 OC,Swift,JS,Python 4種語言的閉包寫法有何不同:


void test() {

    int value = 10;

    void(^block)() = ^{ NSLog(@'%d', value); };

    value ;

    block();

}


// 輸出10


func test() {

    var value = 10

    let closure = { print(value) }

    value = 1

    closure()

}

// 輸出11


function test() {

    var value = 10;

    var closure = function () {

        console.log(value);

    }

    value ;

    closure();

}

// 輸出11


def test():

    value = 10

    def closure():

        print(value)

    value = value 1

    closure()

// 輸出11


可以看出 OC 的寫法默認(rèn)是和其他三種語言不同的。關(guān)于 OC 的閉包原理,iOS 開發(fā)的同學(xué)應(yīng)該都很清楚了,這里不再贅述。當(dāng)然,想要第一種 OC 的寫法輸出11,也很好改,只要把外部需要捕獲進(jìn)去的變量前面加上 __block 關(guān)鍵字就可以了。


最后結(jié)合作用域鏈和閉包舉一個例子:


function createInc(startValue) {

  return function (step) {

    startValue = step;

    return startValue;

  }

}


var inc = createInc(5);

inc(3);


當(dāng)代碼進(jìn)入到 Global Execution Context 之后,會創(chuàng)建 Global Variable Object。全局執(zhí)行上下文壓入執(zhí)行上下文棧。



Global Variable Object 初始化會創(chuàng)建 createInc ,并指向一個函數(shù)對象,初始化 inc ,此時還是 undefined。


接著代碼執(zhí)行到 createInc(5),會創(chuàng)建 Function Execution Context,并壓入執(zhí)行上下文棧。會創(chuàng)建 createInc Activation Object。



由于還沒有執(zhí)行這個函數(shù),所以 startValue 的值還是 undefined。接下來就要執(zhí)行 createInc 函數(shù)了。



當(dāng) createInc 函數(shù)執(zhí)行的最后,并退出的時候,Global VO中的 inc 就會被設(shè)置;這里需要注意的是,雖然 create Execution Context 退出了執(zhí)行上下文棧,但是因?yàn)?inc 中的成員仍然引用 createInc AO(因?yàn)?createInc AO 是 function(step) 函數(shù)的 parent scope ),所以 createInc AO 依然在 Scope 中。


接著再開始執(zhí)行 inc(3)。



當(dāng)執(zhí)行 inc(3) 代碼的時候,代碼將進(jìn)入 inc Execution Context,并為該執(zhí)行上下文創(chuàng)建 VO/AO,scope chain 和設(shè)置 this;這時,inc AO將指向 createInc AO。



最后,inc Execution Context 退出了執(zhí)行上下文棧,但是 createInc AO 沒有銷毀,可以繼續(xù)訪問。

JavaScript 中的模塊


由作用域又可以引申出模塊的概念。


在 ES6 中會大量用到模塊,通過模塊系統(tǒng)進(jìn)行加載時,ES6 會將文件當(dāng)作獨(dú)立的模塊來處理。每個模塊都可以導(dǎo)入其他模塊或特定的 API 成員,同樣也可以導(dǎo)出自己的 API 成員。


模塊有兩個主要特征:


  1. 為創(chuàng)建內(nèi)部作用域而調(diào)用了一個包裝函數(shù);

  2. 包裝函數(shù)的返回值必須至少包括一個對內(nèi)部函數(shù)的引用,這樣就會創(chuàng)建涵蓋整個包裝函數(shù)內(nèi)部作用域的閉包。


JavaScript 最主要的有 CommonJS 和 AMD 兩種,前者用于服務(wù)器,后者用于瀏覽器。在 ES6 中的 Module 使得編譯時就能確定模塊的依賴關(guān)系,以及輸入輸出的變量。CommonJS 和 AMD 模塊都只能運(yùn)行時確定這些東西。


CommonJS 模塊就是對象,輸入時必須查找對象屬性。屬于運(yùn)行時加載。CommonJS 輸入的是被輸出值的拷貝,并不是引用。


ES6 的 Module 在編譯時就完成模塊編譯,屬于編譯時加載,效率要比 CommonJS 模塊的加載方式高。ES6 模塊的運(yùn)行機(jī)制與 CommonJS 不一樣,它遇到模塊加載命令 import 時不會去執(zhí)行模塊,只會生成一個動態(tài)的只讀引用。等到真正需要的時候,再去模塊中取值。ES6 模塊加載的變量是動態(tài)引用,原始值變了,輸入的值也會跟著變,并且不會緩存值,模塊里面的變量綁定其所在的模塊。


 

多線程的同步機(jī)制對資源進(jìn)行加鎖,使得在同一個時間,只有一個線程可以進(jìn)行操作,同步用以解決多個線程同時訪問時可能出現(xiàn)的問題。

同步機(jī)制可以使用synchronized關(guān)鍵字實(shí)現(xiàn)。

當(dāng)synchronized關(guān)鍵字修飾一個方法的時候,該方法叫做同步方法。

當(dāng)synchronized方法執(zhí)行完或發(fā)生異常時,會自動釋放鎖。

下面通過一個例子來對synchronized關(guān)鍵字的用法進(jìn)行解析。

1,是否使用synchronized關(guān)鍵字的不同

public class ThreadTest{ public static void main(String[] args) { Example example = new Example(); Thread t1 = new Thread1(example); Thread t2 = new Thread1(example); t1.start(); t2.start(); }}class Example{ public synchronized void execute() { for (int i = 0; i < 10; i) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println('Hello: ' i); } }}class Thread1 extends Thread{ private Example example; public Thread1(Example example) { this.example = example; } @Override public void run() { example.execute(); }}

是否在execute()方法前加上synchronized關(guān)鍵字,這個例子程序的執(zhí)行結(jié)果會有很大的不同。

如果不加synchronized關(guān)鍵字,則兩個線程同時執(zhí)行execute()方法,輸出是兩組并發(fā)的。

如果加上synchronized關(guān)鍵字,則會先輸出一組0到9,然后再輸出下一組,說明兩個線程是順次執(zhí)行的。

2.多個方法的多線程情況


將程序改動一下,Example類中再加入一個方法execute2()。

之后再寫一個線程類Thread2,Thread2中的run()方法執(zhí)行的是execute2()。Example類中的兩個方法都是被synchronized關(guān)鍵字修飾的。

public class ThreadTest{ public static void main(String[] args) { Example example = new Example(); Thread t1 = new Thread1(example); Thread t2 = new Thread2(example); t1.start(); t2.start(); }}class Example{ public synchronized void execute() { for (int i = 0; i < 20; i) { try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println('Hello: ' i); } } public synchronized void execute2() { for (int i = 0; i < 20; i) { try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println('World: ' i); } }}class Thread1 extends Thread{ private Example example; public Thread1(Example example) { this.example = example; } @Override public void run() { example.execute(); }}class Thread2 extends Thread{ private Example example; public Thread2(Example example) { this.example = example; } @Override public void run() { example.execute2(); }}

如果去掉synchronized關(guān)鍵字,則兩個方法并發(fā)執(zhí)行,并沒有相互影響。

但是如例子程序中所寫,即便是兩個方法:

執(zhí)行結(jié)果永遠(yuǎn)是執(zhí)行完一個線程的輸出再執(zhí)行另一個線程的。

說明:

如果一個對象有多個synchronized方法,某一時刻某個線程已經(jīng)進(jìn)入到了某個synchronized方法,那么在該方法沒有執(zhí)行完畢前,其他線程是無法訪問該對象的任何synchronized方法的。

結(jié)論:

當(dāng)synchronized關(guān)鍵字修飾一個方法的時候,該方法叫做同步方法。

Java中的每個對象都有一個鎖(lock),或者叫做監(jiān)視器(monitor),當(dāng)一個線程訪問某個對象的synchronized方法時,將該對象上鎖,其他任何線程都無法再去訪問該對象的synchronized方法了(這里是指所有的同步方法,而不僅僅是同一個方法),直到之前的那個線程執(zhí)行方法完畢后(或者是拋出了異常),才將該對象的鎖釋放掉,其他線程才有可能再去訪問該對象的synchronized方法。

注意這時候是給對象上鎖,如果是不同的對象,則各個對象之間沒有限制關(guān)系。

嘗試在代碼中構(gòu)造第二個線程對象時傳入一個新的Example對象,則兩個線程的執(zhí)行之間沒有什么制約關(guān)系。

3.考慮靜態(tài)的同步方法


當(dāng)一個synchronized關(guān)鍵字修飾的方法同時又被static修飾,之前說過,非靜態(tài)的同步方法會將對象上鎖,但是靜態(tài)方法不屬于對象,而是屬于類,它會將這個方法所在的類的Class對象上鎖。

一個類不管生成多少個對象,它們所對應(yīng)的是同一個Class對象。

public class ThreadTest{ public static void main(String[] args) { Example example = new Example(); Thread t1 = new Thread1(example); // 此處即便傳入不同的對象,靜態(tài)方法同步仍然不允許多個線程同時執(zhí)行 example = new Example(); Thread t2 = new Thread2(example); t1.start(); t2.start(); }}class Example{ public synchronized static void execute() { for (int i = 0; i < 20; i) { try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println('Hello: ' i); } } public synchronized static void execute2() { for (int i = 0; i < 20; i) { try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println('World: ' i); } }}class Thread1 extends Thread{ private Example example; public Thread1(Example example) { this.example = example; } @Override public void run() { Example.execute(); }}class Thread2 extends Thread{ private Example example; public Thread2(Example example) { this.example = example; } @Override public void run() { Example.execute2(); }}

所以如果是靜態(tài)方法的情況(execute()和execute2()都加上static關(guān)鍵字),即便是向兩個線程傳入不同的Example對象,這兩個線程仍然是互相制約的,必須先執(zhí)行完一個,再執(zhí)行下一個。

結(jié)論:

如果某個synchronized方法是static的,那么當(dāng)線程訪問該方法時,它鎖的并不是synchronized方法所在的對象,而是synchronized方法所在的類所對應(yīng)的Class對象。Java中,無論一個類有多少個對象,這些對象會對應(yīng)唯一一個Class對象,因此當(dāng)線程分別訪問同一個類的兩個對象的兩個static,synchronized方法時,它們的執(zhí)行順序也是順序的,也就是說一個線程先去執(zhí)行方法,執(zhí)行完畢后另一個線程才開始。

4. synchronized塊


synchronized塊寫法:

synchronized(object)

{

}

表示線程在執(zhí)行的時候會將object對象上鎖。(注意這個對象可以是任意類的對象,也可以使用this關(guān)鍵字)。

這樣就可以自行規(guī)定上鎖對象。

public class ThreadTest{ public static void main(String[] args) { Example example = new Example(); Thread t1 = new Thread1(example); Thread t2 = new Thread2(example); t1.start(); t2.start(); }}class Example{ private Object object = new Object(); public void execute() { synchronized (object) { for (int i = 0; i < 20; i) { try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println('Hello: ' i); } } } public void execute2() { synchronized (object) { for (int i = 0; i < 20; i) { try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println('World: ' i); } } }}class Thread1 extends Thread{ private Example example; public Thread1(Example example) { this.example = example; } @Override public void run() { example.execute(); }}class Thread2 extends Thread{ private Example example; public Thread2(Example example) { this.example = example; } @Override public void run() { example.execute2(); }}

例子程序4所達(dá)到的效果和例子程序2的效果一樣,都是使得兩個線程的執(zhí)行順序進(jìn)行,而不是并發(fā)進(jìn)行,當(dāng)一個線程執(zhí)行時,將object對象鎖住,另一個線程就不能執(zhí)行對應(yīng)的塊。

synchronized方法實(shí)際上等同于用一個synchronized塊包住方法中的所有語句,然后在synchronized塊的括號中傳入this關(guān)鍵字。當(dāng)然,如果是靜態(tài)方法,需要鎖定的則是class對象。

可能一個方法中只有幾行代碼會涉及到線程同步問題,所以synchronized塊比synchronized方法更加細(xì)粒度地控制了多個線程的訪問,只有synchronized塊中的內(nèi)容不能同時被多個線程所訪問,方法中的其他語句仍然可以同時被多個線程所訪問(包括synchronized塊之前的和之后的)。

注意:被synchronized保護(hù)的數(shù)據(jù)應(yīng)該是私有的。

結(jié)論:

synchronized方法是一種粗粒度的并發(fā)控制,某一時刻,只能有一個線程執(zhí)行該synchronized方法;

synchronized塊則是一種細(xì)粒度的并發(fā)控制,只會將塊中的代碼同步,位于方法內(nèi)、synchronized塊之外的其他代碼是可以被多個線程同時訪問到的。

JDK 5.0的并發(fā)包


使用synchronized關(guān)鍵字解決線程的同步問題會帶來一些執(zhí)行效率上的問題。

JDK1.4及之前是無法避免這些問題的。




【免責(zé)聲明】本賬號旨在介紹更多的最新信息,部分信息轉(zhuǎn)載自各類紙媒、網(wǎng)媒之所有作品,版權(quán)歸作者本人所有,轉(zhuǎn)載文章目的在于分享信息、提供閱讀,不代表本平臺贊同其觀點(diǎn)和對其真實(shí)性負(fù)責(zé)!若作者或版權(quán)人不愿被使用,請即與我方公眾號tedu_java聯(lián)系,如有侵權(quán)本賬號將迅速給您回應(yīng)并做處理


如果你身邊的小伙伴還在為找工作發(fā)愁,或者想轉(zhuǎn)行,趕快將他(她)的姓名和聯(lián)系方式發(fā)給茜茜老師(17600265125),茜茜老師會與他(她)致電并給他(她)最合適的學(xué)習(xí)建議并免費(fèi)邀請他試聽5天的課程,座位有限,抓緊時間哦~~








目錄



1.靜態(tài)作用域與動態(tài)作用域

2.變量的作用域

3.JavaScript 中變量的作用域

4.JavaScript 欺騙作用域

5.JavaScript 執(zhí)行上下文

6.JavaScript 中的作用域鏈

7.JavaScript 中的閉包

8.JavaScript 中的模塊



靜態(tài)作用域與動態(tài)作用域



在電腦程序設(shè)計(jì)中,作用域(scope,或譯作有效范圍)是名字(name)與實(shí)體(entity)的綁定(binding)保持有效的那部分計(jì)算機(jī)程序。不同的編程語言可能有不同的作用域和名字解析。而同一語言內(nèi)也可能存在多種作用域,隨實(shí)體的類型變化而不同。作用域類別影響變量的綁定方式,根據(jù)語言使用靜態(tài)作用域還是動態(tài)作用域變量的取值可能會有不同的結(jié)果。


  • 包含標(biāo)識符的宣告或定義;

  • 包含語句和/或表達(dá)式,定義或部分關(guān)于可運(yùn)行的算法;

  • 嵌套嵌套或被嵌套嵌套。


原文中此處為鏈接,暫不支持采集

原文中此處為鏈接,暫不支持采集


原文中此處為鏈接,暫不支持采集

原文中此處為鏈接,暫不支持采集


作用域又分為兩種,靜態(tài)作用域和動態(tài)作用域。


靜態(tài)作用域又叫做詞法作用域,采用詞法作用域的變量叫詞法變量。詞法變量有一個在編譯時靜態(tài)確定的作用域。詞法變量的作用域可以是一個函數(shù)或一段代碼,該變量在這段代碼區(qū)域內(nèi)可見(visibility);在這段區(qū)域以外該變量不可見(或無法訪問)。詞法作用域里,取變量的值時,會檢查函數(shù)定義時的文本環(huán)境,捕捉函數(shù)定義時對該變量的綁定。


function f() {

    function g() {

  }

}


靜態(tài)(詞法)作用域,就是可以無須執(zhí)行程序而只從程序源碼的角度,就可以看出程序是如何工作的。從上面的例子中可以肯定,函數(shù) g 是被函數(shù) f 包圍在內(nèi)部。


大多數(shù)現(xiàn)在程序設(shè)計(jì)語言都是采用靜態(tài)作用域規(guī)則,如C/C 、C#、Python、Java、JavaScript……


相反,采用動態(tài)作用域的變量叫做動態(tài)變量。只要程序正在執(zhí)行定義了動態(tài)變量的代碼段,那么在這段時間內(nèi),該變量一直存在;代碼段執(zhí)行結(jié)束,該變量便消失。這意味著如果有個函數(shù)f,里面調(diào)用了函數(shù)g,那么在執(zhí)行g(shù)的時候,f里的所有局部變量都會被g訪問到。而在靜態(tài)作用域的情況下,g不能訪問f的變量。動態(tài)作用域里,取變量的值時,會由內(nèi)向外逐層檢查函數(shù)的調(diào)用鏈,并打印第一次遇到的那個綁定的值。顯然,最外層的綁定即是全局狀態(tài)下的那個值。


function g() {

}


function f() {

   g();

}


當(dāng)我們調(diào)用f(),它會調(diào)用g()。在執(zhí)行期間,g被f調(diào)用代表了一種動態(tài)的關(guān)系。


采用動態(tài)作用域的語言有Pascal、Emacs Lisp、Common Lisp(兼有靜態(tài)作用域)、Perl(兼有靜態(tài)作用域)。C/C 是靜態(tài)作用域語言,但在宏中用到的名字,也是動態(tài)作用域。


變量的作用域



1. 變量的作用域


變量的作用域是指變量在何處可以被訪問到。比如:


function foo(){

    var bar;

}


這里的 bar 的直接作用域是函數(shù)作用域foo();


2. 詞法作用域


JavaScript 中的變量都是有靜態(tài)(詞法)作用域的,因此一個程序的靜態(tài)結(jié)構(gòu)就決定了一個變量的作用域,這個作用域不會被函數(shù)的位置改變而改變。


3. 嵌套作用域


如果一個變量的直接作用域中嵌套了多個作用域,那么這個變量在所有的這些作用域中都可以被訪問:


function foo (arg) {

    function bar() {

        console.log( 'arg:' arg );

    }

    bar();

}


console.log(foo('hello'));   // arg:hello


arg的直接作用域是foo(),但是它同樣可以在嵌套的作用域bar()中被訪問,foo()是外部的作用域,bar()是內(nèi)部作用域。


4. 覆蓋的作用域


如果在一個作用域中聲明了一個與外層作用域同名的變量,那么這個內(nèi)部作用域以及內(nèi)部的所有作用域中將會訪問不到外面的變量。并且內(nèi)部的變量的變化也不會影響到外面的變量,當(dāng)變量離開內(nèi)部的作用域以后,外部變量又可以被訪問了。


var x = 'global';


function f() {

   var x = 'local';

   console.log(x);   // local

}


f();

console.log(x);  // global


這就是覆蓋的作用域。



JavaScript 中變量的作用域



大多數(shù)的主流語言都是有塊級作用域的,變量在最近的代碼塊中,Objective-C 和 Swift 都是塊級作用域的。但是在 JavaScript 中的變量是函數(shù)級作用域的。不過在最新的 ES6 中加入了 let 和 const 關(guān)鍵字以后,就變相支持了塊級作用域。到了 ES6 以后支持塊級作用域的有以下幾個:


with 語句


用 with 從對象中創(chuàng)建出的作用域僅在 with 聲明中而非外 部作用域中有效。


try/catch 語句


JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會創(chuàng)建一個塊作用域,其中聲明的變量僅在 catch 內(nèi)部有效。


let 關(guān)鍵字


let關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是{ .. }內(nèi)部)。換句話說,let 為其聲明的變量隱式地了所在的塊作用域。


const 關(guān)鍵字


除了 let 以外,ES6 還引入了 const,同樣可以用來創(chuàng)建塊作用域變量,但其值是固定的 (常量)。之后任何試圖修改值的操作都會引起錯誤。


這里就需要注意變量和函數(shù)提升的問題了,這個問題在前一篇文章里面詳細(xì)的說過了,這里不再贅述了。


不過這里還有一個坑,如果賦值給了一個未定義的變量,會產(chǎn)生一個全局變量。


在非嚴(yán)格模式下,不通過 var 關(guān)鍵字直接給一個變量賦值,會產(chǎn)生一個全局的變量


function func() { x = 123; }

func();

x

<123


不過在嚴(yán)格模式下,這里會直接報(bào)錯。


function func() { 'use strict'; x = 123; }

func();

<ReferenceError: x is not defined


在 ES5 中,經(jīng)常會通過引入一個新的作用域來限制變量的生命周期,通過 IIFE(Immediately-invoked function expression,立即執(zhí)行的函數(shù)表達(dá)式)來引入新的作用域。


通過 IIFE ,我們可以


  • 避免全局變量,隱藏全局作用域的變量。

  • 創(chuàng)建新的環(huán)境,避免共享。

  • 保持全局的數(shù)據(jù)對于構(gòu)造器的數(shù)據(jù)相對獨(dú)立。

  • 將全局的數(shù)據(jù)附加到單例對象上。

  • 將全局?jǐn)?shù)據(jù)附加到方法中。


JavaScript 欺騙作用域



(1). with 語句


with 語句被很多人都認(rèn)為是 JavaScript 里面的糟粕( Bad Parts )。起初它被設(shè)計(jì)出來的目的是好的,但是它導(dǎo)致的問題多于它解決的問題。


with 起初設(shè)計(jì)出來是為了避免冗余的對象調(diào)用。


舉個例子:


foo.a.b.c = 888;

foo.a.b.d = 'halfrost';


這時候用 with 語句就可以縮短調(diào)用:


with (foo.a.b) {

      c = 888;

      d = 'halfrost';

}


但是這種特性卻帶來了很多問題:


function myLog( errorMsg , parameters) {

  with (parameters) {

    console.log('errorMsg:' errorMsg);

  }

}


myLog('error',{});

<errorMsg:error


myLog('error',{ errorMsg:'stackoverflow' }); 

<errorMsg:stackoverflow


可以看到輸出就出現(xiàn)問題了,由于 with 語句,覆蓋掉了第一個入?yún)?。通過閱讀代碼,有時候是不能分辨出這些問題,它也會隨著程序的運(yùn)行,導(dǎo)致發(fā)生不多的變化,這種對未來的不確定性就很容易出現(xiàn)

bug。


with 會導(dǎo)致3個問題:


  • 性能問題

變量查找會變慢,因?yàn)閷ο笫桥R時性的插入到作用域鏈中的。


  • 代碼不確定性

@Brendan Eich 解釋,廢棄 with 的根本原因不是因?yàn)樾阅軉栴},原因是因?yàn)椤皐ith 可能會違背當(dāng)前的代碼上下文,使得程序的解析(例如安全性)變得困難而繁瑣”。


  • 代碼壓縮工具不會壓縮 with 語句中的變量名


所以在嚴(yán)格模式下,已經(jīng)嚴(yán)格禁止使用 with 語句。


Uncaught SyntaxError: Strict mode code may not include a with statement


如果還是想避免使用 with 語句,有兩種方法:


  1. 用一個臨時變量替代傳進(jìn) with 語句的對象。

  2. 如果不想引入臨時變量,可以使用 IIFE 。


(function () {

  var a = foo.a.b;

  console.log('Hello' a.c a.d);

}());


或者


(function (bar) {

  console.log('Hello' bar.c bar.d);

}(foo.a.b));


(2). eval 函數(shù)


eval 函數(shù)傳遞一個字符串給 JavaScript 編譯器,并且執(zhí)行其結(jié)果。


eval(str)


它是 JavaScript 中被濫用的最多的特性之一。


var a = 12;

eval('a 5')

<17


eval 函數(shù)以及它的親戚( Function 、setTimeout、setInterval)都提供了訪問 JavaScript 編譯器的機(jī)會。


Function() 構(gòu)造函數(shù)的形式比 eval() 函數(shù)好一點(diǎn)的地方在于,它令入?yún)⒏忧逦?/span>


new Function( param1, ...... , paramN, funcBody )



var f = new Function( 'x', 'y' , 'return x y' );

f(3,4)

<7


用 Function() 的方式至少不用使用間接的 eval() 調(diào)用來確保所執(zhí)行的代碼除了其自己的作用域只能訪問全局的變量。


在 Weex 的代碼中,就還存在著 eval() 的代碼,不過 Weex 團(tuán)隊(duì)在注釋里面承諾會改掉??偟膩碚f,最好應(yīng)該避免使用 eval() 和 new Function() 這些動態(tài)執(zhí)行代碼的方法。動態(tài)執(zhí)行代碼相對會比較慢,并且還存在安全隱患。


再說說另外兩個親戚,setTimeout、setInterval 函數(shù),它們也能接受字符串參數(shù)或者函數(shù)參數(shù)。當(dāng)傳遞的是字符串參數(shù)時,setTimeout、setInterval 會像 eval 那樣去處理。同樣也需要避免使用這兩個函數(shù)的時候使用字符串傳參數(shù)。


eval 函數(shù)帶來的問題總結(jié)如下:


  1. 函數(shù)變成了字符串,可讀性差,存在安全隱患。

  2. 函數(shù)需要運(yùn)行編譯器,即使只是為了執(zhí)行一個微不足道的賦值語句。這使得執(zhí)行速度變慢。

  3. 讓 JSLint 失效,讓它檢測問題的能力大打折扣。



JavaScript 執(zhí)行上下文




這個事情要從 JavaScript 源代碼如何被運(yùn)行開始說起。


我們都知道 JavaScript 是腳本語言,它只有 runtime,沒有編譯型語言的 buildTime,那它是如何被各大瀏覽器運(yùn)行起來的呢?


JavaScript 代碼是被各個瀏覽器引擎編譯和運(yùn)行起來的。JavaScript 引擎的代碼解析和執(zhí)行過程的目標(biāo)就是在最短時間內(nèi)編譯出最優(yōu)化的代碼。JavaScript 引擎還需要負(fù)責(zé)管理內(nèi)存,負(fù)責(zé)垃圾回收,與宿主語言的交互等。流行的引擎有以下幾種:


蘋果公司的 JavaScriptCore (JSC) 引擎,Mozilla 公司的 SpiderMonkey,微軟 Internet Explorer 的 Chakra (JScript引擎),Microsoft Edge 的 Chakra (JavaScript引擎) ,谷歌 Chrome 的 V8。



其中 V8 引擎是最著名的開源的引擎,它和前面那幾個引擎有一個最大的區(qū)別是:主流引擎都是基于字節(jié)碼的實(shí)現(xiàn),V8 的做法非常極致,直接跳過了字節(jié)碼這一層,直接把 JS 編譯成機(jī)器碼。所以 V8 是沒有解釋器的。(但是這都是歷史,V8 現(xiàn)在最新版是有解釋器的)



在2017年5月1號之后, Chrome 的 V8 引擎的v8 5.9 發(fā)布了,其中的 Ignition 字節(jié)碼解釋器將默認(rèn)啟動 :V8 Release 5.9 。v8 自此回到了字節(jié)碼的懷抱。


V8 在有了字節(jié)碼以后,消除 Cranshaft 這個舊的編譯器,并讓新的 Turbofan 直接從字節(jié)碼來優(yōu)化代碼,并當(dāng)需要進(jìn)行反優(yōu)化的時候直接反優(yōu)化到字節(jié)碼,而不需要再考慮 JS 源代碼。去掉 Cranshaft 以后,就成了 Turbofan Ignition 的組合了。



Ignition TurboFan 的組合,就是字節(jié)碼解釋器 JIT 編譯器的黃金組合。這一黃金組合在很多 JS 引擎中都有所使用,例如微軟的 Chakra,它首先解釋執(zhí)行字節(jié)碼,然后觀察執(zhí)行情況,如果發(fā)現(xiàn)熱點(diǎn)代碼,那么后臺的 JIT 就把字節(jié)碼編譯成高效代碼,之后便只執(zhí)行高效代碼而不再解釋執(zhí)行字節(jié)碼。蘋果公司的 SquirrelFish Extreme 也引入了 JIT。SpiderMonkey 更是如此,所有 JS 代碼最初都是被解釋器解釋執(zhí)行的,解釋器同時收集執(zhí)行信息,當(dāng)它發(fā)現(xiàn)代碼變熱了之后,JaegerMonkey、IonMonkey 等 JIT 便登場,來編譯生成高效的機(jī)器碼。


總結(jié)一下:


JavaScript 代碼會先被引擎編譯,轉(zhuǎn)化成能被解釋器識別的字節(jié)碼。



源碼會被詞法分析,語法分析,生成 AST 抽象語法樹。



AST 抽象語法樹又會被字節(jié)碼生成器進(jìn)行多次優(yōu)化,最終生成了中間態(tài)的字節(jié)碼。這時的字節(jié)碼就可以被解釋器執(zhí)行了。


這樣,JavaScript 代碼就可以被引擎跑起來了。


JavaScript 在運(yùn)行過程中涉及到的作用域有3種:


  1. 全局作用域(Global Scope)JavaScript 代碼開始運(yùn)行的默認(rèn)環(huán)境

  2. 局部作用域(Local Scpoe)代碼進(jìn)入一個 JavaScript 函數(shù)

  3. Eval 作用域 使用 eval() 執(zhí)行代碼


當(dāng) JavaScript 代碼執(zhí)行的時候,引擎會創(chuàng)建不同的執(zhí)行上下文,這些執(zhí)行上下文就構(gòu)成了一個執(zhí)行上下文棧(Execution context stack,ECS)。


全局執(zhí)行上下文永遠(yuǎn)都在棧底,當(dāng)前正在執(zhí)行的函數(shù)在棧頂。



當(dāng) JavaScript 引擎遇到一個函數(shù)執(zhí)行的時候,就會創(chuàng)建一個執(zhí)行上下文,并且壓入執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行完畢的時候,就會將函數(shù)的執(zhí)行上下文從棧中彈出。


對于每個執(zhí)行上下文都有三個重要的屬性,變量對象(Variable object,VO),作用域鏈(Scope chain)和this。這三個屬性跟代碼運(yùn)行的行為有很重要的關(guān)系。


變量對象 VO 是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域。它是一個與上下文相關(guān)的特殊對象,其中存儲了在上下文中定義的變量和函數(shù)聲明。也就是說,一般 VO 中會包含以下信息:


  1. 創(chuàng)建 arguments object

  2. 查找函數(shù)聲明(Function declaration)

  3. 查找變量聲明(Variable declaration)



上圖也解釋了,為何函數(shù)提升優(yōu)先級會在變量提升前面。


這里還會牽扯到活動對象(Activation object):


只有全局上下文的變量對象允許通過 VO 的屬性名稱間接訪問。在函數(shù)執(zhí)行上下文中,VO 是不能直接訪問的,此時由活動對象(Activation Object, 縮寫為AO)扮演 VO 的角色?;顒訉ο笫窃谶M(jìn)入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的 arguments 屬性初始化。



Arguments Objects 是函數(shù)上下文里的激活對象 AO 中的內(nèi)部對象,它包括下列屬性:


  1. callee:指向當(dāng)前函數(shù)的引用

  2. length: 真正傳遞的參數(shù)的個數(shù)

  3. properties-indexes:就是函數(shù)的參數(shù)值(按參數(shù)列表從左到右排列)


JavaScript 解釋器創(chuàng)建執(zhí)行上下文的時候,會經(jīng)歷兩個階段:


  • 創(chuàng)建階段(當(dāng)函數(shù)被調(diào)用,但是開始執(zhí)行函數(shù)內(nèi)部代碼之前)

創(chuàng)建 Scope chain,創(chuàng)建 VO/AO(variables, functions and arguments),設(shè)置 this 的值。


  • 激活 / 代碼執(zhí)行階段

設(shè)置變量的值,函數(shù)的引用,然后解釋/執(zhí)行代碼。


VO 和 AO 的區(qū)別就在執(zhí)行上下文的這兩個生命周期里面。



VO 和 AO 的關(guān)系可以理解為,VO 在不同的 Execution Context 中會有不同的表現(xiàn):當(dāng)在 Global Execution Context 中,直接使用的 VO;但是,在函數(shù) Execution Context 中,AO 就會被創(chuàng)建。



JavaScript 中的作用域鏈



在 JavaScript 中有兩種變量傳遞的方式


1. 通過調(diào)用函數(shù),執(zhí)行上下文的棧傳遞變量。


函數(shù)每調(diào)用一次,就需要給它的參數(shù)和變量準(zhǔn)備新的存儲空間,就會創(chuàng)建一個新的環(huán)境將(變量和參數(shù)的)標(biāo)識符合變量做映射。對于遞歸的情況,執(zhí)行上下文,即通過環(huán)境的引用是在棧中進(jìn)行管理的。這里的棧對應(yīng)了調(diào)用棧。


JavaScript 引擎會以堆棧的方式來處理它們,這個堆棧,我們稱其為函數(shù)調(diào)用棧(call stack)。棧底永遠(yuǎn)都是全局上下文,而棧頂就是當(dāng)前正在執(zhí)行的上下文。


這里舉個例子:比如用遞歸的方式計(jì)算n的階乘。


2. 作用域鏈


在 JavaScript 中有一個內(nèi)部屬性 [[ Scope ]] 來記錄函數(shù)的作用域。在函數(shù)調(diào)用的時候,JavaScript 會為這個函數(shù)所在的新作用域創(chuàng)建一個環(huán)境,這個環(huán)境有一個外層域,它通過 [[ Scope ]] 創(chuàng)建并指向了外部作用域的環(huán)境。因此在 JavaScript 中存在一個作用域鏈,它以當(dāng)前作用域?yàn)槠瘘c(diǎn),連接了外部的作用域,每個作用域鏈最終會在全局環(huán)境里終結(jié)。全局作用域的外部作用域指向了null。


作用域鏈,是由當(dāng)前環(huán)境與上層環(huán)境的一系列變量對象組成,它保證了當(dāng)前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問。


作用域是一套規(guī)則,是在 JavaScript 引擎編譯的時候確定的。

作用域鏈?zhǔn)窃趫?zhí)行上下文的創(chuàng)建階段創(chuàng)建的,這是在 JavaScript 引擎解釋執(zhí)行階段確定的。


function myFunc( myParam ) {

    var myVar = 123;

    return myFloat;

}

var myFloat = 2.0;  // 1

myFunc('ab');       // 2


當(dāng)程序運(yùn)行到標(biāo)志 1 的時候:



函數(shù) myFunc 通過 [[ Scope]] 連接著它的作用域,全局作用域。


當(dāng)程序運(yùn)行到標(biāo)志 2 的時候,JavaScript 會創(chuàng)建一個新的作用域用來管理參數(shù)和本地變量。



由于外層作用域鏈,使得 myFunC 可以訪問到外層的 myFloat 。


這就是 Javascript 語言特有的'作用域鏈'結(jié)構(gòu)(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。


作用域鏈?zhǔn)潜WC對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問。作用域鏈的前端始終是當(dāng)前執(zhí)行的代碼所在環(huán)境的變量對象。而前面我們已經(jīng)講了變量對象的創(chuàng)建過程。作用域鏈的下一個變量對象來自包含環(huán)境即外部環(huán)境,這樣,一直延續(xù)到全局執(zhí)行環(huán)境;全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。



JavaScript 中的閉包



當(dāng)函數(shù)可以記住并訪問所在的詞法作用域,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行,這時就產(chǎn)生了閉包。


接下來看看大家對閉包的定義是什么樣的:


MDN 對閉包的定義:


閉包是指那些能夠訪問獨(dú)立(自由)變量的函數(shù)(變量在本地使用,但定義在一個封閉的作用域中)。換句話說,這些函數(shù)可以「記憶」它被創(chuàng)建時候的環(huán)境。


《JavaScript 權(quán)威指南(第6版)》對閉包的定義:


函數(shù)對象可以通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)體內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi),這種特性在計(jì)算機(jī)科學(xué)文獻(xiàn)中稱為閉包。


《JavaScript 高級程序設(shè)計(jì)(第3版)》對閉包的定義:


閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。


最后是阮一峰老師對閉包的解釋:


由于在 Javascript 語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,因此可以把閉包簡單理解成定義在一個函數(shù)內(nèi)部的函數(shù)。它的最大用處有兩個,一個是前面提到的可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量的值始終保持在內(nèi)存中。


再來對比看看 OC,Swift,JS,Python 4種語言的閉包寫法有何不同:


void test() {

    int value = 10;

    void(^block)() = ^{ NSLog(@'%d', value); };

    value ;

    block();

}


// 輸出10


func test() {

    var value = 10

    let closure = { print(value) }

    value = 1

    closure()

}

// 輸出11


function test() {

    var value = 10;

    var closure = function () {

        console.log(value);

    }

    value ;

    closure();

}

// 輸出11


def test():

    value = 10

    def closure():

        print(value)

    value = value 1

    closure()

// 輸出11


可以看出 OC 的寫法默認(rèn)是和其他三種語言不同的。關(guān)于 OC 的閉包原理,iOS 開發(fā)的同學(xué)應(yīng)該都很清楚了,這里不再贅述。當(dāng)然,想要第一種 OC 的寫法輸出11,也很好改,只要把外部需要捕獲進(jìn)去的變量前面加上 __block 關(guān)鍵字就可以了。


最后結(jié)合作用域鏈和閉包舉一個例子:


function createInc(startValue) {

  return function (step) {

    startValue = step;

    return startValue;

  }

}


var inc = createInc(5);

inc(3);


當(dāng)代碼進(jìn)入到 Global Execution Context 之后,會創(chuàng)建 Global Variable Object。全局執(zhí)行上下文壓入執(zhí)行上下文棧。



Global Variable Object 初始化會創(chuàng)建 createInc ,并指向一個函數(shù)對象,初始化 inc ,此時還是 undefined。


接著代碼執(zhí)行到 createInc(5),會創(chuàng)建 Function Execution Context,并壓入執(zhí)行上下文棧。會創(chuàng)建 createInc Activation Object。



由于還沒有執(zhí)行這個函數(shù),所以 startValue 的值還是 undefined。接下來就要執(zhí)行 createInc 函數(shù)了。



當(dāng) createInc 函數(shù)執(zhí)行的最后,并退出的時候,Global VO中的 inc 就會被設(shè)置;這里需要注意的是,雖然 create Execution Context 退出了執(zhí)行上下文棧,但是因?yàn)?inc 中的成員仍然引用 createInc AO(因?yàn)?createInc AO 是 function(step) 函數(shù)的 parent scope ),所以 createInc AO 依然在 Scope 中。


接著再開始執(zhí)行 inc(3)。



當(dāng)執(zhí)行 inc(3) 代碼的時候,代碼將進(jìn)入 inc Execution Context,并為該執(zhí)行上下文創(chuàng)建 VO/AO,scope chain 和設(shè)置 this;這時,inc AO將指向 createInc AO。



最后,inc Execution Context 退出了執(zhí)行上下文棧,但是 createInc AO 沒有銷毀,可以繼續(xù)訪問。


JavaScript 中的模塊



由作用域又可以引申出模塊的概念。


在 ES6 中會大量用到模塊,通過模塊系統(tǒng)進(jìn)行加載時,ES6 會將文件當(dāng)作獨(dú)立的模塊來處理。每個模塊都可以導(dǎo)入其他模塊或特定的 API 成員,同樣也可以導(dǎo)出自己的 API 成員。


模塊有兩個主要特征:


  1. 為創(chuàng)建內(nèi)部作用域而調(diào)用了一個包裝函數(shù);

  2. 包裝函數(shù)的返回值必須至少包括一個對內(nèi)部函數(shù)的引用,這樣就會創(chuàng)建涵蓋整個包裝函數(shù)內(nèi)部作用域的閉包。


JavaScript 最主要的有 CommonJS 和 AMD 兩種,前者用于服務(wù)器,后者用于瀏覽器。在 ES6 中的 Module 使得編譯時就能確定模塊的依賴關(guān)系,以及輸入輸出的變量。CommonJS 和 AMD 模塊都只能運(yùn)行時確定這些東西。


CommonJS 模塊就是對象,輸入時必須查找對象屬性。屬于運(yùn)行時加載。CommonJS 輸入的是被輸出值的拷貝,并不是引用。


ES6 的 Module 在編譯時就完成模塊編譯,屬于編譯時加載,效率要比 CommonJS 模塊的加載方式高。ES6 模塊的運(yùn)行機(jī)制與 CommonJS 不一樣,它遇到模塊加載命令 import 時不會去執(zhí)行模塊,只會生成一個動態(tài)的只讀引用。等到真正需要的時候,再去模塊中取值。ES6 模塊加載的變量是動態(tài)引用,原始值變了,輸入的值也會跟著變,并且不會緩存值,模塊里面的變量綁定其所在的模塊。

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
深入理解JavaScript作用域和作用域鏈
javascript語言精粹 筆記
前端基礎(chǔ)進(jìn)階(四):詳細(xì)圖解作用域鏈與閉包
談?wù)刯avascript語法里一些難點(diǎn)問題(二)
理解 JavaScript 作用域
尚春 ? Javascript核心概念
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服