先记住这个答案
重命名导入写成 import { count as total } from "./counter.js"。ES 模块的导入不是把值复制过来,而是让本地名字直接指向导出模块里的那个变量,as 只是把本地名字从 count 换成了 total,指向没变。所以导出方后来执行 count++,这边读 total 拿到的就是新值。同时这个绑定是只读的,导入方给 total 赋值会抛 TypeError。
as只改本地名字,不改指向,仍是实时绑定- 导入绑定是只读的,给别名赋值抛
TypeError - 赋给本地变量后得到当前值快照
- 默认导出重命名要写
default as
导出方改值,别名跟着变
counter.js 导出一个 let 变量和一个修改它的函数,另一个模块用别名导入。
// counter.js
export let count = 0
export function inc() {
count++
}
// main.js
import { count as total, inc } from './counter.js'
import { count } from './counter.js'
const snapshot = total // 复制当时的值
inc()
console.log(total, count) // 1 1,别名和原名都读到新值
console.log(snapshot) // 0,快照停在复制那一刻
total = 5 // TypeError: Assignment to constant variable.total 和 count 指向同一个变量,读出来永远一样。snapshot 是普通的 const,赋值那一刻把 0 复制了进来,之后 counter.js 怎么变都和它无关。
最后一行报的是运行时 TypeError,错误信息和给 const 赋值一样。导入绑定对导入方来说就像一个只读变量,只有导出模块自己能改它。
为什么需要重命名
最常见的原因是名字冲突。两个库都导出了 format,或者导入的名字和当前文件的局部变量撞了,就用 as 分开:import { format as formatDate } from "date-fns"。
default的重命名写法- 默认导出在模块内部其实就是一个名为
default的导出。default是保留字,不能直接当变量名,所以用具名语法导入它时必须起别名:import { default as Button, size } from "./Button.js"。它和import Button, { size } from "./Button.js"完全等价,指向的是同一个默认导出。 另一个用途是让名字在当前文件里更有语义。比如配置模块导出enabled,在业务文件里改成import { enabled as isDarkMode },读代码的人一眼就知道它是什么。
容易答错的地方
- 以为
as会像解构一样复制值 - 解构
const { count: total } = obj确实是复制,但import { count as total }不是。两者写法相似、语义完全不同:前者之后和obj.count没关系,后者始终指向原变量。 - 以为能通过别名修改导出方的状态
- 别名和原名一样是只读的。导入方想改状态,只能调用导出模块提供的函数,比如上面的
inc()。这正是模块封装的意义:状态由谁定义就由谁修改。 - 把打包后的表现当成规范行为
- 打包器转译成 CommonJS 时,通常会把导入改写成
_counter.count这样的属性访问来模拟实时绑定。如果某些手写的互操作代码把值提前取出来存了一份,就会出现「读不到新值」的现象,这是转译层的问题,不是as的问题。
面试官还会怎么问?
导出时也能重命名吗?
能。导出方可以写 export { count as total },对外只暴露 total 这个名字。再导出时也能改名:export { count as total } from "./counter.js"。无论在哪一端改名,绑定都还是实时的。
导出的是对象,别名导入后改它的属性会怎样?
可以改,而且所有导入方都能看到。只读限制的是「重新赋值这个绑定」,不限制对象内部。config.debug = true 合法,config = {} 抛错。
重命名会影响 tree shaking 吗?
不会。打包器分析的是静态的导入导出关系,as 只是换名字,它仍然能准确知道你用的是哪个导出,没用到的导出照样能被删掉。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。