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 函数。