Pular para o conteúdo principal

[Medium] 📄 Primitive vs Reference Types

1. What are Primitive Types and Reference Types?

O que são tipos primitivos (Primitive Types) e tipos de referência (Reference Types)?

Os tipos de dados em JavaScript são divididos em duas grandes categorias: tipos primitivos e tipos de referência. Eles possuem diferenças fundamentais na forma como são armazenados na memória e no comportamento de passagem.

Tipos primitivos (Primitive Types)

Características:

  • Armazenados na Stack (pilha)
  • Na passagem, o valor em si é copiado (Call by Value)
  • São imutáveis (Immutable)

Incluem 7 tipos:

// 1. String (cadeia de caracteres)
const str = 'hello';

// 2. Number (número)
const num = 42;

// 3. Boolean (booleano)
const bool = true;

// 4. Undefined
let undef;

// 5. Null
const n = null;

// 6. Symbol (ES6)
const sym = Symbol('unique');

// 7. BigInt (ES2020)
const bigInt = 9007199254740991n;

Tipos de referência (Reference Types)

Características:

  • Armazenados no Heap (monte)
  • Na passagem, a referência (endereço de memória) é copiada (Call by Reference)
  • São mutáveis (Mutable)

Incluem:

// 1. Object (objeto)
const obj = { name: 'John' };

// 2. Array (array)
const arr = [1, 2, 3];

// 3. Function (função)
const func = function () {};

// 4. Date
const date = new Date();

// 5. RegExp
const regex = /abc/;

// 6. Map, Set, WeakMap, WeakSet (ES6)
const map = new Map();
const set = new Set();

2. Call by Value vs Call by Reference

Passagem por valor (Call by Value) vs Passagem por referência (Call by Reference)

Passagem por valor (Call by Value) - Tipos primitivos

Comportamento: O valor em si é copiado; modificar a cópia não afeta o valor original.

// Tipo primitivo: passagem por valor
let a = 10;
let b = a; // Copiar valor

b = 20; // Modificar b

console.log(a); // 10 (não afetado)
console.log(b); // 20

Diagrama de memória:

┌─────────┐
│ Stack │
├─────────┤
│ a: 10 │ ← Valor independente
├─────────┤
│ b: 20 │ ← Valor independente (modificado após cópia)
└─────────┘

Passagem por referência (Call by Reference) - Tipos de referência

Comportamento: O endereço de memória é copiado; duas variáveis apontam para o mesmo objeto.

// Tipo de referência: passagem por referência
let obj1 = { name: 'John' };
let obj2 = obj1; // Copiar endereço de memória

obj2.name = 'Jane'; // Modificar através de obj2

console.log(obj1.name); // 'Jane' (afetado!)
console.log(obj2.name); // 'Jane'
console.log(obj1 === obj2); // true (apontam para o mesmo objeto)

Diagrama de memória:

┌─────────┐                    ┌──────────────────┐
│ Stack │ │ Heap │
├─────────┤ ├──────────────────┤
│ obj1 ───┼───────────────────>│ { name: 'Jane' } │
├─────────┤ │ │
│ obj2 ───┼───────────────────>│ (mesmo objeto) │
└─────────┘ └──────────────────┘

3. Common Quiz Questions

Perguntas frequentes de quiz

Pergunta 1: Passagem de tipos primitivos

function changeValue(x) {
x = 100;
console.log('x dentro da função:', x);
}

let num = 50;
changeValue(num);
console.log('num fora da função:', num);
Clique para ver a resposta
// x dentro da função: 100
// num fora da função: 50

Explicação:

  • num é um tipo primitivo (Number)
  • Ao passar para a função, o valor é copiado, x e num são variáveis independentes
  • Modificar x não afeta num
// Fluxo de execução
let num = 50; // Stack: num = 50
changeValue(num); // Stack: x = 50 (cópia)
x = 100; // Stack: x = 100 (somente x é modificado)
console.log(num); // Stack: num = 50 (não afetado)

Pergunta 2: Passagem de tipos de referência

function changeObject(obj) {
obj.name = 'Changed';
console.log('obj.name dentro da função:', obj.name);
}

