[JS] 원시/참조 타입과 호이스팅

merci·2023년 8월 4일
0

JavaScript

목록 보기
10/15

타입 분류

자바스크립트의 타입은 원시타입과 참조타입으로 나뉩니다.

원시타입

원시타입은 Number, String, Boolean, Null, Undefined, Symbol, bigint 타입을 말합니다.
원시타입의 큰 특징은 불변(immutable)하다는 것입니다.

let a = 100;
a = 50;

위 처럼 a 에 숫자를 할당하면 변수 a는 스택 메모리에 할당된 값을 직접적으로 가리키게 됩니다.
그리고 원시타입은 불변하다고 했으므로 a에 다른값을 할당한다면 불변성을 지키기 위해서 새로운 a가 메모리에 할당되어 값을 가리키게 되고 100의 값을 가진 메모리는 가비지컬렉터가 처리하게 됩니다.

let a = 10;
let b = a;
a = a + 1;
console.log(b); // 10

따라서 위 처럼 ba를 할당하면 b10의 값을 가리키게 되고 새로 할당된 a는 새로운 11의 값을 가리키게 됩니다.
b가 가리키는 값을 직접 변경해주지 않았으므로 b는 계속해서 10의 값을 가리키게 됩니다.
그 결과 콘솔에는 10이 출력됩니다.

참조타입

원시타입을 제외하면 모두 참조타입이 됩니다. ( 객체, 배열, 함수 .. )
참조타입은 불변하다는 특징이 없어 값을 변경할 수 있습니다. ( 동적 )
또한 참조타입은 원시타입과 다르게 동적으로 크기가 변할 수 있어 스택 메모리에 할당할 수 없습니다.
그래서 참조타입은 직접 값을 가리키지않고 힙 메모리를 가리키는 주소의 값을 가지게 됩니다.

let array = []

예를 들어 위 배열에 값을 추가한다면 메모리의 크기는 계속 늘어나므로 동적으로 힙 메모리를 할당해야 합니다.

let newArray = array

만약 위처럼 참조타입을 복사하게 된다면 newArray도 같은 주소를 가리키게 됩니다.
하지만 자바스크립트에서 참조타입을 복사할 때는 주의를 해야합니다.
복사한 변수를 변경시 의도치 않게 원본을 수정할 수 있기 때문입니다.

그러므로 얕은 복사와 깊은 복사에 대해서 알아봅시다.


얕은 복사, 깊은 복사

참조 타입의 복사 방법은 얕은 복사(shallow coyp)와 깊은 복사(deep copy)로 나뉩니다.
얕은 복사는 데이터 주소를 공유하고 깊은 복사는 데이터를 새로운 메모리에 저장 후 다른 주소를 가집니다.

얕은 복사

데이터 주소를 복사하므로 복사한 변수를 수정하면 원본이 수정됩니다.

let origin = { name: 'Jinny' }
let copy = origin;

copy.name = 'Mr.Lee';

console.log(origin.name); // 'Mr.Lee'
console.log(origin === copy); // true
  • 스프레드 연산자
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = { ...obj1 };
obj2.b.c = 3;
console.log(obj1.b.c); // 출력: 3

깊은 복사

새로운 주소를 가지므로 원본에 영향을 미치지 않습니다.

  • JSON 메소드 이용
    ( undefined, symbol, 순환 참조 처리 x )
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 2
  • 라이브러리 lodash
const _ = require('lodash');
const newObj = _.cloneDeep(oldObj);
  • 라이브러리 fast-copy
const copy = require('fast-copy');
const copiedObject = copy(object);


호이스팅

( 호이스팅 = 끌어올림 )

변수의 위치에 따라서 에러가 나왔다가 undefined가 되는데 왜 그런지 이유를 알아보겠습니다.

function foo() {
    a = 2;
    var a;
    console.log(a);
}
foo(); // 2

뭔가 이상하지만 2가 출력됩니다.

function foo() {
    console.log(a);
    var a = 2;
}
foo(); // undefined

이번에는 undefined가 출력됩니다.

왜 이런 결과가 나오는걸까요 ?

먼저 자바스크립트 엔진은 코드를 인터프리팅 하기 전에 소스 코드를 파싱해 AST구조로 만듭니다.
이후 AST를 바이트 코드로 컴파일한 후 인터프리터로 실행합니다.

이 과정에서 var a = 2;를 하나의 구문으로 생각할 수도 있지만, 자바스크립트는 다음 두 개의 구문으로 분리하여 봅니다.

