关于JavaScript中的CALL、Apply、Bind的源码重写以及剖析面试题

185 阅读7分钟

引言

在上一篇关于this指向情况汇总中提到了callapply以及bind,接下来我们将继续深度研究一下关于他们的用法,并尝试重新书写一下源码,使自己完全掌握。

小试牛刀

首先通过一道题来试一下我们对callapply以及bind的掌握程度

const fn = function fn(x, y) {
    this.total = x + y;
    console.log(this)
    return this
}

let obj = {
    name: 'obj'	
}

从上面这道题我们可以发现,目前fnobj之间没有任何关系,所以现在我们要实现鼠标点击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时的返回值。

  • callapply的区别不在赘述,所以上面的例子中只使用了callbind作对比进行说明。

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

在前面的 小试牛刀中,fnobj不存在任何关系,现在实现一个新的需求: 在不使用callapplybind的情况下,将fn 中的this修改为obj

请先思考一下,再继续向下看🤔

问题剖析

这里我们需要考虑的就是关于this指向的问题了,建议先看一下关于JavaScriptthis指向情况汇总(小白建议,大佬膜拜🙏)。由于我能想到的方式就是将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源码进行分析这道题:

温馨提示:请准备笔和纸,小心自己稀里糊涂蒙了哦🙈

  1. 首先执行 B.call(A, 40, 30)

    • 首先判断context = context == null ? window : context,得出context => Aparams=[40, 30]this => B
    • 第二步执行 contextType = typeof context,得出contextType=>function
    • 根据if条件语句进行判断,不符合该条件,则向下执行
    • 第四步,创建唯一的keykey = Symbol('KEY')
    • 第五步执行context[key] = this,此时的thisB,则得到A[key]=B
    • 第六步执行context[key](...params),便得到A[key](40, 30)也就是B(40, 30),此时的 thisA
    • 第七步执行B方法中的var res = x - y,得到res=10this.name = A,结论如下图:

    进行验证:

  2. 第二步执行B.call.call.call(A, 20, 10)

    • 首先我们能够看到有三个call方法,但是最终执行的只是最后一个call方法,然后进行判断context = context == null ? window : context,得出context => Aparams=[20, 10]this => B.call.call,此时的this也就是call方法本身
    • 第二步执行 contextType = typeof context,得出contextType=>function
    • 根据if条件语句进行判断,不符合该条件,则向下执行
    • 第四步,创建唯一的keykey = Symbol('KEY')
    • 第五步执行context[key] = this,此时的thiscall,则得到A[key]=call
    • 第六步执行context[key](...params),便得到A[key](20, 10)也就是call(20, 10),也就是将call方法进行第二次执行,此时的thisAcontext => 20params => 10
    • 第七步此时将进入if语句中,因为此时的context既不是Object,也不是Function,所以执行 context = Object(context),也就是执行 context = new Number(20)
    • 第八步,再次创建唯一的keykey = Symbol('KEY')
    • 第九步执行context[key] = this,也就是new Number(20)[key] = A
    • 第十步执行context[key](...params),也就是new Number(20)[key](10),将A执行,此时的thisnew Number(20)
    • 最后一步执行A方法中的var res = x + y,得到res=20 + undefinedres = NaN,因为thisnew Number(20)this.name = undefined,得到的结论如下图:

    进行验证:

  3. 第三部执行 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它是一个匿名空函数,什么都不输出,此时的thisAparams => [60, 50]
  4. 第四步执行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 => NaNthis.name = undefined
    • 进行验证:
  5. 趁热打铁走一波~~~

  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的源码,由于applycall区别较小,可自己尝试书写。如有发现不够完善的地方不要吝啬指出。


biu biu biu ~ ❤️❤️❤️❤️ 点关注 🙈🙈 不迷路 点个赞哦😘