首頁  >  文章  >  web前端  >  前端進階(一):記憶體空間圖解

前端進階(一):記憶體空間圖解

PHPz
PHPz原創
2017-04-04 17:23:491166瀏覽

前端進階(一):記憶體空間圖解

變數物件與堆記憶體

var a = 20;
var b = 'abc';
var c = true;
var d = { m: 20 }

因為JavaScript#有自動垃圾回收機制,所以對於前端開發來說,記憶體空間並不是一個常被提及的概念,很容易被大家忽略。特別是很多不是電腦專業的朋友在進入到前端之後,會對記憶體空間的認知比較模糊,甚至有些人乾脆就是一無所知。

當然也包括我自己。在很長一段時間裡認為記憶體空間的概念在JS的學習中並不是那麼重要。但後我當我回過頭來重新整理JS基礎時,發現由於對它們的模糊認知,導致了很多東西我都理解得併不明白。例如最基本的引用資料型別和引用傳遞到底是怎麼回事?例如淺複製與深複製有什麼不同?還有閉包,原型等等。

因此後來我才漸漸明白,想要對JS的理解更加深刻,就必須對記憶體空間有一個清晰的認知。

一、堆疊與堆疊

註:棧,也可以叫堆疊

與C/C++不同,JavaScript中並沒有嚴格意義上區分棧記憶體與堆疊記憶體。因此我們可以粗淺的理解為JavaScript的所有資料都保存在堆記憶體中。但在某些場景,我們仍然需要基於堆疊資料結構的想法來處理,例如JavaScript的執行上下文(關於執行上下文我會在下一篇文章中總結)。執行上下文在邏輯上實作了堆疊。因此理解堆疊資料結構的原理與特性任然十分重要。

要簡單理解堆疊的存取方式,我們可以透過類比乒乓球盒來分析。如下圖左側。

前端進階(一):記憶體空間圖解

乒乓球盒子與堆疊類比

這種乒乓球的存放方式與堆疊中存取資料的方式如出一轍。處於盒子中最頂層的乒乓球5,它一定是最後被放進去,但可以最先被使用。而我們想要使用底層的乒乓球1,就必須將上面的4個乒乓球取出來,讓乒乓球1處於盒子頂層。這就是堆疊空間先進後出,後進先出的特徵。圖中已經詳細的顯示了棧空間的儲存原理。

堆存取資料的方式,則與書架與書非常相似。

書雖然也整齊的存放在書架上,但是我們只要知道書的名字,我們就可以很方便的取出我們想要的書,而不用像從乒乓球盒子裡取乒乓一樣,非得將上面的所有乒乓球拿出來才能取到中間的某一個乒乓球。好比在JSON格式的資料中,我們儲存的<a href="http://www.php.cn/wiki/1051.html" target="_blank">key</a>-value是可以無序的,因為順序的差異並不影響我們的使用,我們只需要關心書的名字。

二、變數物件與基礎資料型別

JavaScript的執行上下文產生之後,會建立一個叫做變數物件的特殊物件(具體會在下一篇文章與執行上下文一起總結), JavaScript的基礎資料型別往往會保存在變數物件中。

嚴格意義上來說,變數物件也是存放在堆記憶體中,但是由於變數物件的特殊職能,我們在理解時仍然需要將其於堆記憶體區分開來。

基礎資料型態都是一些簡單的資料段,JavaScript中有5中基礎資料類型,分別是Undefined、<a href="http://www.php.cn/wiki/62.html" target="_blank">Null</a>、Boolean、Number、<a href="http://www.php.cn/wiki/57.html" target="_blank">String</a>。基礎資料類型都是按值訪問,因為我們可以直接操作保存在變數中的實際的值。

三、引用資料型別與堆記憶體

與其他語言不通,JS的引用資料型,例如陣列Array,它們值的大小是不固定的。引用資料型別的值是保存在堆記憶體中的物件。 JavaScript不允許直接存取堆記憶體中的位置,因此我們不能直接操作物件的堆記憶體空間。在操作物件時,實際上是在操作物件的參考而不是實際的物件。因此,引用類型的值都是按引用存取的。這裡的引用,我們可以粗淺地理解為保存在變數物件中的一個位址,該位址與堆疊記憶體的實際值相關聯。

为了更好的搞懂变量对象与堆内存,我们可以结合以下例子与图解进行理解。

var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中

前端進階(一):記憶體空間圖解

上例图解

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

理解了JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的一些特点了。

在前端面试中我们常常会遇到这样一个类似的题目

// demo01.js
var a = 20;
var b = a;
b = 30;

// 这时a的值是多少?
// demo02.js
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;

// 这时m.a的值是多少

在变量对象中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。

前端進階(一):記憶體空間圖解

demo01图解

在demo02中,我们通过var n = m执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在变量对象中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在变量对象中访问到的具体对象实际上是同一个。如图所示。

因此当我改变n时,m也发生了变化。这就是引用类型的特性。

前端進階(一):記憶體空間圖解

demo02图解

通过内存的角度来理解,是不是感觉要轻松很多。除此之外,我们还可以以此为基础,一步一步的理解JavaScript的执行上下文,作用域链,闭包,原型链等重要概念。其他的我会在以后的文章慢慢总结,敬请期待。

内存空间管理

因为JavaScript具有自动垃圾收集机制,所以我们在开发时好像不用关心内存的使用问题,内存的分配与回收都完全实现了自动管理。但是根据我自己的开发经验,了解内存机制有助于自己清晰的认识到自己写的代码在执行过程中发生过什么,从而写出性能更加优秀的代码。因此关心内存是一件非常重要的事情。

JavaScript的内存生命周期

1. 分配你所需要的内存
2. 使用分配到的内存(读、写)
3. 不需要时将其释放、归还

为了便于理解,我们使用一个简单的例子来解释这个周期。

var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100);  // 使用内存
a = null; // 使用完毕之后,释放内存空间

第一步和第二步我们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。

JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。

在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此a = null其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。

  • 在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。

  • 要详细了解垃圾收集机制,建议阅读《JavaScript高级编程》中的4.3节


以上是前端進階(一):記憶體空間圖解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn