讀書筆記4:變數、作用域和記憶體問題
阿新 • • 發佈:2018-12-10
基本型別和引用型別的值
(1)概念
基本型別的值指的是簡單的資料段,而引用型別的值指那些可能由多個值構成的物件。 引用型別的值是儲存在記憶體中的物件。JavaScript 不允許直接操作物件的記憶體空間,實際上操作的是物件的引用,而不是實際的物件。引用型別的值是按引用訪問的。 兩種型別的值得在記憶體中的位置是不一樣的,基本型別值在記憶體中佔據固定大小的空間,被儲存在棧記憶體中;引用型別的值時物件,儲存在堆記憶體中。 包含引用型別值得變數實際上包含的並不是物件本身,而是一個指向該物件的指標,也可以理解為該物件在堆記憶體中的地址,而這個變數是儲存在棧記憶體中的。 因此,複製引用型別的值時,實際上覆制的是指向儲存在堆中的物件的指標。它們會指向同一個物件,改變其中一個變數會影響到另一個變數。
(2)函式傳遞引數
ECMAScript 中所有函式的引數都是按值傳遞的。
關於按值和按引用傳遞:
- 按值傳遞(call by value):函式的形參是被呼叫時所傳實參的副本,修改形參的值並不會影響實參,是最常用的求值策略。
- 按引用傳遞(call by reference)時,函式的形參接收實參的隱式引用,而不再是副本。這意味著函式形參的值如果被修改,實參也會被修改。同時兩者指向相同的值。
- 按值傳遞由於每次都需要克隆副本,對一些複雜型別,效能較低;按引用傳遞會使函式呼叫的追蹤更加困難,有時也會引起一些微妙的BUG。
JS中,函式引數按值傳遞有什麼特點?
- 傳遞基本型別的值時,被傳遞的值會被複制給一個區域性變數(即命名引數,或
arguments
- 傳遞引用型別的值時,會把這個值在記憶體中的地址(指標)複製給一個區域性變數,因此這個區域性變數的變化會反映在函式的外部。
這部分例子可看大神部落格,其中提到按共享傳遞 call by sharing的思想
例1:向函式引數傳遞引用型別值
var foo = {name:'foo'};
function test(obj){
obj.name = 'test'; //第一步操作
obj = {name:'bar'}; //第二步操作
};
test(foo);
console.log(foo); //結果:Object {name: "test"}
首先,這裡
foo
儲存的是物件的指標,不是物件本身,按值傳遞給引數obj
的也就是這個指標,稱為物件1; 第一步操作,obj
指向的物件仍為物件1,此時修改了物件1的name
屬性; 第二步操作,給obj
賦了新值,指向了新的物件,稱為物件2;相當於執行了obj = new Object(); obj = {name:'bar'};
這裡obj
獲得一個全新的物件。 在全域性環境下,變數foo
沒有重新賦值,一直是指向物件1; 所以改變函式內部變數obj
的值,不會影響外部環境中引數原值foo
。
(3)檢測型別
- 使用
instanceof
操作符判斷一個例項是否屬於某種型別; - instanceof 可以在繼承關係中用來判斷一個例項是否屬於它的父型別,在多層繼承關係中,
instanceof
運算子同樣適用; - 所有引用型別的值都是
Object
的例項,檢測一個引用型別值和Object
建構函式時,instanceof
操作符始終返回true
。
要深入理解 instanceof
操作符,會涉及到原型繼承機制,後續章節中會提及。此部落格中有一些例子。
執行環境和作用域
(1)概念理清
- 執行環境:定義了變數或函式有權訪問的其他資料,決定了它們各自的行為,有時也被叫做執行上下文。某個執行環境中的所有程式碼執行完畢後,該環境被銷燬,儲存在其中的所有變數和函式也隨之銷燬。執行環境有全域性執行環境和函式執行環境之分。
- 變數物件:環境中定義的所有變數和函式都儲存在這個物件中。
- 宿主環境:宿主環境一般由外殼程式建立和維護,可以為JavaScript等多種指令碼語言提供服務。宿主環境一般會建立一套公共物件系統,這套物件系統對所有指令碼語言開放,並允許它們自由訪問。同時,宿主環境還會提供公共介面,用來裝載不同的指令碼語言引擎。這樣我們可以在同一個宿主環境中裝載不同的指令碼引擎,並允許它們共享宿主物件。
- 全域性執行環境:全域性執行環境是最外圍的一個執行環境。在 Web 瀏覽器中,全域性執行環境被認為是 window 物件,因此所有全域性變數和函式都是作為window物件的屬性和方法建立的。程式碼載入瀏覽器時,全域性執行環境被建立;當我們關閉網頁或者瀏覽器時全域性執行環境才被銷燬。
- 作用域鏈:作用域鏈的用途,是保證對執行環境有權訪問的所有變數和函式的有序訪問。程式碼在一個環境中執行時,會建立變數物件的一個作用域鏈。作用域鏈的前端,始終都是當前執行的程式碼所在環境的變數物件。內部環境可以通過作用域鏈訪問所有的外部環境,但外部環境不能訪問內部環境中的任何變數和函式。
- 活動物件:此概念針對函式。在一個函式物件被呼叫的時候,會建立一個活動物件,首先將該函式的每個形參和實參,都新增為該活動物件的屬性和值;將該函式體內顯示宣告的變數和函式,也新增為該活動的的屬性。這個活動物件作為該函式執行環境的作用域鏈的最前端,並將這個函式物件的[[scope]]屬性裡作用域連結入到該函式執行環境作用域鏈的後端。
- 函式物件:在一個函式定義的時候, 會建立一個這個函式物件的
[[scope]]
屬性,並將這個[[scope]]
屬性指向定義它的作用域鏈上。
(2)延長作用域鏈
- 在作用域鏈前端臨時新增一個變數物件,該變數物件會在程式碼執行後被移除。
try-catch
語句的catch
模組:當try程式碼塊中發生錯誤時,執行過程會跳轉到catch
語句,然後異常物件推入一個可變物件並置於作用域的頭部;在catch
程式碼塊內部,函式的所有區域性變數將會被放在第二個作用域鏈物件中。with
語句:with
語句是物件的快捷應用方式,用來避免書寫重複程式碼。width
語句看上去更高效,實際上產生了效能問題,一般不建議使用。
(3)塊級作用域
塊級作用域和函式作用域 塊級作用域:變數在離開定義的塊級程式碼後立即被回收; 函式作用域:變數在定義的環境中以及巢狀的子函式中處處可見;
ES5 及以前版本 JavaScript
都沒有塊級作用域,只有兩種:函式作用域和全域性作用域。使用 var 宣告的變數會自動被新增到最接近的環境中。
直到 ES6 中定義了塊級作用域,使用 let
/ const
宣告的變數只能在塊級作用域裡訪問。
let
宣告的特點:
let
所宣告的變數,只在let命令所在的程式碼塊內有效;- 不存在變數提升,變數一定要在聲明後使用,否則報錯;
- 只要塊級作用域記憶體在let命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。在程式碼塊內,使用
let
命令宣告變數之前,該變數都是不可用的,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ); - 由於存在“暫時性死區”,在let命令宣告變數之前,
typeof
操作符檢查此變數也會報錯; let
不允許在相同作用域內,重複宣告同一個變數。
以上,就是《Javascript高階程式設計》第四章的學習筆記。