let person = { name: 'Original' };
changeObject(person);
console.log('person.name fora da função:', person.name);
Clique para ver a resposta
// obj.name dentro da função: Changed
// person.name fora da função: Changed

Explicação:

  • person é um tipo de referência (Object)
  • Ao passar para a função, o endereço de memória é copiado
  • obj e person apontam para o mesmo objeto
  • Modificar o conteúdo do objeto através de obj também afeta person
// Diagrama de memória
let person = { name: 'Original' }; // Heap: criar objeto @0x001
changeObject(person); // Stack: obj = @0x001 (copiar endereço)
obj.name = 'Changed'; // Heap: @0x001.name = 'Changed'
console.log(person.name); // Heap: @0x001.name (mesmo objeto)

Pergunta 3: Reatribuição vs modificação de propriedade

function test1(obj) {
obj.name = 'Modified'; // Modificar propriedade
}

function test2(obj) {
obj = { name: 'New Object' }; // Reatribuir
}

let person = { name: 'Original' };

test1(person);
console.log('A:', person.name);

test2(person);
console.log('B:', person.name);
Clique para ver a resposta
// A: Modified
// B: Modified (não é 'New Object'!)

Explicação:

test1: Modificação de propriedade

function test1(obj) {
obj.name = 'Modified'; // ✅ Modifica a propriedade do objeto original
}
// person e obj apontam para o mesmo objeto, portanto é modificado

test2: Reatribuição

function test2(obj) {
obj = { name: 'New Object' }; // ❌ Apenas muda a referência de obj
}
// obj agora aponta para um novo objeto, mas person ainda aponta para o original

Diagrama de memória:

// Antes de test1
person ────> { name: 'Original' }
obj ────> { name: 'Original' } (o mesmo)

// Depois de test1
person ────> { name: 'Modified' }
obj ────> { name: 'Modified' } (o mesmo)

// Execução de test2
person ────> { name: 'Modified' } (sem alteração)
obj ────> { name: 'New Object' } (novo objeto)

// Depois de test2
person ────> { name: 'Modified' } (ainda sem alteração)
// obj é destruído, o novo objeto é coletado pelo garbage collector

Pergunta 4: Passagem de arrays

function modifyArray(arr) {
arr.push(4);
console.log('1:', arr);
}

function reassignArray(arr) {
arr = [5, 6, 7];
console.log('2:', arr);
}

let numbers = [1, 2, 3];
modifyArray(numbers);
console.log('3:', numbers);

reassignArray(numbers);
console.log('4:', numbers);
Clique para ver a resposta
// 1: [1, 2, 3, 4]
// 3: [1, 2, 3, 4]
// 2: [5, 6, 7]
// 4: [1, 2, 3, 4]

Explicação:

  • modifyArray: Modifica o conteúdo do array original, numbers é afetado
  • reassignArray: Apenas muda a referência do parâmetro, numbers não é afetado

Pergunta 5: Operações de comparação

// Comparação de tipos primitivos
let a = 10;
let b = 10;
console.log('A:', a === b);

// Comparação de tipos de referência
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log('B:', obj1 === obj2);
console.log('C:', obj1 === obj3);
Clique para ver a resposta
// A: true
// B: false
// C: true

Explicação:

Tipos primitivos: Comparam valores

10 === 10; // true (mesmo valor)

Tipos de referência: Comparam endereços de memória

obj1 === obj2; // false (objetos diferentes, endereços diferentes)
obj1 === obj3; // true (apontam para o mesmo objeto)

Diagrama de memória:

obj1 ────> @0x001: { value: 10 }
obj2 ────> @0x002: { value: 10 } (mesmo conteúdo mas endereço diferente)
obj3 ────> @0x001: { value: 10 } (mesmo endereço que obj1)

4. Shallow Copy vs Deep Copy

Cópia superficial vs Cópia profunda

Cópia superficial (Shallow Copy)

Definição: Copia apenas o primeiro nível; objetos aninhados continuam sendo referências.

Método 1: Operador de espalhamento (Spread Operator)

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = { ...original };

// Modificar primeiro nível: não afeta o objeto original
copy.name = 'Jane';
console.log(original.name); // 'John' (não afetado)

