javascript作用域總結

阿集啊發表於2017-10-26

作用域

當你宣告一個函式的時候,你就建立了一個作用域氣泡。前期的js只有函式可以建立作用域,現代js引入了塊級作用域,每進入一次花括號{}就生成了一個塊級作用域。

作用域樹

有些人會叫作用域鏈,但是我覺得這應該是個樹形結構。樹的頂層是全域性作用域,在作用域內宣告函式會建立函式作用域子節點,遇{}會建立塊級作用域子節點,從而構成一顆樹

function a{
    if(true){

    }
}
function b{

}複製程式碼

如上程式碼構成如下作用域樹

作用域樹
作用域樹

變數宣告 var,function,let,const

var、function宣告的變數依附最近的函式作用域或全域性作用域,let、const宣告的變數依附於最近的塊級作用域、函式作用域或全域性作用域,即所有作用域氣泡。

{
    var a=1;
    function b(){}
}
for(var c = 1;c<=1;c++){
    var d =1;
    function e(){}
}
if(true){
    var f=1;
    function g(){}
}
function h (){
    var i =1;
}
//  a,b,c,d,e,f,g,h上層無函式宣告,依附全域性作用域,都可以訪問到
console.log(a);
console.log(b);
console.log(c);
console.log(d);
console.log(e);
console.log(f);
console.log(h);
// i 依附h函式作用域,無法訪問到
console.log(i);//reference error

{
    let j=1;
    const k=1;
}
for(let l = 1;l<=1;l++){
    let m =1;
}
if(true){
    let n=1;
}
function p (){
    let o =1;
}
// 依附最近的作用域,無法訪問到
console.log(j);//reference error 
console.log(k);//reference error
console.log(l);//reference error
console.log(m);//reference error
console.log(n);//reference error
console.log(o);//reference error複製程式碼

變數值查詢

在當前作用域往上查詢,找到第一個匹配的識別符號時停止返回變數值,如果查到全域性作用域都沒有,則丟擲reference error錯誤,因此變數值查詢有就近原則

javascript 程式碼執行前有編譯,產生變數提升

javascript 程式碼執行前要經過編譯環節,該環節會生成部分作用域樹(有些是動態生成),如下程式碼

console.log(a);// 輸出a的函式定義
function a() {
    console.log(b);// undefined
    var b = 2;
    console.log(b);// 2
}複製程式碼

經過編譯環節後,等價於

function a() {
    var b;
    console.log(b);
    b = 2;
    console.log(b);
}
console.log(a);複製程式碼

只有function和var這種變數宣告會變數提升,而let,const不會。變數提升的時候,函式宣告會首先被提升,然後才是變數。(我的記憶方法是函式的第一公民,權利最大)

foo(); // 1
function foo() { 
    console.log( 1 );
}
var foo = function() { 
    console.log( 2 );
};
foo();// 2複製程式碼

經過編譯環節後

function foo() { 
    console.log( 1 );
}
var foo;// 已經宣告過,被忽略
foo(); // 1
foo = function() { console.log( 2 );};
foo(); // 2複製程式碼

function,var,let,const的區別

  1. function,var宣告的變數依附在最近函式作用域或全域性作用域,let,const宣告的變數依附在最近塊級作用域、函式作用域或全域性作用域
  2. function,var宣告的變數在編譯階段產生變數提升,且函式優先提升。let,const不會產生變數提升
  3. fucntion,var在同一作用域重複宣告變數,後者會覆蓋前者(前者與後者的關係要看編譯環節過後的程式碼);而let,const 會直接丟擲語法錯誤
  4. const 宣告變數的同時需要賦值,否則丟擲語法錯誤,且變數的指向不能變(但是變數指向的內容可以變)
const a;//報錯,沒有賦值初值
const b = {a:1};
b.a = 2;//沒錯,變數指向的內容可以變
b = {a:2}//出錯,變數的指向不能變複製程式碼

