細說JavaScript中的變量,作用域和垃圾回收
在 JavaScript 中,數(shù)據(jù)類型可分為基本類型和引用類型,
基本類型有六種:Null,Undefined,String,Boolean,Number,Symbol;
而引用類型就是傳說中的 Object 了。
其中基本類型是按值傳遞,而引用類型的值是按引用訪問的,所以在操作對象時,實際上是在操作對象的引用而不是實際的對象 ( ps:在為對象添加屬性時,操作的是實際的對象 )。
關(guān)于基本類型和引用類型的不同,大概有以下幾點:
1、引用類型是動態(tài)的屬性,而基本類型不是。
對于引用類型,我們可以為其添加、刪除屬性和方法,但不能給基本類型的值添加屬性:
// 基本類型 var name = 'Fly_001'; name.age = 22; alert(name.age); // undefined; // 引用類型 var person = new Object(); person.name = 'Fly_001'; alert(person.name); // 'Fly_001';
2、復(fù)制的方式不同。
如果從一個變量向另一個變量復(fù)制基本類型的值,會將值復(fù)制到為新變量分配的位置上:
var num1 = 5; var num2 = num1;
當使用 num1 的值來初始化 num2 時,num2 中也保存了值5,但該值只是 num1 中 5 的一個副本,兩個變量不會互相影響。
當從一個變量向另一個變量復(fù)制引用類型的值時,傳遞的是一個指針,其指向存儲在堆中的一個對象,在復(fù)制結(jié)束后,兩個變量實際上將引用同一個對象,改變其中一個變量就會影響另一個變量:
var obj1 = new Object(); var obj2 = obj1; obj1.name = 'Fly_001'; alert(obj2.name); // 'Fly_001';
3、傳遞參數(shù)的特點。
這是一個容易困惑的點 。
ECMAScript 中所有函數(shù)的參數(shù)都是按值傳遞的。也就是說,把函數(shù)外部的值復(fù)制給函數(shù)內(nèi)部的參數(shù),就和把值從一個變量復(fù)制到另一個變量一樣。基本類型值的傳遞如同基本類型變量的復(fù)制一樣,而引用類型的傳遞,則如同引用類型變量的復(fù)制一樣,這一點確實會引起很多小伙伴的爭議,歡迎討論~
- 在向參數(shù)傳遞基本類型的值時,被傳遞的值會被復(fù)制給一個局部變量( 即 arguments 對象中的一個元素 )。
- 在向參數(shù)傳遞引用類型的值時,會把這個值在內(nèi)存中的地址復(fù)制給一個局部變量,因此該局部變量的變化會反映到函數(shù)的外部:
function addTen(num) { num += 10; return num; } var count = 20; var result = addTen(count); alert(count); // 20,木有變化; alert(result); // 30 function setNmae(obj) { obj.name = 'Fly_001'; } var person = new Object(); setName(person); alert(person.name); // 'Fly_001';
在上面代碼中我們創(chuàng)建了一個對象,并將其保存在了變量 person 中。然后,這個對象被傳遞到 setName () 函數(shù)中就被復(fù)制給了 obj,在這個函數(shù)內(nèi)部,obj 和 person 引用的是同一個對象。
很多小伙伴會認為該參數(shù)是按引用傳遞的,為了證明對象是按值傳遞的,再看下這個修改過的代碼:
function setName(obj) { obj.name = 'Fly_001'; obj = new Object(); obj.name = 'juejin'; } var person = new Object(); setName(person); alert(person.name); // 'Fly_001';
如果 person 是按引用傳遞的,那么 person 就會自動被修改為指向其 name 屬性為 ‘juejin’ 的新對象。但接下來再訪問 person.name 時仍然顯示 ‘Fly_001’,這說明即使在函數(shù)內(nèi)部修改了參數(shù)的值,但原始的引用仍保持不變。( 實際上,當在函數(shù)內(nèi)部重寫 obj 時,這個變量引用的就是一個局部對象了,其將在函數(shù)執(zhí)行完畢后立即被銷毀。)
4、檢測類型的操作符不同。
檢測基本類型適宜用 typeof 操作符
alert(typeof 'Fly_001'); // 'string'; alert(typeof []); // 'object';
因為 typeof 操作符的返回值為 'undefined','string','boolean','number','symbol','object','function' 其中之一。
它可以很友好地指出某一具體基本類型,而對于引用類型則籠統(tǒng)地返回 'object'( typeof 對 數(shù)組、正則、null 都會返回 'object' )。
在檢測引用類型時更適合用 instanceof 操作符:
result = varible instanceof constructor;
如果變量是給定引用類型的實例( 根據(jù)它的原型鏈來識別 ),那 instanceof 操作符將會返回 true。
執(zhí)行環(huán)境及作用域
下面聊下 JavaScript 中很重要的一個概念 —— 執(zhí)行環(huán)境。
JS 中每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象,在 Web 瀏覽器中,全局執(zhí)行環(huán)境是 window 對象,因此所有全局變量和函數(shù)都是作為 window 對象的屬性和方法創(chuàng)建的。
某個執(zhí)行環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境將會被銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀,全局執(zhí)行環(huán)境直至網(wǎng)頁或瀏覽器關(guān)閉時才被銷毀( 如果存在閉包,情況又有所不同,會在后面幾篇提到 ,多謝 吳hr 指正)。
每個函數(shù)都有自己的執(zhí)行環(huán)境。當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入一個環(huán)境棧中。而在函數(shù)執(zhí)行之后,棧會將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。
var color = 'blue'; function changeColor() { var anotherColor = 'red'; function swapColors() { var tempColor = anotherColor; anotherColor = color; color = tempColor; // 這里可以訪問 color、anotherColor 和 tempColor; } swapColors(); // 這里可以訪問 color 和 anotherColor,但不能訪問 tempColor; } changeColor(); // 這里只能訪問 color;
以上代碼共涉及 3 個執(zhí)行環(huán)境:全局環(huán)境、changeColor() 的局部環(huán)境和 swapColor() 局部環(huán)境。其中,內(nèi)部環(huán)境可以通過作用域鏈訪問所有的外部環(huán)境,但外部環(huán)境不能訪問內(nèi)部環(huán)境中的任何變量和函數(shù)。 這些環(huán)境之間的聯(lián)系是線性的、有次序的。每個環(huán)境可以向上搜索作用域鏈 ,以查詢變量和函數(shù)名;但任何環(huán)境都不能通過向下搜索作用域鏈而進入另一個執(zhí)行環(huán)境。
延長作用域鏈
雖然執(zhí)行環(huán)境的類型總共只有兩種 —— 全局和局部 (函數(shù)),但還是兩種辦法來延長作用域鏈~ 就是通過 try-catch 語句的 catch 塊和 with 語句。
這兩個語句都會在作用域鏈的前端添加一個變量對象。對 with 語句來說,會將指定的對象添加到作用域鏈中;對于 catch 語句來說,會創(chuàng)建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。
沒有塊級作用域
JavaScript 沒有塊級作用域經(jīng)常會導(dǎo)致理解上的困惑 。在其它類 C 的語言中,由花括號封閉的代碼塊都有自己的作用域,即執(zhí)行環(huán)境,但在 JavaScript 中卻不是這樣:
if (true) { var color = 'blue'; } alert(color); // 'blue'; for (var i = 0; i < 10; i ++) { // dosomething } alert(i); // 10;
使用 var 聲明的變量會自動被添加到最接近的環(huán)境中。在函數(shù)內(nèi)部,最接近的環(huán)境就是函數(shù)的局部環(huán)境,若初始化變量時沒有使用 var 聲明,該變量會自動被添加到全局環(huán)境。( 創(chuàng)建塊范圍局部變量使用 let 關(guān)鍵字更方便 ):
function add(num1, num2) { var sum = num1 + num2; return sum; } var result = add(10, 20); // 30; alert(sum); // 'sum is not defined';
在上面代碼中,雖然 sum 從函數(shù)中返回了,但在函數(shù)外部是訪問不到的。如果省略 var 關(guān)鍵字,這時 sum 是可以訪問到的( 不過在嚴格模式下,初始化未聲明的變量會報 'xxx is not defined' 錯 )。
模仿塊級作用域
雖然 js 沒有塊級作用域,但我們可以用匿名函數(shù)來模仿塊級作用域~,語法格式如下:
(function() { // 這里是塊級作用域; }) ();
將函數(shù)聲明包含在一對圓括號里,表示它實際上是一個函數(shù)表達式,而緊隨其后的圓括號會立即調(diào)用這個函數(shù)。實際上就相當于:
var someFunction() { // 這里是塊級作用域; }; someFunction();
同時因為 JavaScript 將 function 關(guān)鍵字當作一個函數(shù)聲明的開始,后面不能直接跟圓括號,而函數(shù)表達式后面可以跟圓括號,所以將函數(shù)聲明加上圓括號轉(zhuǎn)換成函數(shù)表達式。
無論在什么地方,只要臨時需要一些變量,就可以使用私有作用域:
function outputNumbers(count) { (function () { for (var i = 0; i < count; i ++) { alert(i); } }) (); alert(i); // 會導(dǎo)致錯誤,讀取不到 i; }
因為在匿名函數(shù)中定義的任何變量,都會在執(zhí)行結(jié)束時立即銷毀,所以變量 i 只能在循環(huán)中使用。
查詢標識符
當在某個環(huán)境中為了讀取或?qū)懭攵靡粋€變量或函數(shù)名 ( 標識符 ),必須通過搜索來確定該它實際代表什么。
搜索過程從作用域的前端開始,向上逐級查找,如果存在一個局部的變量的定義,則停止搜索,即同名局部變量將覆蓋同名全局變量:
var color = 'blue'; function getColor() { var color = 'red'; // 局部變量; return color; } alert(getColor()); // 'red'; alert(window.color); // 'blue';
垃圾收集
JavaScript 具有自動垃圾收集機制,所以開發(fā)人員不必擔心內(nèi)存使用問題,是不是很開森 ,但最好還是了解下 。
首先我們來分析函數(shù)中局部變量的正常生命周期:局部變量只在函數(shù)執(zhí)行的過程中存在,函數(shù)執(zhí)行結(jié)束后就會釋放掉它們的內(nèi)存以供將來使用。所以 垃圾收集器必須跟蹤哪些變量有用、哪些變量沒用,具體到瀏覽器的實現(xiàn)有兩個策略:標記清除和引用計數(shù)
標記清除
此乃 JavaScript 中最常用的垃圾收集機制。
垃圾收集器在運行的時候會把存儲在內(nèi)存中的所有變量都加上標記,然后去掉環(huán)境中的變量及被環(huán)境中的變量引用的變量的標記,
在此之后還有標記的變量將被視為準備刪除的變量,因為環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后垃圾收集器完成內(nèi)存清除工作,銷毀那些帶標記的值并回收它們所占用的內(nèi)存空間。
引用計數(shù)
另一種出鏡率不高的垃圾收集策略是引用計數(shù)。
它主要跟蹤記錄每個值被引用的次數(shù),當某個值的引用次數(shù)為 0 時,則說明沒有辦法再訪問這個值了,因此就可以將其占用的內(nèi)存空間回收。
但引用計數(shù)會存在一個循環(huán)引用的問題:
function problem() { var objA = new Object(); var objB = new Object(); objA.someOtherObject = objB; objB.anotherObject = objA; }
也就是說,在函數(shù)執(zhí)行完之后,objA 和 objB 還將繼續(xù)存在,因此它們的引用次數(shù)永遠不會是 0,假如這個函數(shù)被重復(fù)多次調(diào)用,就會導(dǎo)致大量內(nèi)存得不到回收 。
為了避免這樣的循環(huán)引用問題,最好在不使用它們的時候手動斷開連接:
objA.someOtherObject = null; objB.anotherObject = null;
當垃圾收集器下次運行時,就會刪除這些值并回收它們所占用的內(nèi)存。
Tips:一旦數(shù)據(jù)不再有用,最好將其設(shè)為 null。
( 此條適合全局變量和全局對象的屬性,因為局部變量會在它們離開執(zhí)行環(huán)境時自動被解除引用 )。
ok,JavaScript 基礎(chǔ)的變量、作用域和垃圾回收咱就先講到這,下一篇會聊聊 JavaScript 面向?qū)ο蟮某绦蛟O(shè)計和函數(shù)表達式。
以上就是細說JavaScript中的變量,作用域和垃圾回收的詳細內(nèi)容,更多關(guān)于JavaScript變量 作用域 垃圾回收的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript文件的同步和異步加載的實現(xiàn)代碼
本篇文章主要介紹了JavaScript文件的同步和異步加載的實現(xiàn)代碼,具有一定的參考價值,有興趣的可以了解一下2017-08-08js時間戳轉(zhuǎn)yyyy-MM-dd HH-mm-ss工具類詳解
這篇文章主要介紹了js時間戳轉(zhuǎn)yyyy-MM-dd HH-mm-ss工具類,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-04-04設(shè)置checkbox為只讀(readOnly)的兩種方式
設(shè)置checkbox為只讀的方法有很多,在本文為大家詳細介紹下兩種比較實用的方法,感興趣的朋友不要錯過2013-10-10