// Modificar objeto aninhado: afeta o objeto original!
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Kaohsiung' (afetado!)

Método 2: Object.assign()

const original = { name: 'John', age: 30 };
const copy = Object.assign({}, original);

copy.name = 'Jane';
console.log(original.name); // 'John' (não afetado)

Método 3: Cópia superficial de arrays

const arr1 = [1, 2, 3];

// Método 1: Operador de espalhamento
const arr2 = [...arr1];

// Método 2: slice()
const arr3 = arr1.slice();

// Método 3: Array.from()
const arr4 = Array.from(arr1);

arr2[0] = 999;
console.log(arr1[0]); // 1 (não afetado)

Cópia profunda (Deep Copy)

Definição: Copia completamente todos os níveis, incluindo objetos aninhados.

Método 1: JSON.parse + JSON.stringify (mais comum)

const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading', 'gaming'],
};

const copy = JSON.parse(JSON.stringify(original));

// Modificar objeto aninhado: não afeta o objeto original
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei' (não afetado)

copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'gaming'] (não afetado)

Limitações:

const obj = {
date: new Date(), // ❌ Será convertido em string
func: () => {}, // ❌ Será ignorado
undef: undefined, // ❌ Será ignorado
symbol: Symbol('test'), // ❌ Será ignorado
regexp: /abc/, // ❌ Será convertido em {}
circular: null, // ❌ Referências circulares causam erro
};
obj.circular = obj; // Referência circular

JSON.parse(JSON.stringify(obj)); // Erro ou perda de dados

Método 2: structuredClone() (navegadores modernos)

const original = {
name: 'John',
address: { city: 'Taipei' },
date: new Date(),
};

const copy = structuredClone(original);

// Pode copiar corretamente objetos especiais como Date
console.log(copy.date instanceof Date); // true

Vantagens:

  • ✅ Suporta Date, RegExp, Map, Set, etc.
  • ✅ Suporta referências circulares
  • ✅ Melhor desempenho

Limitações:

  • ❌ Não suporta funções
  • ❌ Não suporta Symbol

Método 3: Implementação recursiva de cópia profunda

function deepClone(obj) {
// Tratar null e não-objetos
if (obj === null || typeof obj !== 'object') {
return obj;
}

// Tratar arrays
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}

// Tratar Date
if (obj instanceof Date) {
return new Date(obj);
}

// Tratar RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}

// Tratar objetos
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}

return cloned;
}

// Exemplo de uso
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading'],
date: new Date(),
};

const copy = deepClone(original);
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei' (não afetado)

Método 4: Usar Lodash

import _ from 'lodash';

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = _.cloneDeep(original);

Comparação: Cópia superficial vs Cópia profunda

CaracterísticaCópia superficialCópia profunda
Níveis copiadosApenas primeiro nívelTodos os níveis
Objetos aninhadosContinuam referênciasCompletamente independentes
DesempenhoRápidoLento
MemóriaPoucaMuita
Caso de usoObjetos simplesEstruturas aninhadas complexas

5. Common Pitfalls

Armadilhas comuns

Armadilha 1: Acreditar que a passagem de parâmetros pode alterar tipos primitivos

// ❌ Entendimento incorreto
function increment(num) {
num = num + 1;
return num;
}

let count = 5;
increment(count);
console.log(count); // 5 (não se torna 6!)

// ✅ Forma correta
count = increment(count); // Precisa receber o valor de retorno
console.log(count); // 6

Armadilha 2: Acreditar que reatribuição pode alterar o objeto externo

// ❌ Entendimento incorreto
function resetObject(obj) {
obj = { name: 'Reset' }; // Apenas muda a referência do parâmetro
}

let person = { name: 'Original' };
resetObject(person);
console.log(person.name); // 'Original' (não foi resetado!)

// ✅ Forma correta 1: Modificar propriedades
function resetObject(obj) {
obj.name = 'Reset';
}

// ✅ Forma correta 2: Retornar novo objeto
function resetObject(obj) {
return { name: 'Reset' };
}
person = resetObject(person);

Armadilha 3: Acreditar que o operador de espalhamento é cópia profunda

