【追尋javascript高手之路02】變量、作用域知多少?


前言

本來想把這個與上篇博客寫到一起的,但是考慮到是兩個知識點還是分開算了,於是我們繼續今天的學習吧。

基本類型與引用類型

ECMAScript的的變量有兩種類型:

基本類型(值類型):簡單數據段

引用類型:多個值構成的對象

在變量賦值結束后,解析器必須知道這個變量時基本數據類型還是引用類型,需要注意的是string在js中是值類型。

復制的差異

值類型的復制會在內存中創建副本,所以彼此間不會影響,但是引用類型只是將變量的引用復制,其指向的仍然是一個對象,會相互影響:

1 var a = {};
2 a.a = 6;
3 var b = a;
4 b.a = 66;
5 alert(a.a);//66

這是一個典型的例子,值類型的就不舉例了。

執行環境與作用域

PS:作用域是面試官的最愛,也是我們經常栽跟頭的地方,這塊我們有必要搞清楚

執行環境(execution)是javascript中非常重要的一個概念。

執行環境定義了變量或者函數有權限訪問其他數據,決定了他們各自的行為。

每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的所有變量和函數都保存到這個對象中,雖然代碼無法訪問,但是解析器會用到他。

在web瀏覽器中window對象便是我們的全局執行環境(最外圍的執行環境,宿主不同便有差異,比如node.js可能就不一樣),因此所有全局變量和函數都是作為window對象的屬性或者方法創建的。

銷毀

某個執行環境中的所有代碼執行完畢,該環境便銷毀,變量對象中的屬性和函數也就完蛋了(閉包會讓情況有所不同)。

函數作用域

每個函數都有自己的“作用域”范圍(執行環境),當執行流進入一個函數時,函數的環境就會進入一個環境棧中,在函數執行完畢后,棧會將其環境彈出,控制權又重新交回之前的環境。ECMAScript程序的執行流就是這個機制在控制。

作用域鏈

當代碼在一個環境中執行時會創建變量對象(variable object),並且該對象擁有一個作用域鏈,作用域鏈的用途是保證對執行環境有權限訪問的所有變量與函數的有序訪問。

作用域的最前端便是當前執行代碼所在環境的變量對象,若這個變量時函數,就將其活動對象(activation object)作為變量對象,

活動對象最開始只包含一個變量arguments,整個作用域鏈會一直向上延伸直到window,這里上一個經典的例子:

 1 var color = 'blue';
 2 function changeColor() {
 3     var anotherColor = 'red';
 4     function swapColor() {
 5         var tmpColor = anotherColor;
 6         anotherColor = color;
 7         color = tmpColor;
 8     }
 9     swapColor();
10 }
11 changeColor();

以上代碼涉及三個執行環境(execution context):

① 全局環境

② changeColor局部環境

③ swapColor局部環境

全局變量中有一個color和一個函數changeColor

changeColor中有一個anotherColor變量和swapColor函數

swapColor函數中有tmpColor,但是他可以訪問以上所有的變量

我們看到三個框了,內部環境可以通過作用域鏈訪問所有外部環境,但是外部環境不能訪問內部的,js中只有函數能產生局部作用域。

延長作用域鏈

有些語句可以在作用域鏈頂端臨時增加一個變量對象,該變量對象會在代碼執行后被移除,以下情況會引起作用域鏈加長:

① try-catch的catch塊

② with語句

這兩個語句都會在作用域鏈前端增加一個變量對象,對with語句來說,會將指定對象添加到作用域鏈中,對catch來說,會創建一個新的變量對象,其中包含被拋出的錯誤對象的聲明。

1 function buildUrl() {
2     var q = '?p=true';
3     with (location) {
4         var url = href + q;
5     }
6     return url;
7 }

在此with語句接收的是location對象,因此其變量對象就包含了location對象的屬性和方法,而且被放到作用域鏈的前端了,我們的href就是有意義的了,相當於location.href。

閉包

作用域鏈與閉包在一起后情況又會有所不同,在此我們再來看看閉包是什么:

閉包是指有權限訪問另一個函數作用域中變量的函數:
在函數內部創建函數,使用到了外部的變量,並且將此函數返回,就形成了閉包。

我認為閉包產生的原因是因為想私有化變量,但是提供一個對外的訪問接口,所以提出了閉包的概念
 1 function Klass() {
 2     var name;
 3     var getName = function () {
 4     return name;
 5     };
 6     var setName = function (n) {
 7         name = n;
 8     };
 9     return { getName: getName, setName: setInterval };
10 }

比如這個應用,是比較常見的,name應該是私有的,但是我們要將其接口公布出來。

這里,當Klass被調用了,即使他返回了,但是內部函數仍然可以訪問name變量,內部函數包含了外部Klass函數的作用域,我們來理一理這個東西:

當某個函數調用時會創建一個執行環境(execution context)以及相關的作用域鏈。
然后,使用arguments和其它命名參數的值來初始化函數的活動對象(activation object)。
在作用域鏈中,外部函數的活動對象始終處於第二位,可能還有第三位第四位。。。
在函數執行過程中,為了讀取和寫入變量的值,就需要在作用域鏈中查找變量
 1 function compare(v1, v2) {
 2     if (v1 < v2) {
 3         return -1;
 4     } else if (v1 > v2) {
 5         return 1;
 6     } else {
 7         return 0;
 8     }
 9 }
10 var r = compare(5, 10);

以上定義了compare函數,然后在全局下調用了他,第一次調用時,會創建this、arguments、v1、v2的活動對象。

全局執行環境的變量this、r、compare在compare執行環境作用域鏈的第二位:

后台的每一個執行環境都有一個表示變量的對象——變量對象。

全局環境的變量對象始終都在,像compare這種局部變量的變量對象只在函數執行時候存在

在創建compare函數時,會創建一個預先包含全局變量的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]屬性中

當調用compare函數時,會為函數創建一個執行環境,然后復制函數的[[Scope]]屬性中的對象構建新的執行環境的作用域鏈

為此又有一個活動對象(在此作為變量對象使用)被創建並推入執行環境作用域鏈的最前端。

上例中,對於compare執行環境而言,其作用域鏈包含兩個變量對象:

1 本地活動對象

2 全局變量對象

一般來說,函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域,但是閉包讓情況有所轉變

在函數內部定義的函數將會包含函數(外部函數)的活動對象添加到它自己的作用域鏈中。
所以在Klass內部定義的getName與setName的作用域鏈中事實上是包含了外部Klass的活動對象的。

在函數Klass返回后,其中的getName作用域鏈被初始化為包含Klass函數的活動對象和全局變量對象
這樣getName就可以訪問Klass函數中的所有變量,這樣做的結果就是阻止了Klass執行結束后銷毀其name變量。
換句話說,Klass的執行環境與其作用域鏈會被回收,但是其activation object會被保留在內存中,知道getName被銷毀后才回收,最后Klass的活動對象才被銷毀

PS:顯然閉包占有更多的資源,若是不及時回收會對性能造成影響,這里各位要小心才行。

閉包與變量

作用域鏈的這種配置機制也產生了一個影響不到的問題:

閉包只能取得包含函數(外部函數)中變量的最后一個值。
因為閉包所保存的是函數的活動對象,是整個變量對象,而不是某一個值
1 function createFunc() {
2     var r = [];
3     for (var i = 0; i < 10; i++) {
4         r[i] = function () {
5             return i;
6         };
7     }
8     return r;
9 }

這是一有趣的代碼,我們這里返回了一個數組,數組里面裝了10函數,但是我們這是個函數保存的是同一個外部函數的活動對象,而i最后的值是10,所以所有函數最后打印出來的都是10。這里要怎么處理各位都知道了,我還是貼個代碼吧:

 1 function createFunc() {
 2     var r = [];
 3     for (var i = 0; i < 10; i++) {
 4         r[i] = (function (num) {
 5            return function () {
 6                 return num;
 7             } 
 8         })(i);
 9     }
10     return r;
11 }

這里把各個閉包函數保存的活動對象接不相同,所以產生了不同的結果。

寫到此處我感覺我所知道的東西都差不多了,若是您有什么補充,或者我這里有什么問題,請提出來。下面我們來演練一番

做幾道題吧

好了,說了這么多虛的,我們來一點干貨作為本文的結束吧,完了我就休息了,等晚上再寫點東西就結束了。

settimeout與setInterval

 1 var a = 6;
 2 setTimeout(function () {
 3     alert(a);
 4     a = 666;
 5 }, 1000);
 6 a = 66;
 7 
 8 
 9 var a = 6;
10 setInterval(function () {
11     alert(a);
12     a = 666;
13 }, 1000);
14 a = 66;

我這里稍微修改了一下代碼,我們來看看:

第一段代碼是上次寒冬老師問我的問題,因為延時函數會后面點執行,所以打印的是66

而第二段代碼有所不同的是,先打印的是66,后面便一直是666了

第二題

1 var tt = 'aa'; 
2 function test(){ 
3 alert(tt); 
4 var tt = 'dd'; 
5 alert(tt); 
6 } 
7 test(); 

這道題有點意思哦,他還考察了變量聲明提前的問題。
其中var tt的定義會提前,這道題相當於:

1 var tt = 'aa'; 
2 function test(){ 
3 var tt;
4 alert(tt); 
5 tt = 'dd'; 
6 alert(tt); 
7 } 
8 test(); 

在互動變量中應該使用內部tt,所以第一次是undefined第二次是dd,這道題本身不難,但是容易引起混淆:

1 var a = '11';
2 var a;
3 alert(a);//11

