2024.06.30前端周刊

162 阅读6分钟

本专栏(FE weekly news)文章随缘更新,由于精力有限,每篇周报的内容可能不多,欢迎大家关注

1. TC39会议提案

TC39会议在6月11号召开,会议上讨论了很多提案,介绍几个比较重要的

Deferred Import Evaluation - Stage 2.7

背景介绍

通常来说,模块的加载会在其他逻辑代码执行前完成

import operation from 'operation'

const doSomething = function (target) {
  return operation(target);
}

但是大量的模块加载可能会导致页面初始化阶段的性能问题,为了解决该问题,我们可以通过异步加载来导入部分模块。

// CommonJS
exports.doSomething = function (target) {
  const operation = require('operation');
  return operation(target);
}
// ES modules
export async function doSomething (target) {
  const { operation } = await import('operations');
  return operation(target);
}

现有的异步加载方案有两个缺点:

  1. 对代码的侵入性较大,如上面所见,对项目进行优化时工作量较大。
  2. 在使用前才加载对应模块,很可能会产生卡顿,降低用户体验。

提案介绍

为了解决上述问题,TC39提出了一种新的模块异步导入方法。

使用提案中的方法,上面的异步导入代码可以改写为:

// defer关键字声明异步导入
import defer * as operations from "operations"

exports.doSomething = function (target) {
  return operations.operation(target);
}

此处的operations脚本会在最初被加载,但不会执行(比如在operations的最外层打个debugger断点,该断点此时也不会被执行)。只有执行operations.operation(target)时,才会触发operations内脚本的执行。

Error.isError() - Stage2

提案介绍

通过Error.isError()来判定当前值是否异常(比如说除数为0等)。在我看来就类似于try catch吧,不过他的错误定位范围更小一些,可读性也更好。

const func = (a: number) => {
    isError(6/a) {
        console.log('an error occurred')
    } else {
        console.log('result is ' + 6/a)
    }
}

RegExp Escaping - Stage 2.7

背景介绍

假设我们现在有一个这样的需求: 写一个函数,需要把一个长文本中的指定字符串剔除。

我们通过以下代码实现

const replaceFunc = (longStr:string, regxStr: string) => {
    return longStr.replace(new RegExp(regxStr, 'g'), '')
}

replaceFunc('somestrhellosomestr', 'hello.') // somestromestr

上面的代码在某些情况下,会得到预期外的结果。就如同示例一般,因为hello.中的.是正则表达式中的特殊字符,代表“换行符 \n 之外的任何单字符”,因此匹配到的不是hello.字符串,而是hello + \n以外任意单字符

提案介绍

新提案提出了一种转义api, 可以通过RegExp.escape,将指定字符串转义,这样便可以避免regxStr中特殊字符导致的匹配结果非预期的问题

const replaceFunc = (longStr:string, regxStr: string) => {
    const escapeStr = RegExp.escape(regxStr) // 即'hello\\.'
    return longStr.replace(new RegExp(escapeStr, 'g'), '')
}
replaceFunc('somestrhellosomestr', 'hello.') // somestrhellosomestr

Promise.try - Stage3

背景介绍

对于异常的捕获,同步和异步有着不同的方案。 比如说下面这段代码

const fixedPromise = v => new Promise((resolve, reject) =>{
    setTimeout(() =>{
        resolve(v.toFixed())
    }, 3000)
})

const func1 = num => {
    if (typeof num !== "number") {
        throw new Error("id must be a number");
    }
    return fixedPromise(num);
}

func1('a string').catch(err => {
    // 未触发此处
    console.log('error occurred')
})

Promise.catch是无法捕获func1中抛出的异常的。想要捕获这个异常,还要用try catch包裹

try {
    func1('a string').catch(err => {
        console.log('I am promise.catch', err)
    })
}.catch(e){
    console.log('I am try catch', e)
}
// 输出 I am try catch Error: id must be a number

提案介绍

新提案提出了Promise.try方法,来实现对同步异步代码异常处理方案的统一, 我们使用Promise.try包裹func1函数

