<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> var name = "李四"; function Coder(name) { this.name = name; function alerts() { alert(this.name); } this.getName = function() { console.log(this.name) }; this.delayGetName = function() { setTimeout(function() { alert(this.name); }, 1000); //李四 }; } var me = new Coder('張三') me.delayGetName(); </script> </head> <body> </body> </html>
上面的 setTimeout 里面的this 指向window;
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> var name = "李四"; function Coder(name) { this.name = name; function alerts() { alert(this.name); } this.getName = function() { console.log(this.name) }; this.delayGetName = function() { var that=this; //改變this指向 setTimeout(function() { alert(that.name); }, 1000); //張三 }; } var me = new Coder('張三') me.delayGetName(); </script> </head> <body> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> var name = "李四"; function Coder(name) { this.name = name; function alerts() { alert(this.name); } this.getName = function() { console.log(this.name) }; this.delayGetName = function() { setTimeout(function() { var that=this; //setTimeout 里面的this 指向window alert(that.name); }, 1000); //李四 }; } var me = new Coder('張三') me.delayGetName(); </script> </head> <body> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> var name = "李四"; function Coder(name) { this.name = name; function alerts() { alert(this.name); } this.getName = function() { console.log(this.name) }; this.delayGetName = function() { setTimeout(function() { alert(this.name); }.bind(this), 1000); // 張三 }; } var me = new Coder('張三') me.delayGetName(); </script> </head> <body> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> var name = "李四"; function Coder(name) { this.name = name; function alerts() { alert(this.name); } this.getName = function() { console.log(this.name) }; this.delayGetName = function() { setTimeout(()=>{ //使用箭頭函數 alert(this.name); }, 1000); // 張三 }; } var me = new Coder('張三') me.delayGetName(); </script> </head> <body> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> function Coder(name) { this.name = name; function alerts() { alert(this.name); } this.getName = function() { console.log(this.name) }; this.delayGetName = function() { setTimeout(alerts.bind(this), 1000); //張三 }; } var me = new Coder('張三') me.delayGetName(); //延遲一秒輸出Jins </script> </head> <body> </body> </html>
再來看看匿名函數的this:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript" src="js/jquery-3.0.0.min.js"></script> <script type="text/javascript"> $(function() { var name = "李四"; var obj2 = { name: "張三", fun: function() { console.log(this); console.log(this.name); } } /* $(document).click(function() { console.log(this.name); //李四 }.bind(window)); */ /* $(document).click(function() { console.log(window.name);//李四 }); */ /* $(document).click(function() { var that = window; console.log(that.name); //李四 }); */ //$(document).click(obj2.fun.bind(obj2)); //張三 $(document).click(obj2.fun); //document undefined //$(document).click($.proxy(obj2.fun, obj2)); //張三 }) </script> </head> <body> </body> </html>
bind顧名思義,綁定。
bind()方法會創建一個新函數,當這個新函數被調用時,它的this值是傳遞給bind()的第一個參數,它的參數是bind()的其他參數和其原本的參數。
上面這個定義最后一句有點繞,我們來理一下。
bind()接受無數個參數,第一個參數是它生成的新函數的this指向,比如我傳個window,不管它在何處調用,這個新函數中的this就指向window,這個新函數的參數就是bind()的第二個、第三個、第四個....第n個參數加上它原本的參數。(行吧,我自己都蒙圈了)
我們還是看看栗子比較好理解,舉個bind()最基本的使用方法:
this.x = 9; var module = { x: 81, getX: function() { return this.x; } }; module.getX(); // 返回 81 var retrieveX = module.getX; retrieveX(); // 返回 9, 在這種情況下,"this"指向全局作用域 // 創建一個新函數,將"this"綁定到module對象 // 新手可能會被全局的x變量和module里的屬性x所迷惑 var boundGetX = retrieveX.bind(module); boundGetX(); // 返回 81
這里很明顯,我們在window對象下調用retrieveX,得到的結果肯定是window下的x,我們把module對象綁定到retrieveX的this上,問題就解決了,不管它在何處調用,this都是指向module對象。
還有bind()的其他參數,相信第一次接觸bind()的朋友看到上面的定義都會蒙圈。
還是舉個栗子:
function list() { return Array.prototype.slice.call(arguments); } var list1 = list(1, 2, 3); // [1, 2, 3] // 創建一個擁有預設初始參數的函數 var leadingThirtysevenList = list.bind(undefined,[69,37],{a:2}); var list2 = leadingThirtysevenList(); // [[69,37],{a:2}] var list3 = leadingThirtysevenList(1, 2, 3); // [[69,37],{a:2}, 1, 2, 3]
list函數很簡單,把傳入的每個參數插入到一個數組里,我們用bind()給list函數設置初始值,因為不用改變list中this的指向,所以直接傳undefined,從第二個參數開始,就是要傳入list函數的值,list2和list3的返回值很好的說明了一切。
我自己一般使用的bind()的場景是配合setTimeout函數,因為在執行setTimeout時,this會默認指向window對象,在使用bind()之前,我是這么做的:
function Coder(name) { var that = this; that.name = name; that.getName = function() { console.log(that.name) }; that.delayGetName = function() { setTimeout(that.getName,1000) }; } var me = new Coder('Jins') me.delayGetName()//延遲一秒輸出Jins
在函數內頂層定義一個that緩存this的指針,這樣不論怎么調用,that都是指向 Coder的實例,但是多定義一個變量總是讓人不太舒服。
使用bind()就簡單多了:
function Coder(name) { this.name = name; this.getName = function() { console.log(this.name) }; this.delayGetName = function() { setTimeout(this.getName.bind(this),1000) }; } var me = new Coder('Jins') me.delayGetName()//延遲一秒輸出Jins
這樣就OK了,直接把setTimeout的this綁定到外層的this,這肯定是我們想要的!
行吧,先聊這么多,堅持學習!
bind()
方法會創建一個新的函數,成為綁定函數。當調用這個綁定函數時,綁定函數會以創建它時傳入的第一個參數作為this
,傳入bind()
方法的第二個以及以后的參數加上綁定函數運行時本身的參數按照順序作為原函數的參數來調取原函數。
實際使用中我們經常會碰到這樣的問題:
var name = "pig"; function Person(name){ this.name = name; this.getName = function(){ setTimeout(function(){ console.log("Hello,my name is "+this.name); },100); } } var weiqi = new Person("衛旗"); weiqi.getName(); //Hello,my name is pig
這個時候輸出this.name
是pig
,原因是this的指向是在運行函數時確定的,而不是在定義函數時確定的,再因為setTimeout
是在全局環境下只想,所以this就指向了window
。
以前解決這個問題的辦法通常是緩存this
,例如:
var name = "pig"; function Person(name){ this.name = name; this.getName = function(){ //在這里緩存一個this var self = this; setTimeout(function(){ //在這里是有緩存this的self console.log("Hello,my name is "+self.name); },100); } } var weiqi = new Person("衛旗"); weiqi.getName(); //Hello,my name is 衛旗
這樣就解決了這個問題,非常方便,因為它使得setTimeout函數中可以訪問Person的上下文。
現在有一個更好的解決辦法,可以使用bind()函數,上面的例子可以被更新為:
var name = "pig"; function Person(name){ this.name = name; this.getName = function(){ setTimeout(function(){ console.log("Hello,my name is "+this.name); }.bind(this),100); //注意上面這一行,添加了bind(this) } } var weiqi = new Person("衛旗"); weiqi.getName(); //Hello,my name is 衛旗
bind()最簡單的用法是創建一個函數,使得這個函數無論怎么樣調用都擁有同樣的this值。JavaScript新手經常犯的一個錯誤就是將一個方法從一個對象中拿出來,然后再調用,希望方法中的this是原來的對象(比如在回調函數中傳入這個方法)。如果不做特殊處理的話,一般會丟失原來的對象。從原來的函數和原來的對象創建一個綁定函數,則可以很漂亮的解決這個問題:
//定義全局變量x var x = "window"; //在module內部定義x var module = { x:"module", getX:function(){ console.log(this.x); } } module.getX(); //返回module,因為在module內部調用getX() var getX = module.getX; getX(); //返回window,因為這個getX()是在全局作用域中調用的 //綁定getX()並將this值設為module var boundGetX = getX.bind(module); boundGetX(); //返回module,綁定以后this值始終為module
瀏覽器支持情況:
Browser | Version support |
---|---|
Chrome | 7 |
FireFox(Gecko) | 4.0(2) |
Internet Explorer | 9 |
Opera | 11.60 |
Safari | 5.14 |
很不幸,Function.prototype.bind
在IE8及以下版本中不被支持,所以如果沒有一個備選方案的話,可能會在運行時出現問題。bind
函數在ECMA-262第五版才被加入。它可能不無法在所有瀏覽器上運行。你可以在腳本部分加入如下代碼,讓不支持的瀏覽器也能使用bind()
功能。
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== "function") { // closest thing possible to the ECMAScript 5 internal IsCallable function throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply(this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; }
語法
fun.bind(thisArg[, arg1[, arg2[, …]]])
參數
thisArg
,當綁定函數被調用時,該參數會作為原函數運行時的this指向,當使用new
操作符調用綁定函數時,該參數無效。
arg1, arg2, …
,當綁定函數被調用時,這些參數加上綁定函數本身的參數會按照順序作為原函數運行時的參數。
描述
bind()函數會創建一個新的函數(一個綁定的函數)有同樣的函數體(在ECMAScript 5 規范內置Call屬性),當該函數(綁定函數的原函數)被調用時this值綁定到bind()的第一個參數,該參數不能被重寫。綁定函數被調用時,bind()也接受預設的參數提供給原函數。一個綁定函數也能使用new操作符創建對象:這種行為就像把原函數當成構造器。提供的this值被忽略,同事調用的參數被提供給模擬函數。
總結:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> var name = "李四"; function Coder(name) { this.name = name; function alerts() { console.log('alert:' + this.name); } this.getName = function() { console.log('this.getName'+this.name) }; this.delayGetName = function() { setTimeout(function() { console.log('--:' + this.name) }, 1000); }; this.delayGetName0 = function() { setTimeout(() => { console.log('0:' + this.name); }, 1000); }; this.delayGetName1 = function() { var that = this; setTimeout(function() { console.log('1:' + that.name); }, 1000); }; this.delayGetName2 = function() { setTimeout(function() { console.log('2:' + this.name); }.bind(this), 1000); }; this.delayGetName3 = function() { setTimeout(function() { console.log('3:' + this.name); }.call(this), 1000); }; this.delayGetName4 = function() { setTimeout(function() { console.log('4:' + this.name); }.apply(this), 1000); }; this.delayGetName5 = function() { setTimeout(alerts.bind(this), 1000); }; this.delayGetName6 = function() { setTimeout(this.getName.bind(this), 1000); }; } var me = new Coder('張三'); me.delayGetName(); me.delayGetName0(); me.delayGetName1(); me.delayGetName2(); me.delayGetName3(); me.delayGetName4(); me.delayGetName5(); me.delayGetName6(); </script> </head> <body> </body> </html>
前言
回想起之前的一些面試,幾乎每次都會問到一個js中關於call、apply、bind的問題,比如…
- 怎么利用call、apply來求一個數組中最大或者最小值
- 如何利用call、apply來做繼承
- apply、call、bind的區別和主要應用場景
雖然網上有很多關於這方面的博客和文章,但還是決定寫一篇自己對這方面知識的理解。
作用
首先問個問題,這三個函數的存在意義是什么?答案是改變函數執行時的上下文,再具體一點就是改變函數運行時的this指向。有了這個認識,接下來我們來看一下,怎么使用這三個函數。
舉個栗子
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function Person(name){
this.name = name;
}
Person.prototype = {
constructor: Person,
showName: function(){
console.log(this.name);
}
}
var person = new Person('qianlong');
person.showName();
|
上面的代碼中person調用showName方法后會在瀏覽器的控制台輸出qianlong
接下來
1
2
3
|
var animal = {
name: 'cat'
}
|
上面代碼中有一個對象字面量,他沒有所謂的showName方法,但是我還是想用?怎么辦?(坑爹了,這好像在讓巧媳婦去做無米之炊),不過沒關系,call、apply、bind可以幫我們干這件事。
1
2
3
4
5
6
|
// 1 call
person.showName.call(animal);
// 2 apply
person.showName.apply(animal);
// 3 bind
person.showName.bind(animal)();
|
啦啦啦,有木有很神奇,控制台輸出了三次cat
我們拿別人的showName方法,並動態改變其上下文幫自己輸出了信息,說到底就是實現了復用
區別
上面看起來三個函數的作用差不多,干的事幾乎是一樣的,那為什么要存在3個家伙呢,留一個不就可以。所以其實他們干的事從本質上講都是一樣的動態的改變this上下文,但是多少還是有一些差別的..
call、apply與bind的差別
call和apply改變了函數的this上下文后便執行該函數,而bind則是返回改變了上下文后的一個函數。
call、apply的區別
他們倆之間的差別在於參數的區別,call和aplly的第一個參數都是要改變上下文的對象,而call從第二個參數開始以參數列表的形式展現,apply則是把除了改變上下文對象的參數放在一個數組里面作為它的第二個參數。
1
2
3
|
fn.call(obj, arg1, arg2, arg3...);
fn.apply(obj, [arg1, arg2, arg3...]);
|
應用
知道了怎么使用和他們之間的區別,接下來我們來了解一下通過call、apply、bind的常見應用場景。
- 求數組中的最大和最小值
1
2
3
4
5
6
7
|
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
Math.max.apply(Math, arr);
Math.max.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
Math.min.apply(Math, arr);
Math.min.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
|
- 將偽數組轉化為數組
js中的偽數組(例如通過
document.getElementsByTagName
獲取的元素)具有length屬性,並且可以通過0、1、2…下標來訪問其中的元素,但是沒有Array中的push、pop等方法。我們可以利用call、apply來將其轉化為真正的數組這樣便可以方便地使用數組方法了。
1
2
3
4
5
6
|
var arrayLike = {
0: 'qianlong',
1: 'ziqi',
2: 'qianduan',
length: 3
}
|
上面就是一個普通的對象字面量,怎么把它變成一個數組呢?最簡單的方法就是
1
|
var arr = Array.prototype.slice.call(arrayLike);
|
上面arr便是一個包含arrayLike元素的真正的數組啦( 注意數據結構必須是以數字為下標而且一定要有length屬性 )
- 數組追加
在js中要往數組中添加元素,可以直接用push方法,
1
2
3
4
5
6
7
|
var arr1 = [1,2,3];
var arr2 = [4,5,6];
[].push.apply(arr1, arr2);
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]
|
判斷變量類型
對於對象型的數據類型,我們可以借助call來得知他的具體類型,例如數組
1
2
3
4
5
6
|
function isArray(obj){
return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([])
// true
isArray(
'qianlong') // false
|