一口气看完,ES8,9,10,13,14,15中 30多个最具变革性的JavaScript特性

ES8包含了许多有价值的特性,彻底改变了我们编写JavaScript的方式。

代码变得更简洁、更易编写,并升级了新功能。

我们来看看这些特性,看看你是否错过了哪些。

1.尾随逗号

在ES8之前,尾随逗号会导致语法错误!

❌ 之前:

const colors = [
    'red',
    'blue',
    'green',
    'yellow', // ❌ 不允许
];

const person = {
    name: 'Tari Ibaba',
    site: 'codingbeautydev.com' // ❌ 不行
};

但这引发了一些问题,重新排列列表会带来麻烦:

842a2c47eb334c09344cb0bfc6d4d6ca.gif

我们还必须总是在最后一项添加逗号才能添加新项 — 这会使git差异变得混乱:

2822436d0a48c7d4e2b91448b1cefd89.png

所以ES8修复了所有这些:

✅ 现在:

const colors = [
    'red',
    'blue',
    'green',
    'yellow', // ✅ yes
];

const person = {
    name: 'Tari Ibaba',
    site: 'codingbeautydev.com', // ✅ yes
};

它们带来的好处也使得像Prettier这样的工具在格式化后默认添加它们:

e1a0b048a1a67934221fdb199feadddf.gif

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);

00dea99815196f4e9e633cc03978cc2c.png

Object.entries()

const person = {
    name: 'Tari Ibaba',
    site: 'codingbeautydev.com',
    color: '🔵blue',
};

const arr = Object.entries(person);

/*
[
  ['name', 'Tari Ibaba'],
  ['site', 'codingbeautydev.com'],
  ['color', '🔵blue']
]
*/

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);

feac23332196abe7296054a7641f0b9b.png

4.原生字符串填充

2016年3月22日,流行的NPM包left-pad被创建者作为一种抗议形式下架,导致数千个软件项目崩溃。

这让许多人担心我们可能过度依赖外部模块 — 即使是像字符串填充这样简单的功能。

但幸运的是,ES8为JavaScript带来了原生的字符串填充功能,即padStartpadEnd字符串方法:

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)
);

d0f66b8c881ef3d83f2b95732c56e314.png

最后的思考

总的来说,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: '🔵' });

/* Output:
{
  name: 'Coding Beauty',
  site: 'codingbeautydev.com',
  theme: '🔵'
}
*/

覆盖属性:

const langs = {
  j: 'java',
  c: 'c++',
};

console.log({ ...langs, j: 'javascript' });

// Output: { j: 'javascript', c: 'c++' }

这使得它特别适合在默认值的基础上构建,尤其是在制作公共实用程序时。

或者像我用Material UI定制默认主题那样:

9d36b53e697f3ea5a37ac83b4e067618.png

使用展开语法,你甚至可以去掉不想在副本中出现的对象属性。

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"意味着未处理的。

5c3fe176d9ab3933a4eeb1a7f923f746.png

这就是为什么我们有String.raw()但没有String.cooked()

4. 复杂的正则表达式特性

说到正则表达式,ES9并没有让人失望。

它完全装载了最先进的正则表达式特性,用于高级字符串搜索和替换。

向后查找断言

这是一个新特性,用于确保只有某个特定模式出现在你要搜索的内容之前:

  • 正向后查找:白名单 ?<=pattern

  • 负向后查找:黑名单 ?<!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 = /(?<!\$)\d+/g;

console.log(str.match(regexNeg)); // ['20', '50']

aeac57024ab3867de38cfcd3be73bd6e.png

命名捕获组

捕获组一直是正则表达式中最宝贵的特性之一,用于以复杂的方式转换字符串。

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通过?<name>来命名捕获组解决了这个问题:

const str = 'The cat sat on a map';

// left & right
console.log(str.replace(/(?<left>[a-z])(a)(?<right>t)/g, '$<left>*$<right>'));

// -> The c*t s*t on a map

ee36bc58549d404386298f2df9330bd3.png

你知道当VS Code中出现错误时,你可以快速Alt + 点击跳转到错误发生的确切位置吗?👇

848e1ff051e9bcdb4f2842cfeda084af.png

VS Code使用捕获组使文件名可点击,从而实现这种快速导航。

我想它大概是这样的:

// The stupidly long regex
const regex = /(?<path>[a-z]:[a-z].(?:?:\\/|(?:\\/?)))[\w \-]+):(?<line>\d+):(?<char>\d+)/gi;

// ✅ String.raw!
const filePoint = String.raw`C:\coding-beauty\coding-beauty-javascript\index.js:3:5`;

const extractor = /(?<path>[a-z]:[a-z].(?:?:\\/|(?:\\/?)))[\w \-]+):(?<line>\d+):(?<char>\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 <button> tag
// <button onclick="navigateWithButtonFilepointInnerText">filePoint</button>

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模块:

89dae23d79c37184c6428bf0ba971a0f.png

2. 扁平化曲线

flat()flatMap()提供了更清晰的方式来轻松扁平化多维数组。

消除了痛苦的数组循环扁平化代码的需求:

a01e4648717652542406e3b6c1bd908e.png

flatMap()相当于调用map(),然后flat(1)

86e3643d17157eed08cf8cabf512abb4.png

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块的错误参数:

b80b9d8b35edadc9b02629533dddb9b3.png

6. 无惊喜排序

稳定的数组排序。

以前,在对数组进行排序时,我们绝对无法保证相等元素的排列。

但在ES10之后的JS代码中,我们100%确定react总是在vue之前,vue总是在angular之前。

1b7846731a76fd6aefb914edb7fa6e4a.png

195d7f026af679f9b4aef82fdfec1366.png

7. 要么做大,要么回家:BigInt

BigInt的名称揭示了它的目的:用于加载难以置信的巨大整数值:

30a8f0e39646e43c62a0e13b51a965c1.png

e8829eb648a18371bb6238fc7909636f.png

因为普通整数做不到:

dabac4fec1ca3dbed9cd3578f93e49af.png

最后的思考

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. 从末尾开始查找数组

从第一项开始搜索并不总是理想的:

3416ef665029d9c8ff13f30fa4a4d0d7.png

你可以很容易地看到,对我们的巨大列表从末尾而不是开始搜索会快得多。

ff100e16a02dac6aac78d7111f1da1a3.png

有时你必须从末尾搜索才能让你的程序工作。

比如我们想在一个数字列表中找到最后一个偶数,findfindIndex会非常不准确。 调用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);

af5a4f3692ea709260448f392e84b8d7.png

字面意思就是让恐龙级的 Lodash 库失去了最后的存在理由 - 再也不需要了!

0c21e4b978efe5e79734a7f2aa6230c0.png

我原本期待一个新的实例方法,比如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安装 - 又多了一个依赖!

fc8cba046a9159cea8843506b248069a.png

但现在有了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_Extensions=Greek}&&\p{Letter}]/v;

最后的想法

总的来说,ES15对JavaScript来说是一个重大飞跃,包含了几个对现代开发至关重要的特性。帮助你以更简洁、更富表现力、更清晰的方式编写更干净的代码。

最后:

CSS技巧与案例详解

vue2与vue3技巧合集

VueUse源码解读

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@大迁世界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值