前置知识
工欲善其事,必先利其器。手写call、apply、bind考的就是对this
的掌控。
首先这个this的确定原则很简单——谁调用,就是谁
,举个很简单的例子:
function test() {
console.log(this)
}
const obj = {name: '张三'}
obj.test = test
test() // 浏览器控制台打印为:window
obj.test() // 浏览器打印为:obj
是不是谁调用就是哪个?那么还有一些比较容易混淆this
的实际应用场景,比如在对象里面的箭头函数,其this指向是谁呢?接着在上例的基础上添加如下代码:
obj.test2 = ()=>{
setTimeout(() => {
console.log(this)
})
}
obj.test3 = function(){
setTimeout(() => {
console.log(this)
})
}
obj.test2() // 浏览器控制台打印:window
obj.test3() // 浏览器控制台打印:obj
由此我们可以知道,箭头函数的this是由其定义所在作用域的this来决定的。obj.test3
的定义所在作用域就是全局作用域window
,所以obj.test3
的this
就是指向window
。这种情况,就不满足谁调用this就是谁
的口诀。
我们还应当明白一个概念,就是执行上下文的概念,能够让我们在判断this指向的时候更加的得心应手。
执行上下文的三个重要部分组成:
- 作用域链
- 变量对象
- this
那什么时候才会创建这个执行上下文呢?所谓的作用域链、变量对象又是什么?
我这里总结了两种创建上下文的情况:
- JS代码在被加载运行的时候会创建
全局执行上下文
- 函数在被执行的时候会创建
局部执行上下文
理解了记住这两条情况,对我们整个的JS学习以及工作生涯中,都是非常有用的。比如我们如何判断闭包?
很多人在学这一块的时候,可能就记住了——“内部函数可以访问,外部函数的变量”
这句话是没有问题的,但是有的人可能就只认为以下第一种才是闭包,第二种就不是:
// 第一种 非常典型的闭包,相信大家都会
function func1() {
const a = 1
return function() {
console.log(a)
}
}
// 第二种 这种也是闭包,闭包与否与是否返回一个函数没有关系,而是看他有没有引用局部上下文中的变量
function func1(){
const a = 1
function fun2() {
console.log(a)
}
fun2()
}
再讲一个前置知识——执行上下文栈:
每个函数都有自己的上下文,当代码执行流进入函数时,函数的上下文就会被推到上下文栈顶层,在执行完毕之后,弹出上下文栈,将执行权返还给之前的上下文。整个JS代码的执行流,都是通过这个上下文栈来控制的。
好,了解执行上下文栈
之后,我们再来讲作用域链,以及变量对象又是什么呢?
所谓变量对象就是代码在执行前、或者函数执行前就已经既定的对象,里面包含了该上下文中定义所有的函数以及变量。而作用域链,就决定了上下文栈中各级变量对象的访问顺序。
而this,依旧是秉承谁调用this就是谁的情况。红宝书对此的解释是:
*如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则this在非严格模式下等于window,在严格模式下等于undefined。如果作为某个对象的方法调用,则this等于这个对象。
有了上面的基础,下面就开始手写call、apply、bind
手写call
// 我们首先得在Function的原型对象上挂载一个我们自己call方法
// 1. 原生call方法接收1+n个参数,一个要绑定为this的对象,以及n个参数
Function.prototype.mycall = function(thisArg, ...arg) {
// 2. 再定义一个symbol对象,用作函数名,避免出现有函数重名的情况
const MYCALL = Symbol('mysymbol')
// 3. 前面我们已经知道,当一个方法被一个对象调用的时候,它的this为这个对象
// 所以此时mycall的this指向为调用它的函数,我们直接将它挂载道thisArg上面即可
thisArg.__proto__[MYCALL] = this
// 4. call的特点是不是立即执行啊?所以我们还要调用这个函数,并将参数传进去
thisArg[MYCALL](...arg)
// 5. 调用完毕之后,我们还要在thisArg的原型对象上删除这个方法
delete thisArg.__proto__[MYCALL]
}
function test(name, age) {
console.log(this)
console.log(`my name is ${name}, and I ${age} years old`)
}
// 定义obj,用作传入的对象
const obj = {
name: '张三',
age: 18
}
// 对照原来的call
test.mycall(obj, obj.name, obj.age)
test.call(obj, obj.name, obj.age)
手写apply
call
和apply
的区别想必大家都十分清楚,因此只要在上面的基础之上做一点小小的改动即可
// 1. 将之前的剩余参数arg改成普通的参数,用来接收数组
Function.prototype.myapply= function(thisArg, arg) {
// 2. 这里改成myapply
const MYAPPLY= Symbol('myapply')
// 3. 这里也改掉
thisArg.__proto__[MYAPPLY] = this
// 4. 还有这
thisArg[MYAPPLY](...arg)
// 5. 调用完毕之后,依旧要在thisArg的原型对象上删除这个方法
delete thisArg.__proto__[MYAPPLY]
}
function test(name, age) {
console.log(this)
console.log(`my name is ${name}, and I ${age} years old`)
}
// 定义obj,用作传入的对象
const obj = {
name: '张三',
age: 18
}
// 对照原来的apply
test.myapply(obj, [obj.name, obj.age])
test.apply(obj, [obj.name, obj.age])
手写bind
bind
的实现思路相较于上面两个有一丢丢区别,但是大差不差,我们要明白两个点:
bind
在调用的时候可以传入参数- 在执行的时候又可以传入参数
那么该怎么合并这些参数呢?其实很简单,看代码
// bind依旧是接收1+n个参数,this以及其它参数
/ bind依旧是接收1+n个参数,this以及其它参数
Function.prototype.mybind = function(thisArg, ...arg) {
// 1. 老规矩,定义Symbol对象
const MYBIND = Symbol('mybind')
// 2. 将this绑定到thisArg的原型上
thisArg.__proto__[MYBIND] = this
// 3. 细节来了,bind不是立即执行的,因此我们需要返回一个函数出去
// 这里定义 ...arg2 用来接收二阶段可能会传入的参数
return (...arg2)=>{
// 在这个函数里面去执行mybind 且要合并参数
thisArg[MYBIND](...arg, ...arg2)
// 4. 执行完毕之后,依旧是要在thisArg原型上做删除操作
delete thisArg.__proto__[MYBIND]
}
}
function test(name, age, food, drink) {
console.log(this)
console.log(`my name is ${name}, and I ${age} years old`)
console.log(`FavoriteFood:${food}, favoriteDrink:${drink}`)
}
// 定义obj,用作传入的对象
const obj = {
name: '张三',
age: 18
}
test.mybind(obj, obj.name, obj.age)(['红烧肉', '黄焖鸡'], '可乐')
test.bind(obj, obj.name, obj.age)(['啤酒小龙虾', '蒜蓉腰子'], '啤酒')
结语
至此所有内容完毕,上述代码均以测试可以直接放到控制台中运行测试。如果有问题,希望大家积极提出。
更新日志
2024/4/18 在手写apply、call的代码中最后应是删除thisArg原型上的的函数,而非this,已修改。