深刻學習js之——call和apply#10

深刻學習js系列是本身階段性成長的見證,但願經過文章的形式更加嚴謹、客觀地梳理js的相關知識,也但願可以幫助更多的前端開發的朋友解決問題,期待咱們的共同進步。javascript

若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。前端


開篇

ECMAScript3 給 Function 的原型定義了兩個方法,他們是 Function.prototype.callFunction.prototype.apply 在實際開發中特別是在一些函數式風格的代碼書寫中,call 和 apply 方法尤爲重要。java

call 和 apply 的區別

Function.prototype.callFunction.prototype.apply都是很是經常使用的方法,他們的做用如出一轍,區別僅僅是傳入的參數形式不一樣。數組

apply 接收兩個參數,第一個參數指定了函數體內部 this 對象的指向,第二個參數爲一個帶下標的集合,這個集合能夠是數組,也能夠爲類數組,apply 方法把這個集合中的元素做爲參數傳遞給被調用的函數。瀏覽器

var func = function(a, b, c) {
  console.log([a, b, c]); // => [1,2,3]
};
func.apply(null, [1, 2, 3]);
複製代碼

在這段代碼中,參數 1,2,3 被放在一個數組中一塊兒傳遞給 func 函數,他們分別對應 func 參數列表中的 a, b, c微信

call 傳入的參數數量不固定,跟 apply 相同的是,第一個參數也是表明函數體內的 this 指向,從第二個參數開始日後,每一個參數被依次傳入函數:閉包

var func = function(a, b, c) {
  console.log([a, b, c]); // 輸出 [1,2,3]
};
func.call(null, 1, 2, 3);
複製代碼

當調用一個函數時候,js 的解析器並不會計較形參和實參的數量、類型以及順序上的區別,js 的參數在內部就是用一個數組來表示的,從這個意義上面來講,call 比 apply 的使用率更高,咱們沒必要關心具體有多少參數被傳入函數,只要使用 call 一股腦的推動去就能夠了。app

apply 是包裝在 call 上面的一顆語法糖,若是咱們明確的知道了函數接收多少個參數,並且想一目瞭然的表達形參和實參的對應關係,那麼就可使用 apply 來傳遞參數。函數

當咱們使用 call 或者 apply 的時候,若是咱們傳入的第一個參數爲 null,函數體內部的 this 會指向默認的宿主對象,在瀏覽器中則是 window:post

var func = function(a, b, c) {
  alert(this === window); // true
};
func.apply(null, [1, 2, 3]);
複製代碼

可是在嚴格模式下面,函數體內部的 this 仍是 null

var func = function(a, b, c) {
 "use strict";
  alert(this === null); // 輸出true
};

func.apply(null, [1, 2, 3]);
複製代碼

有時候咱們使用 call 或者 apply 的目標並非在於指定 this 指向而是另有用途 好比借用其餘對象的方法,那麼咱們能夠傳入 null 來代替某一個具體的對象;

Math.max.apply(null, [1, 2, 4, 5]); // 輸出5
複製代碼

寫到這裏咱們總結一下:

他們倆之間的差異在於參數的區別,call 和 aplly 的第一個參數都是要改變上下文的對象,而 call 從第二個參數開始以參數列表的形式展示,apply 則是把除了改變上下文對象的參數放在一個數組裏面做爲它的第二個參數。

call 和 apply 的用途

1.改變 this 指向:

call 和 apply 最多見的用途就是改變函數內部的 this 指向,咱們看個例子:

var obj1 = {
  name: "louis"
};

var obj2 = {
  name: "jack"
};

window.name = "window";

var getName = function() {
  alert(this.name);
};

getName(); //輸出 window
getName.call(obj1); // 輸出 louis
getName.call(obj2); // 輸出 jack
複製代碼

當執行 getName.call(obj1)這句代碼的時候,getName 函數體內的 this 指向 obj1 對象,因此此處的

var getName = function () {
  alert(this.name);
}

實際上至關於:

var getName = function () {
  alert(obj1.name); // 輸出louis
}
複製代碼

