訂閱
糾錯
加入自媒體

帶你解讀JavaScript中的變量、作用域和內存問題

一、基本類型和引用類型的值

基本類型值:簡單的數(shù)據(jù)段;

引用類型值:多個值構成的對象;

回顧:

基本數(shù)據(jù)類型:undefined;null;number;boolean;string;按照值訪問的,可以操作保存在變量中的實際的值;

引用數(shù)據(jù)類型:例如Array;不能直接訪問值,它是保存在內存中的對象;

JavaScript不允許直接訪問內存中的位置;即不能直接操作對象的內存空間;

我們在操作對象時,其實是操作對象的引用,而不是對象;

注意:如果我們復制保存著某個對象的變量時,那么兩個變量就會指向同一個對象,當我們?yōu)閷ο筇砑訉傩詴r,操作的就是實際的對象;

1.1 動態(tài)的屬性

引用類型

var person = new Object() // 創(chuàng)建一個對象person.name = '張三' // 設置對象屬性console.log(person.name) // 輸出對象屬性

這個屬性會一直伴隨著對象,除非對象銷毀,否則該屬性會一直存在;

基本類型

var name = 'Nick'name.age = 20console.log(name.age) // undefined

只有引用值可以動態(tài)添加后面可以使用的屬性;

1.2 復制變量值

基本類型

var s = 'hello'var s1 = sconsole.log(s1) // 'hello'console.log(s1 == s) // true

解釋:

再新創(chuàng)建一個變量s1,它的值和s一樣,都是字符型'hello',所以s1 == s;兩者完全獨立,互不干擾;

引用類型

var obj1 = new Object()var obj2 = obj1obj1.name = 'nick'console.log(obj2.name)console.log(obj2 == obj1)

圖示:

我們的變量名obj1儲存的是一個對象的引用,它指向堆里面的一個對象(object),通過復制,我們只是復制了一個變量obj2,它的指向和obj1一樣都是指向object,所以設置完obj1.name = 'nick',之后修改的是指向的對象的屬性,由于obj2也是指向這個對象,所以obj2.name = 'nick';

1.3 傳遞參數(shù)

函數(shù)的傳參類似于我們變量的復制,我們來查看一下;

1.3.1 基本類型的傳參

image.png

解釋:參數(shù)作為函數(shù)的局部變量,其實并不會對全局變量造成影響,所以count還是20;

1.3.2 引用類型的傳參

image.png

解釋:此處obj和obj1引用的是同一個對象;那么問題來了,針對于引用類型,參數(shù)的傳遞是按照值還是按照引用呢?看下面的例子:

image.png

這里如果是按照引用傳遞,obj1的指向應該變成函數(shù)內部創(chuàng)建的對象,并且其age值為21,但是實際輸出為20,說明即使在函數(shù)內部修改了參數(shù)的值,其原始引用仍未改變;

函數(shù)內部創(chuàng)建的obj會隨著函數(shù)調用結束而被銷毀;

二、作用域

2.1 執(zhí)行環(huán)境和作用域

執(zhí)行環(huán)境: 定義了變量或函數(shù)有權訪問的其它數(shù)據(jù),決定了它們的行為。

全局執(zhí)行環(huán)境是最外層的執(zhí)行環(huán)境。根據(jù) ECMAScript實現(xiàn)的宿主環(huán)境,表示全局執(zhí)行環(huán)境的對象可能不一樣。在瀏覽器中,全局執(zhí)行環(huán)境就是我們常說的 window 對象。

執(zhí)行環(huán)境中的代碼在執(zhí)行的時候,會創(chuàng)建變量對象的一個作用域鏈(scope chain)。這個作用域鏈決定了各級上下文中的代碼在訪問變量和函數(shù)時的順序。

代碼正在執(zhí)行的執(zhí)行環(huán)境的變量對象始終位于作用域鏈的最前端。如果上下文是函數(shù),則其活動對象(activation object)用作變量對象。活動對象最初只有一個定義變量:arguments 。(全局執(zhí)行環(huán)境中沒有這個變量。)

