這博客是越來越難寫了,參考資料少,難度又高,看到什麼寫什麼吧! 眾多周知,在JavaScript中有幾個基本類型,包括字元串、數字、布爾、null、undefined、Symbol,其中大部分都可以在我之前那篇博客(https://www.cnblogs.com/QH-Jimmy/p/9212923 ...
這博客是越來越難寫了,參考資料少,難度又高,看到什麼寫什麼吧!
眾多周知,在JavaScript中有幾個基本類型,包括字元串、數字、布爾、null、undefined、Symbol,其中大部分都可以在我之前那篇博客(https://www.cnblogs.com/QH-Jimmy/p/9212923.html)中找到,均繼承於Primitive類。但是仔細看會發現少了兩個,null和undefined呢?這一節,就來探索一下,V8引擎是如何處理null、undefined兩種類型的。
在沒有看源碼之前,我以為是這樣的:
class Null : public Primitive { public: // Type testing. bool IsNull() const { return true; } // ... }
然而實際上沒有這麼簡單粗暴,V8對null、undefined(實際上還包括了true、false、空字元串)都做了特殊的處理。
回到故事的起點,是我在研究LoadEnvironment函數的時候發現的。上一篇博客其實就是在講這個方法,包裝完函數名、函數體,最後一步就是配合函數參數來執行函數了,代碼如下:
// Bootstrap internal loaders Local<Value> bootstrapped_loaders; if (!ExecuteBootstrapper(env, loaders_bootstrapper, arraysize(loaders_bootstrapper_args), loaders_bootstrapper_args, &bootstrapped_loaders)) { return; }
這裡的參數分別為:
1、env => 當前V8引擎的環境變數,包含Isolate、context等。
2、loaders_bootstrapper => 函數體
3、arraysize(loaders_bootstrapper_args) => 參數長度,就是4
4、loaders_bootstrapper_args => 參數數組,包括process對象及3個C++內部方法
5、&bootstrapped_loaders => 一個局部變數指針
參數是啥並不重要,進入方法,源碼如下:
static bool ExecuteBootstrapper(Environment* env, Local<Function> bootstrapper, int argc, Local<Value> argv[], Local<Value>* out) { bool ret = bootstrapper->Call( env->context(), Null(env->isolate()), argc, argv).ToLocal(out); if (!ret) { env->async_hooks()->clear_async_id_stack(); } return ret; }
看起來就像JS裡面的call方法,其中函數參數包括context、null、形參數量、形參,當時看到Null覺得比較好奇,就仔細的看了一下實現。
這個方法其實很簡單,但是實現的方式非常有意思,源碼如下:
Local<Primitive> Null(Isolate* isolate) { typedef internal::Object* S; typedef internal::Internals I; // 檢測當前V8引擎實例是否存活 I::CheckInitialized(isolate); // 核心方法 S* slot = I::GetRoot(isolate, I::kNullValueRootIndex); // 類型強轉 直接是Primitive類而不是繼承 return Local<Primitive>(reinterpret_cast<Primitive*>(slot)); }
只有GetRoot是真正生成null值的地方,註意第二個參數 I::kNullValueRootIndex ,這是一個靜態整形值,除去null還有其他幾個,所有的類似值定義如下:
static const int kUndefinedValueRootIndex = 4; static const int kTheHoleValueRootIndex = 5; static const int kNullValueRootIndex = 6; static const int kTrueValueRootIndex = 7; static const int kFalseValueRootIndex = 8; static const int kEmptyStringRootIndex = 9;
上面的數字就是區分這幾個類型的關鍵所在,繼續進入GetRoot方法:
V8_INLINE static internal::Object** GetRoot(v8::Isolate* isolate,int index) { // 獲取當前isolate地址併進行必要的空間指針偏移 // static const int kIsolateRootsOffset = kExternalMemoryLimitOffset + kApiInt64Size + kApiInt64Size + kApiPointerSize + kApiPointerSize; uint8_t* addr = reinterpret_cast<uint8_t*>(isolate) + kIsolateRootsOffset; // 根據上面的數字以及當前操作系統指針大小進行偏移 // const int kApiPointerSize = sizeof(void*); // NOLINT return reinterpret_cast<internal::Object**>(addr + index * kApiPointerSize); }
這個方法就對應了標題,指針偏移。
實際上根本不存在一個正規的null類來生成一個對應的對象,而只是把一個特定的地址當成一個null值。
敢於用這個方法,是因為對於每一個V8引擎來說isolate對象是獨一無二的,所以在當前引擎下,獲取到的isolate地址也是唯一的。
如果還不明白,我這個靈魂畫手會讓你明白,超級簡單:
最後返回一個地址,這個地址就是null,強轉成Local<Primitive>也只是為了垃圾回收與類型區分,實際上並不關心這個指針指向什麼,因為null本身不存在任何方法可以調用,大多數情況下也只是用來做變數重置。
就這樣,只用了很小的空間便生成了一個null值,並且每一次獲取都會返回同一個值。
驗證的話就很簡單了,隨意的在node啟動代碼裡加一段:
auto test = Null(env->isolate());
然後看局部變數的調試框,當前isolate的地址如下:
第一次指針偏移後,addr的地址為:
通過簡單計算,這個差值是72(16進位的48),跟第一次偏移量大小一致,這裡根本不關心指針指向什麼東西,所以字元無效也沒事。
第二次偏移後,得到的null地址為:
通過計算得到差值為48(16進位的30),算一算,剛好是6*8。
最後對這個地址進行強轉,返回一個Local<Primitive>類型的null對象。