在JavaScript中生成不可修改属性对象的方法

 更新时间:2024年12月04日 10:00:39   作者:JYeontu  
这篇文章主要介绍了在 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不可修改属性的对象的资料请关注脚本之家其它相关文章!

相关文章

  • 原生js实现trigger方法示例代码

    原生js实现trigger方法示例代码

    这篇文章主要给大家介绍了关于利用原生js实现trigger方法的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用js具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • 小程序实现上传视频功能

    小程序实现上传视频功能

    这篇文章主要为大家详细介绍了小程序实现上传视频功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-08-08
  • javascript Echart可视化学习

    javascript Echart可视化学习

    这篇文章主要为大家介绍了Echart可视化学习的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • JavaScript数组塌陷实例解析

    JavaScript数组塌陷实例解析

    这篇文章主要为大家介绍了JavaScript数组塌陷实例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • Javascript使用post方法提交数据实例

    Javascript使用post方法提交数据实例

    这篇文章主要介绍了Javascript使用post方法提交数据,实例分析了javascript实现post提交数据的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-08-08
  • 基于JS实现简单的样式切换效果代码

    基于JS实现简单的样式切换效果代码

    这篇文章主要介绍了基于JS实现简单的样式切换效果代码,涉及简单的javascript控制页面元素样式变换的技巧,非常简单实用,需要的朋友可以参考下
    2015-09-09
  • js实现input密码框显示/隐藏功能

    js实现input密码框显示/隐藏功能

    这篇文章主要为大家详细介绍了js实现input密码框显示和隐藏功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-10-10
  • 前端使用xlsx导出数据生成Excel文件的全过程

    前端使用xlsx导出数据生成Excel文件的全过程

    这篇文章主要给大家介绍了关于前端使用xlsx导出数据生成Excel文件的相关资料,最近在做项目中,后端偷懒不做导出功能,让我前端实现,所以在这里记录下前端导出功能,需要的朋友可以参考下
    2023-08-08
  • Wordpress ThickBox 添加“查看原图”效果代码

    Wordpress ThickBox 添加“查看原图”效果代码

    上一次修改了点击图片动作 , 这次添加一个“查看原图”的链接,点击后将在一个新浏览器窗口(或Tab)打开该图片的原始链接地址。
    2010-12-12
  • JavaScript父子窗体间的调用方法

    JavaScript父子窗体间的调用方法

    这篇文章主要介绍了JavaScript父子窗体间的调用方法,涉及javascript调用窗体的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03

最新评论