title: let/const/var
categories: Javascript
tag:

  • ES6
    date: 2021-11-26 17:16:34

let/const/var

在 ES5 中我们声明变量都是使用的 var 关键字,从 ES6 开始新增了两个关键字可以声明变量:let、const

  • let、const 在其他编程语言中都是有的,所以也并不是新鲜的关键字;
  • 但是 let、const 确确实实给 JavaScript 带来一些不一样的东西;

let 关键字:

  • 从直观的角度来说,let 和 var 是没有太大的区别的,都是用于声明一个变量

const 关键字:

  • const 关键字是 constant 的单词的缩写,表示常量、衡量的意思;
  • 它表示保存的数据一旦被赋值,就不能被修改;
  1. const name = 'abc'
  2. name = 'cba' //TypeError: Assignment to constant variable.
  • 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;
  1. const obj = {
  2. name: 'aaa'
  3. }
  4. obj.name = 'bbb'
  5. console.log(obj) //{ name: 'bbb' }

注意:另外 let、const 不允许重复声明变量;

  1. let bar = 'bar'
  2. let bar = 'qq' //Identifier 'bar' has already been declared

let/const 作用域提升

let、const 和 var 的另一个重要区别是作用域提升:

  • 我们知道 var 声明的变量是会进行作用域提升的;
  • 但是如果我们使用 let 声明的变量,在声明之前访问会报错;
  1. console.log(foo) // Cannot access 'foo' before initialization
  2. let foo = 'foo'

那么是不是意味着 foo 变量只有在代码执行阶段才会创建的呢?

  • 事实上并不是这样的,我们可以看一下 ECMA262 对 let 和 const 的描述;
  • 这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;

let/const 有没有作用域提升呢?

从上面我们可以看出,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问的。

  • 那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?

事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解;

  • 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;
  • 在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;

所以我的观点是 let、const 没有进行作用域提升,但是会在解析阶段被创建出来。

Window 对象添加属性

我们知道,在全局通过 var 来声明一个变量,事实上会在 window 上添加一个属性:

  • 但是 let、const 是不会给 window 上添加任何属性的。
  • 那么我们可能会想这个变量是保存在哪里呢?

我们先回顾一下最新的 ECMA 标准中对执行上下文的描述

13_es6基础知识(一) - 图1

变量被保存到 VariableMap 中

也就是说我们声明的变量和环境记录是被添加到变量环境中的:

  • 但是标准有没有规定这个对象是 window 对象或者其他对象呢?
  • 其实并没有,那么 JS 引擎在解析的时候,其实会有自己的实现;
  • 比如 v8 中其实是通过 VariableMap 的一个 hashmap 来实现它们的存储的。
  • 那么 window 对象呢?而 window 对象是早期的 GO 对象,在最新的实现中其实是浏览器添加的全局对象,并且一直保持了 window 和 var 之间值的相等性

13_es6基础知识(一) - 图2

块级作用域

在我们前面的学习中,JavaScript 只会形成两个作用域:全局作用域和函数作用域。

13_es6基础知识(一) - 图3

ES5 中放到一个代码中定义的变量,外面是可以访问的:

  1. {
  2. var foo = 'foo'
  3. }
  4. console.log(foo) //foo

在 ES6 中

  1. {
  2. let foo = 'foo'
  3. }
  4. console.log(foo) //ReferenceError: foo is not defined

let/const 的块级作用域

在 ES6 中新增了块级作用域,并且通过 let、const、function、class 声明的标识符是具备块级作用域的限制的

  1. {
  2. let foo = 'foo'
  3. function demo() {
  4. console.log('function demo')
  5. }
  6. }
  7. demo() //function demo

但是从以上代码我们会发现函数拥有块级作用域,但是外面依然是可以访问的:

  • 这是因为引擎会对函数的声明进行特殊的处理,允许像 var 那样进行提升;

if 语句,switch 语句,for 语句都是块级作用域

if 语句

  1. if (true) {
  2. var foo = 'foo'
  3. }
  4. console.log(foo) //foo
  5. if (true) {
  6. let bar = 'bar'
  7. }
  8. console.log(bar) //ReferenceError: bar is not defined

switch 语句

  1. var color = 'red'
  2. switch (color) {
  3. case 'red':
  4. var foo = 'foo'
  5. }
  6. console.log(foo) //foo
  7. switch (color) {
  8. case 'red':
  9. let bar = 'bar'
  10. }
  11. console.log(bar) //ReferenceError: foo is not defined

