🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

黑馬喽大鬧天宮與JavaScript的尋親記:作用域與作用域鏈全解析

黑馬喽大鬧天宮與JavaScript的尋親記:作用域與作用域鏈全解析

開場白:一個變數的"無法無天"與它的"尋親之路"


📖 第一章:黑馬喽的囂張歲月

話說在前端江湖的ES5時代,有個叫var的黑馬喽,這傢伙簡直無法無天!它想來就來,想走就走,完全不顧什麼塊級作用域的規矩。

// 你們看看這黑馬喽的德行
for (var i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i); // 你猜輸出啥?3,3,3!
    }, 100);
}
// 循環結束了,i還在外面晃蕩
console.log(i); // 3,瞧瞧,跑出來了吧!

但今天咱們不僅要扒一扒var的底褲,還要講講變數們是怎麼"尋親"的——這就是作用域作用域鏈的故事。


🔧 第二章:編譯器的三把斧——代碼的"梳妝打扮"

要說清楚作用域,得先從JavaScript的編譯說起。別看JS是解釋型語言,它在執行前也要經歷一番"梳妝打扮"。

2.1 詞法分析:拆解字符串的魔術

想像一下,編譯器就像個認真的語文老師,把代碼這個長句子拆成一個個有意義的詞語:

var a = 1vara=1

注意:空格要不要拆開,得看它有沒有用。就像讀書時要不要停頓,得看語氣!

2.2 語法分析:構建家譜樹

拆完詞之後,編譯器開始理清關係——誰聲明了誰,誰賦值給誰,最後生成一棵抽象語法樹(AST)

這就像把一堆零散的家庭成員信息,整理成清晰的家譜。

2.3 代碼生成:準備執行

最後,編譯器把家譜樹轉換成機器能懂的指令,準備執行。

關鍵點:JS的編譯發生在代碼執行前的一瞬間,快到你幾乎感覺不到!


💕 第三章:變數賦值的三角戀

var a = 1這麼簡單的一行代碼,背後居然上演著一場"三角戀":

  • 🎯 編譯器:幹髒活累活的媒人,負責解析和牽線
  • JS引擎:執行具體動作的新郎
  • 🏠 作用域:管理賓客名單的管家

3.1 訂婚儀式(編譯階段)

// 當看到 var a = 1;
編譯器:管家,咱們這有叫a的變數嗎?
作用域:回大人,還沒有。
編譯器:那就在當前場合聲明一個a!

3.2 結婚典禮(執行階段)

JS引擎:管家,我要找a這個人賦值!
作用域:大人請,a就在這兒。
JS引擎:好,把1賦給a!

這裡涉及到兩種查找方式:

LHS查詢:找容器(找新娘)

var a = 1; // 找到a這個容器裝1

RHS查詢:找源頭(找新娘的娘家)

console.log(a); // 找到a的值
foo();         // 找到foo函數本身

編譯過程示意圖


🐒 第四章:黑馬喽的罪證展示

在ES5時代,var這傢伙真是目中無人:

4.1 無視塊級作用域

{
    var rogue = "我是黑馬喽,我想去哪就去哪";
}
console.log(rogue); // 照樣能訪問!

4.2 變數提升的詭計

console.log(naughty); // undefined,而不是報錯!
var naughty = "我提升了";

這貨相當於:

var naughty;          // 聲明提升到頂部
console.log(naughty); // undefined
naughty = "我提升了"; // 賦值留在原地

🙏 第五章:如來佛祖的五指山——let和const

ES6時代,如來佛祖(TC39委員會)看不下去了,派出了letconst兩位大神:

5.1 塊級作用域的緊箍咒

{
    let disciplined = "我在塊裡面很老實";
    const wellBehaved = "我也是好孩子";
}
console.log(disciplined); // ReferenceError!出不來咯

5.2 暫時性死區的降妖陣

console.log(rebel); // ReferenceError!此路不通
let rebel = "想提升?沒門!";

真相let/const其實也會提升,但是被關進了"暫時性死區"這個五指山裡,在聲明前誰都別想訪問!


