JavaScript高级系列(十三) - ES6~ES13-ES6核心知识点

JavaScript高级系列(十三) - ES6~ES13-ES6核心知识点

ES6是JS历史上最重要的一个版本,也被称为ES2015。它增加了许多新的语言特性,模块系统、类、箭头函数、模板字面量等等,这些新特性极大地提高了开发者的编码效率和代码的可维护性。ES6的发布标志着JS进入了一个新的时代,也让JS这门语言更加适合大型项目开发。

我们将介绍ES6的一些核心知识点,包括let和const、模板字符串、函数的默认参数-剩余参数-箭头函数、展开运算符、数值的表示。这些知识点不仅仅是ES6的语法,也是现代前端开发中必不可少的基础知识。

对于一些更加高级的ES6新特性,比如Promise、Proxy、Reflect、Generator等会在后续专门的篇章中讲解。

通过学习这些知识点,我们可以更好地理解和应用ES6的新特性,并将其应用到实际项目中。

一. let/const

1.1. let/const基本使用

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

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

let关键字:

  • 从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量
js 复制代码
let foo = "foo"
foo = "why"

var bar = "bar"
bar = "why"

const关键字:

  • const关键字是constant的单词的缩写,表示常量、衡量的意思;
  • 它表示保存的数据一旦被赋值,就不能被修改;
js 复制代码
const foo = "foo"
foo = "why" // TypeError: Assignment to constant variable.

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

js 复制代码
const obj = {
  name: "why",
  age: 18
}

// obj = {} // TypeError: Assignment to constant variable.
obj.name = "kobe"

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

js 复制代码
let name = "why"
let name = "kobe" // SyntaxError: Identifier 'message' has already been declared

1.2. let/const作用域提升

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

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

js 复制代码
console.log(foo) // undefined

var foo = "foo"

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

js 复制代码
console.log(foo) // ReferenceError: Cannot access 'foo' before initialization

let foo = "foo"

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

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

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

  • 那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
  • 事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解;
  • 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;
  • 在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;

1.3. window对象添加属性

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

js 复制代码
var foo = "foo"

console.log(window.foo) // foo

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

js 复制代码
let foo = "foo"

console.log(window.foo) // undefined

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

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

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

1.4. let/const块级作用域

1.4.1. 理解块级作用域

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

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

js 复制代码
{
  // 编写语句
  var foo = "foo"
}

console.log(foo) // foo 可以访问到

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

js 复制代码
{
  let foo = "foo"
  function bar() {
    console.log("bar")
  }
  class Person {}
}

console.log(foo) // ReferenceError: foo is not defined
bar() // 可以访问
var p = new Person() // ReferenceError: foo is not defined

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

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

1.4.2. 块级作用域应用

那么增加了这个块级作用域后,它有什么作用呢?

if的块级作用域

js 复制代码
if (true) {
  var foo = "foo"
  let bar = "bar"
}

console.log(foo) // foo
console.log(bar) // ReferenceError: bar is not defined

switch、case的块级作用域:

js 复制代码
var color = "red"
switch (color) {
  case "red":
    var foo = "foo"
    let bar = "bar"
    break
}

console.log(foo) // foo
console.log(bar) // ReferenceError: bar is not defined

for循环的块级作用域:

js 复制代码
for (var i = 0; i < 10; i++) {
  // console.log(i)
}

console.log(i) // 10

1.4.3. 块级作用域场景

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

html 复制代码
  <button>按钮1</button>
  <button>按钮2</button>
  <button>按钮3</button>
  <button>按钮4</button>

如果我们通过var来实现,会有问题:

js 复制代码
var btns = document.getElementsByTagName("button")
for (var i = 0; i < btns.length; i++) {
  btns[i].onclick = function() {
    console.log("第" + i + "个按钮被点击~") // 全部是第4个按钮被点击
  }
}

var问题的解决方案:

js 复制代码
var btns = document.getElementsByTagName("button")
for (var i = 0; i < btns.length; i++) {
  (function(i) {
    btns[i].onclick = function() {
      console.log("第" + i + "个按钮被点击")
    }
  })(i)
}

使用let或者const来实现:

js 复制代码
var btns = document.getElementsByTagName("button")
for (let i = 0; i < btns.length; i++) {
  btns[i].onclick = function() {
    console.log("第" + i + "个按钮被点击")
  }
}

1.5. let、const暂时性死区

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

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

if (true) {
  console.log(foo) // ReferenceError: Cannot access 'foo' before initialization

  let foo = "bar"
}

1.6. var、let、const的选择

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

对于var的使用:

  • 我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题;
  • 其实是JavaScript在设计之初的一种语言缺陷;
  • 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解;
  • 但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;

对于let、const:

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

二. 字符串模板

2.1. 字符串模板基本使用

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

js 复制代码
const name = "why"
const age = 18
const height = 1.88

