call、apply和bind的区别

133 阅读4分钟

call、apply和bind都是为了改变函数运行时的this指向。

所以先说清楚this

函数调用方法分为:方法调用模式、函数调用模式、构造器调用模式。

方法调用模式

当一个函数被保存为对象的一个方法时,如果调用表达式包含一个提取的动作,那么它就是被当做一个方法来调用,此时的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就是谁。

函数调用模式

就是普通的函数调用,此时的this被绑定在window上

普通的函数调用
function fn(){
    console.log(this)
}
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就是不带任何修饰的函数调用,fn1()也就等于function(){console.log(this.a).call(undifined)},因此上面的this绑定的是window。严格模式下是undifined。

还有一种,回调函数也属于函数调用模式

var a = 1;
functtion fn1(fn){
    fn()
    console.log(a)
}
fn2(){
    var a = 2
}
fn(f2) // 1

代码改写
var a = 1;
function fn1(){
    (function(){var a = 2})()
    console.log(a)
}
fn1()

改写之后发现,仍然是一个普通的函数调用。fn1.call(undifined),this指向window。同样的setTimeout也是一个回调函数,所以函数内部的this指向window。

构造器调用模式

new一个函数时,背地里会创建一个连接到prototype成员的新对象,同时this会绑定到那个新对象上。

function Person(name,age){
    this.name = name
    this.age = age
    this.sayName = function(){
        console.log(this.name)
    }
}
var p1 = new Person('Arya', 24)
p1.sayName() // 'Arya'

此时的this绑定在p1上,也就是p1.sayName.call(p1)

说了这么多函数调用方法来解释this指向,终于要进入我们的正题,call、apply和bind是怎么去改变this的指向的呢。

call、apply

call方法第一个参数是要绑定给this的值,后面传入的是一个参数列表。apply方法第一个参数也是要绑定给this的值,后面传入的是个数组。当第一个参数为null或者undifined的时候,默认指向window。

var arr = [1,2,3,4]
var max = Math.max.call(null,arr[0],arr[1],arr[2],arr[3])
var max = Math.max.apply(null,arr)
此时this指向window

var obj = {
    msg: 'My name is: '
}
function getName(firstName, lastName){
    console.log(this.msg+firstName+' '+lastName)
}
getName.call(obj,'Arya','Chen')
getName.apply(obj,['Arya','Chen'])

事实上apply 和 call 的用法几乎相同, 唯一的差别在于:当函数需要传递多个变量时, apply 可以接受一个数组作为参数输入, call 则是接受一系列的单独变量。

对于什么时候该用什么方法,其实不用纠结。如果你的参数本来就存在一个数组中,那自然就用 apply,如果参数比较散乱相互之间没什么关联,就用 call。

bind

和call很相似,第一个参数是this的指向,从第二个参数开始是接收的参数。区别在意bind方法返回值是函数以及bind接收的参数列表的使用。

1、bind返回值是函数

var obj = {
    name: 'Arya'
}
function printName(){
    console.log(this.name)
}
var p1 = printName.bind(obj)
console.log(p1)  // ƒ printName(){console.log(this.name)}
p1() // 'Arya'

bind方法不会立即执行,而是返回一个改变了上下文this的函数,而printName中的this并没有改变,仍然指向全局对象window。

2、参数的使用

function fn(a,b,c){
    console.log(a,b,c)
}
var fn1 = fn.bind(null,'Arya')
fn1('A','B','C') // Arya A B
fn.call(null,'Arya') // Arya undefined undefined

call 是把第二个及以后的参数作为 fn 方法的实参传进去,而 fn1 方法的实参实则是在 bind 中参数的基础上再往后排。

使用场景

求数组中的最大值最小值

var arr = [1,45,6,34,67,5]
var max = Math.max.apply(null,arr)
var min = Math.min.apply(null,arr)

将类数组转化为数组

var arrayLike={
0:'a',
1:'b',
length:2,
push:Array.prototype.push
}
var trueArr = Array.prototype.slice.call(arrayLike)

数组追加

var arr1 = [1,2,3]
var arr2 = [4,5,6]
var total = [].push.apply(arr1,arr2)
console.log(arr1,arr2,total) (6) [1, 2, 3, 4, 5, 6] (3) [4, 5, 6] 6

判断变量类型

function isArray(obj){
    return Object.prototype.toString.call(obj) == '[object Array]'
}
isArray([]) //true
isArray('123') //false

利用call和apply做继承

function Person(name,age){
    this.name = name
    this.age = age
    this.sayAge=function(){
        console.log(this.age)
    }
}

function Female(){
    Person.apply(this, arguments) //将父元素所有方法在这里执行一遍就继承了
}
var obj = new Female('Arya', 20)

使用log代console.log

function log(){
    console.log.apply(console, arguments)
}
// 当然也有更方便的 var log = console.log()

总结 call、apply和bind函数存在的区别: bind返回对应函数, 便于稍后调用; apply, call则是立即调用,参数传递方式不同。 除此外, 在 ES6 的箭头函数下, call 和 apply 将失效,

  • 箭头函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象;所以不需要类似于var _this = this这种丑陋的写法
  • 箭头函数不可以当作构造函数,也就是说不可以使用 new 命令, 否则会抛出一个错误
  • 箭头函数不可以使用 arguments 对象,该对象在函数体内不存在. 如果要用, 可以用 Rest 参数代替
  • 不可以使用 yield 命令, 因此箭头函数不能用作 Generator 函数,什么是Generator函数可自行查阅资料。