深刻淺出 JavaScript 變量、做用域和內存 v 0.5

本文主要從原理入手分享變量和做用域的相關知識,最後結合本文所分享知識,再次深刻了解下閉包的運行原理。html

主要參考《JS高級程序設計》 《JS權威指南》 《高性能 JS》 三本書。前端



目錄閉包

變量函數

1.1 變量的聲明性能

1.2 變量類型的特色學習

執行環境和做用域測試

再談談閉包this

變量

對 JavaScript 稍微有點了解的同窗都知道,JavaScript 中的變量與其餘語言的變量有很大區別。spa

JS 的弱類型的特性決定了變量只是在特定時間(生命週期做用域中)用於保存特定值的一個名字而已。翻譯

一個變量能夠在生命週期內任意改變類型。(太靈活了以致於好複雜)

JavaScript 變量能夠用來保存兩種類型的值: 基本類型值引用類型值

變量的聲明

在JavaScript 程序中,使用一個變量以前應該先聲明它。變量使用 var 關鍵字聲明的。

若是在聲明的時候沒有進行初始化,那麼默認初始化爲undefined。

重複聲明和遺漏聲明

使用 var 關鍵字重複聲明變量時合法的並且無害的,若是重複聲明帶有初始化,那麼該聲明和一條普通的賦值語句沒什麼兩樣。

在非嚴格模式下,給未聲明的變量賦值,JS 會在全局做用域中建立一個同名變量,並賦值。(這會形成不少bug,所以應該使用 var 來聲明變量。)

保存引用類型值的變量咱們能夠爲其添加、改變和刪除其屬性和方法

var person = new Object ();

person.name = "tony";
console.log (person.name);    // tony

person.name = "googny";
console.log (person.name);    // googny

delete person.name;
console.log (person.name);    // undefined

基本數據類型值卻不能動態添加、修改和刪除其屬性話說它也沒屬性啊。。。)

var googny = "boy";
googny.girlfriend = "candice";
console.log (googny.girlfriend);    // undefined 沒有女友。。。

基本類型值源自如下5種基本數據類型:Undefined、Null、Boolean、NumberString

基本類型值和引用類型值有如下特色:

  • 基本類型值在內存中佔據固定大小的空間,因此存放在棧內存中(分不清堆棧的同窗可參考網上其它教程)。
  • 保存基本類型值的變量的複製是直接拷貝值的,會建立這個值的一個副本。
//    簡單測試一下
var num1 = 10;
var num2 = num1;

console.log (num1 === num2);     // true

num2 +=10;

console.log (num1 === num2);    // false 
  • 引用類型的值是對象,存放在堆內存中。
  • 包含引用類型值的變量是指向該對象的一個指針,而不是對象自己。
  • 引用類型變量之間的複製實際上是拷貝了對象的指針,並無拷貝對象,因此兩個變量最終都指向同一個對象
var googny = new Object ();
googny.girl = "candice";
var tony = googny;
console.log (googny.girl);    // candice
console.log (tony.girl);    // candice

tony.girl = 'candice lee';
console.log (googny.girl);    // candice lee
console.log (tony.girl);    // candice lee
  • 肯定一個值是基本類型可使用 typeof 操做符,而肯定一個值是哪一種引用類型可使用 instanceof 操做符。
var s = "googny";
var i = 10;
var b = true;
var o = new Object();
var a = new Array();
var u;
var n = null;
console.log( typeof  s);    // string
console.log( typeof  i);    // number
console.log( typeof  b);    // boolean
console.log( typeof  o);    // object
console.log( typeof  a);    // object
console.log( typeof  u);    // undefined
console.log( typeof  n);    // object

 

//    instanceof
var person = new Object();
var girls = new Array();
console.log( person instanceof Object);    // true
console.log( girls instanceof Array);    // true

執行環境和做用域

(《JS 高級程序設計》裏對執行環境和做用域的講解讓我暈暈的,有好多疑問,因此參考了其它的書目,好比《高性能 JavaScript 》下載地址 (翻譯的實在不敢恭維,我都懷疑這個電子書和紙質書不同))

這裏就經過《高性能 JavaScript 》一書中對做用域的原理講解來反過來掌握做用域鏈、變量對象,執行環境,活動對象等相關概念和標識符解析的過程

做用域對JavaScript有許多影響,從肯定哪些變量能夠被函數訪問,到肯定this的值。(關於做用域與性能的話題,請讀者參考《高性能 JavaScript 》)