作用域鏈中的下一個變量對象來自包含執(zhí)行環(huán)境,再下一個對象來自再下一個包含執(zhí)行環(huán)境。以此類推直至全局執(zhí)行環(huán)境;全局執(zhí)行環(huán)境的變量對象始終是作用域鏈的最后一個變量對象。

代碼執(zhí)行時的標識符解析是通過沿作用域鏈逐級搜索標識符名稱完成的。搜索過程始終從作用域鏈的最前端開始,然后逐級往后,直到找到標識符。(如果沒有找到標識符,那么通常會報錯。)

image.png

以上代碼涉及 3 個執(zhí)行環(huán)境:全局執(zhí)行環(huán)境、 changeColor() 的局部執(zhí)行環(huán)境和 swapColors() 的局部執(zhí)行環(huán)境。全局執(zhí)行環(huán)境中有一個變量 color 和一個函數(shù) changeColor() 。changeColor() 的局部執(zhí)行環(huán)境中有一個變量 anotherColor 和一個函數(shù) swapColors() ,但在這里可以訪問全局上下文中的變量 color 。其它函數(shù)同理;

2.2 延長作用域鏈

雖然執(zhí)行環(huán)境主要有全局環(huán)境和局部環(huán)境兩種,但有其他方式來延長作用域鏈。某些語句會導致在作用域鏈前端臨時添加一個變量對象,這個對象在代碼執(zhí)行后會被刪除。通常在兩種情況下會出現(xiàn)這個現(xiàn)象,即代碼執(zhí)行到下面任意一種情況時:

try / catch 語句的 catch 塊;

with 語句;

這兩種情況下,都會在作用域鏈前端添加一個變量對象。對 with 語句來說,會向作用域鏈前端添加指定的對象;對 catch 語句而言,則會創(chuàng)建一個新的變量對象,這個變量對象會包含要拋出的錯誤對象的聲明。如下所示:

image.png

這里, with 語句接收 location 對象,因此 location 會被添加到作用域鏈前端。buildUrl() 函數(shù)中定義了一個變量 qs 。當 with 語句中的代碼引用變量 href 時,實際上引用的是location.href ,也就是自己變量對象的屬性。在引用 qs 時,引用的則是定義在buildUrl() 中的那個變量,它位于函數(shù)環(huán)境的變量對象中;至于with語句內部,則定義了一個url的變量,因而url變成函數(shù)執(zhí)行環(huán)境的一部分,可以作為函數(shù)的值被返回;

2.3 沒有塊級作用域

image.png

這里我們很疑惑,這個color在{}中,不應該是局部變量嗎?為什么在全局中也能夠輸出;

解釋:在這里if語句聲明的變量將會添加到當前的執(zhí)行環(huán)境(即全局環(huán)境),使用for語句也是一樣;

image.png

聲明變量

使用var聲明的變量會被自動添加到最接近的環(huán)境中,在函數(shù)內部聲明,最接近的環(huán)境就是函數(shù)的局部環(huán)境;在with語句中,最接近的環(huán)境就是函數(shù)環(huán)境;如果沒有使用var聲明變量,那么就會自動添加到全局環(huán)境中;

image.png

這里原因就不做過多解釋了,但是如果我們在該函數(shù)內部省略var,直接聲明sum,那么在函數(shù)外部也是可以輸出sum的,因為此時他就是一個全局變量;

在JavaScript中,不聲明而直接初始化變量是一種錯誤做法;

三、垃圾回收

3.1 垃圾回收機制

JavaScript 是使用垃圾回收的語言,也就是說執(zhí)行環(huán)境負責在代碼執(zhí)行時管理內存。JavaScript 通過自動內存管理實現(xiàn)內存分配和閑置資源回收。

基本過程:確定某個變量不會再使用,然后釋放它占用的內存。

這個過程是周期性的,即垃圾回收程序每隔一定時間就會自動運行。垃圾回收過程是一個近似且不完美的方案,因為某塊內存是否還有用,屬于“不可判定的”問題,意味著靠算法是解決不了的。

3.2 性能問題

