应监听 voiceschanged 事件再调用 getVoices(),因语音引擎异步加载导致初始返回空数组;需兼容各浏览器 voice 字段差异,过滤中文语音应综合 lang、name 等字段,并动态重试获取。

getVoices() 返回空数组?先确认语音引擎已就绪
直接调用 speechSynthesis.getVoices() 常常返回空数组,这不是 bug,而是浏览器语音引擎加载异步导致的——声音列表在页面加载时还未初始化完成。Chrome、Edge、Safari 都有此行为,Firefox 稍好但也不保证立即可用。
正确做法是监听 voiceschanged 事件,等它触发后再取值:
const synth = window.speechSynthesis;
synth.onvoiceschanged = () => {
const voices = synth.getVoices();
console.log(voices.length, 'available voices');
};
- 不要在页面脚本顶部或
DOMContentLoaded里直接调getVoices() -
onvoiceschanged可能触发多次(如系统新增语言包),建议用once: true或加防重逻辑 - 首次触发可能延迟 100–500ms,对需要立即展示语音列表的 UI(如下拉框),应加 loading 状态
不同浏览器返回的 voice 对象字段差异大
SpeechSynthesisVoice 对象在各浏览器中字段支持不一致,尤其 lang、name、voiceURI 的格式和稳定性差别明显。例如:
- Chrome 和 Edge:多数 voice 的
lang是"zh-CN"或"en-US",但部分系统语音可能返回"zh"或空字符串 - Safari:
name值常含厂商前缀(如"com.apple.ttsbundle.siri_female_zh-CN_compact"),不适合直接展示 - Firefox:某些 voice 缺失
lang,需靠name启发式判断(如含"zh"就当中文)
安全读取建议:
立即学习“前端免费学习笔记(深入)”;
const safeLang = (voice) => voice.lang || voice.name?.toLowerCase().includes('zh') ? 'zh-CN' : 'en-US';
如何过滤出真正可用的中文语音?别只看 lang 字段
仅靠 voice.lang === 'zh-CN' 过滤会漏掉不少实际能说中文的 voice(比如 lang === 'zh' 或 lang === ''),也会误入标称中文但发音生硬/缺失的系统语音。
- 优先组合判断:
voice.lang?.startsWith('zh') || voice.name?.includes('Chinese') || voice.name?.includes('zh') - 排除测试类 voice:
!voice.name?.includes('Dummy') && !voice.voiceURI?.includes('local') - 实测发现 macOS 上
com.apple.speech.synthesis.voice.ting-ting支持流利普通话,但lang是空字符串——必须依赖name - Windows Edge 中文 voice 的
voiceURI常为"Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)",可据此识别 Neural 类高质量语音
动态添加语音后 getVoices() 不更新?检查是否绕过了事件机制
用户安装新 TTS 引擎(如 Windows 新增中文语音包、macOS 安装第三方语音)后,getVoices() 不自动刷新,除非触发 voiceschanged。但有些场景下事件不触发:
- 页面已卸载或
speechSynthesis被回收(如 SPA 路由切换未保留实例) - 监听器绑定太晚(比如在按钮点击后才绑
onvoiceschanged) - 使用了
setTimeout模拟轮询,但未清空旧定时器造成重复执行
稳妥方案是封装一个可重试的获取函数:
function fetchVoices(timeout = 3000) {
return new Promise(resolve => {
const synth = speechSynthesis;
const voices = synth.getVoices();
if (voices.length > 0) return resolve(voices);
const timer = setTimeout(() => resolve([]), timeout);
synth.onvoiceschanged = () => {
clearTimeout(timer);
resolve(synth.getVoices());
};
});
}
注意:这个 Promise 不会 reject,因为“无语音”本身就是合法状态,不是错误。
设备语音能力高度依赖系统配置和浏览器版本,同一段代码在 macOS Safari 和 Chrome 上拿到的 voice 数量可能差 3 倍;别假设用户一定有某个特定 voice,始终做兜底处理(比如 fallback 到英文 voice 或禁用语音功能)。


















