【JS】快速了解ES特性之三(ES10)

2,076 阅读9分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

  接着上一篇快速了解ES特性之二(ES9) ,今天我们主要来讲ES新特性之 ES10 。前面我们已经讲了 ES 的由来、版本对应规则等等内容,这里我就不再赘述,我们直接开始

快速了解ES特性 是我的系列文章,现在已有

  1. 快速了解ES特性之一(ES7-ES8)
  2. 快速了解ES特性之二(ES9)
  3. 快速了解ES特性之三(ES10)

ES2019 / ES10

Optional catch binding 可选的异常捕获

  在 ES10 之前我们在 try catch 的时候,必须在 catch 的时候指定一个抛出的错误。现在则不需要了。

下面简单举一下例子

// ES10 之前
try {
  throw `error`;
} catch (e) { // 必须申明捕获的异常
  console.log(e);
}

// 现在
try {
  throw `error`;
} catch { // 这里捕获的错误现在是可选的了,可以不写
}

JSON superset

  在 ES10 之前,ECMAScript 并不支持所有的 JSON格式 。正常的 JSON字符 可以包含未转义的 U+2028 LINE SEPARATORU+2029 PARAGRAPH SEPARATOR 字符,但 ECMAScript 却是不支持的。现在从 ES10 开始就可以愉快的支持啦。🤣

const code = '"\u2028\u2029"';
// 以前下面两种写法会炸,报'SyntaxError',现在不会了
JSON.parse(code);
eval(code);

Symbol.prototype.description

  在 ES10 之前,我们知道 Symbol 的描述只是通过构造传入,并不能直接获取到。如果我们要获取,只能通过 toString 转换


interface SymbolConstructor {
    /**
     * Returns a new unique Symbol value.
     * @param  description Description of the new Symbol object.
     */
    (description?: string | number): symbol;
}
const hi = Symbol("hi");
console.log(hi.toString());
// Symbol(hi)
console.log(hi.toString() === "Symbol(hi)");
// true

现在我们可以直接获取到这个描述

interface Symbol {
    /**
     * Expose the [[Description]] internal slot of a symbol directly.
     */
    readonly description: string | undefined;
}
const hi = Symbol("hi");
console.log(hi.description);
// hi
console.log(hi.description === "hi");
// true

详情同学们可以在这儿看:Symbol description accessor

Function.prototype.toString revision

  我们直接看示例:

function hi() {
  // hello world
  console.log(`hello world`);
}
console.log(hi.toString());
// function hi() {
//   // hello world
//   console.log(`hello world`);
// }
console.log(hi.toString.toString());
// function toString() { [native code] }

我们从上面的示例可以看出,方法的 toString() 会打印方法的源码,源码咋写的,就返回啥。这里也没啥好讲的,想要了解更多的同学可以在这儿查看详情:Function.prototype.toString revision

Object.fromEntries

  我们在 ES8 中新增了 Object.entries 方法,可以将对象转成 entry 数组。在 ES10 则增加了一个从 entry 数组生成对象的方法。

interface ObjectConstructor {
    /**
     * Returns an object created by key-value entries for properties and methods
     * @param entries An iterable object that contains key-value entries for properties and methods.
     */
    fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { [k: string]: T };

    /**
     * Returns an object created by key-value entries for properties and methods
     * @param entries An iterable object that contains key-value entries for properties and methods.
     */
    fromEntries(entries: Iterable<readonly any[]>): any;
}

我们通过看上面的方法定义可以知道,Object.fromEntries 接受一个可迭代的对象,然后生成一个新的对象。下面我给大家举例说明一下

const obj = { a: 1, b: 2, c: 3 };
const entries = Object.entries(obj);
// [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]
const newObj = Object.fromEntries(entries);
// { a: 1, b: 2, c: 3 }

我们也可以从一个简单的 字符串为keyMap 中生成

const map = new Map().set(`a`, 1).set(`b`, 2).set(`c`, 3);
const obj = Object.fromEntries(map);
console.log(obj);
// { a: 1, b: 2, c: 3 }

如果不是字符串的 key,将会自动转换成字符串的

const map2 = new Map().set(1, 1).set([1, 2, 3], 2).set(Symbol(`hi`), 3);
const obj2 = Object.fromEntries(map2);
console.log(obj2);
// { '1': 1, '1,2,3': 2, [Symbol(hi)]: 3 }

Map 一样的可迭代对象也可以进行转换,比如:URLSearchParams

