< 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 란?]
- 숫자, 문자열 등 원시 자료형은 값을 비교한다.
- 배열, 객체 등 참조 자료형은 값 혹은 속성을 비교하지 않고, 참조되는 위치를 비교한다.
위 예시에서 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() 사용
lodash 라이브러리의 ㅡ.isEqual(객체1,객체2) 를 사용하면 객체도 직접적인 값을 비교하는 깊은 비교를 할 수 있다.
< 얕은 복사 vs 깊은 복사 >
[얕은 복사 Shallow Copy 란?]
아래 예시를 살펴보자.
aArray와 bArray는 배열이기 때문에, 비교를 하면 false가 나온다. 이 때, bArray는 spread 연산자를 사용해 ...aArray 를 복사하고, 거기에 4를 추가한 배열이다.
Object.assign() 을 사용해서도 얕은 복사를 할 수 있다. [] 에 bArray를 할당한다고 이해하면 되겠다. cArray를 찍어보면 bArray와 똑같은 값을 갖는다.
dArray 라는 배열은 cArray를 복사하고 10을 추가한 배열이다.
여기서 아래와 같은 작업을 하면 어떻게 될까?
현재 dArray의 4번째 원소는 Array(3)인 [5,6,7] 이다. 여기에 8을 push 했으니,
dArray는 [1,2,3,4,[5,6,7,8],10] 이 되는 것은 자명하다. 결과를 찍어보면, 아래와 같이 나온다.
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미포함)에 대한 얕은 복사본을 새로운 배열 객체로 반환한다. 원본 배열은 바뀌지 않는다.
[얕은 동결 이란?]
얕은 동결은 객체를 동결해서 동결된 객체는 불변성을 가진다.
위 예시에 aObject를 찍어보면 아래와 같이 변경이 안된 것을 확인할 수 있다.
그렇다면, 아래와 같은 코드는 어떻게 될까?
Depth가 더 깊이 들어간 부분을 수정하려고 하는 코드이다.
얕은 복사처럼 얕은 동결에서도 깊이 들어간 부분은 변경이 된다. 이 이유는 객체 안에 Depth 안쪽까지는 동결을 시키지 못했기 때문에 이렇게 반영이 되는 것이다. 객체 안 까지는 동결시키지 못한다는 의미이다!
이렇듯, 얕은 복사나 얕은 동결은 중첩된 구조에서 올바른 역할을 수행하지 못한다.
[깊은 복사 Deep Copy 란?]
아래 예시를 살펴보자.
JSON.parse(JSON.stringify(객체))를 사용해서 newAObject를 만들었다. 두 객체는 당연히 같은 값을 나타낸다. 그렇다면 정말 깊은 복사가 된 것일까?
aObject 의 cObject의 값 a를 3으로 변경했는데, 이번에는 newAObject의 cObject 값은 변하지 않고 진짜 aObject의 값만 변했다.
얕은 복사를 했을 때는 얕은부분만 복사를 했기 때문에 중첩이 된 깊은 부분은 복사가 되지 않아서 똑같이 3으로 변했을 것이다.
하지만, 이번엔 깊은 복사가 됐기 때문에 실제로 aObject 값을 바꾸더라도 newAObject의 값은 그 객체 그대로 값을 보존하고 있는 것이다. 정말 다 복사가 잘 된 것이다.
위의 JSON.parse(JSON.stringify()) 방법 말고도 spread operator를 이용해서도 깊은 복사를 해줄 수 있다.
spread 연산자로도 깊은 복사가 가능하다. 중첩이 되는 부분도 스프레드 오퍼레이터를 사용을 해버리는 것인데,
즉 겉 객체를 얕은 복사를 하고, 또 그 내부의 중첩된 부분도 spread operator를 통해 얕은 복사를 해버리는 것이다.
그럼 중첩된 부분까지 다 제대로 복사가 이루어져 최종적으로 깊은 복사와 똑같은 효과를 보인다.
값 변경시도를 했을 때 결과는 아래와 같다.
spread operator 로 깊은 복사를 했다면, lodash 라이브러리를 이용해서도 deepcopy를 할 수 있겠다.
https://www.jsdelivr.com/package/npm/lodash?tab=collection
문법은 _.cloneDeep(target) 로 간단하다. lodash를 사용해서 객체를 복사해도 깊은복사가 된다.
한 객체의 값 변경을 하면 해당 객체만 바뀐 것을 보면, 두 객체가 독립적인 것을 확인 할 수 있다.
마지막으로, 깊은 복사를 할 수 있는 방법으로 structuredClone 이 있다.
https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
이 역시 두 객체가 독립적으로 동작하는 깊은 복사의 방법이다.
< 함수 표현식, 함수 선언문 >
- 함수 선언문(statement) 과 함수 표현식(expression) 의 차이가 뭘까?
먼저 함수 선언문은 아래와 같다.
function funcDeclaration(){
return 'A function declaration 함수 선언문';
}
함수 선언은 함수를 만들고 이름을 지정하는 것이다. function 키워드 다음에 함수 이름을 작성할 때 함수 이름을 선언한다.
다음은 함수 표현식이다.
let funcExpression = function(){
return 'A function expression 함수 표현식';
}
함수 표현식은 함수를 만들고 변수에 할당하는 경우이다. 함수는 익명이므로 이름이 없다.
※ 둘의 차이점은?
함수 선언식은 호이스팅에 영향을 받지만,
함수 표현식은 호이스팅에 영향을 받지 않게 된다.
이 말은 브라우저가 "자바스크립트를 해석할 때 함수 선언식은 호이스팅에 영향을 받기 때문에, 맨 위로 끌어올려지게 된다." 는 말이다.
함수 이름이 있는 함수 선언식은 함수호출 아래 부분에서 함수가 작성되어도 호이스팅 돼서 함수가 잘 실행된다.
정리해보면,
함수 선언은 코드가 실행되기 전에 로드되지만, 함수 표현식은 인터프리터가 해당 코드 줄에 도달할 때만 로드된다.
var 문과 유사하게 함수 선언은 다른 코드의 맨 위로 호이스팅 된다.
함수 표현식은 정의된 범위에서 로컬 변수의 복사본을 유지할 수 있도록 호이스팅되지 않는다.
참고 자료
따라하며 배우는 자바스크립트 A-Z