TypeScript 不可變性 readonly, Readonly<T>的使用小結(jié)
哈嘍,大家好,歡迎來到【哈希茶館】!在日常的全棧開發(fā)中,我們經(jīng)常會(huì)遇到因數(shù)據(jù)被意外修改而導(dǎo)致的各種詭異 Bug。特別是在 JavaScript 這種動(dòng)態(tài)語言中,對(duì)象的屬性默認(rèn)是可變的,這在大型或多人協(xié)作的項(xiàng)目中很容易引入不確定性。TypeScript 作為 JavaScript 的超集,為我們提供了強(qiáng)大的類型系統(tǒng),其中就包括了實(shí)現(xiàn)不可變性的有效工具。今天,我們就來聊聊如何使用 readonly 和 Readonly<T> 來提升代碼的健壯性和可維護(hù)性。
什么是不可變性?為什么它很重要?
簡(jiǎn)單來說,不可變性(Immutability)指的是一個(gè)對(duì)象在創(chuàng)建之后,其狀態(tài)就不能再被修改。如果我們想改變這個(gè)對(duì)象,不能直接修改原始對(duì)象,而是應(yīng)該創(chuàng)建一個(gè)新的對(duì)象,這個(gè)新對(duì)象擁有修改后的狀態(tài)。
擁抱不可變性有諸多好處:
- 可預(yù)測(cè)性更高:當(dāng)你知道一個(gè)數(shù)據(jù)結(jié)構(gòu)不會(huì)被意外改變時(shí),代碼的行為就更容易預(yù)測(cè)和推理。
- 簡(jiǎn)化調(diào)試:如果應(yīng)用狀態(tài)是不可變的,追蹤狀態(tài)變更就變得簡(jiǎn)單,因?yàn)槊看巫兏紩?huì)產(chǎn)生新的對(duì)象。
- 性能優(yōu)化:在某些場(chǎng)景下(例如 React 的 PureComponent 或 Redux),通過比較對(duì)象的引用就能快速判斷數(shù)據(jù)是否發(fā)生變化,從而避免不必要的渲染或計(jì)算。
- 更易于并發(fā)處理:不可變數(shù)據(jù)天然線程安全,因?yàn)樗鼈儾粫?huì)被多個(gè)線程同時(shí)修改(雖然在前端 JavaScript 中并發(fā)場(chǎng)景相對(duì)較少,但這個(gè)理念依然重要)。
readonly修飾符:讓屬性只讀
TypeScript 提供了 readonly 修飾符,可以用來標(biāo)記類的屬性或接口的屬性,表示這些屬性在對(duì)象初始化之后就不能再被修改。
1. 類屬性的readonly
class UserProfile {
readonly userId: string;
name: string;
constructor(userId: string, name: string) {
this.userId = userId; // 可以在構(gòu)造函數(shù)中賦值
this.name = name;
}
updateName(newName: string) {
this.name = newName; // 普通屬性可以修改
}
// 嘗試修改 readonly 屬性會(huì)導(dǎo)致編譯錯(cuò)誤
// updateUserId(newUserId: string) {
// this.userId = newUserId; // Error: Cannot assign to 'userId' because it is a read-only property.
// }
}
const user = new UserProfile("user-123", "Alice");
console.log(user.userId); // "user-123"
user.updateName("Bob");
console.log(user.name); // "Bob"
// user.userId = "user-456"; // Error: Cannot assign to 'userId' because it is a read-only property.
在上面的例子中,userId 屬性被 readonly 修飾后,只能在構(gòu)造函數(shù) constructor 中被初始化。之后任何嘗試修改 userId 的行為都會(huì)在編譯階段被 TypeScript 編譯器捕獲并報(bào)錯(cuò)。
2. 接口屬性的readonly
readonly 同樣可以用于接口定義:
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // Error: Cannot assign to 'x' because it is a read-only property.
function movePoint(p: Point, dx: number, dy: number): Point {
// 不能直接修改 p.x 和 p.y
// 要返回一個(gè)新的 Point 對(duì)象
return { x: p.x + dx, y: p.y + dy };
}
const p2 = movePoint(p1, 5, 5);
console.log(p1); // { x: 10, y: 20 } (原始對(duì)象未改變)
console.log(p2); // { x: 15, y: 25 } (返回了新對(duì)象)
這里,Point 接口的 x 和 y 屬性都是只讀的。當(dāng)我們嘗試修改 p1.x 時(shí),編譯器會(huì)報(bào)錯(cuò)。movePoint 函數(shù)也遵循了不可變性的原則,它沒有修改傳入的 Point 對(duì)象,而是返回了一個(gè)全新的 Point 對(duì)象。
注意:readonly 提供的是淺不可變性。如果 readonly 屬性本身是一個(gè)對(duì)象,那么這個(gè)對(duì)象的屬性仍然是可以修改的。
interface Config {
readonly serverUrl: string;
readonly settings: {
timeout: number;
retries: number;
};
}
const appConfig: Config = {
serverUrl: "https://api.example.com",
settings: {
timeout: 5000,
retries: 3,
},
};
// appConfig.serverUrl = "https://new.api.example.com"; // Error: Cannot assign to 'serverUrl' because it is a read-only property.
appConfig.settings.timeout = 10000; // 這是允許的!
console.log(appConfig.settings.timeout); // 10000
appConfig.settings 本身是 readonly 的,意味著你不能給 appConfig.settings 賦一個(gè)新的對(duì)象。但是 settings 對(duì)象內(nèi)部的 timeout 屬性依然是可變的。
Readonly<T>工具類型:創(chuàng)建全只讀類型
當(dāng)我們需要將一個(gè)現(xiàn)有類型的所有屬性都變成只讀時(shí),TypeScript 的 Readonly<T> 工具類型就派上用場(chǎng)了。它會(huì)構(gòu)造一個(gè)新類型,將類型 T 的所有屬性都設(shè)置為 readonly。
interface MutableConfigs {
port: number;
debugMode: boolean;
features: string[];
}
// 使用 Readonly<T> 將 MutableConfigs 的所有屬性變?yōu)橹蛔x
type ImmutableConfigs = Readonly<MutableConfigs>;
const initialConfigs: MutableConfigs = {
port: 3000,
debugMode: true,
features: ["featureA", "featureB"]
};
// immutableAppConfig 的類型是 ImmutableConfigs
const immutableAppConfig: ImmutableConfigs = initialConfigs;
// immutableAppConfig.port = 8080; // Error: Cannot assign to 'port' because it is a read-only property.
// immutableAppConfig.debugMode = false; // Error: Cannot assign to 'debugMode' because it is a read-only property.
// 同樣,Readonly<T> 也是淺不可變的
// immutableAppConfig.features.push("featureC"); // 這仍然是允許的,因?yàn)?features 是一個(gè)數(shù)組對(duì)象
// console.log(immutableAppConfig.features); // ["featureA", "featureB", "featureC"]
// 如果要使數(shù)組也只讀,可以這樣定義:
interface TrulyImmutableConfigs {
readonly port: number;
readonly debugMode: boolean;
readonly features: ReadonlyArray<string>; // 使用 ReadonlyArray<T>
}
const trulyImmutableAppConfig: TrulyImmutableConfigs = {
port: 8000,
debugMode: false,
features: ["logging", "monitoring"]
};
// trulyImmutableAppConfig.features.push("newFeature"); // Error: Property 'push' does not exist on type 'readonly string[]'.
// trulyImmutableAppConfig.features[0] = "newLogging"; // Error: Index signature in type 'readonly string[]' only permits reading.
Readonly<T> 非常適合用于函數(shù)參數(shù)或返回值,以明確表示函數(shù)不會(huì)(或不應(yīng)該)修改傳入的對(duì)象,或者返回一個(gè)不應(yīng)被修改的對(duì)象。
function processConfig(config: Readonly<MutableConfigs>): void {
console.log(`Processing config for port: ${config.port}`);
// config.port = 8081; // Error: Cannot assign to 'port' because it is a read-only property.
}
processConfig(initialConfigs);
ReadonlyArray<T>
對(duì)于數(shù)組,TypeScript 提供了 ReadonlyArray<T> 類型。一個(gè) ReadonlyArray<T> 移除了所有會(huì)修改數(shù)組的方法(如 push, pop, splice 等),并且其索引簽名也是只讀的,防止通過索引直接修改元素。
let readOnlyNumbers: ReadonlyArray<number> = [1, 2, 3, 4, 5]; // readOnlyNumbers.push(6); // Error: Property 'push' does not exist on type 'readonly number[]'. // readOnlyNumbers[0] = 0; // Error: Index signature in type 'readonly number[]' only permits reading. // 可以使用不會(huì)修改原數(shù)組的方法 const doubledNumbers = readOnlyNumbers.map(n => n * 2); console.log(doubledNumbers); // [2, 4, 6, 8, 10] console.log(readOnlyNumbers); // [1, 2, 3, 4, 5] (原始數(shù)組未改變)
實(shí)踐中的考量與深不可變性
readonly 和 Readonly<T> 為我們提供了在 TypeScript 中實(shí)現(xiàn)不可變性的基礎(chǔ)工具。它們主要提供的是淺不可變性。這意味著如果一個(gè)只讀屬性本身是一個(gè)復(fù)雜對(duì)象(如對(duì)象或數(shù)組),那么這個(gè)復(fù)雜對(duì)象的內(nèi)部屬性仍然是可變的。
要實(shí)現(xiàn)深不可變性(Deep Immutability),即對(duì)象及其所有嵌套的子對(duì)象都不可變,通常需要:
- 遞歸地定義只讀類型:手動(dòng)為嵌套結(jié)構(gòu)定義
readonly。 - 使用第三方庫:像 Immer 或 Immutable.js 這樣的庫提供了更完善的深不可變數(shù)據(jù)結(jié)構(gòu)和操作方法。
在大多數(shù)日常開發(fā)場(chǎng)景中,TypeScript 內(nèi)建的淺不可變性已經(jīng)能解決很多問題,特別是在定義配置對(duì)象、函數(shù)契約等方面。當(dāng)處理復(fù)雜的狀態(tài)管理(如 Redux store)時(shí),可以考慮引入專門的不可變數(shù)據(jù)庫。
總結(jié)
擁抱不可變性是提升代碼質(zhì)量、減少 Bug、增強(qiáng)系統(tǒng)可維護(hù)性的重要手段。TypeScript 的 readonly 修飾符和 Readonly<T> 工具類型為我們提供了在編譯時(shí)強(qiáng)制實(shí)施不可變性的能力。
- 使用
readonly標(biāo)記那些初始化后不應(yīng)再改變的類屬性或接口屬性。 - 使用
Readonly<T>將現(xiàn)有類型的所有屬性轉(zhuǎn)換為只讀,常用于函數(shù)參數(shù)和返回值。 - 對(duì)于數(shù)組,使用
ReadonlyArray<T>來確保數(shù)組內(nèi)容不被修改。
雖然它們提供的是淺不可變性,但這已經(jīng)是向前邁出的一大步。養(yǎng)成使用這些工具的習(xí)慣,你的代碼會(huì)變得更加穩(wěn)健和可預(yù)測(cè)。
到此這篇關(guān)于TypeScript 不可變性 readonly, Readonly<T>的使用小結(jié)的文章就介紹到這了,更多相關(guān)TypeScript 不可變性 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在HTML中引入JavaScript的三種方式及優(yōu)缺點(diǎn)詳解
本文詳細(xì)介紹了HTML中引入JavaScript的三種方式,包括事件句柄的使用(如點(diǎn)擊事件觸發(fā)警告框)、腳本塊的編寫位置及其執(zhí)行順序,以及引入外部JS文件的實(shí)踐,需要的朋友可以參考下2026-02-02
Next.js?16的實(shí)踐案例和坑點(diǎn)總結(jié)
Next.js16的到來,總體來說對(duì)Next.js開發(fā)者來說是好事,并且讓我看到了未來在技術(shù)選擇中,是否對(duì)AI友好將會(huì)成為關(guān)鍵點(diǎn),這篇文章主要介紹了Next.js?16實(shí)踐案例和坑點(diǎn)的相關(guān)資料,需要的朋友可以參考下2026-01-01
JavaScript實(shí)現(xiàn)日期格式化的操作詳解
在我們做業(yè)務(wù)開發(fā)的漫長(zhǎng)歲月里,會(huì)多次跟時(shí)間打交道,相信大多數(shù)小伙伴對(duì)日期格式化也并不陌生,本文簡(jiǎn)單記錄了JavaScript實(shí)現(xiàn)日期格式化的過程,以及一些拓展,希望對(duì)大家有所幫助2023-05-05
JavaScript圣杯布局與雙飛翼布局實(shí)現(xiàn)案例詳解
這篇文章主要介紹了JavaScript圣杯布局與雙飛翼布局實(shí)現(xiàn)案例,這是前端面試中需要掌握的知識(shí)點(diǎn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
純JavaScript實(shí)現(xiàn)獲取onclick、onchange等事件的值
這篇文章主要介紹了JavaScript獲取onclick、onchange等事件的值的方法,需要的朋友可以參考下2014-12-12
使用webpack-dev-server處理跨域請(qǐng)求的方法
本篇文章主要介紹了使用webpack-dev-server處理跨域請(qǐng)求的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04

