如何在 javascript 中保持对象不可变的数组?

How to keep an array with objects immutable in javascript?

我想创建一个基于两个数组的数组 - "ideaList" 和 "endorsements" 全局声明。由于 ideaList 和背书用于程序的其他部分,我需要它们是不可变的,我认为 .map 和 .filter 将保持这种不变性。

function prepareIdeaArray(){
var preFilteredIdeas=ideaList
        .filter(hasIdeaPassedControl)
        .map(obj => {obj.count = endorsements
                                 .filter(x=>x.ideaNumber===obj.ideaNumber)
                                 .reduce((sum, x)=>sum+x.count,0); 
                     obj.like = endorsements
                                 .filter(x=>x.ideaNumber===obj.ideaNumber && x.who===activeUser)
                                 .reduce((sum, x)=>sum+x.count,0)===0?false:true
                     obj.position = generatePosition(obj.status)
                     obj.description = obj.description.replace(/\n/g, '<br>')  
                     return obj;});
preFilteredIdeas.sort(compareOn.bind(null,'count',false)).sort(compareOn.bind(null,'position',true))
return preFilteredIdeas;
}

然而,当我在执行此函数后 console.log ideaList 时,我注意到数组的对象都具有 "count"、"like"、"position" 属性有值,这证明数组已经发生了变异。

我尝试仅使用 .map,但结果相同。

你知道我怎样才能防止 ideaList 发生变异吗?此外,我想避免使用 const,因为我首先全局声明 ideaList,然后在另一个函数中为其分配一些数据。

您在地图函数中分配了这些属性,您需要更改它。 (只需声明一个空对象,而不是使用当前的 obj

您可以使用新的 ES6 built-in 不变性机制,或者您可以像这样

在您的对象周围包裹一个漂亮的 getter
var myProvider = {}
function (context)
{
    function initializeMyObject()
    {
        return 5;
    }

    var myImmutableObject = initializeMyObject();

    context.getImmutableObject = function()
    {
        // make an in-depth copy of your object.
        var x = myImmutableObject

        return x;
    }


}(myProvider);

var x = myProvider.getImmutableObject();

这将使您的对象封闭在全局范围之外,但 getter 将可以在您的全局范围内访问。

您可以阅读有关此编码模式的更多信息here

您不是在改变数组本身,而是在改变数组包含引用的对象。 .map() 创建数组的副本,但其中包含的引用指向与原始对象完全相同的对象,您通过直接向它们添加属性来改变它们。

您也需要制作这些对象的副本并将属性添加到这些副本。一个巧妙的方法是在 .map() 回调中使用对象传播:

    .map(({ ...obj }) => {
      obj.count = endorsements
                             .filter(x=>x.ideaNumber===obj.ideaNumber)
      ...

如果您的环境不支持对象传播语法,请使用 Object.assign():

克隆对象
    .map(originalObj => {
      const obj = Object.assign({}, originalObj);
      obj.count = endorsements
                             .filter(x=>x.ideaNumber===obj.ideaNumber)
      ...

为了帮助牢记不变性,您可以将您的值视为原始值。

1 === 2 // false
'hello' === 'world' // false

你也可以将这种思维方式扩展到非原始人

[1, 2, 3] === [1, 2, 3] // false
{ username: 'hitmands' } === { username: 'hitmands' } // false

为了更好地理解它,请看一下MDN - Equality Comparisons and Sameness


如何强制不变性?

通过始终返回给定对象的新实例!

假设我们必须设置待办事项的 属性 status。以旧的方式我们会做:

todo.status = 'new status';

但是,我们可以通过简单地复制给定对象并返回一个新对象来强制实现不变性。

const todo = { id: 'foo', status: 'pending' };

const newTodo = Object.assign({}, todo, { status: 'completed' });

todo === newTodo // false;

todo.status // 'pending'
newTodo.status // 'completed'

回到你的例子,而不是做 obj.count = ...,我们会做:

Object.assign({}, obj, { count: ... }) 
// or
({ ...obj, count: /* something */ })

有些库可以帮助您处理不可变模式:

  1. Immer
  2. ImmutableJS

在JS中,对象是引用的。换句话说,当创建时,您将对象变量指向一个内存位置,该位置旨在保存一个有意义的值。

var o = {foo: 'bar'}

变量 o 现在指向具有 {foo: bar} 的内存。

var p = o;

现在变量 p 也指向相同的内存位置。因此,如果您更改 o,它也会更改 p

这就是您的函数内部发生的情况。即使您使用不会改变其值的数组方法,数组元素本身也是在函数内部被修改的对象。它创建一个新数组 - 但元素指向对象的相同旧内存位置。

var a = [{foo: 1}];     //Let's create an array

//Now create another array out of it
var b = a.map(o => {
   o.foo = 2;
   return o;
})

console.log(a);   //{foo: 2}

一种解决方法是在操作期间为新数组创建一个新对象。这可以使用 Object.assign 或最新的传播运算符来完成。

a = [{foo: 1}];

b = a.map(o => {

    var p = {...o};    //Create a new object
    p.foo = 2;
    return p;

})

console.log(a);  // {foo:1}

制作可变对象“副本”的一种简单方法是将它们字符串化为另一个对象,然后将它们解析回新数组。这对我有用。

function returnCopy(arrayThing) {
    let str = JSON.stringify(arrayThing);
    let arr = JSON.parse(str);
  return arr;
}

您使用 freeze 方法提供您想要使其不可变的对象。

const person = { name: "Bob", age: 26 }
Object.freeze(person)

其实可以使用展开运算符让原数组保持不变,下面y是不可变数组的例子:

const y = [1,2,3,4,5];
function arrayRotation(arr, r, v) {
  for(let i = 0; i < r; i++) {
    arr = [...y]; // make array y as immutable array [1,2,3,4,5]
    arr.splice(i, 0, v);
    arr.pop();
    console.log(`Rotation ${i+1}`, arr);
  }
}
arrayRotation(y, 3, 5)

如果不使用扩展运算符,y 数组会随着循环 运行 的时间而改变。

这是可变数组结果:

const y = [1,2,3,4,5];
function arrayRotation(arr, r, v) {
  for(let i = 0; i < r; i++) {
    arr = y; // this is mutable, because arr and y has same memory address
    arr.splice(i, 0, v);
    arr.pop();
    console.log(`Rotation ${i+1}`, arr);
  }
}
arrayRotation(y, 3, 5)