1. 程式人生 > 其它 >javascript 變數與常量本質

javascript 變數與常量本質

目錄

變數與常量的本質

  • 變數是什麼:用直白的語言描述就是,有一個數據儲存起來了,當接下來需要使用到這個資料時,需要在儲存這個資料的位置把它拿出來用,一般的解決方式就是用一個名稱與這個資料對應起來,下次要用資料直接使用這個名稱就行,這個名稱就是變數。

  • 變數的本質是:當一段資料儲存在計算機的記憶體中,在程式執行的某一時刻需要讀取這段資料時應該如何找到這個記憶體地址呢,解決方案就是變數----變數儲存的就是這個記憶體地址的編號,讀取變數的值即是使用變數儲存的地址編號去檢視該地址段當前儲存的值是什麼。

  • 常量的本質也是這樣,常量儲存記憶體地址,記憶體地址儲存資料,分別只是常量儲存的資料只能是初始化時儲存,之後不能改變,而變數則是隨時可變。

  • 至於變數與記憶體地址的關係,記憶體地址也是一種資料,變數名與記憶體地址之間的聯絡則由程式語言實現,猜測應該是程式在執行準備的時候將變數與記憶體地址儲存的一個表中,當程式在執行階段讀寫變數時,通過查表獲取變數對應的地址,然後從地址中讀寫資料。

與變數有關的概念

  • 作用域相關: 全域性作用域、區域性作用域、變數提升、暫時性死區。

  • 值引用與堆引用

  • 宣告、讀寫與刪除規則

  • 垃圾回收機制

js變數

宣告方式

  • 使用var || let關鍵字,之後跟隨變數名宣告,變數宣告時可以設定初始值,也可以不設定,不設定初始值時的預設值為undefined

    。使用未定義的變數會丟擲xxx is not undefined,基於這個預設行為,推薦的程式設計正規化是始終給宣告的變數賦初始值,如值不確定則顯示的賦值為null

  • 一條宣告變數語句可以定義多個變數,語法是關鍵字後定義表示式用逗號,分隔。

  • 變數名必需是英文字母、數字、下劃線;且數字不能用為首字母。雖然還有其他合法的字元可以做為變數名,但都不是好的選擇。

// JavaScript
let name = 'chen';
var age = 123;
let sex = '男',
    height = 175;
const age = 30;

讀寫與刪除操作

  • 使用變數名即是讀操作,變數名做為左值即是寫操作

  • JS做為動態弱型別語言,寫操作可以給變數賦任何型別的值,讀操作時也不能確定值是何種型別

  • 無法使用 delete 運算子刪除宣告的變數,var 刪除不成功但不報錯,let會報錯。

垃圾回收機制

  • 垃圾回收需要掌握的幾個點是:
  1. 宿主環境會實現好自動垃圾回收機制,一般不需要手動回收。
  2. 函式作用域內的變數會在函式執行結束後釋放,但閉包函式中的變數不會,所以使用閉包後要檢查閉包函式執行的返回值引用情況。
// JavaScript
let name = 'chen';

console.log(name); // 讀操作

name = 123; // 寫操作

name = name > 345 ? name : 345; // 先讀操作再寫操作

變數作用域

  • 變數作用域指的是程式在何種區域內可以讀寫變數,從字面意思來理解:變數產生作用的區域,有以下三種作用域
  1. 全域性作用域: 整個程式從開始執行到結束執行算是一個生命週期,在這個生命週期內有一個頂層的全域性變數,在程式執行的任何時間、任何地點都可以產生作用,換句話說,全域性變數在任何地方都可以讀寫。
  2. 函式作用域:從函式宣告語句的大括號內開始算,到大括號結束為止,在這個區域內宣告的變數從外部無法讀寫,但函式內可以讀寫函式外的變數;當然這不是好的程式設計習慣,好的方式是,如果函式依賴外部資料,永遠都要使用引數傳遞的方式來實現。當然,如果函式作為物件的方法時,可以自由的讀寫物件的屬性。
  3. 塊作用域:ES6中引入的概念:只在是大括號限定的程式碼塊,都會是一個單獨的作用域,迴圈、判斷都形成自己的作用區域。

