先记住这个答案
call、apply、bind 都能指定函数执行时的 this。call(thisArg, a, b) 和 apply(thisArg, [a, b]) 会立即调用函数,只是传参方式不同,一个逐个传,一个传数组。bind(thisArg, a) 不会调用,而是返回一个新函数,这个新函数的 this 永远固定为 thisArg,前面的参数也可以预先填好。所以要马上执行用 call 或 apply,要把函数交给别人稍后调用(事件回调、定时器)用 bind。
call、apply立即调用call逐个传参,apply传数组或类数组bind后再call也改不了this- 箭头函数没有自己的
this,三者都改不了它
一段代码对比三种写法
同一个 greet 函数,分别用三种方式指定 this。
const tom = { name: 'Tom' }
function greet(greeting, mark) {
return greeting + ', ' + this.name + mark
}
console.log(greet.call(tom, 'Hi', '!')) // 参数逐个传
console.log(greet.apply(tom, ['Hello', '?'])) // 参数放数组里
const greetTom = greet.bind(tom, 'Hey') // 固定 this,并预设第一个参数
console.log(greetTom('~'))
console.log(greetTom.call({ name: 'Jerry' }, '.')) // 想再改 this,改不动
console.log(Math.max.apply(null, [3, 9, 5])) // apply 的经典用法:把数组摊开当参数查看输出与解释
Hi, Tom!
Hello, Tom?
Hey, Tom~
Hey, Tom.
9第四行最容易考:greetTom 已经被 bind 过,内部固定了 this 是 tom,再用 call 传 Jerry 也没用,打印的还是 Tom。同理,bind 连着调用两次,只有第一次生效。
各自最常见的使用场景
call 最常见的场景是借用别的对象的方法。比如 Object.prototype.toString.call(value) 用来判断类型,能拿到 '[object Array]' 这种精确结果;ES5 继承里写 Parent.call(this, name),让父构造函数在子类实例上初始化属性。
apply 适合参数本来就在数组里的情况。以前求数组最大值写 Math.max.apply(null, arr),现在用展开语法 Math.max(...arr) 更直观,所以 apply 在新代码里出现得越来越少。
bind 用在「函数要交出去,但 this 不能丢」的场景。比如把对象方法传给 addEventListener 或 setTimeout,如果直接传 obj.method,调用时 this 就不再是 obj 了。写成 obj.method.bind(obj) 就能锁住。React 类组件在构造函数里写 this.handleClick = this.handleClick.bind(this) 就是这个原因。
为什么方法传出去会丢 this
普通函数的 this 不是定义时决定的,而是调用时看「点号前面是谁」。obj.method() 调用,this 是 obj;把 obj.method 赋给变量 fn 再调用 fn(),点号前面什么都没有,非严格模式下 this 是全局对象,严格模式下是 undefined。
bind 返回的函数内部记住了目标 this,每次调用都用它去执行原函数,所以不管以后怎么调用都不会变。箭头函数则是另一个思路:它根本没有自己的 this,用的是定义时外层的 this,所以 call、apply、bind 对它都无效。
容易答错的地方
- 以为
bind会立即执行函数 bind只返回新函数。button.onclick = handler.bind(obj)是对的,写成handler.bind(obj)()就等于handler.call(obj),当场就执行了。- 以为
bind过的函数还能再改this call、apply、再次bind都改不了。唯一的例外是用new调用,这时this指向新实例,绑定的this被忽略。- 第一个参数传
null就等于没传 - 非严格模式下,传
null或undefined时this会变成全局对象,传原始值会被包装成对象。严格模式下传什么就是什么,null就是null。
面试官还会怎么问?
手写一个 call 的思路是什么?
把函数临时挂到目标对象上,用「对象.方法()」的方式调用,这样 this 自然就是这个对象,调用完再删掉。为了避免和已有属性重名,临时属性名用 Symbol()。thisArg 为 null 时退回全局对象,原始值用 Object() 包装一下。
bind 返回的函数 length 和 name 是什么?
name 是 'bound ' 加原函数名,比如 'bound greet'。length 是原函数形参个数减去预设参数个数,最小为 0,上面的 greetTom.length 是 1。
类数组对象怎么用数组方法?
用 call 借用,比如 Array.prototype.map.call(nodeList, fn)。数组方法只要求 this 有 length 和数字索引,不要求真的是数组。现在更常用 Array.from(nodeList) 或展开语法先转成数组。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。