一、理解
JavaScript 的一个特点是,函数存在定义时上下文、运行时上下文和上下文是可以改变的这样的概念。而 call(),apply() 和 bind() 就是用于改变函数运行时上下文的方法,换句话说,执行这三个方法就是为了改变函数体内部this的指向,将一个对象的方法交给另一个对象来执行。
为何要改变执行上下文? 举一个例子:
有两个对象 A 和 B,A 对象有一个方法,而 B 对象因为某种原因,需要用到同样的方法,这个时候我们是单独再为 B 对象扩展一个方法还是直接借用 A 对象的这个方法呢? 答案当然是借用 A 对象的比较好,既完成了需求,又减少了内存的占用。
1、call、apply
call、apply 的用法很类似,它们的共同点是调用之后都会立即执行。
var person = {
name: '张三',
say: function() {
console.log(this.name);
}
}
person.say(); // 张三
对象 person1 也想用 say 方法,就不需要重新定义,可以通过 call 或者 apply 实现:
var person1 = {
name: '李四',
}
person.say.call(person1); // 李四
person.say.apply(person1); // 李四
call 和 apply 的区别:
call 和 apply 的作用一样,区别在于接受参数的形式不一样。call 方法的参数是当前上下文的对象以及参数列表,而 apply 只接受两个参数,第一个参数和 call 一样是对象,而第二个参数是一个参数数组或类数组。
// 在非严格模式下,第一个参数为null或者undefined时会自动替换为指向全局对象
// call()接受参数列表
Math.max.call(null, 1, 2, 3, 4, 5);
// apply()接收参数数组
Math.max.apply(null, [1, 2, 3, 4, 5]);
2、bind
bind() 的作用和 call()、apply() 一样,都是可以改变函数运行时上下文,区别是 call() 和 apply() 在调用函数之后会立即执行,而 bind()方法调用并改变函数运行时上下文后,返回一个新的函数,供我们需要时再调用。
var person = {
name: '张三',
say: function() {
return this.name;
}
}
var person1 = {
name: '李四',
}
// bind() 返回一个新函数,以供之后调用
var say1 = person.say.bind(person1);
console.log(say1); // ƒ () { return this.name; }
console.log(say1()); // 李四
有一点需要注意,如果连续两次或多次 bind() 之后,它输出的值是什么呢?像这样:
var person2 = {
name: '小明',
}
var person3 = {
name: '小红',
}
var say2 = person.say.bind(person1).bind(person2);
var say3 = person.say.bind(person1).bind(person2).bind(person3);
答案是,两次都仍将输出 李四 ,而非期待中的 小明 和 小红 。原因是,在 Javascript 中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind() 是无法生效的。
二、使用
1、关于数组的妙用
获取数组中最大值或最小值:
Math.max.call(null, 1, 2, 3, 4, 5); // 5
Math.max.apply(null, [1, 2, 3, 4, 5]); // 5
Math.min.call(null, 1, 2, 3, 4, 5); // 1
Math.min.apply(null, [1, 2, 3, 4, 5]); // 1
合并两个数组:
var arr1 = [1, 2];
var arr2 = [3, 4];
Array.prototype.push.apply(arr1, arr2); // arr1: [1,2,3,4]
Array.prototype.push.call(arr1, ...arr2); // arr1: [1,2,3,4,3,4]
2、对象的继承
function Animal(name) {
this.name = name;
this.showName = function() {
console.log(this.name);
}
}
function Cat(name) {
Animal.call(this, name);
// Animal.apply(this, [name]);
}
var cat = new Cat("My name is Cat");
cat.showName(); // My name is Cat
Animal.call(this) 使 Animal 对象代替 this 对象,那么 Cat 中就有 Animal 的所有属性和方法了,之后 Cat 对象就可以直接调用 Animal 的方法以及属性了。
3、代理 console.log 方法
日常工作中经常要用到console.log方法,那么我们就可以定义一个log方法来代理它:
function log() {
// console.log.call(console, ...arguments);
console.log.apply(console, arguments);
}
log(1, 2, 3, 4); // 1 2 3 4
4、如何选用
如果不需要关心具体有多少参数被传入函数,选用 apply();
如果确定函数可接收多少个参数,并且想一目了然表达形参和实参的对应关系,选用 call();
如果我们想要将来再调用方法,不需立即得到函数返回结果,则可以使用 bind()。
三、小结
call()、apply() 和 bind() 都是用来改变函数执行时的上下文,可借助它们实现继承;
call() 和 apply() 唯一区别是参数不一样,call() 是 apply() 的语法糖;
bind() 是返回一个新函数,供以后调用,而 apply() 和 call() 是立即调用。
四、手写实现
1、模拟 call()
call 的目的是为了改变函数的执行上下文,然后立即执行函数。
var person = {
name: '小明',
}
function getName() {
console.log(this.name, ...arguments);
}
Function.prototype.myCall = function(context) {
context = context || window; // context 是当前调用函数的对象(person),为null或者undefined时取用window
context.fn = this; // this即为要调用的函数(getName())
let args = [...arguments].slice(1); // 获取传入的参数,从第二个开始
let result = context.fn(...args); // 执行添加的函数fn(getName())
delete context.fn; // 执行完删除这个方法,以免对调用对象(person)造成改变
return result;
}
getName.myCall(person, 30); // 小明 30
2、模拟 apply()
apply() 与 call() 类似,只不过它需要判断一下参数数组是否存在。
var person = {
name: '小明',
}
function getName() {
console.log(this.name, ...arguments);
}
Function.prototype.myApply = function(context) {
context = context || window; // context 是当前调用函数的对象(person),为null或者undefined时取用window
context.fn = this; // this即为要调用的函数(getName())
let args = arguments[1]; // 判断是否存在第二个参数,是否为参数数组
if (args && toString.call(args) !== '[object Array]') {
throw new TypeError('Erorr');
}
let result;
if (args) {
result = context.fn(...args)
} else {
result = context.fn();
}
delete context.fn; // 执行完删除这个方法,以免对调用对象(person)造成改变
return result;
}
getName.myApply(person, [30]);
3、模拟 bind()
这里需要注意下,因为 bind 转换后的函数可以作为构造函数使用,此时 this 应该指向构造出的实例,而 bind 函数绑定的第一个参数。
var person = {
name: '小明',
}
function getName() {
console.log(this.name, ...arguments);
}
Function.prototype.myBind = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window;
// 保存调用函数的引用,这里 self 是getName()
let self = this;
let args = [...arguments].slice(1);
return function F() {
// 判断是否被当做构造函数使用
if (this instanceof F) {
return self.apply(this, args.concat([...arguments]))
}
return self.apply(context, args.concat([...arguments]))
}
}
var getName1 = getName.myBind(person);
getName1(20); // 小明 20
在返回的新函数内部,self.apply(context, args.concat([...arguments]) 才是执行原来的 getName 函数,相当于执行 getName.apply(person)。