컴퓨터 공부/🕸️ Web

Javascript 중급 - 3) undefined vs null, 얕은 비교 vs 깊은 비교, 얕은 복사 vs 깊은 복사, 함수 표현식 vs 함수 선언문

letzgorats 2023. 12. 11. 14:57

< undefined vs null >

※ 공통점

  • 둘 다 원시 자료형(primitive) 이다.
  • undefined 타입은 undefined 값이 유일하며, null 타입은 null 값이 유일하다.
  • (단, typeof null 을 찍어보면 object 라고 나오는데, 처음에 만들 때 잘못해서 이걸 고치면 너무나 많은 오류가 나와서 그대로 나뒀다고 한다. 그래서, 타입스크립트에서는 strict 키워드를 통해 찍어보면 object가 아니라고 한다.)

1) undefined 

- undefined 는 '아무 값도 할당받지 않은 상태'를 의미한다.

- var 키워드로 선언한 변수는 호이스팅으로 올라간 후 undefined로 초기화된다. 그 이후 인터프리터가 해당 소스코드에 도달했을 때 할당한 값이 들어가게 된다.

console.log(a); // undefined
var a = 5;

 

- 변수를 선언한 이후 값을 할당하지 않은 변수를 출력하면 undefined 가 반환된다.

let hello;
console.log(hello); // undefined

 

- 이렇게 undefined 는 개발자가 의도적으로 할당하기 위한 값이 아닌 자바스크립트 엔진이 변수를 초기화할 때 사용한다. 그래서 개발자가 의도적으로 undefined 를 할당하는 것은 권장되지 않는다.

- 그래서 변수에 의도적으로 값이 없다고 할 때는 undefined가 아닌 null 을 사용한다.(이게 중요한 차이라면 차이다.)

 

2) null

- null 은 '비어있는, 존재하지 않는 값'을 의미한다.

- null 은 NULL, Null 과 다르다.

- 의도적으로 변수에 값이 없다는 것을 명시하기 위해서 undefined가 아닌 null을 사용한다.

- null을 할당하면 변수가 이전에 참조하던 값을 명시적으로 참조하지 않겠다고 하는 것이므로, 자바스크립트 엔진이 이 변수에 메모리 공간에 대해 가비지 콜렉션을 수행한다.

※ (가비지 콜렉션 : 더 이상 사용하지 않는 메모리를 자동으로 정리하는 것)


< 얕은 비교 vs 깊은 비교 >

[얕은 비교 Shallow Compare 란?]

- 숫자, 문자열 등 원시 자료형값을 비교한다.

- 배열, 객체 등 참조 자료형은 값 혹은 속성을 비교하지 않고, 참조되는 위치를 비교한다.

값이 같아도 false

위 예시에서 obj1과 obj2의 값이 같아도 false가 나오는 이유는 obj1 과 obj2 둘 다 값을 heap에다 넣어놨는데, 참조되는 위치 값은 다르기 때문이다. 이 참조되는 위치의 값이 다르니까 이 둘이 당연히 다르다고 나온 것이다.

 

그럼, 원시 자료형의 예시를 살펴보자.

원시자료형의 비교

 

참조되는 위치가 아닌 직접적인 값을 비교하기 때문에, 당연히 true가 나온다.

그렇다면, 깊은 비교란 무엇일까?

[깊은비교 Deep Compare 란?]

- 얕은 비교와 달리 깊은 비교객체의 경우에도 값으로 비교한다.

- 깊은 비교 방법은 아래와 같다.

 

1. Object depth 가 깊지 않은 경우 : JSON.stringify() 사용

객체인 경우에도 값으로 비교하는 깊은 비교

 

JSON.stringify(객체1) == JSON.stringify(객체2) 를 사용하면 객체도 직접적인 값을 비교하는 깊은 비교를 할 수 있다.

 

2. Object depth 가 깊은 경우 : lodash 라이브러리의 isEqual() 사용

_.isEqual() 을 사용해서 깊은 비교

 

