核心内容摘要
抖音批量取消关注神器:5行JS代码搞定,告别手动操作(附详细步骤)
为什么 JavaScript 的函数总能清楚地记住变量在哪里被定义为什么闭包如此神奇这一切的答案都隐藏在词法作用域这个核心概念中。
前言从一道经典面试题说起var a 1; function outer() { var a 2; function inner() { console.log(a); } return inner; } var innerFunc outer(); innerFunc(); // 输出什么为什么大多数前端开发者都知道输出结果为2但能完整解释为什么的人却不多。
本篇文章就来彻底揭开JavaScript作用域的神秘面纱。
什么是词法作用域静态作用域 vs 动态作用域词法作用域Lexical Scope也称为静态作用域是 JavaScript 采用的作用域模型。
它的核心特点是函数的作用域在函数定义时就确定了而不是在函数调用时确定。
这与动态作用域形成鲜明对比。
让我们通过代码理解两者的区别var value global; function foo() { console.log(value); } function bar() { var value local; foo(); // 输出什么 } bar();上述代码的输出结果是global。
因为foo()函数在定义时它的作用域链就已确定包含全局作用域。
所以它访问的是全局的value变量而不是调用位置的value。
如果JavaScript动态作用域实际上不是又会发生什么呢var value global; function foo() { console.log(value); // 动态作用域下访问调用位置的value } function bar() { var value local; foo(); // 动态作用域下会输出local } bar();关键区别
总结词法作用域函数的作用域由定义位置决定。
动态作用域函数的作用域由调用位置决定。
词法环境的结构在 JavaScript 引擎内部每个执行上下文都有一个关联的词法环境Lexical Environment。
词法环境由两部分组成环境记录器EnvironmentRecord和对外部词法环境的引用Outer。
LexicalEnvironment { EnvironmentRecord: { //
环境记录器存储变量和函数声明 // 包含声明式环境记录、对象环境记录 }, Outer: null | 父级词法环境引用 //
对外部词法环境的引用 } // 实际代码示例 var globalVar global; function outer() { var outerVar outer; function inner() { var innerVar inner; // inner函数的词法环境 // { // EnvironmentRecord: { innerVar: inner }, // Outer: outer函数的词法环境 // } } }作用域链的形成过程作用域链就是由这些词法环境通过Outer引用连接起来的链式结构。
作用域链的查找机制变量查找的完整流程当 JavaScript 引擎需要访问一个变量时它会按照以下步骤进行查找// 多层嵌套作用域示例 var a global a; var b global b; var c global c; function level1() { var a level1 a; var b level1 b; function level2() { var a level2 a; function level3() { var a level3 a; console.log(a); // level3 a - 找到最近的a console.log(b); // level1 b - 向上两层找到b console.log(c); // global c - 向上三层找到c } level3(); } level2(); } level1();查找变量c的过程如下检查level3的环境记录 → 没有c通过Outer引用检查level2的环境记录 → 没有c通过Outer引用检查level1的环境记录 → 没有c通过Outer引用检查全局环境记录 → 找到c global c如果一直找到最外层都没找到undefined图解作用域链的树状结构让我们用可视化方式理解作用域链全局词法环境 (Global Lexical Environment) ├─ EnvironmentRecord: { a: global a, b: global b, c: global c } ├─ Outer: null │ ├─ level1函数词法环境 (调用时创建) │ ├─ EnvironmentRecord: { a: level1 a, b: level1 b } │ ├─ Outer: 引用 → 全局词法环境 │ │ │ ├─ level2函数词法环境 (调用时创建) │ │ ├─ EnvironmentRecord: { a: level2 a } │ │ ├─ Outer: 引用 → level1词法环境 │ │ │ │ │ ├─ level3函数词法环境 (调用时创建) │ │ │ ├─ EnvironmentRecord: { a: level3 a } │ │ │ ├─ Outer: 引用 → level2词法环境 │ │ │ └─ 变量查找路径level3 → level2 → level1 → 全局 │ │ └─ │ └─ └─作用域链的关键特性静态性词法作用域作用域链在函数定义时就已经确定而不是在调用时确定的。
链式结构像链条一样一环扣一环从当前作用域指向外层作用域。
单向性只能从内层作用域访问外层作用域的变量不能反向访问。
与执行上下文相关每次函数调用都会创建新的执行上下文但作用域链基于函数定义位置确定。
闭包与作用域链的持久化闭包的本质就是函数记住了它被创建时的词法环境function createCounter() { let count 0; // 这个变量本该在函数执行后销毁 return function() { count; // 保持对外部变量的引用这就是闭包 return count; }; } const counter createCounter(); // 即使createCounter执行完毕它的词法环境也不会被销毁 // 因为返回的内部函数仍然引用着它 console.log(counter()); // 1 console.log(counter()); // 2块级作用域的
实现原理ES5作用域的问题在ES5中只有两种作用域全局作用域和函数作用域。
这导致了一些问题// ES5的问题变量提升和缺少块级作用域 function problematic() { console.log(i); // undefined而不是ReferenceError for (var i 0; i 3; i) { // i在整个函数内都可见 setTimeout(function() { console.log(i); // 全部输出3 },
; } console.log(i); // 3循环结束后的i } problematic();ES6引入的let/const带来了真正的块级作用域// 块级作用域示例 function withBlockScope() { if (true) { // 块级作用域开始 let blockScoped 只在块内有效; const constantValue 常量; { // 嵌套块级作用域 let nestedBlock 嵌套块; console.log(blockScoped); // 可以访问外层块的变量 } // console.log(nestedBlock); // ReferenceError } // console.log(blockScoped); // ReferenceError }let/const的
实现原理在编译阶段let/const声明的变量被记录在词法环境中在变量声明之前访问会抛出错误暂时性死区每个{}代码块都会创建一个新的词法环境块级作用域的嵌套结构// 多层块级作用域 { let a 外层块 a; const b 外层块 b; { let a 内层块 a; // 可以重新声明因为不同块 console.log(a); // 内层块 a console.log(b); // 外层块 b - 可以访问外层 { console.log(a); // 内层块 a console.log(b); // 外层块 b } } console.log(a); // 外层块 a }其词法环境结构如下外层块词法环境: { a: 外层块 a, b: 外层块 b, Outer: 全局 } ↓ 内层块词法环境: { a: 内层块 a, Outer: 外层块词法环境 } ↓ 最内层块词法环境: { Outer: 内层块词法环境 }暂时性死区Temporal Dead Zone{ // TDZ开始 console.log(myVar); // undefined console.log(myLet); // ReferenceError var myVar var变量; let myLet let变量; // TDZ结束 }上述实际执行过程简化进入块级作用域创建词法环境var声明被提升初始值为 undefinedlet声明被记录但未初始化在TDZ中不可调用在let初始化前访问 → ReferenceError常见面试题解析多级嵌套作用域var x 10; function foo() { console.log(x); } function bar() { var x 20; foo(); } bar(); // 输出什么上述代码输出结果为10foo函数定义在全局作用域。
因此foo的词法作用域链foo作用域 → 全局作用域。
foo在定义时就确定了作用域链与调用位置无关。
foo中访问x时在自身作用域没找到到全局作用域找到x10。
闭包与循环function createFunctions() { var result []; for (var i 0; i 3; i) { result[i] function() { return i; }; } return result; } var funcs createFunctions(); console.log(funcs[0]()); // 3 console.log(funcs[1]()); // 3 console.log(funcs[2]()); // 3详细解析过程与解决方案可以查看这篇文章JavaScript内存管理揭秘变量究竟存在哪里复杂的嵌套作用域var a 1; function test() { var a 2; function innerTest() { var a 3; return function() { console.log(a); console.log(this.a); }; } var obj { a: 4, getFunc: innerTest() }; return obj.getFunc; } var func test(); func();上述代码的输出结果是3 1:func是innerTest返回的匿名函数匿名函数定义在innerTest内部所以它的词法作用域链匿名函数作用域 →innerTest作用域(a
→test作用域(a
→ 全局(a
console.log(a)在自身作用域没找到到innerTest找到a3console.log(this.a)this指向全局window对象输出全局a1思考题如果JavaScript采用动态作用域而不是词法作用域会有什么影响闭包还能工作吗结语JavaScript的词法作用域机制既是其强大之处也是初学者容易困惑的地方。
深入理解这一机制不仅能帮助你写出更好的代码还能在面试中游刃有余地解答相关题目。
对于文章中错误的地方或者有任何问题欢迎在评论区留言讨论原文 https://juejin.cn/post/75999128