阅读视图

发现新文章,点击刷新页面。

CommonJS 与 ES6 模块引入的区别详解

随着 JavaScript 的发展,模块化编程已经成为现代前端开发的基础。目前主流的模块系统有两种:CommonJS 和 ES6 模块。本文将详细对比这两种模块系统的语法、特性和使用场景。

一、CommonJS 模块系统

CommonJS 最初是为了让 JavaScript 能在服务端(如 Node.js)运行而设计的模块规范。

1. 基本语法

导出模块

// 方式一:直接导出单个值
// moduleA.js
const name = 'John';
module.exports = name;

// 方式二:导出一个对象
// person.js
const person = { 
  name: 'John', 
  age: 30,
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
};
module.exports = person;

// 方式三:使用 exports 快捷方式
// utils.js
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
// 等价于:
// module.exports = { add, subtract }

引入模块

// main.js
// 引入单个值
const name = require('./moduleA');
console.log(name); // 'John'

// 引入对象
const person = require('./person');
console.log(person.name); // 'John'
console.log(person.age);  // 30
person.greet(); // "Hello, I'm John"

// 引入工具函数
const utils = require('./utils');
console.log(utils.add(5, 3)); // 8

2. 核心特性

动态引入

CommonJS 允许在代码运行时动态加载模块:

// 可以根据条件动态引入
if (process.env.NODE_ENV === 'development') {
  const debugModule = require('./debug');
  debugModule.enable();
}

// 可以在函数内部引入
function loadModule(moduleName) {
  return require(`./modules/${moduleName}`);
}

// 可以在循环中引入
const modules = ['moduleA', 'moduleB', 'moduleC'];
modules.forEach(name => {
  const module = require(`./${name}`);
  module.init();
});

同步加载

CommonJS 的模块加载是同步的:

// 同步加载,代码会等待模块加载完成
const fs = require('fs');        // 核心模块
const express = require('express'); // 第三方模块
const myModule = require('./my-module'); // 本地模块

console.log('模块加载完成,继续执行');

值的拷贝

CommonJS 导出的是值的拷贝:

// counter.js
let count = 0;
module.exports = {
  count,
  increment() {
    count += 1;
  },
  getCount() {
    return count;
  }
};

// main.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 (仍然是0,因为 count 是原始值的拷贝)
console.log(counter.getCount()); // 1 (需要通过方法获取最新值)

二、ES6 模块系统

ES6 模块是 ECMAScript 2015 中引入的官方模块规范,现已被现代浏览器和 Node.js 支持。

1. 基本语法

导出模块

// 方式一:命名导出(逐个导出)
// person.js
export const name = 'John';
export const age = 30;
export function greet() {
  console.log(`Hello, I'm ${this.name}`);
}

// 方式二:批量导出
// utils.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
export { add, subtract };

// 方式三:默认导出
// math.js
export default class Math {
  static pi = 3.14159;
  static square(x) {
    return x * x;
  }
}

// 方式四:混合导出
// shapes.js
export const PI = 3.14159;
export default class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  area() {
    return PI * this.radius ** 2;
  }
}

引入模块

// 引入命名导出
import { name, age, greet } from './person.js';
console.log(name, age);
greet();

// 引入并重命名
import { add as addNumbers, subtract } from './utils.js';

// 引入默认导出
import Math from './math.js';
console.log(Math.square(4));

// 同时引入默认和命名导出
import Circle, { PI } from './shapes.js';

// 引入所有导出(命名空间导入)
import * as utils from './utils.js';
console.log(utils.add(5, 3));
console.log(utils.subtract(5, 3));

// 只加载模块但不引入任何内容
import './styles.css';

2. 核心特性

静态引入

ES6 模块的引入必须位于顶层,不能动态引入(至少在基础语法上):

// ✅ 正确:顶层引入
import { readFile } from 'fs';

// ❌ 错误:不能在条件语句中引入
if (condition) {
  import { readFile } from 'fs'; // 语法错误
}

// ❌ 错误:不能在函数中引入
function loadModule() {
  import { readFile } from 'fs'; // 语法错误
}

