开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情
前言
网上有各种优秀的手写call/apply/bind的文章,但是为什么我还要自己写一篇呢?
原因是我自己在学习这三个方法的时候,确实也看了非常多的文章,可能是自己基础也不太好,都是一知半解,当自己理解了之后,也想用自己的理解去教会大家(已经会的同学可以忽略)
前置知识:
- this指向
- 原型和原型链
- 数组方法 slice(), concat()
如果理解这些前置知识,会有助于你的理解和学习
面向需求编程
在我们开始手写之前,我们先梳理一下关于这三个函数的相关知识,以及同异之处。
call
语法:function.call(thisArg, arg1, arg2, ...)
参数:
thisArg: function函数运行时使用的this值,也就是把function函数的this指向改变为thisArg
arg1,arg2,...: 参数列表
返回值: 在调用的时候会立即执行,并且使用提供的this,和参数去执行,如果没有返回值就返回undefined
apply
语法:function.apply(thisArg, [ arg1, arg2, ...])
参数:
thisArg: function函数运行时使用的this值,也就是把function函数的this指向改变为thisArg
[arg1,arg2,...]: 参数列表,需要是一个数组
返回值: 在调用的时候会立即执行,并且使用提供的this,和参数去执行,如果没有返回值就返回undefined
虽然
call()的语法与apply()几乎相同,但根本区别在于,call()接受一个参数列表,而apply()接受一个参数的单数组。
bind
语法:function.bind(thisArg, arg1, arg2, ...)
参数:
thisArg: function函数运行时使用的this值,也就是把function函数的this指向改变为thisArg
arg1,arg2,...: 参数列表
返回值: 在调用的时候不会立即执行,而是返回一个函数,这个函数的this是thisArg,参数是arg1, arg2, ...
在了解了这三个函数的基本语法和用法之后,我们可以开始着手去实现
call()
先写出大概框架
在原型链上挂上我们自己实现的call()函数 myCall()
Function.prototype.myCall(){
}
获取需要改变执行上下文
我们可以通过获取传入的第一个参数来获取,也就是context,这里简写为ctx
但是,会有几种情况:
- 是否传入了需要改变的执行上下文ctx
如果没有传入需要改变的执行上下文,那我们就将它指向window
- 传入的不是一个Object
如果传入的不是一个Object形式,我们就用Object()方法转成Object
开始使用代码实现:
Function.prototype.myCall( ctx ){
ctx = ctx ? Object(ctx) : window;
}
获取参数
获取除了第一个参数之后的参数,我们使用slice()和方法来实现
了解一下slice():slice()方法主要是用来对数组进行截取,或者删除特定项,不会改变原数组,而是返回一个截取之后的新数组
可以参考MDN的介绍:slice()
Function.prototype.myCall( ctx ){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1)
}
将调用的函数设置为对象的方法
谁调用了myCall,这里的this就指向谁
Function.prototype.myCall( ctx ){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1);
ctx.myFn = this;
}
传入参数,调用函数
因为call和apply都是调用就立马执行的,所以,在内部我们需要执行,并且把返回值使用一个变量接住,这里我们用一个res接住
Function.prototype.myCall( ctx ){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1);
ctx.myFn = this;
let res = ctx.myFn(...args);
}
收尾阶段
在我们需求都满足的情况下,就顺利的进入到了收尾阶段了
因为我们在开始的时候,在ctx身上新增了一个对象myFn,在调用完之后,需要删除掉
并且要把执行的返回结果 return 出去
Function.prototype.myCall( ctx ){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1);
ctx.myFn = this;
let res = ctx.myFn(...args);
delete ctx.myFn;
return res;
}
到这里,call()已经实现了,我们来试试看
请一定一定要理解了call()的实现之后再进行接下来apply()和bind()的学习!
apply()
上面有介绍到,call()和apply()几乎是一模一样,但是参数的形式不一样,apply()中的参数需要是数组的形式。
因此大家务必理解了上面call()的实现之后再接下去学习
废话不多说
因为call()和apply()的相似,所以我们在call()的基础上进行修改即可
首先把一样的步骤写下来,在参数截取接收的方法上做出改变
相同的部分如下,有关参数的部分先不写
Function.prototype.myApply (ctx) {
ctx = ctx ? Object(ctx) : window;
ctx.myFn = this
let res = null;
delete ctx.myFn;
return res
}
该如何接收参数?
首先我们来思考该怎么接收参数,并且传入我们要执行的myFn中
参数传入的格式是 [ arg1, arg2, ...],是个数组
传入的arguments是这样的 [{a:1,b:2},["张三", "李四"]]
{a:1,b:2}是我们要改变的this指向,["张三", "李四"]是我们传入的参数,因此我们可以使用arguments[1]取出参数["张三", "李四"]
先不急着敲代码
思考一下,如果没有传入参数,那么arguments[1]就是undefined,所以我们需要先进行一个判断
看代码
Function.prototype.myApply (ctx) {
ctx = ctx ? Object(ctx) : window;
ctx.myFn = this
let res = null;
if (arguments[1]) {
res = ctx.myFn(...arguments[1])
}else {
res = ctx.myFn()
}
delete ctx.myFn;
return res
}
还是一样,我们执行一下
如果能理解call(),那么apply()理解起来也不会难
bind()
bind()相对于上面两个函数,bind的不同在于,它不是立即执行的,而是返回一个函数
因为bind()的这种特性,bind()能够作为构造函数去new一个新的对象,这也是我们需要考虑的
还是一样,我们一步一步来
大概框架
写出大概框架,判断是否传入ctx,就是要改变的this的指向,没有的话直接指向window(反复强调,反复理解)
Function.prototype.myBind(ctx){
ctx = ctx ? Object(ctx) : window;
}
接收参数
和call()一样,没什么好说的
Function.prototype.myBind(ctx){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1)
}
设置一个返回函数
因为bind()不是立即执行的,所以需要返回一个函数
Function.prototype.myBind(ctx){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1)
let Fn = function () {
}
return Fn
}
返回的函数内容
-
返回出去的函数,this的指向是被改变了的,指向的是myBind中的ctx,我们这里使用apply()来改变指向
-
并且在返回出去的函数中需要执行函数,这个执行的函数是第一次调用myBind()的函数,为了方便调用,我们也使用一个对象进行接收
-
要执行的函数中参数有两部分,一部分是在一开始myBind()函数的时候传进去的,另一部分是在,函数执行的时候传进去的,所以我们需要合并在一起
Function.prototype.myBind(ctx){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1);
let fn = this;
let myFn = function () {
return fn.apply(ctx, args.concat(...arguments))
}
return Fn
}
值得注意的是,当它作为构造函数去使用的时候,this的指向也需要改变
那么怎么判断是不是它的构造函数构造出来的呢?
着重理解:
我们可以使用instanceof去原型链上查找,看是不是myFn,如果是,那就是构造函数构造出来的,就让this指向它的构造函数myFn,在函数中就是this。不是的话,那就不是构造函数,就指向ctx就可以了。
Function.prototype.myBind(ctx){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1);
let fn = this;
let myFn = function () {
return fn.apply(this instanceof myFn ? this : ctx, args.concat(...arguments))
}
return myFn
}
连接原型链
构造函数构造出来的实例对象,之间是有原型链关系的
但是在我们上面的操作完成后,myFn和我们调用的函数之间没有联系,因此我需要把他们两个连接起来
可以这样
Function.prototype.myBind(ctx){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1);
let fn = this;
let myFn = function () {
return fn.apply(this instanceof myFn ? this : ctx, args.concat(...arguments))
}
myFn.prototype = this.prototype; // *****看这******
return myFn
}
但是,非常不建议这样修改,如果两个函数对象共用了一个原型的话,其中一个函数进行原型的修改,将会影响到大家,所以非常不建议这样做
所以,我们可以通过一个中间函数进行连接
Function.prototype.myBind(ctx){
ctx = ctx ? Object(ctx) : window;
let args = [...arguments].slice(1);
let fn = this;
let _tempFn = function(){}; // ******看这******
let myFn = function () {
return fn.apply(this instanceof myFn ? this : ctx, args.concat(...arguments))
}
_tempFn.prototype = this.prototype; // *****看这******
myFn.prototype = new _tempFn(); // *****看这******
return myFn
}
到这里,就完成了bind函数的实现
运行一下看看
最后
希望你看完之后也能够学会手写这三个函数
最后的最后,希望大家不要害怕手撕代码,可能第一时间看到手写代码这几个字会害怕,想逃避。
但是希望大家记住一句话,共勉之
消除恐惧最好的办法,就是面对恐惧。