跳至主要内容

[JS] Function基本介紹-4

TL;DR

介紹Function.prototype裡面提供的三種預設的方法-Call、Bind、Apply。

參考資料

相關連結


什麼情況會用到?

因為JS是動態的,this會根據調用的方式有所不同。
但是在許多時候,我們要確保函式在調用的時候,this指向的東西是我們期望的,這時候就要使用一些方法來綁定this。

首先,我們透過以下範例先來看這三個方法能做到什麼:

function foo(para1,para2) {
console.log(this.bar,para1,para2);
}

const obj={
bar:'obj的bar!'
}

// 為了讓this指向obj,首先先在ojb內定義一個屬性fn,並且將function foo賦值
obj.fn=foo;
obj.fn(1,2); // obj的bar! 1 2

//使用CAB方法,則可以直接指定this而不需要直接修改原始物件!
foo.call(obj,1,2); // obj的bar! 1 2
foo.apply(obj,[1,2]); // obj的bar! 1 2
foo.bind(obj)(1,3); // obj的bar! 1 2
info

雖然可以透過直接修改obj的方式來讓this指向obj本身

但是這麼做除了會修改到物件。另外在需要調用不同的物件時也需要在每個調用物件內新增function。會造成難以維護以及記憶體的浪費等等。

使用Call、Bind、Apply方法,可以在呼叫的當下才綁定this指向的物件。

可以發現前面都會將Call跟Apply放在前面,Bind放在最後方。這是因為Call的調用方式與Apply非常接近。

MDN-FUnction.prototype.call
此函數的所有語法大致上與 apply() 相同,他們基本上不同處只有 call() 接受一連串的參數,而 apply() 單一的 array 作為參數

另外又可以簡單分成兩類:

  1. callapply會回傳function執行的結果
  2. bind只會回傳綁定this過後的原函式,不會直接執行

call

使用給定的this參數以及分別給定的參數來呼叫某個函數。

語法:

fun.call(thisArg[, arg1[, arg2[, ...]]])

apply

Call類似,只是語法在傳入參數的部分需要使用陣列包裹

語法:

fun.apply(thisArg, [argsArray])
tip

使用arguments搭配call,apply

function foo(arg1,arg2) {
console.log(arg1,arg2);
}

可以透過以下方式綁定apply

function apply_foo(params) {
return foo.apply(this,arguments);
}

ES6之後有新增展開運算符,可以用來展開arguments綁定到call

function call_foo(params) {
return foo.call(this,...arguments);
}

bind

最後來簡單介紹一下bind

語法如下:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

觀察bind語法可以發現,bind在使用時除了this以外,也可以綁定傳入的參數。

MDN-Function.prototype.bind
bind() 方法,會建立一個新函式。該函式被呼叫時,會將 this 關鍵字設為給定的參數,並在呼叫時,帶有提供之前,給定順序的參數。

也就是說,bind並不會直接執行函式,而是如同字面上的意思綁定(bind)我們所需要的this到該函式上。

並且只要該函式被透過bind綁定過,函式的this就無法再次被修改了!

bind不綁定參數

透過以下範例來了解如何使用bind,以及驗證this是否真的無法再次被綁定:

window.a=20

function foo(){
console.log(this.a);
}

const bind_foo=foo.bind(window);
const obj={
a:1,
bind_foo
}

obj.bind_foo(); //20
obj.bind_foo.call(obj); //20
obj.bind_foo.apply(obj); //20
obj.bind_foo.bind(obj)(); //20

可以發現我們不管是透過callapplybind,都沒有辦法再次改變this的指向。所以bind_foo的this永遠的會是window!

bind綁定參數

bind除了綁定this以外,另外也可以綁定傳入的參數。範例如下:

function foo(para1,para2) {
console.log('this',this);
console.log('params',para1,para2);
console.log('arguments',arguments);
}
const obj={
home:'小明家'
}
const bind_foo=foo.bind(obj,'bind傳入的第一個參數');
bind_foo(2,3);
// this {home:'小明家'}
// params 'bind傳入的第一個參數' 2
// arguments(3) ['bind傳入的第一個參數',2,3]

因為我們在透過bind綁定時,就已經先傳入參數了,所以後續我們在執行bind_foo函式並傳入參數時,因為第一個para1已經被bind指定,所以只會額外再印出para2(也就是 2 )

透過觀察arguments可以看到,我們所傳入bind_foo的參數其實是有正確傳入的,但是因為bind時已經有先指定了,所以只能往後排。

修正callback的this

我們有一段程式碼如下:

class Dog{
constructor(myName){
this.myName=myName;
}
sayHello(){
console.log('Hello,I am',this.myName);
}
delayHello(){
setTimeout(function() {
console.log('(after 1 second...)Hello,I am',this.myName);
}, 1000);
}
}

