相关系列: 从零开始的前端筑基之旅(面试必备,持续更新~)javascript
这部分原本打算放到简单介绍的执行上下文和执行栈里顺带说一句的,后来发现这里面内容也很多,包括暂时性死区、函数及变量提高逻辑、es6中的块级做用域等,就单开了一章。如下内容大概花费10分钟左右,欢迎评论补充知识和点赞~前端
请说出 let,const,var 的区别java
大部分的回答是这样的,node
而实际上, let / const
也有变量提高 。git
先来看个栗子:es6
console.log(aVar); // undefined
console.log(aLet); // causes ReferenceError: aLet is not defined
var aVar = 1;
let aLet = 2;
复制代码
从结果上看,第二行没有找到aLet致使程序报错,代表let声明的变量并无提高github
没关系,再看两个栗子(请在浏览器环境运行,node环境结果不同)面试
let x = 'global';
function func(){
console.log(x);
}
func(); // global
复制代码
func运行时,在函数内没有找到 x 的定义,沿着函数做用域链寻到外层关于 x 的定义。浏览器
let x = 'global';
function func1(){
console.log(x);
let x = 'func';
}
func1();
// Uncaught ReferenceError: Cannot access 'x' before initialization
// at func (<anonymous>:2:15)
// at <anonymous>:4:3
复制代码
咦,报错了?为何func1没有访问到全局环境下的x呢?不要着急,仔细看下错误提示:没法在初始化以前访问x。函数
好好想一想,这个错误意味着在func内第一行程序已经知道在本函数内有一个变量叫x了,只不过没有初始化(initialization)而已。
由此可得出结论**,因为 _let x = ‘func’
_ 在函数做用域内存在变量提高,**阻断了函数做用域链的向上延伸。尽管 x 发生了变量提高,可是在初始化赋值前(before initialization)不容许读取。
这就引出了一个很重要的概念: 暂时性死区 (TDZ)
MDN 上关于暂时性死区的定义
let bindings are created at the top of the (block) scope containing the declaration, commonly referred to as “hoisting”. Unlike variables declared with var, which will start with the value undefined, let variables are not initialized until their definition is evaluated. Accessing the variable before the initialization results in a ReferenceError. The variable is in a “temporal dead zone” from the start of the block until the initialization is processed.
let绑定是在包含声明的(块)范围的顶部建立的,一般称为“提高”。不像用var声明的变量,let声明的变量不会被初始化(initialized)直到它们被定义位置的代码开始执行,在初始化以前访问变量会触发一个ReferenceError。从块的开始到变量初始化,变量都处于“暂时死区”。
简单来讲,let 仅仅发生了提高而没有被赋初值,在显式赋值以前,任何对变量的读写都会触发ReferenceError 错误。从代码块(block)起始到变量赋值之前的这块区域,称为该变量的暂时性死区。
当程序控制流程运行到特定做用域(scope ≈ Lexical Environment) 时:即模块,函数,或块级做用域。在该做用域中代码真正执行以前,该做用域中定义的 let 和 const 变量会首先被建立出来,但由于在 let/const 变量被赋值(LexicalBinding)之前是不能够读写的,因此存在暂时性死区。
来看下下面代码验证下你的理解:
function test(){
var foo = 33;
if(foo) {
let foo = (foo + 55); // ReferenceError
}
}
test();
复制代码
因为词法做用域,表达式let foo = (foo + 55);中的foo被认为是if块中声明的foo,而不是函数第一行声明的var变量。在同一行中,if块的foo已经在词法环境中建立,因此程序不会沿着做用域链向上层寻找foo,但因为变量还未初始化,处于暂时性死区中,访问会触发ReferenceError。
function go(n) {
// n here is defined!
console.log(n); // Object {a: [1,2,3]}
const a = n.a;
for (let n of n.a) { // ReferenceError
console.log(n);
}
}
go({a: [1, 2, 3]});
复制代码
答案已经给了,欢迎在评论区留下你的看法
回到正题,从新看一下变量提高的逻辑.
当进入执行上下文时,
依据上述规则的逻辑,分析下列代码:
代码一:
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
// 依据规则二,函数表达式不会提高
// 依据规则三,相同变量名称不会干扰
复制代码
来看这段代码:
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
// 依据规则二,函数声明中若函数名相同,则后者彻底覆盖前者
复制代码
举一个小栗子巩固一下:
var a = 1;
function foo() {
a = 10;
console.log(a);
function a() {};
}
foo(); // 10
console.log(a); // 1
复制代码
在foo中,函数a存在变量提高,至关于
var a = 1; // 定义一个全局变量 a
function foo() {
// 提高函数声明function a () {}到函数做用域顶端, 函数a也是变量
var a = function () {}; // 定义局部变量 a 并赋值。
a = 10; // 修改局部变量 a 的值
console.log(a); // 打印局部变量 a 的值:10
return;
}
foo();
console.log(a); // 打印全局变量 a 的值:1
复制代码
ES5 只有全局做用域和函数做用域,没有块级做用域。
ES6 的块级做用域必须有大括号,若是没有大括号,JavaScript 引擎就认为不存在块级做用域。
ES6 引入了块级做用域,明确容许在块级做用域之中声明函数。ES6 规定,块级做用域之中,函数声明语句的行为相似于let,在块级做用域以外不可引用。
容许在块级做用域内声明函数。
函数声明相似于var,即会提高到全局做用域或函数做用域的头部。
同时,函数声明还会提高到所在的块级做用域的头部。
友情提示,本篇文章建议与让人恍然大悟的词法做用域及做用域链讲解和简单介绍的执行上下文和执行栈一块儿食用
ps: 我原本觉得暂时性死区是因为建立执行上下文的方式致使的,结果搜资料的时候发现块级做用域没有单独的执行上下文,只有词法环境,若你知道块级做用域与词法环境的相关知识,欢迎在评论区留言,我会及时补充进来
相关系列: 从零开始的前端筑基之旅(面试必备,持续更新~)
若是你收获了新知识,请给做者点个赞吧~
参考文档: