本章內(nèi)容
- 理解基本類型和引用類型的值
- 理解執(zhí)行環(huán)境
- 理解垃圾收集
由于不存在定義某個變量必須要保存何種數(shù)據(jù)類型值的規(guī)則,變量的值及其數(shù)據(jù)類型可以在腳本的生命周期內(nèi)改變隘梨。
4.1 基本類型和引用類型的值
基本類型值指的是簡單的數(shù)據(jù)段程癌,而引用類型值指那些可能由多個值構成的對象。
在將一個值賦給變量時轴猎,解析器必須確定這個值是基本類型值還是引用類型值嵌莉。第3章討論了5種基本數(shù)據(jù)類型:Undefined
、Null
捻脖、Boolean
烦秩、Number
、String
郎仆。這5種基本數(shù)據(jù)類型是按值訪問的,因為可以操作保存在變量中的實際的值兜蠕。
引用類型的值是保存在內(nèi)存中的對象扰肌。與其他語言不同,JavaScript不允許直接訪問內(nèi)存中的位置熊杨,也就是說不能直接操作對象的內(nèi)存空間曙旭。在操作對象時盗舰,實際上是在操作對象的引用而不是實際的對象。為此桂躏,引用類型的值是按引用訪問的钻趋。
在很多語言中,字符串以對象的形式來表示剂习,因此被認為是引用類型的蛮位。ECMAScript放棄了這一傳統(tǒng)。
4.1.1 動態(tài)的屬性
對于引用類型的值鳞绕,我們可以為其添加屬性和方法失仁,也可以改變和刪除其屬性和方法。
var person = new Object();
person.name = "Nicholas";
alert(person.name); //"Nicholas"
但是们何,我們不能給基本類型的值添加屬性萄焦,盡管這樣做不會導致任何錯誤。
var name = "Nicholas";
name.age = 27;
alert(name.age); //undefined
4.1.2 復制變量值
如果從一個變量向另一個變量復制基本類型的值冤竹,會在變量對象上創(chuàng)建一個新值拂封,然后把該值復制到為新變量分配的位置上。
var num1 = 5;
var num2 = num1;
當從一個變量向另一個變量復制引用類型的值時鹦蠕,同樣也會將存儲在變量對象中的值復制一份放到為新變量分配的空間中冒签。不同的是,這個值的副本實際上是一個指針片部,而這個指針指向存儲在堆中的一個對象镣衡。復制操作結束后,兩個變量實際上將引用同一個對象档悠。因此廊鸥,改變其中一個變量,就會影響另一個變量辖所。
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); //"Nicholas"
4.1.3 傳遞參數(shù)
ECMAScript中所有函數(shù)的參數(shù)都是按值傳遞的惰说。訪問變量有按值和按引用兩種方式,而參數(shù)只能按值傳遞缘回。
在向參數(shù)傳遞基本類型的值時吆视,被傳遞的值會被復制給一個局部變量(即命名參數(shù),或者用ECMAScript的概念來說酥宴,就是arguments
對象中的一個元素)啦吧。在向參數(shù)傳遞引用類型的值時,會把這個值在內(nèi)存中的地址復制給一個局部變量拙寡,因此這個局部變量的變化會反映在函數(shù)的外部授滓。
function addTen(num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
alert(count); //20, no changes
alert(result); //30
這里的函數(shù)addTen()
有一個參數(shù)num
,而參數(shù)實際上是函數(shù)的局部變量。在調(diào)用這個函數(shù)時般堆,變量count
作為參數(shù)被傳遞給函數(shù)在孝。在函數(shù)內(nèi)部,參數(shù)num
的值被加上了10淮摔,但這一變化不會影響函數(shù)外部的count
變量私沮。假如num
是按引用傳遞的話,那么變量count
的值也將變成30和橙。如果使用對象:
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); //"NIcholas"
對象person
被傳遞到setName()
函數(shù)中之后就被復制給了obj
仔燕。即使這個對象是按值傳遞的,obj
也會按引用來訪問同一個對象胃碾。當在函數(shù)內(nèi)部為obj
添加name
屬性后涨享,函數(shù)外部的person
也將有所反映;有很多開發(fā)人員錯誤地認為:在局部作用域中修改的對象會在全局作用域反映出來仆百,就說明參數(shù)是按引用傳遞的厕隧。為了證明對象是按值傳遞的,例子:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
如果person
是按引用傳遞的俄周,那么person
就會自動被修改為指向name
屬性值為Greg
的新對象吁讨。但是person.name
顯示的值仍然是Nicholas
。這說明即使在函數(shù)內(nèi)部修改了這個參數(shù)的值峦朗,但原始的引用仍然保持未變建丧。實際上,當在函數(shù)內(nèi)部重寫obj
時波势,這個變量引用的就是一個局部對象了翎朱。而這個局部對象會在函數(shù)執(zhí)行完畢后立即被銷毀。
可以把ECMAScript函數(shù)的參數(shù)想象成局部變量尺铣。
4.1.4 檢測類型
typeof
操作符是確定一個變量是字符串拴曲、數(shù)值、布爾值凛忿,還是undefined
的最佳工具澈灼。如果變量的值是一個對象或null
,則typeof
操作符會像下面例子中所示的那樣返回object
:
var s = "Nicholas";
var a = true;
var i = 22;
var u;
var n = null;
var o = new Object();
alert(typeof s); //string
alert(typeof i); //number
alert(typeof a); //boolean
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof o); //object
通常店溢,我們并不是想知道某個值是對象叁熔,而是想知道它是什么類型的對象。為此床牧,ECMAScript提供了instanceof
操作符荣回。
result = variable instanceof constructor
如果變量是給定引用類型的實例,那么instanceof
操作符就會返回true
戈咳。
alert(person instanceof Object); //變量person是Object嗎心软?
alert(colors instanceof Array); //變量colors是Array嗎革砸?
alert(pattern instanceof RegExp); //變量pattern是RegExp嗎?
根據(jù)規(guī)定糯累,所有引用類型的值都是Object
的實例。在檢測一個引用類型值和Object
構造函數(shù)時册踩,instanceof
操作符始終會返回true
泳姐。當然,如果使用instance
操作符檢測基本類型的值暂吉,則該操作符會返回false
胖秒,因為基本類型不是對象。
4.2 執(zhí)行環(huán)境及作用域
執(zhí)行環(huán)境是JavaScript中最為重要的一個概念慕的。執(zhí)行環(huán)境定義了變量或函數(shù)有權訪問的其他數(shù)據(jù)阎肝,決定了它們各自的行為。每個執(zhí)行環(huán)境都有一個與之關聯(lián)的變量對象肮街,環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中风题。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數(shù)據(jù)時會在后臺使用它嫉父。
全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境沛硅。根據(jù)ECMAScript實現(xiàn)所在宿主環(huán)境不同,表示執(zhí)行環(huán)境的對象也不一樣绕辖。在Web瀏覽器中摇肌,全局執(zhí)行環(huán)境被認為是window
對象,因此所有全局變量和函數(shù)都是作為window
對象的屬性和方法創(chuàng)建的仪际。某個執(zhí)行環(huán)境中的所有代碼執(zhí)行完畢后围小,該環(huán)境被銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀(全局執(zhí)行環(huán)境直到應用程序退出——例如關閉網(wǎng)頁或瀏覽器——時才會被銷毀)树碱。
每個函數(shù)都有自己的執(zhí)行環(huán)境肯适。當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入一個環(huán)境棧中赴恨。而在函數(shù)執(zhí)行之后疹娶,棧將其環(huán)境彈出,把控制權返回給之前的執(zhí)行環(huán)境伦连。
當代碼在一個環(huán)境中執(zhí)行時雨饺,會創(chuàng)建變量對象的一個作用域鏈,用來保證對執(zhí)行環(huán)境有權訪問的所有變量和函數(shù)的有序訪問惑淳。作用域鏈的前端额港,始終都是當前執(zhí)行的代碼所在環(huán)境的變量對象。全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象歧焦。
標識符解析是沿著作用域鏈一級一級地搜素標識符的過程移斩。搜索過程始終從作用域鏈的前端開始肚医,然后逐級地向后回溯,直至找到標識符為止向瓷。
var color = "blue";
function changeColor() {
if (color === "blur") {
color = "red";
} else {
color = "blur";
}
}
changeColor();
alert("Color is now " + color);
函數(shù)changeColor()
的作用域鏈包含兩個對象:它自己的變量對象和全局環(huán)境的變量對象肠套。可以在函數(shù)內(nèi)部訪問變量color
猖任,就是因為可以在這個作用域鏈中找到它你稚。
此外,在局部作用域中定義的變量可以在局部環(huán)境中與全局變量互換使用朱躺,如下所示:
var color = "blue";
function changeColor() {
var anotherColor = "red";
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//這里可以訪問color刁赖、anotherColor和tempColor
}
//這里可以訪問color和anotherColor,但不能訪問tempColor swapColors();
}
//這里只能訪問color changeColor();
以上代碼共涉及3個執(zhí)行環(huán)境:全局環(huán)境长搀、changeColor()
的局部環(huán)境和swapColors()
的局部環(huán)境宇弛。全局環(huán)境中有一個變量color
和一個函數(shù)changeColor()
。changeColor()
的局部環(huán)境中有一個名為anotherColor
的變量和一個名為swapColors()
的函數(shù)源请,但它也可以訪問全局環(huán)境中的變量color
枪芒。swapColors()
的局部環(huán)境中有一個變量tempColor
,該變量只能在這個環(huán)境中訪問到巢钓。無論全局環(huán)境還是changeColor()
的局部環(huán)境都無權訪問tempColor
病苗。然而,在swapColors()
內(nèi)部則可以訪問其他兩個環(huán)境中的所有變量症汹,因為那兩個環(huán)境是它的父執(zhí)行環(huán)境。
函數(shù)參數(shù)也被當作變量來對待,因此其訪問規(guī)則與執(zhí)行環(huán)境中的其他變量相同破婆。
4.2.1 延長作用域鏈
當執(zhí)行流進入下列任何一個語句時,作用域鏈就會得到加長:
-
try-catch
語句的catch
塊裳扯; -
with
語句饰豺。
這兩個語句都會在作用域鏈的前端添加一個變量對象冤吨。對with
語句來說漩蟆,會將指定的對象添加到作用域鏈中怠李。對catch
語句來說扔仓,會創(chuàng)建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明版保。
function buildUrl() {
var qs = "?debug=true";
with(location) {
var url = href + qs;
}
return url;
}
4.2.2 沒有塊級作用域
JavaScript沒有塊級作用域經(jīng)常會導致理解上的困惑。
- 聲明變量
使用var
聲明的變量會自動被添加到最接近的環(huán)境中。如果初始化變量時沒有使用var
聲明森篷,該變量會自動被添加到全局環(huán)境。
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
var result = add(10, 20); //30
alert(sum); //由于sum不是有效的變量钓辆,因此會導致錯誤
function add(num1, num2) {
sum = num1 + num2;
return sum;
}
var result = add(10, 20); //30
alert(sum); //30
- 查詢標識符
當在某個環(huán)境中為了讀取或寫入而引用一個標識符時抖韩,必須通過搜索來確定該標識符實際代表什么双谆。搜索過程從作用域鏈的前端開始顽馋,向上逐級查詢與給定名字匹配的標識符谓厘。如果在局部環(huán)境中找到了該標識符,搜索過程停止寸谜,變量就緒竟稳。如果在局部環(huán)境中沒有找到該變量名,則繼續(xù)沿作用域鏈向上搜索熊痴。搜索過程將一直追溯到全局環(huán)境的變量對象他爸。如果在全局環(huán)境中也沒有找到這個標識符,則意味著該變量尚未聲明果善。
在這個搜索過程中诊笤,如果存在一個局部的變量的定義,則搜索會自動停止巾陕,不再進入另一個變量對象讨跟。換句話說,如果局部環(huán)境中存在著同名標識符鄙煤,就不會使用位于父環(huán)境中的標識符晾匠。
var color = "blue";
function getColor() {
var color = "red";
return color;
}
alert(getColor()); //"red"
alert(window.color); //"blue"
變量查詢也并不是沒有代價的。很明顯梯刚,訪問局部變量要比訪問全局變量更快凉馆,因為不用向上搜索作用域鏈。
4.3 垃圾收集
JavaScript具有自動垃圾收集機制亡资。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續(xù)使用的變量句喜,然后釋放其占用的內(nèi)存。為此沟于,垃圾收集器會按照固定的時間間隔周期性地執(zhí)行這一操作咳胃。
4.3.1 標記清除
JavaScript中最常用的垃圾收集方式是標記清除。當變量進入環(huán)境時標記為“進入環(huán)境”旷太,離開環(huán)境時標記“離開環(huán)境”展懈。
垃圾收集器在運行的時候會給存儲在內(nèi)存中的所有變量都加上標記。然后供璧,去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標記存崖。而在此之后再被加上標記的變量將被視為準備刪除的變量。
4.3.2 引用計數(shù)
另一種不太常見的垃圾手機策略叫做引用計數(shù)睡毒。存在循環(huán)引用問題来惧。
4.3.3 性能問題
在有的瀏覽器中可以觸發(fā)垃圾收集過程,但我們不建議這樣做演顾。
4.3.4 管理內(nèi)存
確保占用最少的內(nèi)存可以讓頁面獲得更好的性能供搀。一旦數(shù)據(jù)不再有用隅居,最好通過將其值設置為null
來釋放其引用——這個做法叫做解除引用。這一做法適用于大多數(shù)全局變量和全局對象的屬性葛虐。局部變量會在它們離開執(zhí)行環(huán)境時自動被解除引用胎源。
function createPerson(name) {
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("Nicholas");
//手工解除globalPerson的引用
globalperson = null;
解除引用的真正作用是讓值脫離執(zhí)行環(huán)境,以便垃圾收集器下次運行時將其回收屿脐。
4.4 小結
JavaScript變量可以用來保存兩種類型的值:基本類型和引用類型涕蚤。基本類型和引用類型值具有以下特點:
- 基本類型值在內(nèi)存中占據(jù)固定大小的空間的诵,因此被保存在棧內(nèi)存中万栅;
- 從一個變量向另一個變量復制基本類型的值,會創(chuàng)建這個值的一個副本西疤;
- 引用類型的值是對象申钩,保存在堆內(nèi)存中;
- 包含引用類型值的變量實際上包含的并不是對象本身瘪阁,而是一個指向該對象的指針;
- 從一個變量向另一個變量復制引用類型的值邮偎,復制的其實是指針管跺,因此兩個變量最終都指向同一個對象;
確定一個值是那種基本類型可以使用typeof
操作符禾进,而確定一個值是哪種引用類型可以使用instanceof
操作符豁跑。
所有變量都(基本類型和引用類型)存在于一個執(zhí)行環(huán)境(也稱作用域)當中,這個執(zhí)行環(huán)境決定了變量的生命周期泻云,以及那一部分代碼可以訪問其中的變量艇拍。
- 執(zhí)行環(huán)境有全局執(zhí)行環(huán)境和函數(shù)執(zhí)行環(huán)境之分;
- 每次進入一個新執(zhí)行環(huán)境宠纯,都會創(chuàng)建一個用于搜素變量和函數(shù)的作用域鏈卸夕;
- 函數(shù)的局部環(huán)境不僅有權訪問函數(shù)作用域中的變量,而且有權訪問其包含環(huán)境婆瓜,乃至全局環(huán)境快集;
- 全局環(huán)境只能訪問在全局環(huán)境中定義的變量和函數(shù),而不能直接訪問局部環(huán)境中的任何數(shù)據(jù)廉白;
- 變量的執(zhí)行環(huán)境有助于確定應該何時釋放內(nèi)存个初。
JavaScript是一門具有自動垃圾收集機制的編程語言。
- 離開作用域的值將被自動標記為可以回收猴蹂,因此將在垃圾收集期間被刪除院溺。
- “標記清除”是目前主流的垃圾收集算法,思想是給當前不使用的值加上標記磅轻,然后再回收其內(nèi)存珍逸。
- 另一種是“引用計數(shù)”逐虚,思想是跟蹤記錄所有值被引用的次數(shù)。JavaScript引擎目前都不再使用這種算法弄息。
- 當代碼中存在循環(huán)引用現(xiàn)象時痊班,“引用計數(shù)”算法就會導致問題。
- 解除變量的引用不僅有助于消除循環(huán)引用現(xiàn)象摹量,而且對垃圾收集也有好處涤伐。應該及時解除不再使用的全局對象、全局對象屬性以及循環(huán)引用變量的引用缨称。