4 變量、作用域和內(nèi)存問題

本章內(nèi)容

  • 理解基本類型和引用類型的值
  • 理解執(zhí)行環(huán)境
  • 理解垃圾收集

由于不存在定義某個變量必須要保存何種數(shù)據(jù)類型值的規(guī)則,變量的值及其數(shù)據(jù)類型可以在腳本的生命周期內(nèi)改變。

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

基本類型值指的是簡單的數(shù)據(jù)段,而引用類型值指那些可能由多個值構(gòu)成的對象。
在將一個值賦給變量時,解析器必須確定這個值是基本類型值還是引用類型值。第3章討論了5種基本數(shù)據(jù)類型:Undefined、Null、Boolean、Number、String。這5種基本數(shù)據(jù)類型是按值訪問的,因?yàn)榭梢圆僮鞅4嬖谧兞恐械膶?shí)際的值。
引用類型的值是保存在內(nèi)存中的對象。與其他語言不同,JavaScript不允許直接訪問內(nèi)存中的位置,也就是說不能直接操作對象的內(nèi)存空間。在操作對象時,實(shí)際上是在操作對象的引用而不是實(shí)際的對象。為此,引用類型的值是按引用訪問的。

在很多語言中,字符串以對象的形式來表示,因此被認(rèn)為是引用類型的。ECMAScript放棄了這一傳統(tǒng)。

4.1.1 動態(tài)的屬性

對于引用類型的值,我們可以為其添加屬性和方法,也可以改變和刪除其屬性和方法。

var person = new Object();
person.name = "Nicholas";
alert(person.name);  //"Nicholas"

但是,我們不能給基本類型的值添加屬性,盡管這樣做不會導(dǎo)致任何錯誤。

var name = "Nicholas";
name.age = 27;
alert(name.age);  //undefined

4.1.2 復(fù)制變量值

如果從一個變量向另一個變量復(fù)制基本類型的值,會在變量對象上創(chuàng)建一個新值,然后把該值復(fù)制到為新變量分配的位置上。

var num1 = 5;
var num2 = num1;

當(dāng)從一個變量向另一個變量復(fù)制引用類型的值時,同樣也會將存儲在變量對象中的值復(fù)制一份放到為新變量分配的空間中。不同的是,這個值的副本實(shí)際上是一個指針,而這個指針指向存儲在堆中的一個對象。復(fù)制操作結(jié)束后,兩個變量實(shí)際上將引用同一個對象。因此,改變其中一個變量,就會影響另一個變量。

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name);  //"Nicholas"

4.1.3 傳遞參數(shù)

ECMAScript中所有函數(shù)的參數(shù)都是按值傳遞的。訪問變量有按值和按引用兩種方式,而參數(shù)只能按值傳遞。
在向參數(shù)傳遞基本類型的值時,被傳遞的值會被復(fù)制給一個局部變量(即命名參數(shù),或者用ECMAScript的概念來說,就是arguments對象中的一個元素)。在向參數(shù)傳遞引用類型的值時,會把這個值在內(nèi)存中的地址復(fù)制給一個局部變量,因此這個局部變量的變化會反映在函數(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í)際上是函數(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ù)中之后就被復(fù)制給了obj。即使這個對象是按值傳遞的,obj也會按引用來訪問同一個對象。當(dāng)在函數(shù)內(nèi)部為obj添加name屬性后,函數(shù)外部的person也將有所反映;有很多開發(fā)人員錯誤地認(rèn)為:在局部作用域中修改的對象會在全局作用域反映出來,就說明參數(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í)際上,當(dāng)在函數(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

如果變量是給定引用類型的實(shí)例,那么instanceof操作符就會返回true

alert(person instanceof Object);  //變量person是Object嗎?
alert(colors instanceof Array);  //變量colors是Array嗎?
alert(pattern instanceof RegExp);  //變量pattern是RegExp嗎?

根據(jù)規(guī)定,所有引用類型的值都是Object的實(shí)例。在檢測一個引用類型值和Object構(gòu)造函數(shù)時,instanceof操作符始終會返回true。當(dāng)然,如果使用instance操作符檢測基本類型的值,則該操作符會返回false,因?yàn)榛绢愋筒皇菍ο蟆?/p>

