深入理解 JavaScript call()、apply()、bind()

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

理解 JavaScript this 文章中已經比較全面的分析了 this 在 JavaScript 中的指向問題,用一句話來總結就是:this 的指向一定是在執行時決定的,指向被調用函數的對象。當然,上篇文章也指出可以通過 call() / apply() / bind() 這些內置的函數方法來指定 this 的指向,以達到開發者的預期,而這篇文章將進一步來討論這個問題。

先來回顧一下,舉個簡單的例子:var leo = {

name: ‘Leo’,

sayHi: function() {

return “Hi! I’m ” + this.name;

}

};

var neil = {

name: ‘Neil’

};

leo.sayHi(); // “Hi! I’m Leo”

leo.sayHi.call(neil); // “Hi! I’m Neil”

基本用法

在 JavaScript 中,函數也是對象,所以 JS 的函數有一些內置的方法,就包括 call(), apply() 和 bind(),它們都定義在 Function 的原型上,所以每一個函數都可以調用這 3 個方法。

Function.prototype.call(thisArg [, arg1 [, arg2, …]]),對於 call() 而言,它的第一個參數為需要綁定的對象,也就是 this 指向的對象,比如今天的引例中就是這樣。

第一個參數也可以是 null 和 undefined,在嚴格模式下 this 將指向瀏覽器中的 window 對象或者是 Node.js 中的 global 對象。var leo = {

name: ‘Leo’,

sayHi: function() {

return “Hi! I’m ” + this.name;

}

};

leo.sayHi.call(null); // “Hi! I’m undefined”

▲ this 指向 window,window.name 沒有定義

除了第一個參數,call() 還可以選擇接收剩下任意多的參數,這些參數都將作為調用函數的參數,來看一下:function add(a, b) {

return a + b;

}

add.call(null, 2, 3); // 5

▲ 等同於 add(2, 3)

apply() 的用法和 call() 類似,唯一的區別是它們接收參數的形式不同。除了第一個參數外,call() 是以枚舉的形式傳入一個個的參數,而 apply() 是傳入一個數組。function add(a, b) {

return a + b;

}

add.apply(null, [2, 3]); // 5

注意:apply() 接受的第二個參數為數組(也可以是一個類數組對象),但不意味著調用它的函數接收的是數組參數。這里的 add() 函數依舊是 a 和 b 兩個參數,分別賦值為 2 和 3,而不是 a 被賦值為 [2, 3]。

接下來說說 bind(),它和另外兩個大有區別。var leo = {

name: ‘Leo’,

sayHi: function() {

return “Hi! I’m ” + this.name;

}

};

var neil = {

name: ‘Neil’

};

var neilSayHi = leo.sayHi.bind(neil);

console.log(typeof neilSayHi); // “function”

neilSayHi(); // “Hi! I’m Neil”

與 call() 和 apply() 直接執行原函數不同的是,bind() 返回的是一個新函數。簡單說,bind() 的作用就是將原函數的 this 綁定到指定對象,並返回一個新的函數,以延遲原函數的執行,這在異步流程中(比如回調函數,事件處理程序)具有很強大的作用。你可以將 bind() 的過程簡單的理解為:function bind(fn, ctx) {

return function() {

fn.apply(ctx, arguments);

};

}

如何做到

這一部分應該是經常出現在面試中。最常見的應該是 bind() 的做到,就先來說說如何做到自己的 bind()。

◆ bind() 的做到

上一節已經簡單地做到了一個 bind(),稍作改變,為了和內置的 bind() 區別,我麼自己做到的函數叫做 bound(),先看一下:Function.prototype.bound = function(ctx) {

var fn = this;

return function() {

return fn.apply(ctx);

};

}

這里的 bound() 模擬了一個最基本的 bind() 函數的做到,即返回一個新函數。這個新函數包裹了原函數,並且綁定了 this 的指向為傳入的 ctx。

對於內置的 bind() 來說,它還有一個特點:var student = { id: ‘2015’ };

function showDetail (name, major) {

console.log(‘The id ‘ + this.id +

‘ is for ‘ + name +

‘, who major in ‘ + major);

}

showDetail.bind(student, ‘Leo’)(‘CS’);

// “The id 2015 is for Leo, who major in CS”

showDetail.bind(student, ‘Leo’, ‘CS’)();

// “The id 2015 is for Leo, who major in CS”

在這里兩次調用參數傳遞的方式不同,但是具有同樣的結果。下面,就繼續完善我們自己的 bound() 函數。var slice = Array.prototype.slice;

Function.prototype.bound = function(ctx) {

var fn = this;

var _args = slice.call(arguments, 1);

return function() {

var args = _args.concat(slice.call(arguments));

return fn.apply(ctx, args);

};

}

這里需要借助 Array.prototype.slice() 方法,它可以將 arguments 類數組對象轉為數組。我們用一個變量保存傳入 bound() 的除第一個參數以外的參數,在返回的新函數中,將傳入新函數的參數與 bound() 中的參數合併。

