第一章: 作用域是什麼
1、 編譯原理
JavaScript 被列為 ‘動態’ 或 ‘解釋執行’ 語言,於其他傳統語言(如 java)不同的是,JavaScript是邊編譯邊執行的。
一段源碼在執行前會經歷三個步驟: 分詞/詞法分析 -> 解析/語法分析 -> 代碼生成
- 分詞/詞法分析
這個過程將字符串分解成詞法單元,如 var a = 2; 會被分解成詞法單元 var、 a、 = 、2、;。空格一般沒意義會被忽略
- 解析/語法分析
這個過程會將詞法單元轉換成 抽象語法樹(Abstract Syntax Tree,AST)。
如 var a = 2; 對應的 抽象語法樹 如下, 可通過 在線可視化AST 網址在線分析
{
"type": "Program",
"start": 0,
"end": 10,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 10,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 2,
"raw": "2"
}
}
],
"kind": "var"
}
],
"sourceType": "module"
}
- 代碼生成
將 AST 轉換成可執行的代碼,存放於內存中,並分配內存和轉化為一些機器指令
2、理解作用域
其實結合上面提到的編譯原理,作用域就好理解了。作用域就是當前執行代碼對這些標識符的訪問權限。
編譯器會在當前作用域中聲明一些變量,運行時引擎會去作用域中查找這些變量(其實就是一個尋址的過程),如果找到這些變量就可以操作變量,找不到就往上一層作用域找(作用域鏈的概念),或者返回 null
第三章: 函數作用域和塊作用域
1、函數中的作用域
每聲明一個函數都會形成一個作用域,那作用域有什麼用呢,它能讓該作用域內的變量和函數不被外界訪問到,也可以反過來說是不讓該作用域內的變量或函數汙染全局。
對比:
var a = 123
function bar() {
//...
}
和
function foo() {
var a = 123
function bar() {
//...
}
}
變量 a 和函數 bar 用一個函數 foo 包裹起來,函數 foo 會形成一個作用域,變量 a 和函數 bar 外界將無法訪問,同時變量或函數也不會汙染全局。
2、函數作用域
進一步思考,上面例子的變量 a 和函數 bar 有了作用域,但函數 foo 不也是暴露在全局,也對全局造成汙染了啊。是的,JavaScript對這種情況提出瞭解決方案: 立即執行函數 (IIFE)
(function foo() {
var a = 123
function bar() {
//...
}
})()
第一個()將函數變成表達式,第二個()執行了這個函數,最終函數 foo 也形成了自己的作用域,不會汙染到全局,同時也不被全局訪問的到。
3、塊作用域
es6之前JavaScript是沒有塊作用域這個概念的,這與一般的語言(如Java ,C)很大不同,看下面這個例子:
for (var i = 0; i < 10; i++) {
console.log('i=', i);
}
console.log('輸出', i); // 輸出 10
for 循環定義了變量 i,通常我們只想這個變量 i 在循環內使用,但忽略了 i 其實是作用在外部作用域(函數或全局)的。所以循環過後也能正常打印出 i ,因為沒有塊的概念。
甚至連 try/catch 也沒形成塊作用域:
try {解決方法1
for (var i = 0; i < 10; i++) {
console.log('i=', i);
}
} catch (error) {}
console.log('輸出', i); // 輸出 10
形成塊作用域的方法當然是使用 es6 的 let 和 const 了, let 為其聲明的變量隱式的劫持了所在的塊作用域。
for (let i = 0; i < 10; i++) {
console.log('i=', i);
}
console.log('輸出', i); // ReferenceError: i is not defined
將上面例子的 var 換成 let 最後輸出就報錯了 ReferenceError: i is not defined ,說明被 let 聲明的 i 只作用在了 for 這個塊中。
除了 let 會讓 for、if、try/catch 等形成塊,JavaScript 的 {} 也能形成塊
{解決方法2
let name = '曾田生'
}
console.log(name); //ReferenceError: name is not defined
早在沒 es6 的 let 聲明之前,常用的做法是利用 函數也能形成作用域 這麼個概念來解決一些問題的。
看個例子
function foo() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i
}
}
console.log(i)// i 作用在整個函數,for 執行完此時 i 已經等於 10 了
return result
}
var result = foo()
console.log(result[0]()); // 輸出 10 期望 0
console.log(result[1]()); // 輸出 10 期望 1
console.log(result[2]()); // 輸出 10 期望 2
這個例子出現的問題是執行數組函數最終都輸出了 10, 因為 i 作用在整個函數,for 執行完此時 i 已經等於 10 了, 所以當後續執行函數 result[x]() 內部返回的 i 已經是 10 了。
利用函數的作用域來解決
function foo() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = function (num) {
return function () { // 函數形成一個作用域,內部變量被私有化了
return num
}
}(i)
}
return result
}
var result = foo()
console.log(result[0]()); // 0
console.log(result[1]()); // 1
console.log(result[2]()); // 2
上面的例子也是挺典型的,一般面試題比較考基礎的話就會被問道,上面例子不僅考察到了塊作用域的概念,函數作用域的概念,還考察到了閉包的概念(閉包後續講但不影響這個例子的理解),多琢磨一下就理解了。
第四章: 提升
提升指的是變量提升和函數提升,為什麼JavaScript會有提升這個概念呢,其實也很好理解,因為JavaScript代碼是先 編譯 後 執行 的,所以在編譯階段就會先對變量和函數做聲明,在執行階段就出現了所謂的變量提升和函數提升了。
1、變量提升
console.log(a); // undefined
var a = 1;
上面代碼 console.log(a); // undefined 就是因為編譯階段先對變量做了聲明,先聲明瞭個變量 a, 並默認賦值 undefined
var a;
console.log(a); // undefined
a = 1;
2、函數提升
函數同樣也存在提升,這就是為什麼函數能先調用後聲明瞭
foo();
function foo() {
console.log('---foo----');
}
注意:函數表達式不會被提升
foo();
var foo = function() {
console.log('---foo----');
}
// TypeError: foo is not a function
注意:函數會首先被提升,然後才是變量
var foo = 1;
foo();
function foo() {
console.log('---foo----');
}
// TypeError: foo is not a function
分析一下,因為上面例子編譯後是這樣的
var foo = undefined; // 變量名賦值 undefined
function foo() { // 函數先提升
console.log('---foo----');
}
foo = 1; // 但接下去是變量被重新賦值了 1,是個Number類型
foo(); // Number類型當然不能用函數方式調用,就報錯了
// TypeError: foo is not a function
第五章: 作用域閉包
閉包問題一直會在JavaScript被提起,是JavaScript一個比較奇葩的概念
1、閉包的產生
閉包的概念: 當函數可以記住並訪問所在的詞法作用域時,就產生了閉包概念貌似挺簡單的,簡單分析下,首先閉包是 產生的,是在代碼執行中產生的,有的一些網絡博文直接將閉包定義為 某一個特殊函數 是錯的。
閉包是怎麼產生的呢,一個函數能訪問到所在函數作用域就產生了閉包,注意到作用域的概念,咱們最上面的章節有提到,看下面例子:
function foo() {
var a = 0;
function bar() {
a++;
console.log(a);
}
return bar;
}
var bat = foo()
bat() // 1
bat() // 2
bat() // 3
結合例子分析一下: 函數 foo 內部返回了函數 bar ,外部聲明個變量 bat 拿到 foo 返回的函數 bar ,執行 bat() 發現能正常輸出 1 ,注意前面章節提到的作用域,變量 a 是在函數 foo 內部的一個私有變量,不能被外界訪問的,但外部函數 bat 卻能訪問的到私有變量 a,這說明了 外部函數 bat 持有函數 foo 的作用域 ,也就產生了閉包。
閉包的形成有什麼用呢,JavaScript 讓閉包的存在明顯有它的作用,其中一個作用是為了模塊化,當然你也可以利用外部函數持有另一個函數作用域的閉包特性去做更多的事情,但這邊就暫且討論模塊化這個作用。
函數有什麼作用呢,私有化變量或方法呀,那函數內的變量和方法被私有化了函數怎麼和外部做 交流 呢, 暴露出一些變量或方法呀
function foo() {
var _a = 0;
var b = 0;
function _add() {
b = _a + 10
}
function bar() {
_add()
}
function getB() {
return b
}
return {
bar: bar,
getB: getB
}
}
var bat = foo()
bat.bar()
bat.getB() // 10
上面例子函數 foo 可以理解為一個模塊,內部聲明瞭一些私有變量和方法,也對外界暴露了一些方法,只是在執行的過程中順帶產生了一個閉包
2、模塊機制
上面提到了閉包的產生和作用,貌似在使用 es6語法 開發的過程中很少用到了閉包,但實際上我們一直在用閉包的概念的。
foo.jsvar _a = 0;bat.js
var b = 0;
function _add() {
b = _a + 10
}
function bar() {
_add()
}
function getB() {
return b
}
export default {
bar: bar,
getB: getB
}
import bat from 'foo'
bat.bar()
bat.getB() // 10
上面例子是 es6 模塊的寫法,是不是驚奇的發現變量 bat 可以記住並訪問模塊 foo 的作用域,這符合了閉包的概念。
小結:
本章節我們深入理解了JavaScript的 作用域,提升,閉包等概念,希望你能有所收穫,下一部分整理下 this解析、對象、原型 等一些概念。
閱讀更多 一字咖啡屋 的文章
關鍵字: 百度 JavaScript 模塊