🧩 第六章:黑馬喽的迷惑行為——詞法作用域的真相

6.1 一個讓黑馬喽困惑的例子

function bar() {
    console.log(myName);  // 黑馬喽:這裡該輸出啥?
}

function foo() {
    var myName = "白嗎喽";
    bar();
    console.log("1:", myName);   // 這個我懂,輸出"白嗎喽"
}

var myName = "黑嗎喽";
foo();  // 輸出:"黑嗎喽","白嗎喽"

黑馬喽挠著頭想:"不對啊!bar()foo()裡面調用,不是應該找到foo()裡的myName = "白嗎喽"嗎?怎麼會是黑嗎喽呢?"

6.2 outer指針:函數的"身份證"

原來,在編譯階段,每個函數就已經確定了自己的"娘家"(詞法作用域):

// 編譯階段發生的事情:
// 1. bar函數出生,它的outer指向全局作用域(它聲明在全局)
// 2. foo函數出生,它的outer也指向全局作用域(它聲明在全局)
// 3. 變數myName聲明提升:var myName = "黑嗎喽"

// 執行階段:
var myName = "黑嗎喽";  // 全局myName賦值為"黑嗎喽"
foo();                 // 調用foo函數

黑馬喽的錯誤理解

bar() → foo() → 全局

實際的作用域查找(根據outer指針):

bar() → 全局

如圖

C9AE3D8E-F1DA-4767-AE87-AF4B1AF8B94D.png

6.3 詞法作用域 vs 動態作用域

詞法作用域(JavaScript):看出生地

var hero = "全局英雄";

function createWarrior() {
    var hero = "部落勇士";

    function fight() {
        console.log(hero); // 永遠輸出"部落勇士"
    }

    return fight;
}

const warrior = createWarrior();
warrior(); // "部落勇士" - 記得出生時的環境

動態作用域:看調用地(JavaScript不是這樣!)

// 假設JavaScript是動態作用域(實際上不是!)
var hero = "戰場英雄";
const warrior = createWarrior();
warrior(); // 如果是動態作用域,會輸出"戰場英雄"

🗺️ 第七章:作用域鏈——變數的尋親路線圖

7.1 每個函數都帶著"出生證明"

var grandma = "奶奶的糖果";

function mom() {
    var momCookie = "媽媽的餅乾";

    function me() {
        var myCandy = "我的棒棒糖";
        console.log(myCandy);    // 自己口袋找
        console.log(momCookie);   // outer指向mom
        console.log(grandma);     // outer的outer指向全局
    }

    me();
}

mom();

7.2 作用域鏈的建造過程

// 全局作用域
var city = "北京";

function buildDistrict() {
    var district = "朝陽區";

    function buildStreet() {
        var street = "三里屯";
        console.log(street);     // 自己的
        console.log(district);   // outer指向buildDistrict
        console.log(city);       // outer的outer指向全局
    }

    return buildStreet;
}

// 編譯階段就確定的關係:
// buildStreet.outer = buildDistrict作用域
// buildDistrict.outer = 全局作用域

如圖

⚔️ 第八章:作用域鏈的實戰兵法

8.1 兵法一:模組化開發

function createCounter() {
    let count = 0; // 私有變數,外部無法直接訪問

    return {
        increment: function() {
            count++; // 閉包:outer指向createCounter作用域
            return count;
        },
        getValue: function() {
            return count;
        }
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
// console.log(count); // 報錯!count是私有的

8.2 兵法二:解決循環陷阱

黑馬喽的坑

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 3, 3, 3 - 所有函數共享同一個i
    }, 100);
}

作用域鏈的救贖

// 方法1:使用let創建塊級作用域
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 0, 1, 2 - 每個i都有自己的作用域
    }, 100);
}

// 方法2:IIFE創建新作用域
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j); // 0, 1, 2 - j在IIFE作用域中
        }, 100);
    })(i);
}

8.3 兵法三:正確的函數嵌套

