深入探索call/apply/bind的内部机制

上一回说到Function.prototype上有三个神奇的方法call/apply/bind,他们都可以改变函数中的this指向。
接下来的时间喃,请跟随我踏上“深入探索call/apply/bind的内部机制”之旅,知其然而知其所以然——才能屡战屡胜呀!

bind方法原理

前一篇文章this的情况总结中,我们了解了bind方法的用法,简单回顾:
[function].call([context],params1,params2,…),预先修改this,预先存储参数,而函数不被立即执行。
下面我们就来康康到底是如何实现这个功能的!

let obj = {
	name:'OBJ'
}
function func(){
	console.log(this,x,y);
}

Function.prototype.bind = function bind(context = window,...params){
	let _this = this;
	return function anonymous(){
		_this.call(context,...params);
		// _this.apply(context,params);
	}
}
document.body.onclick = func.bind(obj,10,20);
  • 执行bind方法返回一个匿名函数,将这个匿名函数地址绑定给元素的点击事件。bind函数上下文中创建的匿名函数被上下文以外的变量占用,因此形成一个不销毁的上下文——闭包。
  • 我们将需要修改的this指向和需要传递给函数的参数传进来,利用闭包的机制预先保存在不销毁的上下文中。
  • 分析函数中的this:
    • bind函数中的this:指向要执行的函数func,因为func调用了bind函数(func.bind())
    • 匿名函数中的this:取决于事件触发时的情况。匿名函数创建时还未执行,事件触发时才执行。上例中,元素点击事件触发匿名函数,因此匿名函数中的this:body元素;若通过定时器调用setTimeout(func.bind(obj,10,20),1000),则该匿名函数作为回调函数,this则指向window。
    • 也正是因为匿名函数中的this已经不再是func了,所以才需要用_this这样一个变量先存起来。
  • 在匿名函数中,我们可以取得事先在闭包中保存的变量,然后执行call / apply方法更改this指向,并传入参数。

call方法原理

那么问题来了,call方法是怎么实现修改this指向并传参的呢?
先上代码:

/*
 * @params
 * 		context:把函数中的this指向修改为context
 * 		...params: ES6中的剩余运算符,除了context之外的其他参数,以数组形式存储在params中
 * @return: 返回值与要执行的函数的返回值相同
 * /
	
Function.prototype.call = function call(context,...params){

	// 对传入的context参数判断并处理
	context == undefined ? context = window : null;
	if(!/^(object|function)$/.test(typeof context)){
		if(/^symbol|bigint$/.test(typeof context)){
			context = Object(context);
		}else{
			new context.constructor(context);
		}
	}
	
	// 核心代码
	let key = Symbol('AA');
	let result;
	context[key] = this;
	result = context[key](...params);
	delete context[key];
	return result;
}

let obj = {
	name:'OBJ'
}
function func(x,y){
	console.log(this);
	return x + y;
}
func.call(obj,10,20);

代码解析:

  1. 首先看核心代码部分:我们希望能执行函数,并把函数中的this修改为我们指定的参数context。上例中,我们想执行func函数并将函数中this修改为obj,obj怎样才能成为func函数中的this呢?通过成员访问的形式!obj.xxx = func,给obj添加一个属性xxx(叫别的名字也行),并将要执行的函数赋值给这个属性。那么我们执行obj.xxx()时,不就是在执行func()么!而且函数中的this也会变为obj!

    context[key] = this;   // call函数中的this是要执行的函数func(因为func.call),这个操作是给context添加一个属性,并把要执行的函数赋值给这个属性
    result = context[key](...params);     // 通过成员访问的形式执行这个函数,同时把参数传进去
    delete context[keys];    // 添加了一个属性,用完了得再删掉
    

    为了保证我们给context添加属性的时候,不会因为属性名相同而覆盖掉原有的属性,最好是取一个唯一的属性名——通过Symbol创建唯一值或时间戳。问题又来了:怎么能保证context一定可以被添加属性呢?

  2. 传入值context类型判断及处理

    常见的数据类型有:number、string、boolean、null、undefined、symbol、bigint、object、function,其中,只有object和function两种类型是可以为其添加属性的。 也就是说,当传入的值不为object或function时,我们需要做一定的处理,以保证代码能够正常运行。

    在此之前,我们需要了解创建一个值的两种方法
    ① 字面量创建:let num1 = 10; let obj1 = {};等。
    ② 构造函数创建:let num2 = new Number(10); let obj2 = new Object();等。

    创建一个引用类型值时,两种创建方式即obj1obj2没什么区别。
    创建一个基本类型值时:通过字面量方式创建得到的结果num1为基本类型值,不可以为其设置属性;通过构造函数创建得到的结果num2为对象类型值,可以为其设置属性。但两者都是所属类的实例,都可以调用原型上的方法。
    为了避免程序出错,当context传进来基本类型值时,需要将其转换为对应的对象类型值。例如,new num1.constructor(num1)数字num1调用原型上的constructor属性找到所属类Number,然后通过构造函数方式,创建一个原始值为num1的对象类型值。
    还有特殊情况 ( 手动捂脸: 怎么那么多特殊情况!!!):基本类型中,symbol和bigint不能被new。那咋办捏,他俩简单一点,直接用Object套起来就能转换为相应的对象类型。

    (终于到了总结情况的时刻)
    于是,当传入的context为以下三种情况时,需要做特殊的处理:
    (1) 不传参数 / null / undefined 时,默认将this修改为window

    context == undefined ? context = window : null;
    

    (2) 传入值为symbol / bigint 类型时,将其转换为对象类型

    context = Object(context);
    

    (3) 传入值为number / string / boolean基本类型值时,通过构造函数创建对应的对象类型值:

    new context.constructor(context);
    

apply方法原理

就没有什么好说的啦,和call方法基本一样,只不过执行函数传参数的时候传入数组就好啦!

result = context[key](params);

喏,这就是新鲜出炉的call / apply / bind方法原理咯。
需要注意的点太多啦,以至于总结完毕之后感觉文章还是有些混乱。
欢迎批评指正~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值