前言
在前面的学习中,我们已经掌握了基本的 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/pop、shift/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.from 和 Array.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中,先后提出了 Promise、Generator 和 async...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) {
// 函数体
}
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、箭头函数、异步编程、... 运算符、模块化,这些语法在日常开发中都非常常用,熟悉、掌握、活用这些语法,能极大地提高你的代码编写能力、工程化能力、代码清晰度。