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(); 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){ addTen(2); |
在低版本瀏覽器沒有 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 函數。