五、JavaScript 扩展(一)

118 阅读4分钟

前言

在前面的学习中,我们已经掌握了基本的 JavaScript 语法、常用的一些数据结构以及一点点核心概念,本文主要针对前面的基础进行进阶扩展学习。

1. 值的扩展

1.1 字符串扩展

模板字符串

字符串是通过字面量的方式创建的:const str = "abc"

日常开发中最常见的业务就是字符串拼接,尤其是包含变量的字符串拼接。在 JavaScript 中,可以使用 + 运算符来拼接字符串。

function introduction(name, age) {
  console.log("大家好,我叫" + name + ", 今年 " + age + " 岁了。");
}

从上面的示例可以看到,使用 + 运算符来拼接字符串很简单,这是一个简单的例子。如果我们想写一整段文本,且里面要插入换行符、空格,且要插入变量,且要保证一定的排版……这时再用 + 运算符来拼接的话,那将是一场噩梦。

所以 ES6 之后引入了 模板字符串 这个特性,表达式是两个反引号 (``) ,中间可以包含任意合法字符,可以通过 ${exp} 的方式插入变量,用这种方式改写上面的例子:

function introduction(name, age) {
  console.log(`大家好,我叫${name}, 今年 ${age} 岁了。`);
}

模板字符串除了写法上更简洁外,它还能保留换行符和空格,极大程度上保留了编码时的字符串格式,简化了字符串拼接的操作。

标签函数

这个特性在一般的业务开发中很少用到,可以戳 标签函数 来了解。

扩展方法

可查阅 MDN String 来了解最新的 String 支持的方法和属性,这些方法和属性的支持度和废弃状态都可以查到,使用到的时候可以直接查。

1.2 数组扩展

在 ES6 之前,数组的操作就已经非常丰富了,在其他编程语言里,堆和队列这样的数据结构还需要手动实现,但 JavaScript 的数组本身就既可以作为栈也可以作为队列,因为它有 push/popshift/unshift 这两对 API。

  • Array.prototype.push:在数组的末尾追加元素
  • Array.prototype.pop:取出数组末尾的元素
  • Array.prototype.shift:取出数组的第一个元素
  • Array.prototype.unshift:在数组的最前端追加元素

栈的特性:后进先出。结合这两对 API,将数组作为栈使用:push 入栈,pop 出栈。 队列的特性:先进先出。结合这两对 API,将数组作为队列使用:push 入队,shift 出队列。

虽然 ES6 之前的数组已经具备了很强大的功能,但由于各浏览器厂商对 JavaScript 内核实现的不一致,以及历史原因,还是存在一定的问题。

ES6 约定了 数组空位 的值为 undefined,增加了 Array.fromArray.of 两个静态方法来将其他类型的数据结构转为数组,并增加了 keys/values/entries 这样的实例方法来更为便捷地遍历数组,以及 find/findIndex 这样的实例方法来更为便捷地查找数组里的元素。

在 Chrome 的控制台里,数组空位的地方显示的是 empty ,实际取到的值是 undefined

const arr = [];
arr.length = 2;
console.log(arr); // [undefined, undefined],在 Chrome 的 devtools 控制台上会看到 [empty × 2]

目前 JavaScript Array 支持的 API 及其可以查阅 MDN Array

1.3 函数扩展

在 ES6 之前,函数只能通过 function 关键字来定义,所以以前定义函数都是:

var fn = function fn(){}

function fn2() {}

var obj = {
  method: function() {}
}

(function iife(){})()

ES6 提出了两个扩展写法:箭头函数、对象方法简写。

var fn = () => {};

() => {};

var obj = {
  method() {}
};

(() => {})();

function 函数和箭头函数有两大区别:

  • this 指向不同

    • 箭头函数中的 this 是静态的,与它所在作用域的 this 相同
    • function 函数中的 this 是动态的,可以在调用时改变其内部的 this 指向
  • arguments 参数

    • 关于这个参数,可以查看文档 MDN Arguments 对象
    • 箭头函数内部没有 arguments
    • function 函数在内部可以直接访问 arguments

在 JavaScript Function 中,一直以来都有很多让人迷惑的 this 行为,原因是 function 函数的 this 绑定方式有好几种。