其實,到現在整個 bound() 函數的做到都離不開閉包,你可以查看文章 理解 JavaScript 閉包。

在文章 理解 JavaScript this 中,我們提到 new 也能改變 this 的指向,那如果 new 和 bind() 同時出現,this 會聽從誰?function Student() {

console.log(this.name, this.age);

}

Student.prototype.name = ‘Neil’;

Student.prototype.age = 20;

var foo = Student.bind({ name: ‘Leo’, age: 21 });

foo(); // ‘Leo’ 21

new foo(); // ‘Neil’ 20

從例子中已經可以看出,使用 new 改變了 bind() 已經綁定的 this 指向,而我們自己的 bound() 函數則不會:var foo = Student.bound({ name: ‘Leo’, age: 21 });

foo(); // ‘Leo’ 21

new foo(); // ‘Leo’ 21

所以我們還要接著改進 bound() 函數。要解決這個問題,我們需要清楚原型鏈以及 new 的原理,在後面的文章中我再來分析,這里只提供解決方案。

var slice = Array.prototype.slice;

Function.prototype.bound = function(ctx) {

if (typeof this !== ‘function’) {

throw TypeError(‘Function.prototype.bound – what is trying to be bound is not callable’);

}

var fn = this;

var _args = slice.call(arguments);

var fBound = function() {

var args = _args.concat(slice.call(arguments));

// 在綁定原函數 fn 時增加一次判斷,如果 this 是 fBound 的一個實例

// 那麼此時 fBound 的調用方式一定是 new 調用

// 所以,this 直接綁定 this(fBound 的實例對象) 就好

// 否則,this 依舊綁定到我們指定的 ctx 上

return fn.apply(this instanceof fBound ? this : ctx, args);

};

// 這里我們必須要聲明 fBound 的 prototype 指向為原函數 fn 的 prototype

fBound.prototype = Object.create(fn.prototype);

return fBound;

}

大功告成。如果看不懂最後一段代碼,可以先放一放,後面的文章會分析原型鏈和 new 的原理。

◆ call() 的做到function foo() {

console.log(this.bar);

}

var obj = { bar: ‘baz’ };

foo.call(obj); // “baz”

我們觀察 call 的調用,存在下面的特點:

當函數 foo 調用 call,並傳入 obj 時,似乎是在 obj 的原型上增加了一個 foo 方法。

foo.call() 除第一個參數外的所有參數都應該傳給 foo(),這一點在做到 bind() 時已處理過。

不能對 foo 和 obj 做任何修改。

那就來看看,以示區別,我們自己做到的 call 叫做 calling。Function.prototype.calling = function(ctx) {

ctx.fn = this;

ctx.fn();

}

我們完成了第一步。

在完成第二步時,我們需要用到 eval(),它可以執行一段字符串類型的 JavaScript 代碼。var slice = Array.prototype.slice;

Function.prototype.calling = function(ctx) {

ctx.fn = this;

var args = [];

for (var i = 1; i < args.length; i++) {

args.push(‘arguments[‘ + i + ‘]’);

}

eval(‘ctx.fn(‘ + args + ‘)’);

}

這里我們避免採用和做到 bind() 同樣的方法獲取剩餘參數,因為要使用到 call,所以這里採用循環。我們需要一個一個的將參數傳入 ctx.fn(),所以就用到 eval(),這里的 eval() 中的代碼在做 + 運算時,args 會發生類型轉換,自動調用 toString() 方法。

做到到這里,大部分的功能以及完成,但是我們不可避免的為 ctx 手動添加了一個 fn 方法,改變了 ctx 本身,所以要把它給刪除掉。另外,call 應該有返回值,且它的值是 fn 執行過後的結果,並且如果 ctx 傳入 null 或者 undefined,應該將 this 綁定到全局對象。我們可以得到下面的代碼:var slice = Array.prototype.slice;

Function.prototype.calling = function(ctx) {

ctx = ctx || window || global;

ctx.fn = this;

var args = [];

for (var i = 1; i < args.length; i++) {

args.push(‘arguments[‘ + i + ‘]’);

}

var result = eval(‘ctx.fn(‘ + args + ‘)’);

delete ctx.fn;

return result;

}

◆ apply() 的做到

apply() 的做到與 call() 類似,只是參數的處理不同,直接看代碼吧。var slice = Array.prototype.slice;

Function.prototype.applying = function(ctx, arr) {

ctx = ctx || window || global;

ctx.fn = this;

var result = null;

var args = [];

if (!arr) {

result = ctx.fn();

} else {

for (var i = 1; i < args.length; i++) {

args.push(‘arr[‘ + i + ‘]’);

}

result = eval(‘ctx.fn(‘ + args + ‘)’);

}

delete ctx.fn;

return result;

}

About 尋夢園
尋夢園是台灣最大的聊天室及交友社群網站。 致力於發展能夠讓會員們彼此互動、盡情分享自我的平台。 擁有數百間不同的聊天室 ,讓您隨時隨地都能找到志同道合的好友!