“变量、作用域、闭包”的版本间差异
跳到导航
跳到搜索
无编辑摘要 |
|||
第163行: | 第163行: | ||
<span style="color: green">'''词法作用域(Lexical Scope)'''</span>:即“词法分析时生成的作用域”,根据源代码中“声明变量的位置”来确定该变量在何处可用 | <span style="color: green">'''词法作用域(Lexical Scope)'''</span>:即“词法分析时生成的作用域”,根据源代码中“声明变量的位置”来确定该变量在何处可用 | ||
示例: | |||
: <syntaxhighlight lang="JavaScript" highlight=""> | : <syntaxhighlight lang="JavaScript" highlight=""> | ||
let x = "Eijux"; | let x = "Eijux"; | ||
第178行: | 第178行: | ||
==== 欺骗“词法作用域” ==== | ==== 欺骗“词法作用域” ==== | ||
<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=""> | #: <syntaxhighlight lang="JavaScript" highlight=""> | ||
var num = 10 | var num = 10 | ||
var str = "var num = 20" | var str = "var num = 20" | ||
第194行: | 第197行: | ||
fn(str) | 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> | </syntaxhighlight> | ||
== 闭包 == | == 闭包<ref group="MDN" name="closure"/><ref group="javascript.info" name="closure"/> == | ||
第202行: | 第219行: | ||
== 补充内容 == | == 补充内容 == | ||
=== var 没有“块作用域”<ref group="javascript.info" name="var"/> === | === '''var 没有“块作用域”'''<ref group="javascript.info" name="var"/> === | ||
<big>⭐</big> <span style="color: blue">'''var 变量没有“块作用域”''':将会使用(块所在的)“函数作用域”或“全局作用域”</span> —— 这是因为在早期的 JavaScript 中,块没有<span style="color: red">'''词法环境'''</span> | <big>⭐</big> <span style="color: blue">'''var 变量没有“块作用域”''':将会使用(块所在的)“函数作用域”或“全局作用域”</span> —— 这是因为在早期的 JavaScript 中,块没有<span style="color: red">'''词法环境'''</span> | ||
第315行: | 第332行: | ||
<references group="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="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="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="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="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="Scope">参考:[https://developer.mozilla.org/zh-CN/docs/Glossary/Scope MDN:《Scope(作用域)》]</ref> | ||
<ref group="MDN" name=" | <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> | |||
</references> | </references> | ||
=== javascript.info === | === javascript.info === | ||
<references group="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="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> | </references> | ||
=== 其他 === | === 其他 === | ||
<references/> | <references/> |
2023年4月16日 (日) 17:32的版本
变量[MDN 1]
ECMAScript 6 中引入了 let、const。
JavaScript 有三种声明方式:
- var:声明一个变量,初始化可选。
- let:声明一个变量,初始化可选。
- const:声明一个常量,必须初始化。
“局部变量”/“全局变量”只与其定义位置有关:在“函数内部”/“函数之外”声明的变量
var:变量提升[MDN 2]
提升(Hoisting):变量和函数的声明会在物理层面(在编译阶段被放入内存中)移动到代码的最前面 —— 变量/函数的“初始化”和“使用”可以在“声明”之前(先使用再声明)。 函数和变量相比,会被优先提升 —— 函数会被提升到更靠前的位置。
- 变量提升[MDN 3]:
bla = 2 var bla; // 可以理解为: var bla; bla = 2;
- 函数提升[MDN 4]:
hoisted(); // logs "foo" function hoisted() { console.log('foo'); } // 可以理解为: function hoisted() { console.log('foo'); } hoisted(); // logs "foo"
⭐ JavaScript 只会提升声明,不会提升其初始化 —— 如果在“声明”和“初始化”之前“使用”,将使用 undefined
- 示例:
// “使用”先于“初始化” console.log(num); // Returns undefined var num; num = 6; // “初始化”先于“使用” num = 6; console.log(num); // returns 6 var num;
⭐ 函数表达式(function expressions)不会被提升
- 示例:
notHoisted(); // TypeError: notHoisted is not a function var notHoisted = function() { console.log('bar'); };
let:暂时性死区[MDN 5]
暂时性死区(Temporal dead zone,TDZ):从“代码块的开始”直到“声明变量的行”,let 或 const 声明的变量都处于“暂时性死区”中 —— 这并不是“提升”!!!
- 在“暂时性死区”中访问变量将抛出 ReferenceError:
{ // 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) }
- “暂时性死区”取决于“执行顺序”(时间),而非“代码顺序”(位置):
{ // TDZ starts at beginning of scope const func = () => console.log(letVar); // OK let letVar = 3; // End of TDZ (for letVar) func(); // Called outside TDZ! }
var 与 let 的区别[MDN 3][MDN 5]
\ | var | let |
---|---|---|
块作用域 | ✘ | ✔ |
变量提升 | ✔ | ✘ |
重新声明 | ✔
|
✘
|
作用域[MDN 6][1]
作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问。
- 全局作用域:可被当前文档中的任何其他代码所访问;
- 函数作用域:可被当前函数内部的其他代码所访问;
- 块作用域:可被当前块(由
{ }
包围)内部的其他代码所访问;- 仅 let、const 支持
ECMAScript 6 之前没有“块作用域”:语句块中声明的变量将成为语句块所在函数(或全局作用域)的局部变量。
作用域链
作用域链:由于作用域的层层嵌套而形成的关系。
作用域链用于变量的查找过程:
var x = 10 function outer() { var x = 20 function inner() { console.log(x) } inner() } outer() // 20
- 以上代码,包括三个作用域:
- 全局作用域:包含变量 x(=10)
- 函数作用域(outer):包含变量 x(=20)
- 函数作用域(inner):不包含变量
- 查找过程:
- console.log(x) 将在“函数作用域(inner)”中查找变量“x”:失败
- 在“函数作用域(outer)”查找变量“x”:成功(x = 20)
- 使用该变量:输出 20
作用域模型:词法作用域[2]
从语言的层面来说,作用域模型分两种:(根据作用域的生成时机)
- 静态作用域:在代码编写时完成划分,作用域沿以定义位置向外延伸
- 也称“词法作用域”,是最为普遍的一种作用域模型
- 动态作用域:在代码运行时完成划分,作用域链以调用栈向外延伸
- 由 bash、Perl 等语言所使用
词法作用域(Lexical Scope):即“词法分析时生成的作用域”,根据源代码中“声明变量的位置”来确定该变量在何处可用
示例:
let x = "Eijux"; function getX() { return x; } console.log(getX()) // Eijux
- 变量 x 的“词法作用域”为“全局作用域”(定义时的作用域)而非“getX 函数作用域”
⭐ 一个声明(定义变量、函数等)的词法作用域就是其“定义时的作用域” —— 注意:并不是“调用时的作用域”
欺骗“词法作用域”
虽然“词法作用域”在代码编写时就确定,但仍然可以修改:
- 通过 eval() 修改词法作用域:
var num = 10 var str = "var num = 20" function fn(str) { eval(str) console.log(num) } fn(str)
- 通过 with 修改词法作用域:
var x = { a: 1 } function fn(obj) { with (obj) { a = 2 } } console.log(x.a) // 1 fn(x) console.log(x.a) // 2
闭包[MDN 9][javascript.info 1]
补充内容
var 没有“块作用域”[javascript.info 2]
⭐ var 变量没有“块作用域”:将会使用(块所在的)“函数作用域”或“全局作用域” —— 这是因为在早期的 JavaScript 中,块没有词法环境
var 将透传 if、for 和其它代码块:
if (true) { var x = 5; } console.log(x); // 5 if (true) { let y = 5; } console.log(y); // ReferenceError: y is not defined
let 会不会“提升”?
关于这一点,MDN 都描述得前后不一致,也是很操蛋了…… 1、MDN:《语法和数据类型》[MDN 1]: 在 ECMAScript 6 中,let和const同样会被提升变量到代码块的顶部但是不会被赋予初始值。在变量声明之前引用这个变量,将抛出引用错误(ReferenceError)。这个变量将从代码块一开始的时候就处在一个“暂时性死区”,直到这个变量被声明为止。 2、MDN:《语法和数据类型》[MDN 5]: var 和 let 的另一个重要区别,let 声明的变量不会在作用域中被提升,它是在编译时才初始化(参考下面的暂时性死区)。
两种说法,以一个类似示例来说明:
var foo = 33; if (foo) { console.log(foo) // ReferenceError: Cannot access 'foo' before initialization let foo = 44; }
对于 ReferenceError 该如何解释呢 ❓
- 说法一:由于 let 变量提升,导致此处使用“块作用域内定义变量”,但该变量处于“暂时性死区”,所以出现此错误。
- 网上有内容更是强行将 let 变量创建分为三个过程:创建、初始化、赋值 …… 有点扯。
- 说法二:由于词法作用域,导致此处使用“块作用域内定义变量”,但该变量处于“暂时性死区”,所以出现此错误。
💡 个人理解:let 并不会提升,以上示例是“词法作用域”影响。—— 或许看看 JS 引擎执行过程更清楚
一个有意思的例子
一个涉及到“Event Loop”和“var 作用域”的例子。
对于以下代码:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i) }, 100 * i) }
- 是否预期输出:
0 1 2 3 4 5 6 7 8 9
- 但,实际输出:
10 10 10 10 10 10 10 10 10 10
其原因有两点:
- setTimeout 是一个任务,将在 for 所有循环完成之后执行;
- i 由 var 定义:所有的 setTimeout 实际上都引用了“相同作用域里的同一个 i” —— 而它在所有循环之后其值为 10;
要与预期输出一致,有两种方式:
- 通过使用“立即执行的函数表达式”来捕获 i:
for (var i = 0; i < 10; i++) { (function(i) { setTimeout(function() { console.log(i) }, 100 * i) })(i) }
- 不能妄想通过 let、const 来捕获:
for (var i = 0; i < 10; i++) { setTimeout(function() { const v = i; console.log(i); }, 100 * i) }
- 使用 let 替换 var:
for (let i = 0; i < 10; i++) { (function(i) { setTimeout(function() { console.log(i) }, 100 * i) })(i) }
var作用域或函数作用域 当用let声明一个变量,它使用的是词法作用域或块作用域。
参考
MDN
- ↑ 1.0 1.1 参考:MDN:《语法和数据类型》
- ↑ 参考:MDN:《Hoisting(变量提升)》
- ↑ 3.0 3.1 参考:MDN:《var》
- ↑ 参考:MDN:《function》
- ↑ 5.0 5.1 5.2 参考:MDN:《let》
- ↑ 参考:MDN:《Scope(作用域)》
- ↑ 参考:MDN:《eval()》
- ↑ 参考:MDN:《with》
- ↑ 参考:MDN:《闭包》