0%

最近開始幫公司重新設計官方網站,同時也負責開發,觀察近期網站 UI 趨勢,Micro Interactions & Animated Illustrations 成為現代網頁的特色,而且更能夠吸引瀏覽者的眼球,於是在規劃網站的時候打算使用由 Airbnb 開源的 lottie-web Library 來產生 svg 動畫, svg 不但可以節省網路資源也比 gif 看起來更順暢。

使用 Adobe illustrator 和 Adobe Effect 製作動畫

首先我用 illustrator 繪製好基礎向量素材,再 import 進 After Effect 去製作動畫效果,並不斷調整讓它看起來更順暢。
將 illustrator 檔案 import 進去 After Effect 的時候,有幾種方式,可以參考這個連結:A Guide to Importing Adobe Illustrator Files into After Effects
但因為 Bodymovin 無法轉出 AI (.ai) 的圖層,會變成 missing image and vector ,所以必須將 AI (.ai) 圖層轉換成 shape layer,方法是選取要轉換的圖層,然後到 Layer > Create > Create Shapes from Vector Layer,他就會產生一個新的 shape layer。
-Learn how to convert Illustrator layers into shape layers in a composition.

BTW 如果不使用 illustrator 畫,直接在 AE 裡面製作 shape layer 就沒有這個困擾了。

另外,版本與系統的相符也是一個需要注意的地方,這次在使用 AE 之前先花了一段時間升級 mac 系統 和重新安裝 AE XD

目前在 After Effect 中使用 Bodymovin 時需要注意 After Effect 版本須在 15.0 以上,如果使用 mac 系統的話目前至少 要 macOS versions 10.12 (Sierra), 10.13 (High Sierra), 10.14 (Mojave)等版本之系統才支援,並需要在 After Effect 的 Preference 設定中打勾這項:Allow Scripts To Write Files And Access Network option 否則 Bodymovin 無法運作。

To allow scripts to write files and communicate over a network, choose Edit > Preferences > General (Windows) or After Effects > Preferences > General (Mac OS), and select the Allow Scripts To Write Files And Access Network option.

在 After Effect 2019 以後的版本,該選項被移到 scrtiping & expressions 的 pannel 裡了,可以參考以下連結說明:
The following existing preferences related to scripting and expressions have been moved from the General preferences panel to the new Scripting & Expressions panel.
-adobe support community

透過 plug-in ‘Bodymovin’ 輸出動畫 JSON 檔,在專案中引用 lottie library 使動畫運行在 web 頁面上

把動畫修改的差不多後就可以透過 Bodymovin 來輸出 JSON 檔案,來運用到專案裡實際跑在 Browser 中看看,我用以下方法將 Bodymovin 轉出的 JSON 檔運用到專案中:

  1. 開啟 Bodymovin (Navigate to Windows -> Extensions -> Bodymovin),選好 export 設定與指定路徑資料夾
    關於如何設定 Bodymovin:How to Use Lottie ? | After Effects Plug-in ‘Bodymovin’

  2. 可以打開 demo.html 在瀏覽器中先看看 demo 是否沒有問題

  3. 在專案資料夾路徑底下執行: npm install lottie-web 安裝運行動畫的 js script

  4. 在動畫運行的 js 檔中(ex: app.js)import lottie from 'lottie-web'

  5. 並將 json 檔也 import 進來: import animationData from '../lottie/data.json'

  6. call lottie.loadAnimation() to start an animation
    for example:

    1
    2
    3
    4
    5
    6
    7
    let animObj = lottie.loadAnimation({
    container: this.animBox, // the dom element that will contain the animation
    renderer: 'svg',
    loop: true,
    autoplay: true,
    animationData: animationData //the path to the animation json
    })
  7. 在 react JSX 的 html 描述部分用 react 的 ref 屬性指定要產生動畫的 dom element:

    1
    <div style={{width: 300, margin: '0 auto'}} ref={ref => this.animBox = ref}></div>

關於除了用 npm install 來安裝 package 的使用方法,airbnb 的官方 github 有詳細的文件說明其他使用方法,例如 CDN 或透過 Bodymovin getPlayer 來取得 js script 後再引用到專案中,可以參考以下連結:
lottie github documentation

Reference

Lottie animations for Web 2019
How to export an animation with Bodymovin 2017

什麼是同步、非同步?

同步(synchronous)的意思是指一次只能進行一件事、一件任務,非同步或稱異步(asynchronous)的意思則是不用等上一任務完成再執行。只看名字容易搞混,但如果想成是同一個步道和不同步道,會比較好理解。同步道因為只有一個步道所以一次只能執行一項任務,而不同步道可以多個任務一起執行。

JavaScript 如何實現非同步?

JavaScript 這種編程語言,我們稱之為腳本(script)、直譯式語言,可以寫在 HTML 中,在頁面加載的時候會自動執行。只要瀏覽器或伺服器有搭載 JavaScript Engine 在環境中,就可以執行 JavaScript。

Scripts are provided and executed as a plain text. They don’t meed a special preparation or a compilation to run. In this aspect, JavaScript is very different from another language called JAVA. JAVA needs to be compiled into machine code.
在使用 C、C++、JAVA 之類傳統語言,執行前需要先編譯,這可以為程式碼產生機器的有效表達方式,通常可以優化執行時期的效能。早期的 JavaScript 因為直譯式語言的命令稿語言設計而在性能上表現不佳,但因其對 web 的發展重要性日益增長,許多企業資源和優秀人才的投入,現在既可收命令稿語言的便利性又可享有編譯式語言的性能。

直譯式和編譯式程式語言的差別在於:直譯式語言需要經過 直譯器 interpreter 逐行轉換,且它是在執行時才被直譯成執行碼,效能一部分取決於直譯器的速度,直譯式語言多為動態語言(dynamic language),具有靈活的型別處理,在執行時才動態生成等彈性。直譯式語言仰賴一個執行環境(execution context)語言可用的功能由這個執行環境提供。
編譯式語言經由 編譯器 compiler 轉換成目的碼(object code)再由連結器(linker)轉換成可執行的二進位碼(byte code)。編譯式語言多為靜態語言(static langauge)有事先定義的型別、型別檢查及高效能執行速度等特性。

由 Google 開發(2008)的 open source Engine:V8 是實現 JavaScript 在瀏覽器環境非同步執行的最佳功臣;在 V8 之前的 Engine 都太慢了,Google 為了讓 JavaScript 能夠在瀏覽器上跑得更快,開發了以 C++ 語言寫成的 V8。V8 將 JavaScript 在執行前編譯成了機器碼(machine code,是電腦的CPU可直接解讀的資料);而位元組碼(bytecode,是為了實現特定軟體運行、軟體環境、與硬體環境無關)或是解釋執行它,並使用了 inline-caching 行內快取等方法,提升效能,使其速度能夠媲美二進制編譯。

每個瀏覽器(Google Chrome, FireFox, IE, Safari)都有自己的 JavaScript Engine implementation and all of them have a JavaScript runtime that provide web APIs. 這些 web APIs 是種應用程式,負責許多瀏覽器運作所需的操作,包括 send HTTP Request, listen to DOM events, delay execution by using setTimeout and setInterval, caching, database storage…etc. 也可以儲存資料、暫存在瀏覽器。其他 Engine 還有 Spider Monkey(used by FireFox)以及 Chakra(used by IE) 等。

JavaScript is a single threaded language that can be non-blocking.

JavaScript 是單一執行緒(single threaded execution)的程式語言,意思是一次只能處理一個需求(任務),所有的 line code 在執行堆疊(call stack)中記錄執行情況,每次只會執行一個程式碼片段(one thing at a time = one call stack)。

