深刻理解之JavaScript之call, apply, bind方法

在JavaScript中,call、apply和bind是Function對象自帶的三個方法,這三個方法的主要做用是改變函數執行時的上下文,再具體一點就是改變函數運行時的this指向javascript

  • Function.prototype.call()

call() 方法調用一個函數, 其具備一個指定的 this 值和多個參數(參數的列表)。java

fun.call(thisArg, arg1, arg2, ...)

thisArg的取值有如下4種狀況:react

  1. 不傳,或者傳null, undefined, 函數中的this指向window對象(非嚴格模式下)
  2. 傳遞另外一個函數的函數名,函數中的this指向這個函數的引用
  3. 傳遞字符串、數值或布爾類型等基礎類型,函數中的this指向其對應的包裝對象,如 String、Number、Boolean
  4. 傳遞一個對象,函數中的this指向這個對象.

咱們能夠用以下代碼驗證一下:數組

function func1() {   
    console.log(this);   
}       

function func2() {}       

var obj = { name: "jacky" };  

func1.call();   // Window
func1.call(null);   // Window
func1.call(undefined);   // Window
func1.call(1);   // Number {1}
func1.call('');   // String {""}
func1.call(true);   // Boolean {true}
func1.call(func2);   // ƒ func2() {}
func1.call(obj);   // {name: "jacky"}

咱們再來看個例子,理解怎麼改變this的指向:app

function Animal(){
    this.name = 'animal';
    this.sayName = function(){
        console.log(this.name);
    }
}

function Cat(){
    this.name = 'cat';
}

var animal = new Animal();
var cat = new Cat();

animal.sayName.call(cat); // cat
// this 永遠指向最後調用它的那個對象
// 該例子中sayName方法的this指向Cat
  • Function.prototype.apply()

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

以下代碼:this

function Animal(...args) {
    console.log(`${this.name} 和其餘參數 ${args}`); 
};

let cat = {
    name: 'xiaomao'
};

// 1. 使用 call
Animal.call(cat, 1, 2, 3); // xiaomao 和其餘參數 1,2,3

// 2. 使用 apply
Animal.apply(cat, [1, 2, 3]); // xiaomao 和其餘參數 1,2,3
  • call、apply使用

因爲兩個方法實際效果是同樣的,對於二者平時用該如何選擇呢?prototype

  1. 參數數量/順序肯定就用call,參數數量/順序不肯定的話就用apply。
  2. 考慮可讀性:參數數量很少就用call,參數數量比較多的話,把參數整合成數組,使用apply。
  3. 參數集合已是一個數組的狀況,用apply。
  • call, apply 的應用

1. 數組拼接

let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];

// 用 apply方法
[].push.apply(arr1, arr2);  // 給arr1添加arr2
console.log(arr1); // [1, 2, 3, 4, 5, 6]

2. 獲取數組中的最大值或最小值

var numbers = [1, 4, 6, 2, 3, 100, 98]; 
console.log( Math.max.apply(Math, numbers) );   // 100
console.log( Math.max.call(Math, 1, 4, 6, 2, 3, 100, 98) ); // 100
console.log( Math.min.call(Math, ...numbers) ); // 1

3. 判斷數據類型

let arr = [1, 2, 3, 4];
let str = 'string',
    obj = { name: 'jacky' }
// 判斷傳入參數是否爲數組
function isArray(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
}
console.log(isArray(arr)); // true
console.log(isArray(str)); // false
console.log(Object.prototype.toString.call(arr)); // [object Array]
console.log(Object.prototype.toString.call(str)); // [object String]
console.log(Object.prototype.toString.call(obj)); // [object Object]
console.log(Object.prototype.toString.call(null)); // [object Null]

4. 調用父類構造函數實現繼承

function Animal(name){      
    this.name = name;      
    this.showName = function(){      
        console.log(this.name);      
    }      
}      

function Cat(name){    
    Animal.call(this, name);    
}      

var cat = new Cat("xiaomao");     
cat.showName();   // xiaomao

缺點:代理

  1. 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
  2. 每次子類實例化都要執行父類函數,從新聲明父類this裏所定義的方法,所以父類方法沒法複用。

5. 類數組對象轉數組

function func() {
    var args = [].slice.call(arguments);
    console.log(args);
}
func(1, 2, 3); // [1, 2, 3]

還有像調用 getElementsByTagName , document.childNodes 之類的,它們返回NodeList對象都屬於僞數組。code

6. 代理console.log方法

注意這裏傳入多少個參數是不肯定的,因此使用apply是最好的。

function log(){
    console.log.apply(console, arguments);
};
log(1);    // 1
log(1, 2, 3);    // 1 2 3
  • Function.prototype.bind()

MDN的解釋是:

bind()方法會建立一個新函數,稱爲綁定函數,當調用這個綁定函數時,綁定函數會以建立它時傳入 bind()方法的第一個參數做爲 this,傳入 bind() 方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。

說直白一點,bind方法是建立一個函數,而後能夠在須要調用的時候再執行函數,並不是是當即執行函數;而call,apply是在改變了上下文中的this指向後並當即執行函數。

bind 接收的參數類型與 call 是同樣的,給它傳遞的是一組用逗號分隔的參數列表。

var bar = function(){   
    console.log(this.x);   
}
var foo = { 
    x: 2   
}   
bar(); // undefined

var func = bar.bind(foo);   
func(); // 2
  • bind的應用

bind綁定回調函數的this指向

class PageA {
    constructor(callBack) {
        this.className = 'PageA'
        this.MessageCallBack = callBack 
        this.MessageCallBack('執行了')
    }
}
class PageB {
    constructor() {
        this.className = 'PageB'
        this.pageClass = new PageA(this.handleMessage)
    }
    // 回調函數
    handleMessage(msg) {
        console.log('處理' + this.className + '的回調 ' + msg) // 處理PageA的回調 執行了
    }
}
new PageB();

運行上面的代碼,咱們發現回調函數this丟失了?問題出在這行代碼

this.pageClass = new PageA(this.handleMessage)

傳遞過去的this.handleMessage是一個函數內存地址,沒有上下文對象,也就是說該函數沒有綁定它的this指向。

解決方案:用bind綁定this的指向

this.pageClass = new PageA(this.handleMessage.bind(this))

這也是爲何react的render函數在綁定回調函數的時候,也要使用bind綁定一下this的指向,也是由於一樣的問題以及原理。

  • 多個bind的狀況
var bar = function() {
    console.log(this.x);
}

var obj1 = {
    x: 2
}
var obj2 = {
    x: 3
}
var obj3 = {
    x: 4
}

var func1 = bar.bind(obj1).bind(obj2);
var func2 = bar.bind(obj1).bind(obj2).bind(obj3);
func1(); // 2
func2(); // 2

輸出結果都爲第一個綁定的obj對象的x值。緣由是,在Javascript中,bind()方法返回的外來的綁定函數對象僅在建立的時候記憶上下文(若是提供了參數),屢次 bind() 是無效的。更深層次的緣由, bind() 的實現,至關於使用函數在內部包了一個 call / apply ,第二次 bind() 至關於再包住第一次 bind() ,故第二次之後的 bind 是沒法生效的。

  • 總結

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

call和apply改變了函數的this上下文後便執行該函數,而bind則是返回改變了上下文後的一個函數,其內的this指向爲建立它時傳入bind的第一個參數,而傳入bind的第二個及之後的參數做爲原函數的參數來調用原函數。bind的參數能夠在函數執行的時候再次添加。

相關文章
相關標籤/搜索