我眼中的js編程(2)--詳解作用域內(nèi)變量和函數(shù)的聲明與訪問

我眼中的js編程(1)主要介紹了js是用來做什么的,這一篇開始及以后總結(jié)js具體該怎么用。本篇總結(jié)了作用域內(nèi)變量和函數(shù)的聲明與訪問。先看一段有意思的代碼。

var a = a;
console.log(a) // undefined

let b = b;
console.log(b) // ReferenceError

let c;
c = c;
console.log(c) // undefined

很有意思的結(jié)果。為什么是這樣呢?這個問題放一邊,我先扯點沒用的,等讀到文章最后,如果你理解了,就一定會知道為什么。

(ps:先扯會兒淡)所有的編程語言都有相通之處,也有其各自擅長的地方。js和其他語言一樣,數(shù)組、函數(shù)、對象等數(shù)據(jù)類型以及各個數(shù)據(jù)類型處理數(shù)據(jù)的api、運算符、變量的作用域、對象的創(chuàng)建和繼承,這些概念,雖然各種語言的語法規(guī)則不一樣,但是本質(zhì)上是一樣的。一通百通,沒有太多新鮮之處。

js的獨特之處在于能夠處理網(wǎng)頁的交互效果。這事兒只有js能干,因為瀏覽器只有js引擎,沒有php引擎、java引擎,為什么是這樣?這和js與瀏覽器的歷史有關(guān),阮一峰老師有一篇文章Javascript誕生記很好的做了詮釋。

作用域

作用域有什么用?某個功能的代碼,把其中沒有必要暴露的函數(shù)和變量封裝起來,實現(xiàn)最小暴露。

(ps:繼續(xù)扯淡)軟件設(shè)計中有最小暴露原則,就是用來實現(xiàn)某功能的代碼,應該最大限度的暴露最少的東西。不止是軟件,生活中的事物也遵循著這個原則,比如數(shù)據(jù)線,只是暴露了一個和手機的接口,其余的線路都在包裝線內(nèi)部隱藏,比如智能手機,暴露在外面的就是個外殼和屏幕,復雜的線路和芯片被隱藏在手機內(nèi)部,而拆解開手機內(nèi)部的具體零件,每個零件同樣也是遵循著最小暴露原則,甚至社會組織結(jié)構(gòu)比如飯店,大廳的餐桌、服務(wù)員對外暴露,而后廚的炊具、廚師隱藏在內(nèi)部,而且仔細觀察生活中的各類事物,無不遵循這這樣的原則,而且是遞歸最小暴露,即拆解開來的零件同樣也遵守,零件的零件仍然遵守。腦洞大開扯遠了。。。下面說一下作用域怎么使用?聊聊作用域的相關(guān)規(guī)則(先聊有啥用,再聊怎么用,要知其然和所以然)。

變量的作用域在寫代碼的時候就確定了。es6之前js只有全局作用域和函數(shù)作用域(try-catch語句的作用域、eval()方法的作用域等暫不考慮),在es6中有了塊作用域、新的全局let作用域、for循環(huán)作用域、模塊作用域(參考自深入淺出es6)。js中變量的作用域是整個前后封閉的函數(shù)代碼塊,而不是開始于變量聲明之處(有些編程語言的作用域是這樣的)。嵌套作用域是編程語言的核心理念之一,js中常見的作用域()有:

  • 函數(shù)作用域
    var聲明的變量所在的函數(shù)的整個代碼塊。
  • 塊作用域
    let(const)聲明的變量(常量)所在的外層塊{ }
if(true){
  let a = 5
}
console.log(a) // ReferenceError
  • 新的全局let作用域
    let聲明的全局變量不是全局對象的屬性,let聲明的全局變量存在于一個不可見的塊作用域中,理論上是頁面中包含所有js代碼的不可見的外層塊。
let a = 1
console.log(window.a) // undefined
  • for作用域
    for循環(huán)中()中變量是let聲明的時候,比如for(let i = 0;i<5;i++){...},每次迭代都為i綁定新的塊作用域,這個塊就是for(){ }的{ }
for(let i = 0;i<5;i++){
  setTimeout(function(){
    console.log(i) // 每個1s輸出一次,分別輸出0 1 2 3 4
  },1000 * i)
}
  • 模塊作用域

常量和變量

js中的變量和常量是需要用關(guān)鍵字聲明的(php不用聲明)
關(guān)鍵字var聲明變量:var age = 18
關(guān)鍵字let聲明變量:let name = 'yanhaoqi'
關(guān)鍵字const聲明常量:const STUDENT_NUM = 30
常量和變量有全局局部之分,下面以變量為例說明。

  • 全局變量
    全局變量定義在全局對象中,可在任何作用域訪問到。
    ECMAScript本身具有全局對象,但全局對象不是任何其他對象的屬性,所以它沒有名字。瀏覽器環(huán)境的全局對象是window,表示允許js代碼的瀏覽器窗口,瀏覽器窗口就是瀏覽器端js的最大操作范圍(權(quán)限)。node環(huán)境下的全局對象是globle。
  • 局部變量
    局部變量聲明在局部作用域,只在局部作用域內(nèi)可見。

下面主要講 作用域內(nèi)變量的訪問規(guī)則
先看兩段代碼:

console.log(name) // undefined
var name
console.log(name) // undefined
name = 'yanhaoqi'
console.log(name) // yanhaoqi
console.log(age) // ReferenceError
let age
console.log(age) // undefined
age = 18
console.log(age) // 18

為什么會有這樣的區(qū)別呢?你可能會說,let聲明的變量沒有變量提升,其實這樣說是不完全準確的,在這里我深入討論下一些細節(jié)。