异步加载

但在实际使用中,可以通过动态 import() 实现异步加载:

// ✅ 动态引入(返回 Promise)
if (condition) {
  import('./heavy-module.js')
    .then(module => {
      module.doSomething();
    })
    .catch(err => {
      console.error('模块加载失败', err);
    });
}

// 使用 async/await
async function loadAdminModule() {
  try {
    const adminModule = await import('./admin.js');
    adminModule.init();
  } catch (error) {
    console.error('加载失败', error);
  }
}

// 按需加载路由组件(Vue/React 常见用法)
const UserProfile = () => import('./views/UserProfile.vue');

值的引用

ES6 模块导出的是值的引用,导出和导入的变量指向同一块内存:

// counter.js
export let count = 0;
export function increment() {
  count += 1;
}

// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 (直接更新了)

三、核心区别对比

四、实际应用场景

1. CommonJS 适用场景

// Node.js 服务端应用
const express = require('express');
const mongoose = require('mongoose');
const config = require('./config');

// 条件加载不同环境的配置
const env = process.env.NODE_ENV || 'development';
const dbConfig = require(`./config/${env}.js`);

// 动态加载插件
function loadPlugin(pluginName) {
  try {
    return require(`./plugins/${pluginName}`);
  } catch (err) {
    console.error(`插件 ${pluginName} 加载失败`);
    return null;
  }
}

2. ES6 模块适用场景

// 现代前端应用(React/Vue 项目)
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

// 按需加载(代码分割)
const LazyComponent = React.lazy(() => import('./LazyComponent'));

// 明确导入需要的内容,便于 Tree Shaking
import { debounce, throttle } from 'lodash-es';

// 类型导入(TypeScript)
import type { User, Product } from './types';

五、混合使用注意事项

在 Node.js 环境中,可以混合使用两种模块系统,但需要注意:

// ES6 模块中引入 CommonJS 模块
import package from 'commonjs-package'; // 默认导入
import { something } from 'commonjs-package'; // 命名导入(有限支持)

// CommonJS 中引入 ES6 模块(使用动态 import)
async function loadESModule() {
  const esModule = await import('./es-module.mjs');
  console.log(esModule.default);
}

package.json 配置

{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module", // 设置后,.js 文件默认使用 ES6 模块
  
  "exports": {
    ".": {
      "import": "./dist/index.mjs", // ES6 模块入口
      "require": "./dist/index.cjs"  // CommonJS 模块入口
    }
  }
}

总结

  1. CommonJS 适合 Node.js 服务端开发,特别是需要动态加载的场景

  2. ES6 模块 是现代前端开发的标准,支持静态分析和 Tree Shaking

  3. 动态 import() 填补了 ES6 模块的动态加载能力

  4. 实际开发 中,建议新项目优先使用 ES6 模块,可以获得更好的工具支持和性能优化

选择哪种模块系统,应根据项目运行环境、团队习惯和具体需求来决定。

揭秘JavaScript中那些“不冒泡”的DOM事件

在前端开发中,DOM事件流(捕获阶段→目标阶段→冒泡阶段)是核心基础之一。我们熟知的clickkeydownmouseover等事件,都会在触发后从目标元素向上冒泡至父元素、document甚至window,这也是事件委托的核心原理。

但并非所有事件都遵循这一规则——有些事件仅在触发的目标元素上生效,不会向上传播,也就是所谓的**“不冒泡的事件”**。本文将系统梳理这类事件,解析其特性与应用场景,帮你避开开发中的常见坑。

一、为什么有些事件不冒泡?

事件是否冒泡,本质是由其设计初衷决定的:

  • 冒泡事件的核心是“通用交互”(如点击、鼠标移动),允许父元素统一处理子元素的同类事件,便于实现事件委托;

  • 不冒泡事件多与“元素专属状态/资源加载”相关(如焦点、资源加载),这类事件的影响范围仅局限于目标元素本身,向上传播无实际意义。

