# [JS201] 進階 JavaScript:那些你一直搞不懂的地方 ###### tags:`JavaScript 觀念` [TOC] ## 變數 prmitive type is immutable,例如: ```javascript= var a = 'abc' a.toUpperCase() console.log(a) //abc // 如果是 obj 的話則會改變 ``` object type is reference type ,會指向相同的記憶體,例如: ```javascript= var obj1 = { number: 10 } var obj2 = obj1 obj2.number = 20 console.log(obj1, obj2) //20, 20 ``` 讓你摸不透的 = 賦值, = 賦值會指向不同的記憶體,例如: ```javascript= var obj = { number: 10 } var obj2 = obj1 obj2 = [] console.log(obj, obj2) //{ number: 10 } [] ``` == 與 === 的差別 有套規則再走,[轉換表](JavaScript-Equality-Table) == 會做型態轉換,規則有點複雜 === 在 objtect type 之下,比較的是記憶體位置,例外`NaN 不等於任何東西` ```javascript= var obj = { number: 10 } var obj2 = obj1 obj2 = [] console.log(obj, obj2) //false ``` 變數的生存範圍,函式A裡面再一個函式B的話,scope chain 會是 B => A => global ## 從 Hoisting 理解底層運作機制 > 甚麼時候會有 hosting? > hosting 發生在宣告變數的時候,當 EC 裡面使用到一個變數,但是這個變數在該 EC 裡面卻找不到宣告,就會往上層 EC 找(hoisting),一職都找不到的話就會把宣告提升到全域 > 執行函式在上面,宣告函式在下面 > 還有個原因是不提升的話,沒有辦法達成 function 互相呼叫。 * 先尋找存不存在,不存在再 hoisting? 內層使用外層的已宣告變數也是一種 hosting * 「變數宣告」、「參數」、「函式」會被提升 ```javascript= console.log(a) // undefined var a = 10 // equals to var a // hoisting console.log(a) // undefined a = 10 ``` ```javascript= test(10) // 10 function test (a) { // hoisting console.log(a) } ``` ```javascript= test(10) // undefined var test = function (a) { // hoisting console.log(a) } // equals to var test // hoisting test(10) // undefined test = function (a) { console.log(a) } ``` Hoisting 會提升到哪裡?提升到變數有被宣告或是 global ```javascript= var a = 1; function test(){ var a = 7; inner() function inner(){ // var a 不會發生 console.log('3.', a); a = 30; b = 200; } } test() ``` ### 理解 Execution Context 與 Variable Object * 發生 Hoisting 的權重: 函式 > arguments > 變數宣告 * 初始化完之後才會做賦值(不是絕對,下面 closure 段落的變數 obj1 獲得匿名函式是因為函式執行的結果回傳該匿名函式),也就是說變數一開始會是 undefined,等初始化完之後再給值 * 當我們在進入一個 EC 的時候,會按照順序做以下三件事 1. 把參數放到 VO 裡面並設定好值,傳什麼進來就是什麼,沒有值的設成 undefined 1. 把 function 宣告放到 VO 裡,如果已經有同名的就覆蓋掉 1. 把變數宣告放到 VO 裡,如果已經有同名的則忽略 每 call 一個函式,都會產生一組 EC/VO (包括參數也會初始化): ![](https://i.imgur.com/USc8PKl.png) ![](https://i.imgur.com/qbu0slK.png) 適用 var 宣告的變數,let const 不適用這規則 ![](https://i.imgur.com/Z1pT7E8.png) ![](https://i.imgur.com/jm5olBo.png) ### let 與 const 的詭異行為 ==變數宣告 let , const 到給變數賦值以前,都會存在於 TDZ==:Temporal Dead Zone,在 TDZ 裡面的時候,變數是不能被存取的 ```javascript= function test() { yo() // c 的 TDZ 開始 // ReferenceError: Cannot access 'c' before initialization let c = 10 // c 的 TDZ 結束 function yo(){ console.log(c) } } test() ``` 執行流程大概是這樣 ![](https://i.imgur.com/Kbavzeo.png) test EC scopeChain 忘記放到圖片了...。 ## 從 Closure 更進一步理解 JS 運作 總結,Closure,在一個函式裡面回傳另一個函式。 > 下面的範例要分清楚什麼是"宣告"函式,什麼是"執行"函式 > 小括號和大括號一起出現 > 宣告,只有小括號 >執行。用變數來代替函式 > 宣告,用變數和小括號 > 執行。 假設有一個函式, ```javascript= function complex(num) { return num * num * num } ``` 我們不希望每次傳入相同的 num 時都重複做一次計算然後回傳,曾經傳入的 num 就不要再做一次計算,直接回傳 + 印出結果就好,例如這樣, ```javascript= obj1(1) // 1 obj1(2) // 8 ``` 該怎麼做? 我們寫另外一個函式來做這個判斷, ```javascript= function storeComplex(fn) { let result = {} if (result[num]) { return result[num] } result[num] = fn(num) return result[num] } ``` 但這樣做還是沒辦法達到目的,因為每次呼叫函式 `storeComplex` 都會宣告一個新的 `let result = {}`,而且我們該怎麼把參數 `num` 傳進去?所以我們需要再宣告一個變數存放 `result` 並且修改原先 `storeComplex` 的邏輯,最後變成這樣, ```javascript= function complex(num) { return num * num * num } function storeComplex(fn) { let result = {} return function (num) { if (result[num]) { // 這裡不能用這種寫法 result.num return result[num] } result[num] = fn(num) return result[num] } } const obj1 = storeComplex(complex) // obj1 用來宣告一個匿名函式 // function (num) { // if (result[num]) { // return result[num] // } // result[num] = fn(num) // return result[num] // } console.log(obj1(1)) // obj1(1) 相當於把 1 丟給那個匿名函式 console.log(obj1(2)) console.log(obj1(2)) // 這裡不會再做一次運算 ``` 再延伸,上面程式碼的 EC 會怎麼跑? > 畫在紙本筆記裡 > 算是影片 再次 cosplay JS 引擎 的複雜化版本 ### 初始化的時候,會產生下列物件(?),注意,是初始化,不是執行: * 每個 EC 都有一個 scope chain 會被建立,scope chain 的內容是 自己的 AO + 自己的`.[[Scope]]`內容 * 函式的 EC 裡面是 AO,非函式的 EC 裡面是 VO * 在 EC 裡面,如果有函式被宣告,就會有額外的 `.[[Scope]]` 屬性,它的內容會跟宣告時產生的 EC 的 scopeChain 內容相同 ![](https://i.imgur.com/DyfAzGL.png) > 上面這張圖片的程式碼寫得不完整 ![](https://i.imgur.com/LzULHzr.png) **注意** 宣告和執行的差異之一是: 宣告函式 => 有`.[[Scope]]` 執行函式 => 沒有產生`.[[Scope]]`,而是從它開始尋找需要的變數 ### 日常生活中的作用域陷阱 * IIFE,立刻執行匿名函式的方法 ![](https://i.imgur.com/Ndtbr3h.png) ![](https://i.imgur.com/WCqZmLs.png) ![](https://i.imgur.com/e7bpSXE.png) ### Closure 可以應用在哪裡? ![](https://i.imgur.com/HFAe6oV.png) ![](https://i.imgur.com/DflJenV.png) (OOP...!!) ## 物件導向基礎與 prototype * class name 需要是大寫開頭 * 屬於 class 的函式不需要加上 function prefix * ==constructor 是一個特殊函式,它在 instance 產生(new)的時候會自動執行==,例如 ```javascript= class Dog { constructor (name) { this.name = name } getName() { return this.name } sayHello() { console.log('hi i am ', this.name) } } const dog1 = new Dog('bunny') dog1.sayHello() // 我們沒有執行 this.name = name 但還是可以取得 this.name,因為在 instance 產生的時候就已經做了這件事了 ``` ### class 在 ES6才出現,==ES5==之前要用 ==prototype, new== 來達成 OOP 以上面段落的 ES6 class 範例為例,ES5 裡面想做到同樣的事情需要這樣做↓ * ES5 裡面的 constructor 是一個函式,使用 new 來指定該函式是一個 contructor, ```javascript= function dog (name) { this.name = name } const dog1 = new dog('bunny') console.log(dog1) const dog2 = new dog('bunny2') console.log(dog2) ``` * 其餘的 class 函式則放到 protorype 裡面 ![](https://i.imgur.com/WIu8zhL.png) ### instance 使用`.__proto__` 這個屬性來串接 instance 媽媽的`.prototype` * 變數都會擁有 .__proto__ 這個屬性(非絕對,一直往上層找的話最後會是 null) ```javascript= let a = 1 // undefined a.__proto__ // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …} a.protorype // undefined a.__proto__.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …} a.__proto__.__proto__.__proto__ // 這個就是原型鍊 // null ``` * `.__proto__` 和`.prototype`的關係 ![](https://miro.medium.com/max/2625/1*nDBFaMpflmSsIKfMLxWIvQ.jpeg) ![](http://yycjs.com/the-weird-parts/images/proto-class.png) * 繼承間的關係會是:Object 生出 Instance,Function 生出 funtcion。所以下面的程式碼的結果會是這樣。 ![](https://i.imgur.com/UOmA1l4.png) ```javascript= function Dog(name) { this.name = name } var d = new Dog('nick') console.log(d.__proto__.__proto__ === Object.prototype) // true,因為`Dog.prototype`是個物件 console.log(Dog.__proto__ === Function.prototype) // true,因為 Dog 其實就是個 Function 的 instance console.log(Function.prototype.__proto__ === Object.prototype) // true ``` 再舉個影片的例子, 通過 Dog 這個函數所 new 出來的 d 會具有`.__proto__` 屬性,它指向`Dog.prototype`。 `Dog.prototype.__proto__` 則指向 `Object.prototype`,因為`Dog.prototype`是個物件 ![](https://i.imgur.com/owQVPBP.png) ### new 所做的事情 這裡不使用 new 這個關鍵字,而是自己寫一個函式來達成它的工作 ![](https://i.imgur.com/SklbEFh.png) ### 物件導向的繼承:Inheritance ![](https://i.imgur.com/Tr6r3Ot.png) 所以 new 做的事情就是: 1. 創造一個 obj, O 1. call() constructor 函式,把 constructot 的屬性存放到 O 1. 把O`.__proto__` 串接 O 媽媽的`.prototype` 1. 回傳 O ## 先學完物件導向,學 this 才有意義 :::warning this 這邊我寫得不好,直接看HW和專文比較好 ::: this 跟函式怎麼被呼叫有關(arrow function 是例外) * 非 OOP 呼叫函式,this 就是 global (在沒有意義的地方呼叫 this,預設值會是什麼?) * OOP 的方式呼叫函式,this 就是 instance 本身(call 與 apply) * 用物件來呼叫函式,this 可以用 call 來判斷(用另一種角度來看 this 的值) ### 在沒有意義的地方呼叫 this,預設值會是什麼? 在寬鬆模式下,global this => Window(瀏覽器)/global(Nodejs) 在嚴格模式下(`'use strict'`),global this => undefined ### 另外兩種呼叫 function 的方法:call 與 apply `.call()` 和 `.apply()` 用處是一樣的,都是用來呼叫函式並設定函式的 this 的值和傳參數進去,只是前者參數是一個一個傳,後者參數是用==陣列==的形式傳的 ![](https://i.imgur.com/htdjiYg.png) > 為什麼用額外用 call/apply 來呼叫函式,直接呼叫函式不好嗎? 詳見下面程式碼,因為 this 所指向的內容是會因應它被呼叫的方式或者是 arrow function 而改變 ```javascript= const obj = { '0': 1, '1': 2 } Array.prototype.first = function() { return this } console.log(Array.prototype === Array.prototype.first()) // true console.log(Array.prototype.first()) // Object(0) [ first: [Function (anonymous)] ] console.log(Array.prototype.first.call(obj)) // { '0': 1, '1': 2 } ``` ```javascript= function log() { console.log(this); } var a = { a: 1, log: log }; var b = { a: 2, log: log }; log(); // instrument.ts:129 Window {0: Window, 1: global, window: Window, self: Window, document: document, name: "", location: Location, …} a.log(); // instrument.ts:129 {a: 1, log: ƒ} b.log.apply(a) // instrument.ts:129 {a: 1, log: ƒ} ``` ### 用另一種角度來看 this 的值 快速判斷 this 的內容的方法 ```javascript= const obj = { a:123, b: { fn: function () { console.log(this) } } } const a = obj.b.fn console.log(a()) // a() 可以理解成 umdefined.a.call(undefined) // 同理 obj.b.fn() 可以理解成 obj.b.fn.call(obj.b) // 上面兩個註解同樣是使用同一個函式,但是 this 卻會不一樣,因為 this 的值跟函式怎麼被呼叫有關 ``` ![](https://i.imgur.com/kRVE72l.png) ### 強制指定 this:bind() ```javascript= const obj = { a:123, b: { fn: function () { console.log(this) } } } const a = obj.b.fn.bind('ffdaf') // 用 bind 來綁定 fn 的 this a() // .bind() 跟 call, apply 不同,它會回傳一個函式 ``` ![](https://i.imgur.com/I2mDZoo.png) ### arrow function 的 this arrow function 的 this 跟函式怎麼被呼叫沒關系而是跟它的 function scope 有關 > 在宣告它的地方的 this 是什麼,它的 this 就是什麼 ```javascript= const obj = { a:123, b: { test: console.log('this ', this), // 不好的寫法(?),他會馬上執行 fn: function () { console.log('fn', this) }, fn2: () => { console.log('fn2', this) } } } // obj.b.fn() obj.b.fn2() // arrow function 的 this 是個例外, this 的值跟函式是怎樣被呼叫的無關,而是跟宣告函式的位置有管(scope) // 所以 fn2 和 test 的 this 其實是一樣的 ``` ![](https://i.imgur.com/XXJ9MOw.png) ## event loop ![](https://i.imgur.com/kr7KfLc.png) ![](https://i.imgur.com/x8iwiYq.png) ## 練習題 https://github.com/Lidemy/mentor-program-4th/issues/16 ### ANS ```javascript= // 題目說明請參考: // https://github.com/Lidemy/mentor-program-4th/issues/16 export class Robot { constructor (x, y) { this.x = x this.y = y } getCurrentPosition () { return {'x': this.x, 'y': this.y} } go (direction) { if (direction === 'E') return this.x += 1 if (direction === 'W') return this.x -= 1 if (direction === 'S') return this.y -= 1 if (direction === 'N') return this.y += 1 } } export function debounce(fn, delay) { let timer = null return function (...args) { if(timer) { clearTimeout(timer) } timer = setTimeout(() => { fn(...args) }, 250) } } export function memoize(fn) { const storage = {} return function (num) { if(storage[num]) { return storage[num] } storage[num] = fn(num) return storage[num] } } ```