在JavaScript中生成不可修改屬性對(duì)象的方法
說(shuō)在前面
數(shù)據(jù)的可變性常常是一個(gè)需要謹(jǐn)慎處理的問(wèn)題。可變數(shù)據(jù)可能會(huì)導(dǎo)致難以預(yù)測(cè)的副作用,尤其是在大型項(xiàng)目或復(fù)雜的應(yīng)用程序中。不可變數(shù)據(jù)結(jié)構(gòu)提供了一種解決方案,它能使代碼更加健壯、可維護(hù)和易于調(diào)試。
需求
編寫一個(gè)函數(shù),該函數(shù)接收一個(gè)對(duì)象 obj ,并返回該對(duì)象的一個(gè)新的 不可變 版本。
不可變 對(duì)象是指不能被修改的對(duì)象,如果試圖修改它,則會(huì)拋出錯(cuò)誤。
此新對(duì)象可能產(chǎn)生三種類型的錯(cuò)誤消息。
- 如果試圖修改對(duì)象的鍵,則會(huì)產(chǎn)生以下錯(cuò)誤消息:
`Error Modifying: ${key}`。 - 如果試圖修改數(shù)組的索引,則會(huì)產(chǎn)生以下錯(cuò)誤消息:
`Error Modifying Index: ${index}`。 - 如果試圖調(diào)用會(huì)改變數(shù)組的方法,則會(huì)產(chǎn)生以下錯(cuò)誤消息:
`Error Calling Method: ${methodName}`。你可以假設(shè)只有以下方法能夠改變數(shù)組:['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse']。
obj 是一個(gè)有效的 JSON 對(duì)象或數(shù)組,也就是說(shuō),它是 JSON.parse() 的輸出結(jié)果。
請(qǐng)注意,應(yīng)該拋出字符串字面量,而不是 Error 對(duì)象。
示例
示例 1
輸入:
obj = {
"x": 5
}
fn = (obj) => {
obj.x = 5;
return obj.x;
}
輸出:{"value": null, "error": "Error Modifying: x"}
解釋:試圖修改對(duì)象的鍵會(huì)導(dǎo)致拋出錯(cuò)誤。請(qǐng)注意,是否將值設(shè)置為與之前相同的值并不重要。
示例 2
輸入:
obj = [1, 2, 3]
fn = (arr) => {
arr[1] = {};
return arr[2];
}
輸出:{"value": null, "error": "Error Modifying Index: 1"}
解釋:試圖修改數(shù)組會(huì)導(dǎo)致拋出錯(cuò)誤。
示例 3
輸入:
obj = {
"arr": [1, 2, 3]
}
fn = (obj) => {
obj.arr.push(4);
return 42;
}
輸出:{ "value": null, "error": "Error Calling Method: push"}
解釋:調(diào)用可能導(dǎo)致修改的方法會(huì)導(dǎo)致拋出錯(cuò)誤。
示例4
輸入:
obj = {
"x": 2,
"y": 2
}
fn = (obj) => {
return Object.keys(obj);
}
輸出:{"value": ["x", "y"], "error": null}
解釋:沒(méi)有嘗試進(jìn)行修改,因此函數(shù)正常返回。
代碼實(shí)現(xiàn)
1. 函數(shù)功能概述
通過(guò)遞歸地處理對(duì)象及其屬性,并利用 Proxy 對(duì)象來(lái)攔截對(duì)對(duì)象的修改操作,從而使得經(jīng)過(guò)處理后的對(duì)象無(wú)法被修改,達(dá)到了“不可變”的效果。
2. 函數(shù)參數(shù)及內(nèi)部邏輯分析
makeImmutable 函數(shù)
const makeImmutable = function (obj) {
if (typeof obj !== "object" || !obj) return obj;
// 遞歸對(duì)象進(jìn)行代理攔截
Object.keys(obj).forEach((key) => {
obj[key] = makeImmutable(obj[key]);
});
return createProxy(obj);
};
函數(shù)接受一個(gè)參數(shù) obj,代表要被處理成不可變對(duì)象的原始對(duì)象。
- 首先進(jìn)行一個(gè)條件判斷:
if (typeof obj!== "object" ||!obj) return obj;。這個(gè)判斷的目的是,如果傳入的obj不是對(duì)象類型(比如是基本數(shù)據(jù)類型,如數(shù)字、字符串、布爾值等)或者obj本身是null,那么就直接返回這個(gè)原始的obj,因?yàn)榛緮?shù)據(jù)類型和null本身就是不可變的,不需要進(jìn)行后續(xù)的處理。 - 如果
obj是對(duì)象類型且不為null,那么就會(huì)進(jìn)入下面的處理邏輯:- 通過(guò)
Object.keys(obj).forEach((key) => { obj[key] = makeImmutable(obj[key]); });這行代碼,它會(huì)遍歷對(duì)象obj的所有鍵名(使用Object.keys方法獲取鍵名數(shù)組),對(duì)于每個(gè)鍵名對(duì)應(yīng)的屬性值,再次調(diào)用makeImmutable函數(shù)進(jìn)行遞歸處理。這樣做的目的是確保對(duì)象內(nèi)部的嵌套對(duì)象也能被正確地處理成不可變對(duì)象。 - 最后,經(jīng)過(guò)遞歸處理后的對(duì)象
obj會(huì)被傳遞給createProxy函數(shù)進(jìn)行進(jìn)一步處理,然后返回處理后的結(jié)果。
- 通過(guò)
createProxy 函數(shù)
function createProxy(obj) {
const isArray = Array.isArray(obj);
// 攔截 Array 原生方法
if (isArray) {
["pop", "push", "shift", "unshift", "splice", "sort", "reverse"].forEach(
(method) => {
obj[method] = () => {
throw `Error Calling Method: ${method}`;
};
}
);
}
return new Proxy(obj, {
set(_, prop) {
throw `Error Modifying${isArray ? " Index" : ""}: ${prop}`;
},
});
}
函數(shù)接受一個(gè)參數(shù) obj,就是經(jīng)過(guò)前面 makeImmutable 函數(shù)遞歸處理后的對(duì)象。
- 首先通過(guò)
const isArray = Array.isArray(obj);判斷傳入的對(duì)象obj是否是數(shù)組類型。 - 如果
obj是數(shù)組類型,那么會(huì)通過(guò)以下代碼攔截?cái)?shù)組的一些原生方法:
["pop", "push", "shift", "unshift", "splice", "sort", "reverse"].forEach(
(method) => {
obj[method] = () => {
throw `Error Calling Method: ${method}`;
};
}
);
這里遍歷了數(shù)組的一些常見的修改方法,如 pop(刪除數(shù)組末尾元素)、push(在數(shù)組末尾添加元素)、shift(刪除數(shù)組開頭元素)、unshift(在數(shù)組開頭添加元素)、splice(插入、刪除或替換數(shù)組元素)、sort(對(duì)數(shù)組元素進(jìn)行排序)、reverse(反轉(zhuǎn)數(shù)組元素順序)等。對(duì)于每個(gè)方法,都將其重新定義為一個(gè)函數(shù),當(dāng)調(diào)用這些方法時(shí),會(huì)拋出一個(gè)包含方法名的錯(cuò)誤信息,比如調(diào)用 push 方法時(shí)會(huì)拋出 Error Calling Method: push,這樣就阻止了對(duì)數(shù)組進(jìn)行這些修改操作。
- 最后,無(wú)論傳入的對(duì)象
obj是數(shù)組還是其他普通對(duì)象,都會(huì)通過(guò)以下代碼創(chuàng)建一個(gè)Proxy對(duì)象并返回:
return new Proxy(obj, {
set(_, prop) {
throw `Error Modifying${isArray? " Index" : ""}: ${prop}`;
},
});
這里創(chuàng)建的 Proxy 對(duì)象定義了一個(gè) set 攔截器。當(dāng)嘗試對(duì)這個(gè)代理對(duì)象進(jìn)行屬性設(shè)置操作(比如 obj['newProp'] = 'value';)時(shí),就會(huì)觸發(fā)這個(gè) set 攔截器。攔截器內(nèi)部會(huì)拋出一個(gè)錯(cuò)誤信息,其中根據(jù)對(duì)象是否是數(shù)組來(lái)決定錯(cuò)誤信息中的用詞。如果是數(shù)組,錯(cuò)誤信息會(huì)顯示 Error Modifying Index: [屬性名],表示修改數(shù)組的索引位置相關(guān)的錯(cuò)誤;如果是普通對(duì)象,錯(cuò)誤信息會(huì)顯示 Error Modifying: [屬性名],總之就是阻止了對(duì)對(duì)象進(jìn)行屬性設(shè)置的修改操作。
3. 整體功能總結(jié)
通過(guò) makeImmutable 函數(shù)和 createProxy 函數(shù)的協(xié)同工作,首先對(duì)傳入的對(duì)象進(jìn)行遞歸處理,確保其內(nèi)部嵌套的對(duì)象也能被處理成不可變對(duì)象,然后通過(guò)創(chuàng)建 Proxy 對(duì)象并設(shè)置相應(yīng)的攔截器,攔截了對(duì)對(duì)象的各種修改操作(包括數(shù)組的特定修改方法和普通對(duì)象的屬性設(shè)置操作),最終使得經(jīng)過(guò)處理后的對(duì)象成為一個(gè)不可變對(duì)象,任何試圖修改它的操作都會(huì)拋出相應(yīng)的錯(cuò)誤信息。
4.完整代碼
/**
* @param {Array} arr
* @return {(string | number | boolean | null)[][]}
*/
var jsonToMatrix = function (arr) {
let keySet = new Set();
const isObject = (x) => x !== null && typeof x === "object";
const getKeyName = (object, name = "") => {
if (!isObject(object)) {
keySet.add(name);
return;
}
for (const key in object) {
getKeyName(object[key], name + (name ? "." : "") + key);
}
};
arr.forEach((item) => getKeyName(item));
keySet = [...keySet].sort();
const getValue = (obj, path) => {
const paths = path.split(".");
let i = 0;
let value = obj;
while (i < paths.length) {
if (!isObject(value)) break;
value = value[paths[i++]];
}
if (i < paths.length || isObject(value) || value === undefined) return "";
return value;
};
const res = [keySet];
arr.forEach((item) => {
const list = [];
keySet.forEach((key) => {
list.push(getValue(item, key));
});
res.push(list);
});
return res;
};
5、功能測(cè)試
(1)修改對(duì)象屬性
obj = makeImmutable({ x: 5 });
obj.x = 6;
//Error Modifying: x

