深入理解JavaScript

91 阅读14分钟

JavaScript 最初只是一个在网页上编写简单交互的脚本语言,自 2009 年 12 月 ES5 发布之后,JavaScript 才真正得到了广泛的应用。在最近的几年中,JavaScript 更是一直占据编程语言开发榜首的位置(数据来自 octoverse.github.com,直达链接:octoverse.github.com/2022/top-pr…

image-20230219103939462.png

2015 年 之后之后,TC 39 技术委员会 开始保持了 每年一次 的版本的迭代,为 JavaScript 注入了大量的全新特性。

有过开发经验的小伙伴应该可以感知到,最近几年来 ES6、ES7、ES8... 各种新的 ECMAScript 版本规范接踵而来,让我们应接不暇。

每一个版本都会带来大量全新的特性,比如 ESM、Map、Set、反射、代理、迭代器、生成器......

而这些新的特性,很多都成为了 面试 或者 工作 中的高频难点。

所以对于我们这些开发者来说,这些全新特性,就成了我们必须要掌握的内容。

那么下面就让我们一起,拿出一个小时的时间,跟随作者 T J 克罗德,一起 深入理解现代 JavaScript 吧!

概述

整本书一共分为 19 个章节,我把这十九个章节分为了六个部分,以帮助大家来捋清楚整本书的脉络:

  • 第一章:想要了解现在 JavaScript 的新特性,那么首先我们需要先搞明白一些专业名词。比如 TC 39 是什么意思? ES6 与 ES2015 有什么关系?把这些搞明白之后,才方便我们后续的学习。
  • 第二章 - 第四章:作者按照 变量 、函数、类 这样 以小到大 的顺序,一步一步的讲解了自 ES6 之后的全新特性。其中会有我们所熟悉的 let、const。也会有一些我们不太熟悉的概念,比如 TDZ、“rest”参数、类继承、newTarget 等等。并且最重要的是,从这一章开始存在一个 旧习换新 的环节,这个环节告诉我们 如何用新语法代替之前的旧习惯
  • 第五章 - 第七章:这是三个章节主要讲解了 对象 相关的内容,从 对象的新特性开始 讲到 普通对象的不可迭代性依据迭代器,讲到生成器,最后又插入了对象结构的概念。
  • 第八章 - 第九章:这两个章节主要以异步为主。以 Promise 作为切入点,引出 异步函数、异步迭代器、异步生成器以及 for await of 的概念。
  • 第十章 - 第十四章:主要以传统概念的新增为主。比如:针对传统字符串拼接,新增的 模板字符串、针对于传统数组,新增的 类型化数组、针对模块化,新增的 ESM 、针对反射,代理需求而新增的 ProxyReflect,以及 Map、Set、WeakMap、WeakSet 等概念。
  • 第十五章 - 第十九章:以零碎的概念为主,比如 新的正则标志、共享内存、Bigint、取幂运算、空值合并 等知识点。以及 未来展望 等正在进行的改进。

第一章:ES2015~ES2020 及后续版本的新特性

在第一章中,主要包含三块内容,其目的是为了同步一些基本的名词和概念,以便后续的学习。

如果大家比较细心的话,那么应该会注意到咱们在视频中提到过多次的 TC39,那么这个 TC39 指的是什么意思呢?

所谓 TC39 指的是 TC39 技术委员会,他们主要 负责《通用、跨平台、与供应商无关的编程语言 ECMAScript 的标准化》 。也就是说,JavaScript 的标准 就是 TC39 委员会 定制的。

而这个标准,从 2015 年 之后,每年都会有一个版本的更新。而描述这些版本的方式主要有两种 ES版本号(ES6、ES7)、ES年份(ES2015、ES2016)

那么这个版本号和年份之间有什么关系呢?

很多小伙伴会把 ES6 当做是 2016 年之后的新增特性,这个是 不对的。所谓的 ES6 代表的是 2015 年 ECMAScript 的第六版,用版本号来说,被称作 ES6,但是如果用年份来表示,那就是 ES2015

同理所谓的 ES7,指的是 2016 年 ECMAScript 的第七版,年份号为 ES2016。以此类推。

所以大家以后千万不要把 ES6 当做是 2016年 之后发布的内容咯~~

而对于 TC39 来说,在制定新的 JavaScript 标准 时,是具备一个明确的流程的,这个流程一共被分为 5 个阶段。这 5 个阶段咱们没有必要详细去说,但是咱们需要知道的是 整个标准的制定流程是有非常严格标准的,并不是 “拍脑袋” 决定的

同时,这个流程对于我们这些开发者而言,也是可以参与进去的,如果大家想要参与到标准制定中,那么可以看下书中第一章的内容,里面有详细介绍。

而对于 TC39 来说,它们在提交制定标准时,来必须要遵循一个核心思想,那就是 别毁坏 Web(Don't break the web) 。也就是 新语法,必须保证兼容性。

当新的语法被发布之后,一些浏览器可能没有办法及时支持新的语法,所以这个时候就需要使用到 Babel 了。Babel 可以把新的标准语法降级为被浏览器兼容的语法版本。

比如,我现在想要把 ES6 语法应用到 IE 11 浏览器上,那么就可以按照脑图的方式进行(注意:版本号)。

第二章:块级作用域声明: let 和 const

let 和 constES6 之后新增的声明变量的方式。这两个关键字虽然算是 ES6 新特性,但是我相信对于大多数的小伙伴来说它已经不是一个新鲜的东西了。在实际开发中,我们早已经对它们进行了大量的使用。

但是,很多情况下,应用和掌握 通常是两个概念。下面咱们就来看看 let 和 const,看看里面是否有大家的知识盲点。

对于现在的 JavaScript 来说,声明变量的方式一共有三种:

  • var:变量,会跳出块级作用域
  • let:变量,不会跳出块级作用域
  • const:常量,不会跳出块级作用域

这里有一个块级作用域的概念。

所谓块级作用域指的是 两个大括号中间的内容,比如 for 循环、if、函数 只要存在 {} 那么都会生成块级作用域。

那么按照刚才我们所说的三种声明变量关键字的区别,如下代码的运行结果也就不意外了:

if (true) {
  var msg1 = 'var 变量'
  let msg2 = 'let 变量'
  const msg3 = 'const 变量'
}
​
console.log(msg1); // var 变量
console.log(msg2); // Uncaught ReferenceError: msg2 is not defined
console.log(msg3); // Uncaught ReferenceError: msg3 is not defined
复制代码

除了块级作用域之外,varlet、const 在特性上也有一些区别。这个区别主要体现在两个方面 变量提升、暂时性死区(TDZ

咱们先来看变量提升。对于 var 声明的变量而言,会存在变量提升的概念,也就是可以 先使用、后定义,咱么来看这段代码:

console.log(msg) // undefined (并不会报错)
var msg = 'hello world'
复制代码

在这段代码中,msg 变量先被使用,后声明。虽然打印了 undefined,但是它并不会报错。

原因是因为,以上代码会被编译为以下形式:

var msg
console.log(msg) // undefined
msg = 'hello world'
复制代码

msg 变量的定义会被提升到最前面。而这种形式就叫做 变量提升

但是如果我们使用 let 或 const 来代替 var 的话,因为 let、const 不具备变量提升,所以就会抛出对应的错误:

console.log(msg2); // Uncaught ReferenceError: Cannot access 'msg2' before initialization
let msg2 = 'hello word'
复制代码

而这样的错误,就被叫做 暂时性死区( temporal dead zone,简称TDZ )。

旧习换新

在开头的时候咱们说过从第二章开始,都会有一个 旧习换新 的环节,这里的旧习换新主要包含两点:

  • 第一点是 不要使用 var,改用 letconst:因为无论是 跳出块级作用域也好,还是变量提升也好,在标准图灵完备的编程语言中,都不是一个应该具备的特性。
  • 第二点是 缩小变量的作用域,从而提升可维护性:想要理解这句话,可能需要具备一定的编程经验。如果大家不是很理解的话,那么可以想象一下 一万行代码的文件和一百行代码的文件 哪个更好维护?我们始终需要谨记 代码越少,越容易维护。所以 缩小你的作用域空间,减少逻辑的复杂度。

第三章:函数的新特性

函数作为 JavaScript 世界一等公民,是我们在实际项目开发中,无时无刻不在使用的东西。

在这一章中,咱们主要从 参数、this 指向、构造函数 这三个方面来去说明函数的新特性。

参数

函数的参数分为两种 形参、实参。所谓形参指的是 定义函数时指定的形式参数。所谓实参指的是 调用函数时,传递的实际参数。

而在定义形参时,我们可以通过 赋值符 = 的形式,为形参指定 默认值。这表示 如果没有传递对应的实参,则该形参默认为该值

function fn(name = '张三') {
  console.log(name); // 张三
}
fn()
复制代码

这个默认值可以为 任意的单一表达式,比如我们可以指定一个 立即执行的箭头函数,那么此时默认值会为该函数的值:

// 箭头函数:() => '李四'
// (() => '李四')() 表示立即执行的箭头函数
function fn(name = (() => '李四')()) {
  console.log(name);
}
fn()
复制代码

JavaScript 中,函数的实参和形参并不要求是一一对应的。也就是说 实参的数量可以超过形参的数量。那么在这种情况下,如果我们想要获取到 多余的实参,一共有两种方式:

  • 第一种是传统的 arguments,但是它并不是通用的,在箭头函数中无法使用

    function fn(name) {
      console.log(arguments); // ['张三', 30, '男', callee: ƒ, Symbol(Symbol.iterator): ƒ]
    }
    ​
    const fn = (name) => {
      console.log(arguments); // Uncaught ReferenceError: arguments is not defined
    }
    ​
    fn('张三', 30, '男')
    复制代码
    
  • 第二种是 ES6 之后 新增的 “rest” 参数,它是通用的,表示 接收所有的剩余参数

    function fn(name, ...args) {
      console.log(args); // [30, '男']
    }
    ​
    const fn = (name, ...args) => {
      console.log(args); // [30, '男']
    }
    ​
    fn('张三', 30, '男')
    复制代码
    

this 指向

针对 this 指向,指的是 普通函数和箭头函数 下的 this 指向问题。

这应该是一个 面试 时的高频问点。当大家遇到这样的问题时,大多数时候只需要从三个方面进行回答即可:

  • 首先第一个方面是 普通函数的 this 指向:针对于普通函数而言,this 指向调用方

    function fn() {
      console.log(this); // window
    }
    fn() // window.fn()
    复制代码
    
  • 然后是箭头函数:针对于箭头函数而言,不会修改 this 指向,即 this 指向上层作用域中的 this

    const person = {
      name: '张三',
      fn() {
        console.log(this); // person
    
        const subFn = () => {
          console.log(this); // 指向上层作用域(fn)中的 this
        }
        subFn()
      }
    }
    person.fn()
    复制代码
    
  • 最后是 call、apply、bind 这三个 API:它们都可以在 普通函数 中修改 this 指向,this 指向它们的第一个参数

    const person = {
      name: '张三'
    }
    
    const fn = () => {
      console.log(this); // window  箭头函数永远不会修改 this 指向
    }
    
    function fn2() {
      console.log(this); // person
    }
    
    fn.apply(person)
    fn2.call(person)
    fn.bind(person)()
    复制代码
    

构造函数

构造函数通常指 首字母大写的普通函数 。也就是说:箭头函数永远不可以作为构造函数使用

function Person(name) {
  // this 指向实例对象
  this.name = name
}
const p = new Person('张三')
console.log(p); // Person {name: '张三'}


const Person2 = (name) => {
  // this 指向 window
  this.name = name
}
const p2 = new Person2('张三')
console.log(p2); // Uncaught TypeError: Person2 is not a constructor
复制代码

旧习换新

这一章的旧习换新主要包含四点内容,也是对本章重点内容的总结:

  • 首选是关于箭头函数和普通函数的使用场景: 不想修改 this 指向时,使用箭头函数。需要改变 this 的指向时,使用普通函数
  • 其次是关于参数默认值:使用参数默认值,而不要使用代码为参数赋初始值
  • 第三是关于剩余参数: 使用 rest 参数替代 arguments 关键字 来获取剩余参数

第四章:类

针对于第四章而言,从名字到内容都非常的纯粹,一个字

那么对于这一章的内容,让我们从一个问题开始:我们常说 JavaScript 实际上没有类,只是用原型来模拟了类?是这样的吗?

答案是 当然不是。其实从 ES2015 之后,ECMAScript 标准为 JavaScript 提供了 的概念。它并不是原型的模拟,只是 可以用原型来模拟类而已

那么下面咱们就来看看 ES2015 之后的 类语法

类语法

类语法分为 创建使用 两部分。

咱们先来看类的创建:

const fnName = 'fn' + Math.floor(Math.random() * 1000)
class Color {
  // 一个《构造函数》
  constructor(r = 0, g = 0, b = 0) {
    // 三个《数据属性》
    this.r = r
    this.g = g
    this.b = b
  }

  // 一个《访问器属性》
  get rgb() {
    // 可通过 实例.rgb 访问
    return `rgb(${this.r}, ${this.g}, ${this.b})`
  }

  set rgb(val) {
    // 为 r、g、b 赋值
    // 可通过 实例.rgb = xx 访问
  }

  // 一个《原型方法》
  toString() {
    return `重写的原型方法:${this.rgb}`
  }

  // 一个静态方法
  static fromCss(r, g, b) {
    // 利用 new this 可以直接得到 Color 实例
    return new this(r, g, b)
  }

  // 动态方法名
  [fnName]() {
    return `动态方法名为:${fnName}`
  }
}
复制代码

在这段代码中,我们通过类语法 class 创建了一个类 Color,这里大家注意,根据规范 类名首字母应该大写。这里的代码我已经写好了注释,大家可以在这里暂停来查看下对应的代码内容。

而如果想要使用类的话,那么必须要通过 new 关键字来进行使用:

const c = new Color(30, 144, 255)
console.log(c[fnName]()); // 动态方法名为:fn275
console.log(c.toString()); // 重写的原型方法:rgb(30, 144, 255)
console.log(c.rgb); // rgb(30, 144, 255)
console.log(Color.fromCss(255, 255, 255)); // Color {r: 255, g: 255, b: 255}
复制代码

以上两段代码可能有一些复杂,如果大家之前没有接触过类的话,我建议大家最好是在这里暂停视频,把上面的两段代码写一下,以加强对类的理解。

类继承

继承在编程语言中是一个非常常见的概念,在 ES6 之前想要完成继承,那么多数情况下需要使用 原型继承 的方式。而原型继承有很多种,比如:组合式继承、原型式继承、寄生式继承、寄生式组合继承 ...... 很多种方式。

但是在实际开发中,如果我们直接使用类语法的话,那么想要实现继承就非常容易了。只需要使用到一个关键字 extends

class SubColor extends Color {}
复制代码

super 关键字

而除了 extends 之外,类继承的时候还有另外一个关键字 supersuper 关键字可以用来 处理与父类相关的事情

它的使用场景主要有两个:

作为函数使用

super 关键字可以直接作为函数进行使用。比如:在构造函数中使用时,super 可以直接调用父类的构造函数,在通常情况下 这是一个必须的操作

class SubColor extends Color {
  constructor(r = 0, g = 0, b = 0, a = 1) {
    // 触发父类的构造函数
    super(r, g, b)
    this.a = a
  }
}
复制代码

作为属性查询使用

super 关键字可以用来 访问一个对象字面或类的 [[Prototype]] 的方法和属性。比如:我们可以在静态方法中利用 super 访问父类的静态方法

  class SubColor extends Color {
    static fromCss(r, g, b, a = 1) {
      // 通过 super 调用父类的静态方法
      const result = super.fromCss(r, g, b)
      //  code....
      return new this(r, g, b, a)
    }
  }
  // 子类重写的 formCss。result = {color: red} + {fontSize: 20px}
  console.log(Color.fromCss(255, 255, 255));
  console.log(SubColor.fromCss(255, 255, 255, 1)); // SubColor {r: 255, g: 255, b: 255, a: 1}
复制代码

new.target

对于类而言,最后一个需要大家关注的概念就是 new.target

new.target 属性允许你 检测函数或构造方法是否是通过 new 运算符被调用的,并且可以返回一个指向构造方法或函数的引用

我们可以利用它来判断 当前触发构造函数时是通过哪个类来触发的 ,这在 多层继承判断来源时会非常有用

  class Color {
    constructor() {
      console.log(`new.target.name: ${new.target.name}`);
    }
  }

  class SubColor extends Color {
    constructor() {
      super()
    }
  }

  new Color() // new.target: 指向 Color

  new SubColor() // new.target: 指向 SubColor
复制代码

旧习换新

这里的旧习换新环节,就比较简单了,只有一点: 实际开发中,通过 class 来完成类的构建和继承。

第五章:对象的新特性

接下来我们来看对象在 ES6 之后的新特性。

对象在我们日常开发中使用的场景是非常多的,所以这一章中的很多新特性大家或多或少的应该都有一些了解。我挑选了几个日常开发中最常用的语法,来给大家进行下分享。

首先是 可计算的属性名

有些时候,我们可能希望 对象的 key 是一个不确定的唯一值。 比如:世界上每一个人都是唯一的,所以 person 对象应该具备一个唯一的 “特性” ,那么我们就可以通过这种方式来进行表示

const key = Symbol('key')
const person = {
  name: '张三',
  // 可计算的属性名
  [key]: key
}
console.log(person); // {name: '张三', Symbol(key): Symbol(key)}
复制代码

在这段代码中,我们利用 Symbol 构建了一个 key,然后利用 [key] 作为 person唯一 key 名

同时,为了方便对象字面量的编写,ES6 之后提供了 属性简写 的语法: 当 key 和 value 拥有同样的变量名时,那么可以进行简写

const name = '张三'
const person = {
  // name: name
  name
}
复制代码

属性简写 是我们在日常开发中非常常用的一种方式。

除了属性简写之后,还有另外一个新特性也是我们在日常开发中非常常见的,那就是 展开运算符

展开运算符以 ... 的形式进行表示,可以用在对象的展开和合并的多个场景中:

const names = ['张三', '李四', '王五']

// 展开
console.log(...names); // 张三 李四 王五

// 合并
console.log(['赵六', ...names]); //['赵六', '张三', '李四', '王五']
复制代码

除此之外,在 ES6 之中还提供了很多的新增方法,这些方法咱们就不一个一个说了。大家如果对哪个方法不熟悉的话,可以到 MDN 上进行对应的查询。

旧习换新

最后是对象的旧习换新环节,这个环节的内容比较多,主要有 5 个:

  1. 当你需要一个动态的 key 时,可以通过可计算的属性名直接创建该对象
  2. 多使用属性的简写,以此来简化对象构建的过程
  3. Object.assign 方法,这是一个 ES6 新增的方法。可以 将一个对象的可枚举属性复制到另一个对象上 。但是要注意,这是一个浅拷贝的
  4. Symbol 可以构建一个唯一值。 使用 Symbol 作为 key 名,可以避免属性名冲突
  5. 最后是关于实例的原型,之前访问实例的原型时多通过 __proto__ 访问。现在可以通过 Object.setPrototypeOf、Object.getPrototypeOf 来直接访问原型