我眼中的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)比如飯店,大廳的餐桌售淡、服務員對外暴露斤葱,而后廚的炊具、廚師隱藏在內(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
進行處理(引擎在編譯時候的任務之一)
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
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ì)就是這樣的益缠。
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ù)揉抵。
最后,關(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謝謝