在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。