執行堆疊(call stack)會紀錄目前執行到程式的哪個部分,如果進入了某個 function,便會把這個 function 添加到堆疊的最上方,如果 function 執行了 return 便會把 function 從堆疊中抽離(pop off),在堆疊中的資料是遵守 first-in-last-out 的順序。

雖然 JavaScript 執行程式是同步的,逐一執行,但因為在瀏覽器環境中,還有 Rendering Engine 和 HTTP Request,所以整個網頁在執行過程中可以達到非同步的運作

JavaScript Run-Time Environment?

JavaScript Run-Time Environment 就是指 JavaScript 的執行環境,下圖為執行環境的示意:

avaScript Run-Time Environment

圖片來源為How JavaScript works in browser and node?

JavaScript 依靠這些機制運作執行:

  • Memory Heap 記憶、儲存。 JavaScript automatically allocates memory when objects are created and frees it when they are not used anymore(Garbage Collection).
  • Call Stack 執行堆疊,紀錄當前程序所在位置的數據結構,從最上方開始執行。
  • Event Loop 事件循環,不斷檢查 Call Stack 是否為空的。
  • Callback Queue 佇列,遵守 fisrt-in-first-out 的資料處理順序。
  • Web APIs 由瀏覽器提供的應用程式,例如:fetch(), DOM events, setTimeout, setInterval…etc. These Web APIs are asynchronous. That means, you can instruct these APIs to do something in background and return data once done, meanwhile we can continue further execution of JavaScript code. While instructing these APIs to do something in background, we have to provide a callback function. Responsibility of callback function is to execute some JavaScript once Web API is done with it’s work.

在瀏覽器執行 JavaScript 時, call stack 會將不屬於 JavaScript 原生處理範圍的函式或程式碼,丟給 Web APIs 處理,再將處理結果丟到 Callback Queue,透過 Event Loop 不斷查詢是否 Call Stack is empty?如果沒有等待執行的程式碼,再將 Callback Queue 佇列中的第一項放到 Call Stack 中去執行。由於 Event Loop 運作的關係,瀏覽器的 JavaScript 可以同時執行多個需求,而不需要等待上個需求完成才進行下一個。

非同步的 JavaScript

以下介紹幾個在 JavaScript 中實現非同步操作的語法:

setTimeout() & setInterval()

setTimeout 和 setInterval 是由瀏覽器提供的 Web APIs,可以延遲任務執行時間,作為計時器使用。

ES6: Promise

為了解決 callback hell 的問題和串接 function 的需求,JavaScript 在 ES6 中加入了 Promise,Promise is saying that “hey, I am doing somthing and I promise to let you know when I have the result.” A promise is an object that may produce a single value some time in the future, either a resolved value, or a reason that it’s not resolved(rejected). Promise 讓非同步請求處理更容易,是一種非同步編程的解決方案。Promise object 是一個構造函數,需要用 new 語法來生成 promise 實例:

1
2
3
4
5
6
7
8
const promise = new Promise( function(resolve, reject){
console.log('Promise');
if(true) {
resolve(value); // state 由 pending 轉為 fulfilled
} else {
reject(error); // state 由 pending 轉為 rejected
}
});

Promise 有三種狀態:pending、fulfilled、rejected,分別代表當前腳本任務的執行狀況。當 Promise 實例生成後,可以用 .then(function(){}, function(){}) 方法分別指定 fulfilled 和 rejected 狀態時的回調函數(callback function):

1
2
3
4
5
6
7
8
promise.then(
function(value){
console.log('resolved');
},
function(error){
console.log('rejected'); // rejected 狀態的 callback function 可選,不一定要指定
}
);

fetch() API

A fetch simply return a promise.

1
2
3
4
5
6
7
fetch('url')
.then( response => response.json())
// we would get a response which was a promise.
//.json() convert it into sth that can be used in js.
.then( data => console.log(data))
.catch((error) => console.log(error));
//it's going to check and run if anything before it fails.

fetch 實作基於 es6 promise,在 2015 年由 google 發佈,fetch 回傳的 promise 物件不會再有收到 response 但是 http status 呈現 404 或 500 的時後變成 rejected,只會在網路出現問題或是被阻止 request 時,狀態才會變成 rejected,其他都是 fulfilled。
由於 fetch 會返回一個 promise 所以可以使用 .then 來串接,避免撰寫 callback hell 程式碼,通常使用時會用 .catch 來捕捉 promise 發生的 error。

ES8: Async/Await

Async/Await is bulit on top of Promises. 目的是簡化使用 promise 的行為。我們可以使用 async ()=>{}來宣告一個非同步函式,這個非同步函式會返回一個 promise。 Every function that returns a promise can be considered as async function. async function 是不管怎樣都會回傳 Promise 的函式,雖然我們回傳的不是一個 Promise,但因為它是 async function 的關係,JS 會自動把它包成 Promise,所以可以使用 then

1
2
3
4
5
6
7
const foo = async () => {
return 1;
}

foo().then((res) => {
console.log(res); // 1
});

await 必須在 async function 裡面才能使用,await可以在所有 return a promise 的程式碼前使用,await 會等 promise 執行完,再執行下一行。await 也能夠把 Promise 回傳的值接起來,通常我們在呼叫 API(例如執行 fetch、axios)的時候就很好用:

1
2
3
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then((data) => console.log(data))

改寫成 async function:

1
2
3
4
5
6
async function fetchUsers() {
const resp = await fetch('https://jsonplaceholder.typicode.com/users');
// the function is going to pause until we get a response from fetch
const data = await response.json();
console.log(data);
}

使用 async/await 處理多個 非同步請求以及除錯,比使用 promise 的 then 鏈,更容易除錯:

這是使用 promise 和 try/catch 用法:

1
2
3
4
5
6
7
8
9
10
11
function loadData() { 
// Catches synchronous errors
try {
getJSON().then((response) => {
var parsed = JSON.parse(response);
console.log(parsed);
}).catch( err => console.log(err)); // Catches asynchronous errors
} catch(e) {
console.log(e);
}
}

這是使用 asyn/await 和 try/catch:

1
2
3
4
5
6
7
8
async function loadData() {
try {
var data = JSON.parse(await getJSON());
console.log(data);
} catch(e) {
console.log(e);
}
}

在錯誤發生時,如果是用 .then 返回的錯誤堆疊不提供錯誤發生在哪裡,不容易找到錯誤發生原因:

1
2
3
4
5
6
7
8
9
10
11
function loadData() {
return callAPromise()
.then(callback1)
.then(callback2)
.then(callback3)
.then(() => {throw new Error("boom")})
}

loadData()
.catch((err) => {console.log(err)});
// Error: boom at callAPromise.then.then.then.then (index.js:8:13)

使用 async/await 便可以逐行找到錯誤發生的地方:

1
2
3
4
5
6
7
8
9
10
11
async function loadData() {
await callAPromise1()
await callAPromise2()
await callAPromise3()
await callAPromise4()
await callAPromise5()
throw new Error("boom");
}
loadData()
.catch((err) => {console.log(err)});
// Error: boom at loadData (index.js:7:9)

JavaScript vs Node.js

Node.js is a JavaScript runtime bulit on Google chrome’s V8 engine. 2009 Ryan Doll decided it would be great to run it ouside the browser. So he created Node.js which is actually a C++ program. It’s an executable C++ program that provides JS runtime for us.
Node.js uses Google chrome’s V8 engine to provide JavaScript runtime but does not rely only on it’s event loop. It uses libuv library (written in c) to work along side V8 event loop to extend what can be done in background. Node follows same callback approach like Web APIs and works in similar fashion as the browser.

Node.js 的執行系統圖示:

nojs-system-diagram-by-busyrich

圖片來源為nojs-system-diagram-by-busyrich

