JavaScript 核心觀念(42)-函式以及 This 的運作-this:call, apply, bind 與 嚴謹模式

前言

接下來將會介紹 this 很特別的東西,也就是 callapply 以及 bind,最後也會提到嚴謹模式對於 this 的用途是什麼。

call, apply 以及 bind

首先這章節將會介紹「call, apply 以及 bind」這三個方法,而這三個方法其實會影響 this 的呼叫,但這邊要注意,這三個方法其實觀念都是非常接近的,因此就非常容易混淆。

首先先看基本的範例程式碼

1
2
3
4
5
6
7
8
var obj = {
name: 'Ray'
}

function fn () {
console.log(this); // window
}
fn();

在此上方是我們前面章節的 simalp call 狀況,因此 this 就會指向 Global,那麼當我們希望 this 重新指向到 obj 的話該怎麼做呢?首先我們可以先使用 call

這邊先從 MDN 對於 call說明,以下擷取 MDN 說明

你可以在呼叫一個現存的函數時,使用不一樣的 this 物件。 this 會參照到目前的物件,呼叫的物件上。

這是什麼意思呢?簡單來講我們可以指定 this 的指向,而寫法如下:

1
2
3
4
5
6
7
8
var obj = {
name: 'Ray'
}

function fn () {
console.log(this); // { name: 'Ray' }
}
fn.call(obj);

此時就可以發現 this 的指向到 obj 了,除此之外假設若函式本身還有其他參數要傳入,那麼就可以接著寫在 calll 之後,因此 call 第一個參數就會是 this 想要指向的地方,而這之後則會是函式的傳入參數

1
2
3
4
5
6
7
8
var obj = {
name: 'Ray'
}

function fn (a, b) {
console.log(this, a, b); // { name: 'Ray' }, 1, 2
}
fn.call(obj, 1, 2);

接下來是 applyapply 其實也是類似的觀念,因此寫法也可以改成這樣

1
2
3
4
5
6
7
8
var obj = {
name: 'Ray'
}

function fn () {
console.log(this); // { name: 'Ray' }
}
fn.apply(obj);

但是 applycall 還是有差異的,首先差異是什麼呢?在 call 的後方是以參數的形式傳入,而 apply 則必須採用陣列方式傳入函式的參數

1
2
3
4
5
6
7
8
var obj = {
name: 'Ray'
}

function fn (a, b) {
console.log(this, a, b); // { name: 'Ray' }, 111, 222
}
fn.apply(obj, [111, 222]);

而這邊我也擷取 MDN 對於 apply 的說明

apply 與 call() 非常相似,不同的是支援的傳入參數類型。使用陣列形式的參數,而不是命名過的接收參數。

最後來說一下 bind,首先 bindapplycall 最大差異在於,bind 並不會立刻執行,而 applycall 是會立刻執行的,這邊是什麼意思呢?來看一下範例程式碼

1
2
3
4
5
6
7
8
var obj = {
name: 'Ray'
}

function fn (a, b) {
console.log(this, a, b); // { name: 'Ray' }, 111, 222
}
fn.bind(obj, [111, 222]);

當如果你將上方程式碼貼入到瀏覽器的 console 你會發現只會出現 fn 函式的本身而不會呼叫函式,因此就必須宣告一個變數並呼叫該函式才可以

1
2
3
4
5
6
7
8
9
10
var obj = {
name: 'Ray'
}

function fn (a, b) {
console.log(this, a, b); // { name: 'Ray' }, 111, 222
}
var fn2 = fn.bind(obj, 111, 222);

fn2();

因此可以看到當我們呼叫 fn2() 時,bind 它就會將 this 指向到 obj 中。

除此之外 bind 還有一個特定就是,當你已經傳入特定參數之後,後來想再傳入其他參數是無法的

1
2
3
4
5
6
7
8
9
10
var obj = {
name: 'Ray'
}

function fn (a, b) {
console.log(this, a, b); // { name: 'Ray' }, 111, 222
}
var fn2 = fn.bind(obj, 111, 222);

fn2(333, 444);

而這部分的規則很簡單,只要你有先傳入參數,那麼 bind 就會自動幫你鎖定參數,因此若你不想一開始就傳入特定參數,而是希望後來才傳入,那麼就可以改成以下

1
2
3
4
5
6
7
8
9
10
var obj = {
name: 'Ray'
}

function fn (a, b) {
console.log(this, a, b); // { name: 'Ray' }, 111, 444
}
var fn2 = fn.bind(obj, 111);

fn2(444);

在此前面章節範例其實我們都是透過傳入一個物件給 this,因此其實傳入純值也是可以的

1
2
3
4
function fn (a, b) {
console.log(this, a, b); // String{ "Ray" }, 1, 2
}
fn.call('Ray', 1, 2);

基本上這邊看到這個結果應該會有點特別,this 出來是一個建構式的方式去呈現,但是這邊有一個好玩的地方就是,如果你傳入的是 undeinfed,那麼 this 就會改指向 window

1
2
3
4
function fn (a, b) {
console.log(this, a, b); // window, 1, 2
}
fn.call(undefined, 1, 2);

那麼是什麼原因造成這種狀況呢?其實 MDN 有說明

注意,它可能是一個無法在函數內看到的值:若這個函數是在非嚴苛模式( non-strict mode ), null 、undefined 將會被置換成全域變數,而原生型態的值將會被封裝

因此這就是為什麼 this 會指向到 window 與出現的是一個建構式。

嚴格模式

接下來來講一下什麼是嚴格模式

嚴格模式

嚴格模式是什麼呢?由於 JavaScript 在開發上是相對寬鬆且自由的程式語言,因此在開發上就很容易發生一些問題,那麼嚴格模式就是在避免我們發生這些狀況,以避免會出現問題的程式碼還是可以運作

嚴格模式說明

那這個最主要特色在什麼呢?例如當你 this 如果是指向 window 那麼就會出現錯誤,例如 sample call 會變成指向到 undefined

1
2
3
4
5
6
7
'use strict'

function fn () {
console.log(this); // undefined
}

fn();

而變數的話若沒有給予出初值則會出現「VM543:3 Uncaught ReferenceError: a is not defined

1
2
3
'use strict'

a = 1;

除此之外,使用 'use strict' 若在 ES6 ES7 ES8 等未來版本可能會使用的語法,你也會被禁止使用,避免佔用該語法。

'use strict' 還有一個特別的地方,也就是它必須在程式碼最前面才有效,否則以下都無效

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn () {
console.log(this); // window
}

fn();

'use strict'

function fn1 () {
console.log(this); // window
}

fn1();

但我們可以用執行環境來限制

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn () {
console.log(this); // window
}
fn();
(function() {
'use strict'

function fn1 () {
console.log(this); // undefined
}

fn1();
})()

因此嚴格模式是依照執行環境來限制的。

而嚴格模式對於 call 也有一個幫助,它可以避免 this 變成建構式

1
2
3
4
5
function fn (a, b) {
'use strict'
console.log(this, a, b); // "Ray" , 1, 2
}
fn.call('Ray', 1, 2);

最後這邊也可以參考一下 MDN 對於嚴格模式的說明

參考文獻