call、apply、bind的作用是改变函数运行时this的指向
方法调用模式:
当一个函数被保存为对象的一个方法时,如果调用表达式包含一个提取属性的动作,那么他就是被当做一个方法来调用,此时的this被绑定到这个对象。例如
var a = 1; | obj.fn(); 输出结果:2 |
- 此时 this 是指 obj 这个对象,obj.fn()实际是 obj.fn.call(obj) 。
- 事实上谁调用这个函数,this就是谁。
- DOM对象绑定事件也属于方法调用模式,因此它绑定的this就是事件源DOM对象。如
document.addEventListener('click', function(e){ | 点击页面,依次输出:document 和 window对象 |
解析:点击页面监听click事件属于方法调用,this指向事件源DOM对象,即 obj.fn.apply(obj),setTimeout内的函数属于回调函数,可以这么理解,f1.call(null, f2),所以this指向window。
函数调用模式:
就是普通函数的调用,此时的this被绑定到window
最普通的函数调用 | 函数嵌套 | 把函数赋值后再调用 |
function fn(){ | function fn1(){ | var a = 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; | var a = 1; |
仍旧是最普通的函数调用,f1.call(undefined),this指向window,打印出的是全局的a;借此可以解释为什么setTimeout总是丢失this了,因为它也就是一个回调函数而已。
setTimeout(function(){ |
构造器调用模式:
new一个函数时,背地里会创建一个连接到 prototype 成员的新对象,同时 this 会被绑定到那个新对象上
function Person(name, age){ | var per = new Person('yw', 2); per.sayAge(); // 2 |
call
call方法第一个参数是要绑定给 this 的值,后面传入的是一个参数列表。当第一个参数为 null、undefined的时候,默认指向window。
var arr = [1, 2, 3, 89, 46]; |
可以这么理解
obj.fn(); => obj.fn.call(obj); | fn(); => fn.call(null); | f1(f2); => f1.call(null, f2); |
来看一个例子:
var obj = { message: 'My name is: '}; | 输出:My name is: yang wei |
apply
apply接收两个参数,第一个参数是要绑定给 this 的值,第二个参数是一个参数数组。当第一个参数为 null、undefined的时候,默认指向window。
var arr = [1, 2, 3, 89, 46]; |
可以这么理解
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: '}; | 输出:My name is: yang wei |
可以看到,obj是作为函数上下文的对象,函数 getName 中 this 指向了 obj 这个对象,参数 firstName 和 lastName 是放在数组中传入 getName 函数。
call 和 apply 可用来借用别的对象的方法,这里以call()为例
var Person1 = function(){ | var person = new Person2(); person.getName(); //yang Person2实例化出来的对象 person 通过 getName 方法拿到了Person1 中的name。 因为Person2中,Person1.call(this) 的作用就是使用Person1对象代替 this 对象, 那么Person2 就有了 Person1 中的所有属性和方法了, 相当于 Person2 继承了 Person1 的属性和方法。 |
对于什么时候用什么方法?如果参数本来就存在一个数组中,那就用 apply ,如果参数比较散乱相互之间没有什么关联,就用call。
bind
和 call 很相似,第一个参数是 this 的指向,从第二个参数开始是接收的参数列表。区别在于 bind 方法返回值是函数以及 bind 接收的参数列表的使用。
(1)bind返回值函数
var obj = { name: 'yangwei' }; | console.log(yw); //function(){ ... } |
bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而原函数 printName 中的 this 并没有被改变,依旧指向全局对象 window。
(2)参数的使用
function fn(a, b, c){ 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 ){ |
应用场景
求数组中的最大和最小值 | var arr = [1, 2, 3, 89, 46]; |
将类数组转为数组 | var trueArr = Array.prototype.slice.call( arrayLike ); |
数组追加 | var arr1 = [1, 2, 3]; |
判断变量类型 | function isArray(obj){ |
利用call和apply做继承 | fucntion Person(name, age){ |
使用log代理console.log | function log(){ |
总结
bind返回对应函数,便于之后调用;apply、call则是立即调用。除此之外,在ES6的箭头函数下,call和apply将失效,对于箭头函数来说:
- 箭头函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象;所以不需要类似于var _this = this这种丑陋的写法;
- 箭头函数不可以当作构造函数,也就是说不可以使用 new 命令, 否则会抛出一个错误;
- 箭头函数不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 Rest 参数代替;
- 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。