作用域鏈:以上三種作用域都是可以從內向外一層層查詢變數,直到全域性變數;如果有找到則做讀寫操作,如果未找到則丟擲異常 XXX not is undefined 。這種象鏈條一樣從內到外的查詢方式就叫作用域鏈。區域性作用域可以訪問全域性作用域中的變數,全域性無法訪問區域性作用中的變數。當局部作用域與全域性作用域存在同名變數時,優先使用區域性作用域。
函式中使用函式外的變數,其行為是函式定義時確定變數位置,跟函式在哪個位置執行無關。

varlet 的區別

  • 作用域範圍不同:var 只有全域性作用域與函式作用域,let除有全域性與函式作用域外,還有塊作用域,塊作用域指的是隻要在{}大括號內就會形成區域性作用域,大括號外無法訪問。
// 變數作用域

// 全域性作用域
var name = 'abc'

function fn() {
    // 區域性作用域
    console.log(name);

    var name = 123;
}

// 全域性作用域
let age = 36;

function fnName() {
    // 函式作用域
    let age = 40;

    // 塊作用域
    if (true) {
        let age = 50;
    }

    for (let i = 0; i < 10; i++) {
        let age = 60;
        console.log(age);
    }
}
  • 變數提升與暫時性死區:var宣告的變數會在程式碼執行之前先有一個變數提升的機制,變數提升就是當程式執行到相應作用域內時,會先將var關鍵字宣告的變數名儲存為一張與記憶體地址對應的表,記憶體地址中的值預設為undefined,這就是需要注意的地方,變數提升的只是宣告部分,賦的值不會被提升。這種行為會導致在var關鍵字宣告語句之前就可以讀寫操作變量了,而var關鍵字又沒有塊作用域的概念,極易產生直覺之外的效果,是BUG的高發地。但是let沒有變數提升的機制,變數必需在宣告語句之後才可以使用。但這又存在一個暫時性死區的問題,什麼是暫時性死區:當前作用域開始執行之後到變數宣告之前的區域都是死區;如果使用的變數名在當前作用域中不存在,程式會自動向外查詢更高一層的作用域,直到全域性作用域;但如果這個變數名在當前作用域是有宣告的,只是執行到這一刻的時候還沒到宣告語句,此時就不會向外查詢,而當前也還沒到宣告語句處,使用該變數名是非法的,是在暫時性死區中。

“暫時性死區”(TDZ)。意味著 typeof 不再是一個百分之百安全的操作,有些死區很隱蔽,比如函式引數設定預設值時前面的引數值是由後面引數定義的情況 function(x = y , y = 2){} ,由於此時引數還未宣告會報錯。

  • var宣告的變數可以在同一作用域內重複宣告,但重複宣告變數是無效的,只是不報錯,如果後面重複宣告且初始化賦值了前面已經宣告過的變數,則後宣告變數覆蓋前面的值。而let宣告的變數不可以重複宣告

  • var宣告的變數是頂層全域性物件(window || global)的屬性,無法通過delete刪除,而let宣告的變數不再是頂層全域性物件的屬性。

  • var使用未宣告變數與為未宣告變數賦值產生的行為是不一樣的,使用會報錯,而賦值會隱式的生成全域性變數,此點要極力避免,在嚴格模式下賦值也會報錯。let則統一了這兩種操作的結果為丟擲異常。

// JavaScript

var arrEs5 = [];

for (var i = 0; i < 10; i++) {
    arrEs5[i] = function() {
        console.log(i)
    }
}

arrEs5[0](); // 10

let arrEs6 = [];

for (let i = 0; i < 10; i++; i++) {
    arrEs6[i] = function() {
        console.log(i)
    }
}

arrEs6[0](); // 0

//for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。

for (let i = 0; i < 3; i++) {
    let i = 'abc';
    console.log(i)
}

// abc
// abc
// abc

js常量

  1. ES6語言標準才引入常量的語法支援,使用const來宣告常量。
  2. 常量必需在宣告時初始化賦值,宣告語句之後不可以寫常量,只可以讀取常量的值。
  3. 常量的作用域規則與let相同。
  4. 常量的值為複合型別時,常量實際指向的是複合型別資料的堆記憶體,此時複合型別中的值可以更改,只是不能更改該常量對複合資料的引用關係

本質上並不是變數的值不能改動,而是變數指向的那個記憶體地址不能改動。所以簡單型別的資料,值就儲存在那個記憶體地址,而複合型別的資料,記憶體地址儲存的只是一個指標,因此將一個物件宣告為常量是可以更改物件屬性的。