下面的文章內容會根據理解程度不斷修正。
js變量作用域:
定義:變量在它申明的函數體以及函數體內嵌套的任意函數體內有定義。
function AA(){ var bb='我是AA內部變量'; function TT(){ alert(bb); } alert(bb); TT(); } AA();
如上圖,兩次彈出的都是“我是AA內部變量”。
JS的變量作用域是函數級的,也就是在AA內部申明的變量,在AA內部任意位置,包括它嵌套的函數內也是有定義的。
在函數AA外面,bb就是沒有定義的。當然如果去掉bb前面var,bb變量就會自動變成全局變量,此時bb在函數AA外也會有效。
JS變量提升:
定義:函數體內申明的變量,會被自動提前到最前面申明。
function AA(){ alert(bb);//undefined alert(cc);//報錯,變量未定義 var bb='我是AA內部變量'; alert(bb);//我是AA內部變量 }
如上,會依次 返回 undefined,報錯,我是AA內部變量
第一次alert為什么沒有報錯呢?這就是變量提升的原因,js執行時會自動將變量申明提升到最前面,但是賦值並不會因此提升。
提升之后就等價於下面這樣。
function AA(){
var bb; //定義自動提前,不賦值, alert(bb); bb='我是AA內部變量'; }
JS執行環境
執行環境或者叫執行上下文,我理解為代碼執行時所處的環境,這個環境決定了它有權訪問哪些變量或者函數。
var bb='全局變量' function AA(){ var bb='局部變量'; var s=function(){ alert(bb); } s(); return s; } var cc=AA();//局部變量 cc(); //局部變量
如上,依次進入的執行環境是window,AA,s
window---全局執行環境
AA,s ---函數執行環境
變量對象VO[variable object] 活動對象AO[activation object]
每個執行環境都有一個變量對象,這個變量對象的屬性綁定了在這個環境里定義的所有變量和函數,形參。VO理解為代碼編譯時產生。
VO綁定以下屬性:
1.函數形參
2.函數申明
3.變量申明
函數被調用后,執行環境就切換成了對應的函數,此時活動對象就會產生。也就是說AO可以理解為函數執行時產生的。
進入函數執行環境后,實際上AO就相當於函數的VO,只是說在函數執行環境里 VO屬性不能被直接訪問,所以生成AO來替代訪問。
var bb='全局變量' function AA(y){ var bb='局部變量'; function s(){ alert(bb); } } AA(5);
上面代碼依次進入的執行環境有兩個,首先window,然后是函數AA
全局執行環境window:VO綁定的屬性依次,函數AA,變量bb
函數AA執行環境:VO綁定的屬性依次是,形參y=5,函數s,變量bb
在全局執行環境中,VO屬性是可以被訪問的,而進入函數執行環境后VO屬性不能被直接訪問,此時會生成活動對象AO替代VO,可以訪問AO屬性。
VO/AO產生的過程也是變量提升的過程,優先提升函數,然后是變量。
此時VO[function]===AO
注:在函數執行環境中,用表達式的方式申明的函數,對應的函數表達式不會加入VO
function AA(){ var sub = function _sub(a, b){ alert(typeof _sub); return a - b; } }
sub作為變量會加入VO,_sub作為函數表達式則不會加入。
JS作用域鏈
作用域鏈包含了執行環境有權訪問的變量、函數的有序訪問。它是一個由變量對象(VO/AO)組成的單向鏈表,主要用來進行變量查找。
JS內部有一個[[scope]]屬性,這個屬性就是指向作用域鏈的頂端。
var bb='全局變量' function AA(y){ var bb='局部變量'; function s(){
var z=0; alert(bb); }
s(); } AA(5);
暫且理解JS在代碼編譯時創建作用域鏈,分析上面的代碼的 作用域鏈:
全局執行環境:[[scope]]----->VO[AA,bb] 只有全局VO,[[scope]]直接指向VO。
函數AA執行環境:[[scope]]---->VO[[y,s,bb]VO[[AA,bb]],首先全局VO壓入棧,然后函數AA VO壓入棧頂,[[scope]]屬性指向棧頂,變量、函數搜索就從棧頂開始。
函數s執行環境:[[scope]]--->VO[[z]]VO[[y,s,bb]VO[[AA,bb]],首先全局VO壓入棧,然后依次AA,s壓入棧,s處於棧頂,[[scope]]屬性直接指向s的VO。
應用:比如調用s,進入s執行環境,在執行alert時,首先會去查找bb的申明,會先在作用域鏈的頂端查找,沒查到就會沿着鏈繼續往下查找,直到查到就停止。
總結:
函數執行時,將當前的函數的VO放在鏈表開頭,后面依次是上層函數,最后是全局對象。變量查找則依次從鏈表的頂端開始。JS有個內部屬性[[scope]],這個屬性包含了
函數的作用域對象的集合,這個集合就稱為函數的作用域鏈。它決定了,哪些變量或者函數能在當前函數中被訪問,以及它的訪問順序。
閉包
我理解為,函數能夠訪問另一個函數中的變量,這樣就構成了一個閉包。只要某個變量在另外一個函數中還存在引用,那么這個變量的值在內存中就不會被釋放,除非這個函數不會再執行。
function getvalue(){ for(var i=0;i<10;i++){ var yy=i; setTimeout( function(){ var zz=document.getElementById('tt').innerText; zz+=','; zz+=yy; document.getElementById('tt').innerText=zz; } , 100) } }
定時器中的匿名函數會在for結束之后依次執行,匿名函數中引用了變量yy,根據作用域鏈規則查找,首先在匿名函數中尋找yy的定義,沒有找到,然后去它的上層查找。
yy定義在getvalue中,又yy被其他函數引用着,所以它的結果不會被釋放。
而for循環執行結束之后yy的結果是9,所以最后的輸出會是:,9,9,9,9,9,9,9,9,9,9
那如果我們想要輸出0123456789怎么辦呢?看下面的代碼
function getvalue(){ for(var i=0;i<10;i++){ var yy=i; ( function(yy) { setTimeout( function(){ var zz=document.getElementById('tt').innerText; zz+=','; zz+=yy; document.getElementById('tt').innerText=zz; } , 100) } )(yy) } }
上面在定時器外面套了一個立即執行函數
(function(yy){
....
})(yy)
代碼執行到這個匿名函數時,這個匿名函數會自動執行,也就是for循環,每次循環到這里這個函數就會立即執行掉,並把參數yy傳入匿名函數中。
現在來看定時器中的匿名函數的作用域鏈。
[[scope]]--->VO[[變量z]] VO[[形參yy,匿名function]] VO[[匿名function,變量yy]] VO[[函數getvalue]]
此時,定時中的匿名函數引用的變量yy,從作用域鏈中查找可以發現,它來自於上層立即執行函數的形參,而立即執行函數是每次
for循環都會立即執行並把參數傳入。我們知道,只要某個變量在另外一個函數中還存在引用,那么這個變量的值在內存中就不會被釋放,
系統會每次把立即函數執行的形參傳入值保存起來,所以定時器的中的匿名函數在執行結果就會是下面這樣。
淺談this
一般而言,this指向執行環境所處的環境,也就是函數被調用時所處的環境。看到資料說:函數調用f(x,y)其實內部是f.call(this, x, y)這樣執行的,所以this是在調用的時候傳入的。
如果函數是直接執行的那么this指向window,如果有調用者,那么this指向調用者。
var a=1; var BB=function(){ alert(this.a); } var DD={ a:4, f:BB } BB();//1 this 指向window var zz=DD.f; zz(); //1 this指向window DD.f(); //4 this指向f的調用者DD
上面的例子說明,作為對象方法時,this指向的是函數的調用者,直接調用或者在一般函數中調用時,this指向的就是全局對象
var a=1; var BB=function(){ this.a=2; } var zz=new BB();//this 指向new出來的對象 alert(a); //1 所以this.a賦值不會影響全局變量中的a,此時的this指向的不是全局對象 alert(zz.a);//2 BB();//直接執行,this指向window,執行之后,全局變量a的值被改變 alert(a); //2
上面說明,作為構造函數時,函數里的this指向的是new出來的對象。
var a=1; function BB(){ alert(this.a); } function CC(){ this.a=3; } BB(); //1 var dd=new CC(); BB.apply(dd); //3 實際上this指向dd BB.call(dd);//3 實際上this指向dd
上面說明,call和apply可以改變this的指向,使用call、apply時,傳入的第一個參數就會成為this的指向對象。