先记住这个答案
可选链用于函数调用时写成 fn?.()。?. 左边的值是 null 或 undefined 时,调用被跳过,表达式直接返回 undefined,括号里的参数也不会求值;否则就正常调用。用在方法上写成 obj.fn?.(),调用时 this 仍然是 obj,和 obj.fn() 完全一样,不会因为加了 ?. 就丢失。要注意 ?. 的位置:obj?.fn() 只在 obj 缺失时短路,obj 存在但没有 fn 时还是会抛 TypeError。
obj.fn?.()调用时this仍是objobj?.fn()只保护obj,不保护fn- 短路时括号里的参数不会求值
- 只防
null和undefined,不防非函数值
跑一遍看 this 和短路
一个日志对象,分别调用存在的方法、不存在的方法,再把方法取出来单独调用。
'use strict'
const logger = {
prefix: 'app',
write(msg) {
return `${this.prefix}:${msg}`
}
}
console.log(logger.write?.('ready')) // this 仍是 logger
console.log(logger.flush?.(console.log('参数被求值了'))) // 短路,参数不求值
const write = logger.write
try {
write?.('ready') // 取出来再调,this 是 undefined
} catch (err) {
console.log(err.name)
}
let config = null
console.log(config?.load()) // config 是 null,直接短路查看输出与解释
app:ready
undefined
TypeError
undefined第一行正常输出 app:ready,说明 this 就是 logger。第二行 flush 不存在,调用被跳过,参数里的 console.log 也没有执行,控制台里看不到「参数被求值了」。
第三段是面试官想考的点:write?.() 只能保证 write 存在,它是从对象上取下来的,前面没有 logger. 了,this 是 undefined,读 this.prefix 抛 TypeError。所以 ?. 不会帮你保住 this,也不会让丢掉的 this 回来,this 保不保得住只看调用时前面有没有对象。
为什么 ?. 不影响 this
obj.fn() 的 this 是 obj,是因为调用时引擎知道这个函数是从 obj 上取出来的。obj.fn?.() 在语法上还是一个「从 obj 上取 fn 再调用」的表达式,?. 只是在调用前多加了一步检查:fn 是不是 null 或 undefined。检查通过就按原来的方式调用,obj 这个接收者一直都在。
- 常见用法:可选的回调
- 组件和工具函数经常接收可选的回调,比如
props.onChange?.(value)、options.onSuccess?.(data)。以前要写if (typeof onChange === 'function') onChange(value)或者onChange && onChange(value),现在一个?.就够了。因为是通过props.调用的,回调里如果用了this,拿到的是props对象,这一点和不加?.时一样。
容易答错的地方
- 以为
?.()能防止所有调用错误 - 它只在左边是
null或undefined时短路。属性存在但不是函数,比如obj.fn是字符串,obj.fn?.()照样抛TypeError: obj.fn is not a function。方法内部抛出的错误也会正常往外传,?.不会吞掉。 - 把
?.放错位置 - 接口数据里
user可能为空,写成user.getName?.()保护不了,user是null时读getName就报错了。要保护user就写user?.getName(),两段都不确定就写user?.getName?.()。
面试官还会怎么问?
a?.b.c() 在 a 是 null 时会报错吗?
不会。短路会跳过整条链后面的所有部分,.c() 也不会执行,整个表达式返回 undefined。不需要写成 a?.b?.c?.(),只在真正可能缺失的那一段加 ?. 就行。
new 能和可选链一起用吗?
不能,new Foo?.() 是语法错误。可选链也不能出现在赋值号左边,obj?.a = 1 同样是语法错误。
eval?.() 和 eval() 有什么区别?
eval(code) 是直接 eval,代码在当前作用域执行,能读写当前的局部变量。eval?.(code) 不算直接调用,变成了间接 eval,代码在全局作用域执行,读不到局部变量。这是个冷门知识点,实际项目里应该避免用 eval。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。