call、apply和bind方法的用法以及區(qū)別

call、apply、bind的作用是改變函數(shù)運(yùn)行時(shí)this的指向,所以先說清楚this。

以下是函數(shù)的調(diào)用方法:

方法調(diào)用模式:

當(dāng)一個(gè)函數(shù)被保存為對(duì)象的一個(gè)方法時(shí),如果調(diào)用表達(dá)式包含一個(gè)提取屬性的動(dòng)作,那么它就是被當(dāng)做一個(gè)方法來調(diào)用,此時(shí)的this被綁定到這個(gè)對(duì)象。

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

此時(shí)的this是指obj1這個(gè)對(duì)象,obj1.fn()實(shí)際上是obj1.fn.call(obj1),事實(shí)上誰調(diào)用這個(gè)函數(shù),this就是誰。補(bǔ)充一下,DOM對(duì)象綁定事件也屬于方法調(diào)用模式,因此它綁定的this就是事件源DOM對(duì)象。如:

document.addEventListener('click', function(e){
    console.log(this);
    setTimeout(function(){
        console.log(this);
    }, 200);
}, false);

點(diǎn)擊頁面,依次輸出:document和window對(duì)象
解析:點(diǎn)擊頁面監(jiān)聽click事件屬于方法調(diào)用,this指向事件源DOM對(duì)象,即obj.fn.apply(obj),setTimeout內(nèi)的函數(shù)屬于回調(diào)函數(shù),可以這么理解,f1.call(null,f2),所以this指向window。

函數(shù)調(diào)用模式:

就是普通函數(shù)的調(diào)用,此時(shí)的this被綁定到window

  • 最普通的函數(shù)調(diào)用
function fn1(){
      console.log(this)//window
    }
fn1()
  • 函數(shù)嵌套
function fn1(){
    function fn2(){
        console.log(this)//window
    }
    fn2()
}
fn1()
  • 把函數(shù)賦值之后再調(diào)用
var a = 1
var obj1 = {
    a:2,
    fn:function(){
        console.log(this.a)
    }
}
var fn1 = obj1.fn
fn1()//1

obj1.fn是一個(gè)函數(shù)function(){console.log(this.a)},此時(shí)fn1就是不帶任何修飾的函數(shù)調(diào)用,function(){console.log(this.a)}.call(undefined),按理說打印出來的 this 應(yīng)該就是 undefined 了吧,但是瀏覽器里有一條規(guī)則:

如果你傳的 context 就 null 或者 undefined,那么 window 對(duì)象就是默認(rèn)的 context(嚴(yán)格模式下默認(rèn) context 是 undefined)

因此上面的this綁定的就是window,它也被稱為隱性綁定。
如果你希望打印出2,可以修改fn1()fn1.call(obj1),顯示地綁定this為obj1

  • 回調(diào)函數(shù)
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()

仍舊是最普通的函數(shù)調(diào)用,f1.call(undefined),this指向window,打印出的是全局的a。
借此,我們終于可以解釋為什么setTimeout總是丟失this了,因?yàn)樗簿褪且粋€(gè)回調(diào)函數(shù)而已。

setTimeout(function() {
    console.log(this)//window
    function fn(){
        console.log(this)//window
    }
    fn()
}, 0);
構(gòu)造器調(diào)用模式:

new一個(gè)函數(shù)時(shí),背地里會(huì)將創(chuàng)建一個(gè)連接到prototype成員的新對(duì)象,同時(shí)this會(huì)被綁定到那個(gè)新對(duì)象上

function Person(name,age){
// 這里的this都指向?qū)嵗?    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}

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

call

call 方法第一個(gè)參數(shù)是要綁定給this的值,后面?zhèn)魅氲氖且粋€(gè)參數(shù)列表。當(dāng)?shù)谝粋€(gè)參數(shù)為null、undefined的時(shí)候,默認(rèn)指向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)

看一個(gè)例子:

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

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

getName.call(obj, 'Dot', 'Dolby')

apply