函数的 this 绑定

  • 默认绑定
    • 调用函数的时候没有指定 this 值,此时函数内部的 this 会默认绑定为 window/undefined
      • 严格模式下是 undefiend
      • 非严格模式下是 window
  • 隐式绑定
    • 这种绑定适用于对象方法
    • 在对象中被用于定义为对象方法的 function 函数,其内部的 this 将被隐式绑定到包含它的这个对象上
  • 显示绑定
    • 通过 call/apply 的方式来调用函数
    • 通过 bind 来强绑定,得到新的函数引用
  • new 绑定
    • 构造函数内的 this 会指向其构造出的对象
function fn1(){
  console.log(this);
}
fn1(); // 此时打印的是 undefined 或者 window

const obj = {
  name: "obj",
  method: fn1,
};
obj.method(); // 此时打印的是 obj

const obj2 = {
  name: "obj2",
};
fn1.call(obj2); // 此时打印的是 obj2

const obj3 = {
  name: "obj3",
};
const obj3Fn = fn1.bind(obj3);
obj3Fn();  // 此时打印的是 obj3

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const p = new Person("Sherry", 8);
console.log(p);

箭头函数和 function 函数各有特点,一般在 事件监听回调函数 里需要考虑 this 指向的问题,此时使用箭头函数来定义回调函数,避免回调函数里的 this 指向问题。

还有在 React Class Component 开发时也需要考虑这个问题。

有很多种方式可以改变函数、对象方法中的 this 指向,只要理解了 “任何函数/方法都是一个引用值”,知道 function 函数的 this 指向是动态确定的,就能理解为什么函数里的 this 值被意外修改了。

函数参数

  • ES2015 为函数参数增加了一个特性:支持设置默认值。
  • ES2017 为函数参数增加了一个特性:参数列表支持尾随逗号。
// 函数参数支持默认值
function foo(height, width = 10) {
  // 函数体
}

function thisIsALongFunctionName(
  thisIsALongArgName,
  thisIsALongArgName2, // 这里可以加逗号了
) {
  // 函数体
}

参数列表支持尾随逗号 这个特性有什么实际意义吗?当然是有的,比如上面的例子,支持了尾随逗号之后, 对 Prettier、ESLint、Git 更友好了。

事件监听

事件监听在 JavaScript 开发里再常见不过了,存在 addEventListener 的代码,必然也需要 removeEventListener

addEventListener 里,你是否经常这样写:

xxx.addEventListener("change", (e) => {
  console.log(e.target.value);
});

如果是这样,那你一定会面临一个问题:怎么移除这个事件监听?

对不起,没有办法,只能等 xxx 被垃圾回收的时候,这个事件监听才会被销毁。

更合适的做法是这样:

const handler = (e) => {
  console.log(e.target.value);
};

xxx.addEventListener("change", handler);

// 在合适的地方
xxx.removeEventListener("change", handler);

2. 语法扩展

2.1 异步编程

JavaScript 一直是单线程的,且ES6之前没有对异步的支持,那时候想要执行一些异步的逻辑,就必须使用回调函数,这就导致了被人们所诟病的“回调地狱”。

ES6中,先后提出了 PromiseGeneratorasync...await 来支持 JavaScript 中的异步编程。

  • ES2015 提出了 Promise 规范,正式让 JavaScript 支持了异步编程,Promise 的链式调用较好地解决了“回调地狱”这个问题。
  • Generator 也是在 ES2015中被提出的,它是由对象生成器函数返回,并且它符合可迭代协议、迭代器协议,一般业务开发中使用较少。
  • async...await 于 ES2017 中写入规范,它本质上是一个语法糖,将异步代码用看似同步代码的方式去编写,通常用于改造 Promise 链。
function promiseDemo() {
  new Promise((resolve) => {
    setTimeout(() => {
      resolve("返回值1");
    }, 1000);
  })
    .then((res) => {
      console.log(res);
      return "返回值2";
    })
    .then((res) => {
      console.log(res);
      return "返回值3";
    })
    .then((res) => {
      console.log(res);
      return "返回值4";
    })
    .then((res) => {
      console.log(res);
      return "返回值5";
    });
}