If you compare browser diagram with above node diagram, you can see the similarities. The entire right section looks like Web API but it also contains event queue (callback queue/message queue) and the event loop. But V8, event queue and event loop runs on single thread while worker threads are responsible to provide asynchronous I/O operation. That’s why Node.js is said to have as non-blocking event driven asynchronous I/O architecture.

I/O 指 input/output,資訊處理系統與外部世界(使用者、另一個資訊處理系統)之間的通訊,包括輸入:接收訊號或資料;輸出:發送訊號或資料。任何資訊傳入或傳出 CPU/記憶體組合,例如通過磁盤驅動器讀取資料,就會被認為是 I/O。

Asynchronous Node.js

由 Node.js 建立的後端伺服器,可以使用 Express.js 框架來處理非同步請求的操作,Express.js 是一個基於 Node.js 平台,極簡、靈活的框架,擁有強大的特性可以幫助開放網頁應用。
使用 Express.js 就是調用各種中間件(middleware)來處理各種請求、管理路由,其本身也可以視為是一種中間件(middleware)。通過 Express.js 提供的執行函數調用對應的方法,比起直接在 Node.js 中寫處理程式,會更加方便和快捷。

Reference

How JavaScript works in browser and node?
Udemy course “Advanced JavaScript Concepts”
學習Express前,都會搞懂這幾個問題

延伸閱讀

HTTPS/HTTP/JSON/AJAX/RestfulAPI
JS-ECMAScript

ECMAScript

1996 年創造 JavaScript 的 NetScape 公司決定將 JavaScript 提交給標準化組織 ECMA,希望讓這種語言成為國際標準。1997 年 ECMAScript 1.0 發佈,規定了 browser 腳本語言的標準。
使用 ECMAScript 這個名稱的原因是:

  1. 商標註冊:JAVA 是 Sun 公司的商標,根據授權協議 JavaScript 只有 NetScape 公司可以使用。
  2. 可表示這個語言的制定者為 ECMA,保證其開放性和中立性。
    標準制定者每個月開一次會,委員會供來自任何人的提案,經過多次開會等一個提案足夠成熟,就可以正式進入標準。每年會正式發佈一次,作為當年正式版本。接下來一年期間根據此版本為基礎做微幅變動,直到隔年草案便正式成為新的正式版本。在 2015 年 6 月發佈的就是 ECMAScript2015 又稱為 ES6,由於在上一次是 ES5.1(2009.11),ES6 包含了較多的新特性,從 ES5.1 之後到 ES6 訂定的標準。
    規範的推動主要會經過以下幾個階段:
  • Stage 0: strawman——最初想法的提交。
  • Stage 1: proposal(提案)——由TC39至少一名成員倡導的正式提案文件,該文件包括API事例。
  • Stage 2: draft(草案)——功能規範的初始版本,該版本包含功能規範的兩個實驗實現。
  • Stage 3: candidate(候選)——提案規範通過審查並從廠商那裡收集反饋
  • Stage 4: finished(完成)——提案準備加入ECMAScript,但是到瀏覽器或者Nodejs中可能需要更長的時間。

以下簡要的依年份條列從 2015 年至今(2019)加入的新功能:

ES6

  • let & const & block scope 的概念:避免 var 宣告時,出現區域變數覆蓋全域變數或在 for loop 中循環變數洩露成為全域變數的副作用發生。
  • Arrow Function:節省 code line,而且沒有自己的 this 值,使用來自外部的 this,作為 object method 的時候不需要再 .bind(this)
  • Class
  • Promise
  • mudule
  • Template Strings
  • 解構賦值 (destructuring):從數組中獲取值並賦值到變量中,變量的順序與數組中對象順序對應。
  • Array Spread operator
  • Object.assign()
  • Symbol
  • Set, Map, weakSet, weakMap
  • 函數參數默認值
  • 對象屬性簡寫

ES7

  • includes():可以使用在 string 或 array,檢查有無某元素,返回 true 或 false
  • exponential operator 指數運算符:**

ES8

  • String padding: padStart(),padEnd() 新增字串長度到指定長度
  • can use ‘ending commas’ in list or calling 函數參數列表結尾允許逗號
  • Object.values(), Object.entries()
  • Async Await

ES9

  • Object Spread operator
  • .finally() 在 promise 完成(resovled or rejected)後執行一個指令
  • Asynchronous iterators for await(... of...)
    1
    2
    3
    4
    5
    async function process(array) {
    for await (let i of array) {
    doSomething(i);
    }
    }

ES10

ES10 在 2019 年 2 月中推出,在新版的 Nodejs 和 Chrome 已經可以使用。

  • [].flat(),[].flatMap()

  • Object.fromEntries

    1
    2
    3
    4
    5
    6
    7
    const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
    const obj = Object.fromEntries(map);
    console.log(obj); // { foo: "bar", baz: 42 }

    const arr = [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ];
    const obj = Object.fromEntries(arr);
    console.log(obj); // { 0: "a", 1: "b", 2: "c" }
  • String.trimStart() and String.trimEnd() 減少空白字串

  • try{} catch{} 可以省略 catch 回傳的表達參數 catch(err){} -> catch{}

  • revised Function#toString:
    function.toString() 回傳確實的 Function 程式碼,包括空白字元和註解:

    1
    2
    3
    4
    5
    6
    7
    8
    function exampleFuncton() {
    // Hello, I'm an ordinary function
    }

    console.log(exampleFunction.toString());
    // function exampleFunction() {
    // // Hello, I'm an ordinary function
    // }
  • Symbol Description:
    在建構 symbol 時把 description 作第一個參數傳入,就可以通過 toString() 取得:

    1
    2
    3
    const symbolExample = Symbol("Symbol description");
    console.log(symbolExample.toString());
    // 'Symbol(Symbol description)'
  • JSON ⊂ ECMAScript
    The unescaped line separator U+2028 and paragraph separator U+2029 characters were not accepted in the pre-ES10 era.

  • well-formed JSON.stringify
    JSON.stringify() may return characters between U+D800 and U+DFFF as values for which there are no equivalent UTF-8 characters. However, JSON format requires UTF-8 encoding. The proposed solution is to represent unpaired surrogate code points as JSON escape sequences rather than returning them as single UTF-16 code units.

  • stable Array.sort()
    The previous implementation of V8 used an unstable quick-sort algorithm for arrays containing more than 10 items. A stable sorting algorithm is when two objects with equal keys appear in the same order in the sorted output as they appear in the unsorted input.

  • 新增數據資料的基本型別:BigInt (stage 3)
    BigInt is the 7th primitive type: an arbitrary precision integer. The variables can now represent 253 numbers and not just max out at 9007199254740992.

  • Dynamic import (stage 3)
    Dynamic import() returns a promise for the module namespace object of the requested module. Therefore, imports can now be assigned to a variable using async/await.

  • Standardized globalThis object (stage 3)


Reference

What’s New in ES10? Javascript Features ES2019
February 16, 2019

JavaScript: What’s new in ECMAScript 2019 (ES2019)/ES10?
ES6、ES7、ES8、ES9、ES10新特性一览
Twelve ES10 Features in Twelve Simple Examples

什麼是 module?為什麼要用 module?

什麼是 module?

Modules are just clusters of code. Good authors divide their books into chapters and sections; good programmers divide their programs into modules.
在進行軟體專案時,複雜性總是伴隨,妥善應用抽象、介面和他們底層的概念,將某項功能的需要注意的複雜性最小化,直到他變成單一功能的分支,也就是將一個大的 program 拆分成互相依賴的小文件、再用簡單的方法拼裝起來,透過妥善設計的介面(API)讓使用者知道如何引用一個 module ,這樣的思維就是模組化開發思維。

