在JavaScript中生成不可修改属性对象的方法
说在前面
数据的可变性常常是一个需要谨慎处理的问题。可变数据可能会导致难以预测的副作用,尤其是在大型项目或复杂的应用程序中。不可变数据结构提供了一种解决方案,它能使代码更加健壮、可维护和易于调试。
需求
编写一个函数,该函数接收一个对象 obj
,并返回该对象的一个新的 不可变 版本。
不可变 对象是指不能被修改的对象,如果试图修改它,则会抛出错误。
此新对象可能产生三种类型的错误消息。
- 如果试图修改对象的键,则会产生以下错误消息:
`Error Modifying: ${key}`
。 - 如果试图修改数组的索引,则会产生以下错误消息:
`Error Modifying Index: ${index}`
。 - 如果试图调用会改变数组的方法,则会产生以下错误消息:
`Error Calling Method: ${methodName}`
。你可以假设只有以下方法能够改变数组:['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse']
。
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
,代表要被处理成不可变对象的原始对象。
- 首先进行一个条件判断:
if (typeof obj!== "object" ||!obj) return obj;
。这个判断的目的是,如果传入的obj
不是对象类型(比如是基本数据类型,如数字、字符串、布尔值等)或者obj
本身是null
,那么就直接返回这个原始的obj
,因为基本数据类型和null
本身就是不可变的,不需要进行后续的处理。 - 如果
obj
是对象类型且不为null
,那么就会进入下面的处理逻辑:- 通过
Object.keys(obj).forEach((key) => { obj[key] = makeImmutable(obj[key]); });
这行代码,它会遍历对象obj
的所有键名(使用Object.keys
方法获取键名数组),对于每个键名对应的属性值,再次调用makeImmutable
函数进行递归处理。这样做的目的是确保对象内部的嵌套对象也能被正确地处理成不可变对象。 - 最后,经过递归处理后的对象
obj
会被传递给createProxy
函数进行进一步处理,然后返回处理后的结果。
- 通过
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
函数递归处理后的对象。
- 首先通过
const isArray = Array.isArray(obj);
判断传入的对象obj
是否是数组类型。 - 如果
obj
是数组类型,那么会通过以下代码拦截数组的一些原生方法:
["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
,这样就阻止了对数组进行这些修改操作。
- 最后,无论传入的对象
obj
是数组还是其他普通对象,都会通过以下代码创建一个Proxy
对象并返回:
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可以更高效地比较前后状态的差异,从而决定是否需要重新渲染组件。
- 代码示例(以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 Name</button> </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
函数中,首先尝试直接修改传入的任务列表(这会因为列表是不可变的而抛出错误),然后通过创建一个新的列表副本并添加新任务的方式来返回一个新的不可变任务列表。这种方式符合函数式编程的原则,即不修改传入的参数,而是返回一个新的值,保证了函数的可预测性和无副作用的特性。
以上就是在JavaScript中生成不可修改属性对象的方法的详细内容,更多关于JavaScript不可修改属性的对象的资料请关注脚本之家其它相关文章!
相关文章
Wordpress ThickBox 添加“查看原图”效果代码
上一次修改了点击图片动作 , 这次添加一个“查看原图”的链接,点击后将在一个新浏览器窗口(或Tab)打开该图片的原始链接地址。2010-12-12
最新评论