# 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];
};
}
```