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

沒有嘗試進行修改,因此函數(shù)正常返回。
實際應用場景
1、狀態(tài)管理(如在React或Vue中)
在現(xiàn)代前端框架中,不可變數(shù)據(jù)結構有助于優(yōu)化組件的更新機制。以React為例,當組件的狀態(tài)(state)是不可變對象時,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 {
// 嘗試修改會拋出錯誤,這符合不可變數(shù)據(jù)的理念
state.user.name = 'Jane';
} catch (error) {
console.log(error);
}
// 正確的更新方式(假設使用 immer.js等庫輔助更新)
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>
);
};
在這個示例中,通過 makeImmutable 函數(shù)將初始狀態(tài)對象轉換為不可變對象,然后在組件的狀態(tài)管理中使用。當試圖直接修改不可變狀態(tài)對象的屬性時會拋出錯誤,這提醒開發(fā)者使用正確的方式來更新狀態(tài),如創(chuàng)建一個新的對象副本并更新副本中的屬性,最后將新的不可變對象作為新狀態(tài)。這種方式可以確保React能夠準確地檢測狀態(tài)變化,提高組件更新的性能。
2、數(shù)據(jù)緩存
在一些需要緩存數(shù)據(jù)的場景中,確保緩存數(shù)據(jù)不被意外修改是很重要的。使用不可變對象可以提供這種安全性,因為一旦數(shù)據(jù)被緩存,就不能被修改,從而保證了數(shù)據(jù)的一致性。
- 代碼示例:
const cache = {};
const getDataFromServer = async () => {
// 假設這是從服務器獲取數(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);
}
};
當從服務器獲取數(shù)據(jù)后,通過 makeImmutable 函數(shù)將數(shù)據(jù)存儲在緩存對象 cache 中。如果后續(xù)有代碼試圖修改緩存中的數(shù)據(jù),會拋出錯誤,這樣就保證了緩存數(shù)據(jù)的穩(wěn)定性和一致性,避免因為意外修改導致數(shù)據(jù)不一致的問題。
3、函數(shù)式編程
在函數(shù)式編程中,不可變數(shù)據(jù)是一個核心概念。函數(shù)應該是無副作用的,即不應該修改外部的數(shù)據(jù)結構。通過使用不可變對象,可以確保函數(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ù)中,首先嘗試直接修改傳入的任務列表(這會因為列表是不可變的而拋出錯誤),然后通過創(chuàng)建一個新的列表副本并添加新任務的方式來返回一個新的不可變任務列表。這種方式符合函數(shù)式編程的原則,即不修改傳入的參數(shù),而是返回一個新的值,保證了函數(shù)的可預測性和無副作用的特性。
以上就是在JavaScript中生成不可修改屬性對象的方法的詳細內容,更多關于JavaScript不可修改屬性的對象的資料請關注腳本之家其它相關文章!
相關文章
利用prop-types第三方庫對組件的props中的變量進行類型檢測
本篇文章主要介紹了利用prop-types第三方庫對組件的props中的變量進行類型檢測的相關知識,具有很好的參考價值。下面跟著小編一起來看下吧2017-05-05
詳解JavaScript中扁平與樹形數(shù)據(jù)的轉換
這篇文章主要為大家想介紹了JavaScript中實現(xiàn)扁平與樹形數(shù)據(jù)相互轉換的方法,文中的示例代碼講解詳細,對我們學習JavaScript有一定的幫助,需要的可以參考一下2023-01-01

