前端技術——js 變數、作用域和記憶體問題
阿新 • • 發佈:2019-01-10
js變數、作用域和記憶體問題
基本型別和引用型別的值
基本資料型別的值
- 基本資料型別:
- Undefined、Null、Boolean、Number和String 這5種基本型別是按照值訪問的,因為可以操作儲存在變數中的實際的值。
- 基本型別指的是簡單的資料段。
- 備註:在許多語言中如java 字串以物件的形式來表示的,因此被認為是引用型別的。ECMAScript放棄了這一傳統。
- 對於基本型別的值複製
- 對於基本型別的值複製,只是單純的完成值的copy 如下:
var num1=5;
var num2=num1;
- 在此,num1中儲存了值是5,當使用num1的值來初始化num2時,num2中也儲存了值5。但是num2中的5和num1中的5是完全獨立的,該值只是num1中5的一個副本。此後,這來個變數可以參與任何操作而不會互相影響。
- 傳遞引數
- ECMAScript中所有的引數都是按值傳遞的。也就是說,把函式外部的值複製給函式內部的引數,就喝把值從一個變數複製到另一個變數一樣。基本型別值的傳遞如同基本型別變數的複製一樣,而引用型別值的傳遞,則如同引用型別變數的複製一樣。有不少開發人員在這一點上可能回感到困惑,因為訪問變數有按值傳遞和按引用兩種方式,而引數只能按值傳遞。
function addTen(num){ num +=10; return num;}
var count=20;
var result=addTen(count);
alert(count); //20,沒有變化
alert(result); //30
- 判斷是否是基礎型別
<head> <script> var s = "hello world"; var b = true; var i = 22; var u; var n = null; var o = new Object(); console.log("s 是不是字串:" + isString(s)); console.log("i是不是數字型別:" + isNumber(i)); console.log("b 是不是boolean 型別:" + isBoolean(b)); console.log("u 是不是undefined:" + isUndefined(u)); console.log("n 是不是 null " + isNull(n)); //在js中if條件為null/undefined/0/NaN/""表示式時,統統被解釋為false,此外均為true . //為空判斷函式 function isNull(arg1) { return !arg1 && arg1 !== 0 && typeof arg1 !== "boolean" ? true : false; } function isUndefined(u) { if (typeof u == 'undefined') { return true; } else { return false; } } function isBoolean(b) { if (typeof b == "boolean" && b.constructor == Boolean) { return true; } else { return false; } } function isNumber(num) { if (typeof num == 'number' && num.constructor == Number) { return true; } else { return false; } } function isString(str) { if (typeof s == 'string' && s.constructor == String) { return true; } else { return false; } } function buildUrl() { var qs = "?debug=true"; with (location) { var url = location.href = qs; } console.log(url); } </script> </head> <body></body>
引用資料型別的值
- 引用型別指的是那些可能有多個值構成的物件。
- 定義基本型別值和引用型別值的方式是類似的:建立一個變數併為該變數賦值。但是這個值存到變數中以後,對不同型別的值操作不一樣。對於引用型別的變數可以動態新增屬性。
- 引用型別的可以動態新增屬性,對於基本型別則不可以。我個人理解引用型別就是物件,只有物件才會有屬性。
var person=new Object(); person.name="hello!"
alert(person.name); //hello!
- 引用型值的複製
- 對於引用型值複製,當從一個變數向另一個變數複製引用型別的值時,同樣也會將儲存在變數中的值複製一份到新變數的空間,不同的是,這個值的副本實際上是一個指標,而這個指標指向儲存在堆中的一個物件。複製操作結束後,倆個變數實際上引用同一個物件。因此改變其中一個變數,就會影響到另一個變數:
var obj1=new Object();
var obj2=obj1;
obj1.name="hello"
alert(obj2.name); //hello
- 圖示:
- 引用型別引數的傳遞
- 對於引用型別的引數傳遞也是值的傳遞,只是這個值是指標。如下:
functon setName( obj){ obj.name="hello" ;}
var person =new Object();
setName(person);
alert(person.name); //hello
- 這裡obj和person引用的是同一個物件。換句話說,即使找個物件是按值傳遞的,obj也會按引用來訪問同一個物件。於是,當在函式內部為obj新增name屬性後,函式外部的person也將有所反映。
- 我們再來看一個例子
function setName(obj){ obj.name="hi"; obj=new Object(); obj.name="Greg"; } var person=new Object(); setName(person); alert(person.name); //hi
- 這個例子與前一個例子的唯一區別,就是在setName()函式中添加了倆行程式碼:一行程式碼為obj重新定義了一個物件,另一行程式碼為該物件定義了一個帶有不同值的name屬性。在把person傳遞給setName()後,其name屬性被設定為“hi” 。然後又將一個新物件賦給變數obj,同時將其name屬性設定為“Greg”的新物件。但是,當接下來在訪問person.name時,顯示的值仍然是“hi”。這說明即使在行數內部修改了引數的值,但是原始的引用仍然保持未變。實際上,當在行數內部重寫obj時,這個變數引用的就是一個區域性物件了。而這個區域性物件會在函式執行完畢後立即被銷燬。
檢測型別
- 要檢測一個變數是不是基本資料型別,可以用typeof操作符是最佳的工具。說的更具體一些,typeof操作符是確定一個變數是字串、數值、布林值、還是unfefined的最佳工具。如果變數的值是一個物件或者null 則typeof 操作符會像下面例子中所示的那樣返回Object
var s="nicholas"; var b=true;
var i=22; var u;
var n=null;
var o=new Object();
alert(typeof s); //string
alert(typeof i); //number
alert(typeof b); //boolean
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof o); //object
- 在使用typeof 時只是為了檢測基本資料型別時,但是檢測引用型的值時,這個操作符的用處不大。通常,我們並不是想知道某個值是物件,而是想知道他是什麼型別的物件。為此,ECMAScript提供了instanceof 操作符,其語法如下所示:
result =variable instanceof constructor
- 如果變數是給定引用型別那麼操作符就會返回true。請看下面的例子
alert(person instanceof Object); //變數person 是Object嗎?
alert(colors instanceof Array); //變數colors 是Array嗎?
alert(pattern instanceof RegExp); //變數pattern是RegExp嗎?
- 根據規定,所有引用型別的值都是Object的例項。因此,在檢測一個引用型別值和Objecct建構函式時,instanceof操作符始終會返回true。當然,如果使用instanceof操作符檢測基本型別的值,則該操作符始終會返回false,因為基本型別不是物件。
執行環境和作用域
-
執行環境
- 執行環境是javaScript中最為重要的一個概念。執行環境定義了變數或者函式有權訪問的其他資料,決定了他們各自的行為。每個執行環境都有一個與之關聯的變數物件,環境中定義的所有變數和函式都儲存在這個物件中。雖然我們編寫的程式碼無法訪問這個物件,但解析器在處理資料時會在後臺使用它。
- 全域性執行環境是最外圍的一個執行環境。根據ECMAScript實現所在的宿主環境不同,表示執行環境的物件也不一樣。在Web瀏覽器中,全域性執行環境被認為是Window物件,因此所有全域性變數和函式都是作為window物件的屬性和方法建立的。某個執行環境中的所有程式碼執行完畢後,該環境被銷燬,儲存在其中的所有變數和函式定義也隨之銷燬(全域性執行環境知道應用程式退出——例如關閉網頁或者瀏覽器時才會被銷燬)
- 每個函式都有自己的執行環境。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中 。而在函式執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript程式中的執行流程是有這個方便的機制控制著。
- 當代碼在一個環境中執行時,會建立變數物件的一個作用域鏈。作用域鏈的用途,是保證對執行環境有權訪問的所有變數和函式的有序訪問。作用域鏈的前端,始終是當前執行的程式碼所在環境的變數物件。如果這個環境是函式,則將其活動物件作為變數物件。活動物件在最開始時只包含一個物件,即arguments物件(這個物件在全域性環境中是不存在的)。作用域鏈中的下一個變數物件來自包含外部環境,而再下一個變數物件則來自下一個包含環境。這樣,一直延續到全域性執行環境;全域性執行環境的變數物件始終都是作用域中的最後一個物件。
- 標示符解析是沿著作用域鏈一級一級地搜尋表示符的過程。搜尋過程始終從作用域鏈的前端開始,然後主機向後回溯,直至找到表示符為止(如果找不到標示符,通常會導致錯誤發生)。
- 請看下面的程式碼
-
這個簡單的例子中,函式changeColor()的作用域鏈包含倆個物件:它自己的變數物件(其中定義著arguments物件)和全域性環境的變數物件。可以在函式內部訪問變數color,就是因為可以在這個作用域鏈中找到它。
-
此外,在區域性作用域中定義的變數可以在區域性環境中與全域性變數互換使用,如下面這個例子所示:
-
以上程式碼中共涉及3個執行環境:全域性、changeColor()的區域性環境和swapColors()的區域性環境。全域性環境中有一個變數color和一個函式changeColor()。changeColor()的區域性環境中有一個名為anotherColor的變數和一個名為swapColors()的函式,但是它也可以訪問全域性環境中的變數color 。swapColors()的區域性環境中有一個變數tempColor,該變數只能在這個環境中訪問到。無論全域性環境還是changeColor()的區域性環境都無權訪問tempColor。然而,在swapColors()內部則可以訪問其他來個環境中的所有變數,因為那兩個環境是它的父執行環境。如圖形象的展示了這個例子的作用域鏈。
沒有塊級作用域
- javascript 沒有塊級作用域,經常會導致理解上的困惑。沒有塊級作用域只是針對ES5.對於ES6中增加了let 使其有了塊級作用域。下面的講解只是針對ES5 在其他語言java和c語言中,有花括號封閉的程式碼塊都有自己的作用域,因而支援根據條件定義變數。例如,下面的程式碼在javascript中並不會得到想象中的結果:
if(true){ var color= "bule";} alert(color); //"blue"
- 這裡是一個if 語句中定義了變數color。如果是在c c++或java中,color會在if 語句執行完畢後被銷燬。但是在javascript中,if語句中的變數生命會講變數新增到當前的執行環境(這裡的執行環境是全域性環境)中。在使用for語句時尤其要牢記這一差異,例如:
for(var i=0;i<10;i++){ doSomething(i); } alert(i);//10
- 對於有塊級作用域的語言來說,for語句初始化變數的表示式所定義的變數,只會存在域迴圈的環境中。而對於javaScript來說,有for語句建立的變數i即使在for迴圈執行結束後,也依舊會存在於迴圈外部的執行環境中。
- 1.宣告變數
- 使用var生命的變數會自動被新增到最接近的環境中。在函式內部,最接近的環境就是函式的區域性環境;在with語句中,最接近的環境是函式環境。如果初始化變數時沒有使用var宣告,該變數會自動被新增到全域性環境。如下所示:
function add(num1,num2){ var sum=num1+num2; return sum;} var result=add(10,20); //30
alert(sum); //由於sum不是有效的變數,因此會導致錯誤。
- 以上程式碼中的函式 add()定義了一個名為sum的區域性變數,該變數包含加法操作的結果。雖然結果值從函式中返回了,但變數sum在函式外部是訪問不到的。如果省略這個例子中的var關鍵字,那麼當add()執行完畢後,sum也將訪問到:
function add(num1,num2){ sum=num1+num2; return sum;}
var result=add(10,20); //30
alert(sum); //30
- 這個例子中的變數sum在被初始化賦值時沒有使用var關鍵字。於是,當呼叫完add()之後,新增到全域性環境中的變數sum將繼續存在;即使函式已經執行完畢,後的愛嗎依舊可以訪問它。
垃圾收集
- javaScript 具有自動垃圾收集機制,也就是說,執行環境會負責管理程式碼執行過程中使用的記憶體。而在C和C++之類的語言中,開發人員的一項基本任務就是手工跟蹤記憶體的使用情況,這是造成許多問題的一個根源。在編寫javaScript程式時,開發人員不用在關心記憶體使用問題,所需記憶體的分配以及無用記憶體的回收完全實現了自動管理。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續使用的變數,然後釋放其佔用的記憶體。為此,垃圾收集器會按照固定的時間間隔,週期性地執行這一操作。
- 下面我們來分析一下函式中區域性變數的正常生命週期。區域性變數只在函式執行的過程中存在。而在這過程中,會為區域性變數在棧記憶體上分配相應的空間,以便儲存它們的值。然後在函式中使用這些變數,直至函式執行結束。此時,區域性變數就沒有存在的必要了,因此可以釋放它們的記憶體以共將來使用。在這種情況下,很容易判斷變數是否還有存在的必要了;但並非所有情況都這麼容易就能得出結論。垃圾收集器必須跟蹤哪個變數有用哪個變數沒用,對於不再有用的變數打上標記,已備將來收回其佔用的記憶體。用於標識無用變數的策略可能會因實現而異,但具體到瀏覽器中的實現,則通常有倆個策略。
- 標記清除(主要推薦)
- 引用計數
小結
- javaScript變數可以用來儲存倆種類型的值:基本型別值和引用型別值。基本型別的值源自以下5種基本資料型別:undefined、Null、 Boolean、 Number和String 。基本型別值和引用型別值具有以下特點:
- 基本型別值在記憶體種佔據固定大小的空間,因此被儲存在棧記憶體中。
- 從一個變數向另一個變數複製基本型別的值,會建立這個值的一個副本。
- 引用型別的值是物件,儲存在堆記憶體中。
- 包含引用型別值的變數實際上包含的並不是物件本身,而是一個指向該物件的指標。
- 從一個變數向另一個變數複製引用型別的值,複製的其實是指標,因此倆個變數最終都指向同一個物件。
- 確定一個值是哪種基本型別可以使用typeof 操作符,而確定一個值是哪種引用型別可以使用instanceof操作符。
- 所有變數都存在於一個執行環境(也稱為作用域)當中,這個執行環境決定了變數的生命週期,以及哪一部分程式碼可以訪問其中的變數。以下是關於執行環境的幾點總結:
- 執行環境有全域性執行環境(也稱為全域性環境)和函式執行環境之分。
- 每次進入一個新執行環境,都會建立一個用於搜尋變數和函式的作用域鏈。
- 函式的區域性環境不僅有全訪問函式作用域中的變數,而且有許可權訪問其包含父環境,乃至全域性環境。
- 全域性環境只能訪問在全域性環境中定義的變數和函式,而不能直接訪問區域性環境中的任何 資料。
- 變數的執行環境有助於確定應該何時釋放記憶體。
- JavaScript是一門具有自動垃圾收集機制的程式語言,開發人員不必關心記憶體分配和回收問題。可以對javaScript的垃圾收集例程作如下總結。
- 離開作用域的值將被自動標記為可以回收,因此將在垃圾收集期間被刪除。
- “標記清除”是目前主流的垃圾收集演算法,這個演算法的思想是給當前不實用的值加上標記,然後在其回收其記憶體。
- 另一種垃圾收集演算法是“引用計數”,這種演算法的思想是跟蹤記錄所有值被引用的次數。javaScript引擎目前都不在使用這種演算法;但在ie中訪問非原生javaScript物件(Dom元素)時,這種演算法仍然可能會導致問題。
- 當代碼中存在迴圈引用現象時,“引用計數”演算法就會導致問題。
- 接觸變數的引用不僅有助於消除迴圈引用的現象,而且對垃圾收集也有好處。為了確保有效的回收記憶體,應該及時解除不再使用的全域性物件、全域性物件屬性以及迴圈引用變數的引用。