call、apply和bind方法的用法以及區別
call、apply、bind的作用是改變函數運行時this的指向,所以先說清楚this。
以下是函數的調用方法:
方法調用模式:
當一個函數被保存為對象的一個方法時,如果調用表達式包含一個提取屬性的動作,那么它就是被當做一個方法來調用,此時的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 函數的異步應用