TAT.vorshen callable-object
In Web开发 on 2021年03月14日 by view: 548
0

原文地址:https://github.com/vorshen/blog/blob/master/callable-object/index.md
今天我们来聊一聊可调用对象,从底层来说,调用是指新建了栈帧,寄存器指向发生了变化。
从直观上看可以加 () 执行的就是可调用对象!比如我们熟悉的 javascript 中函数。

javascript 中的 callable

但是有没有想过,为什么这段代码可以按顺序执行?如果了解 C 或者 Java,程序的入口一定是一个 main 函数,为什么 js 中无需 main 函数了呢?

从 v8 源码一探究竟,这是因为 v8 会将整个 js 代码,包装成一个函数,源码位置如下:

Code 对象非常的重要,这个就是 v8 中函数执行的关键,v8 相关原话有:

Code describes objects with on-the-fly generated machine code.

JSFunctions are pairs (context, function code), sometimes also called closures.

JSFunction(v8 内数据类型) 相比较 JSObject 重大的差异也就是多了 code 属性,这也就是 Function 可以执行,而 Object 无法执行的原因。

其实我们将上面列子中的 js 代码,编译成字节码,也可以看出来整个文本可以执行的原因。

没接触过字节码也没关系,从上面至少能看到 generated bytecode for function 出现了两次,意味着有两个函数。
注意点 2 那里有一个 drink 关键字,代表是我们显示声明的函数;注意点 1 那里就是整段 js 代码,被作为了一个匿名函数执行。
注意点 3 就是调用 drink 的地方。

不过 js 本身是一个函数式编程语言,函数式是如何表现的我们不用多说,重点说一说「闭包」,闭包一词不可能有前端开发不知道 (哪怕没用过,面试也遇到过),那我们思考一下,为什么闭包可以跨越栈帧的限制?
以下面这个函数为例:

如果使用 d8 输出字节码,可以看到总共有三个 generated bytecode for function。整段执行的过程,我们先按常理猜测一下,函数执行作用域变化应该如下:

这里总共有三个阶段,重点看后面两个。

  • 第二阶段是执行了匿名的自执行函数,此时声明了一个 flag 变量在对应的作用域。
  • 第三阶段是执行 drink 函数,这里用到了两个变量。
    1. console,来自于上层的作用域,可以理解。
    2. flag,这个就比较诡异了,因为理论上 flag 应该随着匿名函数的执行结束销毁了才对

这里 v8 做了处理,当解析脚本的时候,发现这样的情况,会在匿名函数执行阶段将 flag 拷贝到堆中,并且给 drink 函数增加一个 scope 引用。
所以真实的图应该是这样:

从字节码上我们可以看到当 return 的函数使没使用闭包,字节码是截然不同的,如下:

作用域查找的代码在 https://github.com/v8/v8/blob/master/src/ast/scopes.cc#L1975,感兴趣的同学可以自行查阅。

C++ 中的 callable

如果查看 v8 源码的同学,深入到执行 Code 具体执行,发现最后是通过 Adress 类型,而 Adress 就是表示了一个地址,下面是 v8 的 Adress 源码:

那么地址可以执行么?当然可以,看如下 C++ 代码:

我们没有采用显式调用的方式,而是采取了通过函数入口地址来调用,我们来看一下这种方式和直接调用汇编上的差异。

左边是通过地址调用,右边是直接调用,可以看到汇编层面都是 call 命令,只是函数指针是手动获取地址再赋到了寄存器中执行而已。

虽然 C++ 不是函数式编程语言,无法显性的传递函数作为参数,但是我们知道了函数其实就是一个地址,所以可以使用函数指针解决。示例代码很简单就不贴了。

对于 C++ 层面的 callable,那可就广泛了,只要是重载了 operator() 的对象,都可以成为 callable,如下:

我们一般称为这种对象为函数对象,这也是 lambda 表达式的原理,比如下面两个执行方式,原理是一样的。

不过还是 lambda 在写法上方便了很多,而且 lambda 在没有捕获场景下,是可以作为函数指针进行调用的。

第一个 drink 可以正常指定,第二个就不行了,因为拥有捕获的 lambda 表达式是无法转换为函数指针的。

不存在从 "lambda []void ()->void" 到 "callback" 的适当转换函数

对于上面这种情况,可以采用函数包装器模版,我们只需要将上面的代码改成这样就行.

之所以可以这也,是因为 function 只关心你是不是 callable 的,并不在乎你本身是如何 call 的。

总结

简单分析了一下程序中的 callable 对象,如果有什么问题,可以留言讨论,奥力给。

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2021/03/callable-object/

发表评论