4.2 執(zhí)行環(huán)境及作用域

執(zhí)行環(huán)境是JavaScript中最為重要的一個概念。執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了它們各自的行為。每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象,環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數(shù)據(jù)時會在后臺使用它。
全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境。根據(jù)ECMAScript實(shí)現(xiàn)所在宿主環(huán)境不同,表示執(zhí)行環(huán)境的對象也不一樣。在Web瀏覽器中,全局執(zhí)行環(huán)境被認(rèn)為是window對象,因此所有全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的。某個執(zhí)行環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境被銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀(全局執(zhí)行環(huán)境直到應(yīng)用程序退出——例如關(guān)閉網(wǎng)頁或?yàn)g覽器——時才會被銷毀)。
每個函數(shù)都有自己的執(zhí)行環(huán)境。當(dāng)執(zhí)行流進(jìn)入一個函數(shù)時,函數(shù)的環(huán)境就會被推入一個環(huán)境棧中。而在函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。
當(dāng)代碼在一個環(huán)境中執(zhí)行時,會創(chuàng)建變量對象的一個作用域鏈,用來保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問。作用域鏈的前端,始終都是當(dāng)前執(zhí)行的代碼所在環(huán)境的變量對象。全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。
標(biāo)識符解析是沿著作用域鏈一級一級地搜素標(biāo)識符的過程。搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直至找到標(biāo)識符為止。

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,就是因?yàn)榭梢栽谶@個作用域鏈中找到它。
此外,在局部作用域中定義的變量可以在局部環(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)境都無權(quán)訪問tempColor。然而,在swapColors()內(nèi)部則可以訪問其他兩個環(huán)境中的所有變量,因?yàn)槟莾蓚€環(huán)境是它的父執(zhí)行環(huán)境。

函數(shù)參數(shù)也被當(dāng)作變量來對待,因此其訪問規(guī)則與執(zhí)行環(huán)境中的其他變量相同。

4.2.1 延長作用域鏈

當(dāng)執(zhí)行流進(jìn)入下列任何一個語句時,作用域鏈就會得到加長:

  • 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)常會導(dǎo)致理解上的困惑。

  1. 聲明變量
    使用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不是有效的變量,因此會導(dǎo)致錯誤
function add(num1, num2) {
  sum = num1 + num2;
  return sum;
}
var result = add(10, 20);  //30
alert(sum);  //30
  1. 查詢標(biāo)識符
    當(dāng)在某個環(huán)境中為了讀取或?qū)懭攵靡粋€標(biāo)識符時,必須通過搜索來確定該標(biāo)識符實(shí)際代表什么。搜索過程從作用域鏈的前端開始,向上逐級查詢與給定名字匹配的標(biāo)識符。如果在局部環(huán)境中找到了該標(biāo)識符,搜索過程停止,變量就緒。如果在局部環(huán)境中沒有找到該變量名,則繼續(xù)沿作用域鏈向上搜索。搜索過程將一直追溯到全局環(huán)境的變量對象。如果在全局環(huán)境中也沒有找到這個標(biāo)識符,則意味著該變量尚未聲明。
    在這個搜索過程中,如果存在一個局部的變量的定義,則搜索會自動停止,不再進(jìn)入另一個變量對象。換句話說,如果局部環(huán)境中存在著同名標(biāo)識符,就不會使用位于父環(huán)境中的標(biāo)識符。
var color = "blue";
function getColor() {
  var color = "red";
  return color;
}
alert(getColor());  //"red"
alert(window.color);  //"blue"

變量查詢也并不是沒有代價的。很明顯,訪問局部變量要比訪問全局變量更快,因?yàn)椴挥孟蛏纤阉髯饔糜蜴湣?/p>

4.3 垃圾收集

JavaScript具有自動垃圾收集機(jī)制。這種垃圾收集機(jī)制的原理其實(shí)很簡單:找出那些不再繼續(xù)使用的變量,然后釋放其占用的內(nèi)存。為此,垃圾收集器會按照固定的時間間隔周期性地執(zhí)行這一操作。

