“Give me a chance to know you. ”
更多內容: 移步這里
1. 作用域
1.1. 編譯原理
盡管通常將 JavaScript 歸類為“動態” 或“解釋執行” 語言, 但事實上它是一門編譯語言。
程序中的一段源代碼在執行之前會經歷三個步驟, 統稱為“編譯”
- 分詞/詞法分析(Tokenizing/Lexing)
- 這個過程會將由字符組成的字符串分解成(對編程語言來說) 有意義的代碼塊, 這些代
碼塊被稱為詞法單元(token)。 例如, 考慮程序 var a = 2;。 這段程序通常會被分解成
為下面這些詞法單元: var、 a、 =、 2 、 ;。
- 這個過程會將由字符組成的字符串分解成(對編程語言來說) 有意義的代碼塊, 這些代
- 解析/語法分析(Parsing)
- 這個過程是將詞法單元流(數組) 轉換成一個由元素逐級嵌套所組成的代表了程序語法
結構的樹。 這個樹被稱為“抽象語法樹”(Abstract Syntax Tree, AST)。
- 這個過程是將詞法單元流(數組) 轉換成一個由元素逐級嵌套所組成的代表了程序語法
- 代碼生成
- 將 AST 轉換為可執行代碼的過程稱被稱為代碼生成。
1.2. 作用域嵌套
當一個塊或函數嵌套在另一個塊或函數中時, 就發生了作用域的嵌套。 因此, 在當前作用
域中無法找到某個變量時, 引擎就會在外層嵌套的作用域中繼續查找, 直到找到該變量,
或抵達最外層的作用域(也就是全局作用域) 為止。
將作用域處理的過程可視化,如下面的建筑:
作用域是一套規則, 用于確定在何處以及如何查找變量(標識符)。
2. 詞法作用域
作用域共有兩種主要的工作模型:
- 詞法作用域(重點討論)
- 動態作用域(如bash腳本,perl中的一些模式)
2.1. 詞法階段
詞法化的過程會對源代碼中的字符進行檢查,如果是有狀態的解析過程,還會賦予單詞語義——名稱來歷
詞法作用域是由你在寫代碼時將變量和塊作用域寫在哪里來決定的
如:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
可以將以上代碼想象成幾個逐級包含的氣泡
① 包含著整個全局作用域, 其中只有一個標識符: foo。
② 包含著 foo 所創建的作用域, 其中有三個標識符: a、 bar 和 b。
③ 包含著 bar 所創建的作用域, 其中只有一個標識符: c。
作用域氣泡由其對應的作用域塊代碼寫在哪里決定, 它們是逐級包含的。
查找
作用域氣泡的結構和互相之間的位置關系給引擎提供了足夠的位置信息,作用域查找會在找到第一個匹配的標識符時停止
全局變量會自動成為全局對象(比如瀏覽器中的window對象)的屬性,因此可以不直接通過全局對象的詞法名稱,而是間接地通過對全局對象屬性的引用來對其進行訪問。
window.a通過這種技術可以訪問那些被同名變量所遮蔽的全局變量。 但非全局的變量
如果被遮蔽了, 無論如何都無法被訪問到。
無論函數在哪里被調用,也無論它如何被調用,它的詞法作用域都只由函數被聲明時所處的位置決定。
詞法作用域查找只會查找一級標識符,比如a、b和c。如果代碼中引用了foo.bar.baz,詞法作用域查找只會試圖查找 foo 標識符,找到這個變量后, 對象屬性訪問規則會分別接管對 bar 和 baz 屬性的訪問
2.2. 欺騙詞法
JavaScript 中有兩個機制可以“欺騙”詞法作用域:eval(..)和with。前者可以對一段包
含一個或多個聲明的“代碼”字符串進行演算,并借此來修改已經存在的詞法作用域(在
運行時)。后者本質上是通過將一個對象的引用當作作用域來處理,將對象的屬性當作作
用域中的標識符來處理,從而創建了一個新的詞法作用域(同樣是在運行時)。
3. 函數作用域和塊作用域
究竟是什么生成了一個新的氣泡?只有函數會生成新的氣泡嗎?JavaScript中的其他結構能生成作用域氣泡嗎?
3.1. 隱藏內部實現
3.1.1. 最小授權|最小暴露原則
指在軟件設計中,應該最小限度地暴露必要內容,而將其他內容都“隱藏”起來,比如某個模塊或對象的API設計。——可延伸到如何選擇作用域來包含變量和函數
如:
function doSomething(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething(2); // 15
/*在這個代碼片段中, 變量 b 和函數 doSomethingElse(..) 應該是 doSomething(..) 內部具體實現的“私有” 內容。 給予外部作用域對 b 和 doSomethingElse(..) 的“訪問權限” 不僅沒有必要且危險*/
// 更合理
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
d
oSomething(2); // 15
//設計上將具體內容私有化
3.1.2. 規避沖突
全局命名空間
用變量作為庫的命名空間
所有需要暴露給外界的功能都會成為這個對象(命名空間)的屬性,而不是將自己的標識符暴漏在頂級的詞法作用域中如:
var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { // ... }, doAnotherThing: function() { // ...26 } };
3.2. 函數作用域
區分函數聲明和表達式最簡單的方法是看function關鍵字出現在聲明中的位置(不僅僅是一行代碼,而是整個聲明中的位置)。如果function是聲明中的第一個詞,那么就是一個函數聲明,否則就是一個函數表達式。
1. 匿名和具名
始終給函數表達式命名是一個最佳實踐
setTimeout( function timeoutHandler() { // 快看, 我有名字了
console.log( "I waited 1 second!" );
}, 1000 );
2. 立即執行函數表達式
/*第一種*/
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
/* 第二種形式*/
(function foo(){ .. })()
/*進階*/
/*將 window 對象的引用傳遞進去, 但將參數命名為 global*/
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
/*IIFE 還有一種變化的用途是倒置代碼的運行順序, 將需要運行的函數放在第二位, 在 IIFE執行之后當作參數傳遞進去。*/
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
函數表達式 def 定義在片段的第二部分, 然后當作參數(這個參數也叫作 def) 被傳遞進
IIFE 函數定義的第一部分中。 最后, 參數 def(也就是傳遞進去的函數) 被調用, 并將
window 傳入當作 global 參數的值。
函數不是唯一的作用域單元。塊作用域指的是變量和函數不僅可以屬于所處的作用域,也可以屬于某個代碼塊(通常指 { .. } 內部)。
4. 變量提升
先有蛋(聲明) 后有雞(賦值)。
JavaScript 引擎將 var a和 a = 2 當作兩個單獨的聲明, 第一個是編譯階段的任務, 而第二個則是執行階段的任務。無論作用域中的聲明出現在什么地方,都將在代碼本身被執行前首先進行處理。可以將這個過程形象地想象成所有的聲明(變量和函數) 都會被“移動” 到各自作用域的最頂端, 這個過程被稱為提升
只有聲明本身會被提升, 而賦值或其他運行邏輯會留在原地。
4.1. 函數優先
函數聲明和變量聲明都會被提升。 但是一個值得注意的細節(這個細節可以出現在有多個
“重復” 聲明的代碼中) 是 函數會首先被提升, 然后才是變量。
5. 作用域閉包
閉包的創建和使用在你的代碼中隨處可見。你缺少的是根據你自己的意愿來識別、擁抱和影響閉包的思維環境
5.1 什么是閉包
當函數可以記住并訪問所在的詞法作用域,即使函數是在當前詞法作用域之外執行,這時就產生了閉包。
function foo() {
var a = 2;
function bar() {
console.log(a); // 2
}
bar();
}
foo();
//基于詞法作用域的查找規則, 函數bar() 可以訪問外部作用域中的變量 a
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友, 這就是閉包的效果。
bar() 顯然可以被正常執行。 但是在這個例子中, 它在自己定義的詞法作用域以外的地方執行
foo() 執行后垃圾回收器用來釋放不再使用的內存空間,閉包的“神奇”之處正是可以阻止這件事情的發生。 事實上內部作用域依然存在,bar() 依然持有對該作用域的引用, 而 這個引用就叫作閉包。
常見的閉包:
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
//timer 具有涵蓋 wait(..) 作用域的閉包, 因此還保有對變量 message 的引用。
//wait(..) 執行 1000 毫秒后, 它的內部作用域并不會消失, timer 函數依然保有 wait(..)作用域的閉包。
只要使用了回調函數, 實際上就是在使用閉包!
5.2. 循環和閉包
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i); //6
}, i * 1000);
}
/*所有的回調函數依然是在循環結束后才會被執行, 因此會每次輸出一個 6 出來。*/
原因
缺陷是我們試圖假設循環中的每個迭代在運行時都會給自己“捕獲” 一個 i 的副本。
但是根據作用域的工作原理, 實際情況是盡管循環中的五個函數是在各個迭代中分別定義的,
但是它們都被封閉在一個共享的全局作用域中, 因此實際上只有一個 i。
我們需要更多的閉包作用域, 特別是在循環的過程中每個迭代都需要一個閉包作用域
//它需要有自己的變量, 用來在每個迭代中儲存 i 的值:
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})();
}
//使用let
//本質上這是將一個塊轉換成一個可以被關閉的作用域。
for (var i = 1; i <= 5; i++) {
let j = i; // 是的, 閉包的塊作用域!
setTimeout(function timer() {
console.log(j);
}, j * 1000);
}
//塊作用域和閉包聯手
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
5.3. 模塊
5.3.1. 模塊方式演進
模塊有兩個主要特征:
- 為創建內部作用域而調用了一個包裝函數;
包裝函數的返回值必須至少包括一個對內部函數的引用,這樣就會創建涵蓋整個包裝函數內部作用域的閉包。
```javascript
//exa1:
//兩個私有數據變量 something和 another, 以及 doSomething() 和 doAnother()
//它們的詞法作用域(而這就是閉包) 也就是 foo() 的內部作用域。
function foo() {
var something = "cool";
var another = [1, 2, 3];function doSomething() {
console.log(something);
}function doAnother() {
console.log(another.join(" ! "));
}
}
//模塊
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
//1. CoolModule() 只是一個函數, 必須要通過調用它來創建一個模塊實例。 如果不執行外部函數, 內部作用域和閉包都無法被創建。
//2. CoolModule() 返回一個用對象字面量語法 { key: value, ... } 來表示的對象。 這個返回的對象中含有對內部函數而不是內部數據變量的引用
//改進
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 foo = (function CoolModule(id) {
function change() {
// 修改公共 API
publicAPI.identify = identify2;
}
function identify1() {
console.log(id);
}
function identify2() {
console.log(id.toUpperCase());
}
var publicAPI = {
change: change,
identify: identify1
};
return publicAPI;
})("foo module");
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
```
當通過返回一個含有屬性引用的對象的方式來將函數傳遞到詞法作用域外部時,我們已經創造了可以觀察和實踐閉包的條件。
因此 一個從函數調用所返回的,只有數據屬性而沒有閉包函數的對象并不是真正的模塊
5.3.2. ES6的模塊
ES6 的模塊沒有“行內” 格式, 必須被定義在獨立的文件中(一個文件一個模塊),可
以在導入模塊時異步地加載模塊文件。
//bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello
//foo.js
// 僅從 "bar" 模塊導入 hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello(hungry).toUpperCase()
);
}
import可以將一個模塊中的一個或多個API導入到當前作用域中,并分別綁定在一個變量上(在我們的例子里是hello)。module會將整個模塊的API導入并綁定到一個變量上(在我們的例子里是foo)。export會將當前模塊的一個標識符(變量、函數)導出為公共API。這些操作可以在模塊定義中根據需要使用任意多次。
5.3.3. 動態作用域
動態作用域并不關心函數和作用域是如何聲明以及在何處聲明的,只關心它們從何處調用。換句話說,作用域鏈是基于調用棧的,而不是代碼中的作用域嵌套。
function foo() {
console.log(a); //2 —— 如果是動態作用域3
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
JavaScript并不具有動態作用域。它只有詞法作用域
主要區別:
詞法作用域是在寫代碼或者說定義時確定的,而動態作用域是在運行時確定的。(this也是!)詞法作用域關注函數在何處聲明,而動態作用域關注函數從何處調用
6. this詞法
6.1. _self
常見this綁定丟失解決方案: 'var _self = this'
6.2. 箭頭函數
ES6 中的箭頭函數引入了一個叫作 this 詞法的行為
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout(() => { // 箭頭函數是什么鬼東西?
this.count++;
console.log("awesome?");
}, 100);
}
}
};
obj.cool(); // 很酷吧 ?
簡單來說,箭頭函數在涉及this綁定時的行為和普通函數的行為完全不一致。它放棄了所有普通this綁定的規則,取而代之的是用當前的詞法作用域覆蓋了this本來的值
這個代碼片段中的箭頭函數只是“繼承”了cool()函數的this綁定(因此調用它并不會出錯)。
6.3. bind
//正確使用和包含 this 機制
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout(function timer() {
this.count++; // this 是安全的
// 因為 bind(..)
console.log("more awesome");
}.bind(this), 100); // look, bind()!
}
}
};
obj.cool(); // 更酷了。
文章列表