lodash 라이브러리의 ㅡ.isEqual(객체1,객체2) 를 사용하면 객체도 직접적인 값을 비교하는 깊은 비교를 할 수 있다.


< 얕은 복사 vs 깊은 복사 >

[얕은 복사 Shallow Copy 란?]

아래 예시를 살펴보자.

spread operator 를 사용해서 얕은복사

 

aArray와 bArray는 배열이기 때문에, 비교를 하면 false가 나온다. 이 때, bArray는 spread 연산자를 사용해 ...aArray 를 복사하고, 거기에 4를 추가한 배열이다.

Object.assign()을 이용해서 얕은복사

 

Object.assign() 을 사용해서도 얕은 복사를 할 수 있다. [] 에 bArray를 할당한다고 이해하면 되겠다. cArray를 찍어보면 bArray와 똑같은 값을 갖는다.

 

cArray를 얕은복사한 dArray

 

dArray 라는 배열은 cArray를 복사하고 10을 추가한 배열이다.

여기서 아래와 같은 작업을 하면 어떻게 될까?

dArray[4]에 8 push

 

현재 dArray의 4번째 원소는 Array(3)인 [5,6,7] 이다. 여기에 8을 push 했으니,

dArray는  [1,2,3,4,[5,6,7,8],10] 이 되는 것은 자명하다. 결과를 찍어보면, 아래와 같이 나온다.

cArray도 영향을 받았다

 

cArray도 영향을 받았다. cArray의 4번째원소였던 [5,6,7] 이 [5,6,7,8] 이 된 것을 확인할 수 있다. 

중첩된 배열이나 객체가 있다면, cArray를 shallow copy 해서 dArray를 만들고, dArray를 변경했을 때, cArray에 있는 그 중첩된 부분도 변경되는 것을 볼 수 있다.

 

그래서 얕은 복사(Shallow Copy)라고 하는 것이다. 깊은 부분은 복사가 안되니까 말이다.

 

spread operator, Object assign() 말고도, Array.from(), slice() 등도 shallow copy를 한다. 

※ slice() 메서드는 어떤 배열의 begin 부터 end 까지(end미포함)에 대한 얕은 복사본을 새로운 배열 객체로 반환한다. 원본 배열은 바뀌지 않는다.

slice()메서드 : :https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/slice

 

[얕은 동결  이란?]

얕은 동결은 객체를 동결해서 동결된 객체는 불변성을 가진다.

위 예시에 aObject를 찍어보면 아래와 같이 변경이 안된 것을 확인할 수 있다.

aObject.a 는 여전히 a

 

그렇다면, 아래와 같은 코드는 어떻게 될까?

depth가 더 들어간 부분 수정

 

Depth가 더 깊이 들어간 부분을 수정하려고 하는 코드이다.

a가 1에서 3으로 변경됐다.

 

얕은 복사처럼 얕은 동결에서도 깊이 들어간 부분은 변경이 된다. 이 이유는 객체 안에 Depth 안쪽까지는 동결을 시키지 못했기 때문에 이렇게 반영이 되는 것이다. 객체 안 까지는 동결시키지 못한다는 의미이다!

이렇듯, 얕은 복사나 얕은 동결은 중첩된 구조에서 올바른 역할을 수행하지 못한다.

[깊은 복사 Deep Copy 란?]

아래 예시를 살펴보자.

aObject
JSON.parse(JSON.stringify(객체))

 

JSON.parse(JSON.stringify(객체))를 사용해서 newAObject를 만들었다. 두 객체는 당연히 같은 값을 나타낸다. 그렇다면 정말 깊은 복사가 된 것일까?

aObject의 cObject의 a값을 3으로 변경

 

aObject 의 cObject의 값 a를 3으로 변경했는데, 이번에는 newAObject의 cObject 값은 변하지 않고 진짜 aObject의 값만 변했다.

얕은 복사를 했을 때는 얕은부분만 복사를 했기 때문에 중첩이 된 깊은 부분은 복사가 되지 않아서 똑같이 3으로 변했을 것이다.

