web前端高级JavaScript - 深挖call方法的底层实现原理并封装自己的call方法

363 阅读4分钟

深挖call的底层实现原理

在js中有3个方法可以改变函数中this的指向,那就是call、apply和bind。关于三个方法的更多详细请参考web前端高级 - JavaScript中函数内的this指向。这里只讲一下call的底层实现原理。下面来我们来看一段代码:

let obj = {
	name:'Alvin'
}

let name = 'window'
function fn(){
	console.log(this.name);
}
fn();//window
fn.call(obj);//Alivn

上面的代码中,当我们直接调用fn函数时输出了全局变量name的值“window”,说明这个时候函数fn中的this指向的是window。当我们执行fn.call(obj)时结果输出了obj对象中的name值“Alvin”,则表明这个时候函数fn中的this已经指向了对象obj了。仅从表象上我们可以分析得知:

  • 函数fn被调用执行了
  • 函数fn中的this指向发生了变化,不再指向window而是指向了传给call的参数obj了。

结合以上两点我们来尝试自己封装一个call函数

封装自己的call函数

根据上面分析出来的两点结论,我们再来分析一下如果我们自己要去封装一个call函数应该如何去实现

  • 首先call函数应该接收至少一个参数[obj]
  • 然后让调用call的函数fn执行,并且让函数fn内部的this指向call第一个参数obj
function _call(obj){

}
  • 那么问题又来了:obj是一个单独的对象,fn也是一个独立的函数,如何才能让这两个玩意发生关系,关联在一起呢?其实也不难,前面讲关于this指向时我们已经知道:“在一般情况下,谁调用函数,那么函数内部的this指向的就是谁,也就是说“点”前面是谁,函数内部的this就会指向谁,那我们就想办法让函数fn变成obj的一个属性,比如动态给obj添加一个fn属性并赋值为函数fn,这样就可以直接通过obj.fn来直接调用函数了。
  • 那么又引来了第二个问题,我们该如何在_call函数内部获取到外面真正要执行的fn函数呢?其实_call函数内部的this指向的就是我们要调用的函数fn,因为在调用_call时我们都会用函数点_call来调用,例如:fn._call(obj)。而上面已经说到:谁调用,函数内部的this就会指向谁。所以_call内部的this肯定会指向调用者fn的

两个问题都解决了,下面我们就用代码来实现一下:

//给Function的原型属性新增一个_call方法
Function.prototype._call = function (obj){
	obj.fn = this;
	obj.fn();
}

let obj = {
	name:'Alvin'
}

let name = 'window'
function fn(){
	console.log(this.name);
}
fn();//window
fn._call(obj);//Alivn

上面的代码运行,从表面上看已经实现了跟原生call方法相同的效果;但是原生call方法的实现肯定不止这么简单的两句代码就能实现,所以还有很多需要优化的地方:

  • 在_call中我们平白无故给obj添加了一个临时属性fn,那么用完应该再给删掉
  • 如果obj中本身就有fn这个属性,那么我们在_call中使用obj.fn会把原来的覆盖掉,所以最好采用唯一值来添加属性然后再删除
  • 如果fn函数还有其它参数我们应该如果获取
  • 如果传入的obj不是一个对象或者根本就没有传入obj参数,那么上面添加临时属性的操作肯定会报错
  • 如果函数fn还有返回值,还需要把返回值给返回

结合上面的几个问题,我们对_call方法做进一步优化

//给Function的原型属性新增一个_call方法
Function.prototype._call = function (obj, ...params){
	//1. 如果obj是undefined或null,应该让obj指向window
	obj == null ? obj = window : null;
	//2. 如果obj不是一个对象,需要转成对象
	if(typeof obj !== 'function' && typeof obj !== 'object'){
		obj = Object(obj);
	}
	//3.给obj添加一个唯一值属性,避免与原有属性冲突
	let objKey = Symbol();
	//给obj添加一个唯一值属性objKobjKey并赋值为调用函数
	obj[objKey] = this;
	//4.调用函数fn并接收返回值
	let result = obj[objKey](...params);
	//5.删除给obj新增的临时属性
	delete obj[objKey];
	//6.将函数fn的执行结果返回
	return result;
}

//结果验证
let obj = {
	name:'Alvin'
}

let name = 'window'
function fn(){
	console.log(this.name);
}
fn();//window
fn._call(obj);//Alivn

至此我们就实现了原生call函数的运行原理分析以及模拟call的实现原理封装自己的_call方法

  • 原生bind的重写
Function.prototype.bind = function(obj, ...params){
	let that = this;//原来要执行的方法
	//因为bind只会改天方法中的this指向,而不会调用函数执行,所以我们这里返回一个匿名函数,然后在匿名函数中再让原来的方法去调用call改变方法中的this指向。但不会立即执行,要等到匿名函数被调用才会执行。//如按钮的事件绑定
	return function(...args){
		params = params.concat(args);
		return that.call(obj, ...params);
	}
}

function fn(){
	console.log(this)
}
button.onclick = fn.bind(obj);