for 语句

  1. // for语句也是块级作用域
  2. for (var i = 0; i < 10; i++) {
  3. console.log('hello' + i)
  4. }
  5. console.log(i) //10
  6. for (let i = 0; i < 10; i++) {
  7. console.log(i)
  8. }
  9. console.log(i) //ReferenceError: i is not defined

块级作用域的应用

13_es6基础知识(一) - 图4

解决办法

采用立即执行函数。

  1. const btns = document.getElementsByTagName('button')
  2. for (var i = 0; i < btns.length; i++) {
  3. //相当于多加了一层作用域
  4. ;(function (n) {
  5. btns[i].onclick = function () {
  6. console.log('第' + n + '个按钮点击')
  7. }
  8. })(i)
  9. }

现在我们可以使用 let。因为 let 有块级作用域,直接在循环里面找到,不会去全局找

  1. const btns = document.getElementsByTagName('button')
  2. for (let i = 0; i < btns.length; i++) {
  3. console.log('第' + i + '个按钮点击')
  4. }

暂时性死区

在 ES6 中,我们还有一个概念称之为暂时性死区:

  • 它表达的意思是在一个代码中,使用 let、const 声明的变量,在声明之前,变量都是不可以访问的;
  • 我们将这种现象称之为 temporal dead zone(暂时性死区,TDZ);

13_es6基础知识(一) - 图5

var、let、const 的选择

那么在开发中,我们到底应该选择使用哪一种方式来定义我们的变量呢?

对于 var 的使用:

  • 我们需要明白一个事实,var 所表现出来的特殊性:比如作用域提升、window 全局对象、没有块级作用域等都是一些

历史遗留问题;

  • 其实是 JavaScript 在设计之初的一种语言缺陷;
  • 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对 JavaScript 语言本身以及底层的理解;
  • 但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用 var 来定义变量了;

对于 let、const:

  • 对于 let 和 const 来说,是目前开发中推荐使用的;
  • 我们会优先推荐使用 const,这样可以保证数据的安全性不会被随意的篡改;
  • 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用 let;
  • 这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;

模板字符串

字符串模板

在 ES6 之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly)。

ES6 允许我们使用字符串模板来嵌入 JS 的变量或者表达式来进行拼接:

  1. 首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;
  2. 其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容
  1. const name = 'dh'
  2. const age = 22
  3. console.log(`my name is ${name},age is ${age}`)
  4. // my name is dh,age is 22

标签模板字符串

普通调用函数

  1. function foo(m, n) {
  2. console.log(m, n)
  3. }
  4. foo('hello', 'world') //hello world

用标签模板字符串

  1. function foo(m, n) {
  2. console.log(m, n)
  3. }
  4. foo`` //[ '' ] undefined
  5. foo`Hello world` //[ 'Hello world' ] undefined
  6. //第一个参数是模板字符串的整个字符,切成了多块,放到了一个数组中
  7. //第二个参数是第一个${}
  8. foo`Hello${name}Wo${age}rld` //[ 'Hello', 'Wo', 'rld' ] dh

函数

函数的默认参数

在 ES6 之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:

  • 传入了参数,那么使用传入的参数;
  • 没有传入参数,那么使用一个默认值;

而在 ES6 中,我们允许给函数一个默认值:

  1. function foo(m = 20, n = 10) {
  2. console.log(m, n)
  3. }
  4. foo() //20 10

解构和默认值一起使用

  1. function foo({ name = 'why', age = 18 } = {}) {
  2. console.log(name, age)
  3. }
  4. foo()

另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的):

  • 但是 JavaScript 允许不将其放到最后,但是意味着还是会按照顺序来匹配;

另外默认值会改变函数的 length 的个数,默认值以及后面的参数都不计算在 length 之内了。

函数的剩余参数

ES6 中引用了 rest parameter,可以将不定数量的参数放入到一个数组中:

  • 如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
  1. function foo(m, n, ...args) {
  2. console.log(m, n) //20,30
  3. console.log(args) //[ 40, 50 ]
  4. }
  5. foo(20, 30, 40, 50)

那么剩余参数和 arguments 有什么区别呢?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
  • arguments 对象不是一个真正的数组,而 rest 参数是一个真正的数组,可以进行数组的所有操作;
  • arguments 是早期的 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而 rest 参数是 ES6 中提供 并且希望以此来替代 arguments 的;

剩余参数必须放到最后一个位置,否则会报错。

