影子的知识库

影子的知识库

  • 知识库
  • GitHub

›ES6

JVM系列

  • JVM内存区域
  • 对象创建-布局-访问
  • 内存溢出实战
  • 内存区域回收
  • 四大引用
  • 垃圾回收算法
  • HotSpot回收算法细节

Java系列

  • java注解
  • springboot请求参数绑定
  • springboot请求参数校验框架
  • YAML语法
  • 动态代理
  • classpath和java命令
  • springboot-aop编程
  • springboot统一异常处理
  • springboot数据库和事务
  • springboot拦截器
  • springboot中的web配置
  • docker的简单开发
  • springboot自动配置
  • 数据库的隔离级别
  • springboot监控
  • java类加载
  • java-agent的相关内容
  • 类加载器详解
  • java的SecurityManager
  • maven学习

Node

    JS 基础

    • 语法基础和数据类型
    • 数据类型转换
    • 语句 表达式 运算符
    • 变量与对象
    • 函数
    • 数据处理
    • 常用 API
    • 重点知识

    ES6

    • 块级作用域
    • 字符串和正则表达式
    • 函数
    • 对象
    • Symbol
    • Set和Map
    • 迭代器和生成器
    • 类
    • 数组
    • Promise

    Node 基础

    • 模块系统
    • package.json
    • 内置对象
    • npm脚本的使用
    • Buffer
    • Stream
    • 事件循环机制
    • 示例代码

    stream系列

    • 流的缓冲
    • 可读流
    • 可写流
    • 双工流和转换流
    • 自定义流

后期计划

  • 学习计划
  • 专题研究计划
Edit

本文内容

本文记录一些关于 JavaScript 中块级作用域的内容

var 声明及变量提升机制

在函数作用域或者全局作用域通过 var 声明的变量,无论在哪里声明的,实际上都会被当成当前作用域顶部声明的变量

function getValue(condition) {
    if (condition) {
        var value = "blue";
        return value;
    } else {
        // 此处可以访问变量 value,其值为 undefined
        return null;
    }
    // 此处可以访问变量 value,其值为 undefined
}

注:如果我们访问一个从没声明过的变量,会抛出错误。而如果访问一个声明但是没有赋值的变量,会访问到 undefined。

上述代码实际上被 JavaScript 引擎替换成了如下代码:

function getValue(condition) {
    var value;
    if (condition) {
        var value = "blue";
        return value;
    } else {
        return null;
    }
}

变量 value 的声明,被提升到了当前作用域的顶部。

块级作用域

ES6 添加了块级作用域,块级作用域和其它语言的块级作用域很像了,存在于:

  • 函数内部
  • 块中(大括号之间的区域)

let 声明

let 声明的语法和 var 相同。但是 let 声明可以把变量的作用域限制在当前代码块中。let 声明的变量不会有变量提升,而且变量存在于块级作用域中,就比较像是其它语言常规的声明的作用域了。

function getValue(condition) {
    if (condition) {
        let value = "blue";
        return value;
    } else {
        // 此处 value 不存在,访问将会报错
        return null;
    }
    // 此处 value 不存在,访问将会报错
}

禁止重复声明

如果当前作用域已经存在了某个标识符,使用 let 再次声明的话,将会抛出错误

var s = 1;
let s = 2;

输出:

