call、apply和bind方法的用法以及區別

call、apply和bind方法的用法以及區別
  call、apply、bind的作用是改變函數運行時this的指向,所以先說清楚this
1、方法調用模式:
  當一個函數被保存爲對象的一個方法時,如果調用表達式包含一個提取屬性的動作,那麼他就是被當做一個方法來調用,此時的this被綁定到這個對象。例如

var a = 1;
var obj = {
    a: 2,
    fn: function(){
        console.log(this.a);
    }
}

obj.fn();

輸出結果:2

  此時的 this 是指 obj 這個對象,obj.fn()實際是 obj.fn.call(obj) ,事實上誰調用這個函數,this就是誰。補充一下,DOM對象綁定事件也屬於方法調用模式,因此它綁定的this就是事件源DOM對象。如

document.addEventListener('click', function(e){
    console.log(this);
    setTimeout(function(){
        console.log(this);
    }, 200);
}, false);  
點擊頁面,依次輸出:document 和 window對象

   解析:點擊頁面監聽click事件屬於方法調用,this指向事件源DOM對象,即 obj.fn.apply(obj),setTimeout內的函數屬於回調函數,可以這麼理解,f1.call(null, f2),所以this指向window。
2、函數調用模式:
  就是普通函數的調用,此時的this被綁定到window

最普通的函數調用 函數嵌套 把函數賦值後再調用
function fn(){
    console.log(this); //window
}
fn();
function fn1(){
    function fn2(){
        console.log(this); //window
    }
    fn2();
}
fn1();
var a = 1;
var obj = {
    a: 2,
    fn: function(){
        console.log(this.a);
    }
}
var fn1 = obj.fn;
fn1();//1

      obj.fn是一個函數 function(){console.log(this.a)} ,此時fn1就是不帶任何修飾的函數調用,function(){console.log(this.a)}.call(undefined),按理說打印出來的this應該就是undefined,但是瀏覽器裏有一條規則:"如果你傳入的context是null或者undefined,那麼window對象就是默認的context(嚴格模式下默認context是undefined)"。因此上面的this綁定的就是window,它被稱爲隱式綁定。如果希望打印出2,可以修改fn1()爲fn1.call(obj);

回調函數  改寫代碼
var a = 1;
function f1(fn){
    fn();
    console.log(a);//1
}
f1(f2);
function f2(){
    var a = 2;
}
var a = 1;
function f1(){
    (function(){
        var a = 2;
    })();
    console.log(a);//1
}

    仍舊是最普通的函數調用,f1.call(undefined),this指向window,打印出的是全局的a;藉此可以解釋爲什麼setTimeout總是丟失this了,因爲它也就是一個回調函數而已。

setTimeout(function(){
    console.log(this); // window
    function fn(){
        console.log(this); // window
    }
    fn();
}, 0);

3、構造器調用模式:
    new一個函數時,背地裏會創建一個連接到 prototype 成員的新對象,同時 this 會被綁定到那個新對象上

function Person(name, age){
    // 這裏的this都是指向實例
    this.name  = name;
    this.age = age;
    this.sayAge = function(){
        console.log(this.age);
    }
}

var per = new Person('yw', 2);

per.sayAge(); // 2

4、call
    call方法第一個參數是要綁定給 this 的值,後面傳入的是一個參數列表。當第一個參數爲 null、undefined的時候,默認指向window。

var arr = [1, 2, 3, 89, 46];
var max = Math.max.call(null, arr[0], arr[1], arr[2], arr[3], arr[4]); // 89

可以這麼理解

obj.fn();  => obj.fn.call(obj);  fn(); => fn.call(null);  f1(f2); => f1.call(null, f2);

來看一個例子:

var obj = { message: 'My name is: '};
function getName(firstName, lastName){
    console.log(this.message + firstName + ' ' + lastName);
}
getName.call(obj, 'yang', 'wei');
輸出:My name is: yang wei

5、apply
    apply接收兩個參數,第一個參數是要綁定給 this 的值,第二個參數是一個參數數組。當第一個參數爲 null、undefined的時候,默認指向window。

var arr = [1, 2, 3, 89, 46];
var max = Math.max.apply(null, arr); // 89

可以這麼理解

obj.fn();  => obj.fn.apply(obj);   fn(); => fn.apply(null); f1(f2); => f1.apply(null, f2);

    事實上 apply 和 call 的用法幾乎相同,唯一的差別在於:當函數需要傳遞多個變量時,apply可以接收一個數組作爲參數輸入,call則是接收一系列的單獨變量。
來看一個例子:

