本篇文章带你从零开始 由浅入深实现 call apply bind,从优先级分析和相互调用关系来解析其原理
前置知识: 了解this的绑定规则:箭头函数的this指向,new,显式绑定(call apply bind),隐式绑定,默认绑定。
这里简单介绍一下:
- 默认绑定:当函数直接调用时,this在非严格模式下,会默认绑定到window(浏览器环境)或global(nodeJS环境),
- 隐式绑定:当函数通过
.运算符调用,如obj.foo(),那么函数foo的this会依据就近原则指向最近的obj- 显示绑定:函数可以通过bind、apply、call显示绑定this;函数通过new创建的对象的this会绑定到对象本身
- 箭头函数:箭头函数中的this指向其父级作用域的this
不同的绑定方式也存在优先级,优先级高的无法被优先级低的进行修改,优先级排序如下:箭头函数 > new > bind > call&apply > 隐式绑定 > 默认绑定
call & apply
MDN概念:
人话:call 和 apply的作用一致都是修改this,不过传递的参数存在区别。
- call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法
- apply() 方法在使用一个指定的 this 值和若干个指定的参数值组成的数组的前提下调用某个函数或方法
故下面我们以call的实现进行主要讲解
举个例子
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
从例子中我们可以看出 call实际上做了两件事情:
- 改变this指向 指向foo
- 执行bar方法
这也正是我们实现call的关键点所在,实现的核心思想是:利用隐式绑定来实现显示绑定。同学可以先根据这一点先自己思考一下如何利用隐式绑定来实现this的更改效果,再看下面的实现代码。
实现代码
// 利用隐式绑定实现显式绑定
Function.prototype.myCall = function (that, ...rest) {
that.fn = this // this 指向函数;that指向要绑定的对象;将函数添加到对象上再用对象来调用函数,实现隐式将this绑定到对象上
that.fn(...rest)
}
代码解释:
- 首先要模拟原生call的使用方式 我们需要修改函数的原型 添加自己的myCall方法,这样即可通过bar.myCall()方式进行调用
- myCall接受的第一个参数是目标对象,并且支持传入调用的函数的参数,我们这里使用剩余参数的格式实现
- 其核心代码用人话来说就是在目标对象上添加自己的函数,再在目标对象上进行调用,这种调用就是隐式绑定,可以将函数的this绑定到目标对象上。对应到代码中 that就是目标对象,this指向被调用的函数(这也是隐式绑定,可以好好想想为什么),因此在that上添加this 并且调用this,就可以实现将函数的this指向目标对象的目的了
存在问题
- 修改了目标对象,添加了fn属性,这是不合理的副作用
- 缺少边界情况的处理,用户传入undefind和null怎么办
改进代码
// 利用隐式绑定实现显式绑定
Function.prototype.myCall = function (that, ...rest) {
that = that || globalThis // 边界情况处理 处理用户传入undefined或者null的情况 默认绑定到全局this。注意在nodeJS中是globalThis 在浏览器环境中是window
that.fn = this // this 指向函数;that指向要绑定的对象;将函数添加到对象上再用对象来调用函数,实现隐式将this绑定到对象上
that.fn(...rest)
delete that.fn
}
至此call的已基本实现,而apply就交给小伙伴自己去试试了~
bind
MDN概念:
Function实例的bind()方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其this关键字设置为给定的值,同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。
叽里咕噜一大堆总结成一句话:bind的作用同样是改变this指向,但是它不是立刻执行,而是返回一个改变了this指向+预设参数的函数。
看个例子
var foo = {
value: 'f',
};
function bar(param1, param2) {
console.log(this.value + param + param2);
}
bar.bind(foo, "1")("2") // f12
bar.bind接受的第一个参数也是目标this指向,但是它也支持传入函数的默认参数配置,比如代码中传入了"1",相当于之后在调用bar.bind返回的函数时,param1会被默认置为"1"。bar.bind返回的是一个函数,因此代码中可以进一步调用函数 并且将"2"传递给param2
因此其任务和call近似,同样包括修改this指向,区别在于之前是直接调用 这边是返回一个函数,但实现思路大差不差 都是通过隐式绑定实现
实现代码
Function.prototype.myBind= function(that, ...rest){
that = that || globalThis
return (...args) => {
that.fn = this;
that.fn(...rest, ...args)
delete that.fn
}
}
代码解释
- 边界情况处理同上
- 修改this的方式同样是通过隐式绑定实现,但是有个细节点需要注意,返回的函数使用了箭头函数。这是为了确保函数内部的this才能够始终指向上层作用域的this 即指向函数;否则会变成默认绑定从而指向global。这是箭头函数的特性之一。
存在问题
这段代码无法处理使用bind之后使用返回的函数进行new操作的情况了,因为你无法new一个无法修改this的函数
举个例子:
Function.prototype.myBind = function(that, ...rest){
that = that || globalThis
return (...args) => {
that.fn = this;
that.fn(...rest, ...args)
delete that.fn
}
}
var foo = {
value: 'f',
};
function bar(param1, param2) {
console.log(this.value + param1 + param2);
}
const bindFunc = bar.myBind(foo, "1")
const instance = new bindFunc("2") // error
修改方式
有同学会想了 既然是因为箭头函数无法进行new操作,那我们不用箭头函数不就好了?让我们来try try!
Function.prototype.myBind = function (that, ...rest){
that = that || globalThis
const curFn = this // 提前保存外层this指向的调用函数
return function (...args) {
that.fn = curFn; // 因为使用一般函数,其this是根据调用方动态变化的,所以我们需要手动获取上层的this 即curFn
that.fn(...rest, ...args)
delete that.fn
}
}
var foo = {
value: 'f',
};
function bar(param1, param2) {
console.log(this.value + param1 + param2);
}
const bindFunc = bar.myBind(foo, "1")
const instance = new bindFunc("2") // f12
似乎是修改成功了,但是隐隐约约中透露着一股不对劲。按理来说new出来的实例对象的指针应当是指向实例对象本身,但是我们的bind实现中并没有对这个处理,那如果构造函数中包含对实例对象的赋值 通过mybind指向的对象岂不是也会被修改?让我们来验证一下!
Function.prototype.myBind = function (that, ...rest) {
that = that || globalThis
const curFn = this // 提前保存外层this指向的调用函数
return function (...args) {
that.fn = curFn; // 因为使用一般函数,其this是根据调用方动态变化的,所以我们需要手动获取上层的this 即curFn
that.fn(...rest, ...args)
delete that.fn
}
}
var foo = {
value: 'f',
};
function bar(param1, param2) {
this.value = "bar" // 在构造函数中进行赋值 添加实例的value
console.log(this.value + param1 + param2);
}
const bindFunc = bar.myBind(foo, "1")
const instance = new bindFunc("2") // bar12
console.log(foo) // { value: 'bar' }
console.log(instance) // {}
可以看到,在我们在实例中添加this.value = "bar"后,虽然构造函数的输出是正确的,但是其并没有在创建的实例中添加,反而修改了myBind绑定的foo对象!
由于我们目前暂时不了解new的实现方式,所以我们根据已有代码进行推测,这可能是因为new操作的过程中可能存在一个调用构造函数的时机,但是我们myBind返回的构造函数中的this指向是绑定到myBind传入的参数 foo上,而没有绑定到实例对象上,故而修改了foo而没有修改实例。
这里推测暂时看不懂也没关系,等下面看完了new的实现再回头看就能串联起来了~
因此其可能的修改方式就是判断当前是不是在new一个实例,如果是就不修改this指向直接调用方法,如果是进行bind的逻辑处理。
但有同学可能又要一头雾水了,我怎么知道它是不是在new一个实例啊,这看起来也妹有区别啊?莫急,等我们先聊聊new的实现,理解其中原理,方可知晓真谛。
欲知后事如何,且听下回分解~