uni-app——# **`??` 和 `||` 的区别,不只是两个字符那么简单** ## 背景 我们的项目是一个基于 uni-app 的微信小程序,使用 GitLab CI/CD 自动构建并发布
?? 和 || 的区别,不只是两个字符那么简单
背景
我们的项目是一个基于 uni-app 的微信小程序,使用 GitLab CI/CD 自动构建并发布体验版。某天测试反馈:表单中明明设置了 required: false 的选填字段,在体验版上仍然显示必填星号,且提交时会触发必填校验。
更诡异的是——本地开发工具运行完全正常,只有 CI 自动构建后的版本才有问题。
排查过程
第一反应:代码没生效?
首先确认了 dev 分支上的代码确实已经把相关字段改成了 required: false:
js
const formFields = computed(() => [
{ label: '用户名', prop: 'username', type: 'input', required: true },
{ label: '昵称', prop: 'nickname', type: 'input', required: false },
{ label: '邮箱', prop: 'email', type: 'input', required: false },
{ label: '手机号', prop: 'phone', type: 'input', required: false },
]);
代码没问题,commit 也在 dev 分支上,排除了"没合进去"的可能。
第二反应:CI 缓存?
清除了 GitLab CI 缓存,手动触发重新构建,问题依旧。排除缓存因素。
深挖:CI 构建和本地构建到底差在哪?
仔细审查 CI 部署脚本 build-deploy.js,发现了一个"优化"步骤——postProcessJS,它会在构建完成后对所有 JS 文件做一轮处理:
js
async function postProcessJS(projectPath) {
// ...遍历所有 JS 文件
for (const filePath of jsFiles) {
let content = fs.readFileSync(filePath, 'utf-8');
// 修复空值合并运算符 - 旧版微信不支持
content = content.replace(/\?\?/g, '||');
// ...其他处理
fs.writeFileSync(filePath, content, 'utf-8');
}
}
就是这行:
js
content = content.replace(/\?\?/g, '||');
它把构建产物中所有的 ??(空值合并运算符)全局替换成了 ||(逻辑或运算符)。
问题根因
?? 和 || 看起来相似,但语义完全不同:
| 表达式 | ??(空值合并) | ||(逻辑或) |
| ------------------------ | ---------------- | --------------- |
| null ?? 'default' | 'default' | 'default' |
| undefined ?? 'default' | 'default' | 'default' |
| false ?? 'default' | false | 'default' |
| 0 ?? 'default' | 0 | 'default' |
| '' ?? 'default' | '' | 'default' |
核心区别:
??只在值为null或undefined时取右侧值||在所有 falsy 值(false、0、''、null、undefined、NaN)时都取右侧值
我们的表单组件中,判断字段是否必填的逻辑是:
js
function isRequired(field) {
return field.hidden === true ? false : field.required ?? defaultRequired;
}
其中 defaultRequired = true(组件默认所有字段必填)。
当我们显式设置 required: false 时:
text
// 源码(正确)
false ?? true → false ✅ 识别为选填
// CI 构建后(错误)
false || true → true ❌ 变成了必填!
false 是一个合法的、有意义的值,?? 会保留它,但 || 会把它当作"空"值而丢弃掉。
影响范围
这个替换不仅影响了 required 字段,还波及了所有使用 ?? 的场景:
js
// 表单验证逻辑
if (!field.required || field.hidden || ...) { ... }
// 占位符生成
field.required ?? defaultRequired ? '' : '(选填)'
// 其他组件中的默认值处理
const value = config.enabled ?? true; // 被替换后,enabled=false 时会变成 true
任何用 ?? 来处理 false、0、'' 等 falsy 默认值的代码,在 CI 构建后行为都会发生变化。
修复方案
删除这行全局替换:
diff
- // 修复空值合并运算符 - 旧版微信不支持
- content = content.replace(/\?\?/g, '||');
现代微信基础库(2.14.0+)已原生支持 ?? 语法。如果确实需要兼容更老的基础库版本,应该使用 Babel 插件 @babel/plugin-proposal-nullish-coalescing-operator 来做正确的语法降级,而不是简单的字符串替换。
正确的 Babel 配置示例
js
// babel.config.js
module.exports = {
plugins: [
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-chaining'
]
};
Babel 会将:
js
a ?? b
正确转换为:
js
a !== null && a !== void 0 ? a : b
教训总结
1. 字符串替换不是语法转换
正则替换是"文本操作",它不理解代码语义。?? 和 || 的区别不在于字符,而在于对 falsy 值的处理。正确的做法是使用 AST 级别的工具(如 Babel)进行转换,它能理解上下文并生成等价代码。
2. CI 和本地环境差异是最难排查的 Bug 类型
当"本地正常、线上不行"时,重点排查 CI 流程中构建之外的额外步骤:后处理脚本、代码压缩、文件替换等。建议在 CI 流程中增加产物对比步骤,自动检测异常变更。
3. "兼容性修复"可能引入更大的问题
这行代码的初衷是解决旧版微信不支持 ?? 的兼容性问题,但简单粗暴的替换引入了逻辑错误。兼容性处理应该交给专业的编译工具链,而不是自己动手做字符串替换。
4. 关注 falsy 值的语义
在 JavaScript 中,false、0、'' 都是合法的业务值。选择 ?? 还是 || 不是风格偏好,而是语义选择。代码审查时遇到 ??,要意识到作者是刻意区分"空值"和"falsy 值"的。
5. 建立 CI 脚本的代码审查机制
这类"后处理脚本"往往被认为是"临时方案"或"小优化",容易逃过严格的代码审查。建议:
- 所有 CI 脚本变更需要 Senior 审批
- 禁止在 CI 中使用正则替换修改代码逻辑
- 建立 CI 脚本的单元测试
附:快速自查
如果你的项目中也有类似的后处理脚本,可以快速检查:
bash
# 搜索是否存在 ?? 到 || 的替换
grep -rn '\?\?' ci-scripts/ | grep replace
# 搜索是否存在其他危险的正则替换
grep -rn '\.replace(' ci-scripts/ | grep -E '(\?\?|\&\&|\|\|)'
如果有,尽快移除并改用 Babel 等工具来处理语法兼容性。
更多推荐



所有评论(0)