We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
path: js-why-not
这里还是首先澄清一下,这一段并不是说不要使用可选链操作符,相反,可选链操作符由于语言支持的原因,比起任何其他方案(比如 lodash.get 或者 undefsafe)都要好(例如,对于类型系统的支持、JS 引擎优化等等)。那么,为什么还是要单独提出避免使用呢?
实际上这里并不针对可选链单种操作符:
// Optional chaining const myVar = myObject.prop_a?.prop_b
以下的写法同样是需要斟酌的(或操作符):
// OR operator const myVar = myObject.prop_a || {}
这些写法本质上是一样的。考虑第一个可选链的例子,实际上等价于
const propA = myObject.prop_a || {} const myVar = propA.prop_b
某些情况下喜欢用空对象模式来形容这种操作。所谓空对象模式就是,当某个数据为空时,将其作为一个空对象处理。
空对象模式有一定的好处。例如,考虑以下例子:
function getFoo(arg) { return arg.foo || {} } function getBar(arg) { return getFoo(arg).bar || {} } function getBaz(arg) { return getBar(arg).baz || {} }
当传入的 arg 参数缺少 foo 时,调用 getBaz 依然可以保证不出错。
arg
foo
getBaz
但是空对象模式仍然是一种相当反模式的用法。考虑上述的 arg 参数的类型,本应为
type Arg = { foo: { bar: { baz: unknown } } }
但是当我们使用空对象时,实际上这个类型数据在后续的调用中丢失了,意味着它变成了
type Arg = { foo?: { bar?: { baz: unknown } } }
上面的例子实际上说明了,使用可选链或者或操作符默认值的一个缺陷是向下污染。考虑一个具体的例子,假设我们在写一个组件:
const FooComponent = props => ( <div>{props.info.name}</div> ) const BarComponent = props => ( <FooComponent info={props.data.user.info} /> )
可能正常情况下这个组件工作得很好,但某个情况下,传给 BarComponent 的 data 没有 user 这个属性。按照上面的模式,代码会变成
BarComponent
data
user
const FooComponent = props => ( <div>{props.info?.name}</div> ) const BarComponent = props => ( <FooComponent info={props.data.user?.info} /> )
原本只是 BarComponent 对数据需要额外处理,现在变成了 FooComponent 也需要修改对应的实现了!这里显然,一个更好的解决方案是
FooComponent
const FooComponent = props => ( <div>{props.info.name}</div> ) const BarComponent = props => ( props.data.user ? <FooComponent info={props.data.user.info} /> : <div /> )
另一个问题是,使用可选链/或操作符可能错误地掩盖了我们遇到的问题。依然考虑上面的例子,当我们使用可选链来解决这个问题的时候,实际上我们解决的不是问题,而是报错。对于很多前端框架而言,代码依然在按照报错的情况运行,只是我们不再看得到 Cannot read property 'info' of undefined 了。通常这意味着我们忽略了真正的问题:
Cannot read property 'info' of undefined
为什么传给 BarComponent 的 data 没有 user 这个属性?
显然对于这个问题,在不同的场景下都应该有更合适的处理方式:
其实可选链操作符很多时候还是有用的,但这种情况通常只发生在【对应的参数原本就接受一个可以为空的数据】的情况。如果大家觉得不好判断的话,只要在用的时候多想想,是否现在或者未来会有其他边缘情况的问题就好了;或者思考一下,当我们使用 TypeScript 去写的时候,是否就会有问题暴露出来?
.then()
回顾一下异步处理的历史:
异步处理的方式随着语言的发展越来越多,我们很多时候是全部都在使用的。但这里想要提的是,我们应当尽量避免使用 Promise 的原型方法,而是使用 async function 来代替。
众所周知,async function 的可读性远大于 Promise。这是因为 Promise 本身就是异步操作的原语,相当于是说,Promise 实际上只提供了底层接口,而 async function 则是开发层面的使用方式。
简单对比一下:
async function checkStatus(time, interval) { return new Promise(resolve => { function poll() { if (time <= 0) { resolve(undefined) return } fetchValue().then(value => { if (value) { resolve(value) return } time -= 1 setTimeout(poll, interval) }) } }) }
和
function wait(interval) { return new Promise(resolve => setTimeout(resolve, interval)) } async function checkStatus(time, interval) { let value while (time > 0) { try { value = await fetchValue() if (value) return value } catch { // continue } await wait(interval) time -= 1 } return value }
显然下面的可读性更好(而且是在上面的代码已经被抽象过的条件下,不然会更加糟糕)
为什么会有可读性的差异呢?除了所谓的可以写成同步的方式之外,一个比较重要的原因在于:我们可以在 async function 里使用大部分的语言结构(for/while/try-catch 等),JS 已经为其做了同步/异步的处理。
继续上面的这一点来展开的话,之所以在 async function 内可以使用多数语言结构,一个重要的原因在于,await 操作本身语义也被统一了。考虑一下:
let value function getValue() { if (value) return value return new Promise(resolve => { return fetchValue() }).then((result) => { value = result return value }) }
上面的例子似乎也挺自然的,如果有一个缓存的值则使用这个值,如果没有则返回一个 Promise。就好像我们写
let value async function getValue() { if (value) return value value = await fetchValue() return value }
好像也没什么区别呀?
但是,正如这篇文章要讨论的,考虑下面的调用情况
function printValue() { getValue().then(value => { console.log(value) }) }
在第一个例子中,这段代码会有一个问题,就是当 value 有缓存值的时候,getValue 的返回值并没有 then 方法。除非:
function printValue() { const result = getValue() if (typeof result.then === 'function') { result.then(value => { console.log(value) }) } else { console.log(result) } }
或者聪明的同学会想到
function printValue() { Promise.resolve(getValue()).then(value => { console.log(value) }) }
然而,如果我们使用第二个例子是,实际上 async 关键字已经将我们的返回值统一成了 Promise,在调用的时候便没有后顾之忧了。
不仅如此,假设我们的调用是
async function printValue() { const value = await getValue() console.log(value) }
那么不论上面哪一种写法,都是可以工作的,因为 await 关键字也已经将返回值统一成了 Promise 来处理。这意味着当我们使用 async 或者 await 的时候,同步值也会作为异步来处理,也就不需要我们关心两者的区别。
另一个问题是,原型方法只能 polyfill,而 async/await 及其涉及的语法可以转译。两者的最大区别在于转译可以保证生产环境使用兼容的代码,而原型 polyfill 只能靠 browserslist 的声明来推断。
在这个问题上,最明显的表现是 Promise#finally(ES2019)。当某些浏览器没有被 browserslist 覆盖到时,使用 .finally 就会报错。
尽管目前在肉眼可见的未来内,Promise 对象不会有新的原型方法了,我们只需要对 .finally做特殊处理就可以;但是未来的事情,谁又能说清呢?:)
The text was updated successfully, but these errors were encountered:
No branches or pull requests
path: js-why-not
为什么避免使用 ?. 操作符?
这里还是首先澄清一下,这一段并不是说不要使用可选链操作符,相反,可选链操作符由于语言支持的原因,比起任何其他方案(比如 lodash.get 或者 undefsafe)都要好(例如,对于类型系统的支持、JS 引擎优化等等)。那么,为什么还是要单独提出避免使用呢?
空对象模式
实际上这里并不针对可选链单种操作符:
以下的写法同样是需要斟酌的(或操作符):
这些写法本质上是一样的。考虑第一个可选链的例子,实际上等价于
某些情况下喜欢用空对象模式来形容这种操作。所谓空对象模式就是,当某个数据为空时,将其作为一个空对象处理。
类型问题
空对象模式有一定的好处。例如,考虑以下例子:
当传入的
arg
参数缺少foo
时,调用getBaz
依然可以保证不出错。但是空对象模式仍然是一种相当反模式的用法。考虑上述的
arg
参数的类型,本应为但是当我们使用空对象时,实际上这个类型数据在后续的调用中丢失了,意味着它变成了
向下污染
上面的例子实际上说明了,使用可选链或者或操作符默认值的一个缺陷是向下污染。考虑一个具体的例子,假设我们在写一个组件:
可能正常情况下这个组件工作得很好,但某个情况下,传给
BarComponent
的data
没有user
这个属性。按照上面的模式,代码会变成原本只是 BarComponent 对数据需要额外处理,现在变成了
FooComponent
也需要修改对应的实现了!这里显然,一个更好的解决方案是展现
另一个问题是,使用可选链/或操作符可能错误地掩盖了我们遇到的问题。依然考虑上面的例子,当我们使用可选链来解决这个问题的时候,实际上我们解决的不是问题,而是报错。对于很多前端框架而言,代码依然在按照报错的情况运行,只是我们不再看得到
Cannot read property 'info' of undefined
了。通常这意味着我们忽略了真正的问题:显然对于这个问题,在不同的场景下都应该有更合适的处理方式:
其实可选链操作符很多时候还是有用的,但这种情况通常只发生在【对应的参数原本就接受一个可以为空的数据】的情况。如果大家觉得不好判断的话,只要在用的时候多想想,是否现在或者未来会有其他边缘情况的问题就好了;或者思考一下,当我们使用 TypeScript 去写的时候,是否就会有问题暴露出来?
为什么避免使用
.then()
等方法?回顾一下异步处理的历史:
异步处理的方式随着语言的发展越来越多,我们很多时候是全部都在使用的。但这里想要提的是,我们应当尽量避免使用 Promise 的原型方法,而是使用 async function 来代替。
可读性
众所周知,async function 的可读性远大于 Promise。这是因为 Promise 本身就是异步操作的原语,相当于是说,Promise 实际上只提供了底层接口,而 async function 则是开发层面的使用方式。
简单对比一下:
和
显然下面的可读性更好(而且是在上面的代码已经被抽象过的条件下,不然会更加糟糕)
为什么会有可读性的差异呢?除了所谓的可以写成同步的方式之外,一个比较重要的原因在于:我们可以在 async function 里使用大部分的语言结构(for/while/try-catch 等),JS 已经为其做了同步/异步的处理。
异步化
继续上面的这一点来展开的话,之所以在 async function 内可以使用多数语言结构,一个重要的原因在于,await 操作本身语义也被统一了。考虑一下:
上面的例子似乎也挺自然的,如果有一个缓存的值则使用这个值,如果没有则返回一个 Promise。就好像我们写
好像也没什么区别呀?
但是,正如这篇文章要讨论的,考虑下面的调用情况
在第一个例子中,这段代码会有一个问题,就是当 value 有缓存值的时候,getValue 的返回值并没有 then 方法。除非:
或者聪明的同学会想到
然而,如果我们使用第二个例子是,实际上 async 关键字已经将我们的返回值统一成了 Promise,在调用的时候便没有后顾之忧了。
不仅如此,假设我们的调用是
那么不论上面哪一种写法,都是可以工作的,因为 await 关键字也已经将返回值统一成了 Promise 来处理。这意味着当我们使用 async 或者 await 的时候,同步值也会作为异步来处理,也就不需要我们关心两者的区别。
兼容性
另一个问题是,原型方法只能 polyfill,而 async/await 及其涉及的语法可以转译。两者的最大区别在于转译可以保证生产环境使用兼容的代码,而原型 polyfill 只能靠 browserslist 的声明来推断。
在这个问题上,最明显的表现是 Promise#finally(ES2019)。当某些浏览器没有被 browserslist 覆盖到时,使用 .finally 就会报错。
尽管目前在肉眼可见的未来内,Promise 对象不会有新的原型方法了,我们只需要对 .finally做特殊处理就可以;但是未来的事情,谁又能说清呢?:)
The text was updated successfully, but these errors were encountered: