YDNJS
You Don't Know JS
英文原文
中文翻譯
六個系列
Up & Going Scope
Closures this
Object Prototypes Types
Grammar Async
Performance ES6
Beyond
二、Scope & Closures
中文
英文
Scope 是變數的作用域範圍,Closures 閉包是 Outer 函式執行結束後,Outer 的資料還能被 inner 函式讀取到的那些資料
Chapter 1: What is Scope?
變數是程式中最基本的東西,變數可以用來儲存、讀取數值。儲存與尋找這些變數的規則就是 Scope。接下來就是探討 Scope 的規則是怎麼被制定的
1.1 Compiler Theory
程式語言大致可分成靜態(編譯,先把程式碼全部轉成二進制碼再執行)與動態(直譯,直接一行一行執行 code)兩種。雖然JS 偏向後者,但我們先來看看前者的運作方式
傳統的靜態語言在執行前會經過三個編譯階段
Tokenizing/Lexing:分詞階段。抓出執行句中所有最小的意義單元。
var a = 2;
就有五個 tokensParsing:解析階段。把 tokens 建立成抽象的語法樹Abstract Syntax Tree。語法結構如下
var,VariableDeclaration
a,Identifier
=,AssignmentExpression
2,NumericLiteral
Code-Generation:把語法樹轉成執行碼的階段。變數 a 會被建立,a 會被賦予 2 這個值
實際上,JS 的執行方式是混合靜態和動態兩種的。詳細可參考以下連結
1.2 Understanding Scope
了解 scope 的方法,就是去觀察以下人物的溝通過程。有哪些人呢?
The Cast
Engine: responsible for start-to-finish compilation and execution of our JavaScript program.
Compiler: one of Engine's friends; handles all the dirty work of parsing and code-generation (see previous section).
Scope: another friend of Engine; collects and maintains a look-up list of all the declared identifiers (variables), and enforces a strict set of rules as to how these are accessible to currently executing code.
用 engine 的角度去想,就能理解 JS 的工作方式
Back & Forth
Compiler 在 var a = 2;
會做兩件事情
Compiler 在 Scope 找 a 有沒有被宣告過;沒有的話,就宣告一個 a
Compiler 產生執行碼給 Engine 用來處理
a=2
的賦值。Engine 會先在目前的 Scope 找 a,有找到的話它就會被賦予2、沒找到的話就往上找。都沒找到的話就 throw out an error
Compiler Speak
多了解一些術語
var b = 2;
var a = b;
LHS: 在等號左邊變數的尋找方式,先尋找變量在不在,在的話就賦予值;不在的話就建立變量,再賦予值
RHS: 在等號右邊變數的尋找方式,透過 scope 尋找值
Engine/Scope Conversation
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
Engine 和 scope 說了好長的一段話 ...
Quiz
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
LHS,
c = ..
RHS,
foo(..)
LHS,
a = 2
(a 是 parameter)LHS,
b = ..
RHS,
.. = a
RHS,
a + ..
RHS,
.. + b
1.3 Nested Scope
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
Engine 想找 b,一開始在 scope of foo 中沒找到,在 globe scope 才找到
Building on Metaphors
建築的隱喻。最底層是 current scope, 最頂層是 global scope。
1.4 Errors
為什麼要區分 LHS 和 RHS?因為他們的 look-up 方式不同。RHS 在 current scope 沒找到時,會繼續往上層找。沒找到的話會泡出 ReferenceError;LHS 在沒找到時,Scope 會自動創建一個並交給 Engine。ReferenceError 是 scope 解析失敗,TypeError 是 scope 解析成功,但做了一個沒被定義的動作
1.5 Review (TL;DR)
太長惹,幫你複習一下
Chapter 2: Lexical Scope
scope 是 Engine 用來 look-up variable 的規則。Scope 有兩種描述模型,一個是 Lexical scope (詞法作用域),一個是 dynamic scope (動態作用域)。
2.1 Lex-time
詞法分析時。
編譯器在編譯有三個階段,分詞、解析建立語法樹,以及產生執行碼。Lex-time 就是指第一階段,本章名稱 Lexical scope 也由此而來。
Lexical scope 是指詞法分析時被定義的作用域。nest 關係是嚴格的包含關係,而不是文氏圖那種部份香蕉
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar(b * 3);
}
foo( 2 ); // 2 4 12
Look-ups
scope 查找變數時,inner scope 優先於 outer scope。
2.2 Cheating Lexical
eval( str )
將字串填入,可以解析其中的宣告式 這種動態生成的盡量不要使用
function foo(str, a) {
eval( str ); // cheating!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3
with( obj )
傳入一個物件 可以直接修改 key 的 value
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 -- 哦,全局作用域被泄漏了!
Performance
如果使用 eval 和 with,會讓執行速度變慢 因為 lex-time 時,scope 無法完全掌握變數
Review (TL;DR)
Chapter 3: Function vs. Block Scope
除了函數,還有什麼東西能產生 scope 嗎
3.1 Scope From Functions
function foo(a) {
var b = 2;
// some code
function bar() {
// ...
}
// more code
var c = 3;
}
3.2 Hiding In Plain Scope
doSomethingElse 不會在全域中被其他人使用到
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
Collision Avoidance
隱藏變數的另一個好處,不會被其他相同的 identifier 干擾
function foo() {
function bar(a) {
i = 3; // changing the `i` in the enclosing scope's for-loop
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); // oops, infinite loop ahead!
}
}
foo();
Global "Namespaces"
若單純透過一個物件,命名許多函式想當 api 的用的話,可能容易跟其他 libraries 干擾
var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
doAnotherThing: function() {
// ...
}
};
Module Management
另一種避免衝突的方式是使用 Module 管理
3.3 Functions As Scopes
加入 foo() 可以避免 a 的衝突,但 foo 名字本身會污染當前的作用域(也就是全域)
var a = 2;
function foo() { // <-- insert this
var a = 3;
console.log( a ); // 3
} // <-- and this
foo(); // <-- and this
console.log( a ); // 2
但透過這個方式就不會污染到了(立即函式),而且能立即執行
var a = 2;
(function foo(){ // <-- insert this
var a = 3;
console.log( a ); // 3
})(); // <-- and this
console.log( a ); // 2
function... 函數宣告 declartion
(function foo(){...}) 函數表達 expression
Anonymous vs. Named
函數 expression 可以匿名,但函數 declaration 不能 但匿名函式不方便看,因此 inline 函數 expression 的習慣是不錯的選擇
setTimeout( function timeoutHandler(){ // <-- Look, I have a name!
console.log( "I waited 1 second!" );
}, 1000 );
Invoking Function Expressions Immediately
前面的括號將函式 declaration 改成函式 expression,後面的括號用來 invoke 函式
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
IIFE (immediately invoked function expression) 也能傳入參數
var a = 2;
(function IIFE( global ){
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
IIFE 还有另一种变种,它将事情的顺序倒了过来,要被执行的函数在调用和传递给它的参数 之后 给出。这种模式被用于 UMD(Universal Module Definition —— 统一模块定义)项目。
詭異的用法
// 原始
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
// 然後再用 window 當參數傳入 def 函式
var a = 2;
function def(global){
var a = 3;
console.log(a);
console.log(global.a)
}
(function IIFE(def){
def(window);
})(def);
3.4 Blocks As Scopes
除了 function 外,也有其他的 scope unit 靠北,for 迴圈的 i 會變成全域變數 orz
for (var i=0; i<10; i++) {
console.log( i );
}
console.log(i); // 10
With
雖然 With 盡量別用,但它是 block as scope 的栗子。被創見對象的作用玉只會出現在 with d的生命週其中
try/catch
catch 也是一種塊作用域
try {
undefined(); //用非法的操作強制産生一個異常!
}
catch (err) {
console.log( err ); // 好用!
}
console.log( err ); // ReferenceError: `err` not found
let
用 let 宣告的變數,作用域就是在當前 {} 裡面
var foo = true;
if (foo) {
let bar = foo * 2;
console.log( bar );
}
console.log( bar ); // ReferenceError
用 let 宣告的變數也不會被變數提昇
{
console.log( bar ); // ReferenceError!
let bar = 2;
}
Garbage Collection
塊作用域的程式碼執行玩後,記憶體好像會馬上釋放出來的樣子
// 這段程式碼的 object 可能佔用大量記憶體
function process(data) {
// do something interesting
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
// 但加上尖括號就不會了
function process(data) {
// do something interesting
}
// anything declared inside this block can go away after!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
let Loops
前面提到的好栗子
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
也可以解讀成這樣
{
let j;
for (j=0; j<10; j++) {
let i = j; // 每次叠代都重新綁定
console.log( i );
}
}
const
const 宣告的變數一樣是 block-scoped 變數。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // block-scoped to the containing `if`
a = 3; // just fine!
b = 4; // error!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
Chapter 4: Hoisting
不管是 function scope 或是 block scope,在 scope 裡面被宣告的變量都會依附在當前 scope 底下
以下會介紹當在 scope 中的不同位置宣告變量會有什麼差別
4.1 Chicken Or The Egg?
學 JS 的人有種思維傾向,即程式是由上而下依序執行的。那在以下的程式中,什麼會先呢?是 egg(declaration) 還是 assignment(chicken)
// 這會印出 undefined, 還是 2
a = 2;
var a;
console.log( a );
// 這會印出 undefined, 2, 還是 ReferenceError
console.log( a );
var a = 2;
4.2 The Compiler Strikes Again
JS 會先編譯程式(宣告,像是 var a, function foo()),再執行程式
//剛剛第一個程式等同於
var a;
a = 2;
console.log( a );
// 第二個程式等同於
var a;
console.log( a );
a = 2;
像這個是型別錯誤
// var foo;
foo(); // not ReferenceError, but TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
4.3 Functions First
當重複宣告時
最後被宣告的函式優先被變量提昇
函式變數宣告比一般宣告更先被提昇
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}
在條件句中,函式變數會優先被提昇(就不會鳥條件敘述了 XD)。但這種行為可能會被修改,所以盡量不要在括號中宣告函式
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log( "a" ); }
}
else {
function foo() { console.log( "b" ); }
}
Chapter 5: Scope Closure
閉包是最詭異的部份惹
5.1 Enlightenment
Understanding closures is like when Neo sees the Matrix for the first time.
閉包無所不在,直到你看見了他(作者也太詩意了)
5.2 Nitty Gritty
Chapter 5: Scope Closures Appendix A: Dynamic Scope Appendix B: Polyfilling Block Scope Appendix C: Lexical-this Appendix D: Thank You's!
this & Object Prototypes Types & Grammar Async & Performance ES6 & Beyond
Last updated
Was this helpful?