var obj = { message: 'My name is: '};
function getName(firstName, lastName){
    console.log(this.message + firstName + ' ' + lastName);
}
getName.apply(obj, ['yang', 'wei']);
輸出:My name is: yang wei

    可以看到,obj是作爲函數上下文的對象,函數 getName 中 this 指向了 obj 這個對象,參數 firstName 和 lastName 是放在數組中傳入 getName 函數。
    call 和 apply 可用來借用別的對象的方法,這裏以call()爲例

var Person1 = function(){
    this.name = 'yang';
}
var Person2 = fucntion(){
    this.getName = function(){
        console.log(this.name);
    }
    Person1.call(this);
}

var person = new Person2();
person.getName();  //yang

Person2實例化出來的對象 person 通過 getName 方法拿到了Person1 中的name。

因爲Person2中,Person1.call(this) 的作用就是使用Person1對象代替 this 對象,

那麼Person2 就有了 Person1 中的所有屬性和方法了,

相當於 Person2 繼承了 Person1 的屬性和方法。

    對於什麼時候用什麼方法?如果參數本來就存在一個數組中,那就用 apply ,如果參數比較散亂相互之間沒有什麼關聯,就用call。
6、bind
    和 call 很相似,第一個參數是 this 的指向,從第二個參數開始是接收的參數列表。區別在於 bind 方法返回值是函數以及 bind 接收的參數列表的使用。
(1)bind返回值函數

var obj = { name: 'yangwei' };
function printName(){
    console.log(this.name);
}
var yw = printName.bind(obj);
console.log(yw);    //function(){ ... }
yw(); // yangwei

bind 方法不會立即執行,而是返回一個改變了上下文 this 後的函數。而原函數 printName 中的 this 並沒有被改變,依舊指向全局對象 window。
(2)參數的使用

function fn(a, b, c){
    console.log(a, b, c);
}
var fn1 = fn.bind(null, 'yangwei');
fn('A', 'B', 'C');       // A B C
fn1('A', 'B', 'C');     // yangwei A B
fn1('B', 'C');           // yangwei B C
fn.call(null, 'yw');   // yw undefined undefined    call 是把第二個及以後的參數作爲 fn 方法的實參傳進去,而 fn1 方法的實參則是在 bind 中參數中的基礎上再往後排

    有時候我們也用 bind 方法實現函數 珂里化,以下是一個簡單的示例

var add = function(x){
    return function(y){
        return x + y;
    }
}
var increment = add(1);
var addTen = add(10);
increment(2);
// 3

addTen(2);
// 12

    在低版本瀏覽器沒有 bind 方法,我們也可以自己實現:

if( !Function.prototype.bind ){
    Function.prototype.bind = function(){
        var self = this;                                       // 保存原函數
              context = [].shift.call(arguments),    // 保存需要綁定的this上下文
              args = [].slice.call(arguments);          // 剩餘的參數轉爲數組
        return function(){                                   // 返回一個新函數
            self.apply(context, [].concat.call(args, [].slice.call(arguments)));
        }
    }
}

7、應用場景

求數組中的最大和最小值 var arr = [1, 2, 3, 89, 46];
var max = Math.max.apply(null, arr);  // 89
var min = Math.min.apply(null, arr); // 1
將類數組轉爲數組 var trueArr = Array.prototype.slice.call( arrayLike );
數組追加 var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
var total = [].push.apply(arr1, arr2);  // 6
// arr1 = [1, 2, 3, 4, 5, 6]
// arr2 = [4, 5, 6]
判斷變量類型 function isArray(obj){
    return Object.prototype.toString.call(obj) == '[Object Array]';
}
// isArray([])  => true
// isArray('yw') => false
利用call和apply做繼承 fucntion Person(name, age){
    // 這裏的this都指向實例
    this.name = name;
    this.age = age;
    this.sayAge = function(){
        console.log(this.age)
    }
}
function Female(){
    Person.apply(this, arguments); // 將父元素所有方法在這裏執行一遍就繼承了
}
var female = new Female('yw', 27);
使用log代理console.log   function log(){
    console.log.apply(console, arguments);
}

8、總結
    bind返回對應函數,便於之後調用;apply、call則是立即調用。除此之外,在ES6的箭頭函數下,call和apply將失效,對於箭頭函數來說:
    箭頭函數體內的 this 對象, 就是定義時所在的對象, 而不是使用時所在的對象;所以不需要類似於var _this = this這種醜陋的寫法;
    箭頭函數不可以當作構造函數,也就是說不可以使用 new 命令, 否則會拋出一個錯誤;
    箭頭函數不可以使用 arguments 對象,該對象在函數體內不存在。如果要用,可以用 Rest 參數代替;
    不可以使用 yield 命令,因此箭頭函數不能用作 Generator 函數。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章