Good module should be highly self-contained with distinct functionally, allowing them to be shuffled, removed, or added as neccesary, without disrupting the system as a whole.

透過提供一個讓系統其他部分操作的公開介面(API),這個介面(API)裡面有元件公開的方法或屬性,那些方法和屬性也可以稱為接觸點,也就是可在介面(API)公開互動的東西。接觸點越多,需要操作的地方也越多,也有較高的彈性,因為有大量的功能,所以可能也會更難理解、使用。
介面(API)有兩種用途:幫助我們開發元件的新功能、讓使用介面的人(元件、系統)可以受惠於公開的功能,而不需考慮那項功能的背後細節如何實作。因此設計良好合適的介面可以增加功能開發的生產力,當我們持續使用類似的API外貌,就不需要每次都重新擬定新的設計,使用者也可以放心使用。

為什麼要模組化開發?

隨著專案日漸複雜,全域變量容易互相衝突,影響專案的維護性和可拓展性。使用模組化開發方法的好處有:

  1. 提升維護性 (Maintainability)
  2. 命名空間 (Namingspacing)
  3. 提供可重用性 (Reusability)

在 JavaScript 中,管理變量 variable 是一切 coding 的基礎,看起來似乎是越少變量越容易管理,不過透過 scope 的概念可以讓變量在單一作用域內產生作用,不同作用域的變量便不會互相干擾,然而當你想要共用變量或資料時卻只能透過全域變量的設定,或用 callback function 將變量傳出來,設定全域變量容易產生命名衝突,而且當所有變量都需要在全域中,就必須按照一定順序編程。當需要移除舊程式碼、製作新功能時,就會有困難。所以有 module 開發的必要性。

JavaScript Module History

早期 JavaScript 沒有 module 體系,其他語言都有這個功能,例如:Ruby 的 require、Python 的 import,連 css 都有 @import。因為有開發上的實際需要,所以社群制定了一些 module pattern 的加載方法,最主要的有 CommonJS 和 AMD 規範,前者用於 server 後者用於 browser。

  • AMD(Async Module Definition) 非同步載入模組規範
  • CommonJS 同步模組載入規範,Node.js 遵守其規範,透過 require 進行模組同步載入,並透過 exports, module.exports 來輸出模組。主要實現為 Node.js 伺服器端的同步載入和瀏覽器端的 Browserify。
  • CMD(Common Module Definition) 依賴就近,延遲執行
  • CND-Based, inline-script, Script Tags 最傳統的 <script></script>引入方式,但開發大型專案時容易產生弊端:
    • global variable 污染、衝突
    • 文件只能按照順序載入,須由開發者自行判斷模組與函式庫之間的依賴性
    • 資源與版本難以維護
  • UMD(Universal Module Definition) 為了兼容 AMD 和 CommonJS。
  • ES6 Module (2015年)
    ES6 Module 的標準中定義了 JavaScript 的模組化方式,自此產生了 JavaScript 原生的模組語法,其功能可以取代之前社群使用的規範,提供靜態的宣告式 API、以及採用 promise 的動態宣告式,在編譯時就確定 module 之間的依賴關係,以及輸出和輸入的變量。而 CommonJS 只能在運行時確定這些東西。目前 browser 和 Node.js 對 ES6 模組支援度還不完整,大部分還需要透過 babel 轉譯器進行轉譯。

ES6 module 現代模組化開發方法

在 ES6 module 中,每個檔案都是一個模組,有自己的範圍和環境,通過 export 命令輸出指定 code,再用 import 命令輸入文件。(自動採用嚴格模式)
過去 server 開發時會使用一個叫做 Browserify 的 module bundler, Browserify 使用 CommonJS 語法,會產生一個 bundle.js 檔案,讓我們能夠上傳並發佈網站。而現在我們會使用 ES6 module + Webpack + Babel,Webpack 也是一個類似於 Browserify 的 module bundler,但 Webpack 可以結合 Babel 轉譯器,讓瀏覽器也讀懂 ES6,並產生 bundle.js 檔案。
現代的模組化開發多使用以下工具(庫):

NPM (Node Package Manager)

一個強大的套件管理工具、線上資料庫,擁有龐大的社群支持,一開始是為了 Node.js 而發展出來的套件工具,隨著發展變成開發者社群用來 share code、package 的資料庫,透過 npm 提供的指令,可以管理套件版本、加載套件、卸除套件等。使用套件做開發,簡化重複的程式碼,可以加快開發速度和提升程式維護性。

npm 在初始化專案時會產生一個 package.json 的檔案,裡面紀錄專案所使用的 packages 版本紀錄,透過檔案紀錄可以確保團隊工作時所有人使用的版本一致。

其他套件管理工具還有由 facebook 開發的 Yarn,目前也有部分人在使用。


Wepack

Wepack is a static module bundler for morden JavaScript applications.
運行在 Node.js 環境的一個開發工具。

  • 可以使用 CommonJS + AMD + ES6 module 規範。
  • 使用 import 語法和 export 的語法,在 browser 上實現模組化。
  • 能夠將圖片、css 等資源,和 js 檔案都一併打包。
  • 能夠編譯 Sass、Less、JS 擴充語法(JSX, Coffee Script, TypeScript)
  • 可用的擴充 plugin 很多

實作方法:在 webpack.config.js 中設定要進行 bundle 的檔案。自己編寫設定檔,透過指令去驅動的自動化工具,所有的動作都需要自己寫規則去做編譯,將我們寫好的前端框架檔案(preprocess)編譯成目前 browser 看得懂的內容並打包成一包的完整檔案,提交給 server,會在專案底下創建一個 webpack.config.js 和 index.js 檔案。


Babel

Babel is a JavaScript compiler that takes your morden JS code and returns browser compatible JS(older JS code).
有很多開發工具(ex:create-react-app)都自動搭載了 babel 作為轉譯工具。他也可以轉換 react 的 jsx 還有 typescript。


Gulp

自動化工作流程的 library。可以 compile css 也可以 compile JavaScript。
官網


Eslint

程式碼 coding style 檢查工具。


Reference

深入學習 JavaScript 模組化設計 - NICOLAS BEVACQUA 著 O’REILLY
搞懂為何設定 REACT、JSX、ES2015、BABEL、WEBPACK 的學習筆記

JavaScript 資料結構

資料結構是電腦中儲存、組織資料的方式,在 JavaScript 中,資料可以分做基本型別和物件型別,基本型別儲存一個值,物件型別儲存更複雜的資料,為了更簡便的存取複雜的資料,JavaScript 使用有規則可循的結構來管理資料。

The key distinctions between primitives and objects:

  • A primitive
    • Is a value of a primitive type.
    • There are 6 primitive types: string, number, boolean, symbol, null and undefined.
  • An object
    • Is capable of storing multiple values as properties.
    • Can be created with {}, for instance: {name: “John”, age: 30}. There are other kinds of objects in JavaScript: functions, for example, are objects.
    • One of the best things about objects is that we can store a function as one of its properties.

資料結構:

  • index 管理:透過索引(index)數字,一個數字代表一個資料
  • key-value-pair:使用 key-value(通常是一個字串)對應到資料

在 Ruby 語言中,使用 index 的叫做 Array,使用 key-value-pair 的叫做 Hash。
而在 JavaScript 中,使用 index 的也叫做 Array,使用 key-value-pair 的有兩種: Object 和 Map。

JavaScript 為這些資料結構內建了一些方法來方便開發者使用、管理資料:

