手把手教你手写call、apply、bind,学不会来砍我

前置知识

工欲善其事,必先利其器。手写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.test3this就是指向window。这种情况,就不满足谁调用this就是谁的口诀。

我们还应当明白一个概念,就是执行上下文的概念,能够让我们在判断this指向的时候更加的得心应手。

执行上下文的三个重要部分组成:

  1. 作用域链
  2. 变量对象
  3. this

那什么时候才会创建这个执行上下文呢?所谓的作用域链、变量对象又是什么?

我这里总结了两种创建上下文的情况:

  1. JS代码在被加载运行的时候会创建全局执行上下文
  2. 函数在被执行的时候会创建局部执行上下文

理解了记住这两条情况,对我们整个的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

callapply的区别想必大家都十分清楚,因此只要在上面的基础之上做一点小小的改动即可

// 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,已修改。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值