4.3.1 標(biāo)記清除

JavaScript中最常用的垃圾收集方式是標(biāo)記清除。當(dāng)變量進(jìn)入環(huán)境時標(biāo)記為“進(jìn)入環(huán)境”,離開環(huán)境時標(biāo)記“離開環(huán)境”。
垃圾收集器在運(yùn)行的時候會給存儲在內(nèi)存中的所有變量都加上標(biāo)記。然后,去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標(biāo)記。而在此之后再被加上標(biāo)記的變量將被視為準(zhǔn)備刪除的變量。

4.3.2 引用計數(shù)

另一種不太常見的垃圾手機(jī)策略叫做引用計數(shù)。存在循環(huán)引用問題。

4.3.3 性能問題

在有的瀏覽器中可以觸發(fā)垃圾收集過程,但我們不建議這樣做。

4.3.4 管理內(nèi)存

確保占用最少的內(nèi)存可以讓頁面獲得更好的性能。一旦數(shù)據(jù)不再有用,最好通過將其值設(shè)置為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)境,以便垃圾收集器下次運(yùn)行時將其回收。

4.4 小結(jié)

JavaScript變量可以用來保存兩種類型的值:基本類型和引用類型。基本類型和引用類型值具有以下特點(diǎn):

  • 基本類型值在內(nèi)存中占據(jù)固定大小的空間,因此被保存在棧內(nèi)存中;
  • 從一個變量向另一個變量復(fù)制基本類型的值,會創(chuàng)建這個值的一個副本;
  • 引用類型的值是對象,保存在堆內(nèi)存中;
  • 包含引用類型值的變量實(shí)際上包含的并不是對象本身,而是一個指向該對象的指針;
  • 從一個變量向另一個變量復(fù)制引用類型的值,復(fù)制的其實(shí)是指針,因此兩個變量最終都指向同一個對象;
    確定一個值是那種基本類型可以使用typeof操作符,而確定一個值是哪種引用類型可以使用instanceof操作符。

所有變量都(基本類型和引用類型)存在于一個執(zhí)行環(huán)境(也稱作用域)當(dāng)中,這個執(zhí)行環(huán)境決定了變量的生命周期,以及那一部分代碼可以訪問其中的變量。

  • 執(zhí)行環(huán)境有全局執(zhí)行環(huán)境和函數(shù)執(zhí)行環(huán)境之分;
  • 每次進(jìn)入一個新執(zhí)行環(huán)境,都會創(chuàng)建一個用于搜素變量和函數(shù)的作用域鏈;
  • 函數(shù)的局部環(huán)境不僅有權(quán)訪問函數(shù)作用域中的變量,而且有權(quán)訪問其包含環(huán)境,乃至全局環(huán)境;
  • 全局環(huán)境只能訪問在全局環(huán)境中定義的變量和函數(shù),而不能直接訪問局部環(huán)境中的任何數(shù)據(jù);
  • 變量的執(zhí)行環(huán)境有助于確定應(yīng)該何時釋放內(nèi)存。

JavaScript是一門具有自動垃圾收集機(jī)制的編程語言。

  • 離開作用域的值將被自動標(biāo)記為可以回收,因此將在垃圾收集期間被刪除。
  • “標(biāo)記清除”是目前主流的垃圾收集算法,思想是給當(dāng)前不使用的值加上標(biāo)記,然后再回收其內(nèi)存。
  • 另一種是“引用計數(shù)”,思想是跟蹤記錄所有值被引用的次數(shù)。JavaScript引擎目前都不再使用這種算法。
  • 當(dāng)代碼中存在循環(huán)引用現(xiàn)象時,“引用計數(shù)”算法就會導(dǎo)致問題。
  • 解除變量的引用不僅有助于消除循環(huán)引用現(xiàn)象,而且對垃圾收集也有好處。應(yīng)該及時解除不再使用的全局對象、全局對象屬性以及循環(huán)引用變量的引用。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容