TAT.vorshen callable-object
In 未分類 on 2021年03月14日 by view: 4,176
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.ecomenagepro.com/2021/03/callable-object/

發表評論