本回答由Deepseek AI 生成,内容仅供参考,请仔细甄别。

JavaScript 传统脚本 vs 模块脚本对比

特性 传统脚本 模块脚本
严格模式 可选(默认非严格) 强制启用严格模式
作用域 全局作用域 模块作用域(局部)
导入/导出 不支持 ES6 模块语法 支持 import/export
执行时机 立即加载并执行 延迟执行(自动 defer)
加载行为 同步加载,阻塞渲染 异步加载,不阻塞渲染
CORS 限制 宽松,可跨域加载 严格,受 CORS 策略限制
文件要求 任意扩展名,不验证 MIME

需要有效 JavaScript MIME 类型。

推荐 .mjs 扩展名

顶层 await 不支持 支持
this 指向 window(全局对象) undefined(严格模式)
变量污染 容易污染全局命名空间 隔离的模块作用域
依赖管理 手动管理,依赖顺序敏感 自动解析,声明式依赖
重复执行 相同脚本会重复执行 相同模块只执行一次
默认延迟 需要显式添加 defer 属性 自动具有 defer 行为
HTML 中的使用 <script src="file.js"> <script type="module" src="file.js">

详细说明表格

方面 传统脚本 模块脚本
代码示例

<script>

  var x = 1;

</script>

<script type="module">

  import {y} from './mod.js';

</script>

变量声明 变量成为全局属性 变量仅在模块内可见
错误处理 宽松,可能静默失败 严格,立即抛出错误
性能优化 需要手动合并和压缩 支持 Tree-shaking,优化更好
开发体验 全局命名冲突常见 清晰的模块边界,易于维护
浏览器支持 所有浏览器完全支持 现代浏览器支持(IE 不支持)
静态分析 难以进行静态分析 可静态分析,利于工具优化
循环依赖 可能导致运行时错误 有明确的循环依赖处理机制

补充:JavaScript ES6 模块不共享全局命名空间详解

ES6 模块不共享全局命名空间 意味着每个模块都有自己独立的作用域,在模块中声明的变量、函数、类等不会自动成为全局对象的属性,其他模块无法直接访问。

核心概念对比表格
方面 传统脚本 ES6 模块
作用域 全局作用域 模块作用域
变量污染 容易污染全局 隔离的,不会污染
变量访问 所有脚本都可访问 只能通过导出导入
this 指向 window (浏览器) undefined (严格模式)
总结表格
特性 传统脚本 ES6 模块
变量作用域 全局 模块局部
全局污染 容易发生 不会发生
依赖管理 隐式,通过script标签顺序 显式,通过import语句
代码复用 通过全局变量 通过导出导入
测试难度 困难(全局状态) 容易(隔离状态)
严格模式 可选 强制启用
Tree-shaking 不支持 支持
循环依赖 可能导致错误 有明确的处理机制
关键理解点:
  • 🚫 不自动共享 - 模块内的声明不会自动成为全局属性

  • 🛡️ 作用域隔离 - 每个模块有自己的词法作用域

  • 🔒 封装性 - 实现细节被隐藏,只暴露接口

  • 📦 明确依赖 - 必须通过import/export显式声明依赖关系

  • 🌐 可访问全局 - 仍然可以主动访问全局对象,但不推荐修改


核心概念对比表格

方面 传统脚本 ES6 模块
作用域 全局作用域 模块作用域
变量污染 容易污染全局 隔离的,不会污染
变量访问 所有脚本都可访问 只能通过导出导入
this 指向 window (浏览器) undefined (严格模式)

执行时机对比表格

详情推荐阅读

<script> 标签属性(defer async)和 type=“module“ 对比

场景 传统脚本 模块脚本
内联脚本 立即执行 延迟到文档解析后执行
外部脚本 同步下载并执行 异步下载,按顺序执行
多个脚本 按顺序阻塞执行 并行下载,按顺序执行
DOM 访问 可能访问不到未解析的 DOM 总是在 DOM 解析完成后执行
async 属性 支持,异步执行 支持,但执行顺序不确定

实际影响表格

开发方面 传统脚本的影响 模块脚本的影响
代码组织 需要命名空间模式 自然的模块化组织
团队协作 容易产生冲突 清晰的接口边界
构建工具 需要复杂配置 与现代构建工具天然契合
代码复用 通过全局变量共享 通过导出/导入共享
测试维护 依赖全局状态,测试复杂 模块隔离,易于测试

补充:Tree-shaking 详解

Tree-shaking 是一种通过静态代码分析来移除 JavaScript 项目中未使用代码的优化技术。它的名字来源于"摇树"的比喻——就像摇动果树让成熟的果实掉落一样,通过"摇动"代码树来去除无用的代码。

