call, call.call, call.call.call, 你也许还不懂这疯狂的call

11,047 阅读5分钟

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战

前言

Function.prototype.call 我想大家都觉得自己很熟悉了,手写也没问题!!
你确认这个问题之前, 首先看看 三千文字,也没写好 Function.prototype.call,

看完,你感觉还OK,那么再看一道题:
请问如下的输出结果

function a(){ 
    console.log(this,'a')
};
function b(){
    console.log(this,'b')
}
a.call.call(b,'b')  

如果,你也清晰的知道,结果,对不起,大佬, 打扰了,我错了!

本文起源:
一个掘友加我微信,私聊问我这个问题,研究后,又请教了 阿宝哥
觉得甚有意思,遂与大家分享!

结果

结果如下: 惊喜还是意外,还是淡定呢?

String {"b"} "b"

再看看如下的代码:2个,3个,4个,更多个的call,输出都会是String {"b"} "b"

function a(){ 
    console.log(this,'a')
};
function b(){
    console.log(this,'b')
}
a.call.call(b,'b')  // String {"b"} "b"
a.call.call.call(b,'b')   // String {"b"} "b"
a.call.call.call.call(b,'b')  // String {"b"} "b"

看完上面,应该有三个疑问?

  1. 为什么被调用的是b函数
  2. 为什么thisString {"b"}
  3. 为什么 2, 3, 4个call的结果一样

结论:
两个以上的call,比如call.call(b, 'b'),你就简单理解为用 b.call('b')

分析

为什么 2, 3, 4个call的结果一样

a.call(b) 最终被调用的是a,
a.call.call(b), 最终被调用的 a.call
a.call.call.call(b), 最终被执行的 a.call.call

看一下引用关系

a.call === Function.protype.call  // true
a.call === a.call.call  // true
a.call === a.call.call.call  // true

基于上述执行分析:
a.call 被调用的是a
a.call.calla.call.call.call 本质没啥区别, 被调用的都是Function.prototype.call

为什么 2, 3, 4个call的结果一样,到此已经真相

为什么被调用的是b函数

看本质就要返璞归真,ES 标准对 Funtion.prototye.call 的描述

Function.prototype.call (thisArg , ...args)

When the call method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:

  1. If IsCallable(func) is false, throw a TypeError exception.
  2. Let argList be an empty List.
  3. If this method was called with more than one argument then in left to right order, starting with the second argument, append each argument as the last element of argList.
  4. Perform PrepareForTailCall().
  5. Return Call(functhisArgargList).

中文翻译一下

  1. 如果不可调用,抛出异常
  2. 准备一个argList空数组变量
  3. 把第一个之后的变量按照顺序添加到argList
  4. 返回 Call(functhisArgargList)的结果

这里的Call只不是是一个抽象的定义, 实际上是调用函数内部 [[Call]] 的方法, 其也没有暴露更多的有用的信息。

实际上在这里,我已经停止了思考:

a is a function, then what a.call.call really do? 一文的解释,有提到 Bound Function Exotic Objects , MDN的 Function.prototype.bind 也有提到:

The bind() function creates a new bound function, which is an exotic function object (a term from ECMAScript 2015) that wraps the original function object. Calling the bound function generally results in the execution of its wrapped function.

Function.prototype.call 相反,并没有提及!!! 但不排查在调用过程中有生成。

Difference between Function.call, Function.prototype.call, Function.prototype.call.call and Function.prototype.call.call.call 一文的解释,我觉得是比较合理的

function my(p) { console.log(p) }
Function.prototype.call.call(my, this, "Hello"); // output 'Hello'

Function.prototype.call.call(my, this, "Hello"); means:

Use my as this argument (the function context) for the function that was called. In this case Function.prototype.call was called.

So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.

It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string.

重点标出:
So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.

It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string

翻译一下:
Function.prototype.call.call(my, this, "Hello")表示: 用my作为上下文调用Function.prototype.call,也就是说my是最终被调用的函数。

my带着这些 (this, "Hello") 被调用, this 作为被调用函数的上下文,此处是作为my函数的上下文, 唯一被传递的参数是 "hello"字符串。

基于这个理解, 我们简单验证一下, 确实是这样的表象

// case 1:
function my(p) { console.log(p) }
Function.prototype.call.call(my, this, "Hello"); // output 'Hello'

// case 2:
function a(){ 
    console.log(this,'a')
};
function b(){
    console.log(this,'b')
}
a.call.call(b,'b')  // String {"b"} "b"

为什么被调用的是b函数, 到此也真相了。

其实我依旧不能太释怀, 但是这个解释可以接受,表象也是正确的, 期望掘友们有更合理,更详细的解答。

为什么thisString {"b"}

在上一节的分析中,我故意遗漏了Function.prototype.call的两个note

NOTE 1: The thisArg value is passed without modification as the this value. This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value. Even though the thisArg is passed without modification, non-strict functions still perform these transformations upon entry to the function.

NOTE 2: If func is an arrow function or a bound function then the thisArg will be ignored by the function [[Call]] in step 5.

注意这一句:

This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value

两点:

  1. 如果thisArgundefined 或者null, 会用global object替换 这里的前提是 非严格模式
"use strict"

function a(m){
    console.log(this, m);  // undefined, 1
}

a.call(undefined, 1)
  1. 其他的所有类型,都会调用 ToObject进行转换 所以非严格模式下, this肯定是个对象, 看下面的代码:
Object('b') // String {"b"}

note2的 ToObject 就是答案

到此, 为什么thisSting(b) 这个也真相了

万能的函数调用方法

基于Function.prototype.call.call的特性,我们可以封装一个万能函数调用方法

var call = Function.prototype.call.call.bind(Function.prototype.call);

示例

var person = {
    hello() { 
        console.log('hello', this.name) 
    }
}

call(person.hello, {"name": "tom"})  // hello tom

写在最后

如果你觉得不错,你的一赞一评就是我前行的最大动力。

技术交流群请到 这里来。 或者添加我的微信 dirge-cloud,一起学习。

引用

sec-function.prototype.call
Bound Function Exotic Objects
Function.prototype.bind a is a function, then what a.call.call really do?
Difference between Function.call, Function.prototype.call, Function.prototype.call.call and Function.prototype.call.call.call
Javascript Function.prototype.call()
Can't use Function.prototype.call directly