深挖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);