不能直接appendChild后调用函数,因为浏览器异步加载脚本,此时脚本尚未下载、解析、执行完毕,立即调用会报ReferenceError;必须通过onload/onerror监听完成或失败,并用Promise封装确保可靠执行。

动态创建 <script> 标签加载 JS,核心就一条:用 document.createElement('script') + src + 插入 DOM,但必须处理好加载完成时机和错误兜底。
为什么不能直接 appendChild 后就调用函数?
浏览器对动态插入的 <script> 是异步加载的,head.appendChild(script) 这行执行完,脚本远未下载、解析、执行完毕。此时若立刻调用其中定义的函数(比如 init()),会报 ReferenceError: init is not defined。
- IE 用
onreadystatechange,状态要判断this.readyState === 'loaded' || this.readyState === 'complete',只判'complete'在缓存命中时可能漏掉 - 现代浏览器用
onload即可,但必须在appendChild前绑定,否则事件可能丢失 - 没加
onerror监听,脚本 404 或 CORS 失败时静默失败,极难排查
document.createElement('script') 的最小可靠写法
绕过兼容性细节,直接上能在 IE9+ 和所有现代浏览器稳定运行的写法:
function loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load ${url}`));
document.head.appendChild(script);
});
}
// 使用
loadScript('/js/utils.js')
.then(() => console.log('utils loaded'))
.catch(err => console.error(err));
- 不设
async属性——动态创建的<script>默认就是异步行为,显式设script.async = true反而可能导致执行顺序失控 - 不依赖
type="text/javascript"——HTML5 中可省略,设了反而可能在某些旧环境触发 MIME 类型校验 - 始终用
Promise封装——避免回调地狱,也方便链式加载多个依赖
多个 script 有依赖时怎么保证执行顺序?
如果 lib2.js 依赖 lib1.js 中导出的类或函数,就不能靠 async 并行加载,必须串行等待前一个完成。
立即学习“前端免费学习笔记(深入)”;
- 用
Promise.all是错的——它并行发起请求,不保证执行顺序 - 正确做法是链式
.then():loadScript('lib1.js').then(() => loadScript('lib2.js')) - 若脚本本身是 ES 模块(
type="module"),动态插入后无法通过全局变量访问export,得改用import()动态导入 - 注意:动态插入的模块脚本不支持
defer,且import()返回 Promise,与上述loadScript行为一致
容易被忽略的坑:缓存、CORS 和路径
本地开发时常见问题往往不是逻辑,而是基础设施层面的配置遗漏:
-
script.src路径写错(比如漏掉/开头或写成相对路径js/main.js)→ 404,onerror触发但控制台可能被折叠 - 从非同源域名加载(如
https://cdn.example.com/lib.js)→ 必须确保服务端返回Access-Control-Allow-Origin: *,否则onerror触发但无详细错误信息 - 开发中反复修改脚本却没生效 → 浏览器强缓存了
.js,可在src后加时间戳参数:script.src = '/js/app.js?v=' + Date.now();(仅开发用)
真正麻烦的从来不是“怎么插入”,而是“插完怎么确认它真的跑起来了”。把 onload/onerror 绑定、Promise 包裹、路径校验这三件事做扎实,比堆技巧重要得多。



