垃圾回收程序會周期性運行,如果內存中分配了很多變量,則可能造成性能損失,因此垃圾回收的時間調度很重要。尤其是在內存有限的移動設備上,垃圾回收有可能會明顯拖慢渲染的速度和幀速率。

現(xiàn)代垃圾回收程序會基于對 JavaScript 運行時環(huán)境的探測來決定何時運行。探測機制因引擎而異,但基本上都是根據(jù)已分配對象的大小和數(shù)量來判斷的。

由于調度垃圾回收程序方面的問題會導致性能下降,它的策略是根據(jù)分配數(shù),比如分配了 256 個變量、4096 個對象/數(shù)組字面量和數(shù)組槽位(slot),或者 64KB 字符串。只要滿足其中某個條件,垃圾回收程序就會運行。

這樣實現(xiàn)的問題在于,分配那么多變量的腳本,很可能在其整個生命周期內始終需要那么多變量,結果就會導致垃圾回收程序過于頻繁地運行。

由于對性能的嚴重影響,IE7最終更新了垃圾回收程序。IE7 發(fā)布后,JavaScript 引擎的垃圾回收程序被調優(yōu)為動態(tài)改變分配變量、字面量或數(shù)組槽位等會觸發(fā)垃圾回收的閾值。IE7 的起始閾值都與 IE6 的相同。如果垃圾回收程序回收的內存不到已分配的 15%,這些變量、字面量或數(shù)組槽位的閾值就會翻倍。如果有一次回收的內存達到已分配的 85%,則閾值重置為默認值。這么一個簡單的修改,極大地提升了重度依賴 JavaScript 的網頁在瀏覽器中的性能。

3.3 管理內存

為什么需要管理內存?

在使用垃圾回收的編程環(huán)境中,JavaScript 運行在一個內存管理與垃圾回收都很特殊的環(huán)境。分配給瀏覽器的內存通常比分配給桌面軟件的要少很多,分配給移動瀏覽器的就更少了。這更多出于安全考慮而不是別的,就是為了避免運行大量 JavaScript 的網頁耗盡系統(tǒng)內存而導致操作系統(tǒng)崩潰。這個內存限制不僅影響變量分配,也影響調用棧以及能夠同時在一個線程中執(zhí)行的語句數(shù)量。

接觸引用

將內存占用量保持在一個較小的值可以讓頁面性能更好。優(yōu)化內存占用的最佳手段就是保證在執(zhí)行代碼時只保存必要的數(shù)據(jù)。如果數(shù)據(jù)不再必要,那么把它設置為 null ,從而釋放其引用。

局部變量在超出作用域后會被自動解除引用,如下所示:

image.png

在上面的代碼中,變量 globalPerson 保存著 createPerson() 函數(shù)調用返回的值。在 createPerson()內部, localPerson 創(chuàng)建了一個對象并給它添加了一個 name 屬性。然后, localPerson 作為函數(shù)值被返回,并被賦值給 globalPerson 。localPerson 在 createPerson() 執(zhí)行完成超出執(zhí)行環(huán)境后會自動被解除引用,不需要顯式處理。但 globalPerson 是一個全局變量,應該在不再需要時手動解除其引用,最后一行就是這么做的。不過要注意,解除對一個值的引用并不會自動導致相關內存被回收。解除引用的關鍵在于確保相關的值已經不在執(zhí)行環(huán)境里了,因此它在下次垃圾回收時會被回收。

聲明: 本文由入駐維科號的作者撰寫,觀點僅代表作者本人,不代表OFweek立場。如有侵權或其他問題,請聯(lián)系舉報。

發(fā)表評論

0條評論,0人參與

請輸入評論內容...

請輸入評論/評論長度6~500個字

您提交的評論過于頻繁,請輸入驗證碼繼續(xù)

暫無評論

暫無評論

人工智能 獵頭職位 更多
掃碼關注公眾號
OFweek人工智能網
獲取更多精彩內容
文章糾錯
x
*文字標題:
*糾錯內容:
聯(lián)系郵箱:
*驗 證 碼:

粵公網安備 44030502002758號