本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
我先直接贴上代码,有些同学来就想看代码,俺已经拿捏了,直接给你们看:
/* call实现 */
Function.prototype.MyCall = function(that,...args) {
const _this = this;
const fn = Symbol('fn');
that[fn] = _this;
let result = that[fn](...args);
delete that[fn];
return result;
}
/* apply实现 */
Function.prototype.MyApply = function(that,args) {
const _this = this;
const fn = Symbol('fn');
that[fn] = _this;
let result = that[fn](...args);
delete that[fn];
return result;
}
/* bind实现 */
Function.prototype.MyBind = function(that) {
let _self = this;
let args = Array.prototype.slice.call(arguments,1);
let fnNop = function(){};
let fnBound = function() {
let _this = this instanceof _self ? this : that;
return _self.call(_this,args.concat(...arguments));
}
if(this.prototype) {
fnNop.prototype = this.prototype;
}
fnBound.prototype = new fnNop()
return fnBound;
}
那么接下来如果我们需要去手写call()、bind()、apply(),我们就需要知道他们到底做了什么,处于Function.prototype中的这三个函数可以显式的修改函数执行时的this指向。很明显这里的关键是如何修改this指向,我将一步步讲解手写这些函数所需要的技术细节。
this指向
标准函数的this指向执行函数时这个函数所处的上下文对象,箭头函数的this指向定义箭头函数的上下文对象。这里我们只讨论标准函数的this指向。
我相信大多数人刚开始和我都一样,看到上下文对象这几个字就脑壳疼,啥是上下文呀,然后就开始翻一翻js高程,一看还真有执行上下文的介绍,然后开始看书,然后看书上一大堆文字更晕了,啪的一下书关的很快啊。
其实上下文对象就是一个对象(Object),这个Object里有好多的属性,函数在执行的时候,就在这个Object里找,若函数中执行this.xxx,就表示在这个Object中查找xxx这个值。
如何实现call
我们现在来理一下实现call的逻辑。
call是在函数调用中的函数,第一个参数为需要修改为的this指向,可以理解为我要把这个函数放在这个this中调用,脑海中浮现出我上面画的图,this可以看做obj,也就是说可以看作将这个函数放在这个obj中调用。第二个参数是需要向函数中传入的参数。由此我们可以写出基本框架,如下:
Function.prototype.Mycall = function(that,...args) {
}
这里我解释一下为什么要在Function.prototype中定义方法。
函数是由Function构造函数构造的一个对象,本质上也是一个对象,所以当在这个对象中没有call方法时,就会在proto中查找方法,proto指向构造函数的prototype。
我们都知道构造函数产生实例对象是通过new实现的,那么对我们来说了解new的过程中发生了什么能让我们更加深入的理解上述问题。
new的过程由4步组成:
- 创建一个新对象。
- 将构造函数的prototype赋值给新对象的proto。
- 在新对象的上下文中执行构造函数,初始化属性和值。
- 返回这个新对象。
结合原型链与new的知识,所以我们可以知道每个由Function构造的函数都可以使用到Function.prototype中的方法.
Function.prototype.MyCall = function(that,...args) {
const _this = this;
......
}
call显式的修改this指向,通俗的来说就是需要将调用call方法的函数放在指定的上下文对象中执行,所以我们如何能在call方法中获取到调用call方法的函数本身呢?
我们来看一下上面这段代码中的const _this = this,在实际的使用中,会有一个函数调用call方法,再回顾一下上面我说的一句话标准函数的this指向执行函数时这个函数所处的上下文对象,现在这个call函数就是我们要执行的函数,而所处的上下文对象就是执行call函数的函数(本质上也是一个对象)。所以this就是我们获取到的调用call方法的函数本身。
我们拿到了需要改变this指向的函数,知道了在什么对象中执行,所以我们就能手写实现call。
/* call实现 */
Function.prototype.MyCall = function(that,...args) {
const _this = this; //获取需要改变this的函数自身,也就是调用call的这个函数
const fn = Symbol('fn'); //定义一个唯一值,防止与对象中的键名冲突。
that[fn] = _this; // 在这个对象中定义键值对,键为fn,值为函数。
let result = that[fn](...args); // 在that上下文中调用函数,也就修改了this的指向。
delete that[fn]; // 删除属性,消除引用,防止内存泄漏。
return result; //返回函数调用返回值
}
如何实现apply
和实现call大同小异,就是变成了参数数组。所以参数只有2个,第二个参数为数组。后面用拓展运算符展开传参。
/* apply实现 */
Function.prototype.MyApply = function(that,args) {
const _this = this;
const fn = Symbol('fn');
that[fn] = _this;
let result = that[fn](...args);
delete that[fn];
return result;
}
如何实现bind
bind就是返回一个函数嘛,_self获取的就是函数自身上面已经阐述过了,这里Array.prototype.slice.call(arguments,1),就是借用了Array的slice把除了that的后面的参数都切割出来。然后新建一个函数返回。
/* bind实现 */
Function.prototype.MyBind = function(that) {
let _self = this;
let args = Array.prototype.slice.call(arguments,1);
let fnBound = function() {
return _self.call(that,args.concat(...arguments));
}
return fnBound;
}
事实真有这么简单吗?哈哈哈当然没有。
我们要知道一个函数是可以通过new来实例化一个对象的。 当我们如下操作时:
let fn = 函数.MyBind(obj);
let res = new fn(); // 出大问题
再让我们回想一下之前new的4个步骤:
- 创建一个新对象。
- 将构造函数的prototype赋值给新对象的proto。
- 在新对象的上下文中执行构造函数,初始化属性和值。
- 返回这个新对象。
所以关键是我们要区分bind返回的函数最后是正常的调用,还是通过new去实例化了一个对象。 这决定了调用bind的这个函数最后在什么上下文中执行。
看一下实现bind的代码中的这一段。
/* bind实现 */
Function.prototype.MyBind = function(that) {
......
let fnBound = function() {
let _this = this instanceof _self ? this : that;
return _self.call(_this,args.concat(...arguments));
}
......
return fnBound;
}
this instanceof _self ? this : that 这句代码为什么可以用作区分呢?
第一种情况:当函数正常的调用时,函数的this指向执行函数的上下文对象。很明显this instanceof _self === false,所以会执行_self.call(that),也就是会在他原先约定好的that上下文中调用函数。
第二种情况:当函数通过new去实例化了一个对象,看到new的第3步,构造函数的this指向这个新对象。那么关键是为什么this instanceof _self === true,我们看到new的第二步是将构造函数的prototype赋值给新对象的proto,所以如何能使新对象在原型链中沿着proto查找到_self,是我们要在代码中实现的。结合代码解析:
/* bind实现 */
Function.prototype.MyBind = function(that) {
let _self = this;
let args = Array.prototype.slice.call(arguments,1);
+ let fnNop = function(){}; // 创建一个函数
let fnBound = function() {
+ let _this = this instanceof _self ? this : that; // 关键判断
return _self.call(_this,args.concat(...arguments));
}
+ if(this.prototype) {
+ fnNop.prototype = this.prototype;
+ }
+ fnBound.prototype = new fnNop()
// 思考new定义可以得出,此处将fnNop.prototype => funBound.prototype.__proto__
// 所以 fnBound.prototype.__proto__ => fnNop.prototype => _self.prototype
return fnBound;
}
所以当函数通过new去实例化了一个对象时,第2步:新对象.__proto__ => fnBound.prototype,第3步:当构造函数的this指向这个新对象时,就满足了this instanceof _self === true,所以调用bind的这个函数就会在新对象的上下文中执行!
我是梨木,一个前端初学者,希望能在学习前端的过程中,留下自己的思考,给予你们帮助,以上文章若有错误,感谢指出!