由'连环打call'和'this指向'引起的思考

540 阅读4分钟

本文对this指向的讨论不包含箭头函数

call

先看一道面试题

function fn(a,b){
    console.log(this)
    console.log(a)
    console.log(a+b)
}

fn.call(1)
fn.call.call(fn)
fn.call.call.call(fn,1,2)
fn.call.call.call.call(fn,1,2,3)

上边四个语句分别输出什么?

可以仔细思考一段时间

下面给出答案

fn.call(1)  //Number{1}, undefined, NaN
fn.call.call(fn)    //window, undefined, NaN
fn.call.call.call(fn,1,2)   //Number{1}, 2, NaN
fn.call.call.call.call(fn,1,2,3)    //Number{1}, 2, 5

call 函数的作用

call函数的主要作用在于改变 this指向,例如fn.call(1)也就是将call函数接受到的第一个参数1作为调用call函数的函数fn的执行上下文,然后执行fn()this就指向了1call函数的具体用法可参看MDN

关键的第二句

fn.call.call(fn)。这句话,我个人被第一个出现的fn迷惑了很久,而实际上,第二句最终执行的函数是fn.call(),关键在于最后的那个.上,使用call时,最后那个.call之前的才是最后被执行的函数。例如:

var name = '小红'
var o = {
    name: '小明',
    sayName:function(){
        console.log(this.name)
    }
}
o.sayName.call(null) //小红

o.sayName.call(null)最终执行的是sayName函数体内的语句而不是o.sayName(),我个人理解最终应该执行的可以类似于(function sayName(){console.log(this.name)})(),而该语句中的this指向windowcall函数默认第一个参数是null还有undefined时的默认指向。故输出的是window.name
所以fn.call.call(fn)最终执行的语句应该是fn.call(),而call其实是每个函数都有的属性,最终定位在Function.prototype.call,每一个函数的call都是从Function的原型处获取的,而call本身是一个函数,所以call.call指向的是本身。
回到题目本身,我们知道fn.call.call(fn)最终执行的应该是call()而此时call函数的this指向fn,那到底为什么,最后会输出那样的结果呢?我们先此问题放在一边,看看this指向的相关理解。

this永远指向最后调用它的那个对象

我非常赞同这篇文章: this、apply、call、bind 中,对this指向的说明。 this 永远指向最后调用它的那个对象,由于提到的文章已经有了大量的举例和论证,我就不过多阐述了,大意是指假如有obj1.obj2.obj3.func() 语句执行,func函数中的this只会指向obj3也就是.func()中的那个.之前的那个对象,不再往这个对象往上寻找。定义在全局的函数中的this会指向window,如function fn(){}定义一个函数fn,然后执行fn(),等同于window.fn()。 由此,我由此推论:明确this指向的函数调用相当于this.func()

call函数的面纱

折回到第二句fn.call.call.(fn),我们已经知道最终执行的应该是call(),而此时call函数的this指向fn,所以由之上的推论(明确this指向的函数调用相当于this.func())可以推出:最终fn.call.call.(fn)执行的,可看作为fn.call(),此时fn.call()call函数的第一个参数没传故为undefined,默认指向window,所以输出为window, undefined, NaN。其转换过程可以类比为:

/*为了好区分,这里call传入fn1 , typeof fn1 === 'function'*/
fn.call.call(fn1)  =>  
fn1.(fn.call()) => 
fn1.(Function.prototype.call()) => 
fn1.call()

那可能会有疑问了,第一句fn.call(1)岂不是等同于1.fn()了??实际上应该是等同于new Number(1).fn(),我推测call函数的原理大致应该是这样子的:

Function.prototype.selfCall = function(context){
    context === undefined || context === null ? context = window
    /*处理context类型的代码,如果是基本类型,就转为基本类型对象*/
    ...
    /*call函数是属于函数类型的属性,调用call函数时,this指向调用call函数的函数,即typeof this === 'function'*/
    context.fn = this /*将要执行的函数this*/
    var args = /*处理call更多参数的代码*/
    ...
    context.fn(...args)
    delete context.fn
}

也可以参考文章: call, apply, bind实现 本篇文章还是有关 call函数 与 this指向 的老生常谈吧,由于之前参看了许多文章,一直不太得理解,看了call函数的实现,还有this的绑定,由于用得太少,看到一些用法时,还是比较朦胧。工作之余回想起这道面试题,觉得 this绑定与 call函数的实现原理相互印证,感觉对 callthis指向清晰了许多。起码,这道面试题,能够相对清晰的理解了。前端小白一枚,文章有许多不严谨,不清晰之处望多多海涵。
参考文献:
MDN Function.prototype.call
this、apply、call、bind
JavaScript中的call、apply、bind深入理解
call, apply, bind实现