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多平台发布

相关推荐
小白学前端66622 分钟前
React Router 深入指南:从入门到进阶
前端·react.js·react
web1309332039842 分钟前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
outstanding木槿1 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08212 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
隐形喷火龙2 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui
m0_748241122 小时前
Selenium之Web元素定位
前端·selenium·测试工具
风无雨2 小时前
react杂乱笔记(一)
前端·笔记·react.js
前端小魔女2 小时前
2024-我赚到自媒体第一桶金
前端·rust
鑫~阳2 小时前
快速建站(网站如何在自己的电脑里跑起来) 详细步骤 一
前端·内容管理系统cms
egekm_sefg2 小时前
webrtc学习----前端推流拉流,局域网socket版,一对多
前端·学习·webrtc