预解析的基本回顾
- 全局作用域:
全局作用域下的预解析步骤:GO 对象(函数执行前一刻产生)
- 寻找变量声明
- 寻找函数声明
- 执行
- 函数作用域:
函数作用域下的预解析步骤:AO 对象(函数执行前一刻产生)
- 寻找变量声明与形式参数
- 实际参数给形式参数赋值
- 寻找函数声明
- 函数执行
let 与块级作用域的产生
ES5 中存在 var 变量声明提升问题,随之带来重复声明变量覆盖问题,ES5 通常通过立即执行函数,产生独立的作用域解决这个问题;针对这种问题 ES6 中推出 let、块级作用域语法,并且遵循 kiss 原则(keep it simple,stupid)。
KISS 原则是指在设计当中应当注重简约的原则。总结工程专业人员在设计过程中的经验,大多数系统的设计应保持简洁和单纯,而不掺入非必要的复杂性,这样的系统运作成效会取得最优;因此简单性应该是设计中的关键目标,尽量回避免不必要的复杂性。同时这原则亦有应用在商业书信、设计电脑软件、动画、工程上。
let 语法产生了块级作用域,let 的本质上就是为了 js 增加一个块级作用域; let 所在的地方,就是块级作用域在的地方。( let 所在的块,会有块级作用域 , let 在全局,全局就是块 , let 在函数,函数就是块 , let 在 for 的不同位置,也会有不同的块 )块级作用域 if(){},for(var i = 0; i < 10; i++){},{}
let 不能在同一个作用域下重复声明
- 在全局作用域或者函数作用域中,利用 var 声明变量,重复声明变量的话,后面声明的变量将覆盖之前声明的变量。
var a = 1;
var a = 2;
console.log(a); // 2
function fn() {
var a = 1;
var a = 2;
console.log(a); // 2
}
fn();
- 如果是用 let 进行变量声明的话,程序会抛出异常,SyntaxError: Identifier ‘a’ has already been declared
let a = 1;
let a = 2;
console.log(a); // SyntaxError: Identifier 'a' has already been declared
function fn() {
let a = 1;
let a = 2;
console.log(a); // SyntaxError: Identifier 'a' has already been declared
}
fn();
- 如果是 var 声明变量与 let 声明变量同时存在同一个作用域下,程序也会抛出异常 SyntaxError: Identifier ‘a’ has already been declared,var 首先声明变量 a,但是又通过 let 进行声明变量 a,而在同一个作用域下,let 是不允许重复声明同一个变量的
var a = 1;
let a = 2;
console.log(a); // SyntaxError: Identifier 'a' has already been declared
function fn() {
var a = 1;
let a = 2;
console.log(a); // SyntaxError: Identifier 'a' has already been declared
}
- 如果在函数内部,let 声明形式参数,此时程序也会抛出异常;因为形式参数 a 在函数 fn 内部相当于一个临时变量,在函数预解析的时候,已经定义过形式参数 a,当前 let 是不允许在同一个作用域下声明同一个变量
function fn(a) {
let a = 2;
console.log(a); // SyntaxError: Identifier 'a' has already been declared
}
fn();
--->
function fn(a) {
var a = undefined;
let a = 2;
console.log(a); // SyntaxError: Identifier 'a' has already been declared
}
fn();
- 如果不在同一个作用域下,就不会产生重复声明变量的问题
function test(a) {
{
let a = 10;
}
console.log(a); // undefined 因为作用域的问题,打印的是形式参数a的值
}
test();
function test(a) {
{
let a = 10;
console.log(a); // 10 打印的是let声明变量a的值
}
}
test();
let 不会声明提升,它会产生一个暂时性死区
// 块级作用域内
{
// 暂时性死区 a
console.log(a); // ReferenceError
console.log(b); // undefined
let a = 1; // 此时变量 a 没有提升
console.log(a); // 脱离暂时性死区
var b = 2; // 变量 b 提升
}
function test() {
var foo = 33;
if (foo) {
let foo = foo + 55; // 预编译时在这一对花括号时声明变量 foo 在使用赋值时出现暂时性死区
}
}
test();
在函数参数默认值赋值的时候,形式参数不存在变量提升的问题,相当于形式参数是 let 声明
对变量 y 来说,let x = y 代码的执行是在变量 y 的暂时性死区范围内部,所以你在变量 y 暂时性死区内部访问 y,所以抛出 ReferenceError 错误
function test(x = y, y = 2) {
console.log(x, y); // ReferenceError: Cannot access 'y' before initialization
}
test();
--> 可看作以下代码
function test() {
// TDZ start beginning of scope
let x = y; // end of TDZ
let y = 2; // end of TDZ
console.log(x, y);
}
--> 修正为以下顺序
function test(y = 2, x = y) {
console.log(x, y); // 2, 2
}
利用 typeof 尝试 let 暂时性死区的问题,抛出异常 ReferenceError: Cannot access ‘b’ before initialization
console.log(typeof a); // undefined
// 此时let声明的b变量,被let锁在当前行,并不能变量提升,而且被typeof运算符判断时会直接抛出异常
console.log(typeof b);
let b = 2; // ReferenceError: Cannot access 'b' before initialization
let 只能在当前的块级作用域下生效
// 示例中,let声明的变量a只存在于当前的块级作用域中,并不会像var声明的变量一样进行变量提升的现象,所以在块级作用域之外访问变量a是访问不到的。
{
let a = 2;
}
console.log(a); // ReferenceError: a is not defined
function fn() {
let a = 2;
}
console.log(a); // ReferenceError: a is not defined
if (1) {
let a = 2;
}
console.log(a); // ReferenceError: a is not defined
程序运行不到的地方,不会出现错误
for (; 1; ) {
let a = 1;
}
console.log(a); // 不执行,因为上述代码是死循环,程序执行不到这行代码。
for 语句与 let 形成块级作用域,实际上 for(let i = 0; i < 10; i++){}也是一个块级作用域,所以在全局作用下访问变量 i 访问不到
for (let i = 0; i < 10; i++) {}
console.log(i); // ReferenceError: i is not defined
for 循环,通过 var 声明的 i 变量可以看作是全局作用域下的一个变量,通过 for 循环向数组中存入匿名函数 function,但是此时全局作用域下的 i 成为 10,但是接下来的 for 循环又将变量 i 赋值为 0,导致每次匿名函数执行的时候,输出 0 - 9
var arr = [];
for(var i = 0; i < 10; i++) {
arr[i] = function(){
console.log(i);
}
}
for(var i = 0; i < 10; i++) {
arr[i](); // 0 - 9
}
--->
var arr = [];
var i = 0;
for(; i < 10; ) {
arr[i] = function(){
console.log(i);
}
i++;
}
var i = 0;
for(; i < 10;) {
arr[i]();
i++;
}
在 for 循环中变量 i 是被 let 声明,所以每一次循环的时候,因为 let 的原因产生块级作用域,而匿名函数 function 此时作为一个闭包函数被存入数组中,存入的不仅仅是匿名函数,还存入当前匿名函数所处的环境(包含每次循环 i 的值),所以在第二次 for 循环时,执行匿名函数时,匿名函数都能够拿到当时的 i 值 0-9
var arr = [];
for (let i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i); // 0-9
};
}
for (var k = 0; k < 10; k++) {
arr[k]();
}
判断 for 循环中,()和 {} 是否是同一个作用域,下面例子()与 {} 看似是在同一个作用域下
for (var i = 0; i < 10; i++) {
i = "a";
console.log(i); // 'a'
}
for (let i = 0; i < 10; i++) {
i = "a";
console.log(i); // 'a'
}
for (let i = 0; i < 10; i++) {
var i = "a";
console.log(i); // SyntaxError: Identifier 'i' has already been declared
}
由于 let 产生的块级作用域原因,导致 for 循环的()与 {} 不是同一个作用域
for (let i = 0; i < 10; i++) {
let i = "a";
console.log(i); // 10个a
}
以上类似于立即执行函数的块级作用域
for (let i = 0; i < 10; i++) {
var i = "a";
console.log(i); // SyntaxError: Identifier 'i' has already been declared
}
--> 可看作以下代码
while (1) {
let i = 0;
{
var i = "a";
console.log(i); // Uncaught SyntaxError: Identifier 'i' has already been declared
}
i++;
}
块级作用域中函数声明的方式
ES5 规定在函数只能够在顶层作用域和函数作用域中生成,不建议在块级作用域当中用函数声明的方式来声明函数,而用函数表达式
ES5:
function test(){} 合法
function test(){ 合法
function test1(){}
}
ES6:
{
function fn(){} // 浏览器能够解析,但是不推荐
}
if(1) {
function test(){} // 浏览器能够解析,但是不推荐
}
try {
function fn(){} // 浏览器能够解析,但是不推荐
}catch(e) {
function test(){}
}
----> 如果需要在块级作用域中声明函数,利用函数表达式替换
try {
var test1 = function(){}
}catch(e) {
var test2 = function(){}
}
块级作用域返回值问题
块级作用域是没有返回值的,目前 do{}的语法可以让块级作用域有返回值,但是只是草案
函数的块级的作用域并不是命名函数的立即调用
一个是作用域,一个是函数立即执行,你能够通过块级作用域模拟函数立即执行,但完全不是等效的