call的作用
call最主要的作用就是改变this的指向,所有函数都能基于原型链在Function.prototype里找到call方法,所以所有函数都能直接调用call方法,如下:
函数.call(新的this指向,参数1,参数2,…)
call的第一个参数是新的this指向,其余的参数都是调用call方法的函数的实参,调用call方法的函数会被执行。
如果call方法的第一个参数是null或者undefined,在非严格模式下,thi指向window,在严格模式下,this指向null或者undefined;如果没有给call方法传递参数,那么在非严格模式下,this指向window,在严格模式下,this指向undefined。
call的原理
在做题之前需要简单了解一下call的原理,我们先来看一串代码:
function show(){
console.log(this);
}
var obj={}
show();
此时show函数执行后,show函数里面的this指向的是window,我们要将这个this的指向改成obj的话,就需要将show函数添加到obj里面,并且通过obj调用来执行,如下:
function show(){
console.log(this);
}
var obj={};
obj.show=show;
obj.show();
现在show函数里的this就指向obj了。
call方法的实现也是基于这个原理。
还原call方法
了解了call方法的原理之后,我们就可以自己来还原一下call方法,先写一个初级版本的,这里我用函数mycall来表示我重写的call方法:
Function.prototype.mycall = function mycall(context,...params){
context['this'] = this;//this--->show函数
let result = context['this'](...params);
return result;
}
function show(){
console.log(this,...arguments);
}
var obj = {name:'chen'};
show.mycall(obj,10,20);//执行结果是{name:'chen',this:f},10,20
我们不难发现这初级版本有以下几个弊端:
- 如果第一个参数不是引用数据类型,没有原型链,无法添加show函数,所以代码会报错,根据万物皆对象的理论,所有函数都可以通过原型链找到Object的原型对象,而Object本身也是一个函数,我们可以通过Object()将基本数据类型转换为对应的引用数据类型.举个简单的例子,如下:
console.log(Object(123));//Number{123}
conosle.log(Object('haha'));//String{'haha'}
var obj = Object('chen');
console.log(obj instanceof Object);//true
console.log(obj instanceof String);//true
- 如果第一个参数是null或者是undefined,代码也会报错,因为null和undefined也不是引用数据类型,无法添加show函数。(在一开始我们提到,call方法在非严格模式下,第一个参数是null或者undefined的时候,this是指向window的)
- 在执行mycall方法后输出的结果,临时添加在context里的this属性也被打印出来了,所以我们需要在return前把这个临时添加的属性给删除。
经过以上分析,我们可以进一步升级mycall方法:
Function.prototype.mycall = function mycall(context,...params){
//this--->show函数 参数:context params:[10,20]
//2. 第一个参数如果是null或者undefined
//this--->window show函数添加在window身上
if(context === null || context === undefined){
context = window;
}//
//1. 如果第一个参数不是引用数据类型,转换成引用数据类型
if(typeof context !== 'object' || typeof context !== 'function'){
context = Object(context);
}
/*注意:上面两个判断条件不能交换位置,不然当第一个参数是null或者undefined的时候,
this指向的是一个空对象*/
context['this'] = this;
let result = context['this'](...params);
//3. 删除临时添加的属性
delete context['this'];
return result;
}
function show(){
console.log(this,...arguments);
}
var obj = {name:'chen'};
show.mycall(obj,10,20);//执行结果是{name:'chen'},10,20
夺命连环call
题目本体:
const fn1 = function fn1(){
console.log(1);
}
const fn2 = function fn2(){
console.log(2);
}
fn1.call(fn2);
fn1.call.call.call.call(fn2);
Function.prototype.call(fn2);
Function.prototype.call.call.call.call(fn2);
- fn1.call(fn2)
这题是把fn1的this指向改成了fn2,并且执行fn1函数,结果为1.
- fn1.call.call.call.call(fn2)
为了避免混乱,我们可以使用上面初级版本的mycall方法来进行分析推理,可以把这里的call都换成mycall
function mycall(fn2){
fn2['this'] = this;//this----->fn1.mycall.mycall.mycall
let result = fn2['this']();//fn1.mycall.mycall.mycall()
//注意!!!:此时fn1.mycall.mycall.mycall里的this变成了fn2
return result;
}
此时fn1.mycall.mycall.mycall继续执行:
//经过上一步,此时的this指向是fn2,并且括号里没有参数,所以是在window上添加属性
function mycall(){
window['this'] = this;//this----->fn2
let result = window['this']();//fn2()---->所以结果是2
//这里的this变成了window
return result;
}
经过以上步骤可以得出最后的结果是2
我们可以通过上面的过程推导出一个结论:fn1.call.call…(n个call).call(fn2)最后都会变成fn2(),而且此时的this指向是window,所以为了确保最后的this指向是window,我们可以完善这一公式:fn1.call.call…(n个call).call(fn2)---->fn2.call(),所以下面两题我们就可以直接使用这个公式了。
- Function.prototype.call(fn2)
这题是把this指向改成fn2,并且执行函数Function.prototype,Function.prototype是Function的原型对象,但它的本质是一个匿名空函数,这里不做过多阐述,我们可以打印试验一下,打印的结果就是一个空函数,所以这题的答案是空的,什么也没有。
- Function.prototype.call.call.call.call(fn2)
这题可以直接用第二题得出的结论来解答,所以这题就演变成fn2.call(),答案是2。