代码变得更简洁、更易编写,并升级了新功能。
我们来看看这些特性,看看你是否错过了哪些。
1.尾随逗号
在ES8之前,尾随逗号会导致语法错误!
❌ 之前:
const colors = [
'red',
'blue',
'green',
'yellow', // ❌ 不允许
];
const person = {
name: 'Tari Ibaba',
site: 'codingbeautydev.com' // ❌ 不行
};
但这引发了一些问题,重新排列列表会带来麻烦:
图片
我们还必须总是在最后一项添加逗号才能添加新项 — 这会使git差异变得混乱:
图片
所以ES8修复了所有这些:
✅ 现在:
const colors = [
'red',
'blue',
'green',
'yellow', // ✅ yes
];
const person = {
name: 'Tari Ibaba',
site: 'codingbeautydev.com', // ✅ yes
};
它们带来的好处也使得像Prettier这样的工具在格式化后默认添加它们:
图片
2.async/await
这就是async/await的起源!
不再需要烦人的then()嵌套:
❌ 之前:
wait().then(() => {
console.log('WHERE ARE YOU?! 😠');
});
function wait() {
return new Promise((resolve) =>
setTimeout(resolve, 10 * 1000)
);
}
✅ 现在:
// 💡 immediately invoked function expression (IIFE)
(async () => {
await wait();
console.log('WHERE ARE YOU?! 😠');
})();
function wait() {
return new Promise((resolve) =>
setTimeout(resolve, 10 * 1000)
);
}
区别很明显:
❌ 之前:
function getSuggestion() {
fetch('https://api.example/suggestion', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({}) // Pass the necessary payload here
})
.then((res) => {
return res.json();
})
.then((data) => {
const { suggestion } = data;
console.log(suggestion);
});
}
✅ 现在:
async function getSuggestion() {
const res = await fetch('https://api.example/suggestion');
const { suggestion } = await res.json();
console.log(suggestion);
}
10行 → 3行。
使用async/await,我们终于可以为异步代码使用原生的 try-catch:
❌ ES8之前:
startWorkout();
function startWorkout() {
goToGym()
.then((result) => {
console.log(result);
})
.catch((err) => {
console.log(err);
});
}
function goToGym() {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) {
reject(new Error("I'm tired today!😴"));
}
resolve("Let's go!🏃♂️");
});
}
✅ 现在:
startWorkout();
// ✅ async/await
async function startWorkout() {
try {
await goToGym();
} catch (err) {
console.log(err);
}
}
function goToGym() {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) {
reject(new Error("I'm tired today!😴"));
}
resolve("Let's go!🏃♂️");
});
}
3.强大的Object静态方法
Object.values()
一个出色的静态方法,可以将对象的所有值提取到一个数组中:
const person = {
name: 'Tari Ibaba',
site: 'codingbeautydev.com',
color: '🔵blue',
};
const arr = Object.values(person);
// ['Tari Ibaba', 'codingbeautydev.com', '🔵blue']
console.log(arr);
非常适合数据可视化:
const fruits = [
{
name: 'Banana',
pic: '🍌',
color: 'yellow',
},
{
name: 'Apple',
pic: '🍎',
color: 'red',
},
];
const keys = Object.keys(fruits.at(0));
const header = keys.map((key) => `| ${key} |`).join('');
const rows = fruits
.map((fruit) =>
keys.map((key) => `| ${fruit[key]} |`).join('')
).join('\n');
console.log(header + '\n' + rows);
图片
Object.entries()
const person = {
name: 'Tari Ibaba',
site: 'codingbeautydev.com',
color: '🔵blue',
};
const arr = Object.entries(person);
console.log(arr);
将对象中的每个键值对捆绑在一起,生成一个元组列表:
非常适合使用对象的键和值进行数据转换:
以ID为键的对象 → 对象列表:
❌ 之前:
const tasks = {
1: {
title: '🏋️HIIT 30 minutes today',
complete: false,
},
2: {
name: 'Buy the backpack🎒',
complete: true,
},
};
const taskList = Object.keys(tasks).map((id) => ({
id,
...tasks[id],
}));
console.log(taskList);
✅ 现在:
// ✅ 更简洁
const taskList = Object.entries(tasks).map(
([id, task]) => ({
id,
...task,
})
);
console.log(taskList);
图片
4.原生字符串填充
2016年3月22日,流行的NPM包left-pad被创建者作为一种抗议形式下架,导致数千个软件项目崩溃。
这让许多人担心我们可能过度依赖外部模块 — 即使是像字符串填充这样简单的功能。
但幸运的是,ES8为JavaScript带来了原生的字符串填充功能,即padStart和padEnd字符串方法:
const name = 'tari';
console.log(name.padStart(9, ' ')); // ' tari'
console.log(name.padEnd(10, '🔴')); // 'tari🔴🔴🔴🔴'
我们不再需要依赖另一个第三方依赖。
5. Object.getOwnPropertyDescriptors()
名字听起来有点花哨,但很容易理解。
描述符是属性的属性 — 以下之一:
- value
- enumerable
- get
- set
- configurable
- enumerable
const person = {
name: 'Tari Ibaba',
color: '🔵color',
age: 999,
greet: () => console.log('Hey!'),
};
console.log(
Object.getOwnPropertyDescriptors(person)
);
图片
最后的思考
总的来说,ES8对JavaScript来说是一个重大飞跃,引入了几个已成为现代开发必不可少的特性。使你能够编写更简洁、更富表现力和更清晰的代码。
过去10年里,JavaScript取得了长足进步,每年都有全新的功能升级。
今天,我们来看看早期ES9中引入的5个最重要的特性,看看你是否错过了其中一些。
1. 异步生成器和迭代
异步生成器是ES9中一个强大的特性。
就像普通的生成器,但现在它可以在异步工作(如网络请求)后弹出值:
function* asyncGenerator() {
yield new Promise((resolve) =>
setTimeout(() => resolve('done this ✅'), 2000)
);
yield new Promise((resolve) =>
setTimeout(() => resolve('done that ✅'), 3000)
);
}
当我们调用.next()时,我们会得到一个Promise:
const asyncGen = asyncGenerator();
asyncGen.next().value.then(console.log);
asyncGen.next().value.then(console.log);
这是一个强大的工具,可以在web应用中以结构化+可读的方式流式传输数据 — 看看这个为类似YouTube的视频分享应用缓冲和流式传输数据的函数:
async function* streamVideo({ id }) {
let endOfVideo = false;
const downloadChunk = async (sizeInBytes) => {
const response = await fetch(
`api.example.com/videos/${id}`
);
const { chunk, done } = await response.json();
if (done) endOfVideo = true;
return chunk;
};
while (!endOfVideo) {
const bufferSize = 500 * 1024 * 1024;
yield await downloadChunk(bufferSize);
}
}
现在要消费这个生成器,我们将使用for await of — 异步迭代:
for await (const chunk of streamVideo({ id: 2341 })) {
// process video chunk
}
我想知道实际的YouTube JavaScript代码是否使用了这样的生成器?
2.对象的剩余/展开运算符
毫无疑问,你在某处遇到过现代的展开语法。
这是一种快速且不可变地克隆数组的天才方法:
const colors = ['🔴', '🔵', '🟡'];
console.log([...colors, '🟢']);
// ['🔴', '🔵', '🟡', '🟢']
在ES6之前我们从未有过它,现在它无处不在。
Redux就是一个重要的例子:
export default function userState(state = initialUserState, action) {
console.log(arr);
switch (action.type) {
case ADD_ITEM:
return {
...state,
arr: [...state.arr, action.newItem]
};
default:
return state;
}
}
从ES9开始,它也适用于对象:
const info = {
name: 'Coding Beauty',
site: 'codingbeautydev.com',
};
console.log({ ...info, theme: '🔵' });
覆盖属性:
const langs = {
j: 'java',
c: 'c++',
};
console.log({ ...langs, j: 'javascript' });
// Output: { j: 'javascript', c: 'c++' }
这使得它特别适合在默认值的基础上构建,尤其是在制作公共实用程序时。
或者像我用Material UI定制默认主题那样:
图片
使用展开语法,你甚至可以去掉不想在副本中出现的对象属性。
const colors = {
yellow: '🟡',
blue: '🔵',
red: '🔴',
};
const { yellow, ...withoutYellow } = colors;
console.log(withoutYellow);
// Output: { blue: '🔵', red: '🔴' }
这就是如何以不可变的方式从对象中移除属性。
3. String.raw
当我使用String.raw时,我是在说:只给我我给你的东西。不要处理任何东西。不要动那些转义字符:
不再需要转义反斜杠,我们不用写:
const filePath = 'C:\\Code\\JavaScript\\tests\\index.js';
console.log(`The file path is ${filePath}`);
// Output: The file path is C:\Code\JavaScript\tests\index.js
而是写:
const filePath = String.raw`C:\Code\JavaScript\tests\index.js`;
console.log(`The file path is ${filePath}`);
// Output: The file path is C:\Code\JavaScript\tests\index.js
非常适合编写带有大量这些反斜杠的正则表达式:
像这样但更糟:
从这个✅:
const patternString = 'The (\\w+) is (\\d+)';
const pattern = new RegExp(patternString);
const message = 'The number is 100';
console.log(pattern.exec(message));
// ['The number is 100', 'number', '100']
到这个✅:
const patternString = String.raw`The (\w+) is (\d+)`;
const pattern = new RegExp(patternString);
const message = 'The number is 100';
console.log(pattern.exec(message));
// ['The number is 100', 'number', '100']
所以"raw"意味着未处理的。
图片
这就是为什么我们有String.raw()但没有String.cooked()。
4. 复杂的正则表达式特性
说到正则表达式,ES9并没有让人失望。
它完全装载了最先进的正则表达式特性,用于高级字符串搜索和替换。
向后查找断言
这是一个新特性,用于确保只有某个特定模式出现在你要搜索的内容之前:
- 正向后查找:白名单 ?<=pattern
- 负向后查找:黑名单 ?
const str = "It's just $5, and I have €20 and £50";
// Only match number sequence if $ comes first
const regexPos = /(?<=\$)\d+/g;
console.log(str.match(regexPos)); // ['5']
const regexNeg = /(?
图片
命名捕获组
捕获组一直是正则表达式中最宝贵的特性之一,用于以复杂的方式转换字符串。
const str = 'The cat sat on a map';
// $1 -> [a-z]
// $2 -> a
// $3 -> t
// () indicates group
str.replace(/([a-z])(a)(t)/g, '$1*$3');
// -> The c*t s*t on a map
通常,这些组按照它们在正则表达式中的相对位置命名:1, 2, 3...
但这使得理解和更改那些愚蠢的长正则表达式变得更加困难。
所以ES9通过?
const str = 'The cat sat on a map';
// left & right
console.log(str.replace(/(?[a-z])(a)(?t)/g, '$*$'));
// -> The c*t s*t on a map
图片
你知道当VS Code中出现错误时,你可以快速Alt + 点击跳转到错误发生的确切位置吗?👇
图片
VS Code使用捕获组使文件名可点击,从而实现这种快速导航。
我想它大概是这样的:
// The stupidly long regex
const regex = /(?[a-z]:[a-z].(?:?:\\/|(?:\\/?)))[\w \-]+):(?\d+):(?\d+)/gi;
// ✅ String.raw!
const filePoint = String.raw`C:\coding-beauty\coding-beauty-javascript\index.js:3:5`;
const extractor = /(?[a-z]:[a-z].(?:?:\\/|(?:\\/?)))[\w \-]+):(?\d+):(?\d+)/i;
const [path, lineStr, charStr] = filePoint
.match(regex)[0]
.match(extractor)
.slice(1, 4);
const line = Number(lineStr);
const char = Number(charStr);
console.log({ path, line, char });
// Replace all filePoint with
5. Promise.finally
最后我们有了Promise.finally 😉。
你知道finally总是会运行一些代码,无论是否有错误吗?
function startBodyBuilding() {
if (Math.random() > 0.5) {
throw new Error("I'm tired😩");
}
console.log('Off to the gym 🏋️♂️💪');
}
try {
startBodyBuilding();
} catch {
console.log('Stopped excuse🛑');
} finally {
console.log("I'm going!🏃♂️");
}
所以Promise.finally就像那样,但是用于异步任务:
async function startBodyBuilding() {
await think();
if (Math.random() > 0.5) {
throw new Error("I'm tired😩");
}
console.log('Off to the gym 🏋️♂️💪');
}
startBodyBuilding()
.then(() => {
console.log('Started ✅');
})
.catch(() => {
console.log('No excuses');
})
.finally(() => {
console.log("I'm going!🏃♂️");
});
Promise.finally()最大的优点是当你链接许多Promise时:
它也能很好地与Promise链一起工作:
getFruitApiUrl().then((url) => {
return fetch(url)
.then((res) => res.json())
.then((data) => {
fruits.push(data);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
console.log(fruits);
});
});
这是由ES9带来的。
最后的思考
ES9标志着JavaScript的一个重大飞跃,引入了几个对现代开发至关重要的特性。使你能够快速编写更清晰、更简洁、更富表现力的代码。
JavaScript在过去10年里取得了长足的进步,每一年都有全新的功能升级。
还记得我们以前是这样创建"类"的吗?
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log("Hello, " + this.name);
};
是的,变化很大!
让我们来看看ES10(2019年)中引入的7个最重要的特性,看看你是否错过了其中一些。
1. 即时模块化:动态import
ES10那年很棒,import现在可以像require()一样作为函数使用。一个async函数。
将import保持在顶层不再是必须的;我们现在可以在编译时轻松解析模块的名称。
为了高性能,可以选择性地只在绝对需要时加载模块...
if (user.is_admin) {
const admin = await import('./admin.js');
admin.setupDashboard();
}
基于用户或变量输入加载模块...
const language = 'french';
const translations = await import(`./translations/${language}.js`);
它也非常适合使用不再支持require()的ES模块:
2. 扁平化曲线
flat()和flatMap()提供了更清晰的方式来轻松扁平化多维数组。
消除了痛苦的数组循环扁平化代码的需求:
图片
flatMap()相当于调用map(),然后flat(1):
图片
3. 将数组转换为对象
ES10还引入了Object.fromEntries()到JavaScript世界。
快速将键值对列表转换为等效的键值对象:
const entries = [['name', 'John'], ['age', 30]];
const obj = Object.fromEntries(entries);
console.log(obj); // { name: 'John', age: 30 }
4. 精确清理你的字符串
trimStart()和trimEnd()。
在此之前,每个人都在使用NPM的trim - 愉快地给项目增加3.35KB...
即使现在:
npm i trim
然后Array trim()出现了,接着是trimStart()和trimEnd()。
const str = ' Hello, World! ';
console.log(str.trimStart()); // 'Hello, World! '
console.log(str.trimEnd()); // ' Hello, World!'
5. 捕获错误而不带包袱
通过新的可选catch绑定,当你对错误参数无所作为时,现在可以安全地省略catch块的错误参数:
图片
6. 无惊喜排序
稳定的数组排序。
以前,在对数组进行排序时,我们绝对无法保证相等元素的排列。
但在ES10之后的JS代码中,我们100%确定react总是在vue之前,vue总是在angular之前。
图片
图片
7. 要么做大,要么回家:BigInt
BigInt的名称揭示了它的目的:用于加载难以置信的巨大整数值:
图片
图片
因为普通整数做不到:
图片
最后的思考
ES10为JavaScript标志着一个重要的飞跃,引入了几个对现代开发至关重要的特性。
使用它们来编写更清晰、更简洁、更具表现力和清晰度的代码。
ES13包含了许多有价值的特性,彻底改变了我们编写JavaScript的方式。
从异步升级到数组语法糖等等,让我们来看看这些特性,看看你是否错过了其中一些。
1. 顶级await
在ES13之前,我们永远不能在全局作用域中使用await。
❌ 之前:
// X 语法错误:await 只在异步函数中有效
await setTimeoutAsync(3000);
function setTimeoutAsync(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('codingbeautydev.com');
}, timeout);
});
}
我们总是必须将其放在async函数中或创建一个async IIFE(立即执行函数表达式):
// 异步立即执行函数
(async () => {
await setTimeoutAsync(3000);
})();
// 类似 C++
async function main() {
await setTimeoutAsync(3000);
}
✅ ES13之后:
// ✅ 等待超时 - 没有抛出错误
await setTimeoutAsync(3000);
function setTimeoutAsync(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('codingbeautydev.com');
}, timeout);
});
}
2. 类声明升级
2.1 类字段声明
在ES13之前,我们只能在构造函数中声明类字段: 与许多其他语言不同,我们不能在类的最外层作用域中声明或定义它们。
❌ 之前:
✅ 现在有了ES13: 就像在TypeScript中一样:
2.2 私有方法和字段
在ES13之前,创建私有方法是不可能的。 我们还必须使用丑陋的下划线hack来表示私有性 — 但那只是一个指示。
❌ 之前:
class Person {
_firstName = 'Tari';
_lastName = 'Ibaba';
get name() {
return `${this._firstName} ${this._lastName}`;
}
}
const person = new Person();
console.log(person.name); // Tari Ibaba
// 我们仍然可以访问私有成员!
console.log(person._firstName); // Tari
console.log(person._lastName); // Ibaba
// 它们也可以被修改!
person._firstName = 'Lionel';
person._lastName = 'Messi';
console.log(person.name); // Lionel Messi
✅ ES13之后:
我们可以通过在字段前加上井号(#)来为类添加私有字段和成员:
如果你试图从类外部访问它,你会得到一个语法错误:
class Person {
#firstName = 'Tari';
#lastName = 'Ibaba';
get name() {
return `${this.#firstName} ${this.#lastName}`;
}
}
const person = new Person();
console.log(person.name);
// 语法错误:私有字段 '#firstName' 必须在封闭的类中声明
console.log(person.#firstName);
console.log(person.#lastName);
我们可以从错误消息中看到一些有趣的东西:
编译器甚至不期望你从类外部尝试访问私有字段 — 它假设你是在尝试创建一个。
2.3 静态类字段和静态私有方法
静态字段 — 类本身的属性,而不是任何特定实例的属性。
自ES13以来,我们现在可以轻松地为任何类创建它们:
class Person {
static #count = 0;
static eyeCount = 2;
static getCount() {
// 使用 this 访问同级静态成员
return this.#count;
}
// 实例成员
constructor() {
// 使用 this.constructor 访问静态成员
this.constructor.#incrementCount();
}
static #incrementCount() {
this.#count++;
}
}
const person1 = new Person();
const person2 = new Person();
console.log(Person.getCount()); // 2
3. 数组升级:新的at()方法
通常我们会使用方括号([])来访问数组的第N个元素。
const arr = ['a', 'b', 'c', 'd'];
console.log(arr[1]); // b
但从末尾访问第N个项目一直是一个痛点 -- 我们必须使用arr.length - N进行索引:
❌ ES13之前:
const arr = ['a', 'b', 'c', 'd'];
// 倒数第1个元素
console.log(arr[arr.length - 1]); // d
// 倒数第2个元素
console.log(arr[arr.length - 2]); // c
幸运的是,ES13带来了一个新的at()方法,解决了所有这些问题:
const str = 'Coding Beauty';
console.log(str.at(-1)); // y 倒数第1个字符
console.log(str.at(-2)); // t 倒数第2个字符
4. 静态类块
随着静态字段的出现,静态块也来了。 只在类创建时执行一次代码 — 就像C#和Java等OOP语言中的静态构造函数。 所以你可以在类中创建任意多个静态块 — 所有代码都会按你定义的顺序运行:
class Vehicle {
static defaultColor = 'blue';
}
class Car extends Vehicle {
static colors = [];
// 👇 pushes red before green
// 👇 先添加 red,然后添加 green
static {
this.colors.push(super.defaultColor, 'red');
}
static {
this.colors.push('green');
}
}
console.log(Car.colors); // ['blue', 'red', 'green']
5. 错误报告升级
有时我们捕获调用栈下方方法的错误,只是为了将其重新抛出回调用栈上方。 但当我们这样做时,我们会失去原始错误中的关键信息:
try {
userAction();
} catch (err) {
// ❌ doesn't know fundamental cause of error
// ❌ 不知道错误的根本原因
console.log(err);
}
function userAction() {
try {
apiCallThatCanThrow();
} catch (err) {
// 👇 rethrow
// 👇 重新抛出错误
throw new Error('New error message');
}
}
function apiCallThatCanThrow() {
console.log('fetching from codingbeautydev.com...');
throw new Error('throwing for no reason');
}
这就是为什么ES13引入了一个新的cause属性来保留这个重要信息并使调试更容易:
try {
userAction();
} catch (err) {
// ✅ now knows what caused the error
// ✅ 现在知道了错误的原因
console.log(err);
console.log(`Caused by: ${err.cause}`);
}
function userAction() {
try {
apiCallThatCanThrow();
} catch (err) {
// ✅ error cause
// ✅ 错误原因
throw new Error('New error message', { cause: err });
}
}
function apiCallThatCanThrow() {
console.log('fetching from codingbeautydev.com...');
throw new Error('throwing for no reason');
}
最后的思考
总的来说,ES13对JavaScript来说是一个重大飞跃,它带来了几个已成为现代开发必不可少的特性。 使你能够编写更清晰、更简洁、更具表现力的代码。
JavaScript在过去10年里取得了长足的进步,每年都有全新的功能升级。 让我们来看看ES14(2023年)中引入的5个最重要的特性,看看你是否错过了其中一些。
1. toSorted()
甜美的语法糖。
ES14的toSorted()方法使得排序数组并返回一个副本而不改变原数组变得更加容易。
以前我们这样做:
const numbers = [3, 1, 4, 1, 5];
const sorted = [...numbers].sort((a, b) => a - b);
console.log(sorted); // [1, 1, 3, 4, 5]
console.log(numbers); // [3, 1, 4, 1, 5]
现在我们可以这样做✅:
const numbers = [3, 1, 4, 1, 5];
const sorted = numbers.toSorted((a, b) => a - b);
console.log(sorted); // [1, 1, 3, 4, 5]
console.log(numbers); // [3, 1, 4, 1, 5]
toSorted()接受一个回调函数来控制排序行为 - 升序或降序,按字母顺序或数字顺序。就像sort()一样。
2. toReversed()
另一个新的数组方法,用于促进不可变性和函数式编程。
以前 — 使用reverse() ❌:
const numbers = [1, 2, 3, 4, 5];
const reversed = numbers.reverse();
console.log(reversed); // [5, 4, 3, 2, 1]
console.log(numbers); // [5, 4, 3, 2, 1]
现在 — 使用toReversed() ✅:
const numbers = [1, 2, 3, 4, 5];
const reversed = numbers.toReversed();
console.log(reversed); // [5, 4, 3, 2, 1]
console.log(numbers); // [1, 2, 3, 4, 5]
我发现这些不可变方法非常棒,可以不断地链式调用方法,而不用担心原始变量:
const result = numbers.toReversed().toSorted((a, b) => a - b);
3. toSpliced()
函数式编程爱好者无疑会对所有这些新的数组方法感到高兴。 这是.splice()的不可变版本:
const items = [1, 2, 3, 4, 5];
const newItems = items.toSpliced(2, 1, 6, 7);
console.log(newItems); // [1, 2, 6, 7, 4, 5]
console.log(items); // [1, 2, 3, 4, 5]
4. 从末尾开始查找数组
从第一项开始搜索并不总是理想的:
图片
你可以很容易地看到,对我们的巨大列表从末尾而不是开始搜索会快得多。
图片
有时你必须从末尾搜索才能让你的程序工作。
比如我们想在一个数字列表中找到最后一个偶数,find和findIndex会非常不准确。 调用reverse()也不行,即使它会很慢:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const lastEven = numbers.reverse().find(n => n % 2 === 0);
console.log(lastEven); // 10(不正确)
所以在这种情况下,findLast()和findLastIndex()方法就派上用场了。
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const lastEven = numbers.findLast(n => n % 2 === 0);
console.log(lastEven); // 10(正确)
这段代码更短、更易读。最重要的是,它产生了正确的结果。
5. 数组的with()方法
with()是我们快速更改数组元素而不进行任何突变的方法。
以前的常规方式:
const arr = [1, 2, 3, 4, 5];
const newArr = [...arr];
newArr[2] = 6;
console.log(newArr); // [1, 2, 6, 4, 5]
console.log(arr); // [1, 2, 3, 4, 5]
ES14现在让我们这样做:
const arr = [1, 2, 3, 4, 5];
const newArr = arr.with(2, 6);
console.log(newArr); // [1, 2, 6, 4, 5]
console.log(arr); // [1, 2, 3, 4, 5]
最后的思考
还有其他特性,但ES14主要是关于更容易的函数式编程和内置的不可变性。 随着React的兴起,我们看到声明式JavaScript爆炸式地流行起来;很自然地,更多的这些特性会被烘焙到语言中,成为甜美的语法糖。
2024年:又是一个带来全新JS特性升级的不可思议的年份,ES15推出。
从复杂的异步特性到语法糖数组和现代正则表达式,JavaScript编码现在比以往任何时候都更简单、更快捷。
1.原生数组分组终于到来
Object.groupBy():
const fruits = [ { name: 'pineapple🍍', color: '🟡' }, { name: 'apple🍎', color: '🔴' }, { name: 'banana🍌', color: '🟡' }, { name: 'strawberry🍓', color: '🔴' },];const groupedByColor = Object.groupBy( fruits, (fruit, index) => fruit.color);// 原生 group by 示例console.log(groupedByColor);
图片
字面意思就是让恐龙级的 Lodash 库失去了最后的存在理由 - 再也不需要了!
图片
我原本期待一个新的实例方法,比如Array.prototype.groupBy,但不知什么原因他们把它做成了静态方法。
然后我们还有Map.groupBy来用对象键进行分组:
const array = [1, 2, 3, 4, 5];const odd = { odd: true };const even = { even: true };Map.groupBy(array, (num, index) => { return num % 2 === 0 ? even : odd;});// => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
不过几乎没人会这样对数组分组,所以可能不会那么受欢迎。
2.从外部解决promise - 现代方式
使用Promise.withResolvers()。
从外部解决promises是很普遍的需求,在此之前我们不得不使用Deferred类来实现:
class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); }}const deferred = new Deferred();deferred.resolve();
或者从NPM安装 - 又多了一个依赖!
图片
但现在有了ES15的Promise.withResolvers():
const { promise, resolve, reject } = Promise.withResolvers();
看看我如何用它来快速地将事件流promise化 - await一个observable:
// data-fetcher.jsconst { promise, resolve, reject } = Promise.withResolvers();function startListening() { eventStream.on('data', (data) => { resolve(data); });}async function getData() { return await promise;}// client.jsconst { startListening, getData } = require('./data-fetcher.js');startListening();// ✅ 监听单个流事件const data = await getData();
3. Buffer性能升级
Buffers是用来存储应用程序生成的临时数据的小型数据存储。
它们使得在管道的各个阶段之间传输和处理数据变得非常容易。
像这样的管道:
- 文件处理: 输入文件 → buffer → 处理 → 新buffer → 输出文件
- 视频流: 网络响应 → buffer → 显示视频帧
- 餐厅队列: 接待顾客 → 队列/buffer → 服务顾客
const fs = require('fs');const { Transform } = require('stream');const inputFile = 'input.txt';const outputFile = 'output.txt';const inputStream = fs.createReadStream(inputFile, 'utf-8');const transformStream = new Transform({ transform(chunk) { // ✅ 从缓冲区转换块 },});const outputStream = fs.createWriteStream(outputFile);// ✅ 开始管道inputStream.pipe(transformStream).pipe(outputStream);
使用 buffers,每个阶段可以以不同的速度独立处理数据。
但是当通过管道移动的数据超过buffer容量时会发生什么?
以前我们必须将当前所有数据的buffer复制到一个更大的buffer中。
这对性能来说很糟糕,尤其是当管道中将有大量数据时。
ES15为我们提供了解决这个问题的方案:可调整大小的数组buffers。
const resizableBuffer = new ArrayBuffer(1024, { maxByteLength: 1024 ** 2,});// ✅ 调整大小到 2048 字节resizableBuffer.resize(1024 * 2);
4.异步升级
Atomics.waitAsync(): ES2024中另一个强大的异步编码特性:
它是当2个代理共享一个buffer时...
代理1"睡眠"并等待代理2完成任务。
当代理2完成时,它使用共享buffer作为通道进行通知。
const sharedBuffer = new SharedArrayBuffer(4096);const bufferLocation = new Int32Array(sharedBuffer);// 初始化缓冲区位置的初始值bufferLocation[37] = 0x1330;async function doStuff() { // ✅ agent 1:在共享缓冲区位置等待直到通知 Atomics.waitAsync(bufferLocation, 37, 0x1330).then( (r) => { } );}function asyncTask() { // ✅ agent 2:在共享缓冲区位置通知 const bufferLocation = new Int32Array(sharedBuffer); Atomics.notify(bufferLocation, 37);}
如果你认为这类似于普通的async/await,你绝对是对的。
但最大的区别是:这2个代理可以存在于完全不同的代码上下文中 - 它们只需要访问相同的buffer。
而且:多个代理可以在不同时间访问或等待共享buffer - 其中任何一个都可以通知"唤醒"所有其他代理。
这就像P2P网络;而async/await更像是客户端-服务器请求-响应模式。
const sharedBuffer = new SharedArrayBuffer(4096);const bufferLocation = new Int32Array(sharedBuffer);bufferLocation[37] = 0x1330;// ✅ 从 postMessage() 接收到的共享缓冲区const code = `var ia = null;onmessage = function (ev) { if (!ia) { postMessage("Aux worker is running"); ia = new Int32Array(ev.data); } postMessage("Aux worker is sleeping for a little bit"); setTimeout(function () { postMessage("Aux worker is waking"); Atomics.notify(ia, 37); }, 1000);};`;async function doStuff() { // ✅ agent 1:存在于 Worker 上下文中 const worker = new Worker( 'data:application/javascript,' + encodeURIComponent(code) ); worker.onmessage = (event) => { // 记录事件 }; worker.postMessage(sharedBuffer); Atomics.waitAsync(bufferLocation, 37, 0x1330).then( (r) => { } );}function asyncTask() { // ✅ agent 2:在共享缓冲区位置通知 const bufferLocation = new Int32Array(sharedBuffer); Atomics.notify(bufferLocation, 37);}
5.正则表达式v标志和集合操作
这是一个全新的特性,使正则表达式更加清晰和直观。
使用表达式模式查找和操作复杂字符串 - 在集合操作的帮助下:
// A 和 B 是字符类,如 [a-z]// 差异:匹配 A 但不匹配 B[A--B]// 交集:同时匹配 A 和 B[A&&B]// 嵌套字符类[A--[0-9]]
匹配不断增加的Unicode字符集,如:
- 表情符号: 😀, ❤️, 👍, 🎉, 等
- 重音字母: é, à, ö, ñ, 等
- 符号和非拉丁字符: ©, ®, €, £, µ, ¥, 等
所以这里我们使用Unicode正则表达式和v标志来匹配所有希腊字母:
const regex = /[\p{Script_Extensinotallow=Greek}&&\p{Letter}]/v;
最后的想法
总的来说,ES15对JavaScript来说是一个重大飞跃,包含了几个对现代开发至关重要的特性。帮助你以更简洁、更富表现力、更清晰的方式编写更干净的代码。