预备知识:this 指向,作用域
对于bind,apply和call方法,已经是前端面试官乐此不疲的考察内容了。虽然 ES6 对于this的处理已经改善很多,但是这些内容并不会很快被抛弃,不管是面试还是维护老代码,掌握它依然是必要的。
相同点-修改this
至于为什么这三个方法要放到一起对比,因为他们都有一个相同的重要作用:
- 改变函数的执行环境,也就是
this指向
通过代码简单示范:
function say() {
console.info("name", this.name);
}
var personA = {
name: "A",
say,
};
var personB = { name: "B" };
say();
personA.say();
personA.say.call(personB);
personA.say.apply(personB);
personA.say.bind(personB)();
//name undefined
//name A
//name B
//name B
//name B
上面的代码,say方法直接调用,其中的this是全局对象。只有正常调用personA的say方法的时候才会输出A。如果要输出B但是personB中并没有say方法,要怎么做?
可以通过三种方式,call,apply,bind 来完成。call和apply将函数内部的this改变指向,指向了对应函数的传入的第一个参数,也就是personB,这是的say方法中的this.name就是personB的name。故输出B。可能你也注意到,bind函数传入personB调用之后后面还有一个调用,因为bind方法改变this的时候并不执行该函数,而是返回一个新的函数。
call/apply 与 bind
如上,bind的返回和call/apply调用并不一样,**bind调用之后是返回一个新的函数的,该函数的this是已经修改过的,指向传递进bind函数的第一个参数,如果函数需要执行的话,需要自己再额外调用一次新的函数。而call,apply这两个方法在调用之后,函数是直接执行的。**这就是call/apply和bind方法最大的区别。
而对于call和apply方法,依然存在的相同点和不同点,call和apply方法的第一个参数都是调用call/apply后的函数的this指向。区别就是两者接收其他参数的方式,call方法是接受若干个参数列表,而apply方法接受的是一个数组,数组的元素是各个参数。
call 方法::
function.call(thisArg, arg1, arg2, ...)
thisArg并不是必须的,不传值或者值为undefined/null的时候函数的this指向全局对象。
apply 方法:
function.apply(thisArg, [argsArray])
apply 第一个参数则是必须的,第二个参数(数组)中的元素会作为单独的参数传给函数。
call和apply的返回值是改变this后的函数的返回值。
下面是一个call和appply的例子:
function say(age, gender) {
console.info("name/age/gender", this.name + "/" + age + "/" + gender);
}
var personA = {
name: "A",
say,
};
var personB = { name: "B" };
personA.say.call(personB, 20, "man");
personA.say.apply(personB, [24, "woman"]);
手动实现 bind/call/apply 方法
了解了call/aplly/bind方法的作用和特点,为了更好的理解,我们可以手动实现一遍
- call 方法,先看一个常规的
call方法使用例子:
const a = { name: "xw" };
function say(gender, age) {
const info = "name/gender/age:" + this.name + "/" + gender + "/" + age;
console.info(info);
return "返回值:" + info;
}
const data = say.call(a, "man", 20);
console.info(data);
//name/gender/age:xw/man/20
//data 返回值:name/gender/age:xw/man/20
say通过call调用,say自身可以接受两个参数,用于输出,并且具有返回值。可以像这样实现:
Function.prototype.MyCall = function (context) {
var env = context || window; //如果没有传入就指向全局
env.fun = this; // this就是调用Mycall的函数,将其挂在env上。运行时就是say函数
var args = [...arguments].slice(1); //获取第二个开始的参数
var result = env.fun(...args); // 执行函数并传参,相当于say(...args)
delete env.fn;
return result;
};
实现完之后调用一下:
const data = say.MyCall(a, "man", 20); //name/gender/age:xw/man/20
console.info("data", data); // name/gender/age:xw/man/20
执行输出和返回值和上面的call方法调用无异。
- apply 方法。
apply的实现和call很相似,只是参数的格式是数组。
Function.prototype.MyApply = function (context) {
var env = context || window;
env.fun = this;
var result;
if (arguments[1]) { //需要对第二个参数(数组)用到展开,加一层判断
result = env.fun(...arguments[1]);
} else {
result = env.fun();
}
delete env.fn;
return result;
};
//调用
const data = say.MyApply(a, ["man", 20]);
console.info("data", data);
结果和上面一样,且在不传其他参数的时候正常执行。
- bind 方法。区别于
apply和call会直接执行函数,bind则是会返回函数的引用,在传参上还有一个区别,就是**bind是支持柯里化形式传参的**。
柯里化: 将接受多个参数的函数转换成接受一个单一参数的函数,该函数返回一个接受剩余参数且返回结果的新函数。
也就是说,如果一个函数的调用传参是 funtionA(a,b),那么柯里化之后的函数 functionB,应该这样调用 functionB(a)(b),其效果不变
Function.prototype.MyBind = function (context) {
var inThis = this;
var args = [...arguments].slice(1);
return function fun() {
if (this instanceof fun) { //区分是否构造函数
return new inThis(...args, ...arguments); // 支持柯里化,两次获取参数
} else {
return inThis.apply(context, args.concat(...arguments));
}
};
};
然后进行调用测试,也是符合预期的:
const a = { name: "xw" };
function say() {
console.info("name is", this.name);
}
say.MyBind(a)();
// name is xw
总结
call,apply 和 bind 都用来改变函数的this指向,指向call/apply/bind函数的第一个参数,其中call/apply
调用时会直接执行该函数,而bind并不会直接运行函数,而是返回一个新的函数,需要手动调用才会执行。apply接受的其他参数是一个数组,而call函数是直接传多个参数的,一个参数列表。