核心原理对比

方面 传统打包 Tree-shaking
工作原理 包含所有导入的代码 只包含实际使用的代码
代码分析 动态分析(运行时) 静态分析(编译时)
依赖检测 无法检测未使用的导出 识别未引用的导出
最终体积 较大,包含死代码 较小,移除未使用代码

工作流程表格

步骤 描述 示例
1. 静态分析 在编译时分析 import/export 关系 解析 ES6 模块语法
2. 依赖图构建 创建模块间的依赖关系图 确定哪些导出被实际使用
3. 死代码检测 标记未被引用的导出和函数 标记未使用的变量、函数、类
4. 代码移除 从最终打包文件中删除死代码 从 bundle 中删除未使用的代码

技术前提表格

前提条件 为什么重要 示例
ES6 模块语法 静态结构可分析 import/export
静态分析能力 编译时确定依赖关系 不能有动态导入(部分支持)
副作用标记 识别有副作用的代码 /*#__PURE__*/ 注释
构建工具支持 需要打包器实现 Webpack、Rollup、Vite

支持 Tree-shaking 的工具

工具 支持程度 配置方式
Webpack ✅ 完整支持 mode: 'production' 自动启用
Rollup ✅ 原生支持 默认启用
Vite ✅ 基于 Rollup 生产构建自动启用
Parcel ✅ 支持 默认启用
Browserify ❌ 不支持

代码编写最佳实践

// 推荐写法(Tree-shaking 友好)
// 具名导出 - 易于 Tree-shaking
export const util1 = () => { /* ... */ };
export const util2 = () => { /* ... */ };

// 按需导入
import { util1 } from './utils';

// 避免的写法(不利于 Tree-shaking)
// 默认导出对象 - 整个对象都会被打包
export default {
  util1: () => { /* ... */ },
  util2: () => { /* ... */ }
};

// 全局导入
import * as utils from './utils'; // 全部导入,无法优化

副作用处理表格

情况 描述 Tree-shaking 影响
无副作用函数 纯函数,可安全移除 ✅ 完全可移除
有副作用模块 修改全局状态、polyfill ❌ 必须保留
未知副作用 工具无法确定是否有副作用 ⚠️ 保守保留

副作用标记示例

// 告诉打包器这个函数是纯函数
/*#__PURE__*/
const pureFunction = (a, b) => a + b;

// package.json 中标记副作用文件
{
  "sideEffects": [
    "*.css",
    "polyfill.js"
  ]
}

​​​​​实际收益表格

项目类型 Tree-shaking 前 Tree-shaking 后 体积减少
小型工具库 完整库体积 只包含使用函数 60-80%
UI 组件库 全部组件 只使用组件 50-70%
大型应用 所有依赖 实际使用功能 30-50%

局限性

局限性 核心问题 现实影响 解决方案
CommonJS 模块 动态结构无法静态分析 整个模块被打包 转 ES6 模块或换库
动态导入 运行时确定依赖关系 所有可能模块都保留 尽量用静态导入
原型方法 可能通过字符串调用 类中所有方法都保留 改用函数导出
外部依赖 第三方库打包方式差 引入未使用代码 选择(Tree-shaking)友好的库,按需导入
副作用 工具无法确定代码影响 保守保留潜在副作用 明确标记纯函数

CommonJS 模块为什么无法 Tree-shaking

特性 ES6 模块 CommonJS
导入方式 import { func } from 'module' const { func } = require('module')
分析时机 编译时可确定 运行时才能确定
依赖关系 静态,可分析 动态,不可预测

原型方法问题

// 问题描述
// 类定义
class MyClass {
  method1() { /* 可能被使用 */ }
  method2() { /* 可能未被使用 */ }
}

// 使用方式1: 直接调用(可分析)
const instance = new MyClass();
instance.method1(); // ✅ Tree-shaking 知道这个方法被使用了

// 使用方式2: 通过字符串调用(不可分析)
const methodName = 'method' + someVariable;
instance[methodName](); // ❌ 无法静态分析

// 使用方式3: 传递给高阶函数
['method1', 'method2'].forEach(method => {
  instance[method](); // ❌ 保守处理,所有方法都保留
});

// 具体例子
class ArrayUtils {
  static chunk(array, size) { /* 分割数组 */ }
  static shuffle(array) { /* 随机排序 */ }
  static unique(array) { /* 去重 */ }
}

// 明确的调用 - 可优化
ArrayUtils.chunk([1,2,3], 2); // ✅ 只有 chunk 被保留