(2)修改數(shù)組值
obj = makeImmutable([1, 2, 3]); obj[1] = 222; //Error Modifying Index: 1

(3)調(diào)用數(shù)組方法
arr = makeImmutable([1, 2, 3]); arr.push(4) //Error Calling Method: push

(4)獲取屬性值
obj = makeImmutable({ x: 5, y: 6 });
console.log(obj.x); //5
console.log(Object.keys(obj)); //['x', 'y']

沒(méi)有嘗試進(jìn)行修改,因此函數(shù)正常返回。
實(shí)際應(yīng)用場(chǎng)景
1、狀態(tài)管理(如在React或Vue中)
在現(xiàn)代前端框架中,不可變數(shù)據(jù)結(jié)構(gòu)有助于優(yōu)化組件的更新機(jī)制。以React為例,當(dāng)組件的狀態(tài)(state)是不可變對(duì)象時(shí),React可以更高效地比較前后狀態(tài)的差異,從而決定是否需要重新渲染組件。
- 代碼示例(以React為例):
import React, { useState } from 'react';
const initialState = {
user: {
name: 'John',
age: 30
},
todos: ['Task 1', 'Task 2']
};
const immutableInitialState = makeImmutable(initialState);
const App = () => {
const [state, setState] = useState(immutableInitialState);
const handleUpdateUser = () => {
try {
// 嘗試修改會(huì)拋出錯(cuò)誤,這符合不可變數(shù)據(jù)的理念
state.user.name = 'Jane';
} catch (error) {
console.log(error);
}
// 正確的更新方式(假設(shè)使用 immer.js等庫(kù)輔助更新)
setState(prevState => {
const newState = {...prevState };
newState.user = {...prevState.user };
newState.user.name = 'Jane';
return makeImmutable(newState);
});
};
return (
<div>
<p>User Name: {state.user.name}</p>
<button onClick={handleUpdateUser}>Update User Name</button>
</div>
);
};
在這個(gè)示例中,通過(guò) makeImmutable 函數(shù)將初始狀態(tài)對(duì)象轉(zhuǎn)換為不可變對(duì)象,然后在組件的狀態(tài)管理中使用。當(dāng)試圖直接修改不可變狀態(tài)對(duì)象的屬性時(shí)會(huì)拋出錯(cuò)誤,這提醒開發(fā)者使用正確的方式來(lái)更新狀態(tài),如創(chuàng)建一個(gè)新的對(duì)象副本并更新副本中的屬性,最后將新的不可變對(duì)象作為新狀態(tài)。這種方式可以確保React能夠準(zhǔn)確地檢測(cè)狀態(tài)變化,提高組件更新的性能。
2、數(shù)據(jù)緩存
在一些需要緩存數(shù)據(jù)的場(chǎng)景中,確保緩存數(shù)據(jù)不被意外修改是很重要的。使用不可變對(duì)象可以提供這種安全性,因?yàn)橐坏?shù)據(jù)被緩存,就不能被修改,從而保證了數(shù)據(jù)的一致性。
- 代碼示例:
const cache = {};
const getDataFromServer = async () => {
// 假設(shè)這是從服務(wù)器獲取數(shù)據(jù)的異步函數(shù)
const data = await fetch('https://example.com/api/data');
const jsonData = await data.json();
cache['data'] = makeImmutable(jsonData);
return cache['data'];
};
const updateData = () => {
try {
cache['data'].someProperty = 'new value';
} catch (error) {
console.log('不能修改緩存數(shù)據(jù):', error);
}
};
當(dāng)從服務(wù)器獲取數(shù)據(jù)后,通過(guò) makeImmutable 函數(shù)將數(shù)據(jù)存儲(chǔ)在緩存對(duì)象 cache 中。如果后續(xù)有代碼試圖修改緩存中的數(shù)據(jù),會(huì)拋出錯(cuò)誤,這樣就保證了緩存數(shù)據(jù)的穩(wěn)定性和一致性,避免因?yàn)橐馔庑薷膶?dǎo)致數(shù)據(jù)不一致的問(wèn)題。
3、函數(shù)式編程
在函數(shù)式編程中,不可變數(shù)據(jù)是一個(gè)核心概念。函數(shù)應(yīng)該是無(wú)副作用的,即不應(yīng)該修改外部的數(shù)據(jù)結(jié)構(gòu)。通過(guò)使用不可變對(duì)象,可以確保函數(shù)的純度。
- 代碼示例:
const addTask = (tasks, newTask) => {
try {
tasks.push(newTask);
} catch (error) {
console.log(error);
}
const newTasks = [...tasks, newTask];
return makeImmutable(newTasks);
};
const tasks = ['Task 1', 'Task 2'];
const immutableTasks = makeImmutable(tasks);
const newTasks = addTask(immutableTasks, 'Task 3');
在 addTask 函數(shù)中,首先嘗試直接修改傳入的任務(wù)列表(這會(huì)因?yàn)榱斜硎遣豢勺兊亩鴴伋鲥e(cuò)誤),然后通過(guò)創(chuàng)建一個(gè)新的列表副本并添加新任務(wù)的方式來(lái)返回一個(gè)新的不可變?nèi)蝿?wù)列表。這種方式符合函數(shù)式編程的原則,即不修改傳入的參數(shù),而是返回一個(gè)新的值,保證了函數(shù)的可預(yù)測(cè)性和無(wú)副作用的特性。
以上就是在JavaScript中生成不可修改屬性對(duì)象的方法的詳細(xì)內(nèi)容,更多關(guān)于JavaScript不可修改屬性的對(duì)象的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于JavaScript中name的意義沖突示例介紹
這篇文章主要介紹了關(guān)于JavaScript中name的意義沖突,需要的朋友可以參考下2014-05-05
JS實(shí)現(xiàn)圖片預(yù)加載無(wú)需等待
網(wǎng)站開發(fā)時(shí)經(jīng)常需要在某個(gè)頁(yè)面需要實(shí)現(xiàn)對(duì)大量圖片的瀏覽;用javascript來(lái)實(shí)現(xiàn)一個(gè)圖片瀏覽器,讓用戶無(wú)需等待過(guò)長(zhǎng)的時(shí)間就能看到其他圖片2012-12-12
利用prop-types第三方庫(kù)對(duì)組件的props中的變量進(jìn)行類型檢測(cè)
本篇文章主要介紹了利用prop-types第三方庫(kù)對(duì)組件的props中的變量進(jìn)行類型檢測(cè)的相關(guān)知識(shí),具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-05-05
詳解JavaScript中扁平與樹形數(shù)據(jù)的轉(zhuǎn)換
這篇文章主要為大家想介紹了JavaScript中實(shí)現(xiàn)扁平與樹形數(shù)據(jù)相互轉(zhuǎn)換的方法,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)JavaScript有一定的幫助,需要的可以參考一下2023-01-01
常見瀏覽器多長(zhǎng)時(shí)間會(huì)提示“腳本運(yùn)行時(shí)間過(guò)長(zhǎng)”總結(jié)
這篇文章主要介紹了常見瀏覽器多長(zhǎng)時(shí)間會(huì)提示“腳本運(yùn)行時(shí)間過(guò)長(zhǎng)”總結(jié),需要的朋友可以參考下2014-04-04
JS+php后臺(tái)實(shí)現(xiàn)文件上傳功能詳解
這篇文章主要介紹了JS+php后臺(tái)實(shí)現(xiàn)文件上傳功能,結(jié)合實(shí)例形式分析了javascript前臺(tái)表單數(shù)據(jù)提交與后臺(tái)php文件傳輸處理相關(guān)操作技巧,需要的朋友可以參考下2019-03-03
微信小程序如何實(shí)現(xiàn)點(diǎn)擊圖片放大功能
這篇文章主要介紹了微信小程序如何實(shí)現(xiàn)點(diǎn)擊圖片放大功能,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01