const obj4 = Object.fromEntries(new URLSearchParams(`x=1&y=2`));
console.log(obj4);
// { x: '1', y: '2' }

我们也可以自己简单模拟一个可迭代对象

const mapLike = {
  kv: {},
  get(key) {
    return this.kv[key];
  },
  set(key, value) {
    this.kv[key] = value;
    return this;
  },
  [Symbol.iterator]() {
    let index = 0;
    const entries = Object.entries(this.kv);
    return {
      next() {
        const entry = entries[index++];
        return {
          value: entry,
          done: !entry,
        };
      },
    };
  },
};
mapLike.set(`a`, 1).set(`b`, 2).set(`c`, 3);
const obj5 = Object.fromEntries(mapLike);
console.log(obj5);
// { a: 1, b: 2, c: 3 }

我们也可以对对象数组进行降维操作,转换成单纯的 key-value 对象

const arr = [
  { name: `张三`, age: 26 },
  { name: `林大俊`, age: 18 },
];
const obj3 = Object.fromEntries(arr.map(({ name, age }) => [name, age]));
console.log(obj3);
// { '张三': 26, '林大俊': 18 }

Object.fromEntriesObject.entries 本就是一对,中间进行转换可以实现花里胡哨的效果。

这个 Object.fromEntries 也讲的差不多了,下面我们继续学习新的特性

Well-formed JSON.stringify

  我们知道,当我们后端返回一个 JSON 数据的时候需要将 JSON 数据编码成 UTF-8 格式。但是当我们使用 JSON.stringify来序列化的时候,不能将一些非成对的 UTF-16 的代码单元(0xD800–0xDFFF)编码成 UTF-8 。我们可以了解到 UTF-16 代理对是由一个高代理(U+D800—U+DBFF 1,024 个代码点)+ 一个低代理(U+DC00—U+DFFF 1,024 个代码点)组成。我们用屁股想一下也知道,只有一半的 UTF-16 肯定没法子正确的编码成一个对的字符。所以在 ES10 修改了一下 JSON.stringify ,让它可以以一种合理的方式处理这种 错误的字符 :在处理这部分字符的时候,用转义字符代替,而不是编码的方式。

// 正确成对的UTF-16编码组成的非BMP字符,能够正确打印
JSON.stringify('𝌆')
// '"𝌆"'
JSON.stringify('\uD834\uDF06')
// '"𝌆"'

// 错误未配对的UTF-16代理代码单元会被转换成转义字符
JSON.stringify('\uDF06\uD834')
// '"\udf06\ud834"'

// 单个UTF-16代理代码单元也会被转成转义字符
JSON.stringify('\uDEAD')
// '"\udead"'

下面的示例展示了 JSON.stringify 是以转义字符的方式处理这部分字符的

console.log(JSON.stringify(`\u{D800}`) === `"\u{D800}"`);
// false
console.log(JSON.stringify(`\u{D800}`) === `"\ud800"`);
// true

String.prototype.{trimStart,trimEnd}

  这个感觉没有什么好讲的,主要是去除字符串两端的空白字符(空格、换行符等)。

interface String {
    /** Removes the trailing white space and line terminator characters from a string. */
    trimEnd(): string;

    /** Removes the leading white space and line terminator characters from a string. */
    trimStart(): string;
}

ES5 就实现了 trimLefttrimRight 方法。但是呢,和 ES8 新增的 padStartpadEnd 名字有点格格不入(ps: startend 的命名方式更适应 RTL ,更加友好),所以给 trimLefttrimRight 取了个别名: trimStarttrimEnd。为了保证兼容性,大多数引擎中会将 String.prototype.trimLeft.name的值trimLeft变为 trimStartString.prototype.trimRight.name的值trimRight变为 trimEnd

下面我们举例说明一下

const str = ` hi, star \n`;

console.log(str.length);
// 11
console.log(str.trimStart().length);
// 10 去除了左边一个空格
console.log(str.trimEnd().length);
// 9 去除了右边一个空格和一个换行符

console.log(String.prototype.trimLeft.name === `trimStart`);
// true
console.log(String.prototype.trimRight.name === `trimEnd`);
// true

Array.prototype.{flat,flatMap}

  ES10 又给数组新增两个操作符 flatflatMap 。下面我将分开讲解一下它们的作用和用法

flat 扁平化数组

  flat 的作用就如其名一般,将一个嵌套的数组 拍平,通过传入一个 depth 参数,可以将指定深度嵌套的数组解开 拍平,最终合并成一个新的数组返回。如果不传递 depth 参数,默认为 depth1,将解开一层的嵌套数组。如果传入 Infinity ,则会 拍平 所有层级的嵌套数组,这里的所有,指的就是任意层级,随便怎么套,都会变成一维数组,这也是我们最常见的用法。

interface Array<T> {
  flat<A, D extends number = 1>(
    this: A,
    depth?: D
  ): FlatArray<A, D>[]
}

下面请看示例

const arr = [1, [2, [3, [4, [5, 6]]]], [7, [8, [9, [10]]]]];

console.log(arr.flat());// 默认解开一层
// [ 1, 2, [ 3, [ 4, [ 5, 6 ] ] ], 7, [ 8, [ 9, [10] ] ] ]
console.log(arr.flat(2));
// [ 1, 2, 3, [ 4, [ 5, 6 ] ], 7, 8, [ 9, [ 10 ] ] ]
console.log(arr.flat(3));
// [ 1, 2, 3, 4, [ 5, 6 ], 7, 8, 9, [ 10 ] ]
console.log(arr.flat(Infinity));
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

flat 方法还有一个特性,在展开的同时会合并空项。这里的空项仅仅指的是没有值的项,而不是 null 或者 undefined

const arr2 = [1, , [2, , 3, [4, , 5]], null, undefined];
console.log(arr2.flat());
// [ 1, 2, 3, [ 4, , 5 ], null, undefined ] // 只会合并到指定层级的空项,更深层级的不会被合并
console.log(arr2.flat(Infinity));
// [ 1, 2, 3, 4, 5, null, undefined ]

说到这个就不得不说一下我看到的一个比较花哨的解法,同样是用来展开所有嵌套数组。但是却是用 Generator 方法

const arr = [1, [2, [3, [4, [5, 6]]]], [7, [8, [9, [10]]]]];

function* flat(arr) {
  for (const it of arr) {
    if (Array.isArray(it)) {
      yield* flat(it);
    } else {
      yield it;
    }
  }
}

// 直接使用会返回一个挂起的 **Generator**
console.log(flat(arr));
// flat{<suspended>}


// 这里我们用展开符消费一下,这样我们就得到了完全展开的数组
const flatArr = [...flat(arr)];
console.log(flatArr);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

flatMap

   flatMap 这个方法就是一个组合怪,它的作用就是将一个高纬度的值,转换成一个低纬度的值。它是 mapdepth1flat 的组合。

interface Array<T> {
  flatMap<U, This = undefined>(
    callback: (this: This, value: T, index: number, array: T[]) => U | ReadonlyArray<U>,
    thisArg?: This
  ): U[]
}

下面我们来看一下示例

const words = [`hello`, `world`];
const r1 = words.map((item) => item.split(``)).flat();
console.log(r1);
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
const r2 = words.flatMap((item) => item.split(``));
console.log(r2);
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

看起来是不是结果都一样?但是第二种方法的性能会高一些,毕竟第一种要跑两次方法,中间循环多次。如果后面遇到需要先 mapflat 一层的情况,我们就可以优先使用 flatMap ,毕竟性能更佳嘛。

flatMap 的使用我们也不能局限到上面的场景,看上面方法的定义,只要是返回一个数组就成。那么我们就可以实现对结果的新增、删除、修改

const words = [`hello`, `world`];
const r3 = words.flatMap((item, index) =>
  (index === 0 ? item.split(``).concat(`,`) : item.split(``)));
console.log(r3);// 给单词的间隔加了一个逗号
// ['h', 'e', 'l', 'l', 'o', ',', 'w', 'o', 'r', 'l', 'd']

const nums = [1, 2, 3];
const r4 = nums.flatMap((item) => [item, item ** item]);
console.log(r4);
// [1, 1, 2, 4, 3, 27]

总结

  时间她过的可真快啊,一眨眼又到了说晚安的时候了。在 ES10 新特性中我只感觉 JSON supersetWell-formed JSON.stringify 可能需要大家多多了解一下,这两个涉及到了 ES 根本的东西。其他的都是很常见的东西,在其他语言中可能N年前就有了。这些拓展都只是增加了体验,有无都无伤大雅。但是有总是香的,白嫖最爽,可以减少大家很多模板代码。所以,请务必多来点?


如果文章对您有帮助的话,欢迎 点赞评论关注收藏分享 ,您的支持是我码字的动力,万分感谢!!!🌈

如果文章内容出现错误的地方,欢迎指正,交流,谢谢😘

参考资料