JavaScrip Objects methods list

  • Array 陣列

    • 其他資料結構:
      • Stack 堆疊:last-in-first-out,支持 push, pop operation
      • Queue 佇列:first-in-first-out. A Queue is one of most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations: push, shift.
        在 JavaScript 中的 array 可以用作 queue 也可以用作 stack,從陣列的前/後來添加或刪除元素。
    • JavaScrip 陣列內建支援的方法
      • pop():對 array 從尾刪除元素
      • push():對 array 從尾添加元素
      • shift():對 array 從頭刪除元素
      • unshift():對 array 從頭添加元素
      • concat():返回一個新數組:複製當前數組的所有成員並向其中添加 items。如果有任何items 是一個數組,那麼就取其元素。不會改變原 array。
      • slice(start, end):複製並返回切下的部分,不會改變原 array。
      • splice(index, 刪除幾個, “插入”):返回切下的部分,會改變原 array。
      • split():複製一組 array,將 string 轉為 array。
      • join():複製一組 array,將 array 內容轉為 string。
      • sort():對原 array 做排序
      • reverse():對原 array 做倒序
      • indexOf(item, from):從 from index 開始由右至左找 item,返回符合的第一個 index
      • lastIndexOf(item, from):從 from index 開始由左至右找 item,返回符合的第一個 index
      • includes(item, from):從 from index 開始由右至左找 item,返回 true/false
  • Object Array 物件陣列
    是指陣列元素為 object 或 array 的多層結構資料。針對這種類型的資料,JavaScrip也提供一些有用的方法來協助管理:

    • map(function(item, index, array){ 遍歷每一個元素後,返回新 array })
    • find(function(item, index, array){ 查詢到返回 true,return 第一個符合的元素 })
    • filter(function(item, index, array){ 元素通過過濾器時返回 true,return 符合元素 })
    • reduce(function(accumulator, num){})
  • Object

    • Object.assign(target, …source) 產生一個新的 object 或合併 objects
    • Object.keys(obj) 返回一個由 keys 組成的陣列
    • Object.values(obj) 返回一個由 values 組成的陣列
    • Object.entries(obj) 返回一個 key-value-pair 組成的陣列
    • 自創 methods
  • Map 也是 key-value-pair 物件,但是他可以直接被 iterate、 key 可以使用任何資料型別;object的 key 只能是 string。

    • set(key, value): stores the value by the key.
    • get(key): return the value if the key exists, undefined if keys don’t exist.
    • has(key): return true/false whether the key exists or not
    • delete(key): remove value by the key
    • clear(): removes every thing from the set
    • size: counts the elements
    • keys()
    • values()
    • entries()
  • Set is a collection of values, where each value may occur only once.

    • add(value): add a value, return set itself.
    • delete(value): removes the value, return true/false.
    • has(value): return true/false whether the value exists or not.
    • clear(): removes every thing from the set
    • size: counts the elements
    • keys()
    • values()
    • entries()
  • WeakMap & WeakSet
    通常情況下,當某數據存在於內存中時,對象的屬性或者數組的元素或其他的數據結構將被認為是可以獲取的並留存於內存。在一個正常 Map 中,我們將某對象存儲為鍵還是值並不重要。它將會被一直保留在內存中,就算已經沒有指向它的引用。

    • WeakSet

      • WeakSet 是一種特殊的 Set,它不會阻止 JavaScript 將它的元素從內存中移除。
      • 它和 Set 類似,但是我們僅能將對象添加進 WeakSet(不可以是基礎類型)
      • 僅當對象存在其他位置的引用時它才存在於 set 中。
      • 支持 add()、has() 和 delete(),不支持 size、keys() 也不支持迭代器。
    • WeakMap

      • WeakMap 也不會阻止 JavaScript 將它的元素從內存中移除,而且它和 Map 的區別是它的鍵必須是對象,不能是基礎類型的值。
      • 支持 get(key)、set(key, value)、delete(key, value)、has(key)
      • 但不支持 keys()、values()、entries(),我們不能對它進行迭代,所以沒有辦法獲取它的所有鍵值。
      • WeakMap 的目的是,我們可以僅當該對象存在時,為對象存儲一些內容。但我們並不會因為存儲了對象的一些內容,就強制對像一直保留在內存中。

基本型別的 methods list

There are many things one would want to do with a primitive. So JavaScript provides methods to call. JavaScript allows us to work with primitives (strings, numbers, etc.) as if they were objects.

The solution is:
The “object wrappers” are different for each primitive type and are called: String, Number, Boolean and Symbol. Thus, they provide different sets of methods.

JavaScript 允許訪問字符串,數字,布爾值和符號的方法和屬性。當進行訪問時,創建一個特殊的“包裝對象”,它提供額外的功能,運行後即被銷毀。除 null 和 undefined 以外的基本類型都提供了許多有用的方法。從形式上講,這些方法通過臨時對象工作,但 JavaScript 引擎可以很好地調整以優化內部,因此調用它們並不需要太高的成本。

  • Number
    JavaScript 有一個內置的 Math 對象,它包含了一個小型的數學函數和常量庫。

    • Math.random()
    • Math.max(a, b, c…) / Math.min(a, b, c…)
    • Math.pow(n, power)
      分數可使用(須記住使用分數時會損失精度):
    • Math.floor 向下舍入,3.1 變成 3,-1.1 變成 -2。
    • Math.ceil 向上舍入,3.1 變成 4,-1.1 變成 -1。
    • Math.trunc 刪除小數點後的所有內容而不捨入:3.1 變成 3,-1.1 變成-1,IE 瀏覽器不支援。
    • Math.round 向最近的整數舍入:3.1 變成 3,3.6變成4,-1.1變成-1。
    • num.toFixed(precision) 將點數後的數字四捨五入到 n 個數字並返回結果以字符串表示
      檢查是否為特殊 Number?
    • isNaN
    • isFinite
      檢視字符串並返回 Number:他們從字符串中“讀出”一個數字。如果發生錯誤,則返回收集的數字。有時候 parseInt / parseFloat 會返回 NaN。一般發生在沒有數字可讀的情況下。
    • parseInt(str, radix) 返回整數。第二個參數指定了數字系統的基礎,因此 parseInt 還可以解析十六進制數字,二進制數字等字符串。
    • parseFloat() 可以返回浮點數
  • String

    • string.length 字符串長度
    • string[0] 或 string.charAt(0):
      如果沒有找到字符,[] 返回 undefined,而 charAt 返回一個空字符串
    • toLowerCase()
    • toUpperCase()
    • trim() 刪除前後的空格
    • 查找字符的方法:
      • indexOf(substr, pos)
      • includes(substr, pos)
      • endsWith()
      • startsWith()
    • 獲取字符串的方法:
      • slice(start [, end]) 返回從 start 到(但不包括)end 的字符串部分。
      • substring(start [, end]) 返回 start 和 end 之間的字符串部分。這與 slice 幾乎相同,但它允許 start 大於 end。
      • substr(start [, length]) 從 start 開始返回給定 length 的字符串部分。

Reference

javascript.info/map-set-weakmap-weakset
javascript.info/primitives-methods

為什麼會有深淺拷貝的差異?

因為 JavaScript 中,基本型別(primitve)與物件型別(object)的值的賦予方式不同,基本型別是 pass by value,物件型別是 pass by sharing。相關介紹可看:JS-pass by value or reference

因為值賦予方式的差異,在複製物件例如 object, array 等資料類型時,根據拷貝資料的形式,可以分為淺拷貝(shallow copy)及深拷貝(deep copy)。
對淺拷貝來說,只是複製 collection structure,而不是 element,With a shallow copy, two collections now share the individual elements. Collections can be diverse data structures which stores multiple data items.
因此對於基本型別來說,淺拷貝(用等號賦值)會傳值,但對物件型別來說,淺拷貝是傳遞 reference,讓兩者可以共用一個記憶體的物件資料,這樣的話在指派物件型別的資料的第二層或更深層內容時,會同時影響兩個地方。