例如focus(获取焦点)事件,仅对输入框、按钮等可交互元素有意义。若允许冒泡,父元素会无差别接收到所有子元素的焦点变化,反而增加不必要的性能开销和逻辑混乱。

二、常见的不冒泡事件及解析

1. focus / blur:焦点相关事件(核心不冒泡事件)

  • focus:元素获得焦点时触发(如点击输入框、按Tab键切换焦点);

  • blur:元素失去焦点时触发(如点击输入框外区域、切换到其他元素)。

这两个是最典型的不冒泡事件,也是前端开发中最常接触的“非冒泡事件”。

开发提醒:若想监听子元素的焦点变化,不能依赖focus/blur的冒泡,可使用其“冒泡版替代事件”——focusin(对应focus)和focusout(对应blur),这两个事件会正常冒泡,是focus/blur的官方替代方案。

示例代码:

// ❌ 错误:父元素无法捕获子元素的focus事件(不冒泡)
document.querySelector('.parent').addEventListener('focus', () => {
  console.log('父元素捕获focus'); // 不会执行
});

// ✅ 正确:用focusin监听(冒泡)
document.querySelector('.parent').addEventListener('focusin', () => {
  console.log('子元素获取焦点'); // 正常执行
});

2. load / unload:资源加载相关事件

  • load:资源加载完成时触发,常见于imgscriptaudiovideo等元素,仅在目标元素上触发,不会冒泡

  • unload:页面/资源即将被卸载时触发(如关闭标签页、导航到其他页面),仅绑定在window或目标元素上生效,无冒泡行为

注意window.onload是页面所有资源加载完成的事件,虽绑定在window上,但本质也不属于“冒泡事件”——它是全局事件,没有传播对象。

示例代码:

// img加载完成事件(仅在img本身触发)
const img = document.querySelector('img');
img.addEventListener('load', () => {
  console.log('图片加载完成'); // 正常执行
});

// ❌ 父元素无法捕获img的load事件(不冒泡)
document.body.addEventListener('load', () => {
  console.log('捕获图片load'); // 不会执行
});

3. stop:媒体播放相关事件

stop事件仅在audio/video等媒体元素上触发,当媒体播放被主动停止时生效。该事件仅作用于触发的媒体元素,无冒泡行为。例如点击视频的“停止”按钮,仅该视频元素触发stop,其父容器不会接收到该事件。

4. readystatechange:文档状态变化事件

该事件在document.readyState改变时触发(如从"loading""interactive"再到"complete"),仅绑定在documentXMLHttpRequest等对象上生效,不会向父元素传播。常用于监听页面DOM加载完成(作为DOMContentLoaded的补充方案)。

5. scroll:特殊的“条件冒泡”事件

scroll事件是特例:标准规范中scroll不冒泡,但部分浏览器(如Chrome)对其做了“冒泡兼容”——元素的scroll事件不会冒泡,而windowscroll事件是全局事件。

开发建议:若想监听滚动,建议直接绑定到滚动元素本身,而非依赖父元素捕获。

三、如何处理不冒泡事件?

面对不冒泡事件,核心解决思路有三个:

1. 直接绑定到目标元素

针对focus/blur/load等事件,直接在触发的元素上绑定监听函数,是最直接、最可靠的方式。

2. 使用冒泡版替代事件

focusin替代focusfocusout替代blur,利用冒泡特性实现父元素统一监听。

3. 利用事件捕获阶段处理

所有事件(包括不冒泡事件)都会经过**“捕获阶段”**,可在捕获阶段监听不冒泡事件:

// 捕获阶段监听focus事件(即使不冒泡,也能被父元素捕获)
document.querySelector('.parent').addEventListener('focus', () => {
  console.log('捕获阶段捕获focus'); // 正常执行
}, true); // 第三个参数为true,代表在捕获阶段触发

四、总结

不冒泡事件是DOM事件体系的重要组成部分,其设计符合**“事件影响范围最小化”**的原则。核心要点可总结如下:

掌握这些特性,能帮你在事件委托、资源监听、焦点管理等场景中避开陷阱,写出更健壮的前端代码。

❌