apply接受兩個(gè)參數(shù),第一個(gè)參數(shù)是要綁定給this的值,第二個(gè)參數(shù)是一個(gè)參數(shù)數(shù)組。當(dāng)?shù)谝粋€(gè)參數(shù)為null、undefined的時(shí)候,默認(rèn)指向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用法很像,事實(shí)上apply 和 call 的用法幾乎相同, 唯一的差別在于:當(dāng)函數(shù)需要傳遞多個(gè)變量時(shí), apply 可以接受一個(gè)數(shù)組作為參數(shù)輸入, call 則是接受一系列的單獨(dú)變量。
看一個(gè)例子:

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 是作為函數(shù)上下文的對(duì)象,函數(shù) getName 中 this 指向了 obj 這個(gè)對(duì)象。參數(shù) firstName 和 lastName 是放在數(shù)組中傳入 getName 函數(shù)。

call和apply可用來借用別的對(duì)象的方法,這里以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 實(shí)例化出來的對(duì)象 person 通過 getname 方法拿到了 Person1 中的 name。因?yàn)樵?Person2 中,Person1.call(this) 的作用就是使用 Person1 對(duì)象代替 this 對(duì)象,那么 Person2 就有了 Person1 中的所有屬性和方法了,相當(dāng)于 Person2 繼承了 Person1 的屬性和方法。

對(duì)于什么時(shí)候該用什么方法,其實(shí)不用糾結(jié)。如果你的參數(shù)本來就存在一個(gè)數(shù)組中,那自然就用 apply,如果參數(shù)比較散亂相互之間沒什么關(guān)聯(lián),就用 call。像上面的找一組數(shù)中最大值的例子,當(dāng)然是用apply合理。

bind

和call很相似,第一個(gè)參數(shù)是this的指向,從第二個(gè)參數(shù)開始是接收的參數(shù)列表。區(qū)別在于bind方法返回值是函數(shù)以及bind接收的參數(shù)列表的使用。

  • bind返回值是函數(shù)
var obj = {
    name: 'Dot'
}

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

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

bind 方法不會(huì)立即執(zhí)行,而是返回一個(gè)改變了上下文 this 后的函數(shù)。而原函數(shù) printName 中的 this 并沒有被改變,依舊指向全局對(duì)象 window。

  • 參數(shù)的使用
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 是把第二個(gè)及以后的參數(shù)作為 fn 方法的實(shí)參傳進(jìn)去,而 fn1 方法的實(shí)參實(shí)則是在 bind 中參數(shù)的基礎(chǔ)上再往后排。

有時(shí)候我們也用bind方法實(shí)現(xiàn)函數(shù)珂里化,以下是一個(gè)簡單的示例:

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 方法,我們也可以自己實(shí)現(xiàn)一個(gè)。

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

應(yīng)用場景

  • 求數(shù)組中的最大和最小值
var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89
var min = Math.min.apply(null,arr)//1
  • 將類數(shù)組轉(zhuǎn)化為數(shù)組
var trueArr = Array.prototype.slice.call(arrayLike)
  • 數(shù)組追加
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都指向?qū)嵗?    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}
function Female(){
    Person.apply(this,arguments)//將父元素所有方法在這里執(zhí)行一遍就繼承了
}
var dot = new Female('Dot',2)
  • 使用 log 代理 console.log
function log(){
  console.log.apply(console, arguments);
}
// 當(dāng)然也有更方便的 var log = console.log()

總結(jié)

call、apply和bind函數(shù)存在的區(qū)別:

bind返回對(duì)應(yīng)函數(shù), 便于稍后調(diào)用; apply, call則是立即調(diào)用。

除此外, 在 ES6 的箭頭函數(shù)下, call 和 apply 將失效, 對(duì)于箭頭函數(shù)來說:

  • 箭頭函數(shù)體內(nèi)的 this 對(duì)象, 就是定義時(shí)所在的對(duì)象, 而不是使用時(shí)所在的對(duì)象;所以不需要類似于var _this = this這種丑陋的寫法
  • 箭頭函數(shù)不可以當(dāng)作構(gòu)造函數(shù),也就是說不可以使用 new 命令, 否則會(huì)拋出一個(gè)錯(cuò)誤
  • 箭頭函數(shù)不可以使用 arguments 對(duì)象,,該對(duì)象在函數(shù)體內(nèi)不存在. 如果要用, 可以用 Rest 參數(shù)代替
  • 不可以使用 yield 命令, 因此箭頭函數(shù)不能用作 Generator 函數(shù),什么是Generator函數(shù)可自行查閱資料,推薦閱讀阮一峰Generator 函數(shù)的含義與用法Generator 函數(shù)的異步應(yīng)用

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容