const func1 = num => Promise.try(() =>{
     if (typeof num !== "number") {
        throw new Error("id must be a number");
    }
    return fixedPromise(num);
})

func1('a string').catch(err => {
    // 异常在此处被捕获
    // I am proimse.catch Error: id must be a number
    console.log('I am promise.catch', err)
})

包裹后函数内发生的异常,无论触发异常的代码是同步还是异步,都可以在Promise.catch中被捕获。

2. State of JavaScript 2023 出炉

23年的state of js调查出炉了(是的,在24年年中才出炉),挑几个点说一下:

  1. 通过这个调查我发现了之前没关注过的ts生态的一些库,比如Zodyup,这些库的作用是为开发者提供类型校验功能,开发者可以根据校验结果来实现不同代码逻辑。

  2. 开发者对Vite的评价真的高,1.34%的负面评价,前所未见。(作为对比,webpack负面评价为48%),下图为今年state of js的最后总结,可以看到满屏的vite,恭喜尤大。

    image.png

  3. 相比于js,ts的开发者占比进一步增加。

    image.png

  4. 调查还对于一些比较新的featrues使用占比进行了统计,大概挑几个feature说一下:

    • 逻辑运算符:?? - 使用比例80%

      左侧操作数为nullundefined时,返回右侧操作数,否则返回左侧操作数。

      const foo = null ?? 'default string';
      console.log(foo);
      // Expected output: "default string"
      const baz = 0 ?? 42;
      console.log(baz);
      // Expected output: 0
      
    • 顶层await - 74%

      await不仅限于在async函数内部使用,还可以用于模块顶级,当一个模块引用了使用await的子模块时,将会等待子模块执行完再执行下面的代码(但不会阻塞其他子模块的加载)。

      // a.ts
      const colors = fetch("../data/colors.json").then(
         response => response.json()
      );
      
      export default await colors;
      
      import colors from './a'
      
      // 等待fetch返回结果后再执行此处代码
      console.log(colors )
      
    • 动态导入: import() - 62%

      普通的import指令会在最开始就导入模块并执行。import()则可以让你在需要的时候再导入并执行对应模块。该指令会返回一个Promise, 若没有成功执行/导入模块,会通过reject抛出异常

      以下为一些适用import()的场景:

      1. 这个被导入的模块有很大可能用不到,且静态导入该模块会产生副作用或极大增加的你的应用负担。
      2. 要导入的模块可能在最开始并不存在。
      3. 要导入的模块字符串需要动态拼接
      4. 脚本处于非module环境下(比如scripts标签上没有type="module"时)想要导入模块
      // 静态导入
      import * as mod from "/my-module.js";
      
      // 动态导入
      import("/my-module.js").then((mod2) => {
        console.log(mod === mod2); // true
      });
      

      注意: 非必要时不要使用动态导入,该导入方式不利于依赖树的构建,并且可能会影响分析工具对网站的评估结果。

    • 私有属性: # - 35%

      用于在类中声明私有属性,私有属性在以下情况下,将抛出语法错误:

      1. 在类外部引用
      2. 在类内部引用未声明的私有属性
      3. 使用delete移除私有属性
      4. 访问对象中不存在的私有属性
      class ClassWithPrivateField {
        #privateField;
      
        constructor() {;
          delete this.#privateField; // Syntax error
          this.#undeclaredField = 42; // Syntax error
        }
      }
      
      const instance = new ClassWithPrivateField();
      instance.#privateField; // Syntax error
      
      class C {
        #x;
      
        static getX(obj) {
          return obj.#x;
        }
      }
      
      console.log(C.getX(new C())); // undefined
      console.log(C.getX({})); // TypeError: Cannot read private member #x from an object whose class did not declare it
      
    • 逻辑赋值: ||=、&&= - 31%

      逻辑与赋值x &&= y:仅在x为真时为其赋值

      逻辑或赋值||=: 仅在x为假时为其赋值

      const a = { duration: 50, title: '' };
      
      a.duration ||= 10;
      console.log(a.duration);
      // Expected output: 50
      
      a.title ||= 'title is empty.';
      console.log(a.title);
      // Expected output: "title is empty"