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

12

我希望基于全局声明的两个数组 - "ideaList"和"endorsements"创建一个数组。由于在程序其他部分中使用了ideaList和endorsements,我需要它们保持不可变性。我认为使用.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,然后在另一个函数中将一些数据分配给它。


1
制作副本:let clone = [...ideaList] 顺便说一下,.map() 不会改变原数组,它返回一个副本。 - zer00ne
你至少有5个或更多未定义的变量,它们都是全局的吗? - zer00ne
ideaList,endorsements是全局数组,activeUser是全局字符串。hasIdeaPassedControl和compareOn是函数。 - Pierre
8个回答

11

你没有改变数组本身,而是改变了数组中包含的对象的引用。 .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)
      ...

3
为了更好地理解不可变性,您可以将值视为原始类型。
1 === 2 // false
'hello' === 'world' // false

你可以将这种思维方式扩展到非原语类型上。
[1, 2, 3] === [1, 2, 3] // false
{ username: 'hitmands' } === { username: 'hitmands' } // false

为了更好地理解,可以查看MDN - Equality Comparisons and Sameness

如何强制不可变性?

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

假设我们需要设置待办事项的状态属性。以旧方式进行时,我们只需执行:

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

3

在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}

2
你可以使用freeze方法来使对象不可变,需要提供要使其不可变的对象。
const person = { name: "Bob", age: 26 }
Object.freeze(person)

0
实际上,你可以使用扩展运算符来保持原始数组不变。下面的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)

0
你可以使用新的ES6 内置不可变性机制,或者只需像这样在对象周围包装一个漂亮的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将在全局范围内可访问。

您可以在此处阅读更多关于此编码模式的内容。


0
一种简单的方法是将可变对象转换为字符串,然后再解析成新的数组,从而实现“复制”。这对我很有效。
function returnCopy(arrayThing) {
    let str = JSON.stringify(arrayThing);
    let arr = JSON.parse(str);
  return arr;
}

这种方法的性能比仅使用 [...original]original.slice() 差,但是如果数组中有函数、符号或大整数,则无法正常工作。大整数会抛出错误,而符号和函数则会被忽略。 - jimmyfever

-1

你需要在你的映射函数中分配这些属性,你需要进行更改。(只需声明一个空对象,而不是使用你当前的obj


一个例子会有所帮助。 - MichaelE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接