更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端
------ 如下是正文 ------git
JavaScript語言是「動態」或「解釋執行」語言,但事實上是一門編譯語言。但它不是提早編譯的,編譯結果也不能在分佈式系統中移植。github
傳統編譯語言流程中,程序在執行以前會經歷三個步驟,統稱爲「編譯」。面試
分詞/詞法分析(Tokenizing/Lexing)編程
將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊。數組
var a = 2;
複製代碼
上面這段程序會被分解成如下詞法單元:var、a、=、二、;。安全
空格是否會被當作詞法單元,取決於空格在這門語言中是否有意義。性能優化
解析/語法分析(Parsing)前端工程師
將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法結構的數。這個數被稱做抽象語法樹
(Abstract Syntax Tree, AST)。數據結構
var a = 2;
複製代碼
以上代碼的抽象語法樹以下所示:
代碼生成
將AST
轉換成可執行代碼的過程。過程與語言、目標平臺等相關。
簡單來講就是能夠經過某種方法將var a = 2;
的AST轉化爲一組機器指令。用來建立一個叫作a的變量(包括分配內存等),並將一個值存儲在a中。
var a = 2;
存在2個不一樣的聲明。
一、編譯器在編譯時處理(var a
):在當前做用域中聲明一個變量(若是以前沒有聲明過)。
st=>start: Start
e=>end: End
op1=>operation: 分解成詞法單元
op2=>operation: 解析成樹結構AST
cond=>condition: 當前做用域存在變量a?
op3=>operation: 忽略此聲明,繼續編譯
op4=>operation: 在當前做用域集合中聲明新變量a
op5=>operation: 生成代碼
st->op1->op2->cond
cond(yes)->op3->op5->e
cond(no)->op4->op5->e
複製代碼
二、引擎在運行時處理(a = 2
):在做用域中查找該變量,若是找到就對變量賦值。
st=>start: Start
e=>end: End
cond=>condition: 當前做用域存在變量a?
cond2=>condition: 全局做用域?
op1=>operation: 引擎使用這個變量a
op2=>operation: 引擎向上一級做用域查找變量a
op3=>operation: 引擎把2賦值給變量a
op4=>operation: 舉手示意,拋出異常
st->cond
cond(yes)->op1->op3->e
cond(no)->cond2(no)->op2(right)->cond
cond2(yes)->op4->e
複製代碼
L
和R
分別表明一個賦值操做的左側和右側,當變量出如今賦值操做的左側時進行LHS
查詢,出如今賦值操做的**非左側
**時進行RHS
查詢。
retrieve his source value
,即取到它的源值function foo(a) {
console.log( a ); // 2
}
foo(2);
複製代碼
上述代碼共有1處LHS查詢,3處RHS查詢。
LHS查詢有:
a = 2
中,在2
被當作參數傳遞給foo(…)
函數時,須要對參數a
進行LHS查詢RHS查詢有:
最後一行foo(...)
函數的調用須要對foo進行RHS查詢
console.log( a );
中對a
進行RHS查詢
console.log(...)
自己對console
對象進行RHS查詢
遍歷嵌套做用域鏈的規則:引擎從當前的執行做用域開始查找變量,若是找不到就向上一級繼續查找。當抵達最外層的全局做用域時,不管找到仍是沒有找到,查找過程都會中止。
ReferenceError
和做用域判別失敗相關,TypeError
表示做用域判別成功了,可是對結果的操做是非法或不合理的。
ReferenceError
異常。ReferenceError
異常TypeError
異常。(好比對非函數類型的值進行函數調用,或者引用null或undefined類型的值中的屬性)var a = 2
被分解成2個獨立的步驟。
var a
在其做用域中聲明新變量a = 2
會LHS查詢a,而後對其進行賦值詞法做用域是定義在詞法階段的做用域,是由寫代碼時將變量和塊做用域寫在哪裏來決定的,因此在詞法分析器處理代碼時會保持做用域不變。(不考慮欺騙詞法做用域狀況下)
做用域查找會在找到第一個匹配的標識符時中止。
遮蔽效應:在多層嵌套做用域中能夠定義同名的標識符,內部的標識符會「遮蔽」外部的標識符。
全局變量會自動變成全局對象的屬性,能夠間接的經過對全局對象屬性的引用來訪問。經過這種技術能夠訪問那些被同名變量所遮蔽的全局變量,可是非全局的變量若是被遮蔽了,不管如何都沒法被訪問到。
window.a
複製代碼
詞法做用域只由函數被聲明時所處的位置決定。
詞法做用域查找只會查找一級標識符,好比a、b、c。對於foo.bar.baz
,詞法做用域只會查找foo
標識符,找到以後,對象屬性訪問規則會分別接管對bar
和baz
屬性的訪問。
欺騙詞法做用域會致使性能降低。如下兩種方法不推薦使用
eval(..)
函數能夠接受一個字符串爲參數,並將其中的內容視爲好像在書寫時就存在於程序中這個位置的代碼。
function foo (str, a) {
eval( str ); // 欺騙!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
複製代碼
eval('var b = 3')
會被當作原本就在那裏同樣來處理。
eval(..)
中所執行的代碼包含一個或多個聲明,會在運行期修改書寫期的詞法做用域。上述代碼中在foo(..)
內部建立了一個變量b,並遮蔽了外部做用域中的同名變量。eval(..)
在運行時有本身的詞法做用域,其中的聲明沒法修改做用域。function foo (str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2;" );
複製代碼
setTimeout(..)
和setInterval(..)
的第一個參數能夠是字符串,會被解釋爲一段動態生成的函數代碼。已過期,不要使用new Function(..)
的最後一個參數能夠接受代碼字符串(前面的參數是新生成的函數的形參)。避免使用with
一般被當作重複引用同一個對象中的多個屬性的快捷方式,能夠不須要重複引用對象自己。
var obj = {
a: 1,
b: 2,
c: 3
};
// 單調乏味的重複「obj」
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 簡單的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
複製代碼
with能夠將一個沒有或有多個屬性的對象處理爲一個徹底隔離的詞法做用域,這個對象的屬性會被處理爲定義在這個做用域中的詞法標識符。
這個塊內部正常的var聲明並不會被限制在這個塊的做用域中,而是被添加到with所處的函數做用域中。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b : 3
}
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- 很差,a被泄露到全局做用域上了!
複製代碼
上面例子中,建立了o1
和o2
兩個對象。其中一個有a
屬性,另外一個沒有。在with(obj){..}
內部是一個LHS引用,並將2賦值給它。
o1
傳遞進去後,with聲明的做用域是o1
,a = 2
賦值操做找到o1.a
並將2賦值給它。o2
傳遞進去後,做用域o2
中並無a
屬性,所以進行正常的LHS標識符查找,o2的做用域、foo(..)
的做用域和全局做用域都沒有找到標識符a,所以當a = 2
執行時,自動建立了一個全局變量(非嚴格模式),因此o2.a
保持undefined。eval(..)
或with
,它只能簡單的假設關於標識符位置的判斷都是無效的。由於沒法在詞法分析階段明確知道eval(..)
會接收到什麼代碼,這些代碼會如何對做用域進行修改,也沒法知道傳遞給with
用來建立詞法做用域的對象的內容究竟是什麼。eval(..)
或with,全部的優化可能都是無心義的,最簡單的作法就是徹底不作任何優化。代碼運行起來必定會變得很是慢。詞法做用域意味着做用域是由書寫代碼時函數聲明的位置來決定的。
編譯的詞法分析階段基本可以知道所有標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中如何對它們進行查找。
有如下兩個機制能夠「欺騙」詞法做用域:
eval(..)
:對一段包含一個或多個聲明的」代碼「字符串進行演算,藉此來修改已經存在的詞法做用域(運行時)。with
:將一個對象的引用當作做用域來處理,將對象的屬性當作做用域中的標識符來處理,建立一個新的詞法做用域(運行時)。反作用是引擎沒法在編譯時對做用域查找進行優化。由於引擎只能謹慎地認爲這樣的優化是無效的,使用任何一個都將致使代碼運行變慢。不要使用它們
屬於這個函數的所有變量均可以在整個函數的範圍內使用及複用(事實上在嵌套的做用域中也可使用)。
function foo(a) {
var b = 2;
// 一些代碼
function bar() {
// ...
}
// 更多的代碼
var c = 3;
}
複製代碼
foo(..)
做用域中包含了標識符(變量、函數)a、b、c和bar。不管標識符聲明出如今做用域中的何處,這個標識符所表明的變量或函數都將附屬於所處的做用域。
全局做用域只包含一個標識符:foo
。
最小特權原則(最小受權或最小暴露原則):在軟件設計中,應該最小限度地暴露必要內容,而將其餘內容都」隱藏「起來,好比某個模塊或對象的API設計。
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
複製代碼
b
和doSomethingElse(..)
都沒法從外部被訪問,而只能被doSomething(..)
所控制,設計上將具體內容私有化了。
」隱藏「做用域中的變量和函數帶來的另外一個好處是能夠避免同名標識符之間的衝突。
function foo() {
function bar(a) {
i = 3; // 修改for循環所屬做用域中的i
console.log( a + i );
}
for (var i = 0; i < 10; i++) {
bar( i * 2 ); // 糟糕,無限循環了!
}
}
foo();
複製代碼
bar(..)
內部的賦值表達式i = 3
意外的覆蓋了聲明在foo(..)
內部for循環中的i。
解決方案:
var i = 3
。var j = 3
。規避變量衝突的典型例子:
全局命名空間
第三方庫會在全局做用域中聲明一個名字足夠獨特的變量,一般是一個對象,這個對象被用做庫的命名空間,全部須要暴露給外界的功能都會成爲這個對象(命名空間)的屬性,而不是將本身的標識符暴露在頂級的詞法做用域中。
模塊管理
任何庫無需將標識符加入到全局做用域中,而是經過依賴管理器的機制將庫的標識符顯示的導入到另一個特定的做用域中。
var a = 2;
function foo() { // <-- 添加這一行
var a = 3;
console.log( a ); // 3
} // <-- 以及這一行
foo(); // <-- 以及這一行
console.log( a ); // 2
複製代碼
上述函數做用域雖然能夠將內部的變量和函數定義」隱藏「起來,可是會致使如下2個額外問題。
foo()
,意味着foo
這個名稱自己」污染「了所在的做用域。foo()
調用這個函數才能運行其中的代碼。解決方案:
var a = 2;
(function foo(){ // <-- 添加這一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及這一行
console.log( a ); // 2
複製代碼
上述代碼包裝函數的聲明以(function...
開始,函數會被當作函數表達式而不是一個標準的函數聲明來處理。
function
是聲明中的第一個詞foo
被綁定在所在做用域中,能夠直接經過foo()
來調用它。foo
被綁定在函數表達式自身的函數中,而不是所在的做用域。(function foo(){ .. }
中foo
只能在..
所表明的位置中被訪問,外部做用域不行。foo
變量名被隱藏在自身中意味着不會非必要地污染外部做用域。setTimeout( function() {
console.log("I wait 1 second!");
}, 1000 );
複製代碼
上述是匿名函數表達式,由於function()..
沒有名稱標識符。
函數表達式能夠匿名,但函數聲明不能夠省略函數名。
匿名函數表達式有如下缺點:
arguments.callee
引用
解決方案:
行內函數表達式能夠解決上述問題,始終給函數表達式命名是一個最佳實踐。
setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
console.log( "I waited 1 second!" );
}, 1000 );
複製代碼
當即執行函數表達式(IIFE,Immediately Invoked Function Expression)
匿名/具名函數表達式
第一個( )將函數變成表達式,第二個( )執行了這個函數
var a = 2;
(function IIFE() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
複製代碼
改進型(function(){ .. }())
用來調用的( )被移進了用來包裝的( )中。
當作函數調用並傳遞參數進去
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
複製代碼
解決undefined
標識符的默認值被錯誤覆蓋致使的異常
將一個參數命名爲undefined
,可是在對應的位置不傳入任何值,這樣就能夠保證在代碼塊中undefined
標識符的值真的是undefined
。
undefined = true;
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log("Undefined is safe here!");
}
})();
複製代碼
倒置代碼的運行順序,將須要運行的函數放在第二位,在IIFE執行以後當作參數傳遞進去
函數表達式def
定義在片斷的第二部分,而後當作參數(這個參數也叫作def)被傳遞進IIFE函數定義的第一部分中。最後,參數def(也就是傳遞進去的函數)被調用,並將window傳入當作global參數的值。
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
複製代碼
表面上看JavaScript並無塊做用域的相關功能,除非更加深刻了解(with、try/catch 、let、const)。
for (var i = 0; i < 10; i++) {
console.log( i );
}
複製代碼
上述代碼中i
會被綁定在外部做用域(函數或全局)中。
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something( bar );
console.log( bar );
}
複製代碼
上述代碼中,當使用var聲明變量時,它寫在哪裏都是同樣的,由於它們最終都會屬於外部做用域。
塊做用域的一種形式,用with
從對象中建立出的做用域僅在**with
聲明中**而非外部做用域中有效。
ES3規範中規定try/catch的catch分句會建立一個塊做用域,其中聲明的變量僅在catch中有效。
try {
undefined(); // 執行一個非法操做來強制製造一個異常
}
catch (err) {
console.log( err ); // 可以正常執行!
}
console.log( err ); // ReferenceError: err not found
複製代碼
當同一個做用域中的兩個或多個catch分句用一樣的標識符名稱聲明錯誤變量時,不少靜態檢查工具仍是會發出警告,實際上這並非重複定義,由於全部變量都會安全地限制在塊做用域內部。
ES6引入了let
關鍵字,能夠將變量綁定到所在的任意做用域中(一般是{ .. }
內部),即let
爲其聲明的變量隱式地劫持了所在的塊做用域。
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
複製代碼
存在的問題
用let
將變量附加在一個已經存在的的塊做用域上的行爲是隱式的,若是習慣性的移動這些塊或者將其包含在其餘的塊中,可能會致使代碼混亂。
解決方案
爲塊做用域顯示地建立塊。顯式的代碼優於隱式或一些精巧但不清晰的代碼。
var foo = true;
if (foo) {
{ // <-- 顯式的塊
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
複製代碼
在if聲明內部顯式地建立了一個塊,若是須要對其進行重構,整個塊均可以被方便地移動而不會對外部if聲明的位置和語義產生任何影響。
在let進行的聲明不會在塊做用域中進行提高
console.log( bar ); // ReferenceError
let bar = 2;
複製代碼
一、垃圾收集
function process(data) {
// 在這裏作點有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase*/false );
複製代碼
click
函數的點擊回調並不須要someReallyBigData
。理論上當process(..)
執行後,在內存中佔用大量空間的數據結構就能夠被垃圾回收了。可是,因爲click
函數造成了一個覆蓋整個做用域的閉包,JS引擎極有可能依然保存着這個結構(取決於具體實現)。
二、let循環
for (let i = 0; i < 10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
複製代碼
for循環頭部的let不只將i綁定到了for循環的塊中,事實上它將其從新綁定到了循環的每個迭代中,確保使用上一個循環迭代結束時的值從新進行賦值。
{
let j;
for (j = 0; j < 10; j++) {
let i = j; // 每一個迭代從新綁定!
console.log( i );
}
}
複製代碼
ES6引用了const
,能夠建立塊做用域變量,但其值是固定的(常量)
var foo = true;
if(foo) {
var a = 2;
const b = 3; // 包含在if中的塊做用域常量
a = 3; // 正常!
b = 4; // 錯誤!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
複製代碼
var a = 2;
會被當作兩個聲明,var a;
和a = 2;
,第一個聲明在編譯階段進行,第二個賦值聲明會被留在原地等待執行階段。a = 2;
var a;
console.log( a ); // 2
---------------------------------------
// 實際按以下形式進行處理
var a; // 編譯階段
a = 2; // 執行階段
console.log( a ); // 2
複製代碼
console.log( a ); // undefinde
var a = 2;
---------------------------------------
// 實際按以下形式進行處理
var a; // 編譯
console.log( a ); // undefinde
a = 2; // 執行
複製代碼
function foo() {
var a;
console.log( a ); // undefinde
a = 2;
}
foo();
複製代碼
foo(); // 不是ReferenceError,而是TypeError!
var foo = function bar() {
// ...
};
複製代碼
上面這段程序中,變量標識符foo()
被提高並分配給所在做用域,所以foo()
不會致使ReferenceError。此時foo
並無賦值(若是它是一個函數聲明而不是函數表達式,那麼就會賦值),foo()
因爲對undefined
值進行函數調用而致使非法操做,所以拋出TypeError
異常。
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
---------------------------------------
// 實際按以下形式進行處理
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
};
複製代碼
foo(); // 1
var foo;
function foo() {
console.log( 1 );
};
foo = function() {
console.log( 2 );
};
---------------------------------------
// 實際按以下形式進行處理
function foo() { // 函數提高是總體提高,聲明 + 賦值
console.log( 1 );
};
foo(); // 1
foo = function() {
console.log( 2 );
};
複製代碼
var foo
儘管出如今function foo()...
的聲明以前,但它是重複的聲明,且函數聲明會被提高到普通變量以前,所以被忽略foo(); // 3
function foo() {
console.log( 1 );
};
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
};
複製代碼
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log( "a" ); };
}
else {
function foo() { console.log( "b" ); };
}
複製代碼
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 ---- 這就是閉包的效果
複製代碼
bar()
在本身定義的詞法做用域之外的地方執行。
bar()
擁有覆蓋foo()
內部做用域的閉包,使得該做用域可以一直存活,以供bar()
在以後任什麼時候間進行引用,不會被垃圾回收器回收
bar()
持有對foo()
內部做用域的引用,這個引用就叫作閉包。// 對函數類型的值進行傳遞
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 這就是閉包
}
foo();
複製代碼
baz
傳遞給bar
,當調用這個內部函數時(如今叫作fn
),它覆蓋的foo()
內部做用域的閉包就造成了,由於它可以訪問a。// 間接的傳遞函數
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 將baz分配給全局變量
}
function bar() {
fn(); // 這就是閉包
}
foo();
bar(); // 2
複製代碼
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
複製代碼
setTimeout(..)
持有對一個參數的引用,這裏參數叫作timer,引擎會調用這個函數,而詞法做用域在這個過程當中保持完整。這就是閉包// 典型的閉包例子:IIFE
var a = 2;
(function IIFE() {
console.log( a );
})();
複製代碼
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log( i );
}, i * 1000 );
}
//輸入五次6
複製代碼
i
的最終值。i
嘗試方案1:使用IIFE增長更多的閉包做用域
for (var i = 1; i <= 5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i * 1000 );
})();
}
//失敗,由於IIFE做用域是空的,須要包含一點實質內容纔可使用
複製代碼
嘗試方案2:IIFE增長變量
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
})();
}
// 正常工做
複製代碼
嘗試方案3:改進型,將i
做爲參數傳遞給IIFE函數
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
})( i );
}
// 正常工做
複製代碼
let
能夠用來劫持塊做用域,而且在這個塊做用域中聲明一個變量。for (var i = 1; i <= 5; i++) {
let j = i; // 閉包的塊做用域!
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
}
// 正常工做
複製代碼
for
循環頭部的let
聲明會有一個特殊的行爲。變量在循環過程當中不止被聲明一次,每次迭代都會聲明。隨後的每一個迭代都會使用上一個迭代結束時的值來初始化這個變量。上面這句話參照3.4.3–---2.let循環,即如下
{
let j;
for (j = 0; j < 10; j++) {
let i = j; // 每一個迭代從新綁定!
console.log( i );
}
}
複製代碼
循環改進:
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log( i );
}, i * 1000 );
}
// 正常工做
複製代碼
模塊模式須要具有兩個必要條件:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! ") );
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
// 一、必須經過調用CoolModule()來建立一個模塊實例
// 二、CoolModule()返回一個對象字面量語法{ key: value, ... }表示的對象,對象中含有對內部函數而不是內部數據變量的引用。內部數據變量保持隱藏且私有的狀態。
複製代碼
當即調用這個函數並將返回值直接賦予給單例的模塊標識符foo。
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! ") );
}
return {
doSomething: doSomething,
doAnother: doAnother
}
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
複製代碼
大多數模塊依賴加載器/管理器本質上是將這種模塊定義封裝進一個友好的API。
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++ ) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps ); // 核心,爲了模塊的定義引用了包裝函數(能夠傳入任何依賴),而且將返回值(模塊的API),儲存在一個根據名字來管理的模塊列表中。
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
複製代碼
使用上面的函數來定義模塊:
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduct: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
bar.hello( "hippo" );
) // Let me introduct: hippo
foo.awesome(); // LET ME INTRODUCT: HIPPO
複製代碼
在經過模塊系統進行加載時,ES6會將文件當作獨立的模塊來處理。每一個模塊均可以導入其餘模塊或特定的API成員,一樣能夠導出本身的API成員。
ES6模塊沒有「行內」格式,必須被定義在獨立的文件中(一個文件一個模塊)
// bar.js
function hello(who) {
return "Let me introduct: " + who;
}
export hello;
// foo.js
// 僅從「bar」模塊導入hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello( hungry ).toUpperCase();
);
}
export awesome;
// baz.js
// 導入完整的「foo」和」bar「模塊
module foo from "foo";
module bar from "bar";
console.log(
bar.hello( "rhino")
); // Let me introduct: rhino
foo.awesome(); // LET ME INTRODUCT: HIPPO
複製代碼
import
:將一個模塊中的一個或多個API導入到當前做用域中,並分別綁定在一個變量上module
:將整個模塊的API導入並綁定到一個變量上。export
:將當前模塊的一個標識符(變量、函數)導出爲公共APIthis
機制某種程度上很像動態做用域。// 詞法做用域,關注函數在何處聲明,a經過RHS引用到了全局做用域中的a
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
-----------------------------
// 動態做用域,關注函數從何處調用,當foo()沒法找到a的變量引用時,會順着調用棧在調用foo()的地方查找a
function foo() {
console.log( a ); // 3(不是2!)
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
複製代碼
ES3開始,JavaScript中就有了塊做用域,包括with和catch分句。
// ES6環境
{
let a = 2;
console.log( a ); // 2
}
console.log( a ); // ReferenceError
複製代碼
上述代碼在ES6環境中能夠正常工做,可是在ES6以前的環境中如何實現呢?
答案是使用catch分句,這是ES6中大部分功能遷移的首選方式。
try {
throw 2;
} catch (a) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
複製代碼
// 代碼轉換成以下形式
{
try {
throw undefined;
} catch (a) {
a = 2;
console.log( a ); // 2
}
}
console.log( a ); // ReferenceError
複製代碼
let
聲明會建立一個顯式的做用域並與其進行綁定,而不是隱式地劫持一個已經存在的做用域(對比前面的let
定義)。
let (a = 2) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
複製代碼
存在的問題:
let
聲明不包含在ES6中,Traceur編譯器也不接受這種代碼
/*let*/ { let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
複製代碼
{
let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
複製代碼
進階系列文章彙總以下,內有優質前端資料,以爲不錯點個star。
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!