黑马喽大闹天宫与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 = 1 → var、a、=、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委员会)看不下去了,派出了let和const两位大神:
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() → 全局
如图
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();
}
🏆 大结局:黑马喽的毕业总结
经过这番学习,黑马喽终于明白了作用域的真谛:
🎯 作用域的进化史
-
ES5的混乱:
var无视块级作用域,到处捣乱 -
ES6的秩序:
let/const引入块级作用域和暂时性死区 - outer指针机制:词法作用域在编译时确定,一辈子不变
🧠 作用域链的精髓
- outer指针:函数在编译时就确定了自己的"娘家"
- 词法作用域:看出生地,不是看调用地
- 就近原则:先找自己,再按outer指针找上级
- 闭包的力量:函数永远记得自己出生时的环境
💡 最佳实践心法
// 好的作用域设计就像好的家风
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代码!