0%

前言

今天要介紹的 JS 觀念是 call, apply 和 bind 三個 function
這些我目前為止都沒用過
因為在過去的專案中,幾乎沒用到 OOP (Object Oriented Programming)
class 也沒用過
自然就沒遇到適合的使用情境
但有時還是會在一些教學影片看到他們的身影
(即使那不是影片主題)
我就覺得應該要來補一下

this 簡介

this是在 javascript 中的重要語法
他指向的是函式被調用的位置

如果是物件調用函式,this就是那個物件
在瀏覽器中直接印this,就是window物件
在node.js直接印this,就是global物件
在嚴格模式中直接印this,就是undefined

如果我們想為物件打造一個 function
就可以在函式中用上this

1
2
3
4
5
const newFriend = {name: 'Josie'}
const greeting = function(myName){
console.log(`Hi, ${this.name}, my name is ${myName}`)
}
// greeting(???)

這時突然發現物件沒有進入點
解決方法可以把 greeting 定義在物件裡面
或是物件之上如果有一個類別的話
也可以定義在prototype裡面
但我們也可以改用 call, apply, bind 來呼叫函式
接下來就來一一介紹

call

call 的功能是執行函式,並指定函式中的this是誰

1
2
3
4
5
6
7
8
9
10
11
12
13
// function.call(object, arg1, arg2, arg3, ...)
// 執行 function,傳入參數 arg1, arg2 ...,並指定函式中的 this 為 object

const newFriend = { name: 'Josie' }

const greeting = function(myName, otherName) {
return console.log(`Hi, ${this.name}, my name is ${myName}. And this is ${otherName}.`)
}

greeting.call(newFriend, 'Tony', 'Bowen')


// Hi, Josie. My name is Tony. And this is Bowen.

apply

基本上跟 call 做一樣的事情
但提供另一個傳入引數 (argument) 的方式
改成傳入包含所有引數的陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// function.apply(object, [arg1, arg2, arg3, ...])
// 執行 function,傳入參數 arg1, arg2 ...,並指定函式中的 this 為 object

const newFriend = { name: 'Josie' }

const greeting = function(myName, otherName) {
return console.log(`Hi, ${this.name}, my name is ${myName}. And this is ${otherName}.`)
}

const arr = ['Tony','Bowen']

greeting.apply(newFriend, arr)

// Hi, Josie. My name is Tony. And this is Bowen.

bind

將函式中的 this 綁定給某個對象,然後回傳一個新的函式供使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// function.bind(object) 
// return function
// 將 function 和 object 綁定,回傳一個新 function

const newFriend = {name: 'Josie'}

const greeting = function(myName, otherName){
return console.log(`Hi, ${this.name}, my name is ${myName}. And this is ${otherName}.`)
}

const greetToNewFriend = greeting.bind(newFriend)

greetToNewFriend('Tony','Bowen')

// Hi, Josie, my name is Tony. And this is Bowen.

結論

callapply: 執行函式並指定this所指對象
bind: 回傳一個新函式,是原函式與this所指對象綁定的版本

基本上除非函式中包含this
否則不太會用到這三個函式

前言

這次要講一個我在做專案時一直被他搞死的概念
深拷貝與淺拷貝
雖然已經知道這個概念
相關文章也看到好幾次了
但每次還是會在專案中卡好久好久找不到 bug
最後才發現說:「啊 原來又是這個問題啊」

Call by value/reference

首先要釐清的觀念是 call by value 跟 call by reference

在 javascript 中物件是採用 call by reference

也就是例如當你把已存在記憶體中的物件指派到a變數時
a會指向該記憶體位置,而不是複製其值
讓我們看個例子

1
2
3
4
5
const A = {num: 1}
const B = {num: 1}
const C = A
console.log(A===B) // false
console.log(A===C) // true

當我互相比較這三個物件時
會發現AB明明內容相同,卻回傳 false
只有當C是從A assign 來的時候才得到 true
因為其實 A 和 C 是共用同一個記憶體
而物件的比較是去看記憶體位置
所以得到 true
AB雖然內容一樣,卻因為都是新創的物件
他們會占用不同的記憶體位置
對 JS 來說就是不一樣的囉

淺拷貝

上述的 const C = A 就是淺拷貝的一種
他沒辦法做到完全的複製
而是在「某程度」上保有指向相同記憶體的特性

1
2
3
4
5
const A = {num: 1}
const B = A
console.log(A,B) //{num: 1}, {num: 1}
A.num = 10
console.log(A,B) //{num: 10}, {num: 10}

我只更動A,結果B的內容也改了
很多時候出現 bug 就是因為這個原因