(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
/Users/czp/workspace/knowledge-base/demos/node_start/hello.js:2
let s = 2;
    ^

SyntaxError: Identifier 's' has already been declared

但是如果当前作用域内嵌了另一个作用域,便可以在内嵌作用域中使用 let 声明了,例如

var s = 1;
{
    let s = 2;
}

这种方式,可以理解为变量遮蔽。

const 声明

使用 const 声明的是常量,其值一旦被设定后就无法修改。因此每个通过 const 声明的变量,必须同时进行初始化。

const a = 3;
// 错误 没有进行初始化
const b;

const 声明的也是个具有块级作用域的变量(常量)。与 let 的性质一样,如果当前作用域已经存在了某个标识符,使用 const 再次声明会报错。但是如果当前作用域嵌套了内部作用域,在内部的作用域中可以使用 const 再次声明常量,这也是一种变量遮蔽

const 声明的常量,只是指指针不可变,如果 const 声明的是一个对象,那么对象内部的值是可以改变的(一大堆面向对象语言里的常量都有这种特性,就不细说了)

临时死区(TDZ)

临时死区这个概念,其实主要是用来描述 let 和 const 的变量不提示的效果的,实际上来说,let 和 const 声明的变量,除了不会提升之外,还有一些细微的区别,参考下面两种代码的区别

typeof a;
const a = 3;

输出:

(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
/Users/czp/workspace/knowledge-base/demos/node_start/hello.js:1
typeof a;
^

ReferenceError: Cannot access 'a' before initialization

如果不使用 const 声明 a 的话:

typeof a;

将不会报错,因为 typeof 操作符操作一个未声明过的变量时,值是 undefined,而不会直接报错

以上两种代码的例子,可以说明,let 和 const 的变量不提示除了不提示之外还有别的副作用。

实际上,使用 let 和 const 声明的变量,在 JavaScript 运行到它们声明的那一行之前,都处于一个特殊的区域中,称之为临时死区。当我们在 let 和 const 声明语句之前访问它们时,会访问到临时死区的变量,就会抛出错误。

JavaScript 引擎在扫描到变量声明时,要么将其进行作用域提升,要么将其放入到作用域的临时死区中,访问临时死区的变量将会抛出错误。

如果我们在临时死区对应的作用域外面访问该变量,则不会出错

typeof a;
{ const a = 3; }

这里的临时死区实际上属于 {} 里面,所以上面的 typeof a 访问的是个未定义变量,不会有错误抛出

循环中的块级绑定

for(let i = 0;i < 10;i++){
    ;
}
// 这里将不可以访问 i 了
console.log(i);

这个就很类似于其它语言的块级作用域了,讲道理的话循环结束后本来就不应该还能继续访问循环里定义的变量(只能说 ES 6 终于把这个实现的和正常的语言一样了)

循环中的函数

我们可以通过循环里生成函数闭包来体验一下 var 和 let 在循环中的区别

var funcs = [];
for (var i = 0; i < 10; i++) {
    funcs.push(function () {
        console.log(i);
    })
}
funcs.forEach(function (func) {
    func();
});

上述代码将会打印 10 个 10,至于为什么,我们已经在之前介绍闭包的时候讲过了:

  • 外层函数生成 Call 对象
  • 从头到尾只有同一个变量 i
  • 闭包里引用的都是同一个 i
  • 因此最后都输出了 i 的终值 10

这里主要是因为 i 变量从头到尾只有一个,所以闭包里引用的都是同一个值

下面看 let 声明的方式:

var funcs = [];
for (let i = 0; i < 10; i++) {
    funcs.push(function () {
        console.log(i);
    })
}
funcs.forEach(function (func) {
    func();
});

上述代码将会打印 0 到 9。

主要是因为:每次迭代的时候,都会生成一个新的值,第一次迭代的 i 和第二次迭代的 i 在内存中不是同一个对象了。

实际上这种特性是单独定义的,专门存在于循环当中。

循环中的 const 声明

const 声明在不同的循环类型中的表现是不一样的,如下是 for 循环

var funcs = [];
for (const i = 0; i < 10; i++) {
    funcs.push(function () {
        console.log(i);
    })
}
funcs.forEach(function (func) {
    func();
});

将会报错,因为在循环中对 const 进行了修改

而在 for-in 和 for-of 循环中,表现与 let 类似

var funcs = [];
var obj = {
    a: 1,
    b: 2,
    c: 3
};
for(const key in obj){
    funcs.push(function(){
        console.log(key);
    })
}
funcs.forEach(function(func){
    func();
})

上述代码将会打印 a b c

与上面关于循环中的函数一节相似,与其中 let 声明的表现一致,唯一的区别是循环中不能改变 const 的值

全局块作用域绑定

当我们在全局作用域下的时候,使用 var 声明变量就会为全局对象绑定新属性,例如在浏览器环境中

var a = 3
console.log(window.a)

a 会被挂载到全局对象 window 上 (node 下不会有这种行为,因为 node 中我们代码都运行在模块里,是个闭包)

但是使用 let 和 const 声明的变量,将不会被挂载到全局对象下面

let a = 3
console.log(window.a === a) // false

所以如果我们不想给全局对象赋值或者不小心将属性给覆盖掉了的话,使用 let 和 const 要保险很多

最佳实践

一句话:优先用 const、其次是 let,最后是 var

其实就是说,大部分的值我们都不需要修改,就行 scala 里面,基本都是优先用 val,很少用 var 一样。

只有确实需要修改变量采用 let

Last updated on 11/8/2020
← 重点知识字符串和正则表达式 →
  • var 声明及变量提升机制
  • 块级作用域
    • let 声明
    • 禁止重复声明
    • const 声明
    • 临时死区(TDZ)
    • 循环中的块级绑定
    • 循环中的函数
    • 循环中的 const 声明
    • 全局块作用域绑定
    • 最佳实践
影子的知识库
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub
Copyright © 2020 Cen ZhiPeng