代碼在內存中的形狀

代碼在內存中的'形狀'

http://zoo.zhengcaiyun.cn/blog/article/code-shape

前言

衆所周知,js 的基本數據類型有 numberstringbooleannullundefined 等。那麼問題來了 typeof nulltypeof undefined 分別是什麼呢?var 、 const 、 let 變量提升?暫時性死區又是什麼東西?以前剛學 js 的時候有人跟我說 === 相比於 == 不僅比較值還要比較類型,難道不是這樣的?

js 引擎與 V8

通常我們說的瀏覽器的內核一般是指支持瀏覽器運行的最核心的程序,分爲兩個部分,也就是渲染引擎JS 引擎。渲染引擎負責解析 HTML,然後進行佈局,渲染等工作。而 js 引擎顧名思義就是解析並且執行 js 代碼的。

一些常見瀏覽器 js 引擎,比方說老版本 IE 使用 Jscript 引擎,而 IE9 之後使用的 Chakra 引擎。safari 使用的是 SquirrelFish 系列引擎。firefox 使用 monkey 系列引擎。chrome 使用 V8 引擎,而且 nodeJs 其實上就是基於 V8 引擎做了進一步封裝。我們今天討論的內容也都是基於 V8 引擎的。

我們知道 js 引擎(V8)在拿到代碼之後,會進行詞法分析,將 js 代碼拆分成對應的 Token,然後再根據 Token 繼續生成對應的 AST,也就是語法分析的過程。而在這一過程中肯定也伴隨着很多的優化策略。有興趣的同學可以閱讀下我們之前的一篇非常不錯的文章《V8 執行 JavaScript 的過程》。在這裏呢,筆者將從 V8 執行代碼過程中實際操作內存的角度來進行進一步的分享。

首先,我們先認識下這個模型:

V8 內存大體上可以分爲:常量池這三大區域,當然其他的一些(甚至比方說 buffer 模塊需要調配更加底層的 C++ 內存)模塊不在本次討論範圍所以沒有體現。圖中清晰的體現了 js 基本數據類型在內存中的存儲情況。


棧內存結構最大的特點就是小且存儲連續,操作起來簡單方便。在 js 中,變量名是用來保存內存中某塊內存區的地址的,而棧區就是用來保存變量名和內存地址的鍵值對的,所以我們就可以通過變量名獲取或者操作某一內存地址上的內容。而 undefined 正是棧空間中表示未定義含義的一塊特殊的固定的內存區域

console.log(b); // undefined
var a;
var b = '政採雲前端團隊';

然而,js 引擎在實際執行代碼之前,會先從上往下依次處理變量提升和函數定義,然後再按序執行。拿以上代碼塊爲例,這一過程在內存中的具體體現就是:

  1. 常量池

顧名思義,常量池就是用來存儲常量的,包括 stringnumberboolean 這三個基本類型的數據。常量池最大的特點就是:

所以這也就是爲什麼 a===b 是 true,因爲 === 比較的是變量 a 和 b 在內存中的指針指向的物理地址是否相等。

var a = '政採雲前端團隊';
var b = '政採雲前端團隊'

相對於棧和池來說,堆的存儲形態會更加複雜。但是從另一個抽象的角度來說,堆區域卻又是最單一的,因爲存放在堆區域的都是 object

typeof {}; // object
typeof []; // object
typeof null; // object
typeof new Date(); // object
typeof new RegExp(); // object

那麼就有人要問了,null 不是基本類型麼,爲什麼 typeof null 又是 object 呢?

其實正如上文對 undefined 的定義那樣,js 引擎對於 null 的基本定義其實是,在堆內存空間中的具有固定內存地址且唯一存在的一個內置對象。所以這就是 nullundefined 本質上的區別所在。

name = '政採雲前端團隊'
var a = {
    name: '政採雲前端團隊'
}
console.log(name === a.name); // true

實際上,堆內存中的情況是非常複雜但又是非常精妙的。比方,上面這小段代碼,執行過程中會在棧中創建 aname 兩個變量。針對於給 a 賦值的這個對象,v8 會在堆區中分配一塊內存區域。並且區域內部依然會有內部的棧區和堆區,這就是精妙的分型思想。而 name === a.name 也側面引證了常量的唯一。