箭头函数的补充

箭头函数是没有显式原型的,所以不能作为构造函数,使用 new 来创建对象;

箭头函数没有 arguments

箭头函数没有 this

  1. var foo = () => {
  2. console.log('foo')
  3. }
  4. console.log(foo.prototype) //undefined

展开语法

展开语法(Spread syntax)

  • 可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;
  • 还可以在构造字面量对象时, 将对象表达式按 key-value 的方式展开;

展开语法的场景:

  • 在函数调用时使用;
  • 在数组构造时使用;
  • 在构建对象字面量时,也可以使用展开运算符,这个是在 ES2018(ES9)中添加的新特性;

注意:展开运算符其实是一种浅拷贝;

  1. const names = ['abc', 'cba', 'cab']
  2. const name = 'why'
  3. const info = { name: 'why', age: 18 }
  4. function foo(x, y, z) {
  5. console.log(x, y, z)
  6. }
  7. //1. 在函数调用的时候
  8. foo(...names) //abc cba cab
  9. foo(...name) //w h y
  10. //2. 在数组构造的时候
  11. const newNums = [...names, ...name]
  12. console.log(newNums) //[ 'abc', 'cba', 'cab', 'w', 'h', 'y' ]
  13. //3. 在构建对象字面量
  14. const obj = { ...info, address: '广州' }
  15. console.log(obj) //{ name: 'why', age: 18, address: '广州' }

浅拷贝

  1. const info = {
  2. name: 'why',
  3. friend: { name: 'kobe' }
  4. }
  5. const obj = { ...info }
  6. obj.name = 'dh'
  7. console.log(info, obj)
  8. // { name: 'why', friend: { name: 'kobe' } }
  9. // { name: 'dh', friend: { name: 'kobe' } }
  10. //当我们改变了obj中的friend中的名字,原对象也会改变
  11. obj.friend.name = 'gwk'
  12. console.log(info, obj)
  13. // { name: 'why', friend: { name: 'gwk' } }
  14. // { name: 'dh', friend: { name: 'gwk' } }

数值的表示

在 ES6 中规范了二进制和八进制的写法:

  1. //十进制
  2. const num1 = 100
  3. // 2进制
  4. const num2 = 0b100
  5. // 八进制
  6. const num3 = 0o100
  7. //十六进制
  8. const num4 = 0x100
  9. console.log(num1, num2, num3, num4) //100 4 64 256

Symbol 的基本使用

Symbol 是什么呢?Symbol 是 ES6 中新增的一个基本数据类型,翻译为符号。

那么为什么需要 Symbol 呢?

  • 在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
  • 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
  • 比如我们前面在讲 apply、call、bind 实现时,我们有给其中添加一个 fn 属性,那么如果它内部原来已经有了 fn 属性了呢?
  • 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;

Symbol 就是为了解决上面的问题,用来生成一个独一无二的值

  • Symbol 值是通过 Symbol 函数来生成的,生成后可以作为属性名;
  • 也就是在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值;
    Symbol 即使多次创建值,它们也是不同的:Symbol 函数执行后每次创建出来的值都是独一无二的;
    我们也可以在创建 Symbol 值的时候传入一个描述 description:这个是 ES2019(ES10)新增的特性;
  1. const s1 = Symbol()
  2. const s2 = Symbol()
  3. const s3 = Symbol()
  4. // 在定义对象字面量时使用
  5. const obj = {
  6. [s1]: 'abc',
  7. [s2]: 'cba'
  8. }
  9. //新增属性
  10. obj[s3] = 'nba'
  11. //只能通过这种方式访问,不能通过.运算符
  12. console.log(obj[s1])

需要通过Object.getOwnPropertySymbols()

  1. // 需要通过Object.getOwnPropertySymbols()
  2. const sKeys = Object.getOwnPropertySymbols(obj)
  3. console.log(sKeys)

相同的 Symbol

前面我们讲 Symbol 的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的 Symbol 应该怎么 来做呢?

  • 我们可以使用 Symbol.for 方法来做到这一点;
  • 并且我们可以通过 Symbol.keyFor 方法来获取对应的 key;
  1. const s1 = Symbol.for('abc')
  2. const s2 = Symbol.for('abc')
  3. console.log(s1 == s2) //true
  4. const key = Symbol.keyFor(s1)
  5. console.log(s1)
  6. const s3 = Symbol.for(key)
  7. console.log(s1 == s3) //true