作用域精解
[[scope]]:每個javascript函數(shù)都是一個對象,對象中有些屬性我們可以訪問,但有些不可以,這些屬性僅供javascript引擎存取,[[scope]]就是其中一個。
[[scope]]指的就是我們所說的作用域,其中存儲了運行期上下文的集合。
[[scope]]是函數(shù)的一個隱式屬性,不能直接拿出來用。作用域鏈:[[scope]]中所存儲的執(zhí)行期上下文對象的集合,這個集合呈鏈?zhǔn)竭B接,我們把這種鏈?zhǔn)竭B接叫做作用域鏈。
作用域?qū)儆谝粋€函數(shù),一個函數(shù)產(chǎn)生了一樣的作用域。函數(shù)也是一種特殊的對象(有.name屬性),叫函數(shù)類對象。每個對象都有屬性和方法,一切對象都有屬性,是對象就一定有屬性。
- 運行期上下文:當(dāng)函數(shù)執(zhí)行時,會創(chuàng)建一個稱為執(zhí)行期上下文的內(nèi)部對象(AO)。一個執(zhí)行期上下文定義了一個函數(shù)執(zhí)行時的環(huán)境,函數(shù)每次執(zhí)行時對應(yīng)的執(zhí)行上下文都是獨一無二的,所以多次調(diào)用一個函數(shù)會導(dǎo)致創(chuàng)建多個執(zhí)行上下文,當(dāng)函數(shù)執(zhí)行完畢,它所產(chǎn)生的上下文被銷毀。
- 查找變量:在那個函數(shù)查找變量,就在那個函數(shù)從作用域的頂端依次向下查找。
例子:
function a(){
function b(){
function c(){
};
c();
}
b();.
}
a();
//作用域鏈
a defined a.[[scope]] -- > 0:GO
a doing a.[[scope]] -- > 0:aAO
1:GO
b defined b.[[scope]] -- > 0:aAO
1:GO
b doing b.[[scope]] -- > 0:bAO
1:aAO
2:GO
c defined c.[[scope]] -- > 0:bAO
1:aAO
2:GO
c doing c.[[scope]] -- > 0:cAO
1:bAO
2:aAO
3:GO
立即執(zhí)行函數(shù)
示例:
function a(){ //a里面b沒有執(zhí)行
function b(){
var bbb = 234;
console.log(aaa);
}
var aaa = 123;
return b; //這里最后一條語句把b的引用扔出去,給glob;
}
var glob = 100;
var demo = a();
demo();
對于執(zhí)行期上下文,哪個函數(shù)先執(zhí)行完就先銷毀哪個函數(shù)的執(zhí)行期上下文,執(zhí)行期上下文是在執(zhí)行的時候創(chuàng)建的,但要注意,對于函數(shù)的引用,它的執(zhí)行期上下文是不被銷毀的,因為沒有被執(zhí)行(在被定義了時候就有一個初始的執(zhí)行期上下文)。

