你不知道的JavaScript·第一部分

曾田生z發表於2018-06-25

第一章: 作用域是什麼

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 {
  for (var i = 0; i < 10; i++) {
    console.log('i=', i);
  }
} catch (error) {}
console.log('輸出', i); // 輸出 10
複製程式碼

解決方法1

形成塊作用域的方法當然是使用 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 的 {} 也能形成塊

{
  let name = '曾田生'
}

console.log(name); //ReferenceError: name is not defined
複製程式碼

解決方法2

早在沒 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.js

var _a = 0;
var b = 0;
function _add() {
  b = _a + 10
}
function bar() {
  _add()
}
function getB() {
  return b
}
export default {
  bar: bar,
  getB: getB
}
複製程式碼

bat.js

import bat from 'foo'

bat.bar()
bat.getB() // 10
複製程式碼

上面例子是 es6 模組的寫法,是不是驚奇的發現變數 bat 可以記住並訪問模組 foo 的作用域,這符合了閉包的概念。

小結:

本章節我們深入理解了JavaScript的 作用域提升閉包等概念,希望你能有所收穫,下一部分整理下 this解析物件原型 等一些概念。

如果有興趣也可以去我的 github-blogissues ,github也整理了幾篇文章會定期更新,歡迎 star

相關文章