async function asyncAwaitDemo() {
  const res1 = await createResponse(1);
  console.log(res1);
  const res2 = await createResponse(2);
  console.log(res2);
  const res3 = await createResponse(3);
  console.log(res3);
  const res4 = await createResponse(4);
  console.log(res4);
  const res5 = await createResponse(5);
  console.log(res5);

  async function createResponse(count) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(`返回值${count}`);
      });
    });
  }
}

async...await 还可以使用 try..catch 来捕获 await 的异常

async function demo() {
  try {
    await Promise.reject(new Error("demo 异常"));
  } catch (error) {
    console.error(error); // 这里会打印出 error
  }
}

2.2 异常处理

在 ES2019 以前,try...catch 中的 catch 必须指明 catch(error),而 ES2019 提出 catch(error) 可以省略,具体语法如下:

// ES2019 以前
try {
  // 代码执行
} catch(error) {
  // 异常捕获
}


// ES2019 以后
try {
  // 代码执行
} catch {}

2.3 ... 运算符

这个运算符有两种操作:

  • 展开运算。
    • ES2015 中提出时,可以作用于实现了 迭代协议 的目标上,比如数组、字符串
    • ES2018 中增加了支持,可以作用于对象
  • 剩余收集。
    • 剩余元素收集,作用于数组
    • 剩余属性收集,作用于对象
    • 剩余参数收集,作用于函数的参数列表
const arr = [0, 1, 2];
const arr2 = [...arr]; // 数组的展开运算,相当于数组浅拷贝

const str = "hello";
const charts = [...str]; // 字符串的展开运算,相当于将字符串拆分成单个字符的数组

const obj1 = { a: "a", b: "b", c: "c" };
const obj2 = { ...obj1 }; // 对象的展开运算,相当于对象浅拷贝

const [first, ...rest] = arr; // 数组的剩余元素收集,相当于把剩余元素浅拷贝到 rest 数组中
const { a, ...restProperties } = obj1; // 对象的剩余属性收集,相当于把剩余属性浅拷贝到 restProperties 对象中

// 函数的剩余参数收集,相当于把所有参数按顺序放到 args 数组中
function foo(...args) {
  // 函数体
}

// 函数的剩余参数收集,相当于第二个位置及其往后的参数按顺序放到 args 数组中
function baz(arg1, ...args) {
  // 函数体
}

浅拷贝深拷贝 是 JavaScript 里比较重要的概念,你可以点击查阅对应的文档。

2.4 ESM 模块化

ESM 即 ECMAScript Module,这是 ECMAScript 标准的模块化方案,CommonJS、UMD、AMD 这些都是JS模块化的社区方案。

不论哪个模块化,最重要的概念都是 导入/导出。在 ESM 中:

  • 导出:默认导出、具名导出
  • 导入:默认导入、具名导入、异步导入

ESM 的一个模块既可以有默认导出,也可以有具名导出。

导入一个模块时,可以既导入默认导出,也可以导入具名导出。

// #region moduleA.js

const a1 = "a1";
const a2 = "a2";

export default a1; // 默认导出
export { a2 } // 具名导出
export const a3 = "a3"; // 具名导出

// #endregion

// #region moduleB.js

import a1, { a2, a3 } from "./moduleA.js"; // 同时包含了默认导入和具名导入

// 其他代码

// #endregion

异步导入

在 Webpack、Vite 等打包工具里,一般都有内置的一些异步导入实现,ESM 提出的异步导入和 Webpack 的很接近。

// webpack 的异步导入
import("./moduleA.js").then({default: a1, a2, a3} => {
  console.log(a1);
  console.log(a2);
  console.log(a3);
});

// ESM 提案的异步导入
var promise = import("module-name"); // 这是一个处于第三阶段的提案。

导入重命名

关键字:as

import { default as aliasA2, a2 as aliasA2, a3 } from "./moduleA.js"

总结

本文介绍了 ES2015 及以后版本里 JavaScript 引入的新语法,模板字符串、数组 API、箭头函数、异步编程、... 运算符、模块化,这些语法在日常开发中都非常常用,熟悉、掌握、活用这些语法,能极大地提高你的代码编写能力、工程化能力、代码清晰度。