學習Javascript之模擬實現call,apply

前言

本文1630字,閱讀大約須要8分鐘。

總括: 本文從零開始經過提出問題而後解決問題的方式模擬實現了比較完善的call和apply方法前端

每個未曾起舞的日子,都是對生命的辜負。數組

正文

call,apply簡介

首先介紹下call和apply兩個方法,這兩個方法都是掛載在函數的原型上的,因此全部的函數均可以調用這兩個方法。app

注意: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要指向的對象,而後對象調用這個函數,執行完從對象上刪除掉這個函數就行了。步驟以下:this

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

有了思路咱們實現初版call方法spa

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

this.name是函數聲明的名稱,但實際上是不必必定對應函數名稱的,咱們隨便用一個key均可以:prototype

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;
}

其它

callapply還有另外兩個重要的特性,能夠正常返回函數執行結果,接受nullundefined爲參數的時候將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

模擬實現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. 將被調用函數結果返回,第一個參數爲nullundefined的時候被調用函數的this指向window;
  4. 解決類型判斷的問題;

以上。


能力有限,水平通常,歡迎勘誤,不勝感激。

訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

前端進階學習

相關文章
相關標籤/搜索