const foo=new Dog('white');
foo.sayHello(); // Hello,I am white
foo.delayHello(); // (after 1 second...)Hello,I am undefined

可以發現這邊setTimeout內的callback function的this並沒有如我們的預期指向建構函式生成的Dog實體(foo)。

要解決這個問題主要有三種解法:

替身(self)解法

第一種方法是很常見的替身攻擊!

如同foo或bar一樣,這種解法也有常見的變數名稱,通常會取名叫self或vm。
self很好理解,就是自己的意思。vm比較常見於vue等等的框架中,代表的是viewmodel。但是本質上的意思都是相同的,主要是希望可以將this先賦值在變數上,方便後續使用。

直接透過程式碼來了解:

class Dog{
constructor(myName){
this.myName=myName;
}
delayHello(){
const self=this; //或是 const vm=this;
setTimeout(function() {
console.log('(after 1 second...)Hello,I am',self.myName); //調用的時候改用self
}, 1000);
}
}

const foo = new Dog('white');
foo.delayHello(); // (after 1 second...)Hello,I am white

透過this的替身-self,我們就可以正確取用到myName。

bind解法

我們都知道this是可以被改變的,所以我們只要直接把callback function的this給綁定寫死,就不會產生this指向錯誤的問題了:

class Dog{
constructor(myName){
this.myName=myName;
}
delayHello(){
setTimeout(
function() {
console.log('(after 1 second...)Hello,I am',this.myName);
}.bind(this)
,1000);
}
}

const foo=new Dog('white');
foo.delayHello(); // (after 1 second...)Hello,I am white

箭頭函式解法

雖然以上兩種方法都可以解決問題,但是都會讓this直接被綁定,缺乏彈性。ES6之後出現了一個新的方法-箭頭函式(Arrow function),可以解決這個彈性的問題。

許多人一開始在撰寫箭頭函式時,會誤認箭頭函式是傳統函式的語法糖,但是其實實際運作會有所不同。那就是箭頭函式沒有自己的this。因為箭頭函式本身並沒有自己的this,所以他會透過範圍鏈(scope chain)的方式,往外層去尋找this。這麼一來就可以解決callback function的this指向錯誤的問題。

class Dog{
constructor(myName){
this.myName=myName;
}
delayHello(){
setTimeout(()=>{ // 改為箭頭函式的寫法
console.log('(after 1 second...)Hello,I am',this.myName);
},1000);
}
}

const foo=new Dog('white');
foo.delayHello(); // (after 1 second...)Hello,I am white

補充

thisArg

如果我們透過bind傳入的第一個參數(也就是thisArg),並不是一個物件,會發生什麼事?

function foo(para1,para2) {
console.log('this',this);
console.log('typeof this',typeof this);
console.log('params',para1,para2);
}

const bind_foo=foo.bind(1,2,3);
bind_foo();
// this Number{1}
// typeof this object
// params 2 3

改用call的方法傳入:

function foo(para1,para2) {
console.log('this',this);
console.log('typeof this',typeof this);
console.log('params',para1,para2);
}
foo.call('傳入的thisArg',2,3);
// this String{'傳入的thisArg'}
// typeof this object
// params 2 3

如果傳入undefined:

function foo(para1,para2) {
console.log('this',this);
console.log('typeof this',typeof this);
console.log('params',para1,para2);
}
foo.call(undefined,2,3);
// this Window{...}
// typeof this object
// params 2 3

改用apply傳入null:

function foo(para1,para2) {
console.log('this',this);
console.log('typeof this',typeof this);
console.log('params',para1,para2);
}
foo.apply(null,[2,3]);
// this Window{...}
// typeof this object
// params 2 3

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

嚴格模式thisArg

嚴格模式的啟用方式很簡單,只要在最上方加上字串的'use strict'就可以啟用。

需要注意:

  • 一定要放在最上方才可以正確啟用
  • 沒有支援嚴格模式的環境,只會將'use strict'視為純字串,雖然不會拋出錯誤但是可能會在執行上會有所不同
  • 嚴格模式在不同環境下可能會略有不同
function foo(para1,para2) {
'use strict'
console.log(this,typeof this,para1,para2);
}
foo.call(1,2,3); // 1 'number' 2 3
foo.call('傳入的thisArg',2,3); // 傳入的thisArg string 2 3
foo.call(undefined,2,3); // undefined 'undefined' 2 3
foo.call(null,2,3); // null 'object' 2 3

執行結果圖示

上方範例程式碼執行結果

使用嚴格模式時,原始型別也不會進行封裝(boxing),另外null,undefined也不會被轉換為Window

note

先前我們在使用simple call的時候,都會直接說simple call的this 就是window

但是實際上simple call只是不指定this的呼叫方式,也就是說他的thisArg本質實際上是undefined!!!

所以在使用simple call的時候盡量不要去調用this,因為本質就是undefined。