實際開發中,咱們會常常遇到 this。指向被不經意改變的場景,好比有一個 div 節點,div 節點的 onclick 事件中的 this 指向原本是指向這個 div 的:

document.getElementById("div1").onclick = function() {
  alert(this.id); // div1
};
複製代碼

假如該事件中有一個內部函數 func,在事件內部調用 func 的時候,func 函數體內部的 this 就指向了 window 而不是咱們預期的 div,見以下代碼;

document.getElementById("div1").onclick = function() {
  alert(this.id); // 輸出:div1
  var func = function() {
    alert(this.id); // 輸出:undefined window 上面沒有id 屬性
  };
  func();
};
複製代碼

這個時候咱們可使用 call 來修正 func 函數內部的 this,使其依然指向 div:

document.getElementById("div1").onclick = function() {
  var func = function() {
    alert(this.id); // 輸出:div1
  };
  func.call(this);
};
複製代碼

2.Function.prototype.bind

大部分的高級瀏覽器都實現了內置的 Function.prototype.bind, 用來指定函數內部的 this 指向即便沒有原生的 Function.prototype.bind 實現,咱們來模擬一個也不是難事。

Function.prototype.bind = function(context) {
  var self = this; // 保存原函數
  return function() {
    // 返回一個新的函數
    return self.apply(context, arguments);
    // 執行新的函數的時候,會把以前傳入的context 看成新函數體內的this
  };
};

var obj = {
  name: "sven"
};

var func = function() {
  alert(this.name); // 輸出:sven
}.bind(obj);
func();
複製代碼

咱們經過Function.prototype.bind來包裝'func'函數,而且傳入一個對象 context 當作參數,這個 context 就是咱們想要修正的 this 對象。

Function.prototype.bind的內部實現中,咱們先把 func 函數的引用保存起來,而後返回一個新的函數。當咱們在未來執行 func 函數時,實際上先執行的是這個剛剛返回的新函數。在新函數內部,self.apply(context,arguments)這句代碼纔是執行原來的 func 函數,而且指定 context 對象爲 func 函數體內的 this。

3.借用其餘對象的方法

咱們知道,杜鵑既不會築巢,也不會孵雛,而是把本身的蛋寄託給雲雀等其餘鳥類,讓它們代爲孵化和養育。一樣,在 JavaScript 中也存在相似的借用現象。

借用方法的第一種場景是「借用構造函數」,經過這種技術,能夠實現相似於繼承的效果:

function Parent(value) {
  this.val = value;
}
Parent.prototype.getValue = function() {
  console.log(this.val);
};
function Child(value) {
  Parent.call(this, value);
}
Child.prototype = new Parent();

const child = new Child(1);

child.getValue(); // 1
child instanceof Parent; // true
複製代碼

借用方法的第二種運用場景跟咱們的關係更加緊密。

函數的參數列表 arguments 是一個類數組對象,雖然它也有"下標",可是它並不是真正的數組,因此也不能像數組同樣進行排序操做或者往集合裏面添加一個新的元素,這種狀況下,咱們經常使用 Array.prototype 對象上面的方法,好比想往 auguments 中添加一個新的元素,一般會借用 Array.prototype.push:

(function() {
  Array.prototype.push.call(arguments, 3);
  console.log(arguments); // [1,2,3]
})(1, 2);
複製代碼

在操做 arguments 的時候,咱們常常很是頻繁地找 Array.prototype 對象借用方法。

想把 arguments 轉成真正數組的時候,能夠借用Array.prototype.slice 方法;想要截取 arguments 列表中的頭一個元素的時候,又能夠借用Array.prototype.shift方法,那麼這種機制的內部實現原理是什麼呢?咱們能夠看看 V8 引擎源碼,咱們以 Array.prptotype.push()爲例子,看看具體實現:

function ArrayPush(){
    var n = TO_UINT32( this.length );    // 被push的對象的length
    var m = %_ArgumentsLength();     // push的參數個數
    for (var i = 0; i < m; i++) {
        this[ i + n ] = %_Arguments( i );   // 複製元素 (1)
    }
    this.length = n + m;      // 修正length屬性的值 (2)
    return this.length;」
  }
