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​