嚴格模式'use strict'

這塊內容在筆試題上反映吧
詳細看這篇部落格

塊級作用域的實現

塊級作用域是es6才有的東西,之前的javascript引擎並不支援,於是就有了babel編譯神器,將es6的程式碼編譯成低版本javascript引擎能正確執行的程式碼。那babel是怎麼實現塊級作用域的呢,答案就是閉包和變數改名。

image
image

image
image

作用域相關筆試題

第一題:

console.log(a());// 2
var a = function b(){
    console.log(1);
}
console.log(a());// 1
function a(){
    console.log(2);
}
console.log(a());// 1
console.log(b());// reference error複製程式碼

程式碼編譯後,變數提升,函式優先,賦值語句中b為右值,非變數宣告,所以程式碼等價於

function a(){
    console.log(2);
}
var a;
console.log(a());// 2
a = function (){
    console.log(1);
}
console.log(a());// 1

console.log(a());// 1
console.log(b());// reference error複製程式碼

第二題:

function test() {
    console.log(a);// undefined
    console.log(b);// reference error
    console.log(c);// reference error
    var a = b =1;// 等價於 var a=1;b=1;
    let c = 1;
}
test();
console.log(b);// 1
console.log(a);// reference error複製程式碼

var 宣告的變數會變數提升,並依附函式作用域,而,b變數未宣告就作為左值,會變成全域性變數,但是不會變數提升;

第三題:

"use strict";
function test() {
    console.log(a);// undefined
    console.log(b);// reference error
    console.log(c);// reference error
    var a = b =1;// 直接丟擲語法錯誤
    let c = 1;
}
test();
console.log(b);// reference error
console.log(a);// reference error複製程式碼

進入嚴格模式後,b=1這種語法會直接出錯,不會變成全域性變數

第四題:
4.1題

for(var i=0;i<5;i++){
  setTimeout(function(){console.log(i)},0); // 5 5 5 5 5 
}複製程式碼

i 依附函式作用域,執行過程只有一個i,而setTimeout是非同步函式,需要等棧中的程式碼執行完後再執行,此時i已經變為5

4.2題

for(let i=0;i<5;i++){
  setTimeout(function(){console.log(i)},0); // 1 2  3  4
}複製程式碼

let 依附for的塊級作用域,程式碼等價於

for(let i=0;i<5;i++){
  let j = i;
  setTimeout(function(){console.log(j)},0); // 1 2  3  4
}複製程式碼

可以看出每次迴圈都產生一個新的記憶體單元,非同步函式執行時,取到的值為當時保持的快照值。

4.3題

for(var i=0;i<5;i++){
 (function(i){
   setTimeout(function(){console.log(i)},0); // 1 2  3  4
  })(i);
}複製程式碼

使用IIFE來建立快照值,將for 迴圈的i 傳遞給立即執行表示式中的引數i,i是基本資料型別,為賦值傳遞,會產生一個新的記憶體單元,每次迴圈都產生一個快照值,所以非同步函式執行的時候,取到的值為當時保持的快照值。

4.4題

for(var i={j:0};i.j<5;i.j++){
 (function(i){
   setTimeout(function(){console.log(i.j)},0); // 5 5 5 5 5
  })(i);
}複製程式碼

i不是基本資料型別,為引用傳遞,i始終指向同一記憶體單元

4.5題

for(let i={j:0};i.j<5;i.j++){
   setTimeout(function(){console.log(i.j)},0); // 5 5 5 5 5
}複製程式碼

與上面4.4題同理

4.6題
如何改變4.4題的立即執行表示式,輸出1,2,3,4

for(var i={j:0};i.j<5;i.j++){
 (function(i){
   setTimeout(function(){console.log(i.j)},0); // 1 2 3 4
  })(JSON.parse(JSON.stringify(i)));
}複製程式碼

將i序列化和反序列化,產生新的記憶體單元值,不讓i指向同一記憶體單元

相關文章