a銷毀的時候b還沒有執(zhí)行,而是被返回出去了,但是b已經(jīng)拿到了a的執(zhí)行期上下文,以后就可以隨便用。即使a被銷毀,它也可以用,因為它已經(jīng)被拿出來引用了。
閉包
當(dāng)內(nèi)部函數(shù)被保存到外部時,將會生成閉包。閉包會導(dǎo)致原有作用域不釋放,造成內(nèi)存泄漏(也就是內(nèi)存占用)。
閉包的作用
- 實現(xiàn)公有變量
- eg:函數(shù)累加器
function add(){
var count = 0;
function demo(){
count ++;
console.log(count);
}
return demo;
}
var counter = add();
counter();
counter();
counter();
counter();
- 可以做緩存在(存儲結(jié)構(gòu))
- eg:eater
function test(){
var num = 100;
funcrion a(){
num ++;
console.log(num);
}
//a defined a.[[scope]] 0:testAO
// 1:GO
function b(){
num --;
console.log(num);
}
//b defined b.[[scope]] 0:testAO
// 1:GO
return [a, b];
}
var myArr = test();
myArr[0]();
//a doing a.[[scope]] 0:aAO
// 1:testAO
// 2:GO
myArr[1]();
//b doing b.[[scope]] 0:bAO
// 1:testAO
// 2:GO
eater示例:
funcrion eater(){
var food = "";
var obj = {
eat : function(){
console.log("i am eating" + food);
food = "";
},
push : function (myFood){
food = myFood;
}
}
return obj;
}
var eater1 = eater();
eater1.push('banana');
eater1.eat();
這里是一對二的閉包。a和b被定義的時候都有一個狀態(tài):test的AO連著GO。他們共用一個testAO和GO,所以a和test形成一個閉包,b和test也形成一個閉包。a在test基礎(chǔ)上使用num,改完之后,b又在改后的基礎(chǔ)上修改值。
- 可以實現(xiàn)封裝,屬性私有化
eg:Person(); - 模塊化開發(fā),防止污染全局變量
立即執(zhí)行函數(shù)
定義:此函數(shù)沒有聲明,在一次執(zhí)行過后即釋放。適合做初始化工作。
凡是只被執(zhí)行一次、只想要它執(zhí)行一次或者說只想執(zhí)行一次返回結(jié)果,無論如何這個函數(shù)只執(zhí)行一次的話,這個函數(shù)被稱為初始化函數(shù),或者初始化功能。(針對初始化功能的函數(shù))。
比如只需要求一個特別復(fù)雜的數(shù)的函數(shù),只用一次,又很占用內(nèi)存空間。初始化函數(shù)就可以解決這個問題。立即執(zhí)行函數(shù)的好處就是它執(zhí)行完會立即釋放空間。舉個例子:
function a(){
....此處省略10萬行代碼,但只被執(zhí)行一次。
}
立即執(zhí)行函數(shù)是外面一對小括號包著,結(jié)尾再有一對小括號。這里是匿名的,也可以有名稱。這是javascript提供的唯一一個可以立即銷毀函數(shù)的東西。
立即執(zhí)行函數(shù)的例子:
(function (){
var a = 123;
var b = 234;
console.log(a + b);
}());
立即函數(shù)和函數(shù)的區(qū)別:除了執(zhí)行完就被釋放意外,沒有任何其他的區(qū)別。函數(shù)有的東西立即函數(shù)都能有,比如參數(shù)(最后的小括號里就可以寫形參)、返回值。立即函數(shù)雖然執(zhí)行完就被銷毀,但即便這樣也還是可以有返回值。
var num = (function(a, b, c){ //返回值的接收形式
var d = a + b + c*2; // 9
}(1, 2 ,3)); //形參
立即執(zhí)行函數(shù)有執(zhí)行期上下文,也要經(jīng)過預(yù)編譯。立即函數(shù)是函數(shù),函數(shù)就必須有預(yù)編譯過程。
立即執(zhí)行函數(shù)的寫法有兩種:
- 第一種:官網(wǎng)寫法(function (){}())
- 第二種:(function (){})()
W3C建議使用第一種。下面來看看兩種寫法。
function test(){
var a = 123;
}
test(); //這也是表達式
//一般我們執(zhí)行函數(shù)的時候是用函數(shù)名加括號,但
//function test(){ //這是函數(shù)聲明,不是表達式
// var a = 123;
//}()
//這里加個括號是不能執(zhí)行的,而且會報很低端的錯誤。
只有函數(shù)表達式才能被執(zhí)行符號執(zhí)行。)執(zhí)行符號就是一對括號()。像這樣:
var test = function(){ //函數(shù)表達式
console.log('a');
}(); //這里可以執(zhí)行
注意,能被執(zhí)行符號執(zhí)行的表達式,這個函數(shù)名字就會被自動忽略。也就是說,唄執(zhí)行符號執(zhí)行的表達式,基本上就成了立即執(zhí)行函數(shù)。就像上面這個例子,被立即執(zhí)行之后再訪問test,test是訪問不到的,會是undefined。上面這種寫法就和立即執(zhí)行函數(shù)沒有什么區(qū)別。
下面這種情況也能被執(zhí)行。正、負(fù)和嘆號、&都可以把函數(shù)變成表達式。
+ function test(){
console.log("a");
}();
括號()是執(zhí)行符,把函數(shù)放到括號里面,函數(shù)就變成函數(shù)表達式,所以可以執(zhí)行,也就是上面提到的官方寫法。
看一個阿里巴巴的筆試題:
function test(a, b ,c, d){
console.log(a + b + c +d);
}(1, 2, 3, 4);
理論上是不能執(zhí)行、會報錯的。但是執(zhí)行之后發(fā)現(xiàn),不報錯,只是函數(shù)也沒運行。這是為什么?因為函數(shù)一般來講,能不報錯盡量不給你報錯,盡量會找一種不報錯的方式來理解你的代碼。這里他會理解成:
function test(a, b ,c, d){
console.log(a + b + c +d);
}
(1, 2, 3, 4); //逗號運算符的運算
//因為()只有是里面什么的都沒有的時候,系統(tǒng)才會把它當(dāng)做立即執(zhí)行函數(shù)。
閉包
先看一個小demo:
function test(){
var arr = [];
for(var i = 0; i < 10; i++){
arr[i] = function(){
document.write(i + " "); //10 10 10 10 10 10 10 10 10 10 10 10
}
}
return arr;
}
var myArr = test();
for(var j = 0;j < 10; j++){
myArr[j]();
}
這里我們先要的結(jié)果是1到9,但實際上打印出了10個10。為什么呢?程序在識別函數(shù)體的時候,只知道把函數(shù)體給了數(shù)組,但并不知道里面寫了什么,而里面的語句也要在真正運行的時候才去執(zhí)行。另外,這里了for循環(huán)時每個函數(shù)生成的時候用的test執(zhí)行上下文AO是共用一個。雖然定義了,但是沒有立即執(zhí)行。在第二個循環(huán)執(zhí)行的時候,arr數(shù)組被保存到了外部,test也執(zhí)行完了,這時候i也變成了10。
函數(shù)在定義的時候不用看里面的變量或者語句,在執(zhí)行的時候才去查找。
那怎么在現(xiàn)在的原型上打印出1到9呢?用立即執(zhí)行函數(shù):
function test(){
var arr = [];
for(var i = 0; i < 10; i++){
(function (j){
arr[j] = function(){
document.write(j + " ");
}
}(i))
}
return arr;
}
var myArr = test();
for(var j = 0;j < 10; j++){
myArr[j]();
}