“变量、作用域、闭包”的版本间差异
		
		
		
		
		
		跳到导航
		跳到搜索
		
				
		
		
	
 (创建页面,内容为“category:JavaScript  == 变量 ==        === 词法作用域 ===        == 闭包 ==    == <span style="color: green">'''一个有意思的例子'''</span> ==  一个涉及到“Event Loop”和“var 作用域”的例子。  对于以下代码: : <syntaxhighlight lang="JavaScript" highlight=""> for (var i = 0; i < 10; i++) {     setTimeout(function() { console.log(i) }, 100 * i) } </syntaxhighlight> : 是否预期输出: : <syntaxhighlight lang="bash"…”)  | 
				|||
| (未显示同一用户的11个中间版本) | |||
| 第1行: | 第1行: | ||
[[category:JavaScript]]  | [[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 group="Wikioe" name="lexical-environment"/><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>:即“词法分析时生成的作用域”,'''根据源代码中“声明变量的位置”来确定该变量在何处可用'''  | |||
== <span style="color:   | 示例:  | ||
: <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 作用域'''”、“'''闭包'''”(“'''词法环境'''”)的例子。  | |||
对于以下代码:  | 对于以下代码:  | ||
| 第30行: | 第269行: | ||
}  | }  | ||
</syntaxhighlight>  | </syntaxhighlight>  | ||
: 是否预期输出:  | : 其输出如何 ❓  | ||
:{| class="wikitable" style="width:100%;"  | |||
|-  | |||
| style="width:50%;" | 是否预期输出:  | |||
: <syntaxhighlight lang="bash" highlight="">  | : <syntaxhighlight lang="bash" highlight="">  | ||
0  | 0  | ||
1  | 1  | ||
2  | 2  | ||
...  | |||
9  | 9  | ||
</syntaxhighlight>  | </syntaxhighlight>    | ||
: 但,实际输出:  | | style="width:50%;" | 但,实际输出:  | ||
: <syntaxhighlight lang="JavaScript" highlight="">  | : <syntaxhighlight lang="JavaScript" highlight="">  | ||
10  | 10  | ||
10  | 10  | ||
10  | 10  | ||
...  | |||
10  | 10  | ||
</syntaxhighlight>  | </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="">  | #: <syntaxhighlight lang="JavaScript" highlight="">  | ||
for (var i = 0; i < 10; i++) {  | for (var i = 0; i < 10; i++) {  | ||
     (function(i) {  |      let v = i  | ||
         setTimeout(function() { console.log(  |     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)  |      })(i)  | ||
}  | }  | ||
</syntaxhighlight>  | </syntaxhighlight>  | ||
#  | # 通过“'''向函数传递变量作为参数'''”: —— 【没有不必要的闭包】  | ||
#  | #: <syntaxhighlight lang="JavaScript" highlight="">  | ||
for (var i = 0; i < 10; i++) {  | for (var i = 0; i < 10; i++) {  | ||
     setTimeout(function() {   |      setTimeout(function(v) { console.log(v) }, 100 * i, i)  | ||
}  | }  | ||
</syntaxhighlight>  | </syntaxhighlight>  | ||
#   | #: 等同于:  | ||
#: <syntaxhighlight lang="JavaScript" highlight="">  | #: <syntaxhighlight lang="JavaScript" highlight="">  | ||
for (  | 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>  | </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">参考:【'''[[JS 执行过程:执行上下文、词法环境]]'''】</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/>  | |||
2023年4月18日 (二) 07:34的最新版本
变量[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][Wikioe 1][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 函数作用域”
 
⭐ 一个声明(定义变量、函数等)的“词法作用域”就是其“定义时的作用域” —— 注意:并不是“调用时的作用域”
示例:
var x = 0 function foo() { var x = 1 bar() } function bar() { console.log(x) // 0 } foo()
- 由于“词法作用域”(定义时的作用域)决定了:foo、bar 的上级作用域是“全局作用域”,所以 bar 中使用的 x 来自“全局作用域”而非“foo 函数作用域”
 
欺骗“词法作用域”
虽然“词法作用域”在代码编写时就确定,但仍然可以修改:
- 通过 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]
闭包(closure):函数以及其捆绑的词法环境(lexical environment)[Wikioe 1]的组合 —— “词法环境”包含了这个闭包创建时作用域内的任何局部变量
在 JavaScript 中,闭包会随着函数的创建而被同时创建。
示例:
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
- 以上代码包含两个闭包 add5、add10,二者:
- 共享相同的“函数定义”:makeAdder
 - 保存不同的“词法环境”:add5 中 x 为 5,add10 中 x 为 10
 
 
简单来说,闭包让开发者可以从内部函数访问外部函数的作用域
一个有意思的例子[TypeScript 1]
一个涉及到“Event Loop”、“var 作用域”、“闭包”(“词法环境”)的例子。
对于以下代码:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i) }, 100 * i) }
- 其输出如何 ❓
 
是否预期输出: 0 1 2 ... 9
但,实际输出: 10 10 10 ... 10
原因分析:
- 【var 作用域】:var 变量的作用域在 for 外部 —— 每次循环使用的都是同一个 
i- var 变量是没有“块作用域”的 !!!
 
 - 【闭包】:for 循环将会创建 10 个闭包,它们:
- 共享相同的“函数定义”:
function() { console.log(i) } - 共享相同的“词法环境”(“变量环境”),其“环境记录器”将使用同一个 var 变量 —— 
i 
 - 共享相同的“函数定义”:
 - 【Event Loop】:setTimeout 是一个任务,将在 for 所有循环完成之后执行 —— 而 
i在所有循环之后其值为 10 
综上:各个闭包将在 for 所有循环完成之后,使用同一个  i  值(10)来执行函数。
解决问题有多种方式: 总之,使闭包不再使用相同的词法环境[Wikioe 1] 即可 思路一: 若“函数”执行依赖于“外部变量”,则使“词法环境”的“外部环境引用”不同 思路二: 若“函数”执行依赖于“内部变量 / 参数”,则使“词法环境”的“环境记录”不同
思路一:函数的执行依赖于“外部变量”,则“使用 let / const(块作用域)”保证每次迭代的“词法环境”的“外部环境引用”不同
- 使用 let 替换 var:
for (let i = 0; i < 10; i++) { setTimeout(function() { console.log(i) }, 100 * i) }
 - 使用 let/const 捕获 var:
for (var i = 0; i < 10; i++) { let v = i setTimeout(function() { console.log(v) }, 100 * v) }
 
思路二:若“函数”执行依赖于“参数”,则“使用参数捕获外部变量”保证每次迭代的“词法环境”的“环境记录”不同
- 通过“立即执行的函数表达式(IIFE)”[MDN 10][javascript.info 2]: —— 【将产生不必要的闭包(嵌套闭包)】
for (var i = 0; i < 10; i++) { (function(v) { setTimeout(function() { console.log(v) }, 100 * v) })(i) }
 - 通过“向函数传递变量作为参数”: —— 【没有不必要的闭包】
for (var i = 0; i < 10; i++) { setTimeout(function(v) { console.log(v) }, 100 * i, i) }
- 等同于:
 function fn(v) { console.log(v) } for (var i = 0; i < 10; i++) { setTimeout(fn, 100 * i, i) }
 
补充内容
var 没有“块作用域”[javascript.info 2]
⭐ var 变量没有“块作用域”:将会使用(块所在的)“函数作用域”或“全局作用域” —— 这是因为在早期的 JavaScript 中,块没有词法环境[Wikioe 1]
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
示例二:
function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined
- 由于变量提升:即使 var 定义的代码块永远不会执行,变量也存在,但初始化不会执行
 
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 引擎执行过程更清楚
什么是“隐式声明的全局变量”?
隐式声明的全局变量:直接对一个未声明的变量赋值,无论在函数内外都会将其“隐式地”声明为一个“全局变量”
示例,以下变量全都是“隐式声明的全局变量”:
x = '?' function fn() { x = 'hello' y = 'bye' } fn() console.log(x, y) // hello bye
严格模式下,“对一个未声明的变量赋值”将抛出 ReferenceError
- 非严格模式:
 
var x = 0; function f() { var x = y = 1; } f(); console.log(x, y); // 0 1
- 严格模式:
 
'use strict'; var x = 0; function f() { var x = y = 1; // ReferenceError: y is not defined } f(); console.log(x, y);
参考
Wikioe
- ↑ 1.0 1.1 1.2 1.3 参考:【JS 执行过程:执行上下文、词法环境】
 
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:《闭包》
 - ↑ 参考:MDN:《IIFE(立即调用函数表达式)》