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 偏向後者,但我們先來看看前者的運作方式

傳統的靜態語言在執行前會經過三個編譯階段

  1. Tokenizing/Lexing:分詞階段。抓出執行句中所有最小的意義單元。var a = 2; 就有五個 tokens

  2. Parsing:解析階段。把 tokens 建立成抽象的語法樹Abstract Syntax Tree。語法結構如下

    • var,VariableDeclaration

      • a,Identifier

      • =,AssignmentExpression

        • 2,NumericLiteral

  3. 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; 會做兩件事情

  1. Compiler 在 Scope 找 a 有沒有被宣告過;沒有的話,就宣告一個 a

  2. Compiler 產生執行碼給 Engine 用來處理 a=2 的賦值。Engine 會先在目前的 Scope 找 a,有找到的話它就會被賦予2、沒找到的話就往上找。都沒找到的話就 throw out an error

Compiler Speak

多了解一些術語

var b = 2;
var a = b;

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 );
  1. LHS,c = ..

  2. RHS,foo(..)

  3. LHS,a = 2 (a 是 parameter)

  4. LHS,b = ..

  5. RHS,.. = a

  6. RHS,a + ..

  7. 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