至於剛剛為什麼要強調「某程度」呢?
因為有些做法只能在第一層上進行深拷貝
當你的物件結構更複雜時就會出問題
來看一個被誤以為是深拷貝的技巧 spread operator

1
2
3
4
5
const A = {num: 1}
const B = {...A}
console.log(A,B) //{num: 1}, {num: 1}
A.num = 10
console.log(A,B) //{num: 10}, {num: 1}

看似沒問題,但如果改成二階以上的物件的話…

1
2
3
4
5
const A = {num: {ch: '一'}}
const B = {...A}
console.log(A.num.ch, B.num.ch) // "一", "一"
A.num.ch = '四'
console.log(A.num.ch, B.num.ch) //"四", "四"

另外還有一個做法是 Object.assign()
但結果跟解構賦值一樣
僅限於一階物件

深拷貝

我個人最喜歡的作法是用JSON.stringifyJSON.parse
直接把物件文字化再物件化一次
就一定會是另一個物件了
達到深拷貝的效果,像這樣:

const B = JSON.parse(JSON.stringify(A))

或是如果嫌每次都這樣寫麻煩
可以在專案中定義一個 deepClone function

1
2
3
4
5
6
7
8
const deepClone = (obj) => JSON.parse(JSON.stringify(obj))

const A = {num: {ch: '一'}}
const B = deepClone(A)

console.log(A.num.ch, B.num.ch) //"一", "一"
A.num.ch = '四'
console.log(A.num.ch, B.num.ch) //"四", "一"

結論

淺拷貝: 看似複製,其實是指向相同位址
深拷貝: 另用一個記憶體存值,實質意義上的複製

前言

前兩篇的 JS 概念篇,分別整理了 scope 和 closure
這篇就來整理一個 javascript 面試常見的題目

經典問題

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, 0)
}

// 5,5,5,5,5

第一個重點是
setTimeout 會將 console.log 函式放在暫存區
for loop 結束後一共會放五個 console.log 在那邊

第二個重點是 var
在上一篇中我們提到 var 變數的生存範圍是 function
所以i的 scope 就會在 for loop 之外
所以我們可以理解成 for loop 的每個循環
都會共用同一個i (指reference)

閉包會參照環境變數值,而不是複製其值

而最後當程式要來處理暫存區的任務時
由於 local variable 中沒有i
便往上找 function variable i
而 loop 也已經結束,i已經從 0 變成 5 了
可想而知就會印出五次 5

解法1: let

這題有很多解法
下面這個應該是最簡單的了吧

1
2
3
4
5
6
7
for (let i = 0; i < 5; i++) { // 把 var 換成 let
setTimeout(function() {
console.log(i)
}, 0)
}

// 0,1,2,3,4

當我們改用 let 時
i的生存範圍便限制在每個 loop block 當中
有幾個迴圈,i就會產生幾次
setTimeout 在 block 中找得到i,便會記住每個 loop 的i
而不會像原本一樣由於在區域變數中找不到
就往上層找函數變數
這是 ES6 之後的新解法
利用 let 生存範圍的特性

解法2: 閉包

我們可以利用閉包的特性鎖住每個i

1
2
3
4
5
6
7
8
9
10
for (var i = 0; i < 5; i++) {
const timer = (n) => { // 記得定義 parameter n
setTimeout(function() {
console.log(n)
}, 0)
}
timer(i) // 記得傳入 i
}

// 0,1,2,3,4

每次運行timer(i)時會產生一個閉包
同時每次都會產生新的參數n
用來儲存當下傳入的i
所以會得到正確的結果

解法3: IIFE

Immediately-Involked Function Expression (IIFE)
立即呼叫函式表達式
定義函式之後立刻呼叫的寫法
它具有儲存當下環境的特性
是形成閉包的作法之一
以非常白話來解釋:

讓函式立刻運行,而且不用取名

1
2
3
4
5
6
7
8
9
for (var i = 0; i < 5; i++) {
(function(value) {
setTimeout(function() {
console.log(value)
}, 0)
})(i)
}

// 0,1,2,3,4

追根究柢,這其實跟方法二幾乎一樣
只是寫法不同而已

結論

這個題目 tricky 的部分在於
原本應該要利用閉包的特性鎖住每個i
卻又因為閉包只會參照位置
而導致非同步函式事後取到錯誤的值

本篇所用的三個解法的中心思想是

不要參照外部變數

只要不要參照外部變數,讓每次迴圈都產生一個變數來用
便能避免事後才取到改變後的變數

老實說到現在還沒辦法肯定的說我清楚了
但藉由思考這個題目,對於 scope, closure 肯定會比以前更有概念
也更了解 javascript 的運作方式