js引擎在編譯和解釋代碼的時候,聲明的變量有三個階段:
聲明階段(Declaration Phase) :在當前作用域中注冊一個變量(作用域在編譯和解釋之前已經(jīng)確定)
初始化階段(Initialization Phase):在作用域中為變量綁定內(nèi)存,變量初始化為undefined。
賦值階段(Assignment Phase):為初始化的變量分配一個具體的值。

var聲明的變量

上面第一段代碼,js引擎編譯和解釋過程如下:

  • 第一步
    在執(zhí)行任何語句之前,先找到這段代碼中所有的聲明var name進行處理(引擎在編譯時候的任務(wù)之一)
    name變量在任何代碼執(zhí)行前先在作用域頂部通過了 聲明階段 ,在作用域注冊了變量name
    然后緊跟著來到 初始化階段 ,name初始化為undefined,兩個階段之間沒有任何間隙
    這個過程叫變量提升
  • 第二步
    開始執(zhí)行第一句代碼console.log(name),此時結(jié)果是undefined
    然后開始執(zhí)行第一句代碼console.log(name) 結(jié)果是undefined
  • 第三步
    執(zhí)行 var name,沒什么實際意義,因為一開始引擎就找到了var聲明進行了處理,繼續(xù)執(zhí)行后面console.log(name)結(jié)果仍然是undefined,這里就是為了對比下面let代碼的結(jié)果。
  • 第四部
    執(zhí)行后面的console.log(name)結(jié)果是yanhaoqi
屏幕快照 2017-09-13 下午4.39.20.png
let聲明的變量

上面第二段代碼,js引擎編譯和解釋過程如下:

  • 第一步
    在執(zhí)行任何語句之前,先找到這段代碼中所有的聲明let age進行處理,age變量在任何代碼執(zhí)行前先在作用域頂部通過了 聲明階段 ,在作用域中注冊了變量name。不會緊接著進行初始化階段。這算不算let聲明的變量的提升我查到的資料上說法不一,但本質(zhì)的過程就是這樣的。
  • 第二部
    開始執(zhí)行第一句代碼console.log(age),因為age還沒有經(jīng)歷初始化階段,沒有被分配內(nèi)存和初始化為undefined,所以會報錯ReferenceError。
  • 第三步
    執(zhí)行代碼let age。此時age變量才會進行初始化。接著執(zhí)行console.log(age)結(jié)果是undefined。
  • 第四部
    執(zhí)行代碼age = 18。此時完成 賦值階段

變量提升的問題,弄清楚變量的聲明和訪問的過程就ok了,至于有人說let聲明的變量沒有變量提升,有人說let聲明的變量是不完全提升,說法不同而已,管他呢,本質(zhì)就是這樣的。

屏幕快照 2017-09-13 下午5.54.49.png

var聲明的變量在一開始就完成了 聲明階段初始化階段,兩個階段是連在一起的,而let聲明的變量要執(zhí)行到let時候才會完成 初始化階段。let聲明的變量完成了聲明階段還沒有到達初始化階段的時候如果訪問該變量就會報錯ReferenceError,我們稱變量此時處在臨時死區(qū)(Temporal Dead Zone,簡稱TDZ)。

函數(shù)聲明的提升

既然上面詳細解釋了變量的聲明和訪問的過程,順便接著說一下函數(shù)聲明的提升。首先要搞清楚函數(shù)聲明和函數(shù)表達式的區(qū)別,如果關(guān)鍵字function是函數(shù)定義的第一個詞,那這就是一個函數(shù)聲明,否則就是一個函數(shù)表達式。

function foo(){
  console.log(123)
}
函數(shù)聲明
var foo = function(){
  console.log(123)
} 
函數(shù)表達式
(function foo(){
  console.log(123)
})
函數(shù)表達式

明確了什么是函數(shù)聲明后,下面我們討論下函數(shù)聲明的訪問。

[2,3,4,5].reduce(multiplier); // 120
function multiplier(a,b){
  return a * b;
}

在定義multiplier函數(shù)之前就把它作為參數(shù)傳入了reduce()函數(shù),為什么函數(shù)在定義之前就可以使用?我們看下js引擎執(zhí)行這段代碼的具體編譯和解釋的過程。

  • 第一步
    js引擎在執(zhí)行任何代碼之前先找到函數(shù)聲明,并在對應的作用域的頂部完成 聲明階段 、 初始化階段 、 賦值階段
  • 第二步
    開始執(zhí)行第一句代碼[2,3,4,5].reduce(multiplier);,函數(shù)multiplier作為reduce()的參數(shù)。
屏幕快照 2017-09-13 下午6.34.14.png

最后,關(guān)于js語言的設(shè)計中的變量提升和函數(shù)提升的規(guī)則,為什么有變量提升的設(shè)計,這要問js作者Brendan Eich,網(wǎng)上這方面信息較少,我在這里給一篇我覺得解釋的不錯的博客點擊查看。

總結(jié)var let聲明的變量和函數(shù)聲明的訪問的區(qū)別就是,var聲明的變量,聲明階段、初始化階段2個階段是耦合的,let聲明的變量,聲明階段和初始化階段是解耦的,而函數(shù)聲明的聲明階段、初始化階段、賦值階段三個階段都是耦合的。

點擊查看上一篇我眼中的js編程(1)
點擊查看下一篇我眼中的js編程(3)
我眼中的js編程系列是我個人的學習總結(jié),如有錯誤,煩請包涵、不吝賜教,O(∩_∩)O謝謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容