你不知道的JavaScript·第一部分

第一章: 做用域是什麼

一、 編譯原理

JavaScript 被列爲 ‘動態’ 或 ‘解釋執行’ 語言,於其餘傳統語言(如 java)不一樣的是,JavaScript是邊編譯邊執行的。
一段源碼在執行前會經歷三個步驟: 分詞/詞法分析 -> 解析/語法分析 -> 代碼生成java

  • 分詞/詞法分析

這個過程將字符串分解成詞法單元,如 var a = 2; 會被分解成詞法單元 var、 a、 = 、二、;。空格通常沒意義會被忽略git

  • 解析/語法分析

這個過程會將詞法單元轉換成 抽象語法樹(Abstract Syntax Tree,AST)。
如 var a = 2; 對應的 抽象語法樹 以下, 可經過 在線可視化AST 網址在線分析es6

{
  "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 轉換成可執行的代碼,存放於內存中,並分配內存和轉化爲一些機器指令github

二、理解做用域

其實結合上面提到的編譯原理,做用域就好理解了。做用域就是當前執行代碼對這些標識符的訪問權限。
編譯器會在當前做用域中聲明一些變量,運行時引擎會去做用域中查找這些變量(其實就是一個尋址的過程),若是找到這些變量就能夠操做變量,找不到就往上一層做用域找(做用域鏈的概念),或者返回 null面試

第三章: 函數做用域和塊做用域

一、函數中的做用域

每聲明一個函數都會造成一個做用域,那做用域有什麼用呢,它能讓該做用域內的變量和函數不被外界訪問到,也能夠反過來講是不讓該做用域內的變量或函數污染全局。數組

對比:網絡

var a = 123
function bar() {
  //...
}

閉包

function foo() {
  var a = 123
  function bar() {
    //...
  }
}

變量 a 和函數 bar 用一個函數 foo 包裹起來,函數 foo 會造成一個做用域,變量 a 和函數 bar 外界將沒法訪問,同時變量或函數也不會污染全局。模塊化

二、函數做用域

進一步思考,上面例子的變量 a 和函數 bar 有了做用域,但函數 foo 不也是暴露在全局,也對全局形成污染了啊。是的,JavaScript對這種狀況提出瞭解決方案: 當即執行函數 (IIFE)函數

(function foo() {
  var a = 123
  function bar() {
    //...
  }
})()

第一個()將函數變成表達式,第二個()執行了這個函數,最終函數 foo 也造成了本身的做用域,不會污染到全局,同時也不被全局訪問的到。

三、塊做用域

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代碼是先 編譯執行 的,因此在編譯階段就會先對變量和函數作聲明,在執行階段就出現了所謂的變量提高和函數提高了。

一、變量提高

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

上面代碼 console.log(a); // undefined 就是由於編譯階段先對變量作了聲明,先聲明瞭個變量 a, 並默認賦值 undefined

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

二、函數提高

函數一樣也存在提高,這就是爲何函數能先調用後聲明瞭

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一個比較奇葩的概念

一、閉包的產生

閉包的概念: 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包

概念貌似挺簡單的,簡單分析下,首先閉包是 產生的,是在代碼執行中產生的,有的一些網絡博文直接將閉包定義爲 某一個特殊函數 是錯的。

閉包是怎麼產生的呢,一個函數能訪問到所在函數做用域就產生了閉包,注意到做用域的概念,我們最上面的章節有提到,看下面例子:

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 能夠理解爲一個模塊,內部聲明瞭一些私有變量和方法,也對外界暴露了一些方法,只是在執行的過程當中順帶產生了一個閉包

二、模塊機制

上面提到了閉包的產生和做用,貌似在使用 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

相關文章
相關標籤/搜索