複製代碼

經過這段代碼能夠看到,Array.prototype.push 其實是一個屬性複製的過程,把參數按照下標依次添加到push的對象上面,順便修改了這個對象的length屬性,至於被修改的對象是誰,究竟是數組仍是類數組對象,這一點並不重要

按照這種推斷,咱們能夠把」任意「的對象傳入 Array.prototype.push;

var a = {};
Array.prototype.call(a,'first');

console.log(a.length);// 輸出 1
console.log(a[0]); // first
複製代碼

前面之因此把"任意"兩個字加了雙引號,是由於能夠借用Array.prototype.push方法的對象還須要知足如下兩個條件: 一、對象自己要能夠存取屬性 二、對象的length屬性能夠讀寫。

對於第一個條件,對象自己存取屬性並無問題,可是若是借用Array.prototype.push方法的不是一個object類型數據而是一個number類型的數據呢?由於number是基本數據類型,咱們沒法在number 身上存取其餘的數據,那麼從下面的測試代碼能夠發現,一個number類型的數據是不能借用到Array.prototype.push 方法:

var a = 1;
Array.prototype.push.call(a,'first');
console.log(a.length);// 輸出 undefined
console.log(a[0]); // 輸出 undefined
複製代碼

對於第二個條件,函數的length 屬性就是一個只讀的屬性,表示形參的個數,咱們嘗試把一個函數當作this傳入 Array.prototype.push:

var func = function(){}
Array.prototype.push.call(func,'first');
console.log(func.length);
複製代碼

報錯:cannot assign to read only property ‘length’ of function(){}

call的模擬實現

爲了實現 call 咱們首先用一句話簡單的介紹一下 call :

call() 方法在使用一個指定的 this 值和若干個指定的參數值的前提下調用某個函數或者方法

舉一個例子:

var foo = {
  value: 1
};

function bar() {
  console.log(this.value);
}

bar.call(foo); // 1
複製代碼

這裏須要注意兩點: 一、call 改變了 this 的指向,指向到了 foo 二、bar 函數執行了

接下來咱們嘗試模擬實現 call 的這個功能:

模擬實現第一步:

試想當咱們調用 call 的時候,把 foo 對象改造以下:

var foo = {
  value: 1,
  bar: function() {
    console.log(this.value);
  }
};
複製代碼

這個時候 this 就指向了 foo

可是這樣卻給 foo 自己添加了一個屬性,這樣可不行!

不過沒有關係,咱們使用 delete 刪除了就行

因此咱們的模擬的步驟能夠分爲:

一、將函數設置爲對象的屬性。 二、執行這個函數。 三、刪除這個函數。

以上的例子就是 :

// 第一步
foo.fn = bar;
// 第二步
foo.fn();
// 第三步
delete foo.fn;
複製代碼

fn 是對象的屬性名,反正最後也要刪除它,因此起成什麼名字無所謂 根據這個思路,咱們能夠嘗試寫一版,call2 函數:

// 初版
Function.prototype.call2 = function(context) {
  // 首先要獲取調用call的函數,用this能夠獲取
  context.fn = this;
  context.fn();
  delete context.fn;
};

// 測試一下
var foo = {
  value: 1
};

function bar() {
  console.log(this.value);
}

bar.call(foo); // 1;
複製代碼

上述代碼中, 由於一個函數調用了 call2 這個函數,所以在call2 函數的內部能夠拿到這個this,同時這個this 指向的就是調用call2的函數 咱們模擬的目的也是將這個函數做爲參數添加進context這個被綁定的對象上面

模擬實現第二步

最一開始咱們說了,call 函數還能給定參數執行函數,舉一個例子:

var foo = {
  value: 1
};

function bar(name, age) {
  console.log(name);
  console.log(age);
  console.log(this.value);
}
bar.call(foo, "kevin", 18);
// kevin
// 18
// 1
複製代碼

注意:傳入的參數並不肯定,這可怎麼辦? 不急,咱們能夠從 Arguments 對象中取值,取出第二個到最後一個參數,而後放到一個數組裏面。

