博客 / 詳情

返回

徹底弄懂JavaScript作用域問題

這幾次都是些的基礎文章,可能好多人會説基礎不太重要,做前端這麼久,也沒用到多少基礎 <font color="gray">(首先恭喜你,已經進提前進入了被優化名單)</font>。

下面我們來詳細解答一下基礎是什麼。

let 知識, 基礎 

if (知識 === '🏡') {
  基礎 = '地基'
}
if (知識 === '🌲') {
  基礎 = '樹根'
}
if (知識 === '天空') {
  基礎 = '階梯'
  console.log('基礎 makes you up, up, up…… 直到你碰頭') 
}
………………

怎麼樣?認識到基礎的重要性了吧,如果沒有了基礎,代碼就好像無根之木,空中樓閣,雖然賞心悦目,但是總是短暫的。

直到你真正領悟了底層是怎麼運作的,你才能夠真正做到 他強任他強,清風拂山崗;他橫自他橫,明月照大江。

扯遠了,收~

回到我們的正題。今天帶大家瞭解以下 JavaScript 中的作用域問題。請拭拭拭拭拭拭目以待。(自己 get 重點)。

先從最基礎的開始講起。

1.什麼是作用域

作用域是什麼這個問題,好多人都回答不好。請注意:通常來説,作用域就是限制一個變量在程序中的使用範圍。

搜嘎,突然有一種 “同行十二年,不知木蘭是女郎” 的趕腳。

1.1 全局和局部

瞭解了作用域的名字來由之後。我們來認識一下它。

JavaScript 中作用域的邊界是以函數劃分。有 全局局部 作用域之分。

  • 全局作用域:聲明在 <script></script> 標籤內的變量或者不使用var聲明的變量在整個程序中都是可用的,所以叫全局作用域
  • 局部作用域:聲明在函數體內的變量,在整個函數執行環境和其子函數內都是可用的,但是在函數外訪問不到,所以叫局部作用域

小栗子🌰同學上場:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

</body>
</html>
<script>
  var global = "我是全局變量,全局都能看到我";
    global = "我也是全局變量,全局都能看到我";
  function getName() {
    var name = "我是局部變量,只能在getName函數內才能找到我"
  }
</script>

1.2 預解析和變量提升

看到標題的同學是不是會稍有一愣。預解析是什麼玩意兒?變量提升又是啥?(知道答案的同學請配合這個無聊的作者一下,假裝一愣神。)

咳咳~不忙,等本大神(經)來解釋一下。

預解析是在程序執行之前,會進行一遍預檢。查找當前作用域內由 functionvar 。並且每次更換作用域都會在此作用域中執行預解析

變量提升是指,在查找到由 functionvar 後,首先在當前作用域的頂端定義好並賦給默認值。var的默認值為 undefinedfunction的默認值為函數本身

注:

​ 像 var getName = function() {} 這種代碼會被當做變量定義,而不會當做函數定義。

“讓一下,讓一下……” 遠處小栗子攜大量代碼滾滾而來~~

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

</body>
</html>
<script>
  console.log(global) // undefined
  console.log(getName) // function getName(){}

  var global = 1;
  function getName () {}
</script>

誒?怎麼這樣?我不是定義了global嗎?怎麼會輸出 undefined 難道是javascript出現了bug?

不要多想,預解析和變量提升的過程中,並不會將變量賦值,而只是定義,等真正執行的時候才會賦值。修改一下代碼,在global變量下方再次打印。

var global = 1;
console.log(global); // 1

解釋:當執行到打印函數的時候,global已經被賦值為 1。此時已經在執行代碼的階段,而不是在預解析階段。

先練練手:

猜想一下,下方程序如何輸出。

console.log(a) // 1
var a = 1;
console.log(a) // 2
getName()
function getName () {
  console.log(a) // 3
  console.log(b) // 4
  a = 2;
    console.log(a) // 5
  var b = 3;
    console.log(b) // 6
  function b(){}
}

