最近在重讀《javascript高級程式設計3》,覺得應該寫一些部落格記錄學習的一些知識,不然都忘光啦。今天要總結的是js執行環境和作用域。
先來先來看看執行環境
一、執行環境
書上概念,而執行環境定義了變數或函數有權存取的其他數據,決定了他們各自的行為。每個執行環境都有一個與之關聯的變數物件。環境中定義的所有變數和函數都保存在這個物件中。雖然我們在編寫程式碼的時候無法存取這個對象,但解析器在處理資料時會在後台使用到它。
執行環境是一個概念,一種機制,它定義了變數或函數是否有權存取其他資料
在javascript中,可執行的JavaScript程式碼分成三種:
1. Global Code,即全域的、不在任何函數裡面的程式碼,例如:一個js檔案、嵌入在HTML頁面中的js程式碼等。
2. Eval Code,即使用eval()函數動態執行的JS程式碼。
3. Function Code,即使用者自訂函數中的函數體JS程式碼。
跳過Eval Code,只說全域執行環境和函數執行環境。
1、全域環境:
全域環境是最外圍的一個執行環境。全域執行環境被認為是window物件。因此所有全域變數和函數都是作為window物件的屬性和方法建立的。程式碼載入瀏覽器時,全域執行環境被建立(當我們關閉網頁或瀏覽器時全域執行環境才被銷毀)。例如在一個頁面中,第一次載入JS程式碼時建立一個全域執行環境。
這也是為什麼閉包有一個記憶體外洩的缺點。因為閉包中外部函數被當成了全域環境。所以不會被銷毀,一直保存在記憶體中。
2、函數執行環境
每個函數都有自己的執行環境,當執行進入一個函數時,函數的執行環境就會被推入一個執行環境堆疊的頂部並取得執行權。當這個函數執行完畢,它的執行環境又從這個堆疊的頂端被刪除,並且把執行權並還給之前執行環境。這就是ECMAScript程式中的執行流程。
也可以這樣解讀:當呼叫一個 JavaScript 函數時,函數就會進入與該函數相對應的執行環境。如果又呼叫了另一個函數,則會建立一個新的執行環境,並且在函數呼叫期間執行過程都處於該環境中。當呼叫的函數傳回後,執行過程會傳回原始執行環境。因而,運行中的 JavaScript 程式碼就構成了一個執行環境堆疊。
當函數被呼叫時函數的局部環境被創建(函數內的程式碼執行完畢後,該環境被銷毀,同時保存在其中的所有變數和函數定義也隨之被銷毀)。
2-1定義期
函數定義的時候,都會創建一個[[scope]]屬性,通這個物件對應的是一個物件的列表,列表中的物件僅能javascript內部訪問,沒法透過語法存取。
(scope也就是作用域的意思。)
我們定義一全域函數A,那麼A函數就建立了一個A的[[scope]]屬性。此時,[[scope]]裡面只包含了全域物件【Global Object】。
而如果, 我們在A的內部定義一個B函數,那麼B函數同樣會創建一個[[scope]]屬性,B的[[scope]]屬性包含了兩個對象,一個是A的活動對象Activation Object、一個是全域對象,A的活動對像在前面,全域物件排在後面。
簡而言之,一個函數的[Scope]屬性中物件列表的順序是上一層函數的Activation Object對象,然後是上上層的,一直到最外層的全域物件。
下面是範例程式碼:A只有一個scope,B有兩個scope
// 外部函数 function A(){ var somevar; // 内部函数 function B(){ var somevar; } }
2-2執行期
當函數被執行的時候,就是進入這個函數的執行環境,首先會創一個它自己的活動物件【Activation Object】(這個物件中包含了this、參數(arguments)、局部變數(包括命名的參數)的定義和一個變數物件的作用域鏈[[scope chain]],然後,把這個執行環境的[scope]按順序複製到[[scope chain]]裡,最後把這個活動物件推入到[ [scope chain]]的頂部。
// 第一步页面载入创全局执行环境global executing context和全局活动象 // 定义全局[[scope]],只含有Window对象 // 扫描全局的定义变量及函数对象:color【undefined】、changecolor【FD创建changecolor的[[scope]],此时里面只含有全局活动对象】,加入到window中,所以全局变量和全局函数对象都是做为window的属性定义的。 // 程序已经定义好所以在此执行环境内任何位置都可以执行changecolor(),color也已经被定义,但是它的值是undefined // 第二步color赋值"blue" var color = "blue"; // 它是不需要赋值的,它就是引用本身 function changecolor() { // 第四步进入changecolor的执行环境 // 复制changecolor的[[scope]]到scope chain // 创建活动对象,扫描定义变量和定义函数,anothercolor【undefined】和swapcolors【FD创建swapcolors的[[scope]]加入changecolor的活动对象和全局活动对象】加入到活动对象,活动对象中同时还要加入arguments和this // 活动对象推入scope chain 顶端 // 程序已经定义好所以在此执行环境内任何位置都可以执行swapcolors(),anothercolor也已经被定义好,但它的值是undefined // 第五anothercolor赋值"red" var anothercolor = "red"; // 它是不需要赋值的,它就是引用本身 function swapcolors() { // 第七步进入swapcolors的执行环境,创建它的活动对象 // 复制swapcolors的[[scope]]到scope chain // 扫描定义变量和定义函数对象,活动对象中加入变量tempcolor【undefined】以及arguments和this // 活动对象推入scope chain 顶端 // 第八步tempcolor赋值anothercolor,anothercolor和color会沿着scope chain被查到,并继续往下执行 var tempcolor = anothercolor; anothercolor = color; color = tempcolor; } // 第六步执行swapcolors,进入其执行环境 swapcolors(); } // 第三步执行changecolor,进入其执行环境 changecolor();
2-3访问标识符:
当执行js代码的过程中,遇到一个标识符,就会根据标识符的名称,在执行上下文(Execution Context)的作用域链中进行搜索。从作用域链的第一个对象(该函数的Activation Object对象)开始,如果没有找到,就搜索作用域链中的下一个对象,如此往复,直到找到了标识符的定义。如果在搜索完作用域中的最后一个对象,也就是全局对象(Global Object)以后也没有找到,则会抛出一个错误,提示undefined。
二、Scope/Scope Chain(作用域/作用域链)
当代码在一个环境中执行时,都会创建一个作用域链。 作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。整个作用域链是由不同执行位置上的变量对象按照规则所构建一个链表。作用域链的最前端,始终是当前正在执行的代码所在环境的变量对象。
如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,就是函数内部的arguments对象。作用域链中的下一个变量对象来自该函数的包含环境,而再下一个变量对象来自再下一个包含环境。这样,一直延续到全局执行环境,全局执行环境的Variable Object始终是作用域链中的最后一个对象。
如图所示:
书中例子:
var color="blue"; function changecolor(){ var anothercolor="red"; function swapcolors(){ var tempcolor=anothercolor; anothercolor=color; color=tempcolor; // Todo something } swapcolors(); } changecolor(); //这里不能访问tempcolor和anocolor;但是可以访问color; alert("Color is now "+color);
通过上面的分析,我们可以得知内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。
这些环境之间是线性、有次序的。每个环境都可以向上搜索作用域链,以便查询变量和函数名;但任何环境不能通过向下搜索作用域链条而进入另一个执行环境。
对于上述例子的swapcolor()函数而言,其作用域链包括:swapcolor()的变量对象、changecolor()变量对象和全局对象。swapcolor()的局部环境开始先在自己的Variable Object中搜索变量和函数名,找不到,则向上搜索changecolor作用域链。。。。。以此类推。但是,changecolor()函数是无法访问swapcolor中的变量
启示:尽量使用局部变量,能够减少搜索的时间
1、没有块级作用域
与C、C++以及JAVA不同,Javscript没有块级作用域。看下面代码:
if(true){ var myvar = "张三"; } alert(myvar);// 张三
如果有块级作用域,外部是访问不到myvar的。再看下面
for (var i=0;i<10;i++){ console.log(i) } alert(i); // 10
对于有块级作用域的语言来说,比如java或是c#代码,i做为for初始化的变量,在for之外是访问不到的。因为i只存在于for循环体重,在运行完for循环后,for中的所有变量就被销毁了。而在javascript中则不是这样的,在for中的变量声明将会添加到当前的执行环境中(这里是全局执行环境),因此在for循环完后,变量i依旧存在于循环外部的执行环境。因此,会输出10。
2、声明变量
使用var声明变量时,这个变量将被自动添加到距离最近的可用环境中。对于函数内部,最接近的环境就是函数的局部变量。如果初始化变量时没有使用var,该变量会自动添加到全局函数中。
代码如下:
var name = "小明"; function getName(){ alert( name ); //'undefined' var name = '小黄'; alert(name ); //小黄 } getName()
为什么第一个name是undefined呢。这是因为,javascript解析器,进入一个函数执行环境,先对var 和 function进行扫描。
相当于会把var或者function【函数声明】声明提升到执行环境顶部。
也就是说,进入我们的getName函数的时候,标识符查找机制查找到了var,查找的name是局部变量name,而不是全局的name,因为函数里面的name被提升到了顶部。
上面的代码会被解析成下面这样:
var name = "小明"; function getName(){ var name; alert( name ); //'undefined' var name = '小黄'; alert(name ); //小黄 } getName()
延长作用域链:
虽然执行环境只有两种——全局作用域和函数作用域,但是还是可以通过某种方式来延长作用域链。因为有些语句可以在作用域链的顶部增加一个临时的变量对象。
有两种情况会发生这种现象:
1、try-catch语句的catch块;
2、with语句;
以上就是本文的全部内容,希望对大家学习理解javascript执行环境及作用域有所帮助。