最近正值秋招,面试时难免会问到this指向的问题,于是就有了下面这样的对话~
面试官:你说下this吧
我:开始吟唱,疯狂背八股~
面试官:ok,那你知道call、apply和bind的作用以及他们之间的区别吗?
我:常规八股,继续吟唱~
面试官:ok,那你来手写一个call和bind吧
我:啊?
写半天没写出来。。。
面试官:好的,我们今天的面试就到这里,谢谢你的时间
写在开头
本文不讨论this指向的各种问题,相信大家也了解的滚瓜烂熟了,本文旨在带大家熟悉call、apply和bind的区别、用法以及手写出来。
call、apply和bind区别
众所周知,通过 call、apply、bind 我们可以修改函数绑定的 this,使其成为我们指定的对象。通过这些方法的第一个参数我们可以显式地绑定 this。
function foo(name, price) {
this.name = name
this.price = price
}
function Food(category, name, price) {
foo.call(this, name, price) // call 方式调用
// foo.apply(this, [name, price]) // apply 方式调用
this.category = category
}
new Food('食品', '汉堡', '5块钱')
// 浏览器中输出: {name: "汉堡", price: "5块钱", category: "食品"}
call 和 apply 的区别是 call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组。
func.call(thisArg, arg1, arg2, ...) // call 用法
func.apply(thisArg, [arg1, arg2, ...]) // apply 用法
而 bind 方法是设置 this 为给定的值,并返回一个新的函数,且在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
func.bind(thisArg[, arg1[, arg2[, ...]]]) // bind 用法
ES6 方式用了一些 ES6 的知识比如 rest 参数、数组解构
注意: 如果你把 null 或 undefined 作为 this 的绑定对象传入 call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
var a = 'hello'
function foo() {
console.log(this.a)
}
foo.call(null) // 浏览器中输出: "hello"
总的来说:他们两个区别
- 第一个是传参方式不同:
call和bind是列表传参,apply 是数组或伪数组传参 - 第二个是执行机制不同:
call和apply是立即执行,bind不会立即执行而是生成一个修改 this 之后的新函数
手写call、apply
Function.prototype._call = function (context = globalThis, ...args) {
if (typeof context !== "object") context = new Object(context);
let fnKey = Symbol();
context[fnKey] = this;
let res = context[fnKey](...args);
delete context[fnKey];
return res;
};
代码逐行解释:
Function.prototype._call = function (context = globalThis, ...args)
此处将
_call方法添加到Function的原型对象上,使所有函数对象都可以使用该方法。_call方法接受两个参数:context(默认为全局对象globalThis)表示要绑定给函数的上下文对象,...args表示传递给函数的参数列表。
if (typeof context !== "object") context = new Object(context);
在调用函数之前,首先判断
context的类型是否为对象。如果不是对象,将使用Object构造函数创建一个新的对象作为上下文对象。
let fnKey = Symbol(); // 相当于将函数绑定到上下文对象并改变函数执行时的 this 指向。 context[fnKey] = this;
创建一个唯一的符号键
fnKey,并将当前函数对象(调用_call方法的函数)绑定到上下文对象的fnKey键上。这相当于将函数绑定到上下文对象并改变函数执行时的this指向。
let res = context[fnKey](...args);
通过调用上下文对象的
fnKey属性(即绑定的函数),并传递参数列表args,执行函数。
delete context[fnKey];
在函数执行后,删除上下文对象的
fnKey属性,以便清除对函数的引用。
同样的,apply与call基本一致
// 这里传参和call传参不一样
Function.prototype._apply = function (context = globalThis, args) {
if (typeof context !== "object") context = new Object(context);
// args 传递过来的参数
// this 表示调用call的函数
// context 是apply传入的this
// 在context上加一个唯一值,不会出现属性名称的覆盖
let fnKey = Symbol();
// this 就是当前的函数
context[fnKey] = this;
// 绑定了this
let res = context[fnKey](...args);
delete context[fnKey];
return res;
};
这里我就不做解释了
手写bind
Function.prototype._bind = function (context = globalThis, ...args) {
let self = this;
let fBound = function (...innerArgs) {
return self.apply(
this instanceof fBound ? this : context,
args.concat(innerArgs)
);
};
fBound.prototype = Object.create(this.prototype);
return fBound;
};
代码逐行解释:
Function.prototype._bind = function (context = globalThis, ...args) {
此处将 _bind 方法添加到 Function 的原型对象上,使所有函数对象都可以使用该方法。_bind 方法接受两个参数:context(默认为全局对象 globalThis)表示要绑定给函数的上下文对象,...args 表示预设的参数列表。
let self = this;
在进行绑定操作之前,保存原始函数的引用。
let fBound = function (...innerArgs) {
return self.apply(
this instanceof fBound ? this : context,
args.concat(innerArgs)
);
};
创建一个新的函数 fBound,当调用这个新函数时,会执行原始函数,并将原始函数的 this 值绑定到指定的上下文 context。在新函数内部,通过 self.apply() 调用原始函数,将新函数的执行上下文当做 this 值传递给原始函数。this instanceof fBound 检查当前函数是否通过 new 关键字调用,如果是,则将当前的 this 值作为执行上下文;否则,使用指定的上下文对象 context。
fBound.prototype = Object.create(this.prototype);
将新函数 fBound 的原型对象设置为原始函数的原型对象,以确保通过 new 关键字创建的实例能够继承原始函数的原型链上的属性和方法。
return fBound;
返回绑定了指定上下文的新函数 fBound。
后话
在现在的环境下,校招面试只会越来越难,单纯去背八股文已经很难适应面试官的要求了。同时,大厂和一些中厂对于面试时手写也都有比较高的要求,手写题没写出来,往往那场面试就会挂。所以我们一定要掌握一些重点手写题来应对。