我們這個重復定義不會覆蓋其值的約定可能會給你帶來迷糊,這里要將之看着兩個國家的人。

我們稍稍變形:

1 var a = 10;
2         function test() {
3             a = 100;
4             alert(a);
5             alert(this.a);
6             var a;
7             alert(a);
8         }
9         test();

這個代碼相當於:

1 var a = 10;
2 function test() {
3     var a;
4     a = 100;
5     alert(a);//100
6     alert(this.a);//10
7     alert(a);//100
8 }
9 test();

我們只看this.a,這里this指向的是window,至於this的東西,我們后面點再說。

第三題

1 f = function () { return true; };
2 g = function () { return false; };
3 (function () {
4     if (g() && [] == ![]) {
5         f = function f() { return false; };
6         function g() { return true; }
7     }
8 })();
9 alert(f()); // true or false ? 

這道題看上去有點亂。。。考察了很多東西,我們先說說其中的一個有問題的地方:

不要在if里面定義function請使用函數表達式,在if中定義function會讓結果難以估計

javascript可能會修正其中的程序而忽略if判斷,但是這不是我們這里要研究的,但是他直接影響我做題的思路啊,這里果斷給把題改了:

1 f = function () { return true; };
2 g = function () { return false; };
3 (function () {
4     if (g() && [] == ![]) {
5         f = function f() { return false; };
6         g = function() { return true; }
7     }
8 })();
9 alert(f()); // true or false ? 

題目願意是什么我們不管了,現在就這樣做。其中第五行的function f()中的f會被忽略掉,他還有個坑爹的地方是:

var s1 = g();//false
var s2 = g() && []; //false
var s3 = ![]; //false
var s4 = g() && [] == ![]; //false
var s5 = (g() && []) == ![]; //true

這道題稍不注意就要完蛋,s4為什么是false我都猜不透。。。所以答案是true了,這道題我沒有摸透,各位自己去看看吧。

第四題

 1 function say667() {
 2     // Local variable that ends up within closure
 3     var num = 666;
 4     var sayAlert = function () { alert(num); }
 5     num++;
 6     return sayAlert;
 7 }
 8 
 9 var sayAlert = say667();
10 sayAlert();

這道題有點意思,與前面的循環閉包的例子類似,答案是667

第五題

1 var foo = {
2      bar: function() {
3          return this.baz;
4      },
5      baz: 1
6  };
7  (function() {
8      return typeof arguments[0]();
9  })(foo.bar)

剛剛無意間看到了這道題,很有點意思的我們來詳細理一理:

① 下面調用函數時候,使用了前面的對象作為參數

② 對象里面卻是一個函數,而且使用了內部的屬性

③ 返回時候調用了該函數,但是我們知道此時匿名函數的內部this指向的是windows

window沒有baz變量,所以返回的是undefined

補充:

我剛剛把這道題看淺了,我們再來理一理,我們說第九行將函數作為參數傳遞給我們的arguments,經過昨天的學習,我們知道相當於變量賦值:

var a = foo.bar;

如此一來,內部的函數與外部的foo就沒有一毛錢的關系了,我們這里改下代碼:

 1 var foo = {
 2     bar: function () {
 3         alert(this);
 4         var s = '';
 5         return this.baz;
 6     },
 7     baz: 1
 8 };
 9 (function (func) {
10 //                    alert(func());
11     alert(arguments[0]())
12 })(foo.bar);

大家注意看第10,11行調用方式帶來的不同!

① 使用func方式調用的話,3行this指向為window

② 使用arguments[0]()調用的話,this指向為Arguments

不管怎樣,baz他們都是找不到的所以為undefined,至於下面這個有一點點差異。

我們不管怎么調用都會返回111,因為他確實是有值的,不要被他所外衣所疑惑,但是這里還真有個疑惑點

我們以func方式調用時候,會為window增加一個baz=111的屬性,但是使用arguments[0]的方法調用我就搞不清會為誰增加屬性了,各位可以看看。。

經過最后測試,發現:

 1 var foo = {
 2     bar: function () {
 3         return this.baz;
 4     },
 5     baz: 1
 6 };
 7 (function (func) {
 8     arguments['baz'] = 12;
 9     alert(arguments[0]());
10     var s='';
11 })(foo.bar);

他應該是給arguments賦值了,但是arguments是不能亂操作的,所以斷點根本看不到,但是卻被讀取了,這樣改了后會打印12

第六題

 1 var foo = {
 2 bar: function() {
 3 this.baz=123;
 4 return this.baz;
 5 },
 6 baz: 1
 7 };
 8 (function() {
 9 
10 return typeof arguments[0]();
11 })(foo.bar)

這道題是上面那條的變形,這道題很耍賴的,各位看看答案是什么呢?

PS:5,6題好像水有點深,我要理一理先,囧

 

結語

本文暫時到這里了,請各位也上兩道與作用域有關的面試題唄

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM