前言
听说手搓一个mycall函数模拟一下call函数的功能是几乎每一个前段人在面试时会被问到的问题,现在作者就来挑战一下如何手搓一个mycall函数
call函数的主要功能
要模拟一个call函数,我们要先了解一下call函数究竟做了什么。以下是es5版本官方对于
Function.prototype.call的定义
- wtf?这是人类写的?
- 看不懂了,放弃。
1. 修改this指向,并触发函数
调用call函数时,可以将调用者内部的this定向绑定到传入的目标对象上
- 如果传入对象为空,则this指向全局对象(浏览器环境下是window,Node环境下是global)
- 如果传入对象非空,则将this指向该对象
2. 帮助原函数接收参数
call函数还可以帮助原函数接收参数,并且返回函数执行的结果
3. 所有Function类型对象都可以调用
正因为call函数是Function.prototype的一个属性,且所有函数类型对象都是由Function创建的,依据原型链的原则,所以所有函数都可以访问call函数。
- call函数存在于Function对象显式原型上的证据
myCall函数的实现
1. 实现myCall函数被所有函数调用
call函数可以被所有函数调用是因为你被定义在Function的显式原型上,相同的我们得function函数上添加myCall函数
Function.prototype.myCall=function(){}
2. 实现修改this指向:隐式绑定原则
this隐式绑定原则
当函数被上下文对象引用,且被该对象调用时,函数中的this会绑定到上下文对象上。
call函数的关键功能就是修改原函数内部的this指向,而利用this的隐式绑定原则便可以实现 实现步骤:
- 目标对象为空,则目标对象为全局对象(window/global)
- 将myCall的调用者作为属性添加进传入目标对象obj
- 利用目标对象obj调用原函数,即可实现this改变至目标对象
- 当然了我们还需要删除这个属性,复原传入的obj对象
注意事项:利用symbol类型数据防止属性覆盖
如果context对象中原先存在名为fn的属性,则会发生覆盖现象。所以我们可以利用symbol定义一个独一无二的属性名进行属性添加,防止覆盖对象中的原有属性
Function.prototype.myCall=function(context){
context=context||window;
const fn=Symbol('fn');
context[fn]=this;
context[fn]();
delete context[fn]
};
3. 实现接收参数:rest参数
call函数调用过程中可以帮助原函数接收参数,但是这个参数数量是不固定的,call函数也无法知道它的调用者究竟要接收多少个形参。
rest参数
因为我们无法得知原函数存在多少个形参,而利用rest参数可以回避这个问题。 rest参数会将函数多接收的元素打包成数组接收,只要对数组进行解构就可以使得所有参数被正确的传给mycall函数内部,进而传给原函数 图解:
修改后的版本
利用rest参数,实现我们设计的myCall函数可以接收不定量的形参
Function.prototype.myCall=function(context,...args){
context=context||window;
const fn=Symbol('fn');
context[fn]=this;
context[fn](...args);
delete context[fn]
};
最终版本
也许原函数存在执行结果,所以不要忘记接收原函数执行的结果,通过我们设计的myCall函数返回
Function.prototype.myCall=function(context,...args){
context=context||window; // global
let fn=Symbol('fn') //创建一个独一无二的函数名,防止往context对象添加属性时覆盖已有数学
context[fn]=this;//将调用myCall的函数添加进context对象
const res=context[fn](...args) //通过context对象调用函数,使得函数中的this隐式绑定到context上,并接收最后的结果
delete context[fn] //删除前几步添加进context中的函数
return res //返回执行结果
};