function foo() {
    var myName = "yang";

    function bar() {  // 現在bar的outer指向foo了!
        console.log("2:", myName);  // 找到foo的myName
    }

    bar();
    console.log("1:", myName);
}

var myName = "yang1";
foo();  // 輸出:2: yang, 1: yang

🚀 第九章:現代JavaScript的作用域體系

9.1 塊級作用域的精細化管理

function modernScope() {
    var functionScoped = "函數作用域";
    let blockScoped = "塊級作用域";

    if (true) {
        let innerLet = "內部的let";
        var innerVar = "內部的var"; // 依然提升到函數頂部!

        console.log(blockScoped); // ✅ 可以訪問外層的let
        console.log(functionScoped); // ✅ 可以訪問外層的var
    }

    console.log(innerVar); // ✅ 可以訪問
    // console.log(innerLet); // ❌ 報錯!let是塊級作用域
}

9.2 作用域鏈的新層級

// 全局作用域
const GLOBAL = "地球";

function country() {
    // 函數作用域
    let nationalLaw = "國家法律";

    {
        // 塊級作用域1
        let provincialLaw = "省法規";

        if (true) {
            // 塊級作用域2
            let cityRule = "市規定";
            console.log(cityRule);     // ✅ 本市有效
            console.log(provincialLaw); // ✅ 本省有效
            console.log(nationalLaw);   // ✅ 全國有效
            console.log(GLOBAL);        // ✅ 全球有效
        }
        // console.log(cityRule); // ❌ 跨市無效
    }
}

⚡ 第十章:作用域鏈的性能與優化

10.1 作用域查找的代價

var globalVar = "我在最外層";

function level3() {
    // 這個查找要經過:自己 → level2 → level1 → 全局
    console.log(globalVar);
}

function level2() {
    level3();
}

function level1() {
    level2();
}

10.2 優化心法

function optimized() {
    const localCopy = globalVar; // 局部緩存,減少查找深度

    function inner() {
        console.log(localCopy); // 直接訪問,快速!
    }

    inner();
}

🏆 大結局:黑馬喽的畢業總結

經過這番學習,黑馬喽終於明白了作用域的真諦:

🎯 作用域的進化史

  1. ES5的混亂var無視塊級作用域,到處擾擾
  2. ES6的秩序let/const引入塊級作用域和暫時性死區
  3. outer指針機制:詞法作用域在編譯時確定,一輩子不變

🧠 作用域鏈的精髓

  1. outer指針:函數在編譯時就確定了自己的"娘家"
  2. 詞法作用域:看出生地,不是看調用地
  3. 就近原則:先找自己,再按outer指針找上級
  4. 閉包的力量:函數永遠記得自己出生時的環境

💡 最佳實踐心法

// 好的作用域設計就像好的家風
function createFamily() {
    // 外層:家族秘密,內部共享
    const familySecret = "傳家寶";

    function teachChild() {
        // 中層:教育方法
        const education = "嚴格教育";

        return function child() {
            // 內層:個人成長
            const talent = "天賦異禀";
            console.log(`我有${talent},接受${education},知道${familySecret}`);
        };
    }

    return teachChild();
}

const familyMember = createFamily();
familyMember(); // 即使獨立生活,依然記得家族傳承

🌟 終極奧義

黑馬喽感慨地總結道:

"原來JavaScript的作用域就像血緣關係:

  • 作用域是家規(在哪裡能活動)
  • 作用域鏈是族譜(怎麼找到祖先)
  • outer指針是出生證明(一輩子不變)
  • 詞法作用域是家族傳承(看出生地,不是看現住地)"

從此,黑馬喽明白了:想要在前端江湖混得好,就要遵守作用域的家規,理解作用域鏈的族譜,尊重outer指針的出生證明!


🐒 黑馬喽寄語:記住,函數的作用域是它的"娘家",編譯時定親,一輩子不變!理解了這套規則,你就能馴服任何JavaScript代碼!


原文出處:https://juejin.cn/post/7577594229413756947


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝15   💬3   ❤️3
315
🥈
我愛JS
📝1   💬3   ❤️2
45
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付