var a;
a = 2;

변수 선언(생성) 단계와 초기화 단계로 나눈 뒤
선언 단계에서는 그 선언이 소스코드의 어디에 위치하든 해당 스코프의 시작 부분에서 선언이 일어난 것처럼 호이스팅(끌어올림) 후 컴파일되어 바이트 코드가 만들어집니다

그러므로 위 코드는 컴파일 되었을때 바이트코드가 마치 아래처럼 작성한것처럼 변하게 됩니다.

function foo() {
	var a;
    console.log(a);
    a = 2;
}
foo(); // undefined

또한 함수 선언식도 호이스팅이 됩니다.

hoisted(); // Output: "This function has been hoisted."

function hoisted() {
  console.log('This function has been hoisted.');
};

반면에 함수 표현식은 호이스팅이 안됩니다.

expression(); //Output: "TypeError: expression is not a function

var expression = function() {
  console.log('Will this work?');
};

참고로 let, const 타입도 호이스팅이 됩니다.

하지만 선언하기 전에 참조를 하면 var 타입과는 다르게 undefined이 아닌 ReferenceError가 나오게 됩니다.

function foo() {
    console.log(a);
    let a = 2;
}
foo(); // ReferenceError

왜 이런 차이가 발생하는 걸까요 ?

이유는 호이스팅시에 타입에 따라 TDZ(Temporal Dead Zone)가 발생되기 때문입니다.


TDZ

var타입의 호이스팅은 여러 혼란과 버그를 야기하므로 이러한 문제를 해결하기 위해서 ES6에서 letconst의 TDZ가 도입되었습니다.

TDZ(Temporal Dead Zone)는 ES6에서 도입된 개념으로
letconst로 선언된 변수가 그 선언 전에 접근될 수 없는 코드 영역을 의미합니다.

즉, 현재 스코프에 진입할 때부터 실제 변수 선언 위치까지의 영역이 TDZ입니다.
따라서 let, const타입의 변수가 TDZ에 있을 때는 참조하거나 값을 할당하거나 읽는 것이 불가능합니다.

let, const 수명 주기

let, const는 선언 후 초기화 될때까지 TDZ가 존재하게 됩니다.
이 구간에서 변수에 접근하게 되면 ReferenceError 가 발생합니다.

if (true) {
  // 호이스팅되었지만 초기화 x -> TDZ
  console.log(number); // ReferenceError
  let number; // 여기서 undefined로 초기화 됨
  console.log(number); // undefined
  number = 5;
  console.log(number); // 5
}

let이 호이스팅되어 바이트코드에서는 스코프 첫 부분에 선언된것처럼 되더라도 실제 초기화하는 코드 사이 TDZ구간에서 변수에 접근하고 있으므로 ReferenceError에러를 반환합니다.

따라서 의도하지 않은 버그를 막기 위해 에러를 알려주는 let, const 타입 사용을 권장합니다.

var타입의 수명 주기

var타입은 선언시 자동으로 undefined로 할당되기 때문에 호이스팅으로 선언이 끌어올려지더라도 참조하는 타이밍에는 항상 undefined로 값이 초기화 되어 있어 에러가 발생하지 않습니다.

function multiplyByTen(number) {
  	// 호이스팅 + undefined로 초기화
    console.log(ten); // => undefined
    var ten;
    ten = 10;
    console.log(ten); // => 10
    return number * ten;
  }
console.log(multiplyByTen(4)); // => 40

함수 선언식 수명 주기

함수를 선언식으로 만들게 되면 선언, 초기화, 할당이 동시에 이루어집니다.

console.log(sumArray([5, 10, 8])); // => 23

function sumArray(array) {
    return array.reduce(sum);
    function sum(a, b) {
      return a + b;
    }
    // return array.reduce((a, b) => a + b); // 한줄로 만들면
}

호이스팅에 의해 끌어올려질 때 할당까지 동시에 되므로 선언전에 호출이 가능합니다.

함수 표현식 생명 주기

console.log(foo());  // TypeError: foo is not a function
var foo = function() {
    return "Hello";
}

함수 표현식은 선언, 초기화와 실제 할당이 분리되어 있습니다.
foo는 호이스팅되어 undefined로 초기화 됩니다.
하지만 표현식의 할당은 실제 할당하는 부분에서 이루어는데 함수를 호출했으므로 TypeError가 발생하게 됩니다.

profile
작은것부터

0개의 댓글