console.log("my name is" + name + ", age is " + age + ", height is" + height)

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

  • 首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;
  • 其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;
js 复制代码
const name = "why"
const age = 18
const height = 1.88

console.log(`my name is ${name}, age is ${age}, height is ${height}`)
console.log(`我是成年人吗? ${age >= 18 ? '是': '否'}`)

甚至是我们可以进行函数的调用:

js 复制代码
function foo() {
  return "function is foo"
}

console.log(`my function is ${foo()}`)

2.2. 标签模板字符串使用

模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。

我们一起来看一个普通的JavaScript的函数:

js 复制代码
function foo(...args) {
  console.log(args)
}

foo("Hello World")

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

  • 模板字符串被拆分了;
  • 第一个元素是数组,是被模块字符串拆分的字符串组合;
  • 后面的元素是一个个模块字符串传入的内容;
js 复制代码
function foo(...args) {
  console.log(args)
}

// [ 'Hello World' ]
foo("Hello World")

const name = "why"
const age = 18
// [ [ 'Hello ', ' World ', '' ], 'why', 18 ]
foo`Hello ${name} World ${age}`

在styled component中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的:

三. 函数知识补充

3.1. 函数的默认参数

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

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

通常我们会进行如下方式的实现:

js 复制代码
function foo(x, y) {
  x = x || 20
  y = y || 30
  console.log(x, y)
}

foo(50, 100) // 50 100
foo() // 20 30

其实这种做法是有弊端的,如果我们传入null、0、空字符串,那么会认为需要使用默认值:

js 复制代码
foo(null, "") // 20, 30

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

js 复制代码
function foo(x = 20, y = 30) {
  console.log(x, y)
}

foo(50, 100) // 50 100
foo() // 20 30

我们使用ES6默认值的写法不仅仅可以避免上面的问题,而且代码的阅读性也会更强:

  • 其实ES6的默认值转化后是如下的代码:
js 复制代码
function foo() {
  var x =
    arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 20;
  var y =
    arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 30;
  console.log(x, y);
}

默认值也可以和解构一起来使用:

js 复制代码
// 写法一:
function foo({name, age} = {name: "why", age: 18}) {
  console.log(name, age)
}

// 写法二:
function foo({name = "why", age = 18} = {}) {
  console.log(name, age)
}

foo()

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

  • 但是JavaScript允许不将其放到最后,但是意味着还是会按照顺序来匹配;
js 复制代码
function foo(x = 20, y, z) {
  console.log(x, y, z)
}

foo(30, 40) // 30, 40, undefined

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

js 复制代码
function foo(x = 10, y, z) {
  console.log(x, y, z)
}

console.log(foo.length) // 0

3.2. 参数的作用域?

3.3. 函数的剩余参数

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

  • 如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
js 复制代码
function foo(m, n, ...args) {
  console.log(m, n)
  console.log(args)
}

foo(20, 30, 40, 50) // 20, 30 [40, 50]

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

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

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

js 复制代码
function foo(m, ...args, n) {
  console.log(m, args, n)
}

// Rest parameter must be last formal parameter
foo(20, 30, 40, 50)

3.4. 箭头函数的补充

在前面我们已经学习了箭头函数的用法,这里进行一些补充:

  • 箭头函数是没有显示原型的,所以不能作为构造函数,使用new来创建对象;
js 复制代码
var foo = () => {
  console.log("foo")
}

console.log(foo.prototype) // undefined

所以下面的代码会报错:

js 复制代码
// TypeError: foo is not a constructor
var f = new foo()

四. ES6小知识点

4.1. 展开语法

展开语法(Spread syntax)

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

在函数调用时使用:

js 复制代码
// 在调用函数时展开数组
foo(...names)
const name = "abc"
foo(...name)

在数组构造时使用:

js 复制代码
const newNames = [...names, ...name]
console.log(newNames)

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

js 复制代码
const newObj = {...obj, height: 1.88, ...names}
console.log(newObj)

展开语法是一种浅拷贝:

js 复制代码
const obj = {
  friend: { name: "kobe" }
}

const newObj = { ...obj }
newObj.friend.name = "james"
console.log(obj.friend.name) // james

4.2. 数值的表示

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

js 复制代码
const num1 = 100
// b -> binary
const num2 = 0b100
// octonary
const num3 = 0o100
// hexadecimal
const num4 = 0x100

// ES2021新增特性
const num5 = 100_000_000

本文由mdnice多平台发布

相关推荐
GISer_Jing38 分钟前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪2 小时前
CSS复习
前端·css
咖啡の猫4 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲6 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路7 小时前
GeoTools 读取影像元数据
前端
ssshooter7 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry8 小时前
Jetpack Compose 中的状态
前端
dae bal9 小时前
关于RSA和AES加密
前端·vue.js
柳杉9 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化