# JavaScript 概念篇 - Event Loop、Scope、Hoisting、Closure、Prototype、this ###### tags: `Lidemy` 以下為 Lidemy [MTR05]-week 16 課程筆記,如有錯誤,歡迎留言/寄信通知,感謝。 ## Event Loop 如其名,不斷執行的 Event,會不斷偵測 Call Stack 是否為空,如果是空的話就把 Callback Queue 裡面的東西丟到 Call Stack。 接下來要分別說明 Call Stack, Callback Queue 及 Web APIs。一開始先觀看大神影片 "What the heck is the event loop anyway?",影片中的動畫很醒目,看完大概知道有三個主要區塊,也就上面提到的那三個用詞,並且講述程式碼有分同步跟非同步。後來閱讀不同文章/影片(一開始沒發現老師文章有講 Event loop,在做完 hw 1 跟 hw 2 後才看到!QQ),認識 JS 為 single thread,程式執行後會先進入 Call Stack,而執行順序則是 "First In, Last Out (先進的最後出去)",因為它運行是以堆疊的方式進行: ``` Last mission . . . third mission second mission first mission ``` Call Stack 的執行順序會先將 Last mission 解決,一步一步到最後的 first mission。 第二塊則叫 Web APIs,專門接收非同步的程式碼 e.g. DOM、AJAX、setTimeout。同步的程式碼執行完,Web APIs 才會開始執行,並在執行後丟到 Callback Queue,也就是第三個區塊。而 Callback Queue 的執行順序則是"先進的先出去"。 大致講解完這三塊後,就可以認識主角 - Event Loop,它就是"不斷偵測 call stack 是否為空,如果是空的話就把 callback queue 裡面的東西丟到 call stack"。 最後,影片中提到的測試網頁[loupe](http://latentflip.com/loupe/?code=!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D)可供自由使用,hw 1 跟 2 有搭配它來驗證步驟是否正確。 心得:原來出錯時會在程式碼印出 stack stack overflow 科科 ## Scope 作用域,一個變數的所在範圍,一旦離開這個範圍,就無法存取這個變數。在 ES6 以前,唯一產生作用域的方法就是 function,而每個 function 都是獨立的作用域。有一句比喻很適合形容作用域:外面的看不到裡面的,但裡面的看得到外面的。意思是什麼呢?是說你的 function 裡可以存取外面(上方層級)的變數,但外面的作用域卻無法存取 function 裡的變數。 例如: ```js function fn() { var a = 5 } console.log(a) // ReferenceError: a is not defined fn() ``` 因為 a 的值是存在於 fn 這個 scope 裡,因此外面不會拿到。相反,若 a 存在全域上,則可以存取: ```js var a = 5 function fn() { console.log(a) // 5 } fn() ``` 在 ES 6 開始,作用域分為 Global scope 與 Block scope,Global scope 也就是全域、全域變數,在任何地方都能存取到。Block scope 可以是函式、for 迴圈、if else 等。若在 Block scope 裡"僅賦值"變數而"沒宣告",JS 會將這個變數宣告在 Global scope 中。 自由變數... 而 Scope 有分靜態 (static) 與動態 (dynanmic), ## Hoisting "提升",理論上程式碼是由上到下來執行,但若對一個尚未宣告的變數取值,系統會回傳 `__ is not defined`。 然而,若是在取值後才宣告呢? e.g. ``` console.log(x) var x ``` 這時系統會回傳:`undefined`,這就是所謂的 hoisting,將宣告的變數"提升"到取值前的位置,但有一點要注意的是,僅把宣告的變數"提升",而其賦值將不會一起"被提出",例如: ``` console.log(x) var x = 88 // 提升後的想像 var x console.log(x) x = 88 ``` 因此,結果仍是 undefined。 另外,宣告中也有分順位,function 的提升權較高,例如: ``` console.log(x) var x function x () { } ``` 印出的結果是 `[Function: x]` 小結論: 凡宣告的(函式/變數)都會被提升,賦值不會。函式裡有傳進來的參數會影響提升的行為。 原理:............ ## Closure 在一個 function 中回傳一個 function。 當一個函式跑完後,它會將其所有的資源給釋放掉,意思是裡面的變數或其他東西都不會存在了。 以上面 Scope 篇提到的例子說明: ```js function fn() { var a = 5 a++ } console.log(a) // ReferenceError: a is not defined fn() ``` a 的值在執行完函式 fn 後釋放,因此無法拿到它的值。但是,如果我們要從外層拿到 function 裡的值呢?做法是在 function 裡 return 值,並用外層的變數把它接住,將上方的例子改寫成: ```js function fn() { var a = 5 function fn2() { a++ return a } return fn2 // *注意,這邊是 return 一個 function,不是要執行,因此不用加 ();fn 是一個 function,fn() 是執行 function } // 宣告變數來接函式內的值 var result = fn() console.log(result()) // 6 console.log(result()) // 7 ``` 上面的程式碼,可以把 resualt 變數想像成: ```js function result() { a++ return a } ``` 可能你會問,那把 a 變數放到全域不就解決問題了嗎?不是沒道理,但使用 closure 能確保你在 function 裡的值不會被更改到,例如存在全域的話: ```js var a = 5 function fn() { a++ } console.log(a) //6 a = 0 console.log(a) //1 ``` 在全域中賦值,改變了 a 的變數,但若將 a 放入 function 中,就不會受到外部影響。 熟悉後,來將 closure 弄得更簡潔: ```js // 原本 function fn() { var a = 5 function fn2() { a++ return a } return fn2 } var result = fn() console.log(result()) // 6 // 簡化版 function fn() { var a = 5 return fn2() { a++ return a } } var result = fn() console.log(result()) // 6 ``` 總結:closure 就是 function 裡再 return function。當我們執行完 function,它的資源就會被釋放掉、不存在,而 closure 的作用就是用於保存它的資源,如同上述例子中的變數 a,當我們執行完`fn()`後,它就 bye bye 消失了,但透過 closure,將它 a 的值給保存起來。 ## Prototype in JavaScript 在講 Prototype 前,要稍微提到物件導向概念,物件導向會有 Class (類別) 跟 物件(Object),Class 類似藍圖、樣板模型,而 Object 則是一個依照 Class 去建構的實體。例如:建築模板是 Class,而實體的房子則是 Object;工廠的模具是 Class,成品則是 Object。然而在 JS 的原生語法中,並無 Class 這個語法存在,因此使用上會以 function 做為 Constructor。JS 使用 Constructor(建構子)特殊函式,來定義物件與功能。而在 ES6 的 Class 中,其實也隱藏著 Constructor。 ```js // ES6 class Animal { setName(name) { this.name = name return this.name } } var cat = new Animal() console.log(cat.setName('cat')) // cat ``` ```js // ES6 前無 class 的語法,以建構子函式搭配 new 做表示 function Animal(name) { this.name = name } var cat = new Animal('cat') console.log(cat.name) // cat ``` 以上是 JS 裡物件導向的基本用法,ES6 即便能使用 Class,但其實底層仍然是下方的寫法,ES6 只是把它弄得比較好看,因此接下來我們以底層的方式做說明。 我們也可以使用上面提到的 closure,在 function 中回傳 function: ```js function Animal(name) { this.name = name this.getName = function() { return this.name } } var cat = new Animal('cat') var dog = new Animal('dog') console.log(cat.getName()) // cat console.log(dog.getName()) // dog ``` 但這會造成一個問題,每次 new 一個 Animal 的時候,會重新產生新的記憶體,因此你會發現: `console.log(cat.getName === dog.getName) // false` 兩個變數執行相同的功能,但卻是分開兩個 function 去執行。這樣做很沒效率,佔記憶體位置。而解決的方法就是使用這個段落的主角 - Prototype。 ```js function Animal(name) { this.name = name } Animal.prototype.getName = function () { return this.name } var cat = new Animal('cat') var dog = new Animal('dog') console.log(cat.getName()) // cat console.log(dog.getName()) // dog ``` 跟上面一樣的輸出,而且: `console.log(cat.getName === dog.getName) // true` 此時變為 true 了,代表他們是同源同位置。接下來就要介紹 prototype 跟 new 的關聯,這邊以 prototype chain(原型鏈)來作說明: JS 中有一個內部的屬性叫`__protp__`,當設定 new 後系統會幫你綁定,以上面的例子來看會是: ```js cat.__proto__ === Animal.prototype cat.__proto__.__proto__ === Animal.prototype.__proto__ === Object.prototype cat.__proto__.__proto__.__proto__ === Animal.prototype.__proto__.__proto__ === Object.prototype.__proto__ === null ``` 因此,當執行 cat.getName() 時,JS 的執行順序會是: ``` 1. cat.getName() 2. 找 cat 身上是否有 getName(),沒有的話執行下一行 3. 找 cat.__proto__ 有沒有 getName(),沒有的話執行下一行 4. 找 cat.__proto__.__proto__ 有沒有 getName(),沒有的話執行下一行 5. 找 cat.__proto__.__proto__.__proto__ 有沒有 getName() 6. null (已找到頂) ``` (待補充) ## JS 中的 this 在 JS 裡,this 有四種情形。 1. EventListener 中的指向值 ```js document.querySelector('button').addEventListenter('click', () => { this // 等同於上面 Selector 中的 button }) ``` 2. 物件導向中的 this 上面 Prototype in JavaScript 的內容 3. 單純的 this 直接在`console.log(this)`會是什麼呢?JS 有不同的 runtime (執行環境),在瀏覽器上的 this,預設值指的是 Window 或 undefined (在嚴格模式下的話)。而在 node.js 的 this,則會是一個全域的 Object。 4. 物件中的 this ```js var obj = { name: 'hello hi', test: function() { console.log(this.name) } } obj.test() ``` 印出的值是: `'hello hi` 在進一步說明前,我們要先了解,在 JS 裡能否改變 this 的值呢? 答案是有三種方法,分別是使用`call()` & `apply()` 及 `bind()`,下列情況先假設在瀏覽器下且非嚴格模式下的輸出: ```js function test(x, y) { console.log(this, x, y) } test(1, 2) // {window...} 1 2 test.call('123', 1,2) // String {'123'} 1 2 test.apply('321', [1,2]) // String {'321'} 1 2 ``` call 跟 apply 的差別只在於傳參數的方式不一樣,appy 需要傳 array 參數。而使用 bind 時要注意,因為它是回傳一個新的 function,因此要用一個 function 去接住它,而執行 bind 之後的值就不會改變了: ``` function test() { console.log(this) } var fn = test.bind('123') fn() // 123 fn.call('111') // 123 ``` this 的值是在被 call 的那瞬間才被決定的,以一般的 function 呼叫換成用 call() 的方式來驗證結果 ```js const obj = { value: 1, hello: function() { console.log(this.value) }, inner: { value: 2, hello: function() { console.log(this.value) } } } const obj2 = obj.inner const hello = obj.inner.hello obj.inner.hello() // ?? obj2.hello() // ?? hello() // ?? ``` ```js obj.inner.hello() // obj.inner.hello.call(obj.inner) => 2 obj2.hello() // obj2.hello.call(obj.inner) => 2 hello() // hello.call() => undefined / window (在非嚴格模式下) ``` ## 範例 ### Class > 一個 Robot 的 class,初始化的時候可以設置座標 x 跟 y 接著 Robot 會有兩個方法,getCurrentPosition 跟 go,前者會回傳現在機器人所在的 x 與 y 座標,後者可以讓機器人往東南西北任一方向移動,需要傳進 'N', "E", 'S', 'W' 任何一個字串,代表要往哪一個方向走 這個世界是我們所熟悉的二維座標系,因此往北走 Y 座標會增加,往南走 Y 座標會減少,往東走 X 座標會增加,往西走 X 座標會減少 ```js export class Robot { constructor(x, y) { this.x = x; this.y = y; } getCurrentPosition() { return { x: this.x, y: this.y }; } go(direction) { switch (direction) { case "N": this.y += 1; break; case "S": this.y -= 1; break; case "E": this.x += 1; break; case "W": this.x -= 1; break; default: console.log("please input N or E or S or W"); } } } ``` ### debounce (closure) ```js function debounce(fn, delay) { let timer = null; return function (...args) { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn(...args); }, delay); }; } ``` ### memoize ```js export function memoize(fn) { let obj = {}; return function (n) { if (!obj[n]) { obj[n] = fn(n); } return obj[n]; }; } ```