● 오늘 공부한 것
- 원시자료형 VS 참조 자료형
- 원시자료형
- 원시 자료형을 변수에 할당하면 메모리 공간에 값 자체가 저장된다.
- 원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달된다.
- 원시 자료형은 변경 불가능한 값(immutable value)이다. 즉, 한 번 생성된 원시 자료형은 읽기 전용(read only) 값이다.
- 원시 자료형은 number, string, boolean, undefined, null, symbol이 있다.
- 참조자료형
- 참조 자료형을 변수에 할당하면 메모리 공간에 주솟값이 저장된다.
- 참조 값을 갖는 변수를 다른 변수에 할당하면 주솟값이 복사되어 전달된다.
- 참조 자료형은 변경이 가능한 값(mutable value)이다.
- 참조 자료형은 array, object, function (배열, 객체, 함수 가 있다.)
- 원시자료형
정리를 해보자면, 원시자료형은 각각의 변수마다 각각의 메모리가 주어지고, 그 메모리에 값이 바로 부여된다. 하지만 참조자료형 같은 경우에는 주솟값이 저장된다. 그리고 그 값은 특별한 공간인 힙(heap)에 저장해둔다.
let num1 = 3;
let num2 = num1;
console.log(num1, num2); // 3, 3
console.log(num1 === num2); //true
// 원시자료형은 메모리에 저장된 값으로만 판별하기때문에 true가 나온다.
let num3 = 10;
let num4 = num3;
num3 = false;
console.log(num3, num4);
// 원시자료형은 깂이 불변하기 때문에, 즉, 메모리에 값이 부여되기 때문에
// num3에 false를 부여해도, num4는 바뀌지 않는다.
// 문자열은 원시 자료형이지만 배열처럼 인덱스로 문자열의 각 문자에 접근이 가능하다.
let str = "daeun"
console.log(str[0]) // 'd'
console.log(str[2]) // 'e'
//하지만 배열과는 달리 인덱스에 직접 다른 문자를 할당하여 값을 변경할 수 없다.
//문자열도 원시 자료형이기 때문에 값을 변경할 수 없기 때문이다.
str[0] = "g"
console.log(str) // "daeun"
// 즉 값을 변경하고 싶다면 다시 할당해야한다.
// 재할당을 하면 다른 메모리공간을 확보하고, 그 메모리에 값을 부여한다.
str = "gaeun";
console.log(str) // "gaeun"
남아 있는 값 "daeun"은 어떻게 될까? JavaScript 엔진은 이처럼 사용하지 않는 값을 자동으로 메모리에서 삭제한다. 이런 기능을 가비지 콜렉터(garbage collector)라고 한다. 그러나 이 값이 언제 사라질지는 그 누구도 예측할 수 없다.
let array1 = [1, 2, 3, 4, 5];
let array2 = array1;
console.log(array1, array2); // [1,2,3,4,5] [1,2,3,4,5]
console.log(array1 === array2); //true
// 참조자료형은 같은 메모리주소를 가르키고 있으므로, true가 나온다.
array1.push(6);
console.log(array1, array2); // [ 1, 2, 3, 4, 5, 6 ] [ 1, 2, 3, 4, 5, 6 ]
// 참조자료형은 같은 메모리주소를 가르키고 있으므로, 즉,
// 같이 가르키고 있는 메모리가 수정되었으므로 메모리를 수정하면 같이 변한다고 보면된다.
// 이는 array2가 수정을 해도 똑같다.
그렇다면 참조자료형은 원시자료형처럼 복사가 되지 않고, 왜 이런식으로 작동하는 것일까?
원시 자료형의 경우 값의 크기가 거의 일정하기 때문에 새로운 공간을 확보하여 값을 복사하는 방법이 유용하지만, 크기가 일정하지 않은 참조 자료형의 경우 매번 값을 복사한다면 그만큼 효율성은 떨어질 수밖에 없다. 이런 이유로 참조 자료형은 변경이 가능하도록 설계되어 있다.
- 얕은 복사 VS 깊은복사
그렇다면 참조자료형은 계속 같은 메모리 주소를 가르킨 상태에서 값들을 수정해야할까? 그래서 크게 복사를 하는 방법은 얕은복사, 깊은 복사가 있다.
얕은 복사
- 배열 복사하기
- slice()사용하기
let array = [1, 2, 3, 4, 5];
let newArray = array.slice();
console.log(array, newArray); // [ 1, 2, 3, 4, 5 ] [ 1, 2, 3, 4, 5 ]
console.log(array === newArray); // false
- spread syntax 사용하기
let array = [1, 2, 3, 4, 5];
let newArray = [...array];
console.log(array, newArray); // [ 1, 2, 3, 4, 5 ] [ 1, 2, 3, 4, 5 ]
console.log(array === newArray); // false
- 객체복사하기
- Object.assign 사용하기
let obj = { name: "kim", age: 27 };
let newObj = Object.assign({}, obj);
console.log(obj, newObj); // { name: 'kim', age: 27 } { name: 'kim', age: 27 }
console.log(obj === newObj); // false
- spread syntax 사용하기
let obj = { name: "kim", age: 27 };
let newObj = { ...obj };
console.log(obj, newObj); // { name: 'kim', age: 27 } { name: 'kim', age: 27 }
console.log(obj === newObj); // false
하지만 위의 방법들은 얕은 복사로, 깊은 복사까지 할 수 없다. 이중, 삼중중첩문 같은 경우 복사가 안된다는 말이다.
let array1 = [ [1,2], [3,4,], [5,6] ]; // 이중중첩배열
let array2 = [...array1];
// array1 과 array2가 같니?
console.log(array1 === array2); // false
// array1의 0번째 요소와 array2의 0번째 요소가 같니?
console.log(array1[0] === array2[0]); // true;
let obj = { name: "kim", age: 27 , obj : {birth : "1022"}};
let newObj = { ...obj };
// obj랑 newObj 가 같아?
console.log(obj === newObj); // false
// obj의 birth프로퍼티랑 newObj의 birth프로퍼티랑 같아?
console.log(obj.obj === newObj.obj); //true
깊은 복사
- JSON.stringify()와 JSON.parse()
JSON.stringify()는 참조 자료형을 문자열 형태로 변환하여 반환하고, JSON.parse()는 문자열의 형태를 객체로 변환하여 반환한다. 먼저 중첩된 참조 자료형을 JSON.stringify()를 사용하여 문자열의 형태로 변환하고, 반환된 값에 다시 JSON.parse()를 사용하면, 깊은 복사와 같은 결과물을 반환한다.
const arr = [1, 2, [3, 4]];
const copiedArr = JSON.parse(JSON.stringify(arr));
console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
하지만 객체 내에서 함수프로퍼티가 있다면 null로 바뀌게 되어서, 이또한 완전히 깊은 복사라고 보기 어렵다.
const arr = [1, 2, [3, function(){ console.log('hello world')}]];
const copiedArr = JSON.parse(JSON.stringify(arr));
console.log(arr); // [1, 2, [3, function(){ console.log('hello world')}]]
console.log(copiedArr); // [1, 2, [3, null]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
따라서 이런 경우에는 외부라이브러리를 사용해야한다. lodash, ramda를 사용하면된다. 아래는 lodash 라이브러리를 사용한 예시다.
const lodash = require('lodash');
const arr = [1, 2, [3, 4]];
const copiedArr = lodash.cloneDeep(arr);
console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
- 스코프
let outNum = 12;
function makeNum(){
let innerNum = 10;
console.log(innerNum + outNum)
}
makeNum() // 22
console.log(innerNum) // ReferenceError: innerNum is not defined
위에처럼 변수에 접근할 수 있는 범위가 존재한다 중괄호(블록) 안쪽에 변수가 선언되었는가, 바깥쪽에 변수가 선언되었는가가 중요하다. 이 범위를 우리는 스코프라고 부른다.
1) 안쪽 스코프에서 바깥쪽 스코프로는 접근할 수 있지만 반대는 불가능하다.
2) 스코프는 중첩이 가능하다.
3) 스코프에는 전역스코프, 지역스코프가 있다.
4) 지역 변수는 전역 변수보다 더 높은 우선순위를 가진다.
let outNum = 12;
function numArea(){
let outNum = 10;
console.log(outNum)
}
numArea() // 10
console.log(outNum) // 12
- 스코프의 종류
- 하나는 블록 스코프(block scope)라고 부르며, 중괄호를 기준으로 범위가 구분된다.
-또 다른 스코프 종류로는 함수 스코프(function scope)가 있다. function 키워드가 등장하는 함수 선언식 및 함수 표현식은 함수 스코프를 만든다. 화살표 함수는 블록스코프로 취급된다.
- var, let, const 의 차이
함수 스코프에서는 전역스코프에 이름이 같은 변수가 있어도, 함수 내부에서 변수들이 일회용느낌으로 사용되는 것을 알 수 있다. 즉, 지역스코프가 함수 스코프라면, 전역변수와 지역변수의 구분이 잘 되어지고 있다는 말이다.
const num = 10;
function testNum(){
const num = 40;
console.log(num)
}
testNum() // 40
console.log(num) //10
let num = 10;
function testNum(){
let num = 40;
console.log(num)
}
testNum() // 40
console.log(num) // 10
var num = 10;
function testNum(){
var num = 40;
console.log(num)
}
testNum() // 40
console.log(num) // 10
반대로 블록스코프로 같은 변수를 선언하고 출력해보자.
const num = 10;
if (true){
const num = 20;
console.log(num) // 20
}
console.log(num) // 10
let num = 10;
if (true){
let num = 20;
console.log(num) // 20
}
console.log(num) // 10
var num = 10;
if (true){
var num = 20;
console.log(num) // 20
}
console.log(num) // 20
블록스코프에서는 const, let은 함수스코프와 똑같이 작동하지만, var로 선언된 변수는 다시 재선언이 가능해져 우리가 아는데로 작동되지 않는다.
왜냐면 var 로 선언된 전역변수와 전역함수는 window 객체로 속하게 된다. 전역 변수는 가장 바깥 스코프에 정의한 변수이다. 따라서, 어디서든 접근이 가능해진다.
var num = 20;
function testFunc(){console.log("test")}
console.log(num) // 20
console.log(window.num) // 20
console.log(testFunc) // ƒ testFunc(){console.log("test")}
console.log(window.testFunc) // ƒ testFunc(){console.log("test")}
하지만, 잠시 쓰고 버릴 변수라면 함수스코프, 블록스코프 내에서 let, const로 선언하는 것이 안정적이다. 전역변수로 빼고 어디서든 접근 가능하여 변하게 만든다면, 안정적인 코드를 짤 수 없기 때문에 let, const 변수를 활용하는 것이 옳다.
📍 수업이외 학습
- 프로그래머스 다항식 .. 못풀었는데 내일 이어서 풀예정.
- 혼자 실행컨텍스트에 대한 개념을 이해하려고 모지리답다를 온종일 읽고.. 이에관련된 정보들을 모두 찾는 시간에 쏟았다. 내일 정리해서 올릴것