好比這樣:

// arguments = {
  // 0:foo,
  // 1:'kevin',
  // 2:18,
  // lenght:3
  // }
  由於arguments 是類數組對象,因此可使用for 循環

  var args = [];
  for( var i = 1;len = arguments.length;i<len;i++){
    args.push('arguments['+ i +']');
  }

  // 執行以後 arguments 爲 ["arguments[1]","arguments[2]","[arguments[3]"]

複製代碼

不定長的參數的問題解決了,接着咱們要把這個參數數組放到要執行的函數的參數裏面去,這裏咱們使用 eval 方法拼接成一個函數,相似於這樣:

eval("context.fn(" + args + ")");
複製代碼

這裏 args 會自動調用 Array.toString() 這個方法。

這裏的eval可能不是那麼容易理解,這裏作一個簡單的補充說明:

eval函數接收的參數是一個字符串(這點很是重要) 定義和用法:

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

語法:

eval(string)

string必需。要計算的字符串,其中含有要計算的 JavaScript 表達式或要執行的語句。該方法只接受原始字符串做爲參數,若是 string 參數不是原始字符串,那麼該方法將不做任何改變地返回。所以請不要爲 eval() 函數傳遞 String 對象來做爲參數。

簡單來講吧,就是用JavaScript的解析引擎來解析這一堆字符串裏面的內容,這麼說吧,你能夠這麼理解,你把eval當作是<script>標籤。

eval('function Test(a,b,c,d){console.log(a,b,c,d)};Test(1,2,3,4)')
複製代碼

因此咱們第二版刻克服了兩個問題,代碼以下:

Function.prototype.call2 = function(context){
  context.fn = this;
  var args = [];
  for( var i = 1;len = arguments.length;i<len;i++){
    args.push('arguments['+ i +']');
  }

  eval('context.fn('+args+')');
  delete context.fn;
}

//測試
var foo = {
  value:1
};

function bar(name,age){
  console.log(name);
  console.log(age);
  console.log(this.value);
}
bar.call2(foo,'kevin',18);
// kevin
// 18
// 1
複製代碼

模擬第三步驟

模擬代碼已經完成了 80%,還有兩個小點須要注意:

一、this 參數能夠傳遞 null,當爲 null 的時候,視爲指向 window

舉一個例子:

var value = 1;
function bar() {
  console.log(this.value);
}
bar.call(null); // 1
複製代碼

雖然這個例子自己是否是使用 call 的結果都同樣

二、函數是能夠有返回值的

舉個例子:

var obj = {
  value: 1
};
function bar(name, age) {
  return {
    value: this.value,
    name: name,
    age: age
  };
}

console.log(bar.call(obj, "kevin", 18));
// Object{
// value:1,
// name:'kevin',
// age:18
// }
複製代碼

不過都很好解決,讓咱們直接看第三版也就是最最後一版的代碼:

// 第三版
Function.prototype.call2 = function(context) {
  var context = context || window;
  context.fn = this;

  var args = [];
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push("arguments[" + i + "]");
  }

  var result = eval("context.fn(" + args + ")");

  delete context.fn;
  return result;
};
// 測試一下
var value = 2;

var obj = {
  value: 1
};

function bar(name, age) {
  console.log(this.value);
  return {
    value: this.value,
    name: name,
    age: age
  };
}

bar.call2(null); // 2

console.log(bar.call2(obj, "kevin", 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
複製代碼

到這裏 咱們完成了 call 的模擬實現,給本身一個  贊。

apply 的模擬實現

apply 的模擬實現和 call 相似,在這裏直接給出代碼:

// 測試一下
var value = 2;

var obj = {
  value: 1
};

function bar(name, age) {
  console.log(this.value);
  return {
    value: this.value,
    name: name,
    age: age
  };
}

bar.call2(null); // 2

console.log(bar.call2(obj, "kevin", 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
複製代碼

深刻學習JavaScript系列目錄

歡迎添加個人我的微信討論技術和個體成長。

歡迎關注個人我的微信公衆號——指尖的宇宙,更多優質思考乾貨

相關文章
相關標籤/搜索