相比于其他語言,JavaScript 中的變量可謂獨樹一幟。正如 ECMA-262 所規定的,JavaScript 變量是松散類型的,而且變量不過就是特定時間點一個特定值的名稱而已。由于沒有規則定義變量必須包含什么數據類型,變量的值和數據類型在腳本生命期內可以改變。這樣的變量很有意思,很強大,當然也有不少問題。
? JavaScript 變量可以保存兩種類型的值:原始值和引用值。
? 原始值可能是以下 6 種原始數據類型之 一:Undefined、Null、Boolean、Number、String 和 Symbol。
? 原始值和引用值有以下特點。
- 原始值
- 大小固定,因此保存在棧內存上。
- 從一個變量到另一個變量復制原始值會創建該值的第二個副本。
- typeof 操作符可以確定值的原始類型。
- 引用值
- 是對象,存儲在堆內存上。
- 包含引用值的變量實際上只包含指向相應對象的一個指針,而不是對象本身。
- 從一個變量到另一個變量復制引用值只會復制指針,因此結果是兩個變量都指向同一個對象。
- 而 instanceof 操作符用于確保值的引用類型。
原始值與引用值
? ECMAScript 變量可以包含兩種不同類型的數據:原始值和引用值。原始值(primitive value)就是最簡單的數據,引用值(reference value)則是由多個值構成的對象。
? 在把一個值賦給變量時,JavaScript 引擎必須確定這個值是原始值還是引用值。
? 有6 種 原始值:Undefined、Null、Boolean、Number、String 和 Symbol。保存原始值的變量是按值(by value)訪問的,因為我們操作的就是存儲在變量中的實際值。
? 引用值是保存在內存中的對象。與其他語言不同,JavaScript 不允許直接訪問內存位置,因此也就不能直接操作對象所在的內存空間。在操作對象時,實際上操作的是對該對象的引用(reference)而非實際的對象本身。為此,保存引用值的變量是按引用(by reference)訪問的。
動態屬性
? 原始值和引用值的定義方式很類似,都是創建一個變量,然后給它賦一個值。不過,在變量保存了這個值之后,可以對這個值做什么,則大有不同。
? 對于引用值而言,可以隨時添加、修改和刪除其屬性和方法。比如,看下面的例子:
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"
? 這里,首先創建了一個對象,并把它保存在變量 person 中。然后,給這個對象添加了一個名為name 的屬性,并給這個屬性賦值了一個字符串"Nicholas"。在此之后,就可以訪問這個新屬性,直到對象被銷毀或屬性被顯式地刪除。
? 原始值不能有屬性,盡管嘗試給原始值添加屬性不會報錯。比如:
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
? 在此,代碼想給字符串 name 定義一個 age 屬性并給該屬性賦值 27。緊接著在下一行,屬性不見了。記住,只有引用值可以動態添加后面可以使用的屬性。
注意:原始類型的初始化可以只使用原始字面量形式。如果使用的是 new 關鍵字,則 JavaScript 會創建一個 Object 類型的實例,但其行為類似原始值。下面來看看這兩種初始化方式的差異:
let name1 = "Nicholas";
let name2 = new String("Matt"); name1.age = 27;
name2.age = 26; console.log(name1.age); // undefined
console.log(name2.age); // 26 console.log(typeof name1); // string
console.log(typeof name2); // object
復制值
? 除了存儲方式不同,原始值和引用值在通過變量復制時也有所不同。在通過變量把一個原始值賦值到另一個變量時,原始值會被復制到新變量的位置。請看下面的例子:
let num1 = 5;
let num2 = num1;
? 這里,num1 包含數值 5。當把 num2 初始化為 num1 時,num2 也會得到數值 5。這個值跟存儲在 num1 中的 5 是完全獨立的,因為它是那個值的副本。 這兩個變量可以獨立使用,互不干擾。這個過程如下圖所示。
? 在把引用值從一個變量賦給另一個變量時,存儲在變量中的值也會被復制到新變量所在的位置。區別在于,這里復制的值實際上是一個指針,它指向存儲在堆內存中的對象。操作完成后,兩個變量實際上指向同一個對象,因此一個對象上面的變化會在另一個對象上反映出來,如下面的例子所示:
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "hello";
console.log(obj2.name); // "hello"
? 在這個例子中,變量 obj1 保存了一個新對象的實例。然后,這個值被復制到 obj2,此時兩個變量都指向了同一個對象。在給 obj1 創建屬性 name 并賦值后,通過 obj2 也可以訪問這個屬性,因為它們都指向同一個對象。下圖展示了變量與堆內存中對象之間的關系。
時兩個變量都指向了同一個對象。在給 obj1 創建屬性 name 并賦值后,通過 obj2 也可以訪問這個屬性,因為它們都指向同一個對象。下圖展示了變量與堆內存中對象之間的關系。