下面咱們就經過實例來了解做用域的原理:

一些概念:

JS中 函數也是對象,對象就有屬性,屬性又分爲代碼能訪問的,和代碼不能訪問(後者被稱爲內部屬性)。內部屬性是供 JavaScript 引擎使用的。函數的其中一個內部屬性是[[scope]] (通常代碼不可訪問的屬性都帶有[[]]).

該屬性包含了一系列對象表明函數的做用域。這個集合就被稱爲做用域鏈,它決定了哪些變量能訪問,哪些函數能調用。

此函數做用域鏈中的每一個對象都被稱爲變量對象(variable object),這些對象裏面存放着 key-value 對的條目。

當函數被建立的時候,它的做用域鏈就被外部執行環境(父執行環境和爺執行環境)的變量對象「初始化」,這些變量對象中包含該函數可以訪問的數據。

舉個例子:

首先定義一個全局函數:

function add (num1, num2) {
    var sum = num + num2;
    return sum;
}

 

當 add () 函數被建立後,它的做用域鏈被一個變量對象初始化,即全局對象(全部定義在全局做用域中的變量和函數都是改對象的成員)。

如圖所示:

var total = add(5, 10);

 

函數運行的時候,會觸發建立一個內部對象,稱爲執行環境,它定義了一個函數運行時的環境。執行環境和函數的執行一一對應(每次執行完函數,執行環境會被銷燬,下次執行,會從新建立。)

執行環境有本身的做用域鏈,該做用域鏈用於標識符解析。當環境變量被建立的時候,它的做用域鏈被函數的[[Scope]]屬性中包含的對象依次(按順序喲)初始化。

這時,一個被稱爲「活動對象」的新對象就爲執行環境建立好了,這個活動對象就做爲執行環境的變量對象,它包含了全部的本地變量(arguments對象,this)。

而後,這個對象被放在做用域鏈的最前端,當執行環境被銷燬時(函數執行完畢),活動對象也被銷燬。

如圖所示:

在函數運行過程當中,每遇到一個變量,標識符識別過程要決定從哪裏得到或者存儲數據。

此過程搜索執行環境的做用域鏈,查找同名的標識符。搜索工做從運行函數的活動對象(做用域鏈的前端)開始。

若是找到了,那麼就使用這個具備指定標識符的變量;若是沒找到,搜索工做將進入做用域鏈的下一個對象。

此過程持續運行,直到標識符被找到,或者沒有更多對象可用於搜索,這種狀況下標識符將被認爲是未定義的。

(至於搜索過程的效率和做用域之間的關係等話題,也有不少值得學習的,請讀者參考《高性能 JavaScript》)

再論閉包

閉包能夠訪問函數做用域外的變量和方法。 

舉個例子:

var  increment = function ( ) {
    var count = 0 ;
    return function () {
        count ++;
        return count;
    }; 
}();

 

每次調用increment() 函數就會返回一個增量的值,但是count 是在外層函數中定義的,爲何increment 函數能訪問的到呢? 

increment 函數 在匿名外部函數執行時建立,能夠訪問外部做用域的count變量。閉包能訪問count是由於一個特殊的做用域鏈被建立。

當匿名外部函數執行時,一個活動對象被建立,根據上面的知識,咱們知道它做爲做用域鏈上的第一個對象,全局對象是第二個。

當閉包被建立時,它的[[scope]]屬性被這些對象依次初始化(由於這些相對閉包來講都是外部執行環境啊)。

因爲閉包的[[Scope]]屬性包含與執行環境做用域鏈相同的對象引用,會產生反作用。一般,一個函數的活動對象與執行環境一同銷燬。

當涉及閉包時,活動對象就沒法銷燬了,由於引用仍然存在於閉包的[[Scope]]屬性中。

這意味着腳本中的閉包與非閉包函數相比,須要更多內存開銷。在大型網頁應用中,這多是個問題。

當閉包被執行時,一個執行環境將被建立,它的做用域鏈被[[Scope]]中的做用域鏈的兩個對象初始化,而後一個新的活動對象添加到執行環境做用域鏈的前端。

如圖所示

談一點兒性能的問題,如上圖所示,閉包中要對count進行++ 操做,但是閉包的活動對象中沒有count,因此每次調用都得從下一個變量對象中查找,因此性能是有點損失的。

詳細的性能方面的考慮,還得參考《高性能 JavaScript》。

更多內容盡在這裏:相關博客一覽表

相關文章
相關標籤/搜索