Fala, gente! Tudo certo? Recentemente me lembrei de uma situação onde, por um deslize, fiquei horas e horas batendo cabeça. Depois de rever as mesmas linhas milhares de vezes, percebi que se tratava de um erro bem bobinho: passar valores por referência. Na verdade, passar valores por referência não é um problema ou um erro, mas não entender o que isso significa, é. Hoje entenderemos a diferença de valor e referência no JavaScript, e veremos alguns cuidados que devemos tomar.
Perguntinha rápida: qual a saída do console.log do código abaixo?
var x = 1; function change(j) { j = 2; } change(x); console.log(x);
Se você pensou em 1, parabéns! Resposta correta.
Agora que você se provou ser um verdadeiro mestre, aí vai outra: o que deve ser exibido pelo console.log abaixo?
var x = [1, 2, 3]; function change(j) { j.push(4); } change(x); console.log(x);
Desta vez, o valor da variável x realmente é modificado. Mas, por quê? Dois comportamentos diferentes para a mesma coisa? Não exatamente. O que aconteceu no primeiro exemplo é que a variável foi passada por valor, enquanto, no segundo, por referência.
Tipos primitivos e objetos
Para entender o que de fato acontece, é necessário saber que o JavaScript possui dois tipos de dados: primitivos e objetos.
Os primitivos são tipos de dados que não são representados por objetos, ou seja, não possuem métodos. Já um objeto é definido por uma coleção de propriedades que são definidas por associações entre uma chave e um valor. Observe os exemplos abaixo:
// Primitivos (number, boolean, string, null, undefined) var age = 20; var isEven = true; var name = "Joãozinho"; var nullValue = null; var undefinedValue = undefined; // Objetos (objetos, arrays, funções) var car = { name: 'marea turbo', color: 'black', wheels: 4, doors: 4, category: 'sedan', }; var oddNumbers = [1, 3, 5, 7, 9]; var sum = (a + b) => a + b;
Valor
Passar por valor significa criar uma cópia do valor de uma variável no momento de uma atribuição (Através do operador =). Isso quer dizer que apenas valores serão salvos nos endereços de memória. Para visualizar isso, vamos declarar algumas variáveis em endereços fictícios na memória:
var x = 10; // valor: 10 endereço: #1 var y = x; // valor: 10 endereço: #2
O que acontece acima é que, ao atribuir x a y, uma cópia do valor de x é salvo no endereço da memória da variável y. Isso significa que alterações na variável x não impactam na variável y e vice-versa, porque cada uma tem seu próprio endereço na memória.
Vamos revisitar o primeiro exemplo:
var x = 1; function change(j) { j = 2; console.log('j = ', j); // j = 2 } change(x); // o valor de x é passado para dentro da função console.log('x = ', x); // x = 1
Acima, observamos que x foi passado como valor para a função change, ou seja, ao atribuir o valor 2 ao argumento recebido dentro da função (j), o valor de x não é modificado, mas sim o valor de j, que existe apenas dentro do escopo da função.
Essa dinâmica acontece com qualquer valor que seja do tipo primitivo.
Referência
Já passar por referência significa o oposto. Neste caso, o que é atribuído não é o valor, mas sim uma referência a esse valor. Então, se duas variáveis possuem a mesma referência, alterações em qualquer uma delas, significa que haverá alteração em ambas. No JavaScript, objetos são sempre passados por referência.
Objetos são alocados na memória de um modo diferente dos tipos primitivos. Ao declarar um objeto, o que é alocado em um endereço de memória é uma referência a esse valor, e não o valor propriamente dito.
var x = [1, 2, 3]; // valor: #0 endereço: #1 conteúdo: [1, 2, 3] var y = x; // valor: #0 endereço: #2 conteúdo: [1, 2, 3]
No exemplo acima observamos variáveis que possuem a mesma referência a um valor. Veja o que acontece ao alterar o valor de uma das variáveis:
var x = [1, 2, 3]; var y = x; y.push(4); console.log({ x, y }); // { x: [1, 2, 3, 4], y: [1, 2, 3, 4] }
Se revisitarmos o segundo exemplo desse post, veremos isso acontecendo:
var x = [1, 2, 3]; function change(j) { j.push(4); // O valor atrelado à referência é modificado console.log('j = ', j); // j = [1, 2, 3, 4] } change(x); // a referência ao valor contido em x é passado para dentro da função console.log('x = ', x); // x = [1, 2, 3, 4]
Não seria JavaScript se não tivesse mais uma pegadinha... O que deve ser exibido nos console.log abaixo?
var x = [1, 2, 3]; function change(j) { j = [1, 2]; console.log('j = ', j); } change(x); console.log('x = ', x);
Se você pensou que o valor de x seria [1, 2], você errou. Segue os valores de j e x impressos no console:
- J = [1, 2];
- X = [1, 2, 3];
Para entendermos o que houve, basta prestar um pouco mais de atenção na primeira linha da função change. Lá, o valor de j, que é uma referência ao conteúdo presente em x ([1, 2, 3]), não é modificado. O que de fato acontece é que um novo objeto é declarado ([1, 2]) e associado à variável j, que só existe no escopo da função. Então, o valor de j passa a ser uma referência ao valor [1, 2], deixando a referência que carrega o valor [1, 2, 3] intacta.
var x = [1, 2, 3]; function change(j) { j = [1, 2]; // um novo objeto é criado e uma referência de seu valor é atribuído à variável j console.log('j = ', j); // [1, 2] } change(x); // a referência ao valor contido em x é passado para dentro da função console.log('x = ', x); // x = [1, 2, 3]
Clonar valores sem mutar
Com tipos primitivos não precisamos nos preocupar, porque, como vimos, atribuições com tipos primitivos são feitas por valor, e não referência. Por outro lado, com os objetos, devemos tomar cuidado ao realizar atribuições, de modo que não causem efeitos colaterais não desejados.
Para atribuir o valor de uma variável para outra sem que elas tenham a mesma referência, como já observamos, devemos criar um novo objeto, e então atribuí-lo à variável. Para fazer isso, existem alguns caminhos:
var x = [1, 2, 3]; var y = [...x]; // copiar array utilizando sintaxe de espalhamento (spread operator) var z = [].concat(x); // copiar array utilizando concat (Array.prototype.concat()) y.push(4); z.push(4); console.log({ x, y, z }); // { x: [1, 2, 3], y: [1, 2, 3, 4], z: [1, 2, 3, 4] }
No exemplo acima, copiamos o valor da variável x nas variáveis y e z, de modo que as alterações nelas não causem efeitos colaterais, pois cada uma possui como valor a referência para um objeto diferente. Também é possível utilizar essa estratégia com objetos:
var x = { name: 'Gabriel', lastName: 'Gigante' }; var y = {...x}; // copiar objeto utilizando sintaxe de espalhamento (spread operator) var z = Object.assign({}, x); // copiar array utilizando assign y.lastName = 'Silva'; z.lastName = 'Silva'; console.log({ x, y, z }); // { x: { name: 'Gabriel', lastName: 'Gigante' }, y: { name: 'Gabriel', lastName: 'Silva' }, z: { name: 'Gabriel', lastName: 'Silva' } }
Apesar dos exemplos acima funcionarem muito bem para muitos casos, essa estratégia não serve para todos os casos. Tente encontrar um problema no exemplo abaixo:
var x = [1, 2, [1, 2, 3], { name: 'Gabriel', lastName: 'Gigante' }]; var y = { name: 'Gabriel', lastName: 'Gigante', techs: ['JavaScript', 'TypeScript'], social: { github: 'gagigante', linkedIn: 'gagigante', } }; var a = [...x]; var b = {...y}; a[2].push(4); a[3].lastName = 'Silva'; b.techs.push('React'); b.social.github = 'blablabla';
E aí? Conseguiu identificar algum erro no código acima? Há um grande problema nele e, para observar isso, vamos complementar o exemplo com alguns console.log.
var x = [1, 2, [1, 2, 3], { name: 'Gabriel', lastName: 'Gigante' }]; var y = { name: 'Gabriel', lastName: 'Gigante', techs: ['JavaScript', 'TypeScript'], social: { github: 'gagigante', linkedIn: 'gagigante', } }; var a = [...x]; var b = {...y}; a[2].push(4); a[3].lastName = 'Silva'; b.techs.push('React'); b.social.github = 'blablabla'; console.log({ x, a, y, b });
Clonamos as variáveis x e y, que são objetos (Array e Objeto, respectivamente), portanto passados por referência, utilizando o spread operator. Segundo o que vimos, alterações em qualquer uma dessas variáveis não deveriam causar efeitos colaterais, pois cada uma tem uma referência diferente, certo...? Errado!
Existe uma sutil diferença desses objetos para os demais utilizados nos exemplos anteriores. Esses objetos possuem dentro de si outros objetos e, já que são objetos, são passados por referência 🤯.
var x = [1, 2, [1, 2, 3], { name: 'Gabriel', lastName: 'Gigante' }]; var y = { name: 'Gabriel', lastName: 'Gigante', techs: ['JavaScript', 'TypeScript'], social: { github: 'gagigante', linkedIn: 'gagigante', } }; var a = [...x]; // cria um novo array com todos os dados de x e atribui à variável a, porém os dois últimos elementos, por serem objetos, são passados por referência var b = {...y}; // cria um novo objeto com todos os dados de y e atribui à variável b, porém os atributos techs e social, por serem objetos, são passados por referência a[2].push(4); // modifica um array (tipo não primitivo) que é referenciado por duas variáveis (x e a) e, portanto, causa alterações em ambas a[3].lastName = 'Silva'; // modifica um objeto (tipo não primitivo) que é referenciado por duas variáveis (x e a) e, portanto, causa alterações em ambas b.techs.push('React'); // modifica um array (tipo não primitivo) que é referenciado por duas variáveis (y e b) e, portanto, causa alterações em ambas b.social.github = 'blablabla'; // modifica um objeto (tipo não primitivo) que é referenciado por duas variáveis (y e b) e, portanto, causa alterações em ambas console.log({ x, a, y, b }); /** * { * x: [ 1, 2, [ 1, 2, 3, 4 ], { name: 'Gabriel', lastName: 'Silva' } ], * a: [ 1, 2, [ 1, 2, 3, 4 ], { name: 'Gabriel', lastName: 'Silva' } ], * y: { * name: 'Gabriel', * lastName: 'Gigante', * techs: [ 'JavaScript', 'TypeScript', 'React' ], * social: { github: 'blablabla', linkedIn: 'gagigante' } * }, * b: { * name: 'Gabriel', * lastName: 'Gigante', * techs: [ 'JavaScript', 'TypeScript', 'React' ], * social: { github: 'blablabla', linkedIn: 'gagigante' } * }, * } */
Com isso podemos ver que clonar objetos que possuem sub objetos pode causar problemas, porque no caso desses sub objetos, não são criados novos objetos; a referência do mesmo objeto é passada.
Isso é o que chamamos de shallow clonning (clonagem rasa), o que significa que apenas o objeto principal está sendo verdadeiramente clonado. Para fazer uma verdadeira cópia dessas variáveis, será necessário fazer um deep clonning (clonagem profunda):
var x = [1, 2, [1, 2, 3], { name: 'Gabriel', lastName: 'Gigante' }]; var y = { name: 'Gabriel', lastName: 'Gigante', techs: ['JavaScript', 'TypeScript'], social: { github: 'gagigante', linkedIn: 'gagigante', } }; var a = [x[0], x[1], [...x[3]], { ...x[4] }]; var b = {...y, techs: [...y.techs], social: { ...y.social } }; a[2].push(4); a[3].lastName = 'Silva'; b.techs.push('React'); b.social.github = 'blablabla'; console.log({ x, a, y, b });
No exemplo modificado, não só o objeto principal foi clonado, mas também os sub objetos contidos nele. O Matheus Silva ilustra bem esse tema em um de seus posts: Cuidados ao clonar objetos com spread!
Comparar valores e referencias
Para finalizar, um último cuidado que devemos tomar é que, no momento de comparar valores, também existe uma diferença de se comparar valores primitivos e objetos. Observe os exemplos abaixo:
x = 1; y = 1; z = x; console.log(x === y); // true console.log(x === z); // true // basiquinho né arr = [1, 2, 3]; arr2 = [1, 2, 3]; arr3 = arr; console.log(arr === arr2) // false console.log(arr === arr3) // true
O ponto de atenção fica na comparação entre objetos. No exemplo acima, temos uma comparação entre dois objetos com o mesmo conteúdo que retorna false. Isso acontece porque cada um deles é um objeto diferente, apesar de possuírem o mesmo conteúdo. A comparação só retornaria true caso ambas as variáveis na comparação apontassem para o mesmo objeto, como é o caso da última comparação feita no exemplo.
Com isso, esse post vai chegando ao fim. Espero que tenha gostado e que tenha aprendido algo aqui. Se, no momento em que você estiver lendo este blog, já tiver um campo dedicado a comentários, sinta-se à vontade para deixar dúvidas, correções e sugestões de temas para eu abordar por aqui. Obrigado pela a audiência e a gente se vê no próximo post. Até lá!
Referências
Dmitri Pavlutin - The Difference Between Values and References in JavaScript
Abdullah A Malik - Javascript: Understanding the difference between Value Type and Reference Type
Eduardo Rabelo - Diferenças entre "Valor" e "Referência" em JavaScript