하지만, 이번엔 깊은 복사가 됐기 때문에 실제로 aObject 값을 바꾸더라도 newAObject의 값은 그 객체 그대로 값을 보존하고 있는 것이다. 정말 다 복사가 잘 된 것이다.

 

위의 JSON.parse(JSON.stringify()) 방법 말고도 spread operator를 이용해서도 깊은 복사를 해줄 수 있다.

똑같은 예시가 있을 때

 

spread operator를 사용해서 깊은 복사

 

spread 연산자로도 깊은 복사가 가능하다. 중첩이 되는 부분도 스프레드 오퍼레이터를 사용을 해버리는 것인데,

즉 겉 객체를 얕은 복사를 하고, 또 그 내부의 중첩된 부분도 spread operator를 통해 얕은 복사를 해버리는 것이다.

그럼 중첩된 부분까지 다 제대로 복사가 이루어져 최종적으로 깊은 복사와 똑같은 효과를 보인다.

값 변경시도를 했을 때 결과는 아래와 같다. 

깊은 복사가 된 결과

 

spread operator 로 깊은 복사를 했다면, lodash 라이브러리를 이용해서도 deepcopy를 할 수 있겠다.

lodash 라이브러리

https://www.jsdelivr.com/package/npm/lodash?tab=collection

 

jsDelivr - A free, fast, and reliable CDN for JS and Open Source

Optimized for JS and ESM delivery from npm and GitHub. Works with all web formats. Serving more than 150 billion requests per month.

www.jsdelivr.com

_ 의 cloneDeep 가져와서 깊은 복사하기

 

문법은  _.cloneDeep(target) 로 간단하다. lodash를 사용해서 객체를 복사해도 깊은복사가 된다.

 

 

한 객체의 값 변경을 하면 해당 객체만 바뀐 것을 보면, 두 객체가 독립적인 것을 확인 할 수 있다.

 

마지막으로, 깊은 복사를 할 수 있는 방법으로 structuredClone 이 있다.

structuredClone(객체)

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

 

structuredClone() global function - Web APIs | MDN

The global structuredClone() method creates a deep clone of a given value using the structured clone algorithm.

developer.mozilla.org

깊은복사 - structuredClone

 

이 역시 두 객체가 독립적으로 동작하는 깊은 복사의 방법이다.


< 함수 표현식, 함수 선언문 >

- 함수 선언문(statement) 과 함수 표현식(expression) 의 차이가 뭘까?

 

먼저 함수 선언문은 아래와 같다.

function funcDeclaration(){
    return 'A function declaration 함수 선언문';
}

 

함수 선언은 함수를 만들고 이름을 지정하는 것이다. function 키워드 다음에 함수 이름을 작성할 때 함수 이름을 선언한다.

 

다음은 함수 표현식이다.

let funcExpression = function(){
    return 'A function expression 함수 표현식';
}

 

함수 표현식은 함수를 만들고 변수에 할당하는 경우이다. 함수는 익명이므로 이름이 없다.

 

※ 둘의 차이점은?

 

함수 선언식은 호이스팅에 영향을 받지만,

함수 표현식은 호이스팅에 영향을 받지 않게 된다.

 

이 말은 브라우저가 "자바스크립트를 해석할 때 함수 선언식은 호이스팅에 영향을 받기 때문에, 맨 위로 끌어올려지게 된다." 는 말이다.

변수에 할당하는 함수 표현식은 호이스팅 x
함수이름이 있는 함수 선언식은 호이스팅 o

 

함수 이름이 있는 함수 선언식은 함수호출 아래 부분에서 함수가 작성되어도 호이스팅 돼서 함수가 잘 실행된다.

 

정리해보면,

함수 선언은 코드가 실행되기 전에 로드되지만, 함수 표현식은 인터프리터가 해당 코드 줄에 도달할 때만 로드된다.

var 문과 유사하게 함수 선언은 다른 코드의 맨 위로 호이스팅 된다.

함수 표현식은 정의된 범위에서 로컬 변수의 복사본을 유지할 수 있도록 호이스팅되지 않는다.


참고 자료

따라하며 배우는 자바스크립트 A-Z

반응형