javascript 擁有一套設計良好的規則來存儲變量,而且以後能夠方便地找到這些變量,這套規則被稱爲做用域。javascript
做用域就是代碼的執行環境,全局執行環境就是全局做用域,函數的執行環境就是私有做用域,它們都是棧內存。前端
當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(做用域造成的鏈條),因爲變量的查找是沿着做用域鏈來實現的,因此也稱做用域鏈爲變量查找的機制。java
內部環境能夠經過做用域鏈訪問全部外部環境,但外部環境不能訪問內部環境的任何變量和函數。
編譯node
以 var a = 2;爲例,說明 javascript 的內部編譯過程,主要包括如下三步:react
分詞(tokenizing)ajax
把由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)編程
var a = 2;被分解成爲下面這些詞法單元:var、a、=、二、;。這些詞法單元組成了一個詞法單元流數組json
[ "var": "keyword", "a": "identifier", "=": "assignment", "2": "integer", ";": "eos" (end of statement) ]
解析(parsing)api
把詞法單元流數組轉換成一個由元素逐級嵌套所組成的表明程序語法結構的樹,這個樹被稱爲「抽象語法樹」 (Abstract Syntax Tree, AST)數組
var a = 2;的抽象語法樹中有一個叫 VariableDeclaration 的頂級節點,接下來是一個叫 Identifier(它的值是 a)的子節點,以及一個叫 AssignmentExpression 的子節點,且該節點有一個叫 Numericliteral(它的值是 2)的子節點
{ operation: "=", left: { keyword: "var", right: "a" } right: "2" }
將 AST 轉換爲可執行代碼的過程被稱爲代碼生成
var a=2;的抽象語法樹轉爲一組機器指令,用來建立一個叫做 a 的變量(包括分配內存等),並將值 2 儲存在 a 中
實際上,javascript 引擎的編譯過程要複雜得多,包括大量優化操做,上面的三個步驟是編譯過程的基本概述
任何代碼片斷在執行前都要進行編譯,大部分狀況下編譯發生在代碼執行前的幾微秒。javascript 編譯器首先會對 var a=2;這段程序進行編譯,而後作好執行它的準備,而且一般立刻就會執行它
執行
簡而言之,編譯過程就是編譯器把程序分解成詞法單元(token),而後把詞法單元解析成語法樹(AST),再把語法樹變成機器指令等待執行的過程
實際上,代碼進行編譯,還要執行。下面仍然以 var a = 2;爲例,深刻說明編譯和執行過程
編譯
依據編譯器的編譯原理,javascript 中的重複聲明是合法的
// test在做用域中首次出現,因此聲明新變量,並將20賦值給test var test = 20 // test在做用域中已經存在,直接使用,將20的賦值替換成30 var test = 30
執行
查詢
在引擎執行的第一步操做中,對變量 a 進行了查詢,這種查詢叫作 LHS 查詢。實際上,引擎查詢共分爲兩種:LHS 查詢和 RHS 查詢
從字面意思去理解,當變量出如今賦值操做的左側時進行 LHS 查詢,出如今右側時進行 RHS 查詢
更準確地講,RHS 查詢與簡單地查找某個變量的值沒什麼區別,而 LHS 查詢則是試圖找到變量的容器自己,從而能夠對其賦值
function foo(a) { console.log(a) // 2 } foo(2)
這段代碼中,總共包括 4 個查詢,分別是:
一、foo(...)對 foo 進行了 RHS 引用
二、函數傳參 a = 2 對 a 進行了 LHS 引用
三、console.log(...)對 console 對象進行了 RHS 引用,並檢查其是否有一個 log 的方法
四、console.log(a)對 a 進行了 RHS 引用,並把獲得的值傳給了 console.log(...)
嵌套
在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量,或抵達最外層的做用域(也就是全局做用域)爲止
function foo(a) { console.log(a + b) } var b = 2 foo(2) // 4
行 RHS 引用,沒有找到;接着,引擎在全局做用域中查找 b,成功找到後,對其進行 RHS 引用,將 2 賦值給 b
異常
爲何區分 LHS 和 RHS 是一件重要的事情?由於在變量尚未聲明(在任何做用域中都沒法找到變量)的狀況下,這兩種查詢的行爲不同
RHS
// 對b進行RHS查詢時,沒法找到該變量。也就是說,這是一個「未聲明」的變量 function foo(a) { a = b } foo() // ReferenceError: b is not defined
function foo() { var b = 0 b() } foo() // TypeError: b is not a function
LHS
function foo() { a = 1 } foo() console.log(a) // 1
function foo() { 'use strict' a = 1 } foo() console.log(a) // ReferenceError: a is not defined
原理
function foo(a) { console.log(a) } foo(2)
以上面這個代碼片斷來講明做用域的內部原理,分爲如下幾步:
【1】引擎須要爲 foo(...)函數進行 RHS 引用,在全局做用域中查找 foo。成功找到並執行
【2】引擎須要進行 foo 函數的傳參 a=2,爲 a 進行 LHS 引用,在 foo 函數做用域中查找 a。成功找到,並把 2 賦值給 a
【3】引擎須要執行 console.log(...),爲 console 對象進行 RHS 引用,在 foo 函數做用域中查找 console 對象。因爲 console 是個內置對象,被成功找到
【4】引擎在 console 對象中查找 log(...)方法,成功找到
【5】引擎須要執行 console.log(a),對 a 進行 RHS 引用,在 foo 函數做用域中查找 a,成功找到並執行
【6】因而,引擎把 a 的值,也就是 2 傳到 console.log(...)中
【7】最終,控制檯輸出 2
編譯器的第一個工做階段叫做分詞,就是把由字符組成的字符串分解成詞法單元。這個概念是理解詞法做用域的基礎
簡單地說,詞法做用域就是定義在詞法階段的做用域,是由寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變
不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處的位置決定
function foo(a) { var b = a * 2 function bar(c) { console.log(a, b, c) } bar(b * 3) } foo(2) // 2 4 12
在這個例子中有三個逐級嵌套的做用域。爲了幫助理解,能夠將它們想象成幾個逐級包含的氣泡
做用域氣泡由其對應的做用域塊代碼寫在哪裏決定,它們是逐級包含的
氣泡 1 包含着整個全局做用域,其中只有一個標識符:foo
氣泡 2 包含着 foo 所建立的做用域,其中有三個標識符:a、bar 和 b
氣泡 3 包含着 bar 所建立的做用域,其中只有一個標識符:c
做用域氣泡的結構和互相之間的位置關係給引擎提供了足夠的位置信息,引擎用這些信息來查找標識符的位置
在代碼片斷中,引擎執行 console.log(...)聲明,並查找 a、b 和 c 三個變量的引用。它首先從最內部的做用域,也就是 bar(...)函數的做用域開始查找。引擎沒法在這裏找到 a,所以會去上一級到所嵌套的 foo(...)的做用域中繼續查找。在這裏找到了 a,所以引擎使用了這個引用。對 b 來說也同樣。而對 c 來講,引擎在 bar(...)中找到了它
[注意]詞法做用域查找只會查找一級標識符,若是代碼引用了 foo.bar.baz,詞法做用域查找只會試圖查找 foo 標識符,找到這個變量後,對象屬性訪問規則分別接管對 bar 和 baz 屬性的訪問
foo = { bar: { baz: 1 } } console.log(foo.bar.baz) // 1
做用域查找從運行時所處的最內部做用域開始,逐級向外或者說向上進行,直到碰見第一個匹配的標識符爲止
在多層的嵌套做用域中能夠定義同名的標識符,這叫做「遮蔽效應」,內部的標識符「遮蔽」了外部的標識符
var a = 0 function test() { var a = 1 console.log(a) // 1 } test()
全局變量會自動爲全局對象的屬性,所以能夠不直接經過全局對象的詞法名稱,而是間接地經過對全局對象屬性的引用來對其進行訪問
var a = 0 function test() { var a = 1 console.log(window.a) //0 } test()
經過這種技術能夠訪問那些被同名變量所遮蔽的全局變量。但非全局的變量若是被遮蔽了,不管如何都沒法被訪問到
javascript 使用的是詞法做用域,它最重要的特徵是它的定義過程發生在代碼的書寫階段
那爲何要介紹動態做用域呢?實際上動態做用域是 javascript 另外一個重要機制 this 的表親。做用域混亂多數是由於詞法做用域和 this 機制相混淆,傻傻分不清楚
動態做用域並不關心函數和做用域是如何聲明以及在任何處聲明的,只關心它們從何處調用。換句話說,做用域鏈是基於調用棧的,而不是代碼中的做用域嵌套
var a = 2 function foo() { console.log(a) } function bar() { var a = 3 foo() } bar()
【1】若是處於詞法做用域,也就是如今的 javascript 環境。變量 a 首先在 foo()函數中查找,沒有找到。因而順着做用域鏈到全局做用域中查找,找到並賦值爲 2。因此控制檯輸出 2
【2】若是處於動態做用域,一樣地,變量 a 首先在 foo()中查找,沒有找到。這裏會順着調用棧在調用 foo()函數的地方,也就是 bar()函數中查找,找到並賦值爲 3。因此控制檯輸出 3
兩種做用域的區別,簡而言之,詞法做用域是在定義時肯定的,而動態做用域是在運行時肯定的
執行棧,在其餘編程語言中也被叫作調用棧,具備 LIFO(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。
當 JavaScript 引擎首次讀取你的腳本時,它會建立一個全局執行上下文並將其推入當前的執行棧。每當發生一個函數調用,引擎都會爲該函數建立一個新的執行上下文並將其推到當前執行棧的頂端。
引擎會運行執行上下文在執行棧頂端的函數,當此函數運行完成後,其對應的執行上下文將會從執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文。
讓咱們經過下面的代碼示例來理解這一點:
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context');
當上述代碼在瀏覽器中加載時,JavaScript 引擎會建立一個全局執行上下文而且將它推入當前的執行棧。當調用 first()
函數時,JavaScript 引擎爲該函數建立了一個新的執行上下文並將其推到當前執行棧的頂端。
當在 first()
函數中調用 second()
函數時,Javascript 引擎爲該函數建立了一個新的執行上下文並將其推到當前執行棧的頂端。當 second()
函數執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文,即 first()
函數的執行上下文。
當 first()
函數執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到全局執行上下文。一旦全部代碼執行完畢,Javascript 引擎把全局執行上下文從執行棧中移除。
到目前爲止,咱們已經看到了 JavaScript 引擎如何管理執行上下文,如今就讓咱們來理解 JavaScript 引擎是如何建立執行上下文的。
執行上下文分兩個階段建立: 1)建立階段; 2)執行階段
在任意的 JavaScript 代碼被執行前,執行上下文處於建立階段。在建立階段中總共發生了三件事情:
所以,執行上下文能夠在概念上表示以下:
ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, }
This Binding:
在全局執行上下文中, this
的值指向全局對象,在瀏覽器中, this
的值指向 window 對象。
在函數執行上下文中, this
的值取決於函數的調用方式。若是它被一個對象引用調用,那麼 this
的值被設置爲該對象,不然 this
的值被設置爲全局對象或 undefined
(嚴格模式下)。例如:
let person = { name: 'peter', birthYear: 1994, calcAge: function() { console.log(2018 - this.birthYear); } } person.calcAge(); // 'this' 指向 'person', 由於 'calcAge' 是被 'person' 對象引用調用的。 let calculateAge = person.calcAge; calculateAge(); // 'this' 指向全局 window 對象,由於沒有給出任何對象引用
官方 ES6 文檔將詞法環境定義爲:
詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關係。詞法環境由環境記錄(environment record)和可能爲空引用(null)的外部詞法環境組成。
簡而言之,詞法環境是一個包含 標識符變量映射 的結構。(這裏的 標識符 表示變量/函數的名稱, 變量 是對實際對象【包括函數類型對象】或原始值的引用)
在詞法環境中,有兩個組成部分:(1) 環境記錄(environment record) (2) 對外部環境的引用
詞法環境有兩種類型:
this
的值指向這個全局對象。注意:對於 函數環境 而言, 環境記錄 還包含了一個 arguments
對象,該對象包含了索引和傳遞給函數的參數之間的映射以及傳遞給函數的參數的 長度(數量) 。例如,下面函數的 arguments
對象以下所示:
function foo(a, b) { var c = a + b; } foo(2, 3); // arguments 對象 Arguments: {0: 2, 1: 3, length: 2},
環境記錄一樣有兩種類型(以下所示):
抽象地說,詞法環境在僞代碼中看起來像這樣:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符綁定在這裏 outer: <null> } } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符綁定在這裏 outer: <Global or outer function environment reference> } } }
它也是一個詞法環境,其 EnvironmentRecord
包含了由 VariableStatements 在此執行上下文建立的綁定。
如上所述,變量環境也是一個詞法環境,所以它具備上面定義的詞法環境的全部屬性。
在 ES6 中, LexicalEnvironment 組件和 VariableEnvironment 組件的區別在於前者用於存儲函數聲明和變量( let
和 const
)綁定,然後者僅用於存儲變量( var
)綁定。
讓咱們結合一些代碼示例來理解上述概念:
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e *f *g; } c = multiply(20, 30);
執行上下文以下所示:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符綁定在這裏 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符綁定在這裏 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符綁定在這裏 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符綁定在這裏 g: undefined }, outer: <GlobalLexicalEnvironment> } }
注意:只有在遇到函數 multiply
的調用時纔會建立函數執行上下文。
你可能已經注意到了 let
和 const
定義的變量沒有任何與之關聯的值,但 var
定義的變量設置爲 undefined
。
這是由於在建立階段,代碼會被掃描並解析變量和函數聲明,其中函數聲明存儲在環境中,而變量會被設置爲 undefined
(在 var
的狀況下)或保持未初始化(在 let
和 const
的狀況下)。
這就是爲何你能夠在聲明以前訪問 var
定義的變量(儘管是 undefined
),但若是在聲明以前訪問 let
和 const
定義的變量就會提示引用錯誤的緣由。
這就是咱們所謂的變量提高。
這是整篇文章中最簡單的部分。在此階段,完成對全部變量的分配,最後執行代碼。
注:在執行階段,若是 Javascript 引擎在源代碼中聲明的實際位置找不到 let
變量的值,那麼將爲其分配 undefined
值。
Node.js 才支持這個特性,經過 Error.captureStackTrace 來實現,Error.captureStackTrace 接收一個 object 做爲第 1 個參數,以及可選的 function 做爲第 2 個參數。其做用是捕獲當前的調用棧並對其進行裁剪,捕獲到的調用棧會記錄在第 1 個參數的 stack 屬性上,裁剪的參照點是第 2 個參數,也就是說,此函數以前的調用會被記錄到調用棧上面,而以後的不會。
讓咱們用代碼來講明,首先,把當前的調用棧捕獲並放到 myObj 上:
const myObj = {}; function c() {} function b() { // 把當前調用棧寫到 myObj 上 Error.captureStackTrace(myObj); c(); } function a() { b(); } // 調用函數 a a(); // 打印 myObj.stack console.log(myObj.stack); // 輸出會是這樣 // at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack // at a (repl:2:1) // at repl:1:1 <-- Node internals below this line // at realRunInThisContextScript (vm.js:22:35) // at sigintHandlersWrap (vm.js:98:12) // at ContextifyScript.Script.runInThisContext (vm.js:24:12) // at REPLServer.defaultEval (repl.js:313:29) // at bound (domain.js:280:14) // at REPLServer.runBound [as eval] (domain.js:293:12) // at REPLServer.onLine (repl.js:513:10)
上面的調用棧中只有 a -> b,由於咱們在 b 調用 c 以前就捕獲了調用棧。如今對上面的代碼稍做修改,而後看看會發生什麼:
const myObj = {}; function d() { // 咱們把當前調用棧存儲到 myObj 上,可是會去掉 b 和 b 以後的部分 Error.captureStackTrace(myObj, b); } function c() { d(); } function b() { c(); } function a() { b(); } // 執行代碼 a(); // 打印 myObj.stack console.log(myObj.stack); // 輸出以下 // at a (repl:2:1) <-- As you can see here we only get frames before b was called // at repl:1:1 <-- Node internals below this line // at realRunInThisContextScript (vm.js:22:35) // at sigintHandlersWrap (vm.js:98:12) // at ContextifyScript.Script.runInThisContext (vm.js:24:12) // at REPLServer.defaultEval (repl.js:313:29) // at bound (domain.js:280:14) // at REPLServer.runBound [as eval] (domain.js:293:12) // at REPLServer.onLine (repl.js:513:10) // at emitOne (events.js:101:20)
在這段代碼裏面,由於咱們在調用 Error.captureStackTrace 的時候傳入了 b,這樣 b 以後的調用棧都會被隱藏。
如今你可能會問,知道這些到底有啥用?若是你想對用戶隱藏跟他業務無關的錯誤堆棧(好比某個庫的內部實現)就能夠試用這個技巧。
當程序運行出現錯誤時, 一般會拋出一個 Error 對象. Error 對象能夠做爲用戶自定義錯誤對象繼承的原型.
Error.prototype 對象包含以下屬性:
constructor–指向實例的構造函數
message–錯誤信息
name–錯誤的名字(類型)
上述是 Error.prototype 的標準屬性, 此外, 不一樣的運行環境都有其特定的屬性. 在例如 Node, Firefox, Chrome, Edge, IE 10+, Opera 以及 Safari 6+
這樣的環境中, Error 對象具有 stack 屬性, 該屬性包含了錯誤的堆棧軌跡. 一個錯誤實例的堆棧軌跡包含了自構造函數以後的全部堆棧結構.
只查看調用棧:console.trace
a() function a() { b() } function b() { c() } function c() { let aa = 1 } console.trace()
在 JavaScript 中,函數也是對象,所以函數能夠做爲一個對象的屬性,此時該函數被稱爲該對象的方法,在使用這種調用方式時,this 被天然綁定到該對象
var test = { a:0, b:0, get:function(){ return this.a; } }
函數也能夠直接被調用,此時 this 綁定到全局對象。在瀏覽器中,window 就是該全局對象。好比下面的例子:函數被調用時,this 被綁定到全局對象,
接下來執行賦值語句,至關於隱式的聲明瞭一個全局變量,這顯然不是調用者但願的。
function makeNoSense(x) { this.x = x; }
javaScript 支持面向對象式編程,與主流的面向對象式編程語言不一樣,JavaScript 並無類(class)的概念,而是使用基於原型(prototype)的繼承方式。
相應的,JavaScript 中的構造函數也很特殊,若是不使用 new 調用,則和普通函數同樣。做爲又一項約定俗成的準則,構造函數以大寫字母開頭,
提醒調用者使用正確的方式調用。若是調用正確,this 綁定到新建立的對象上。
function Point(x, y){ this.x = x; this.y = y; }
讓咱們再一次重申,在 JavaScript 中函數也是對象,對象則有方法,apply 和 call 就是函數對象的方法。
這兩個方法異常強大,他們容許切換函數執行的上下文環境(context),即 this 綁定的對象。
不少 JavaScript 中的技巧以及類庫都用到了該方法。讓咱們看一個具體的例子:
function Point(x, y){ this.x = x; this.y = y; this.moveTo = function(x, y){ this.x = x; this.y = y; } } var p1 = new Point(0, 0); var p2 = {x: 0, y: 0}; p1.moveTo(1, 1); p1.moveTo.apply(p2, [10, 10])
由於函數內部聲明 的變量是局部的,只能在函數內部訪問到,可是函數外部的變量是對函數內部可見的,這就是做用域鏈的特色了。
子級能夠向父級查找變量,逐級查找,找到爲止
所以咱們能夠在函數內部再建立一個函數,這樣對內部的函數來講,外層函數的變量都是可見的,而後咱們就能夠訪問到他的變量了。
function bar(){ //外層函數聲明的變量 var value=1; function foo(){ console.log(value); } return foo(); }; var bar2=bar; //實際上bar()函數並無由於執行完就被垃圾回收機制處理掉 //這就是閉包的做用,調用bar()函數,就會執行裏面的foo函數,foo這時就會訪問到外層的變量 bar2();
foo()包含bar()內部做用域的閉包,使得該做用域可以一直存活,不會被垃圾回收機制處理掉,這就是閉包的做用,以供foo()在任什麼時候間進行引用。
function addFn(a,b){ return(function(){ console.log(a+"+"+b); }) } var test =addFn(a,b); setTimeout(test,3000);
通常setTimeout的第一個參數是個函數,可是不能傳值。若是想傳值進去,能夠調用一個函數返回一個內部函數的調用,將內部函數的調用傳給setTimeout。內部函數執行所需的參數,外部函數傳給他,在setTimeout函數中也能夠訪問到外部函數。
在一些編程軟件中,好比c語言中,須要使用malloc來申請內存空間,再使用free釋放掉,須要手動清除。而js中是有本身的垃圾回收機制的,通常經常使用的垃圾收集方法就是標記清除。
標記清除法:在一個變量進入執行環境後就給它添加一個標記:進入環境,進入環境的變量不會被釋放,由於只要執行流進入響應的環境,就可能用到他們。當變量離開環境後,則將其標記爲「離開環境」。
一、當自執行函數在循環當中使用時,自執行函數會在循環結束以後纔會運行。好比你在自執行函數外面定義一個數組,在自執行函數當中給這個數組追加內容,你在自執行函數以外輸出時,會發現這個數組當中什麼都沒有,這就是由於自執行函數會在循環運行完後纔會執行。
二、當自執行函數在循環當中使用時,要是自執行函數當中嵌套ajax,那麼循環當中的下標i就不會傳進ajax當中,須要在ajax外面把下標i賦值給一個變量,在ajax中直接調用這個變量就能夠了。
例子:
$.ajax({ type: "GET", dataType: "json", url: "***", success: function(data) { //console.log(data); for (var i = 0; i < data.length; i++) { (function(i, abbreviation) { $.ajax({ type: "GET", url: "/api/faults?abbreviation=" + encodeURI(abbreviation), dataType: "json", success: function(result) { //獲取數據後作的事情 } }) })(i, data[i].abbreviation); } } });
所謂的遞歸函數就是在函數體內調用本函數。使用遞歸函數必定要注意,處理不當就會進入死循環。
const asyncDeal = (i) = > { if (i < 3) { $.get('/api/changeParts/change_part_standard?part=' + data[i].change_part_name, function(res) { //獲取數據後作的事情 i++; asyncDeal(i); }) } else { //異步完成後作的事情 } }; asyncDeal(0);
async/await更加語義化,async 是「異步」的簡寫,async function 用於申明一個 function 是異步的; await,能夠認爲是async wait的簡寫, 用於等待一個異步方法執行完成;
async/await是一個用同步思惟解決異步問題的方案(等結果出來以後,代碼纔會繼續往下執行)
能夠經過多層 async function 的同步寫法代替傳統的callback嵌套
自動將常規函數轉換成Promise,返回值也是一個Promise對象
只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數
異步函數內部可使用await
await 放置在Promise調用以前,await 強制後面點代碼等待,直到Promise對象resolve,獲得resolve的值做爲await表達式的運算結果
await只能在async函數內部使用,用在普通函數裏就會報錯
const asyncFunc = function(i) { return new Promise(function(resolve) { $.get(url, function(res) { resolve(res); }) }); } const asyncDeal = async function() { for (let i = 0; i < data.length; i++) { let res = await asyncFunc(i); //獲取數據後作的事情 } } asyncDeal();
容許模塊經過require方法來同步加載所要依賴的其餘模塊,而後經過exports或module.exports來導出須要暴露的接口。
使用方式:
// 導入 require("module"); require("../app.js"); // 導出 exports.getStoreInfo = function() {}; module.exports = someValue;
優勢:
缺點:
爲何瀏覽器不能使用同步加載,服務端能夠?
參照CommonJs模塊表明node.js的模塊系統
採用異步方式加載模塊,模塊的加載不影響後面語句的運行。全部依賴模塊的語句,都定義在一個回調函數中,等到加載完成以後,回調函數才執行。
使用實例:
// 定義 define("module", ["dep1", "dep2"], function(d1, d2) {...}); // 加載模塊 require(["module", "../app"], function(module, app) {...});
加載模塊require([module], callback);第一個參數[module],是一個數組,裏面的成員就是要加載的模塊;第二個參數callback是加載成功以後的回調函數。
優勢:
缺點:
實現AMD規範表明require.js
RequireJS對模塊的態度是預執行。因爲 RequireJS 是執行的 AMD 規範, 所以全部的依賴模塊都是先執行;即RequireJS是預先把依賴的模塊執行,至關於把require提早了
RequireJS執行流程:
CMD規範和AMD很類似,簡單,並與CommonJS和Node.js的 Modules 規範保持了很大的兼容性;在CMD規範中,一個模塊就是一個文件。
定義模塊使用全局函數define,其接收 factory 參數,factory 能夠是一個函數,也能夠是一個對象或字符串;
factory 是一個函數,有三個參數,function(require, exports, module):
實例:
define(function(require, exports, module) { var a = require('./a'); a.doSomething(); // 依賴就近書寫,何時用到何時引入 var b = require('./b'); b.doSomething(); });
優勢:
缺點:
// AMD define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好 a.doSomething() // 此處略去 100 行 b.doSomething() ... }); // CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() // 此處略去 100 行 var b = require('./b') // 依賴能夠就近書寫 b.doSomething() // ... });
(function (window, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { window.eventUtil = factory(); } })(this, function () { //module ... });
使用方式:
// 導入 import "/app"; import React from 「react」; import { Component } from 「react」; // 導出 export function multiply() {...}; export var year = 2018; export default ... ...
優勢:
缺點:
require使用與CommonJs規範,import使用於Es6模塊規範;因此二者的區別實質是兩種規範的區別;
CommonJS:
ES6模塊
最後:require/exports 是必要通用且必須的;由於事實上,目前你編寫的 import/export 最終都是編譯爲 require/exports 來執行的。