// 动态调用 - 无法优化
const method = getMethodFromConfig(); // 'shuffle'
ArrayUtils[method]([1,2,3]); // ❌ 所有方法都被保留

//解决方案
// 方案1: 使用函数代替类方法
export const chunk = (array, size) => { /* ... */ };
export const shuffle = (array) => { /* ... */ };
export const unique = (array) => { /* ... */ };

// 方案2: 明确导入
import { chunk } from './array-utils.js'; // ✅ 只导入需要的

// 而不是
import ArrayUtils from './array-utils.js'; // ❌ 导入整个类

 外部依赖问题

// 问题描述
// 情况1: 库未提供 ES6 模块版本
import _ from 'lodash'; // CommonJS 版本
// 整个 lodash 都被打包,即使用只用了 _.get

// 情况2: 库的打包方式不友好
import { Button } from 'antd';
// 可能整个 antd 都被打包,而不仅仅是 Button

// 情况3: 副作用严重的库
import 'old-jquery-plugin'; // 有全局副作用,必须全部保留

// 不友好的库
// 不友好的导出方式
export default {
  componentA: /* ... */,
  componentB: /* ... */,
  utils: /* ... */
};

// 使用
import { componentA } from 'unfriendly-lib';
// 但整个 default 对象都被打包

// 友好的库
// 友好的导出方式
export componentA from './components/A';
export componentB from './components/B';
export utils from './utils';

// 使用
import { componentA } from 'friendly-lib';
// 只有 componentA 及其依赖被打包 ✅

// 解决方案
// 方案1: 使用按需导入的库
import Button from 'antd/lib/button'; // 直接导入具体文件
import { get } from 'lodash-es';      // 使用 ES6 模块版本

// 方案2: 使用 babel-plugin-import (Antd)
// .babelrc
{
  "plugins": [
    ["import", { "libraryName": "antd", "style": true }]
  ]
}
// 然后可以这样写
import { Button } from 'antd'; // 自动转换为按需导入

// 方案3: 选择 Tree-shaking 友好的库
import { z } from 'zod';              // ✅ 友好的库
import { motion } from 'framer-motion'; // ✅ 友好的库

副作用识别问题

// 问题描述
// 明显的副作用
window.globalConfig = { ... }; // 修改全局对象
Array.prototype.newMethod = function() { ... }; // 修改原型

// 不明显的副作用
let cache = {};
export function expensiveOperation(input) {
  if (cache[input]) return cache[input]; // 有内部状态
  // ...
}

// CSS 导入
import './styles.css'; // 有渲染副作用,必须保留

// 构建工具的保守策略
// 工具无法确定这个函数是否有副作用
export function complexFunction(obj) {
  // 工具看到 for...in 和赋值操作
  for (const key in obj) {
    this[key] = obj[key]; // 可能修改 this,有副作用?
  }
  // 保守策略:保留整个函数
}

// 解决方案
// 方案1: 明确标记纯函数
/*#__PURE__*/
export const pureFunction = (a, b) => a + b;

// 方案2: package.json 中声明副作用
{
  "sideEffects": [
    "**/*.css",
    "**/*.scss",
    "polyfill.js"
  ],
  "sideEffects": false  // 如果整个包都没有副作用
}

// 方案3: 分离副作用代码
// pure-module.js
export const pureUtil = () => { /* 无副作用 */ };

// side-effects.js  
import './styles.css';
window.myLib = { /* 注册到全局 */ };

总结

Tree-shaking 是现代 JavaScript 开发中的重要优化手段,它通过:

  • ✅ 静态分析 ES6 模块依赖

  • ✅ 移除死代码 减少打包体积

  • ✅ 提升性能 减少下载和执行时间

  • ✅ 改善缓存 更小的文件更易缓存

要充分利用 Tree-shaking,应该使用 ES6 模块语法、遵循纯函数原则、并选择支持该功能的构建工具。

补充:Polyfill 详解

Polyfill 是一段代码(通常是 JavaScript),用于在现代浏览器中实现那些不被支持的新特性。它"填充"了浏览器功能的空白,让开发者能够使用新的 API 和语法,同时保持对旧浏览器的兼容性。

腻子脚本(polyfill)是JavaScript代码,用于为不支持新特性的老旧浏览器提供HTML5和CSS3功能,如视频播放和阴影效果。这些脚本让旧版浏览器也能体验到现代网页的精彩。


Polyfill 基本概念表格

方面 描述 示例
核心目的 在旧环境中实现新功能 让 IE 浏览器支持 Promise
工作原理 检测特性是否存在,不存在时添加实现 if (!window.Promise) { /* 实现 Promise */ }
与 Shim 区别 Polyfill 是 Shim 的一种,专门用于 Web API Shim 是更广义的兼容层
使用时机 当目标浏览器不支持某个标准特性时 需要支持旧版浏览器时

