---
# System prepended metadata

title: JavaScript 概念篇 - Event Loop、Scope、Hoisting、Closure、Prototype、this
tags: [Lidemy]

---

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