前言
本人认为call函数是我前端学习路上的一大难点,在我接触的项目中用的很少,但是还是想把它研究清楚。接下来将根据它能做的事情一步一步将其实现。废话不说,直接进入正题。
call能干什么?
使用一个指定的this值、传入多个参数来调用这个函数函数。
这是最常用的方法 fun.call(obj,args)
call使用示例
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
const res = fn.call({x:10},20,30)
console.log('res',res);
输出如下:
//a 20
//b 30
//this {x: 10}
//res hello
//here
可以看到,在此用例中,call函数接受了三个参数。第一个是一个对象,第二个与第三个是一个number类型数据。最终输出显示,this已经变为传入的对象{x:10},并将后面的20,30作为参数打印出来。
实现
1、定义mycall
首先我们定义个mycall函数。选择Object.prototype.mycall与Function.prototype.mycall都可以。这里涉及到一个原型链的知识点,这里稍微提及一下。
比如我们这里使用了fn.mycall,它首先会在fn这个对象上找,fn这个对象肯定没有撒,那就顺着fn.__proto__去找。fn.__proto__也是个对象,这个对象具体是什么呢?
fn.__proto__这个对象就是Function.prototype这个对象。来验证一下
//接着上面的代码
console.log(fn.__proto__ == Function.prototype);//true
因此,我们如果将mycall函数写入Function.prototype,则是可以找到滴。
如果写到Object.prototype.mycall呢?那就继续找呗。怎么找?沿着__proto__找啊,也就是:fn.__proto__.__proto__。而fn.__proto__.__proto__这个东西又是什么呢?大家只需记住,就是Object.prototype。继续验证一番
//继续接着上面的代码
console.log(fn.__proto__.__proto__ == Object.prototype);//true
因此,将mycall写入Object.prototype也是可以滴。
扯远了扯远了,回到正题。
最终代码来了:
Object.prototype.mycall = function(){
...
}
好吧,属实废话了很多,继续下一步。
2、解构参数
经过上一步骤,我们mycall函数的框架已经搭建完毕,接下来就是要传入参数,并且执行了吧。
问题来了?我们要如何描述传入的参数呢?总不能直接指定a,b,c吧
Object.prototype.mycall = function(a,b,c){
...
}
万一多传几个值不就接收不到了,pass。
我们这里用到...args展开用算符
Object.prototype.mycall = function(...args){
...
}
当然这个不是什么难点,我提它只是想着重说明我个人写mycall的思路。
观察到,mycall函数第一个参数是一个对象,我们的目的是要将this指向这个对象。之后的参数都是mycall函数中使用到的。所以,我们可以用slice函数将参数分割开。
Object.prototype.mycall = function(...args){
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
//{x: 10} [20,30]
}
3、执行
上一步骤已经将参数成功提取出来,接下来就是要执行了。but,如何执行fn函数呢?
以下就是目前mycall的全部代码。
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
}
fn.mycall({x:10},20,30)
难道这样执行?
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
`fn()`
}
fn.mycall({x:10},20,30)
以上的执行方式是不行的,因为这个mycall是个公共函数,如果函数名不叫fn,直接无法使用。
注意,我们可以使用 this
大家都知道,普通函数(非箭头函数),this的指向就是函数的调用者。也就是说,mycall这个函数是由fn函数调用的。因此,在mycall函数内部,this指的就是fn函数,来验证一下
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
console.log('this',this);
}
fn.mycall({x:10},20,30)
执行结果如下:
所以呢,我们可以通过借用this来执行fn函数。
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
this(obj,...ar)
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);
执行结果如下:
4、解决this指向
由上一步骤可以看到,参数已成功传入,输出了a与b,但是this指向的是window,因为fn是在全局下执行的。但是我们想要的this是传入参数{x:10}这个this。怎么办呢?
首先我们肯定对下面这段代码很熟悉吧
let obj = {
name:'test',
say: function(){
console.log("this",this,this.name);
}
}
obj.say()
相信大家一眼就能知道输出。
这里的this指向的是obj这个对象,而不是window(总之一句话,非箭头函数,this指向调用它的人)。say函数被obj对象调用,因此this指向obj。
利用这个特性,我们可以以如下方式来执行上一步中的this函数
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
obj.say = this
obj.say(obj, ...ar)
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);
执行结果如下:
nice,可以看到this已经指向了我们传入的对象obj
也许我们会有这样的想法,这样写行不行呢?
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
obj.say = function(){
console.log("this", this);
this(obj, ...ar)
}
obj.say()
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);
这样首先会报this is not a function错误,为什么呢?
因为obj.say里的this已经不再是fn了,而变成obj这个对象了,obj既然是个对象,哪来的名为this的function,压根没定义。
好,顺着往下走,有同学会想,既然这个this不是fn,那么我就不用this(obj, ...ar)了,我把fn存起来,然后执行不就完了吗?所以我就这么写
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const self = this
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
obj.say = function(){
self(obj, ...ar)
}
obj.say()
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);
这样就行了吗?很遗憾,也是不可以的,因为this指向还是不对的。看一下运行结果,this又指向了window
为什么会这样呢?,是因为self不是被obj这个对象调用,而是被全局调用
可以类比这个例子
let obj = {
name:'test',
say: function(){
fn()
}
}
function fn(){
console.log("this",this);
}
obj.say()
执行结果如下
这个涉及到了this的指向问题,在本文不是重点,我这里就不做过多的阐述。总而言之,是因为fn的调用者是全局对象window,而不是obj。
5、确定返回值
以上4步就差不多完成自定义call函数了,还有个小尾巴,就是确定返回值。这个比较简单,没有很难的。我们只需要确定什么函数该返回什么值即可。
- 首先
self函数(就是fn函数)返回的是"hello"。 - 用
res接收这个返回值,并作为mycall的返回值。
function fn(a,b){
console.log('a',a);
console.log('b',b);
console.log('this',this,this.x);
return 'hello'
}
Object.prototype.mycall = function(...args){
const self = this
const obj = args.slice(0,1)[0]
const ar = args.slice(1)
obj.say = function(){
return self(obj, ...ar)
}
let res = obj.say()
delete obj.say //删除掉obj.say,因为它只是暂时的。
return res
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);
执行结果:
总结
以上就是自定义call函数的实现,里面涉及了原型链、this指向问题,这些知识点都是前端必须要掌握的。mycall函数是按照我的思路方式一步一步来写的,可能并不是完美的,但是自我感觉,还是最容易懂得。如果有什么问题,欢迎小伙伴们指正哦。
如果有同学还不是特别懂,可以观看一下b站上这个自定义call函数的实现,我就是受其启发。
参考视频