// ❌ Entendimento incorreto
const original = {
user: { name: 'John' },
};

const copy = { ...original }; // Cópia superficial!

copy.user.name = 'Jane';
console.log(original.user.name); // 'Jane' (foi afetado!)

// ✅ Forma correta: Cópia profunda
const copy = JSON.parse(JSON.stringify(original));
// ou
const copy = structuredClone(original);

Armadilha 4: Mal-entendido sobre const

// const apenas impede a reatribuição, não é imutabilidade!

const obj = { name: 'John' };

// ❌ Não pode ser reatribuído
obj = { name: 'Jane' }; // TypeError: Assignment to constant variable

// ✅ Propriedades podem ser modificadas
obj.name = 'Jane'; // Funciona normalmente
obj.age = 30; // Funciona normalmente

// Para verdadeira imutabilidade
const immutableObj = Object.freeze({ name: 'John' });
immutableObj.name = 'Jane'; // Falha silenciosamente (no modo estrito, lança erro)
console.log(immutableObj.name); // 'John' (não foi modificado)

Armadilha 5: Problema de referência em loops

// ❌ Erro comum
const arr = [];
const obj = { value: 0 };

for (let i = 0; i < 3; i++) {
obj.value = i;
arr.push(obj); // Todos apontam para o mesmo objeto!
}

console.log(arr);
// [{ value: 2 }, { value: 2 }, { value: 2 }]
// Todos são o mesmo objeto, o valor final é sempre 2

// ✅ Forma correta: Criar novo objeto a cada vez
const arr = [];

for (let i = 0; i < 3; i++) {
arr.push({ value: i }); // Criar novo objeto a cada vez
}

console.log(arr);
// [{ value: 0 }, { value: 1 }, { value: 2 }]

6. Best Practices

Melhores práticas

✅ Abordagens recomendadas

// 1. Ao copiar objetos, usar explicitamente métodos de cópia
const original = { name: 'John', age: 30 };

// Cópia superficial (objetos simples)
const copy1 = { ...original };

// Cópia profunda (objetos aninhados)
const copy2 = structuredClone(original);

// 2. Funções não devem depender de efeitos colaterais para modificar parâmetros
// ❌ Ruim
function addItem(arr, item) {
arr.push(item); // Modifica o array original
}

// ✅ Bom
function addItem(arr, item) {
return [...arr, item]; // Retorna novo array
}

// 3. Usar const para prevenir reatribuição acidental
const config = { theme: 'dark' };
// config = {}; // Vai lançar erro

// 4. Usar Object.freeze quando objetos imutáveis são necessários
const constants = Object.freeze({
PI: 3.14159,
MAX_SIZE: 100,
});

❌ Abordagens a evitar

// 1. Não depender da passagem de parâmetros para modificar tipos primitivos
function increment(num) {
num++; // ❌ Sem efeito
}

// 2. Não confundir cópia superficial com cópia profunda
const copy = { ...nested }; // ❌ Acreditar que é cópia profunda

// 3. Não reutilizar a mesma referência de objeto em loops
for (let i = 0; i < 3; i++) {
arr.push(obj); // ❌ Todos apontam para o mesmo objeto
}

7. Interview Summary

Resumo para entrevistas

Memorização rápida

Tipos primitivos (Primitive):

  • String, Number, Boolean, Undefined, Null, Symbol, BigInt
  • Passagem por valor (Call by Value)
  • Armazenados na Stack
  • Imutáveis (Immutable)

Tipos de referência (Reference):

  • Object, Array, Function, Date, RegExp, etc.
  • Passagem por referência (Call by Reference)
  • Armazenados no Heap
  • Mutáveis (Mutable)

Exemplo de resposta em entrevista

Q: JavaScript é Call by Value ou Call by Reference?

JavaScript é Call by Value para todos os tipos, mas o "valor" passado para tipos de referência é o endereço de memória.

  • Tipos primitivos: Uma cópia do valor é passada, modificações não afetam o valor original
  • Tipos de referência: Uma cópia do endereço é passada, através do endereço é possível modificar o objeto original
  • No entanto, se for reatribuído (mudar o endereço), o objeto original não é afetado

Reference