上一回说到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);
代码解析:
-
首先看核心代码部分:我们希望能执行函数,并把函数中的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一定可以被添加属性呢?
-
传入值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();
等。创建一个引用类型值时,两种创建方式即
obj1
与obj2
没什么区别。
创建一个基本类型值时:通过字面量方式创建得到的结果num1
为基本类型值,不可以为其设置属性;通过构造函数创建得到的结果num2
为对象类型值,可以为其设置属性。但两者都是所属类的实例,都可以调用原型上的方法。
为了避免程序出错,当context传进来基本类型值时,需要将其转换为对应的对象类型值。例如,new num1.constructor(num1)
,数字num1调用原型上的constructor属性找到所属类Number,然后通过构造函数方式,创建一个原始值为num1的对象类型值。
还有特殊情况 ( 手动捂脸: 怎么那么多特殊情况!!!):基本类型中,symbol和bigint不能被new。那咋办捏,他俩简单一点,直接用Object套起来就能转换为相应的对象类型。(终于到了总结情况的时刻)
于是,当传入的context为以下三种情况时,需要做特殊的处理:
(1) 不传参数 / null / undefined 时,默认将this修改为windowcontext == 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方法原理咯。
需要注意的点太多啦,以至于总结完毕之后感觉文章还是有些混乱。
欢迎批评指正~