【每日一學】變量、作用域和內存問題

本章內容

1.理解基本類型和引用類型的值

2.理解執行環境

3.理解垃圾收集

按照 ECMA-262 的定義,JavaScript 的變量與其他語言的變量有很大區別。JavaScript 變量鬆散類型的本質,決定了它只是在特定時間用於保存特定值的一個名字而已。由於不存在定義某個變量必須要保存何種數據類型值的規則,變量的值及其數據類型可以在腳本的生命週期內改變。儘管從某種角度看,這可能是一個既有趣又強大,同時又容易出問題的特性,但 JavaScript 變量實際的複雜程度還遠不止如此。


4.1 基本類型和引用類型的值


ECMAScript 變量可能包含兩種不同數據類型的值:基本類型值和引用類型值。基本類型值指的是簡單的數據段,而引用類型值指那些可能由多個值構成的對象。

在將一個值賦給變量時,解析器必須確定這個值是基本類型值還是引用類型值。第 3 章討論了 5 種基本數據類型:Undefined、Null、Boolean、Number 和 String。這 5 種基本數據類型是按值訪問的,因為可以操作保存在變量中的實際的值。

引用類型的值是保存在內存中的對象。與其他語言不同,JavaScript 不允許直接訪問內存中的位置,也就是說不能直接操作對象的內存空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。為此,引用類型的值是按引用訪問的①。


在很多語言中,字符串以對象的形式來表示,因此被認為是引用類型的。

ECMAScript 放棄了這一傳統。


4.1.1 動態的屬性

定義基本類型值和引用類型值的方式是類似的:創建一個變量併為該變量賦值。但是,當這個值保存到變量中以後,對不同類型值可以執行的操作則大相徑庭。對於引用類型的值,我們可以為其添加屬性和方法,也可以改變和刪除其屬性和方法。請看下面的例子:

var person = new Object();

person.name = "Nicholas";

alert(person.name); //"Nicholas"


以上代碼創建了一個對象並將其保存在了變量 person 中。然後,我們為該對象添加了一個名為

name 的屬性,並將字符串值"Nicholas"賦給了這個屬性。緊接著,又通過 alert()函數訪問了這個新屬性。如果對象不被銷燬或者這個屬性不被刪除,則這個屬性將一直存在。

但是,我們不能給基本類型的值添加屬性,儘管這樣做不會導致任何錯誤。比如:

var name = "Nicholas";

name.age = 27;

alert(name.age); //undefined

在這個例子中,我們為字符串 name 定義了一個名為 age 的屬性,併為該屬性賦值 27。但在下一行訪問這個屬性時,發現該屬性不見了。這說明只能給引用類型值動態地添加屬性,以便將來使用。


4.1.2 複製變量值


除了保存的方式不同之外,在從一個變量向另一個變量複製基本類型值和引用類型值時,也存在不同。如果從一個變量向另一個變量複製基本類型的值,會在變量對象上創建一個新值,然後把該值複製到為新變量分配的位置上。來看一個例子


var num1 = 5;

var num2 = num1;

在此,num1 中保存的值是 5。當使用 num1 的值來初始化 num2 時,num2 中也保存了值 5。但 num2中的 5 與 num1 中的 5 是完全獨立的,該值只是 num1 中 5 的一個副本。此後,這兩個變量可以參與任何操作而不會相互影響。圖 4-1 形象地展示了複製基本類型值的過程。


當從一個變量向另一個變量複製引用類型的值時,同樣也會將存儲在變量對象中的值複製一份放到為新變量分配的空間中。不同的是,這個值的副本實際上是一個指針,而這個指針指向存儲在堆中的一個對象。複製操作結束後,兩個變量實際上將引用同一個對象。因此,改變其中一個變量,就會影響另一個變量,如下面的例子所示:


var obj1 = new Object();

var obj2 = obj1;

obj1.name = "Nicholas";

alert(obj2.name); //"Nicholas"


首先,變量 obj1 保存了一個對象的新實例。然後,這個值被複制到了 obj2 中;換句話說,obj1

和 obj2 都指向同一個對象。這樣,當為 obj1 添加 name 屬性後,可以通過 obj2 來訪問這個屬性,因為這兩個變量引用的都是同一個對象。圖 4-2 展示了保存在變量對象中的變量和保存在堆中的對象之間的這種關係。


4.1.3 傳遞參數


ECMAScript 中所有函數的參數都是按值傳遞的。也就是說,把函數外部的值複製給函數內部的參數,就和把值從一個變量複製到另一個變量一樣。基本類型值的傳遞如同基本類型變量的複製一樣,而引用類型值的傳遞,則如同引用類型變量的複製一樣。有不少開發人員在這一點上可能會感到困惑,因為訪問變量有按值和按引用兩種方式,而參數只能按值傳遞。


