在JavaScript中生成不可修改屬性對(duì)象的方法
說在前面
數(shù)據(jù)的可變性常常是一個(gè)需要謹(jǐ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ù)組,也就是說,它是 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} 解釋:沒有嘗試進(jìn)行修改,因此函數(shù)正常返回。
代碼實(shí)現(xiàn)
1. 函數(shù)功能概述
通過遞歸地處理對(duì)象及其屬性,并利用 Proxy
對(duì)象來(lái)攔截對(duì)對(duì)象的修改操作,從而使得經(jīng)過處理后的對(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)入下面的處理邏輯:- 通過
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)過遞歸處理后的對(duì)象
obj
會(huì)被傳遞給createProxy
函數(shù)進(jìn)行進(jìn)一步處理,然后返回處理后的結(jié)果。
- 通過
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)過前面 makeImmutable
函數(shù)遞歸處理后的對(duì)象。
- 首先通過
const isArray = Array.isArray(obj);
判斷傳入的對(duì)象obj
是否是數(shù)組類型。 - 如果
obj
是數(shù)組類型,那么會(huì)通過以下代碼攔截?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ì)通過以下代碼創(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é)
通過 makeImmutable
函數(shù)和 createProxy
函數(shù)的協(xié)同工作,首先對(duì)傳入的對(duì)象進(jìn)行遞歸處理,確保其內(nèi)部嵌套的對(duì)象也能被處理成不可變對(duì)象,然后通過創(chuàng)建 Proxy
對(duì)象并設(shè)置相應(yīng)的攔截器,攔截了對(duì)對(duì)象的各種修改操作(包括數(shù)組的特定修改方法和普通對(duì)象的屬性設(shè)置操作),最終使得經(jīng)過處理后的對(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']
沒有嘗試進(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è)示例中,通過 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ù)后,通過 makeImmutable
函數(shù)將數(shù)據(jù)存儲(chǔ)在緩存對(duì)象 cache
中。如果后續(xù)有代碼試圖修改緩存中的數(shù)據(jù),會(huì)拋出錯(cuò)誤,這樣就保證了緩存數(shù)據(jù)的穩(wěn)定性和一致性,避免因?yàn)橐馔庑薷膶?dǎo)致數(shù)據(jù)不一致的問題。
3、函數(shù)式編程
在函數(shù)式編程中,不可變數(shù)據(jù)是一個(gè)核心概念。函數(shù)應(yīng)該是無(wú)副作用的,即不應(yīng)該修改外部的數(shù)據(jù)結(jié)構(gòu)。通過使用不可變對(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ò)誤),然后通過創(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)文章
JavaScript 高級(jí)篇之閉包、模擬類,繼承(五)
本篇主要分享我對(duì)閉包的理解及使用閉包完成私有屬性、模擬類、繼承等,結(jié)合大量例子,希望大家能快速掌握!首先讓我們先從一些基本的術(shù)語(yǔ)開始吧2012-04-0412個(gè)提高JavaScript技能的概念(小結(jié))
這篇文章主要介紹了12個(gè)提高JavaScript技能的概念(小結(jié)),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2019-05-05BootStrap學(xué)習(xí)筆記之nav導(dǎo)航欄和面包屑導(dǎo)航
這篇文章主要介紹了BootStrap學(xué)習(xí)筆記之nav導(dǎo)航欄和面包屑導(dǎo)航的相關(guān)資料,需要的朋友可以參考下2017-01-01微信小程序教程系列之頁(yè)面跳轉(zhuǎn)和參數(shù)傳遞(6)
這篇文章主要為大家詳細(xì)介紹了微信小程序教程系列之頁(yè)面跳轉(zhuǎn)和參數(shù)傳遞,微信小程序提供了3種頁(yè)面跳轉(zhuǎn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04js實(shí)現(xiàn)鼠標(biāo)移入移出卡片切換內(nèi)容
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)鼠標(biāo)移入移出卡片切換內(nèi)容,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10JS獲取dom 對(duì)象 ajax操作 讀寫cookie函數(shù)
一些常用的JS (JONEAjax) 獲取dom 對(duì)象,ajax操作,讀寫cookie類代碼,需要的朋友可以參考下。2009-11-11Javascript中Promise的四種常用方法總結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于Javascript中Promise的四種常用方法,分別是處理異步回調(diào)、多個(gè)異步函數(shù)同步處理、異步依賴異步回調(diào)和封裝統(tǒng)一的入口辦法或者錯(cuò)誤處理,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-07-07