1. 程式人生 > >call、apply和bind方法的用法以及區別

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

方法呼叫模式:

當一個函式被儲存為物件的一個方法時,如果呼叫表示式包含一個提取屬性的動作,那麼它就是被當做一個方法來呼叫,此時的this被繫結到這個物件。

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

此時的this是指obj1這個物件,obj1.fn()實際上是obj1.fn.call(obj1),事實上誰呼叫這個函式,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。

函式呼叫模式:

就是普通函式的呼叫,此時的this被繫結到window

  • 最普通的函式呼叫
function fn1(){
      console.log(this)//window
    }
fn1()
  • 函式巢狀
function fn1(){
    function fn2(){
        console.log(this)//window
    }
    fn2()
}
fn1()
  • 把函式賦值之後再呼叫
var a = 1
var obj1 = {
    a:2,
    fn:function(){
        console.log(this.a)
    }
}
var fn1 = obj1.fn
fn1()//1

obj1.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(obj1),顯示地繫結this為obj1

  • 回撥函式
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()

仍舊是最普通的函式呼叫,f1.call(undefined),this指向window,打印出的是全域性的a。
藉此,我們終於可以解釋為什麼setTimeout總是丟失this了,因為它也就是一個回撥函式而已。

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

構造器呼叫模式:

new一個函式時,背地裡會將建立一個連線到prototype成員的新物件,同時this會被繫結到那個新物件上

function Person(name,age){
// 這裡的this都指向例項
    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}

var dot = new Person('Dot',2)
dot.sayAge()//2

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

可以這麼理解:

obj1.fn() 
obj1.fn.call(obj1);

fn1()
fn1.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, 'Dot', 'Dolby')

apply

apply接受兩個引數,第一個引數是要繫結給this的值,第二個引數是一個引數陣列。當第一個引數為null、undefined的時候,預設指向window。

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

可以這麼理解:

obj1.fn() 
obj1.fn.apply(obj1);

fn1()
fn1.apply(null)

f1(f2)
f1.apply(null,f2)

是不是覺得和前面寫的call用法很像,事實上apply 和 call 的用法幾乎相同, 唯一的差別在於:當函式需要傳遞多個變數時, apply 可以接受一個數組作為引數輸入, call 則是接受一系列的單獨變數。
看一個例子:

var obj = {
    message: 'My name is: '
}

function getName(firstName, lastName) {
    console.log(this.message + firstName + ' ' + lastName)
}

getName.apply(obj, ['Dot', 'Dolby'])// My name is: Dot Dolby

可以看到,obj 是作為函式上下文的物件,函式 getName 中 this 指向了 obj 這個物件。引數 firstName 和 lastName 是放在陣列中傳入 getName 函式。

call和apply可用來借用別的物件的方法,這裡以call()為例:

var Person1  = function () {
    this.name = 'Dot';
}
var Person2 = function () {
    this.getname = function () {
        console.log(this.name);
    }
    Person1.call(this);
}
var person = new Person2();
person.getname();       // Dot

從上面我們看到,Person2 例項化出來的物件 person 通過 getname 方法拿到了 Person1 中的 name。因為在 Person2 中,Person1.call(this) 的作用就是使用 Person1 物件代替 this 物件,那麼 Person2 就有了 Person1 中的所有屬性和方法了,相當於 Person2 繼承了 Person1 的屬性和方法。

對於什麼時候該用什麼方法,其實不用糾結。如果你的引數本來就存在一個數組中,那自然就用 apply,如果引數比較散亂相互之間沒什麼關聯,就用 call。像上面的找一組數中最大值的例子,當然是用apply合理。

bind

和call很相似,第一個引數是this的指向,從第二個引數開始是接收的引數列表。區別在於bind方法返回值是函式以及bind接收的引數列表的使用。

  • bind返回值是函式
var obj = {
    name: 'Dot'
}

function printName() {
    console.log(this.name)
}

var dot = printName.bind(obj)
console.log(dot) // function () { … }
dot()  // Dot

bind 方法不會立即執行,而是返回一個改變了上下文 this 後的函式。而原函式 printName 中的 this 並沒有被改變,依舊指向全域性物件 window。

  • 引數的使用
function fn(a, b, c) {
    console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');

fn('A', 'B', 'C');            // A B C
fn1('A', 'B', 'C');           // Dot A B
fn1('B', 'C');                // Dot B C
fn.call(null, 'Dot');      // Dot 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)));
        }
    }
}

應用場景

  • 求陣列中的最大和最小值
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('dot') // false
  • 利用call和apply做繼承
function Person(name,age){
    // 這裡的this都指向例項
    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}
function Female(){
    Person.apply(this,arguments)//將父元素所有方法在這裡執行一遍就繼承了
}
var dot = new Female('Dot',2)
  • 使用 log 代理 console.log
function log(){
  console.log.apply(console, arguments);
}
// 當然也有更方便的 var log = console.log()

總結

call、apply和bind函式存在的區別:

bind返回對應函式, 便於稍後呼叫; apply, call則是立即呼叫。

除此外, 在 ES6 的箭頭函式下, call 和 apply 將失效, 對於箭頭函式來說:

  • 箭頭函式體內的 this 物件, 就是定義時所在的物件, 而不是使用時所在的物件;所以不需要類似於var _this = this這種醜陋的寫法
  • 箭頭函式不可以當作建構函式,也就是說不可以使用 new 命令, 否則會丟擲一個錯誤
  • 箭頭函式不可以使用 arguments 物件,,該物件在函式體內不存在. 如果要用, 可以用 Rest 引數代替
  • 不可以使用 yield 命令, 因此箭頭函式不能用作 Generator 函式,什麼是Generator函式可自行查閱資料,推薦閱讀阮一峰Generator 函式的含義與用法Generator 函式的非同步應用