ES6~ES13新特性

165 阅读10分钟

新的ECMA代码执行描述

在执行学习JavaScript代码执行过程中,我们学习了很多ECMA文档的术语:

 执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;

 执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;

 变量对象:Variable Object,上下文关联的VO对象,用于记录函数和变量声明;

 全局对象:Global Object,全局执行上下文关联的VO对象;

 激活对象:Activation Object,函数执行上下文关联的VO对象;

 作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;

在新的ECMA代码执行描述中(ES5以及之上),对于代码的执行流程描述改成了另外的一些词汇:

 基本思路是相同的,只是对于一些词汇的描述发生了改变;

 执行上下文栈和执行上下文也是相同的;

词法环境( Lexical Environments

词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符;

 一个词法环境是由环境记录(Environment Record)和一个外部词法环境(

oute;r Lexical Environment)组成;

关联的一个函数声明,代码块,try-catch语句,当他们的代码被执行的时候,词法环境被创建出来的

 一个词法环境经常用于关联一个函数声明、代码块语句、try-catch语句,当它们的代码被执行时,词法环境被创建出来;

也就是在ES5之后,执行一个代码,通常会关联对应的词法环境;

 那么执行上下文会关联哪些词法环境呢?

LexicalEnvironment和VariableEnvironment

◼ LexicalEnvironment用于处理let、const声明的标识符:

◼ VariableEnvironment用于处理var和function声明的标识符:

环境记录(Environment Record)

在这个规范中有两种主要的环境记录值:声明式环境记录和对象环境记录。

 声明式环境记录:声明性环境记录用于定义ECMAScript语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与

ECMAScript语言值关联起来的Catch子句。

 对象式环境记录:对象环境记录用于定义ECMAScript元素的效果,例如WithStatement,它将标识符绑定与某些对象的属性

关联起来。

新ECMA描述内存图

let/const基本使用

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

 let、const在其他编程语言中都是有的,所以也并不是新鲜的关键字;

 但是let、const确确实实给JavaScript带来一些不一样的东西;

let关键字:

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

const关键字:

 const关键字是constant的单词的缩写,表示常量、衡量的意思;

 它表示保存的数据一旦被赋值,就不能被修改;

 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;

◼ 注意:

 另外let、const不允许重复声明变量;

<script>


    function foo() {
      console.log("foo function")
    }


    var message = "Hello World"


  </script>

let/const作用域提升

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

 我们知道var声明的变量是会进行作用域提升的;

 但是如果我们使用let声明的变量,在声明之前访问会报错;

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

 事实上并不是这样的,我们可以看一下ECMA262对let和const的描述;

这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;

暂时性死区 (TDZ)

3

我们知道,在let、const定义的标识符真正执行到声明的代码之前,是不能被访问的

 从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)

◼ 使用术语 “temporal” 是因为区域取决于执行顺序(时间),而不是编写代码的位置

<script>
    // ES6之前
    var message1 = "Hello World"
    message1 = "Hello Coderwhy"
    message1 = "aaaaa"
    console.log(message1)


    // ES6开始
    // 1.let
    let message2 = "你好, 世界"
    message2 = "你好, why"
    message2 = 123
    console.log(message2)


    // 2.const
    // const message3 = "nihao, shijie"
    // message3 = "nihao, why"


    // 赋值引用类型
    const info = {
      name: "why",
      age: 18
    }
    // info = {}
    info.name = "kobe"
    console.log(info)



  </script>

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

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

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

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

作用域提升: 在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;

 在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;

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

<script>
    // 1.var变量可以重复声明
    // var message = "Hello World"
    // var message = "你好, 世界"



    // 2.let/const不允许变量的重复声明
    // var address = ""
    let address = "广州市"
    // let address = "上海市"
    const info = {}
    // const info = {}


  </script>

早期语言上设计缺陷

Window对象添加属性

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

 但是let、const是不会给window上添加任何属性的。

那么我们可能会想这个变量是保存在哪里呢?

<script>
    // 1.var声明的变量会进行作用域的提升
    // console.log(message)
    // var message = "Hello World"


    // 2.let/const声明的变量: 没有作用域提升
    // console.log(address)
    console.log(address)
    let address = "广州市"
    const info = {}


  </script>

有被提前创建出来的,但是没有被方法问的,需要被词法版的滚过的

没有作用域提升

var的块级作用域

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

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

//var没有块级作用域 通过var 声明的变量或者非严格模式下(non-strict mode)创建的函数声明没有块级作 1/编写语句 var foo - 用域。在语句块里声明的变量的作用域不仅是其所在的函数或者script,标签内,所设置 变量的影响会在超出语句块本身之外持续存在。换句话说,这种语句块不会引入一个作 用域。尽管单独的语句块是合法的语句,但在javascript中你不会想使用单独的语句 console.log(foo)//foo 可以访问到

let/const的块级作用域

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

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

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

块级作用域的应用

我来看一个实际的案例:获取多个按钮监听点击

