- 作者:老汪软件技巧
- 发表时间:2024-12-04 21:03
- 浏览量:
说在前面
数据的可变性常常是一个需要谨慎处理的问题。可变数据可能会导致难以预测的副作用,尤其是在大型项目或复杂的应用程序中。不可变数据结构提供了一种解决方案,它能使代码更加健壮、可维护和易于调试。。
需求
编写一个函数,该函数接收一个对象obj,并返回该对象的一个新的不可变版本。
不可变对象是指不能被修改的对象,如果试图修改它,则会抛出错误。
此新对象可能产生三种类型的错误消息。
obj是一个有效的 JSON 对象或数组,也就是说,它是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"}
解释:试图修改数组会导致抛出错误。
示例 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}
解释:没有尝试进行修改,因此函数正常返回。
代码实现1. 函数功能概述
通过递归地处理对象及其属性,并利用 Proxy 对象来拦截对对象的修改操作,从而使得经过处理后的对象无法被修改,达到了“不可变”的效果。
2. 函数参数及内部逻辑分析makeImmutable 函数
const makeImmutable = function (obj) {
if (typeof obj !== "object" || !obj) return obj;
// 递归对象进行代理拦截
Object.keys(obj).forEach((key) => {
obj[key] = makeImmutable(obj[key]);
});
return createProxy(obj);
};
函数接受一个参数 obj,代表要被处理成不可变对象的原始对象。
createProxy 函数
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}`;
},
});
}
函数接受一个参数 obj,就是经过前面 makeImmutable 函数递归处理后的对象。
["pop", "push", "shift", "unshift", "splice", "sort", "reverse"].forEach(
(method) => {
obj[method] = () => {
throw `Error Calling Method: ${method}`;
};
}
);
这里遍历了数组的一些常见的修改方法,如 pop(删除数组末尾元素)、push(在数组末尾添加元素)、shift(删除数组开头元素)、unshift(在数组开头添加元素)、splice(插入、删除或替换数组元素)、sort(对数组元素进行排序)、reverse(反转数组元素顺序)等。对于每个方法,都将其重新定义为一个函数,当调用这些方法时,会抛出一个包含方法名的错误信息,比如调用 push 方法时会抛出 Error Calling Method: push,这样就阻止了对数组进行这些修改操作。
return new Proxy(obj, {
set(_, prop) {
throw `Error Modifying${isArray? " Index" : ""}: ${prop}`;
},
});
这里创建的 Proxy 对象定义了一个 set 拦截器。当尝试对这个代理对象进行属性设置操作(比如 obj['newProp'] = 'value';)时,就会触发这个 set 拦截器。拦截器内部会抛出一个错误信息,其中根据对象是否是数组来决定错误信息中的用词。如果是数组,错误信息会显示 Error Modifying Index: [属性名],表示修改数组的索引位置相关的错误;如果是普通对象,错误信息会显示 Error Modifying: [属性名],总之就是阻止了对对象进行属性设置的修改操作。
3. 整体功能总结
通过 makeImmutable 函数和 createProxy 函数的协同工作,首先对传入的对象进行递归处理,确保其内部嵌套的对象也能被处理成不可变对象,然后通过创建 Proxy 对象并设置相应的拦截器,拦截了对对象的各种修改操作(包括数组的特定修改方法和普通对象的属性设置操作),最终使得经过处理后的对象成为一个不可变对象,任何试图修改它的操作都会抛出相应的错误信息。
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)修改数组值
obj = makeImmutable([1, 2, 3]);
obj[1] = 222;
//Error Modifying Index: 1
(3)调用数组方法
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']
没有尝试进行修改,因此函数正常返回。
实际应用场景1、状态管理(如在React或Vue中)
在现代前端框架中,不可变数据结构有助于优化组件的更新机制。以React为例,当组件的状态(state)是不可变对象时,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 {
// 尝试修改会抛出错误,这符合不可变数据的理念
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 Namebutton>
div>
);
};
在这个示例中,通过 makeImmutable 函数将初始状态对象转换为不可变对象,然后在组件的状态管理中使用。当试图直接修改不可变状态对象的属性时会抛出错误,这提醒开发者使用正确的方式来更新状态,如创建一个新的对象副本并更新副本中的属性,最后将新的不可变对象作为新状态。这种方式可以确保React能够准确地检测状态变化,提高组件更新的性能。
2、数据缓存
在一些需要缓存数据的场景中,确保缓存数据不被意外修改是很重要的。使用不可变对象可以提供这种安全性,因为一旦数据被缓存,就不能被修改,从而保证了数据的一致性。
const cache = {};
const getDataFromServer = async () => {
// 假设这是从服务器获取数据的异步函数
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('不能修改缓存数据:', error);
}
};
当从服务器获取数据后,通过 makeImmutable 函数将数据存储在缓存对象 cache 中。如果后续有代码试图修改缓存中的数据,会抛出错误,这样就保证了缓存数据的稳定性和一致性,避免因为意外修改导致数据不一致的问题。
3、函数式编程
在函数式编程中,不可变数据是一个核心概念。函数应该是无副作用的,即不应该修改外部的数据结构。通过使用不可变对象,可以确保函数的纯度。
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 函数中,首先尝试直接修改传入的任务列表(这会因为列表是不可变的而抛出错误),然后通过创建一个新的列表副本并添加新任务的方式来返回一个新的不可变任务列表。这种方式符合函数式编程的原则,即不修改传入的参数,而是返回一个新的值,保证了函数的可预测性和无副作用的特性。
公众号