1. 程式人生 > 實用技巧 >Javascript之模擬實現call,apply

Javascript之模擬實現call,apply

call,apply簡介

首先介紹下call和apply兩個方法,這兩個方法都是掛載在函式的原型上的,所以所有的函式都可以呼叫這兩個方法。

注意:call()方法的作用和 apply() 方法類似,區別就是call()方法接受的是引數列表,而apply()方法接受的是一個引數陣列

例子:

function foo(b = 0) {
	console.log(this.a + b);
}
const obj1 = {
	a: 1
};
const obj2 = {
	a: 2
};
foo.call(obj1, 1); // 2
foo.call(obj2, 2); // 4
foo.apply(obj1, [1]); // 2
foo.apply(obj2, [2]); // 4

對於this不熟悉的同學可以先非同步:理解Javascript的this。總結起來一句話:JavaScript函式的this指向呼叫方,誰呼叫this就指向誰,如果沒人誰呼叫這個函式,嚴格模式下指向undefined,非嚴格模式指向window。

所以本質上call和apply就是用來更改被呼叫函式的this值的。如上,call和apply只有引數的不同,模擬實現了call,那麼apply就只是引數處理上的區別。也就是說,call和apply幹了兩件事:

  1. 改變被呼叫函式的this值;
  2. 傳參呼叫;

###更改this

現在模擬實現call和apply的問題轉移到另一個問題上,即如何去更改一個函式的this值,很簡單:

function foo(b = 0) {
	console.log(this.a + b);
}
const obj1 = {
	a: 1,
  foo: foo
};
const obj2 = {
	a: 2,
  foo: foo
};
obj1.foo(1);
obj2.foo(2);

也就是說我們把這個方法賦值給物件,然後物件呼叫這個函式就可以了。改變一個函式的this步驟很簡單,首先將這個函式賦值給this要指向的物件,然後物件呼叫這個函式,執行完從物件上刪除掉這個函式就好了。步驟如下:

obj.foo = foo;
obj.foo();
delete obj.foo;

有了思路我們實現第一版call方法

Function.prototype.call2 = function(context) {
  context = context || {};
  context[this.name] = this;
  context[this.name]();
  delete context[this.name];
}

this.name是函式宣告的名稱,但其實是沒必要一定對應函式名稱的,我們隨便用一個key都可以:

Function.prototype.call2 = function(context) {
  context = context || {};
  context.func = this;
  context.func();
  delete context.func;
}

使用新的call呼叫上面的函式:

foo.call2(obj1); // 1
foo.call2(obj2); // 2

OK,this的問題解決了,接下來就是傳參的問題:

傳參

函式中的引數儲存在一個類陣列物件arguments中。因此我們可以從arguments裡面去拿從傳到call2裡面的引數:

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  context.func = this;
  context.func();
  delete context.func;
}

此時問題來了,如何把引數params傳遞到func中呢?比較容易想到的辦法是利用ES6的擴充套件運算子

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  context.func = this;
  context.func(...params);
  delete context.func;
}

看下我們的例子:

foo.call2(obj1, 1); // 2
foo.call2(obj2, 2); // 4

還有一個實現,是利用不常用的eval函式,即我們把引數拼接成一個字串,傳給eval函式去執行,

eval() 函式可計算某個字串,並執行其中的的JavaScript程式碼。

看下我們的第二版實現:

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // 注意,此處的this是指的被呼叫的函式
  context.func = this;
  eval('context.func(' + params.join(",") + ')');
  delete context.func;
}

其它

call和apply還有另外兩個重要的特性,可以正常返回函式執行結果,接受null或undefined為引數的時候將this指向window,然後我們來實現下這兩個特性,然後加上必要的判斷提示,這是我們的第三版實現

Function.prototype.call2 = function(context) {
  context = context || window;
  var params = [];
  // 此處將i初始化為1,是為了跳過context引數
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // 注意,此處的this是指的被呼叫的函式
  context.func = this;
  var res = eval('context.func(' + params.join(",") + ')');
  delete context.func;
  return res;
}

然後我們呼叫測試下:

foo.call2(obj1, 1); // 2

foo.call(2, 1); // NaN
foo.call2(2, 1); // context.func is not a function

如上我們發現將物件改成數字2後原始call返回了NaN,我們的call2卻報錯了,說明一個問題,我們直接context = context || window是有問題的。內部還有一個型別判斷,解決這個問題後,我們的第四版實現如下:

Function.prototype.call2 = function(context) {  
  if (context === null || context === undefined) {
		context = window;
  } else {
		context = Object(context) || context;
  }
  var params = [];
  // 此處將i初始化為1,是為了跳過context引數
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // 注意,此處的this是指的被呼叫的函式
  context.func = this;
  var res = eval('context.func(' + params.join(",") + ')');
  delete context.func;
  return res;
}

這就是我們的最終程式碼,這個程式碼可以從ES3一直相容到ES6,此時:

foo.call(2, 1); // NaN
foo.call2(2, 1); // NaN

資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com

模擬實現apply

apply和call只是引數上的區別,將call2改寫就好了:

Function.prototype.apply2 = function(context, arr) {
  if (context === null || context === undefined) {
		context = window;
  } else {
		context = Object(context) || context;
  }
  // 注意,此處的this是指的被呼叫的函式
  context.func = this;
  arr =  arr || [];
  var res = eval('context.func(' + arr.join(",") + ')');
  delete context.func;
  return res;
}

以上就是我們最終的實現,目前還有一個問題就是context.func的問題,這樣一來我們傳進來的context就不能使用func字串作為方法名了。

結論

我們實現過程都解決了以下問題:

  1. 更改被呼叫函式的this;
  2. 將引數傳遞給被呼叫函式;
  3. 將被呼叫函式結果返回,第一個引數為null或undefined的時候被呼叫函式的this指向window;
  4. 解決型別判斷的問題;