可能就會有細心的小夥伴會發現,圖中還有兩個很大的黃色區域,貌似是和函數有關。並且上文剛剛還說堆區裏面都是 object ?也可能會有一些大佬看到此處會微微一笑,這個人接下來肯定要開始扯什麼 new Function() 。所以 function 從定義的抽象上來說也是 object 了。

  1. 函數

是的,在介紹完基礎且常用的三大區域後,接下來我們來聊一聊函數。但是,我們換個角度,還是回到這個模型上來嘗試去理解一下函數的執行、函數的繼承以及閉包。

上代碼:

function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function () {
    console.log('Animal eat');
};

function Dog(name) {
    Animal.apply(this, arguments);
}

var animal = new Animal();

Dog.prototype = animal;
Dog.prototype.constructor = Dog;
var dog = new Dog();
dog.eat();

console.log(Animal.prototype === animal.__proto__); // true

這是一段比較標準的組合繼承的例子,相信這種代碼片段對大家來說應該再熟悉不過了。那麼這樣的一段代碼的運行過程在實際內存中是什麼樣的一個過程呢?

首先,如下左圖,在代碼執行之前會進行變量提升和函數定義,所以會在變量棧和函數定義區中準備好 objAnimaldog 以及一個不容發現的匿名函數。這裏要注意一個點,就是 var a = function() {}function a(){} 是兩個完全不同的概念,給個眼神自己體會。

並且在函數定義時會,就會創建一個對象空間。函數的 prototype 屬性指向到這個地址,這就是函數的原型對象。同時對象內存空間的內部又將會劃分出棧結構空間和堆結構空間。娃,又套上了~

後續會在賦值語句時,將 Animal.proptotype.eat 指向到匿名函數。

至此,變量定義、函數定義以及賦值操作這些基礎的過程已經梳理完成。

我們發現,new Animal()new Dog() 的這部分剛剛並沒有提到。因爲它們還要特殊,我們繼續深入。

如上右圖,其實,js 在執行 var animal = new Animal(); 這種 new 操作符的時候,js 引擎會在棧空間的函數緩存區中創建一塊空間用於保存該函數運行所需要存儲的狀態和變量。空間中有一個 __proto__ 屬性指向到構造函數的 prototype ,也就是圖中的 Animal.prototype。這也就從內存的角度解釋了爲什麼 Animal.prototype === animal.__proto__ 會是 true

實際上,在 new Animal() 執行完之後,本來 GC 就會清除掉函數的緩存區內存,釋放空間。但是由於我們定義了一個 obj 變量,這個變量的內存地址是指向到這塊緩存區,所以阻止了 GC 對這塊內存的回收。這種問題在閉包問題中尤爲典型。我們可以通過定義一個變量來阻止 GC 回收已經運行完的閉包函數的緩存區內存塊,從而達到保護閉包內部的狀態。然後在我們希望釋放閉包空間的時候,將該變量置爲 null ,從而在下一個 GC 週期時釋放該內存區域。

function fn() {
  var a = 1;
  return function () {
    console.log(a);
  };
}
var onj = fn();

最後,我們通過 Dog.prototype = animal; ,將 Dog 的原型指向到了緩存區中的白色區域。我們可以通過打印 Dog.prototype === animalDog.prototype.__proto__ ===Animal.prototype以及 dog.__proto__ === animal 的方式來驗證圖中的指向關係。這也就是原型繼承在具體內存模型中的過程。

總結

在代碼的學習過程中,難免會覺得枯燥,而且有很多內容抽象難懂。強行死記硬背,不去知其所以然的話容易瞭解片面甚至理解錯誤,更何況也非常沒有樂趣。藉助於這種看得見摸得着的模型去理解和分析代碼實際運行的情況會幫助理解,並且能夠發現其中的設計精妙之處。

文中最後部分多次提及到 GC,其實 GC 的模型設計的也是非常巧妙,非常有意思的。可以移步至《V8 引擎垃圾回收與內存分配》繼續閱讀。有興趣的同學可以嘗試將 GC 的模型和這個 V8 內存模型結合在一起去思考下代碼運行和回收的全過程。而且 GC 還只是管理堆空間的垃圾回收,那麼棧空間又是以什麼方式進行自我回收的呢?還有很多很多有趣的東西值得我們思考~

參考文檔

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OqFlCvmAu1AD4okRDZc-9w