关于 ??= 操作符
??= 是 TypeScript 中的一个空值合并赋值操作符(Nullish Coalescing Assignment Operator)。它主要用于在变量已有非空值(即 null 或 undefined)时避免重复赋值。
用法示例
假设有一个变量 x,我们希望如果 x 的值为空(null 或 undefined),则给它赋一个默认值 5:
let x: number | null | undefined = null;
// 使用 ??= 赋值
x ??= 5; // 如果 x 为 null 或 undefined,则赋值为 5
console.log(x); // 输出 5
如果 x 已经有值,则不会进行赋值:
let x: number | null | undefined = 10;
// 使用 ??= 赋值
x ??= 5; // 不会改变 x 的值
console.log(x); // 输出 10
应用场景
- 简化条件判断:减少冗余的条件判断语句。
- 默认值设置:方便地设置默认值,尤其在处理 API 返回的数据时。
??(空值合并运算符)||(逻辑或运算符)?.(可选链运算符)
let value = null;
// ??(空值合并运算符) 这个运算符用于在左侧的值为 null 或 undefined 时,返回右侧的值
let result = value ?? '默认值'; // result = '默认值'
console.log(result,'result');
// ||(逻辑或运算符)用于返回其第一个真值。如果左侧的值为 false、0、""(空字符串)、null、undefined 或 NaN,则返回右侧的值
let num = 0;
let res = value || '默认值'; // result = '默认值'
console.log(res,'res');
// ?.(可选链运算符)用于对可能为 null 或 undefined 的对象进行安全访问。它可以避免因访问不存在的属性而导致的错误。
let obj = null;
let prop = obj?.property; // result = undefined,不会抛出错误
console.log(prop,'prop');
特别的 ?= 操作符(安全赋值运算符) 提案地址:https://github.com/arthurfiorette/proposal-safe-assignment-operator
ECMAScript 提出的安全赋值运算符(?=)旨在简化 JavaScript 中的错误处理,减少对传统 try-catch 代码块的依赖。这一新运算符的引入借鉴了其他编程语言(如 Go、Rust 和 Swift)中的类似结构,通过提供一种更加简洁和高效的方式来处理错误,使得代码可读性和可维护性显著提升[1][2]。
传统的 try-catch 结构常常导致代码深度嵌套,增加了理解和调试的难度。而 ?= 运算符的使用可以让代码流程更加线性,保留原有的逻辑清晰度。这一运算符允许开发者将异步操作的结果与可能的错误通过元组进行解构,从而更优雅地管理错误。例如,当使用 ?= 运算符时,开发者可以轻松地处理网络请求和解析数据的错误,而无需深层嵌套的错误处理逻辑。
运算符的语法结构为 [error, result] ?= await someAsyncFunction();,如果有错误发生,error 将会被赋值,而 result 则会是 null。这种模式确保所有错误在处理时都能被捕获,更加减少潜在的安全隐患。此外,?= 运算符通过标准化错误处理,使得不同 API 之间的处理变得一致,提升了代码的整体可靠性和安全性[4][5]。
总的来说,安全赋值运算符的引入标志着 JavaScript 错误处理的一个重要进步,为开发者提供了一种更清晰、更安全的方式来编写异步代码和处理潜在的错误问题
提案概要
安全赋值运算符 ?= 的目标就是简化错误处理。
它通过将函数的结果转换为一个数组来处理错误。
- 如果函数抛出错误,则运算符返回
[error, null]; - 如果函数成功执行,则返回
[null, result]。
这一运算符与 Promise、async 函数以及任何实现了 Symbol.result 方法的值兼容。
例如,当执行 I/O 操作或与基于 Promise 的 API 交互时,运行时可能会出现意外错误。
如果忽略了这些错误,可能会导致意外的行为和潜在的安全漏洞。使用安全赋值运算符可以有效地处理这些错误:
const [error, response] ?= await fetch("https://www.test.cn/api/list");
提案动机
- 简化错误处理:通过消除
try-catch块,简化错误管理流程; - 增强代码可读性:减少嵌套,提高代码的清晰度,使错误处理的流程更直观;
- 跨API一致性:在不同的 API 中建立统一的错误处理方法,确保行为一致性;
- 提高安全性:减少忽略错误处理的风险,从而增强代码整体安全性。
使用示例
以下是一个典型的不使用 ?= 运算符的错误处理示例:
async function getData() {
const response = await fetch("https://www.test.cn/api/list");
const json = await response.json();
return validationSchema.parse(json);
}
上述函数存在多个可能存在异常的点(例如 fetch()、json()、parse()),我们可以使用 ?= 运算符进行非常简洁、易读的处理:
async function getData() {
const [requestError, response] ?= await fetch("https://www.test.cn/api/list");
if (requestError) {
handleRequestError(requestError);
return;
}
const [parseError, json] ?= await response.json();
if (parseError) {
handleParseError(parseError);
return;
}
const [validationError, data] ?= validationSchema.parse(json);
if (validationError) {
handleValidationError(validationError);
return;
}
return data;
}
提案功能
Symbol.result
任何实现了 Symbol.result 方法的对象都可以与 ?= 运算符一起使用。
Symbol.result 方法必须返回一个数组,其中第一个元素表示错误,第二个元素表示结果。
function test() {
return {
[Symbol.result]() {
return [new Error("错误信息"), null]
},
}
}
const [error, result] ?= test() // Function.prototype also implements Symbol.result
// const [error, result] = test[Symbol.result]()
// error is Error('123')
安全赋值运算符 (?=)
?= 运算符调用运算符右侧对象或函数上的 Symbol.result 方法,确保以结构化方式一致地处理错误和结果。
const obj = {
[Symbol.result]() {
return [new Error("Error"), null]
},
}
const [error, data] ?= obj
// const [error, data] = obj[Symbol.result]()
function action() {
return 'data'
}
const [error, data] ?= action(argument)
// const [error, data] = action[Symbol.result](argument)
结果应符合 [error, null | undefined] 或 [null, data] 的格式。
当在函数中使用 ?= 运算符时,传递给该函数的所有参数都将转发给 Symbol.result 方法。
declare function action(argument: string): string
const [error, data] ?= action(argument1, argument2, ...)
// const [error, data] = action[Symbol.result](argument, argument2, ...)
当 ?= 运算符与对象一起使用时,不会将任何参数传递给 Symbol.result 方法。
declare const obj: { [Symbol.result]: () => any }
const [error, data] ?= obj
// const [error, data] = obj[Symbol.result]()
递归处理机制
在使用 [error, null] 数组时,一旦遇到第一个异常就会生成。然而,如果 [null, data] 数组中的数据也实现了 Symbol.result 方法,那么该方法将会被递归调用。
const obj = {
[Symbol.result]() {
return [
null,
{
[Symbol.result]() {
return [new Error("Error"), null]
},
},
]
},
}
const [error, data] ?= obj
// const [error, data] = obj[Symbol.result]()
// error 是 Error('string')
这种行为有助于处理各种包含 Symbol.result 方法的 Promise 或对象:
async function(): Promise<T>function(): Tfunction(): T | Promise<T>
处理 Promise
Promise 是除了 Function 之外,唯一可以与 ?= 操作符一起使用的实现。
const promise = getPromise()
const [error, data] ?= await promise
// const [error, data] = await promise[Symbol.result]()
你可能已经注意到 await 和 ?= 可以一起使用,而且绝对没问题。由于递归处理特性,它们可以很好地组合在一起。
const [error, data] ?= await getPromise()
// const [error, data] = await getPromise[Symbol.result]()
执行顺序如下:
getPromise[Symbol.result]()调用时可能抛出错误(如果它是一个返回Promise的同步函数)。- 如果抛出错误,错误将被赋值给
error,并且执行将停止。 - 如果没有错误抛出,结果将被赋值给
data。因为data是一个 Promise,并且 Promise 具有Symbol.result方法,所以它将被递归处理。 - 如果 Promise 被拒绝(reject),错误将被赋值给
error,并且执行将停止。 - 如果 Promise 被解决(resolve),结果将被赋值给
data。
通过这种递归处理机制,你可以简化对各种复杂嵌套对象和 Promise 的处理,让代码更加简洁和易读。
Polifll
这个提案还处于初期阶段,要进入标准还需要很长的时间,当下需要使用可以用这个 polifill:
https://github.com/arthurfiorette/proposal-safe-assignment-operator/blob/main/polyfill.js
但是,?= 运算符本身没办法直接进行 polyfill。当针对较旧的 JavaScript 环境时,需要使用编译器将 ?= 运算符转换为相应的 [Symbol.result] 调用。
const [error, data] ?= await asyncAction(arg1, arg2)
// should become
const [error, data] = await asyncAction[Symbol.result](arg1, arg2)
const [error, data] ?= action()
// should become
const [error, data] = action[Symbol.result]()
const [error, data] ?= obj
// should become
const [error, data] = obj[Symbol.result]()
总结
- 新的 JavaScript 提案让你告别 try catch
??用于处理null和undefined的情况。||更广泛用于处理假值(falsy values)。?.用于安全地访问对象属性,避免运行时错误