此時你的答案是什麼呢?

針對上方代碼,通過圖解的方式看下預解析的執行過程。

image

ps:(上圖畫的太複雜,看完需要耐心)

1.3 var和function的優先級

細心地同學可能會發現一個問題,上面的代碼,在getName函數中,我既定義了 b變量,也定義了 b函數。為什麼在 console.log(b) // 4 的時候會輸出 undefined

因為在預解析的過程中,會先查找 function 然後再查找 var 所以,function 會被 var 覆蓋。這裏我們會理解為 在預解析過程中,function的優先級高於var。高優先級的會被低優先級的覆蓋 (是不是很繞?沒關係,多想想,加深下理解)

2.作用域鏈

作用域鏈的執行我們在之前就講過了,有沒有人注意到?有沒有?

好吧,沒有人回答我,看來是沒人注意到了。

就在練手代碼中,getName()函數內,向上查找 a 變量的過程,那個就是作用域鏈的查找過程。

閒言少敍,上高清大圖:

image

<font color="red">特別注意:</font>作用域只能從下向上查找,不可逆向。(從函數外不能訪問函數內的變量)

3. 從全局獲取函數內部的變量

上面講到,在函數外訪問函數內的變量是訪問不到的,如果我堅持要訪問呢?(一般情況下,這種鑽牛角尖的人都容易捱打)

好吧,既然你要訪問,那也是有方法的,我們可以在函數內將變量返回出來,這樣就可以訪問到函數內的變量了。

多説無益,還是代碼最實在:

console.log(getA()); // 通過這種方式,我們就可以訪問到 a 變量。

function getA() {
  var a = 1;
  return a;
}

發散一下思維,你還知道其他方式嗎?

4. 塊級作用域

在ES6到來的時候,javascript迎來一個全新的概念,-- 塊級作用域。顧名思義,塊級作用域可以讓變量只在一塊代碼內生效。

舉個栗子:

{
  var a = 1
}
console.log(a)

上述栗子中的代碼會正常輸出,但是下方的代碼會拋出 a 變量未定義的錯誤a is not defined

{
  let a = 1;
}
console.log(a)

也就是説a變量只在花括號內生效,在花括號外是訪問不到的。

可以聲明塊級作用域的方式有兩種。letconst

目前為止,我們看到了三個定義變量的方式,接下來,讓我們瞅瞅他們之間的不同。

4.1 var、let、const的異同

同: 都可以聲明變量。

異:

var 存在局部作用域,可變量提升,聲明的值可更改。
console.log(a)
var a = 1;
a = 2;
// 上述操作都可以
let 存在塊級作用域,不可變量提升,聲明的值可修改。(只可以先聲明變量,然後再使用)
console.log(a) // 會報錯,  a is not defined
let a = 1;
a = 2;
const 存在塊級作用域,不可變量提升,聲明的值本身不可修改(只可以先聲明變量,然後再使用)
const a = 1;
a = 2; // 會報錯,a不可修改

// 下述情況可運行
const a = []
a[0] = 1;

注意:

  • const聲明的叫做常量,不可以修改其本身,但如果聲明的是複雜類型的對象,對象裏的值是可修改的。

這裏你會發現一個問題,三者的功能是逐步增強的。

4.2 TDZ介紹(暫時性死區)為什麼let和const不能變量提升。

使用letconst聲明的變量,在預解析的時候會將變量放入到一個暫時不可訪問的區間中,此時訪問變量會提示未定義錯誤,在給變量賦值後,將變量放入到正常的執行環境中。使變量可以正常訪問。

console.log(a) // 此時的a在TDZ中
let a = 1;     // 將a從TDZ中移出來
console.log(a) // 此時可以正常訪問a變量

相信你現在已經充滿能量,打開你的代碼,學會分析每一步的執行順序吧。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.