黑马喽大闹天宫与JavaScript的寻亲记:作用域与作用域链全解析

黑马喽大闹天宫与JavaScript的寻亲记:作用域与作用域链全解析

开场白:一个变量的"无法无天"与它的"寻亲之路"


📖 第一章:黑马喽的嚣张岁月

话说在前端江湖的ES5时代,有个叫var的黑马喽,这家伙简直无法无天!它想来就来,想走就走,完全不顾什么块级作用域的规矩。

javascript 复制代码
// 你们看看这黑马喽的德行
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 订婚仪式(编译阶段)

javascript 复制代码
// 当看到 var a = 1;
编译器:管家,咱们这有叫a的变量吗?
作用域:回大人,还没有。
编译器:那就在当前场合声明一个a!

3.2 结婚典礼(执行阶段)

javascript 复制代码
JS引擎:管家,我要找a这个人赋值!
作用域:大人请,a就在这儿。
JS引擎:好,把1赋给a!

这里涉及到两种查找方式:

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

javascript 复制代码
var a = 1; // 找到a这个容器装1

RHS查询:找源头(找新娘的娘家)

javascript 复制代码
console.log(a); // 找到a的值
foo();         // 找到foo函数本身

🐒 第四章:黑马喽的罪证展示

在ES5时代,var这家伙真是目中无人:

4.1 无视块级作用域

javascript 复制代码
{
    var rogue = "我是黑马喽,我想去哪就去哪";
}
console.log(rogue); // 照样能访问!

4.2 变量提升的诡计

javascript 复制代码
console.log(naughty); // undefined,而不是报错!
var naughty = "我提升了";

这货相当于:

javascript 复制代码
var naughty;          // 声明提升到顶部
console.log(naughty); // undefined
naughty = "我提升了"; // 赋值留在原地

🙏 第五章:如来佛祖的五指山------let和const

ES6时代,如来佛祖(TC39委员会)看不下去了,派出了letconst两位大神:

5.1 块级作用域的紧箍咒

javascript 复制代码
{
    let disciplined = "我在块里面很老实";
    const wellBehaved = "我也是好孩子";
}
console.log(disciplined); // ReferenceError!出不来咯

5.2 暂时性死区的降妖阵

javascript 复制代码
console.log(rebel); // ReferenceError!此路不通
let rebel = "想提升?没门!";

真相let/const其实也会提升,但是被关进了"暂时性死区"这个五指山里,在声明前谁都别想访问!


🧩 第六章:黑马喽的迷惑行为------词法作用域的真相

6.1 一个让黑马喽困惑的例子

javascript 复制代码
function bar(){
    console.log( myName);  // 黑马喽:这里该输出啥?
}

function foo(){
    var myName = "白吗喽";
    bar()
    console.log("1:", myName)   // 这个我懂,输出"白吗喽"
}

var myName = "黑吗喽";
foo()  // 输出:"黑吗喽","白吗喽"

黑马喽挠着头想:"不对啊!bar()foo()里面调用,不是应该找到foo()里的myName = "白吗喽"吗?怎么会是黑吗喽呢?"

6.2 outer指针:函数的"身份证"

原来,在编译阶段,每个函数就已经确定了自己的"娘家"(词法作用域):

javascript 复制代码
// 编译阶段发生的事情:
// 1. bar函数出生,它的outer指向全局作用域(它声明在全局)
// 2. foo函数出生,它的outer也指向全局作用域(它声明在全局)
// 3. 变量myName声明提升:var myName = "黑吗喽"

// 执行阶段:
var myName = "黑吗喽";  // 全局myName赋值为"黑吗喽"
foo();                 // 调用foo函数

黑马喽的错误理解

scss 复制代码
bar() → foo() → 全局

实际的作用域查找(根据outer指针):

scss 复制代码
bar() → 全局

如图

6.3 词法作用域 vs 动态作用域

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

javascript 复制代码
var hero = "全局英雄";

function createWarrior() {
    var hero = "部落勇士";
    
    function fight() {
        console.log(hero); // 永远输出"部落勇士"
    }
    
    return fight;
}

const warrior = createWarrior();
warrior(); // "部落勇士" - 记得出生时的环境

动态作用域:看调用地(JavaScript不是这样!)

javascript 复制代码
// 假设JavaScript是动态作用域(实际上不是!)
var hero = "战场英雄";
const warrior = createWarrior();
warrior(); // 如果是动态作用域,会输出"战场英雄"

🗺️ 第七章:作用域链------变量的寻亲路线图

7.1 每个函数都带着"出生证明"

javascript 复制代码
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 作用域链的建造过程

javascript 复制代码
// 全局作用域
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 兵法一:模块化开发

javascript 复制代码
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 兵法二:解决循环陷阱

黑马喽的坑
javascript 复制代码
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 3, 3, 3 - 所有函数共享同一个i
    }, 100);
}
作用域链的救赎
javascript 复制代码
// 方法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 兵法三:正确的函数嵌套

javascript 复制代码
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 块级作用域的精细化管理

javascript 复制代码
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 作用域链的新层级

javascript 复制代码
// 全局作用域
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 作用域查找的代价

javascript 复制代码
var globalVar = "我在最外层";

function level3() {
    // 这个查找要经过:自己 → level2 → level1 → 全局
    console.log(globalVar);
}

function level2() {
    level3();
}

function level1() {
    level2();
}

10.2 优化心法

javascript 复制代码
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. 闭包的力量:函数永远记得自己出生时的环境

💡 最佳实践心法

javascript 复制代码
// 好的作用域设计就像好的家风
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代码!

相关推荐
某只天落42 分钟前
Vue2 通用文件在线预览下载组件:一站式解决多类型文件处理需求(支持视频、文档、图片、Office)
前端
金融数据出海43 分钟前
日本股票市场渲染 KlineCharts K 线图
前端·后端
是Yu欸1 小时前
DevUI MateChat 技术演进:UI 与逻辑解耦的声明式 AI 交互架构
前端·人工智能·ui·ai·前端框架·devui·metachat
梦想CAD控件1 小时前
AI生成CAD图纸(云原生CAD+AI让设计像聊天一样简单)
前端·javascript·vue.js
栀秋6661 小时前
JavaScript 中的 简单数据类型:Symbol——是JavaScript成熟的标志
前端
最爱老虎头1 小时前
Konvajs实现虚拟表格
javascript
Nayana1 小时前
前端控制批量请求并发
前端
ssjlincgavw1 小时前
前端高手进阶:从十万到千万,我的性能优化终极指南(实战篇)
前端
比老马还六1 小时前
Bipes项目二次开发/设置功能-1(五)
前端·javascript