在向參數傳遞基本類型的值時,被傳遞的值會被複制給一個局部變量(即命名參數,或者用

ECMAScript 的概念來說,就是 arguments 對象中的一個元素)。在向參數傳遞引用類型的值時,會把這個值在內存中的地址複製給一個局部變量,因此這個局部變量的變化會反映在函數的外部。請看下面這個例子:


function addTen(num) {

num += 10;

return num;

}

var count = 20;

var result = addTen(count);

alert(count); //20,沒有變化

alert(result); //30


這裡的函數 addTen()有一個參數 num,而參數實際上是函數的局部變量。在調用這個函數時,變量count作為參數被傳遞給函數,這個變量的值是20。於是,數值20被複制給參數num以便在addTen()中使用。在函數內部,參數 num 的值被加上了 10,但這一變化不會影響函數外部的 count 變量。參數num 與變量 count 互不相識,它們僅僅是具有相同的值。假如 num 是按引用傳遞的話,那麼變量 count的值也將變成 30,從而反映函數內部的修改。當然,使用數值等基本類型值來說明按值傳遞參數比較簡單,但如果使用對象,那問題就不怎麼好理解了。再舉一個例子:


function setName(obj) {

obj.name = "Nicholas";

}

var person = new Object();

setName(person);

alert(person.name); //"Nicholas"

以上代碼中創建一個對象,並將其保存在了變量 person 中。然後,這個變量被傳遞到 setName()函數中之後就被複制給了 obj。在這個函數內部,obj 和 person 引用的是同一個對象。換句話說,即使這個變量是按值傳遞的,obj 也會按引用來訪問同一個對象。於是,當在函數內部為 obj 添加 name屬性後,函數外部的 person 也將有所反映;因為 person 指向的對象在堆內存中只有一個,而且是全局對象。有很多開發人員錯誤地認為:在局部作用域中修改的對象會在全局作用域中反映出來,就說明參數是按引用傳遞的。為了證明對象是按值傳遞的,我們再看一看下面這個經過修改的例子:


function setName(obj) {

obj.name = "Nicholas";

obj = new Object();

obj.name = "Greg";

}

var person = new Object();

setName(person);

alert(person.name); //"Nicholas"

這個例子與前一個例子的唯一區別,就是在 setName()函數中添加了兩行代碼:一行代碼為 obj

重新定義了一個對象,另一行代碼為該對象定義了一個帶有不同值的 name 屬性。在把 person 傳遞給setName()後,其 name 屬性被設置為"Nicholas"。然後,又將一個新對象賦給變量 obj,同時將其 name屬性設置為"Greg"。如果 person 是按引用傳遞的,那麼 person 就會自動被修改為指向其 name 屬性值為"Greg"的新對象。但是,當接下來再訪問 person.name 時,顯示的值仍然是"Nicholas"。這說明即使在函數內部修改了參數的值,但原始的引用仍然保持未變。實際上,當在函數內部重寫 obj 時,這個變量引用的就是一個局部對象了。而這個局部對象會在函數執行完畢後立即被銷燬。


4.1.4 檢測類型


要檢測一個變量是不是基本數據類型?第 3 章介紹的 typeof 操作符是最佳的工具。說得更具體一點,typeof 操作符是確定一個變量是字符串、數值、布爾值,還是 undefined 的最佳工具。如果變量的值是一個對象或 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


如果變量是給定引用類型(根據它的原型鏈來識別;第 6 章將介紹原型鏈)的實例,那麼

instanceof 操作符就會返回 true。請看下面的例子:


alert(person instanceof Object); // 變量 person 是 Object 嗎?

alert(colors instanceof Array); // 變量 colors 是 Array 嗎?

alert(pattern instanceof RegExp); // 變量 pattern 是 RegExp 嗎?


根據規定,所有引用類型的值都是 Object 的實例。因此,在檢測一個引用類型值和 Object 構造

函數時,instanceof 操作符始終會返回 true。當然,如果使用 instanceof 操作符檢測基本類型的值,則該操作符始終會返回 false,因為基本類型不是對象。


4.2.1 延長作用域鏈

雖然執行環境的類型總共只有兩種——全局和局部(函數),但還是有其他辦法來延長作用域鏈。這麼說是因為有些語句可以在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執行後被移除。在兩種情況下會發生這種現象。具體來說,就是當執行流進入下列任何一個語句時,作用域鏈就會得到加長:

try-catch 語句的 catch 塊;

with 語句。


4.2.2 沒有塊級作用域


JavaScript 沒有塊級作用域經常會導致理解上的困惑。在其他類 C 的語言中,由花括號封閉的代碼塊都有自己的作用域(如果用 ECMAScript 的話來講,就是它們自己的執行環境),因而支持根據條件來定義變量。




分享到:


相關文章: