在JavaScript中函數的調用能夠有多種方式,但更經典的莫過於call和apply。call跟apply都綁定在函數上,他們兩個的第一個參數意義相同,傳入一個對象,他做爲函數的執行環境(實質上是爲了改變函數的Execution Context執行上下文),也就是this的指向;而第二個參數二者只是類型的不一樣,call傳的是arguments,而apply傳的是array。廢話很少說,先上一個最基礎的例子:javascript
function add(c,d){ return this.a + this.b + c + d; } var o = { a: 1, b: 2 } add.call(o,3,4) // 10 add.apply(o,[3,4]) // 10
再好比我以前看過的Twitter上的關於call和apply的一個面試題:定義一個函數log,傳入任意數量參數,讓它模擬console.log的方法,形如log('hello','world');java
function log(){ console.log.apply(console,arguments); }
當執行這個log函數時,該函數的執行上下文爲console對象,arguments爲傳入的實參。面試
而後此題又有需求,若是要給每一個log信息加一個(app)前綴,好比 '(app) hello world';數組
這時咱們應該想到一點,那就是咱們傳入的實參,也就是arguments並非一個數組,它實質上只是一個類數組的對象,咱們在這裏能夠用 instanceof 來判斷自定義對象。瀏覽器
instanceof的意思就是看左邊對象的原型鏈上是否有右邊構造器的prototype屬性。由上圖能夠看出arguments是一個類數組的對象,而且能夠看出arguments沒有array的方法。app
因此回到剛纔那個題,咱們必須讓傳入的實參變爲Array類型才能夠調用數組的unshift方法(在它的前面加上app)。函數
function log(){ var args = Array.prototype.slice.call(arguments); args.unshift('(app)'); console.log.apply(console,args); } log('hello','world'); // (app) hello world
在OOP面向對象中此方法用的更加多一些,當咱們不想爲另外一個對象建立方法的時候,能夠用call調用foo的方法。this
function foo () {} foo.prototype.name = 'james'; foo.prototype.sayHello = function(){ console.log(this.name); } var obj1 = new foo(); obj1.sayHello(); //james var obj2 = { name: 'bond' } obj1.sayHello.call(obj2); //bond
再好比若是要想調用一些不能直接調用的方法,好比Object.prototype.toString(),咱們也能夠用call,它的本質在於將內部的變量改成包裝對象。es5
這個對象原型的方法指向window,也就是this,可是若是把log函數裏call指向的對象改成window的話,就會輸出global。由於在執行上下文中的全局對象爲Global Context,spa
比方說全局的Math,String,window都存在於[[global]]變量對象中。
window對象依附於Global全局變量對象,雖然權威上來講在NodeJS裏全局是Global對象,可是在全局執行上下文中window指向global,請看下圖。
function log(){
console.log(this===window); // true return Object.prototype.toString.call(this); } log.call(5) // [Object Number] log.call(true) // [Object Boolean]
對於js中的bind方法,他跟apply和call基本同樣,裏面傳遞的參數也是改變this指向的,比方下面一個關於執行上下文的代碼,經過bind改變this的指向。
var User = { count: 1, getCount: function(){ return this.count; } } var func = User.getCount; console.log(func()); //undefined
var func1 = User.getCount.bind(User)
console.log(func1()); // 1
上面的答案是undefined,由於func是在全局做用域window中,window裏面沒有count屬性,因此咱們爲了讓this指向User對象,咱們便想到了使用bind。可是bind是es5纔有的方法,不兼容老版本瀏覽器,那如何解決這個問題呢?下面是我從火狐的MDN上拿下來的bind模擬。
if(!Function.prototype.bind){ Function.prototype.bind = function(oThis){ if(typeof this !== 'function'){ throw new TypeError('What is trying to be bound is not callable'); } var args = Array.prototype.slice.call(arguments,1), fTobind = this, //指向調用bind的函數 fNOP = function(){}, //建立一個空函數,爲了下面的繼承 fBound = function(){ return fTobind.apply(this instanceof fNOP ? this : oThis, //改變this的指向 args.concat(Array.prototype.slice.call(arguments))); //將經過bind傳遞的參數與調用時傳的參數合併 }; fNOP.prototype = this.prototype; //將目標函數的原型傳遞到新函數中 fBound.prototype = new fNOP; //這兩條至關於Object.create的做用 return fBound; } }
咱們經過下面這個例子來分析它吧:
function foo(){ this.b = 100; return this.a; }; var func = foo.bind({a:1}); func() // 1 new func() // {b:100}
fBound指向新函數func,由於foo調用的bind方法,因此fToBind指向目標函數foo,在func()裏面this指向傳進去的對象,也就是bind模擬裏面的oThis,經過this instanceof fNOP來判斷this的指向,由於func()是window調用下的,因此this指向window,因此this instanceof fNOP 返回false,因此oThis也就是傳入的{a:1}爲當前的執行上下文,因此彈出1。但若是是對象調用( new Func() )的話,this instanceof fNOP中的this會指向一個空對象,空對象的原型會指向構造器的prototype屬性,即func的prototype屬性,這裏注意一點,由於foo 中 return的不是對象,因此忽略return。