查看“变量、作用域、闭包”的源代码
←
变量、作用域、闭包
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
[[category:JavaScript]] == 变量<ref group="MDN" name="Variables"/> == ECMAScript 6 中引入了 let、const。 JavaScript 有三种声明方式: # '''var''':声明一个'''变量''',初始化可选。 # '''let''':声明一个'''变量''',初始化可选。 # '''const''':声明一个'''常量''',必须初始化。 “局部变量”/“全局变量”只与其定义位置有关:在“函数内部”/“函数之外”声明的变量 === var:变量提升<ref group="MDN" name="Hoisting"/> === <big>'''提升(Hoisting)'''</big>:变量和函数的'''声明'''会在物理层面(在编译阶段被放入内存中)移动到代码的最前面 —— 变量/函数的“初始化”和“使用”可以在“声明”之前(先使用再声明)。 函数和变量相比,会被优先提升 —— 函数会被提升到更靠前的位置。 # '''变量提升'''<ref group="MDN" name="var"/>: #: <syntaxhighlight lang="JavaScript" highlight=""> bla = 2 var bla; // 可以理解为: var bla; bla = 2; </syntaxhighlight> # '''函数提升'''<ref group="MDN" name="function"/>: #: <syntaxhighlight lang="JavaScript" highlight=""> hoisted(); // logs "foo" function hoisted() { console.log('foo'); } // 可以理解为: function hoisted() { console.log('foo'); } hoisted(); // logs "foo" </syntaxhighlight> <big>⭐</big> <span style="color: blue">'''JavaScript 只会提升声明,不会提升其初始化'''</span> —— 如果在“声明”和“初始化”之前“使用”,将使用 <span style="color: red">'''undefined'''</span> : 示例: : <syntaxhighlight lang="JavaScript" highlight=""> // “使用”先于“初始化” console.log(num); // Returns undefined var num; num = 6; // “初始化”先于“使用” num = 6; console.log(num); // returns 6 var num; </syntaxhighlight> <big>⭐</big> <span style="color: blue">'''函数表达式(function expressions)不会被提升'''</span> : 示例: : <syntaxhighlight lang="JavaScript" highlight=""> notHoisted(); // TypeError: notHoisted is not a function var notHoisted = function() { console.log('bar'); }; </syntaxhighlight> === let:暂时性死区<ref group="MDN" name="let"/> === <big>'''暂时性死区(Temporal dead zone,TDZ)'''</big>:从“代码块的开始”直到“声明变量的行”,'''let''' 或 '''const''' 声明的变量都处于“暂时性死区”中 —— 这并不是“提升”!!! # 在“暂时性死区”中访问变量将抛出 '''ReferenceError''': #: <syntaxhighlight lang="JavaScript" highlight=""> { // TDZ starts at beginning of scope console.log(bar); // undefined console.log(foo); // ReferenceError var bar = 1; let foo = 2; // End of TDZ (for foo) } </syntaxhighlight> # “暂时性死区”取决于“'''执行顺序'''”(时间),而非“代码顺序”(位置): #: <syntaxhighlight lang="JavaScript" highlight=""> { // TDZ starts at beginning of scope const func = () => console.log(letVar); // OK let letVar = 3; // End of TDZ (for letVar) func(); // Called outside TDZ! } </syntaxhighlight> === var 与 let 的区别<ref group="MDN" name="var"/><ref group="MDN" name="let"/> === {| class="wikitable" |+ var 与 let |- ! \ !! var !! let |- | '''块作用域''' || <span style="color: red">✘</span> || <span style="color: green">✔</span> |- | '''变量提升''' || <span style="color: green">✔</span> || <span style="color: red">✘</span> |- | '''重新声明''' | <span style="color: green">✔</span> : <syntaxhighlight lang="JavaScript" highlight=""> var user = "Pete"; var user = "John"; console.log(user); // John </syntaxhighlight> | <span style="color: red">✘</span> : <syntaxhighlight lang="JavaScript" highlight=""> let user; let user; // SyntaxError: 'user' has already been declared </syntaxhighlight> |} == 作用域<ref group="MDN" name="Scope"/><ref>参考:[https://juejin.cn/post/7069578126979760158 《【JavaScript】深入理解JS中的词法作用域与作用域链》]</ref> == <big>'''作用域'''</big>是当前的执行上下文,值和表达式在其中“可见”或可被访问。 # <span style="color: green">全局作用域</span>:可被当前'''文档'''中的任何其他代码所访问; # <span style="color: green">函数作用域</span>:可被当前'''函数'''内部的其他代码所访问; # <span style="color: green">'''块作用域'''</span>:可被当前'''块'''(由 '''<code>{ }</code>''' 包围)内部的其他代码所访问; #* 仅 let、const 支持 ECMAScript 6 之前没有“块作用域”:语句块中声明的变量将成为语句块所在函数(或全局作用域)的局部变量。 === 作用域链 === <big>'''作用域链'''</big>:由于作用域的层层嵌套而形成的关系。 作用域链用于变量的查找过程: : <syntaxhighlight lang="JavaScript" highlight=""> var x = 10 function outer() { var x = 20 function inner() { console.log(x) } inner() } outer() // 20 </syntaxhighlight> : 以上代码,包括三个作用域: :# 全局作用域:包含变量 x(=10) :# 函数作用域(outer):包含变量 x(=20) :# 函数作用域(inner):不包含变量 : 查找过程: :# console.log(x) 将在“函数作用域(inner)”中查找变量“x”:失败 :# 在“函数作用域(outer)”查找变量“x”:成功(x = 20) :# 使用该变量:输出 20 === 作用域模型:<span style="color: blue">'''词法作用域'''</span><ref>参考:[https://www.freecodecamp.org/chinese/news/javascript-lexical-scope-tutorial/ 《JavaScript 中的词法作用域——JS 中的作用域究竟是什么?》]</ref> === <div class="mw-code" style="white-space: normal; line-height: 1.6;"> 从语言的层面来说,作用域模型分两种:(根据作用域的生成时机) # '''静态作用域''':在'''代码编写时'''完成划分,作用域沿以'''定义位置'''向外延伸 #* 也称“<span style="color: green">'''词法作用域'''</span>”,是最为普遍的一种作用域模型 # '''动态作用域''':在'''代码运行时'''完成划分,作用域链以'''调用栈'''向外延伸 #* 由 bash、Perl 等语言所使用 </div> <span style="color: green">'''词法作用域(Lexical Scope)'''</span>:即“词法分析时生成的作用域”,'''根据源代码中“声明变量的位置”来确定该变量在何处可用''' 示例: : <syntaxhighlight lang="JavaScript" highlight=""> let x = "Eijux"; function getX() { return x; } console.log(getX()) // Eijux </syntaxhighlight> : 变量 x 的“词法作用域”为“全局作用域”(定义时的作用域)而非“getX 函数作用域” <big>⭐</big> 一个声明(定义变量、函数等)的“词法作用域”就是其“<span style="color: blue; font-size: 120%">'''定义时的作用域'''</span>” —— 注意:<span style="color: red">'''并不是“调用时的作用域”'''</span> 示例: : <syntaxhighlight lang="JavaScript" highlight=""> var x = 0 function foo() { var x = 1 bar() } function bar() { console.log(x) // 0 } foo() </syntaxhighlight> : 由于“词法作用域”(定义时的作用域)决定了:foo、bar 的上级作用域是“全局作用域”,所以 bar 中使用的 x 来自“全局作用域”而非“foo 函数作用域” ==== 欺骗“词法作用域” ==== <div class="mw-code" style="white-space: normal; line-height: 1.6;"> 虽然“词法作用域”在代码编写时就确定,但仍然可以修改: # '''eval()'''<ref group="MDN" name="eval"/>:将传入的字符串当做 JavaScript 代码进行执行 #*有性能和安全隐患,'''不建议使用''' # '''with'''<ref group="MDN" name="with"/>:扩展一个语句的作用域链 #* 有语义不明和兼容问题,'''已弃用''' </div> # 通过 eval() 修改词法作用域: #: <syntaxhighlight lang="JavaScript" highlight=""> var num = 10 var str = "var num = 20" function fn(str) { eval(str) console.log(num) } fn(str) </syntaxhighlight> # 通过 with 修改词法作用域: #: <syntaxhighlight lang="JavaScript" highlight=""> var x = { a: 1 } function fn(obj) { with (obj) { a = 2 } } console.log(x.a) // 1 fn(x) console.log(x.a) // 2 </syntaxhighlight> == 闭包<ref group="MDN" name="closure"/><ref group="javascript.info" name="closure"/> == <big>'''闭包(closure)'''</big>:'''函数'''以及其捆绑的<span style="color: blue">'''词法环境(lexical environment)'''</span><ref group="Wikioe" name="lexical-environment"/>的组合 —— “词法环境”包含了这个闭包创建时作用域内的任何局部变量 '''在 JavaScript 中,闭包会随着函数的创建而被同时创建。''' 示例: : <syntaxhighlight lang="JavaScript" highlight=""> function makeAdder(x) { // 返回一个函数 return function (y) { return x + y } } var add5 = makeAdder(5) // 闭包 var add10 = makeAdder(10) // 闭包 console.log(add5(2)); // 7 console.log(add10(2)); // 12 </syntaxhighlight> : 以上代码包含两个闭包 add5、add10,二者: :# 共享相同的“函数定义”:makeAdder :# 保存不同的“词法环境”:add5 中 x 为 5,add10 中 x 为 10 简单来说,闭包让开发者可以从内部函数访问外部函数的作用域 === <span style="color: blue">'''一个有意思的例子'''</span><ref group="TypeScript" name="variable"/> === 一个涉及到“'''Event Loop'''”、“'''var 作用域'''”、“'''闭包'''”(“'''词法环境'''”)的例子。 对于以下代码: : <syntaxhighlight lang="JavaScript" highlight=""> for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i) }, 100 * i) } </syntaxhighlight> : 其输出如何 ❓ :{| class="wikitable" style="width:100%;" |- | style="width:50%;" | 是否预期输出: : <syntaxhighlight lang="bash" highlight=""> 0 1 2 ... 9 </syntaxhighlight> | style="width:50%;" | 但,实际输出: : <syntaxhighlight lang="JavaScript" highlight=""> 10 10 10 ... 10 </syntaxhighlight> |} 原因分析: # 【var 作用域】:var 变量的作用域在 for 外部 —— 每次循环使用的都是同一个 <code> i </code> #* var 变量是没有“块作用域”的 !!! # 【闭包】:for 循环将会创建 10 个闭包,它们: #: 共享相同的“函数定义”:<code> function() { console.log(i) } </code> #: 共享相同的“词法环境”(“变量环境”),'''其“环境记录器”将使用同一个 var 变量''' —— <code> i </code> # 【Event Loop】:setTimeout 是一个任务,将在 for 所有循环完成之后执行 —— 而 <code> i </code> 在所有循环之后其值为 10 综上:各个闭包将在 for 所有循环完成之后,使用同一个 <code> i </code> 值(10)来执行函数。 解决问题有多种方式: 总之,<span style="color: blue">'''使闭包不再使用相同的词法环境'''</span><ref group="Wikioe" name="lexical-environment"/> 即可 '''思路一: 若“函数”执行依赖于“外部变量”,则使“词法环境”的“外部环境引用”不同''' '''思路二: 若“函数”执行依赖于“内部变量 / 参数”,则使“词法环境”的“环境记录”不同''' '''思路一''':函数的执行依赖于“外部变量”,则“使用 '''let''' / '''const'''(块作用域)”保证每次迭代的“词法环境”的“外部环境引用”不同 # '''使用 let 替换 var''': #: <syntaxhighlight lang="JavaScript" highlight=""> for (let i = 0; i < 10; i++) { setTimeout(function() { console.log(i) }, 100 * i) } </syntaxhighlight> # '''使用 let/const 捕获 var''': #: <syntaxhighlight lang="JavaScript" highlight=""> for (var i = 0; i < 10; i++) { let v = i setTimeout(function() { console.log(v) }, 100 * v) } </syntaxhighlight> '''思路二''':若“函数”执行依赖于“参数”,则“使用参数捕获外部变量”保证每次迭代的“词法环境”的“环境记录”不同 # 通过“'''立即执行的函数表达式(IIFE)'''”<ref group="MDN" name="IIFE"/><ref group="javascript.info" name="var"/>: —— 【将产生不必要的闭包(嵌套闭包)】 #: <syntaxhighlight lang="JavaScript" highlight=""> for (var i = 0; i < 10; i++) { (function(v) { setTimeout(function() { console.log(v) }, 100 * v) })(i) } </syntaxhighlight> # 通过“'''向函数传递变量作为参数'''”: —— 【没有不必要的闭包】 #: <syntaxhighlight lang="JavaScript" highlight=""> for (var i = 0; i < 10; i++) { setTimeout(function(v) { console.log(v) }, 100 * i, i) } </syntaxhighlight> #: 等同于: #: <syntaxhighlight lang="JavaScript" highlight=""> function fn(v) { console.log(v) } for (var i = 0; i < 10; i++) { setTimeout(fn, 100 * i, i) } </syntaxhighlight> == 补充内容 == === '''var 没有“块作用域”'''<ref group="javascript.info" name="var"/> === <big>⭐</big> <span style="color: blue">'''var 变量没有“块作用域”''':将会使用(块所在的)“函数作用域”或“全局作用域”</span> —— 这是因为在早期的 JavaScript 中,块没有<span style="color: red">'''词法环境'''</span><ref group="Wikioe" name="lexical-environment"/> '''var 将透传 if、for 和其它代码块''' 示例一: : <syntaxhighlight lang="JavaScript" highlight=""> if (true) { var x = 5; } console.log(x); // 5 if (true) { let y = 5; } console.log(y); // ReferenceError: y is not defined </syntaxhighlight> 示例二: : <syntaxhighlight lang="JavaScript" highlight=""> function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined </syntaxhighlight> : 由于变量提升:即使 var 定义的代码块永远不会执行,变量也存在,但初始化不会执行 === let 会不会“提升”? === 关于这一点,MDN 都描述得前后不一致,也是很操蛋了…… 1、MDN:《语法和数据类型》<ref group="MDN" name="Variables"/>: 在 ECMAScript 6 中,let和const同样会被提升变量到代码块的顶部但是不会被赋予初始值。在变量声明之前引用这个变量,将抛出引用错误(ReferenceError)。这个变量将从代码块一开始的时候就处在一个“暂时性死区”,直到这个变量被声明为止。 2、MDN:《语法和数据类型》<ref group="MDN" name="let"/>: var 和 let 的另一个重要区别,let 声明的变量不会在作用域中被提升,它是在编译时才初始化(参考下面的暂时性死区)。 两种说法,以一个类似示例来说明: : <syntaxhighlight lang="JavaScript" highlight=""> var foo = 33; if (foo) { console.log(foo) // ReferenceError: Cannot access 'foo' before initialization let foo = 44; } </syntaxhighlight> 对于 ReferenceError 该如何解释呢 ❓ # 说法一:由于 let 变量提升,导致此处使用“块作用域内定义变量”,但该变量处于“暂时性死区”,所以出现此错误。 #: 网上有内容更是强行将 let 变量创建分为三个过程:创建、初始化、赋值 …… 有点扯。 # 说法二:由于'''词法作用域''',导致此处使用“块作用域内定义变量”,但该变量处于“暂时性死区”,所以出现此错误。 <big>💡</big> 个人理解:let 并不会提升,以上示例是“词法作用域”影响。—— 或许看看 JS 引擎执行过程更清楚 === 什么是“隐式声明的全局变量”? === <big>'''隐式声明的全局变量'''</big>:直接对一个未声明的变量赋值,无论在函数内外都会将其“隐式地”声明为一个“全局变量” 示例,以下变量全都是“隐式声明的全局变量”: : <syntaxhighlight lang="JavaScript" highlight=""> x = '?' function fn() { x = 'hello' y = 'bye' } fn() console.log(x, y) // hello bye </syntaxhighlight> 严格模式下,“对一个未声明的变量赋值”将抛出 ReferenceError # 非严格模式: : <syntaxhighlight lang="JavaScript" highlight=""> var x = 0; function f() { var x = y = 1; } f(); console.log(x, y); // 0 1 </syntaxhighlight> # 严格模式: : <syntaxhighlight lang="JavaScript" highlight=""> 'use strict'; var x = 0; function f() { var x = y = 1; // ReferenceError: y is not defined } f(); console.log(x, y); </syntaxhighlight> == 参考 == === Wikioe === <references group="Wikioe"> <ref group="Wikioe" name="lexical-environment">参考:【[[词法环境(lexical environment)]]】</ref> </references> === MDN === <references group="MDN"> <ref group="MDN" name="Variables">参考:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Grammar_and_Types MDN:《语法和数据类型》]</ref> <ref group="MDN" name="Hoisting">参考:[https://developer.mozilla.org/zh-CN/docs/Glossary/Hoisting MDN:《Hoisting(变量提升)》]</ref> <ref group="MDN" name="function">参考:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function MDN:《function》]</ref> <ref group="MDN" name="var">参考:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/var MDN:《var》]</ref> <ref group="MDN" name="let">参考:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let MDN:《let》]</ref> <ref group="MDN" name="Scope">参考:[https://developer.mozilla.org/zh-CN/docs/Glossary/Scope MDN:《Scope(作用域)》]</ref> <ref group="MDN" name="eval">参考:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval MDN:《eval()》]</ref> <ref group="MDN" name="with">参考:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with MDN:《with》]</ref> <ref group="MDN" name="closure">参考:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures MDN:《闭包》]</ref> <ref group="MDN" name="IIFE">参考:[https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE MDN:《IIFE(立即调用函数表达式)》]</ref> </references> === javascript.info === <references group="javascript.info"> <ref group="javascript.info" name="var">参考:[https://zh.javascript.info/var javascript.info:《老旧的 "var"》]</ref> <ref group="javascript.info" name="closure">参考:[https://zh.javascript.info/closure javascript.info:《变量作用域,闭包》]</ref> </references> === TypeScript === <references group="TypeScript"> <ref group="TypeScript" name="variable">参考:[https://www.tslang.cn/docs/handbook/variable-declarations.html TypeScript:《变量声明》]</ref> </references> === 其他 === <references/>
返回至“
变量、作用域、闭包
”。
导航菜单
个人工具
登录
命名空间
页面
讨论
大陆简体
已展开
已折叠
查看
阅读
查看源代码
查看历史
更多
已展开
已折叠
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
笔记
服务器
数据库
后端
前端
工具
《To do list》
日常
阅读
电影
摄影
其他
Software
Windows
WIKIOE
所有分类
所有页面
侧边栏
站点日志
工具
链入页面
相关更改
特殊页面
页面信息