Typescript export 导出分析-是引用还是值的形式?
在 TypeScript 里,export
导出的内容是否为引用,要依据具体导出的内容类型来判断。
1. 导出原始类型(如数字、字符串、布尔值)
原始类型以值的形式导出,并非引用。在导出原始类型时,导出的是值的副本,而非引用。
// 导出原始类型
export const num = 10;
// 导入原始类型
import { num } from './module';
// 这里的 num 是原始值 10 的副本
2. 导出const 对象和数组
如果用 export const
导出,导入的绑定是只读的(不能直接 myObj = {}
),虽然不能重新赋值,但可以修改对象属性。
当你导出一个对象或者数组时,实际上导出的是对该对象或数组的引用。这意味着导入模块修改这个对象或数组时,会影响到导出模块中的原始对象或数组。
// 导出对象
export const person = { name: 'Alice', age: 25 };
// 导入对象
import { person } from './module';
person.age = 26;
// 这里修改的是同一个对象,导出模块中的 person 对象的 age 也会变为 26
TypeScript/ES 模块的这种设计使得状态共享和模块间通信更加灵活高效。
3. 导出 let
对象
在 TypeScript 中,使用 export let
导出对象时,导出的是引用(reference),而不是副本。这与 JavaScript 的 ES 模块规范一致。
导入方获取的是对原始对象的引用,任何修改都会反映在所有导入该对象的地方。由于是 let
声明,原始模块可以重新赋值,而导入方会看到新值。如果导出的是对象,修改其属性会在所有导入方可见
// module.ts
export let myObj = { value: 1 };
export function updateObj() {
myObj.value = 2; // 修改对象属性
}
export function replaceObj() {
myObj = { value: 3 }; // 重新赋值(因为是 let)
}
// main.ts
import { myObj, updateObj, replaceObj } from './module';
console.log(myObj); // { value: 1 }
updateObj();
console.log(myObj); // { value: 2 } - 属性修改对所有导入方可见
replaceObj();
console.log(myObj); // { value: 3 } - 重新赋值也可见(因为是 let 导出)
4. 导出函数
函数同样是以引用的形式导出的。函数导出后,导入模块使用的是同一个函数实例。
// 导出函数
export function greet() {
return 'Hello!';
}
// 导入函数
import { greet } from './module';
// 这里调用的是同一个函数实例
5. 1.导出class类
在 TypeScript 里,导出类定义是按引用的形式进行的。当把一个类导出并在其他模块中导入时,导入的是同一个类定义。这意味着,无论是在导出模块还是导入模块里对类进行静态属性修改、添加方法等操作,都会影响到这个类的所有引用。
以下是示例代码:
// exportClass.ts
// 定义一个类并导出
export class ExampleClass {
static staticProperty = 'Initial value';
constructor() {}
public instanceMethod() {
return 'This is an instance method.';
}
}
// importClass.ts
// 导入类
import { ExampleClass } from './exportClass';
// 修改类的静态属性
ExampleClass.staticProperty = 'Modified value';
// 创建实例
const exampleInstance = new ExampleClass();
console.log(exampleInstance.instanceMethod());
console.log(ExampleClass.staticProperty);
在上述代码中,ExampleClass
被导出后在另一个模块导入,修改其静态属性 staticProperty
会影响到类的所有引用。
5.2. 类实例化的情况
虽然类是按引用导出,但每次实例化类时都会创建一个新的对象实例。不同的实例之间是相互独立的,对一个实例的属性或方法进行修改,不会影响到其他实例。
// 创建两个不同的实例
const instance1 = new ExampleClass();
const instance2 = new ExampleClass();
// 修改 instance1 的属性不会影响 instance2
// 如果是值拷贝(实际不是这样):
let exportedObj = { value: 1 };
let importedObj = exportedObj; // 实际行为类似这样,但模块系统有更多约束
// 修改会互相影响
exportedObj.value = 2;
console.log(importedObj.value); // 2
总结来说,原始类型是按值导出,对象、数组和函数则是按引用导出。TypeScript 中导出类是按引用形式,即导出的是类定义的引用。不过,每次实例化类时会创建独立的对象实例。
6. 如何设置:属性是只读的
虽然类是按引用导出,但每次实例化类时都会创建一个新的对象实例。不同的实例之间是相互独立的,对一个实例的属性或方法进行修改,不会影响到其他实例。
但如果你希望导出的对象的某些属性是只读的(即导入方不能修改),有几种方法可以实现:
在导出对象的类型中标记某些属性为 readonly
,这样导入方就无法修改这些属性。
方法 1:使用 readonly
修饰符
在导出对象的类型中标记某些属性为 readonly
,这样导入方就无法修改这些属性。
// module.ts
export interface MyObject {
readonly id: number; // 只读属性
name: string; // 可修改属性
}
export const myObj: MyObject = {
id: 1,
name: "Alice"
};
// main.ts
import { myObj } from './module';
myObj.name = "Bob"; // ✅ 允许修改
myObj.id = 2; // ❌ 编译错误:无法修改只读属性
方法 2:使用 Readonly<T>
或 Readonly<Partial<T>>
如果你希望整个对象的所有属性都是只读的,可以使用 Readonly<T>
泛型。
// module.ts
export interface MyObject {
id: number;
name: string;
}
export const myObj: Readonly<MyObject> = {
id: 1,
name: "Alice"
};
// main.ts
import { myObj } from './module';
myObj.name = "Bob"; // ❌ 编译错误:所有属性都是只读的
myObj.id = 2; // ❌ 编译错误
方法 3:使用 Object.freeze()
(运行时只读)
如果你不仅想在编译时限制修改,还想在运行时阻止修改,可以使用 Object.freeze()
。
// module.ts
export const myObj = Object.freeze({
id: 1,
name: "Alice"
});
// main.ts
import { myObj } from './module';
myObj.name = "Bob"; // ❌ 运行时静默失败(严格模式下会报错)
myObj.id = 2; // ❌ 同上
注意:
Object.freeze()
是浅冻结,如果对象包含嵌套对象,内部对象仍然可以修改。如果需要深冻结,可以使用deep-freeze
等库。
总结
方法 | 编译时检查 | 运行时保护 | 适用场景 |
---|---|---|---|
readonly 修饰符 | ✅ | ❌ | 只需要 TS 类型检查 |
Readonly<T> | ✅ | ❌ | 整个对象只读 |
Object.freeze() | ❌ | ✅ | 需要运行时保护 |
Readonly + Object.freeze() | ✅ | ✅ | 最严格保护 |
如果你希望:
-
仅限制 TypeScript 编译时修改 → 用
readonly
或Readonly<T>
-
确保运行时也不能修改 → 用
Object.freeze()
-
最严格的保护 → 同时使用
Readonly<T>
和Object.freeze()