把2016年寒假寫的對《JavaScript高級程序設計》的筆記寫在博客上,同時回看加修改,同時也更新到簡書上。盡量一天一篇一章。
<h2>第四章 變量、作用域和內存問題</h2>
<h3>4.1基本類型和引用類型的值</h3>
ECMAScript變量可能包含兩個不同類型數據的值:基本類型值和引用類型值。
基本類型值指的是簡單的數據段(Boolean類型、Number類型、String類型、Undefined、Null) 引用類型值指那些可能由多個值構成的對象(Object類型、Array類型、Date類型、RegExp類型、Function類型)
ES6中新增了Symbol,是JavaScript的第七種數據類型。
<h4>4.1.1動態的屬性</h4>
基本類型值和引用類型值的區別一:對于引用類型值,我們可以為其添加或刪除屬性和方法,但是基本類型值沒有屬性和方法。
例子:
var person = new Object(); var name = "Nicholas"
person.name = "Nicholas"; name.age = 27;
alert(person.name); //"Nicholas" alert(name.age) //undefined
以上代碼一創建了一個對象并給他一個name屬性,又通過alert訪問成功。代碼二給字符串name定義了一個age屬性,但當我們訪問的時候會發現這個屬性不存在。這說明只能給引用類型值動態地添加屬性,以便將來使用。
<h4>4.1.2復制變量值</h4>
基本類型值和引用類型值的區別二:在復制變量的時候,復制基本類型值和引用類型值也是有區別的。如果只是復制基本類型值,那就是簡單復制到為新變量分配的位置上沒毛病。當復制的是引用類型的值時,同樣會將存儲在變量對象中的值復制一份放到為新變量分配的空間中。不同的是,這個新的副本實際上是一個指針,復制結束后,兩個變量實際上將引用同一個對象。因此,如果復制的是引用類型值,當改變其中一個變量,就會影響另一個變量。例子:
var obj1 = new Object();
var obj2 = obj1;
obj1.name="Nicholas";
alert(obj2.name); //"Nicholas"
變量對象中的變量保存在堆中的對象之間的關系如圖:

