JavaScript 核心觀念(45)-函式以及 This 的運作-總結:函式的常見陷阱題

前言

接下來這章節將會集中介紹一些常見的函式陷阱題目唷~

函式的常見陷阱題

首先在課程開頭有介紹到一個粉絲專頁叫做 LevelHunt,這個粉絲頁我稍微繞了一下其實有相當多種語言的考題,其中也包含 JavaScript 考題。

雖然依照我稍微看一下來講,其實還是比較多偏向 JavaScript (JavaScript 比較奇妙吧 xD)。

那麼回來這章節其實也就會從 LevelHunt 裡面挑幾題來介紹,舉例來講以下第一題

1
2
3
4
5
6
7
8
9
10
11
var myName = 'Global';

var person = {
myName: 'Ray',
getName: function() {
return this.myName;
}
}

var getName = person.getName;
console.log(getName());

這一題答案是 Global 其主要原因是 this 是直接透過 sample call 呼叫,因此就必定會指向到 window

接下來第二題

1
2
3
4
5
6
7
8
9
10
11
12
var myName = 'Global';

var obj = {
myName: 'Ray',
fn: function(a, b, c) {
return this.myName + ',' + a + ',' + b + ',' + c
}
}
var fnA = obj.fn;
var fnB = obj.bind(null, 0);

console.log(fnB(1, 2));

這題答案是 Global, 0, 1, 2,首先在前面章節有說過 bind 函式如果在一開始沒有傳入特定參數,那麼就可以後來在傳入,但是也會因此佔用其中一個參數也就是參數 a,而 bind 的第一個參數都是要設定 this 的指向,而在此因為沒有設置,因此就不會傳入,因此就會形成 simaple call。

那麼額外說明一下,如果希望答案是 null 的話則必須使用嚴格模式

1
2
3
4
5
6
7
8
9
10
11
12
13
var myName = 'Global';

var obj = {
myName: 'Ray',
fn: function(a, b, c) {
'use strict';
return this + ',' + a + ',' + b + ',' + c
}
}
var fnA = obj.fn;
var fnB = obj.bind(null, 0);

console.log(fnB(1, 2)); // null, 0, 1, 2

接下來第三題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var value = 'Global';

var foo = {
value: 'local',
bar: function() {
return this.value;
}
}

// 直接執行
console.log(foo.bar());
//賦值
console.log((foo.bar = foo.bar)());
// or
console.log((false || foo.bar)());

看一題老實講有點特別,但是實際上並沒有那麼困難,首先第一個 this 是定義在 foo 物件下宣告的,因此結果就會是 local

第二個 console 答案則是 Global,首先因為賦予直回去之後並沒有執行,因此在賦予之後才執行,因此就會形成 simaple call,而這部分觀念最主要與表達式有關,因此就是表達式回傳之後在執行,因此當你輸入 foo.bar = foo.bar 其實會出現 function() { return this.value; } 因此我們其實是這直接呼叫該函式,因此這一段概念就跟 a = b = 1 是非常雷同的。

那個第二個觀念也是一樣的,只是加上的運算子 ||,而 || 的條件中當第一個為 false 那麼就會回傳第二個,因此就結果一樣會形成 simaple call。

而這邊額外補充一個小題目

1
2
3
4
5
6
7
8
9
var b = {};

Object.defineProperty(b, 'a', {
value: undefined, // 預設值
writable: false, // 不可修改
})

b.a = 'a';
console.log(b.a); // undefined

之所以會講第二個與第三個與表達是有關的原因在於,若你將題目修改成以下,你會發現結果依然是 Global

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var value = 'Global';

var foo = {
value: 'local',
bar: function() {
return this.value;
}
}

var b = {};

Object.defineProperty(b, 'a', {
value: undefined, // 預設值
writable: false, // 不可修改
})

b.a = 'a';

//賦值
console.log((b.a = foo.bar)());

因此這一段賦予值的過程,就是一個表達式,因此實際執行是執行表達式的結果,而不是賦予值得結果。

那麼接下來看看第四題

1
2
var arr = ['1', '2', '3'].map(parseInt);
console.log(arr);

這一題答案會是 '1', NaN, NaN,那麼為什麼是這樣呢?首先 parseInt 可以將輸入的字串轉換為整數,而他很特別的地方在於,若你沒有指定進位的話,就會發生一點事情,這邊擷取 MDN 部分說明

從 2 到 36,能代表該進位系統的數字。例如說指定 10 就等於指定十進位。一定要定義這個參數以避免他人的困惑、也好預估函式的行為。如果沒有指定 radix 的話,給出的結果會按照實做不同而異,請注意,通常預設值不是 10 進位。

除此之外在這邊其實 map 是一個 callback function,因此會傳入餐個參數,分別是 陣列的值、陣列索引以及陣列本身,所以還原一下實際運作結果就會像下方這樣

1
2
3
var arr = ['1', '2', '3'].map(function(item, index) {
return parseInt(item, index);
});

剛剛有說到 parseInt 第二個參數在 MDN 上是進位的意思,而第一次執行程式碼時就會是這樣子 parseInt(1, 0); 而在此並沒有 0 進位,因此就會改走預設值,這邊也直接擷取 MDN 對於這一段的說明

如果 radix 是 undefined 或 0(或留空)的話,JavaScript 會:
如果 string 由 “0x” 或 “0X” 開始,radix 會變成代表十六進位的 16,並解析字串的餘數。
如果 string 由 0 開始,則 radix 會變成代表八進位的 8 或十進位的 10,但到底會變成 8 還是 10 則取決於各實做。 ECMAScript 規定用代表十進位的 10,但也不是所有瀏覽器都支持。因此,使用 parseInt 時一定要指定 radix。
如果 string 由其他字串開始,radix 就會是十進位的 10。
如果第一個字串無法被解析為任何數字,parseInt 會回傳 NaN。

因此當若為預設值時,就有可能是瀏覽器的預設值,通常可能是 8 進位或者是 10 進位,也因此在傳入這個值的時候他就可以正確地轉換出 1

當執行第二個時,就會變成 parseInt(2, 1);,因此當進位模式是 1 的時候,每數值到達 1 的時候,就會進位到下一位,因此就無法正常轉換,因此答案就是 NaN

那麼當執行第三次時,結果也是類似的 parseInt(3, 2);,觀念與前者相同,當進位是 2 的時候,數值每次到達 2 就會進位到下一位,因此 3 並不存在(當然也包含 2 本身)。

參考文獻