談談JavaScript中的call、apply和bind

call_me_R發表於2019-06-04

JavaScript中,如果想要改變當前函式呼叫的上下文物件的時候,我們都會聯想到call、apply和bind。比如下面?

var name = 'window name';
var obj = {
    name: 'call_me_R'
};
function sayName(){
    console.log(this.name);
}
sayName(); // window name
sayName.call(obj); // call_me_R
複製程式碼

那麼,call, apply和bind有什麼區別呢?

call,apply和bind的區別

在說區別之前,先簡單的說下三者的共同之處吧:

  1. 都是用來改變函式的this物件的指向
  2. 第一個引數都是this要指向的物件
  3. 都可以利用後續引數進行傳參

下面說下區別:

引數的傳遞

參考下MDN web docs -- Function:

call方法傳參是傳一個或者是多個引數,第一個引數是指定的物件,如開篇的obj

func.call(thisArg, arg1, arg2, ...)
複製程式碼

apply方法傳參是傳一個或兩個引數,第一個引數是指定的物件,第二個引數是一個陣列或者類陣列物件。

func.apply(thisArg, [argsArray])
複製程式碼

bind方法傳參是傳一個或者多個引數,跟call方法傳遞引數一樣。

func.bind(this.thisArg, arg1, arg2, ...)
複製程式碼

簡言之,callbind傳參一樣;apply如果要傳第二個引數的話,應該傳遞一個類陣列。

呼叫後是否立執行

call和apply在函式呼叫它們之後,會立即執行這個函式;而函式呼叫bind之後,會返回撥用函式的引用,如果要執行的話,需要執行返回的函式引用。

變動下開篇的demo程式碼,會比較容易理解:

var name = 'window name';
var obj = {
    name: 'call_me_R'
};
function sayName(){
    console.log(this.name);
}
sayName(); // window name
sayName.call(obj); // call_me_R
sayName.apply(obj); // call_me_R
console.log('---divided line---');
var _sayName = sayName.bind(obj);
_sayName(); // call_me_R
複製程式碼

在筆者看來,call, apply 和 bind的區分點主要是上面的這兩點,歡迎有想法的讀者進行補充~?

手寫call, apply, bind方法

這裡是簡單的實現下相關方法的封裝,為了簡潔,我這裡儘量使用了ES6的語法進行編寫,詳細的參考程式碼可以直接戳airuikun大牛的airuikun/Weekly-FE-Interview issues

call方法實現

在上面的瞭解中,我們很清楚了call的傳參格式和呼叫執行方式,那麼就有了下面的實現方法:

Function.prototype.call2 = function(context, ...args){
	context = context || window; // 因為傳遞過來的context有可能是null
	context.fn = this; // 讓fn的上下文為context
	const result = context.fn(...args);
	delete context.fn;
	return result; // 因為有可能this函式會有返回值return
}
複製程式碼

我們來測試下:

var name = 'window name';
var obj = {
    name: 'call_me_R'
};

// Function.prototype.call2 is here ...

function sayName(a){
    console.log(a + this.name);
    return this;
}
sayName(''); // window name
var _this = sayName.call2(obj, 'hello '); // hello call_me_R
console.log(_this); // {name: "call_me_R"}
複製程式碼

apply方法實現

apply方法和call方法差不多,區分點是apply第二個引數是傳遞陣列:

Function.prototype.apply2 = function(context, arr){
    context = context || window; // 因為傳遞過來的context有可能是null
    context.fn = this; // 讓fn的上下文為context
    arr = arr || []; // 對傳進來的陣列引數進行處理
    const result = context.fn(...arr); // 相當於context.fn(arguments[1], arguments[2], ...)
    delete context.fn;
    return result; // 因為有可能this函式會有返回值return
}
複製程式碼

同樣的,我們來測試下:

var name = 'window name';
var obj = {
    name: 'call_me_R'
};

// Function.prototype.apply2 is here ...

function sayName(){
    console.log((arguments[0] || '') + this.name);
    return this;
}
sayName(); // window name
var _this = sayName.apply2(obj, ['hello ']); // hello call_me_R
console.log(_this); // {name: "call_me_R"}
複製程式碼

bind方法實現

bind的實現和上面的兩種就有些差別,雖然和call傳參相同,但是bind被呼叫後返回的是呼叫函式的指標。那麼,這就說明bind內部是返回一個函式,思路開啟了:

Function.prototype.bind2 = function(context, ...args){
    var fn = this; 
    return function () { // 這裡不能使用箭頭函式,不然引數arguments的指向就很尷尬了,指向父函式的引數
        fn.call(context, ...args, ...arguments);
    }
}
複製程式碼

我們還是來測試一下:

var name = 'window name';
var obj = {
    name: 'call_me_R'
};

// Function.prototype.bind2 is here ...

function sayName(){
    console.log((arguments[0] || '') + this.name + (arguments[1] || ''));
}
sayName(); // window name
sayName.bind2(obj, 'hello ')(); // hello call_me_R
sayName.bind2(obj, 'hello ')('!'); // hello call_me_R!
複製程式碼

美滋滋?,成功地簡單實現了call、apply和bind的方法,那麼你可能會對上面的某些程式碼有疑問❓

疑惑點

1. 問:call中為什麼說 context.fn = this; // 讓fn的上下文為context 呢?

答:

我們先來看看下面這段程式碼--

var name = 'window name';
var obj = {
    name: 'call_me_R',
    sayHi: function() {
        console.log('Hello ' + this.name);
    }
};
obj.sayHi(); // Hello call_me_R
window.fn = obj.sayHi;
window.fn(); // Hello window name
複製程式碼

嗯,神奇了一丟丟,操作window.fn = obj.sayHi;改變了this的指向,也就是this由指向obj改為指向window了。

簡單來說:this的值並不是由函式定義放在哪個物件裡面決定的,而是函式執行時由誰來喚起來決定的。

2. 問:bind中返回的引數為什麼是傳遞(context, ...args, ...arguments), 而不是(context, ...args)呢?

答:

這是為了包含返回函式也能傳參的情況,也就是bind()()中的第二個括號可以傳遞引數。

call和apply哪個好?

據調查--call和apply的效能對比,在分不同傳參的情況下,call的效能是優於apply的。不過在現代的高版本瀏覽器上面,兩者的差異並不大。

而在相容性方面,兩者都好啦,別說IE了哈。

在使用的方面還是得按照需求來使用call和apply,畢竟技術都在更新。適合業務的就是最好的~囧

後話

文章首發:github.com/reng99/blog…

更多內容:github.com/reng99/blog…

客官可以star下github的博文倉庫否,歡迎提意見共同成長啊~?

參考

MDN web docs -- Function

airuikun/Weekly-FE-Interview issues

《JavaScript高階程式設計》

相關文章