先回顾一下apply、call、bind三个方法的用法:
- 在一个函数上调用apply、call、bind方法、函数内部的this指向会发生改变,this指向传入apply、call、bind方法的第一个参数;
- apply、call方法的区别是传入的参数不同,apply方法的第二个参数是一个数组(或类数组),而call方法需要传入单个参数,接收的这些参数传给原函数,返回原函数执行之后的结果;
- bind方法返回一个新的函数,bind方法接收的第二个及之后的参数也是传给原函数,调用返回的这个新函数时原函数才会执行
// 原函数
function func(a,b,c){
console.log(this,a,b,c)
}
let obj={
title:'obj'
}
// apply方法第二个参数时一个数组
func.apply(obj,['a','b','c']); // {title: 'obj'},'a','b','c'
// call方法第二个及之后的参数需要单个传入
func.call(obj,'a','b','c'); // {title: 'obj'},'a','b','c'
// bind方法返回一个新的函数,第二个及之后的参数成为了内置参数
let fn=func.bind(obj,'a');
fn(); // {title: 'obj'},'a',undefined,undefined
fn('a','b') // {title: 'obj'},'a','a','b'
fn('a','b','c') // {title: 'obj'},'a','a','b'
重写apply、call、bind方法最重要的是了解JavaScript中this指向问题。在JavaScript中、函数内部this的指向是在函数执行的时候确定的,如果函数作为一个对象的方法调用,那么这时函数内部的this是指向的就是这个对象,如:
let obj={
title:'obj',
fn:function(){
console.log(this.title)
}
}
// 函数作为对象的方法调用,内部的this指向该对象
obj.fn() // 'obj'
// 函数作为普通函数调用,内部的this指向Window对象
let func=obj.fn;
func() // undefined
利用以上这个特性,就可以实现apply、call、bind方法。
apply
在函数func上调用apply方法,依次传入参数context、args,即func.apply(context,args),要改变func内部this的指向,只需要将func作为context对象的方法调用即context.func(),这样执行之后函数内部的this就指向了context对象:
// apply方法接收两个参数
// 第一个参数是this将要指向的对象
// 第二个参数是一个数组
Function.prototype.apply=function(context,args){
// self保存调用apply方法的原函数
const self=this;
// 让原函数self内部this指向context,则需要将self作为context对象的方法调用
// 先将原函数self赋值给context对象的一个属性
let oldFn;
if(context.tmpFn){
oldFn=context.tmpFn
}
context.tmpFn=self;
// 再作为context对象的方法调用
const result=context.tmpFn(...args);
context.tmpFn=oldFn;
// 返回原函数执行的结果
return result;
}
但是使用apply方法还有一些细节,比如第一个参数可以传入null,传null的时候在非严格模式下原函数内部的this指向window,也可以传一个基本类型如字符串,这是this指向的是基本类型的包装类型,第二个参数是可选的,原函数作为对象的方法挂载到对象上、给对象增加了额外的属性,所以可以进行一些优化:
function getGlobalObject(){
return this;
}
// apply方法接收两个参数
// 第一个参数是this将要指向的对象
// 第二个参数是一个数组
Function.prototype.apply=function(context,args){
// self保存调用apply方法的原函数
const self=this;
// 调用 apply方法的必须是一个函数
if(typeof self !=='function'){
throw new TypeError(`${self} is not a function`);
}
// 没有传入context指向全局环境对象
context=context || getGlobalObject();
// 传入的context是基本类型需要对其进行包装
if(context && typeof context !=='object'){
context=new Object(context)
}
// 让原函数self内部this指向context,则需要将self作为context对象的方法调用
// 先将原函数self赋值给context对象的一个属性
let oldFn;
if(context.tmpFn){
oldFn=context.tmpFn
}
context.tmpFn=self;
// 再作为context对象的方法调用
// 第二个参数可以不传
args=args || [];
const result=context.tmpFn(...args);
if(oldFn){
context.tmpFn=oldFn;
} else {
delete context.tmpFn
}
// 返回原函数执行的结果
return result;
}
apply方法除了改变原函数内部this指向之外还隐含一个作用,就是将数组类型的参数拆解成单个参数传递给原函数,在实际开发中经常用到,如Math.max方法本身只能接收单个参数,而利用apply则可以接收一个数组类型的参数,如:
let arr=[3,6,7,9];
console.log(Math.max.apply(null,arr)) // 9
call
call方法与apply方法的区别就是传入参数的格式不同,call方法第二个及之后的参数是单个参数,可以直接利用剩余参数表示,这样其内部实现完全跟上面apply方法相同,如:
Function.prototype.call=function(context,...args){
// 同apply方法
// ............
// 略
}
call方法内部也可以利用arguments对象来解析传入的第二个及之后的参数,但基本逻辑依然同apply方法类似,另外在已有apply方法的基础上,也直接在call方法内部调用apply以最简单的方式实现call方法,如:
Function.prototype.call=function(context,...args){
const self=this;
return self.apply(context,args)
}
bind
bind方法与apply、call方法的不同在于返回的是一个新函数,调用bind方法传入的第二个及之后的参数会作为原函数的默认参数,有了前面的apply方法,实现也比较简单:
Function.prototype.bind=function(context,...args){
const self=this;
return function(){
// 这里的arguments是返回的这个函数调用时传入的参数
return self.apply(context,[...args,...arguments])
}
}