淺拷貝只複製指向某個物件的指標,而不複製物件本身,新舊物件還是共用同一塊記憶體。
而深拷貝是整個複製,包含element,會另外創造一個一模一樣的物件,新物件跟原物件不共用記憶體,修改新物件不會改到原物件。所以當我們在使用有多層結構的物件資料時,要盡量用深拷貝。
一般物件如果用等號賦值:

1
2
3
4
5
6
var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = obj1;
obj2.b = 100;

console.log(obj1); // { a: 10, b: 100, c: 30 } <-- b 被改到了
console.log(obj2); // { a: 10, b: 100, c: 30 }

如何進行淺拷貝、深拷貝?

一般而言 基本型別的拷貝方法就是用 等號賦值,而物件型別例如 array 或 object等就有很多方式,依照拷貝的層次深度可以分為淺拷貝和深拷貝:如果物件中屬性的值也是物件,只能複製到第一層物件的屬性,而無法複製到屬性值的物件(第二層),就無法達到實際的複製,而是會與舊物件一起共用同一塊記憶體;這樣的複製方法稱為「淺拷貝」。相反地,深拷貝會另外創造一個一模一樣的物件,新物件跟原物件不共用記憶體,修改新物件不會改到原物件。

  • 淺拷貝方法

    • Array.concat:一般Array.concat的用法是合併兩個陣列

      1
      2
      3
      4
        var alpha = ['a', 'b', 'c'],
      numeric = [1, 2, 3];
      var alphaNumeric = alpha.concat(numeric);
      ​ console.log(alphaNumeric); // ['a', 'b', 'c', 1, 2, 3]
    • Array.slice:一般Array.slice()的方法是複製一個新的陣列,可帶入參數 Array.slice(start, end),當不輸入參數值的話會直接複製一個。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      var animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
      console.log(animals.slice(2));
      // Array ["camel", "duck", "elephant"]

      console.log(animals.slice(2, 4));
      // Array ["camel", "duck"]

      console.log(animals.slice());
      // Array ['ant', 'bison', 'camel', 'duck', 'elephant'];
    • 手動複製

      1
      2
      3
      4
      5
      6
      var obj1 = { a: 10, b: 20, c: 30 };
      var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };
      obj2.b = 100;

      console.log(obj1); // { a: 10, b: 20, c: 30 } <-- 沒被改到
      console.log(obj2); // { a: 10, b: 100, c: 30 }
    • Object.assign:用來合併物件,用法為 Object.assign(target, ...source)若目標物件為空物件則可視為複製一個source的物件。
      Object.assign({}, obj1)的意思是先建立一個空物件{},接著把obj1中所有的屬性複製過去,因為Object.assign跟我們手動複製的效果相同,所以一樣只能處理深度只有一層的物件,沒辦法做到真正的 Deep Copy,不過如果要複製的物件只有一層的話可以使用他。

    • 展開運算子(Spread Operator) 也只能實現一層的拷貝

      1
      2
      3
      4
      5
      6
      7
      8
      let obj = {name:'john', age:{child: 18}}
      let copy = {...obj};

      copy.name = 'mike';
      copy.age.child = 99;

      console.log(obj); //{name:"john", age:{child: 99}}
      console.log(copy); //{name:"mike", age:{child: 99}}
  • 深拷貝方法

    • JSON.parse(JSON.stringify(object_array)):

      • JSON.parse():把字串轉成物件
      • JSON.stringify():把物件轉成字串
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
        let obj1 = { a:{b:10} };
      let obj2_string = JSON.stringify(obj1);
      console.log(obj2_string); //"{"a":{"b":10}}";

      let obj2 = JSON.parse(obj2_string);
      console.log(obj2); //{a:{b:10}}

      obj2.a.b = 20;
      console.log(obj1); //{a:{b:10}}
      console.log(obj2); //{a:{b:20}}
      ```

      只有可以轉成JSON格式的物件才可以這樣用,像 function、Set、Map..等型態就沒辦法轉成 JSON。

      - **jQuery `$.extend`**

      var $ = require(‘jquery’);
      var obj1 = {
      a: 1,
      b: { f: { g: 1 } },
      c: [1, 2, 3]
      };
      var obj2 = $.extend(true, {}, obj1);
      console.log(obj1.b.f === obj2.b.f); // false

      1
      2

      - **lodash `_.cloneDeep`**

      var _ = require(‘lodash’);
      var obj1 = {
      a: 1,
      b: { f: { g: 1 } },
      c: [1, 2, 3]
      };
      var obj2 = _.cloneDeep(obj1);
      console.log(obj1.b.f === obj2.b.f); // false

      1
      2
      3
      4

      - **自己寫**
      例如下面這個在 react app 裡面用 slice() 和 Object.assign(target, ...sources) 來改變深層結構資料的方法:
      [GitHub Repo:Tripper-app](https://github.com/chinyun/Tripper-app/blob/master/src/containers/App.js)

      updateBudgets = (journey, journeyId) => {
      const index = this.state.journeyList.findIndex(item => item.id === journeyId);
      if (index !== -1) {

      this.setState({
        journeys: [
          ...this.state.journeys.slice(0, index),
          Object.assign({}, this.state.journeys[index], journey[0]),
          ...this.state.journeys.slice(index + 1)
        ]
      })

      }
      };

Reference

關於JAVASCRIPT中的SHALLOW COPY(淺拷貝)及DEEP COPY(深拷貝)
[Javascript] 關於 JS 中的淺拷貝和深拷貝

延伸閱讀

React: Updating state when state is an array of objects

問題:JavaScript 到底是 pass by value 還是 pass by referece?

根據 JavaScript 的資料型態,可以分為兩大類:基本型別 primitive 和 物件型別 object。
基本型別的資料以 純值的形態存在,例如:number、string、boolean、null、undefined、symbol,而 object 的資料可能為 純值 或 多種不同型別組合而成的 物件。

基本型別傳值 pass by value,Object 型別傳址 pass by referece

1
2
3
4
5
6
7
var a = 10;
var b = a;
console.log(a === b); //true

var c = 5;
var d = 5;
console.log(c === d); //true

var b = a; 表面上看起來變數 b 的內容是透過複製變數 a 而來,實際上變數 b 是去建立了一個新的值,然後將變數 a 的內容複製了一份存放到記憶體, a 和 b 其實是存在於兩個不同的記憶體位置,因此變數 a 和變數 b 彼此獨立互不相干,即使更改 a 的內容, b 的值也不會變:

1
2
3
a + 2;
console.log(a); //12
console.log(b); //10

基本型別是不可變的 (immutable),當修改、更新值時,與那個值的副本完全無關,像這種情況,我們通常稱作「傳值」 (pass by value)。

如果是物件型別的資料:

1
2
3
4
5
6
7
var obj1 = { a: 10 };
var obj2 = { a: 10 };
console.log(obj1 === obj2); //false

var obj3 = { b: 20 };
var obj4 = obj3;
console.log(obj3 === obj4); //true

「物件」這類資料型態,在 JavaScript 中是透過「引用」的方式傳遞資料的。
當建立起一個新的物件並賦值給一個變數(var obj3 = { b: 20 };)的時候,JavaScript 會在記憶體的某處存放這個物件({ b: 20 }),再將變數(obj3)指向這個物件的存放位置,因此當var obj4 = obj3;的時候,其實是將 obj4 這個變數也指向了 { b: 20 }這個實體。
這種透過引用的方式來傳遞資料,接收的其實是引用的「參考」而不是值的副本時,
我們通常會稱作「傳址」 (pass by reference)。

特殊情況:

1
2
3
4
5
6
7
var coin1 = { value: 10 };

function changeValue(obj) {
obj = { value: 123 };
}
changeValue(coin1);
console.log(coin1.value); // ?

答案會是 10,因為當coin1指向的資料被做為 function 的參數傳入 function 時,即使資料在 function 內部被重新賦值,外部變數的內容都不會被影響。
在這種情況底下, JavaScript 會將 obj 指向一個新建的 object { value: 123 },不會影響到外部的 coin1 指向的位址的物件內容。

如果不是 重新賦址 而是 修改傳入 的內容:

1
2
3
4
5
6
7
var coin1 = { value: 10 };
function changeValue(obj) {
obj.value = 123;
}

changeValue(coin1);
console.log(coin1.value); // 123

此時變數 coin1 所指向的資料內容被改變,傳入 function 作為 obj 參數的值的 coin1 在 function 內被修改,改變了 value 的值。

垃圾回收 Garbage collection

對於開發者來說,JavaScript 的內存管理是自動的、無形的。我們創建的原始值、對象、函數……這一切都會佔用內存。當某個東西我們不再需要時會發生什麼? JavaScript 引擎如何發現它、清理它?
JavaScript 中主要的內存管理概念是可達性 Reachability,簡言之,可達值是那些以某種方式可訪問或可用的值,它們保證存儲在內存中。
這裡列出固有的可達值基本集合,這些值明顯不能被釋放:

  1. 當前函數的局部變數和參數。Local variables and parameters of the current function.
  2. 嵌套調用時,當前調用鏈上所有函數的變量與參數。Variables and parameters for other functions on the current chain of nested calls.
  3. 全局變數。Global variables.
  4. 還有一些其他的內部變數。(there are some other, internal ones as well)

這些值被稱作根 root。

如果一個值可以通過 引用 或 引用鏈,從根值訪問到,則認為這個值是 可達的。
比方說,如果局部變量中有一個對象,並且該對象具有引用另一個對象的 property,則該對像被認為是可達的。而且它引用的內容也是可達的。

在 JavaScript 引擎中有一個被稱作垃圾回收器(garbage collector)的東西在後台執行。它監控著所有對象的狀態,並刪除掉那些已經不可達的。

Reference

深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?
Garbage collection
[筆記] 談談 JavaScript 中 by reference 和 by value 的重要觀念

Hoisting、Scope 和 Closure 在 JavaScript 中是很重要的觀念,因爲會影響我們如何撰寫 JavaScript。了解這三個概念可以幫助我們了解 JaveScript 在內文執行的運作原理,尤其是在創建和執行階段,JaveScript 的執行機制會如何理解我們寫的程式碼,並跑出我們想要的結果。

Hoisting

JavaScript 內文執行的運作方式有一個機制叫做:Hoisting 提升,在執行任何程式碼前,JavaScript 會把變數和函數的宣告在編譯階段就放入記憶體,編譯後執行時因為已經宣告了,所以如此即便我們先寫調用某一函式的程式碼,再寫該函式的內容,JavaScript 也還是可以知道這段程式碼的意義,程式碼仍然可以運作:

1
2
3
4
5
6
catName("Chloe");

function catName(name) {
console.log("My cat's name is " + name);
}
/*上面程式的結果是: "My cat's name is Chloe"*/
1
2
3
4
num = 6;
num + 7;
var num;
/* 只要 num 有被宣告,就不會有錯誤 */

JavaScript 僅提升宣告的部分,而尚未賦值。如果在使用該變數後才宣告和初始化,那麼該值將是 undefined,以下範例顯示了這個特性。

1
2
3
var x = 1; // 給予 x 值
console.log(x + " " + y); // '1 undefined',此階段 y 尚未被賦予值
var y = 2;

上述程式碼其實是這樣運作的:

1
2
3
4
var x = 1; 
var y; // 宣告 y
console.log(x + " " + y); // '1 undefined'
y = 2; // 賦值 y

函數宣告的優先權比變數宣告高,如果 function 調用時有傳參數進來,就會先宣告該參數代表的變數意義並賦值。

1
2
3
4
5
6
function test(v) {
var v
console.log(v)
v = 3
}
test(10) // 10

需要注意的是,只有 declaration 宣告式的 function (ex:function func(){...})會被在編譯階段提升,而 expression 表達式宣告的 function (ex:let func = function(){...})會在執行階段才被存放到記憶體中。

Scope 作用域

  • Execution Context 執行環境
    要了解 Scope 須先知道 Execution Context 執行環境 的概念。
    Execution Context is a fancy word for describing the environment in which your Javascript code runs.

當 JavaScript engine start up 程式碼準備好開始運行時,就會先建立 global execution context全域執行環境,然後建立一個 global object 和 this,在 browser 的環境中,global object 是 window, this === window,在 node.js 環境中, global object 是 global,this === global。
我們可以 assign variable、function 到 global object 中。

執行環境在建立時會經歷兩個階段,分別是 :

  • Creation Phase 創造階段:變數宣告和函數宣告提升,自動跳過函式裡的程式碼。
  • Execution Phase 執行階段:由上到下、一行一行地執行程式,賦值也是在這階段。

當 JavaScript engine 看到 function name() 函數被執行,就會創建一個 function name() execution context,新的 function execution context 會被加入到Execution stack 執行堆疊,並依序執行(Javascript 是單一執行緒,一次只能做一件事),執行環境 的堆疊過程是具有 順序性 的:first in last out。

  • Scope 作用域是什麼?

Scope determines the accessibility (visibility) of variables. Scope is where can I access the variable where’s that variable in my code. It just defines the accessibility of variables and functions in the code. JavaScript has function scope: Each function creates a new scope. Variables defined inside a function are not accessible (visible) from outside the function.

Scope 可以說是一個變數的生存範圍,出了這個範圍就無法存取到。在 JavaScript 裡面,可以分為兩種 Scope 作用域:

  • Global Scope:表示全域、任何地方都能存取得到
  • Lexical Scope:variable 被寫下來的那個地方,就是作用域
    • function scope
    • block scope
      it means that only by looking at the source code we can determine which environment the variables in data are avaliable in.
      這與 Lexical Environment 有關,在物理上我們將 code 寫在哪裡,那就是該 variable 或 function 的 Lexical Environment。 In JS our lexical scope (avalible data + variables where the function was defined) determines our avalible variables. Not the function is called.(相反地,有一種叫做 Dynamic scope 的作用域機制 就是在程式執行時才動態決定的)
  • Scope Chain
    透過程式碼層層的包裹,由內而外,直到 global scope 的這一條 scope chain,可以幫助找到要找的對象(通常是 variables)被寫下的地方(lexical environment)。

在 ES6 以前,唯一產生作用域的方法就是 function,每一個 function 都有自己的作用域,在作用域外面你就存取不到這個 function 內部所定義的變數,然而 ES6 的時候引入了 let 跟 const,多了 block-scope 的概念。
延伸:ES6: let, const, Block-Level Scope

因應ES6的出現,使用上建議大家不要再用var來宣告變數,改用let與const,而且優先使用const。
因為const在宣告時必須給定值,並且不能再被更改,這可以有效降低出現錯誤的機會。
同理,如果是需要變更的數值則改用作用範圍較小的let做宣告,來減少錯誤出現的機率,Ex: for迴圈。
JavaScript 宣告: var、let、const

Closure 閉包

「閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是參照了自由變數的函式。這個被參照的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。閉包是由函式和與其相關的參照環境組合而成的實體。」–wiki
每個宣告的 function 都會儲存著[[Scope]],而這個資訊裡面就是參照的環境。
「that all functions, independently from their type: anonymous, named, function expression or function declaration, because of the scope chain mechanism, are closures.

from the theoretical viewpoint: all functions, since all they save at creation variables of a parent context. Even a simple global function, referencing a global variable refers a free variable and therefore, the general scope chain mechanism is used;
from the practical viewpoint: those functions are interesting which:

scopeChain 把每一層的 AO 和 VO 記錄下來,而變數就紀錄在AO 或 VO 裡,也因為 return 才把 AO 和 VO 保留下來。
closure 其實就是因為 scopeChain 有 reference 到其他 Execution Context 的 AO(active object) 或是 VO(variable object),所以在離開之後還是可以存取到上層的變數,如果你是以會記住上層資訊的角度來看 closure,那所有的 function 其實都是 closure。

  • 閉包是函式記得並存取 Lexical Scope 語彙範疇的能力,可說是指向特定 scope 的參考,因此當函式是在其宣告的 Lexical Scope 語彙範疇之外執行時也能正常運作

  • 迴圈與閉包搭配使用時的謬誤與陷阱。

    1
    2
    3
    4
    5
    for (var i = 1; i <= 5; i++) {
    setTimeout(function timer() {
    console.log(i);
    }, i * 1000 );
    }

    由於 console.log(i) 中的 i 會存取的範疇是 for 所在的範疇(目前看起來是全域範疇,因為 var 宣告的變數不具區塊範疇的特性),因此當 1 秒、2 秒…5 秒後執行 console.log(i) 時,就會去取 i 的值,而此時 for 迴圈已跑完,i 變成 6,因此就會每隔一秒印出一個「6」。
    解決方法可以利用 IIFE(Immediately Invoked Function Expression)把一個 function 包起來並傳入 i 立即執行,所以迴圈每跑一圈其實就會立刻呼叫一個新的 function,因此就產生了新的作用域。不過在 ES6 裡面有了 block scope 的概念以後,你只要簡單地把迴圈裡面用的 var 改成 let 就行了:因為 let 的特性,所以其實迴圈每跑一圈都會產生一個新的作用域。
    關於此題的其他參考:for迴圈 setTimeout 結合一些示例

  • 模組模式可經由建立一個模組實體來調用內層函式,而內層函式由於具有閉包的特性,因此可存取外層的變數和函式。透過模組模式,可隱藏私密資訊,並選擇對外公開的 API

  • 利用模組依存性載入器或管理器或 ES6 模組來管理模組。

Reference

提升(Hoisting)
我知道你懂 hoisting,可是你了解到多深?
秒懂!JavaSript 執行環境與堆疊
W3C
所有的函式都是閉包:談 JS 中的作用域與 Closure
你懂 JavaScript 嗎?#15 閉包(Closure)
[2019-10-12] 進階 JavaScript - Closure
閉包(Closure)

閉包也會用來作為物件私用(private)的模擬,以及名稱空間的管理等

this

在物件導向程式語言裡面,this 概念指的是 instance 本身。
舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Car {
setName(name) {
this.name = name
}

getName() {
return this.name
}
}

const myCar = new Car()
myCar.setName('hello')
console.log(myCar.getName()) // hello

然而和一般物件導向的程式語言 Java 或 C++ 等不同,在 JavaScript 裡面,你在任何地方都可以存取到 this,所以在 JavaScript 裡的 this 跟其他程式語言慣用的那個 this 有了差異,這就是為什麼 this 難懂的原因。

1
2
3
4
5
function hello(){
console.log(this)
}

hello()

一旦脫離了物件導向,也就是在 class 外面的 this,其實沒有太大的意義。
在這種很沒意義的情況下,this 的值在瀏覽器底下就會是 window,在 node.js 底下會是 global,如果是在嚴格模式,this 的值就會是 undefined。這個規則就是所謂的「預設綁定」。

this 值的改變

  • 可以用 call、apply 與 bind 改變 this 的值:

    1
    2
    3
    4
    5
    6
    7
    'use strict';
    function hello(a, b) {
    console.log(this, a, b)
    }

    hello.call('yo', 1, 2) // yo 1 2
    hello.apply('hihihi', [1, 2]) // hihihi 1 2

    call 跟 apply 的差別就是 apply 在傳參數時要用 array 包起來。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Car {
    hello() {
    console.log(this)
    }
    }

    const myCar = new Car()
    myCar.hello() // myCar instance
    myCar.hello.call('yaaaa') // yaaaa

    可以把原本的 this 值覆蓋掉。

    1
    2
    3
    4
    5
    6
    7
    8
    'use strict';
    function hello() {
    console.log(this)
    }

    const myHello = hello.bind('my')
    myHello() // my
    myHello.call('call') // my

    使用 bind 之後,call 方法也沒有辦法覆蓋掉。
    如果是在非嚴格模式底下,無論是用 call、apply 還是 bind,你傳進去的如果是 primitive 都會被轉成 object:

    1
    2
    3
    4
    5
    6
    7
    function hello() {
    console.log(this)
    }

    hello.call(123) // [Number: 123]
    const myHello = hello.bind('my')
    myHello() // [String: 'my']
  • this 是在運行時求值的,可以適用於任何 function,從不同 object 調用同一個 function 可以會有不同 this 的值。

    1
    2
    3
    4
    5
    6
    7
    8
    const obj = {
    value: 1,
    hello: function() {
    console.log(this.value)
    }
    }

    obj.hello() // 1

    this 的值跟 作用域 跟 程式碼的位置 在哪裡完全無關,只跟「你如何呼叫」有關。

    作用域的概念舉例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var a = 10
    function test(){
    console.log(a)
    }

    const obj = {
    a: 'ojb',
    hello: function() {
    test() // 10
    },
    hello2: function() {
    var a = 200
    test() // 10
    }
    }

    test() // 10
    obj.hello()
    obj.hello2()

    無論我在哪裡,無論我怎麼呼叫test這個 function,他印出來的 a 永遠都會是全域變數的那個 a(// 10),因為作用域就是這樣運作,test 在自己的作用域裡面找不到 a 於是往上一層找,而上一層就是 global scope,這跟你在哪裡呼叫 test 一點關係都沒有。test 這個 function 在宣告的時候就把 scope 給決定好了。

    但 this 卻是完全相反,this 的值會根據你怎麼呼叫它而變得不一樣,例如使用 call、apply 跟 bind 可以用不同的方式去呼叫改變 this 的值。如果 function 是在物件下調用,那麼 this 則會指向此物件,無論 function 是在哪裡宣告;使用物件的方法調用時 this 會指向調用的物件。宣告的位置不重要,重要的是呼叫的方式。

Reference

JavaScript 的 this 到底是誰?
淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂

補充:在 React 中的 bind(this)用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from 'react';

class App extends React.Component {
constructor() {
super();
this.state = {
data: []
}
this.setStateHandler = this.setStateHandler.bind(this);
};

setStateHandler() {
var item = "setState..."
var myArray = this.state.data;
myArray.push(item)
this.setState({data: myArray})
};

render() {
return (
<div>
<button onClick = {this.setStateHandler}>SET STATE</button>
<h4>State Array: {this.state.data}</h4>
</div>
);
}
}

export default App;

this.setStateHandler().bind(this) sets the context for the function setStateHandler() to be the class object. This is necessary so that you could call this.setState({...}) inside the method, because setState() is the method of React.Component. If you do not .bind(this) you would get an error that setState() method is undefined.

what is the usage of : this.method.bind(this)

React 與 bind this 的一些心得
當使用 extend React.Component 的方式去宣告元件的時候,React 確實會綁定 this 到元件內,但是卻有以下特定的地方才會被綁進去生命周期函式,例如 componentDidMount 等等
render 內其他自己定義的 property 就不會被綁入 this ,而且 this 會被指到 windows 這個全域上。