常见的 Polyfill 类型

// JavaScript 新语法
// ES6 Promise - 需要为 IE 添加 polyfill
if (typeof Promise === 'undefined') {
  // 添加 Promise 实现
  window.Promise = class Promise {
    constructor(executor) { /* 实现 Promise */ }
    then(onFulfilled) { /* 实现 then */ }
    catch(onRejected) { /* 实现 catch */ }
  };
}

// 使用
const promise = new Promise((resolve) => {
  setTimeout(() => resolve('done'), 1000);
});

// 为什么 Polyfill 有副作用
// 典型的 Polyfill 结构
if (!Array.prototype.includes) {
  Array.prototype.includes = function(searchElement) {
    // 这个操作修改了全局的 Array 原型!
    // 这就是副作用
    for (let i = 0; i < this.length; i++) {
      if (this[i] === searchElement) {
        return true;
      }
    }
    return false;
  };
}

// 即使你的代码没有使用 includes
// 这个 polyfill 仍然会修改全局环境
// 因此构建工具必须保留它

Tree-shaking 的影响

代码类型 Tree-shaking 效果 原因
工具函数 ✅ 可移除未使用的 没有副作用
Polyfill ❌ 无法移除 修改全局状态
// utils.js - 无副作用,可 Tree-shaking
export const helper1 = () => { /* ... */ };
export const helper2 = () => { /* ... */ };

// polyfill.js - 有副作用,无法 Tree-shaking
if (!window.Promise) {
  window.Promise = class { /* ... */ }; // 修改全局对象!
}

现代 Polyfill 解决方案

// 1. 按需 Polyfill
// 传统方式:引入所有 polyfill
import '@babel/polyfill'; // ❌ 包含所有 polyfill,体积大

// 现代方式:按需引入
import 'core-js/features/promise'; // ✅ 只引入 Promise
import 'core-js/features/array/flat'; // ✅ 只引入 flat

// 2. babel-preset-env + useBuiltIns
// .babelrc
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage", // ✅ 按使用情况自动添加 polyfill
      "corejs": 3
    }]
  ]
}

// 源代码
const promise = new Promise(); // babel 自动添加 Promise polyfill
const flatArray = [].flat();   // babel 自动添加 flat polyfill

// 3. polyfill.io 服务
<!-- 根据浏览器 User-Agent 动态返回需要的 polyfill -->
<script src="https://polyfill.io/v3/polyfill.min.js"></script>
<!-- 
  现代浏览器:返回空文件或很少的 polyfill
  旧浏览器:返回完整的 polyfill 集合
-->

Polyfill 最佳实践

// 推荐做法
// 1. 检测后再添加
if (typeof Object.entries === 'undefined') {
  // 添加 polyfill
}

// 2. 使用现代构建工具配置
// webpack.config.js
module.exports = {
  entry: [
    'core-js/stable', // 按需加载的 core-js
    'regenerator-runtime/runtime', // async/await 支持
    './src/index.js'
  ]
};

// 3. 在 package.json 中声明
{
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

// 避免的做法
// ❌ 无条件添加
Array.prototype.myMethod = function() { ... }; // 污染所有数组

// ❌ 引入整个 polyfill 包
import '@babel/polyfill'; // 包含所有,体积巨大

// ❌ 重复添加
// 多个库都添加了相同的 polyfill

Polyfill 与 Tree-shaking 的关系

// 问题根源
// polyfill.js
// 即使没有显式导出,也有隐式副作用
if (!window.fetch) {
  window.fetch = function() { ... }; // 副作用!
}

// main.js
import './polyfill'; // 这个导入看起来"未使用"
// 但实际上有重要的副作用,必须保留

// 解决方案
// package.json - 告诉构建工具这些文件有副作用
{
  "sideEffects": [
    "**/*.polyfill.js",
    "**/*.css",
    "src/polyfills/**/*"
  ]
}

总结

Polyfill 的核心特点:

  • 🔧 功能填补:在旧浏览器实现新功能

  • 🌍 全局影响:通过修改内置原型或全局对象实现

  • 🚫 副作用明显:因此无法被 Tree-shaking 移除

  • 📦 体积考虑:需要谨慎选择,避免过度使用

现代开发建议:

  • 使用 core-js 和 regenerator-runtime 作为基础

  • 配置 browserslist 明确目标浏览器范围

  • 利用构建工具的按需 polyfill 功能

  • 对必要的 polyfill 文件标记副作用

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