使用let或者const来实现:

<button>按钮0</button>
  <button>按钮1</button>
  <button>按钮2</button>
  <button>按钮3</button>
  
  <script>


    // 1.形成的词法环境
    // var message = "Hello World"
    // var age = 18
    // function foo() {}
    // let address = "广州市"


    // {
    //   var height = 1.88


    //   let title = "教师"
    //   let info = "了解真相~"
    // }


    // 2.监听按钮的点击
    const btnEls = document.querySelectorAll("button")
    // [btn1, btn2, btn3, btn4]
    // for (var i = 0; i < btnEls.length; i++) {
    //   var btnEl = btnEls[i];
    //   // btnEl.index = i
    //   (function(m) {
    //     btnEl.onclick = function() {
    //       debugger
    //       console.log(`点击了${m}按钮`)
    //     }
    //   })(i)
    // }


    
    for (let i = 0; i < btnEls.length; i++) {
      const btnEl = btnEls[i];
      btnEl.onclick = function() {
        console.log(`点击了${i}按钮`)
      }
    }


    // console.log(i)



  </script>

var、let、const的选择

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

对于var的使用:

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

留问题;

 其实是JavaScript在设计之初的一种语言缺陷;

 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解;

 但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;

对于let、const:

 对于let和const来说,是目前开发中推荐使用的;

 我们会优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改;

 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;

 这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;

// 1.var定义的变量是会默认添加到window上的
    // var message = "Hello World"
    // var address = "广州市"


    // console.log(window.message)
    // console.log(window.address)


    // 2.let/const定义的变量不会添加到window上的
    // let message = "Hello World"
    // let address = "广州市"
    
    // console.log(window.message)
    // console.log(window.address)


    // 3.let/var分别声明变量
    var message = "Hello World"
    let adress = "广州市"


    function foo() {
      debugger
    }
    foo()

let/const块级作用域

script>


    // 1.在ES5以及之前, 只有全局和函数会形成自己的作用域
    // 代码块
    // function foo() {
    //   console.log("Hello World")
    // }
    // {
    //   var message = "Hello World"
    // }
    // console.log(message)



    // 2.从ES6开始, 使用let/const/function/class声明的变量是有块级作用域
    
    // console.log(message)
    // foo()
    {
      var message = "Hello World"
      let age = 18
      const height = 1.88


      class Person {}


      function foo() {
        console.log("foo function")
      }
    }


    // console.log(age)
    // console.log(height)
    // const p = new Person()
    foo()


  </script>

let/const/function/class

加上立即执行函数形成自己的作用域,改成let

模板字符串

<script>
    const name = "why"
    const age = 18


    // 1.基本用法
    // 1.1.ES6之前
    // const info = "my name is" + name + ", age is " + age


    // 1.2.ES6之后
    const info = `my name is ${name}, age is ${age}`
    console.log(info)



    // 2.标签模板字符串的用法
    function foo(...args) {
      console.log("参数:", args)
    }


    // foo("why", 18, 1.88)
    foo`my name is ${name}, age is ${age}, height is ${1.88}`


  </script>

字符串模板基本使用

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

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

 首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;

 其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;

标签模板字符串使用

模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。我们一起来看一个普通的JavaScript的函数:

如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:

 模板字符串被拆分了;

 第一个元素是数组,是被模块字符串拆分的字符串组合;

 后面的元素是一个个模块字符串传入的内容;

React的styled-components库

函数的默认参数

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

 传入了参数,那么使用传入的参数;

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

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

<script>


    // 注意: 默认参数是不会对null进行处理的
    function foo(arg1 = "我是默认值", arg2 = "我也是默认值") {
      // 1.两种写法不严谨
      // 默认值写法一:
      // arg1 = arg1 ? arg1: "我是默认值"


      // 默认值写法二:
      // arg1 = arg1 || "我是默认值"


      // 2.严谨的写法
      // 三元运算符
      // arg1 = (arg1 === undefined || arg1 === null) ? "我是默认值": arg1
      
      // ES6之后新增语法: ??
      // arg1 = arg1 ?? "我是默认值"


      // 3.简便的写法: 默认参数
      console.log(arg1)
    }


    foo(123, 321)
    foo()
    foo(0)
    foo("")
    foo(false)
    foo(null)
    foo(undefined)


  </script>

默认参数注意

<script>
    
    // 1.注意一: 有默认参数的形参尽量写到后面
    // 2.有默认参数的形参, 是不会计算在length之内(并且后面所有的参数都不会计算在length之内)
    // 3.剩余参数也是放到后面(默认参数放到剩余参数的前面)
    function foo(age, name = "why", ...args) {
      console.log(name, age, args)
    }


    foo(18, "abc", "cba", "nba")


    console.log(foo.length)


  </script>

剩余参数也是放到后面(默认参数放到剩余参数的前面)

有默认参数的形参, 是不会计算在length之内(并且后面所有的参数都不会计算在length之内)`