引言
在上一篇关于this指向情况汇总中提到了call、apply以及bind,接下来我们将继续深度研究一下关于他们的用法,并尝试重新书写一下源码,使自己完全掌握。
小试牛刀
首先通过一道题来试一下我们对call、apply以及bind的掌握程度
const fn = function fn(x, y) {
this.total = x + y;
console.log(this)
return this
}
let obj = {
name: 'obj'
}
从上面这道题我们可以发现,目前fn与obj之间没有任何关系,所以现在我们要实现鼠标点击body时将fn中的this修改为obj
问题剖析
从需求来看,我们通过三种方法均可实现,但是要注意的是点击时修改this指向,而不是立即修改
// 1 ❎
document.body.onclick = fn.call(obj);
console.log(fn.call(obj)) // {name: 'obj', total: NaN}
// 2
document.body.onclick = () => {
fn.call(obj)
}
// 3
document.body.onclick = fn.bind(obj);
console.log(fn.bind(obj))
// ƒ fn(x, y) {
// console.log(2345678)
// this.total = x + y;
// console.log(this)
// return this
// }
说明:由上面三种解法中我们可以看到:
-
第一种,在没有点击
body时就已经触发了this的改变,所以第一种是错误的解法。 -
第二种和第三种,符合我们的需求,在点击时修改的
this指向。 -
通过上面的方法对比我们可以发现
call方法实现this指向的改变是立即执行的(apply也是这样),而bind则不同。我们可以在上面对应的两处console中可以看到他们的区别,分别对应未点击s时的返回值。 -
call与apply的区别不在赘述,所以上面的例子中只使用了call与bind作对比进行说明。
Bind
Bind原理
执行bind只是先把fn/obj/其他参数都存储起来,返回一个匿名函数做我们的事件绑定,当点击时,再把匿名函数执行,执行时把事先存储的那些信息拿过来处理
Bind源码实现
Function.prototype.mybind = function mybind(context, ...params){
// this => fn
// context => obj 要给函数的改变的this
// params => [10, 20]
return (...args) => {
// args => [ev] 点击行为触发,传递给匿名函数的信息,例如:事件对象
this.call(context, ...params.concat(args));
}
}
document.body.onclick = fn.mybind(obj, 10, 20)
Call
在前面的 小试牛刀中,fn与obj不存在任何关系,现在实现一个新的需求: 在不使用call、apply、bind的情况下,将fn 中的this修改为obj
请先思考一下,再继续向下看🤔
问题剖析
这里我们需要考虑的就是关于this指向的问题了,建议先看一下关于JavaScript中this指向情况汇总(小白建议,大佬膜拜🙏)。由于我能想到的方式就是将obj添加新的属性,例如:fn: fn,这样在调用时this的指向将会发生改变。
const fn = function fn(x, y) {
this.total = x + y;
console.log(this)
return this
}
let obj = {
name: 'obj'
}
obj.fn = fn;
obj.fn(10, 20);
说明:通过上面的解法已经实现了我们的需求,this的指向改变为了obj,但是this中多了属性fn,所以再添加一部操作即可。
const fn = function fn(x, y) {
this.total = x + y;
console.log(this)
return this
}
let obj = {
name: 'obj'
}
obj.fn = fn;
obj.fn(10, 20);
delete obj.fn
CALL源码重写
Function.prototype.call = function call(context, ...params) {
// this => fn
// context => obj 给函数改变的this
// params => [10, 20] 给函数传递的参数
// 由于基本类型值是无法设置键值对的 所以需要对context类型进行处理 具体说明请看下面:
context = context == null ? window : context;
let contextType = typeof context;
// contextType 既不是Object 也不是function
if (!/^(object|function)$/i.test(contextType)) {
context = Object(context);
}
// 为了保证建立关联的属性保持唯一 所以使用Symbol
let result, key = Symbol('KEY');
context[key] = this;
result = context[key](...params);
delete context[key];
return result;
};
let res = fn.call(obj, 10, 20);
console.log(res)
说明:基于基本类型值是无法设置键值对的情况进行说明
// 1
let n = 10;
n.xxx = 100;
console.log(n.xxx) // undefined
// 2
let m = new Number(10);
m.xxx = 100;
console.log(m.xxx) // 100
根据上面两个例子可以看出基本类型值是无法设置键值对的,但是将其变为对应所属类的引用类型值,便可以进行设置键值对。下面来实现将一个值 变为 所属类的引用类型值。
let n = 10;
n = new n.constructor(n);
// n.constructor() 表示 对应的所属类
说明:上面的方法对于Symbol/BigInt不友好,因为不能被new,没有构造函数
Object(n)
说明:这种方法是最好的,直接变为自己所属类的引用类型实例
深度剖析面试题
var name = 'joe';
function A(x, y) {
var res = x + y;
console.log(res, this.name);
}
function B(x, y) {
var res = x - y;
console.log(res, this.name);
}
B.call(A, 40, 30);
B.call.call.call(A, 20, 10);
Function.prototype.call(A, 60, 50);
Function.prototype.call.call.call(A, 80, 70);
根据Call源码进行分析这道题:
温馨提示:请准备笔和纸,小心自己稀里糊涂蒙了哦🙈
-
首先执行
B.call(A, 40, 30)- 首先判断
context = context == null ? window : context,得出context => A,params=[40, 30],this => B - 第二步执行
contextType = typeof context,得出contextType=>function - 根据if条件语句进行判断,不符合该条件,则向下执行
- 第四步,创建唯一的
key值key = Symbol('KEY') - 第五步执行
context[key] = this,此时的this是B,则得到A[key]=B - 第六步执行
context[key](...params),便得到A[key](40, 30)也就是B(40, 30),此时的this是A - 第七步执行
B方法中的var res = x - y,得到res=10,this.name = A,结论如下图:
进行验证:
- 首先判断
-
第二步执行
B.call.call.call(A, 20, 10)- 首先我们能够看到有三个
call方法,但是最终执行的只是最后一个call方法,然后进行判断context = context == null ? window : context,得出context => A,params=[20, 10],this => B.call.call,此时的this也就是call方法本身 - 第二步执行
contextType = typeof context,得出contextType=>function - 根据if条件语句进行判断,不符合该条件,则向下执行
- 第四步,创建唯一的
key值key = Symbol('KEY') - 第五步执行
context[key] = this,此时的this是call,则得到A[key]=call - 第六步执行
context[key](...params),便得到A[key](20, 10)也就是call(20, 10),也就是将call方法进行第二次执行,此时的this是A,context => 20,params => 10 - 第七步此时将进入if语句中,因为此时的context既不是Object,也不是Function,所以执行
context = Object(context),也就是执行context = new Number(20) - 第八步,再次创建唯一的
key值key = Symbol('KEY') - 第九步执行
context[key] = this,也就是new Number(20)[key] = A - 第十步执行
context[key](...params),也就是new Number(20)[key](10),将A执行,此时的this是new Number(20) - 最后一步执行
A方法中的var res = x + y,得到res=20 + undefined,res = NaN,因为this是new Number(20),this.name = undefined,得到的结论如下图:
进行验证:
- 首先我们能够看到有三个
-
第三部执行
Function.prototype.call(A, 60, 50)- 我们能够发现与第一步
B.call(A, 40, 30)类似,也就是说B = Function.prototype - 在
B.call(A, 40, 30)中,最终执行B函数,在Function.prototype.call(A, 60, 50)执行Function.prototype函数,但是Function.prototype它是一个匿名空函数,什么都不输出,此时的this是A,params => [60, 50]
- 我们能够发现与第一步
-
第四步执行
Function.prototype.call.call.call(A, 80, 70)Function.prototype.call.call.call(A, 80, 70)同B.call.call.call(A, 20, 10),按照第二步执行的结论可以得出将执行A(70),this => new Number(80),结果为res => NaN,this.name = undefined- 进行验证:
-
趁热打铁走一波~~~
function fn1(){console.log(1);}
function fn2(){console.log(2);}
fn1.call(fn2);
fn1.call.call.call(fn2);
Function.prototype.call(fn2);
Function.prototype.call.call.call(fn2);
按照上面的结论就可以做来这道题,答案如下:
1. 1
2. 2
3. 什么都不执行
4. 2
最后
本文中所说的源码重写,并非真正的源码,而是使用javaScript按照源码的思想去实现相同的功能。本文并没有重写apply的源码,由于apply与call区别较小,可自己尝试书写。如有发现不够完善的地方不要吝啬指出。
biu biu biu ~ ❤️❤️❤️❤️ 点关注 🙈🙈 不迷路 点个赞哦😘