圖片來自《JavaScript高級程序設計》
可以看到當變量復制后,指針仍然指向一開始的Object,而不是復制出多一個Object。.
<h4>4.1.3傳遞參數</h4>
ECMAScript中所有函數的參數都是按值傳遞的。(無論參數是引用類型值和基本類型值)。也就是說,把函數外部的值復制給函數內部的參數,就和4.1.2復制變量值的原理一樣,把一個變量復制到另一個變量(函數的參數)一樣。有不少開發人員在這點會感到困惑,因為訪問變量有按值和按引用兩種方式,而參數只能按值傳遞。
--------------------------討論參數傳遞的是引用類型值的情況--------------------------
function setName(obj){
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
以上代碼創建了一個對象person,這個變量被傳遞到setName()函數中后被復制給了obj,在這個函數內部,obj和person引用的是同一個對象。換句話說,即使這個變量是按值傳遞的,obj也會按引用來訪問同一個對象(遵循4.1.2的復制變量值原理)。于是當為函數內部為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"
這段代碼增加了兩行,為obj重新定義了一個對象,第二行為該對象定義了一個帶有不同值的name屬性。如果person是按引用傳遞的,那么person最后會被自動修改為指向其name屬性值為“Greg”的新對象。但是在函數外訪問person.name時,顯示的值仍然是"Nicholas"。這說明即使在函數內部修改了參數的值,但原始的引用仍然保持未變。實際上,當在函數內部重寫obj時,這個變量引用的就是一個局部對象了(這個函數范圍的局部對象)。而這個局部對象會在函數執行完畢后立即被銷毀。
<h4>4.1.4檢測類型</h4>
檢測變量類型的方法有兩種,一種是檢測基本類型值的,用typeof,另一種是檢測引用類型值的,用instanceof。
typeof操作符是確定一個變量是string,boolean,number,undefined的最佳工具,如果變量是對象(根據規定,所有引用類型的值都是Object的實例)或null,則typeof操作符返回的值會是“object”。例子:
var s = "Nicholas", b = true, i = 22, u , n = null, o = new Object(), d = new Date();
alert(typeof s); //string
alert(typeof i); //number
alert(typeof b); //boolean
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof i); //object
alert(typeof d); //object
因為typeof只能檢測基本類型值,檢測引用類型值時只會返回object,所以ECMAScript又提供了一個insanceof操作符,用法跟typeof不同,且只返回true 或 false。
<div style="border: 1px solid #ccc; padding:10px"><strong>?另類的情況</strong>
使用typeof操作符檢測函數時,該操作符會返回"function"。在Safari 5 及之前版本和Chrome 7及之前的版本中使用typeof檢測正則表達式時,由于規范的原因,這個操作符也返回“function”。ECMA-262規定任何在內部實現 [ [ call ] ] 方法的對象都應該在應用typeof操作符時返回“function“。由于上述瀏覽器(Safari 5,Chrome 7)中的正則表達式也實現了這個方法,因此對正則表達式應用typeof會返回“function”。在IE和Firefox中,對正則表達式應用typeof會返回“object”.</div>
如果變量是給定引用類型(根據它的原型鏈來識別,第6章將介紹原型鏈。#原書句)的實例那么instanceof操作符就會返回true。例子:
alert(person instanceof Object); //變量person是Object嗎?
alert(colors instanceof Array); //變量colors是Array嗎?
alert(pattern instanceof RegExp); //變量pattern是RegExp嗎?
//親測左右兩邊位置不可互換,互換不會出現提示框
因為根據規定,所有引用類型的值都是Object的實例,因此把Date,Array,RegExp等引用類型值用instanceof 與Object驗證時,始終都會返回true。用instanceof操作符檢測基本類型值時,該操作符時鐘返回false,因為基本類型不是對象。
<h3>4.2執行環境及作用域</h3>
作用域鏈重要的一點就是內部執行環境可以使用其外部環境的變量和函數,并且可以改變那個變量的值,只要那個變量不是被當作參數傳進去的而是直接使用的。(當作參數傳入的是按值傳遞,改變的是復制出來的變量,不會改變原來的變量)
執行環境(execution context)和作用域其實超級簡單。每個執行環境都有一個與之關聯的變量對象(variable object),環境變量中定義的所有變量和函數都保存在這個對象中。但是我們無法用代碼訪問到這個變量對象。但解析器在處理數據時會在后臺使用它。
全局執行環境是最外圍的執行環境。根據ECMAScript實現所在的宿主環境不同,表示執行環境的對象也不一樣。在Web瀏覽器中,全局執行環境被認為是window對象(第七章將詳細討論),因此,所有全局變量和函數都是作為window對象的屬性和方法創建的(window對象是個變量對象,全局變量和函數是它的屬性和方法)。某個執行環境(例如一個函數)中的所有代碼執行完畢后,該環境被銷毀,保存在其中的所有變量和函數定義也隨之銷毀(全局執行環境直到應用程序退出(網頁關閉或瀏覽器關閉時才被銷毀))
在Node.js中的全局執行環境是global
每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之后,棧將其環境彈出,把控制權交給之前的執行環境。ECMAScript程序中的執行流正是由這個方便的機制在控制。
當代碼在其中一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的最前端,始終都是當前執行代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象(activation object)作為變量對象。活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中不存在)。作用域鏈中的下一個變量對象來自包含它的外部環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延伸到全局執行環境;
標識符解析是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程從作用域鏈最前端開始,然后逐級向后回溯,直到找到標識符為止(如果找不到,就會發生錯誤)
例子:
var color = "blue";
function changeColor(){
if(color == "blue"){
color = "red";
}
}
changeColor();
alert(color); // "red"
在這個例子中,函數changeColor( )的作用域鏈包含兩個對象:它自己的變量對象(其中定義著arguments對象)和全局環境的變量對象。可以在函數內部訪問變量color,就是因為可以在這個作用域鏈中找到它。
此外,在局部作用域中定義的變量可以在局部環境中與全局變量互換使用,如下面這個例子所示:
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();```
以上代碼涉及三個執行環境:全局環境、changeColor()的局部環境和swapColor() 的局部環境。swapColor的局部變量中有一個變量tempColor,該變量只有在swapColor環境中能訪問到,但是swapColor()內部可以訪問其他兩個環境中的所有變量。
越在內部的局部環境,作用域鏈越長。對于這個例子中的swapColor()而言,其作用域鏈中包含3個對象:swapColor( )的變量對象、changeColor()的變量對象和全局對象。swapColor()的局部環境開始時會現在自己的變量對象中搜索變量和函數名,如果搜索不到則再搜搜上一級作用域鏈。changeColor()的作用域鏈中只包含兩個對象:它自己的變量對象和全局對象。也就是說,它不能訪問swapColor()的環境。
<h4>4.2.1延長作用域</h4>
有些語句可以在作用域鏈前端臨時增加一個變量對象,該變量對象會在代碼執行后被移除。在兩種情況下會發生這種現象,具體來說,就是當執行流執行到下列任何一個語句時,作用域鏈會得到增長
- try-catch語句的catch塊
- with語句
這兩個語句都會在作用域鏈的前端添加一個變量對象。對with語句來說,會將指定的對象添加到作用域鏈中,對catch語句來說,會創建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。例子:
function buildUrl(){
var qs = "?debug=true";
with(location){
var url = href + qs;
}
return url;
}
?添加一個with語句塊的知識點</strong><br>當在with語句塊中使用方法或者變量時,程序會檢查該方法是否是本地函數,變量是否是已定義變量,如果不是,它將檢查偽對象(with的參數),看它是否為該對象的方法,屬性。如上面例子with語句塊中的href,本地無定義,則with語句塊會自動加上location.href,所以href實際上為href。這個就是with的功能。with 語句是運行緩慢的代碼塊,尤其是在已設置了屬性值時。大多數情況下,如果可能,最好避免使用它。
<br>
在此,with語句接收的是Location對象,因此其變量對象中就包含了location對象的所有屬性和方法,而這個變量對象被添加到了作用域鏈的最前端,buildUrl()函數中定義了一個變量qs。當在with語句中引用變量href時(實際引用的是location.href)。可以在當前執行環境的變量對象中找到。當引用變量qs時,引用的則是在buildUrl( )中定義的那個變量,而該變量位于函數環境的變量對象中。至于with語句的內部,則定義了一個名為url的變量,因而url就成了函數執行環境的一部分,所以可以作為函數的值被返回。
<h4>4.2.2沒有塊級作用域</h4>
<strong>?添塊級作用域</strong><br>任何一對花括號中的語句都屬于一個塊,在這之中定義的所有變量在代碼塊之外都是不可見的,我們稱之為塊級作用域。
<br>
**作用域有兩種,塊級作用域和函數作用域**
講到這就好理解。JS沒塊級作用域就是說在for循環和if語句塊中定義的變量是可見的,可以被外部使用的,但像其他的語言Java,C,C#語言中,在for,if語句中定義的變量在語句執行完畢之后就會被**銷毀**。但在JavaScript中,if語句中的變量聲明會將變量添加到當前執行環境中。注意只是當前執行環境,如果for循環是在一個函數里,則定義的i在函數里是確定的數,在全局環境中仍然是not defined。例子:
```if(true){
var color = "blue";
}
alert(color) //"blue"
for(var i=0; i<0; i++){<br>
dosomething;<br>}
alert(i) //10<br>
function add(){
for (var i = 0; i<10; i++){<br>
dosomething;
}<br>
alert(i); //10<br>
}
add();<br>
alert(i); // **全局環境中 i is not defined**
但是ECMA2015有辦法解決沒有塊級作用域這個問題,就是用 let 或 const代替 var 去聲明 i 。這樣 i 就只會在for循環中被聲明,for循環結束之后就會被銷毀。
1、聲明變量
使用var聲明的變量會自動添加到最接近的環境中。在函數內部,最接近的環境就是函數局部環境。在with語句中,最接近環境是函數環境。如果初始化變量時沒有使用var聲明,該變量會自動被添加到全局環境。在編寫JavaScript的過程中,不聲明而直接初始化變量是一個常見的錯誤做法,不建議這樣做,在嚴格模式下,初始化未經聲明的變量會導致錯誤。
2、JavaScript的查詢標識符機制
就是當要讀取或寫入一個標識符時,會通過搜索來確定標識符,搜索過程會從作用域鏈的最前端開始,向上逐級查找,如果在局部環境中找到了該標識符,則搜索過程停止。如果一直找到全局環境仍未找到這個標識符,則意味著該變量未聲明。
<h3>4.3垃圾收集</h3>
JavaScript具有自動垃圾收集機制,執行環境會負責管理代碼執行過程中使用的內存。而在C和C++之類的語言中,開發人員的一項基本任務就是手工跟蹤內存的使用情況。編寫JavaScript的開發人員就不用關心內存使用的問題。所需內存的分配以及無用內存的回收完全實現了自動管理。
垃圾收集機制的原理很簡單: 把再也不用的變量找出來,刪除。為此,垃圾收集器會按照固定的時間間隔周期性地執行這一操作。
具體到瀏覽器中,通常有兩個垃圾收集策略。
<h4>4.3.1標記清除</h4>
IE、Firefox、Opera、Chrome、和Safari的JavaScript實現使用的都是標記清除式的垃圾收集策略,只不過垃圾收集的時間間隔不同。
標記清除(mark-and-sweep)原理:當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量表記為“進入環境”。被這么標記的變量邏輯上應該隨時可能使用到,所以是不會刪除的。而當變量離開環境時,則將其標記為“離開環境”。垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記,然后,它會去掉環境中的變量以及被環境中的變量引用的變量的標記。而在此之后再被加上標記的變量將被視為準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最后,垃圾收集器完成內存清除工作,銷毀那些被標記的值并回收他們所占用的內存空間。
<h4>4.3.2引用計數</h4>
這種垃圾收集策略不太常用。
引用計數的原理簡單來說就是每引用一次該變量就對引用次數加一,當引用的變量被替換掉就對引用次數減一。當引用次數變為0就將該變量占用的內存回收回來。
但因為這種機制在循環引用時有BUG,所以現在已經不用。而仍然提到這個垃圾回收機制是因為IE8及以前的瀏覽器中有一部對象并不是原生JavaScript對象。例如其BOM和DOM中的對象就是使用C++以COM(component Object Model 組件對象模型)對象的形式實現的,而COM對象的垃圾收集機制采用的就是引用技術策略。因此,即使IE的JavaScript引擎是使用標記清除策略實現的,但JavaScript訪問的COM對象依然是基于引用計數策略的。(但其實只要不涉及循環引用,引用技術策略就不會有問題)
下面的例子展示了使用COM對象導致的循環引用的問題:
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;
這個例子在一個DOM元素(element)與一個原生JavaScript對象(myObject)之間創建了循環引用。其中,變量myObject有一個名為element的屬性指向element對象;而變量element也有個屬性名叫someObject回指myObject。由于存在循環引用,即使將例子中的DOM從頁面中移除,它也永遠不會被回收。
為了避免類似這樣的循環問題,最好是在不使用他們的時候手工斷開原生JavaScript對象與DOM元素之間的連接。例如這樣消除前面例子創建的循環引用:
myObject.element = null;
element.someObject = null;
將變量設置為null意味著切斷變量與它此前引用的值之間的連接。當垃圾收集器下次運行時,就會刪除這些值并回收它們占用的內存。
IE9已經把BOM和DOM對象都轉換成了真正的JavaScript對象。這樣,就避免了兩種垃圾收集算法并存導致的問題,也消除了常見的內存泄漏問題。所以這是個IE8及之前的問題了。
<h4>4.3.3性能問題</h4>
性能問題就是各瀏覽器的垃圾收集機制問題,跟開發人員關系不大,雖然可以手工啟動垃圾收集機制但作者不建議我們這么做。
IE中調用window.CollectGarbage( );Opera 7 及更高版本中調用windo.opera.collect( )會立即執行垃圾收集。
<h4>4.3.4管理內存</h4>
有垃圾收集機制的JavaScript在編寫時一般不用操心內存管理問題。但是JavaScript在進行內存管理和垃圾收集時面臨的問題還是有點與眾不同。其中一個主要的問題,就是分配給Web瀏覽器的可用內存數量通常比分配給桌面應用程序的少,這樣是處于安全考慮,防止瀏覽器占用全部系統內存導致系統崩潰。內存限制問題不僅會影響給變量分配內存,同時還會影響調用棧以及在一個線程中能夠同時執行的語句數量。
因此,確保占用最少的內存可以讓頁面獲得更好的性能(頁面占用內存越少,性能越好)。優化內存的最佳方式,就是為執行中的代碼只保存必要的數據。一旦數據不再有用,就通過將其設置為null來釋放其引用——這個做法叫做解除引用(dereferencing)。這一做法適用于大多數全局變量和全局對象的屬性。局部變量本來就會在離開執行環境后自動解除引用,如下面這個例子所示:
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("Nicholas");<br>
//手工解除globalPerson的引用
globalPerson = null;```
在這個例子中,變量globalPerson取得了createPerson函數返回的值,由于函數內的局部變量在離開執行環境后就**自動解除引用**,所以不用我們顯式地解除引用,但對于全局變量globalPerson,在我們不需要它之后,我們可以**手工解除引用**。不是說解除了引用后該值就會被馬上回收,解除引用的作用是讓值脫離執行環境,在垃圾收集器下次運行時就會將其回收。