
코드스테이츠 - 유어클래스 콘텐츠를 참고하여 작성하였습니다.
[Day5]
2023년 6월 30일
📗목차
원시 자료형 - number, string, boolean, undefined, null, symbol
참조 자료형 - 배열, 객체, 함수
/** 원시 자료형 **/
let num = 5;
let copiedNum = num; // 5
copiedNum = 6;
console.log(num); // 5
console.log(copiedNum); // 6
console.log(num === copiedNum); // false
/** 참조 자료형 **/
let arr = [0, 1, 2, 3];
let copiedArr = arr;
console.log(arr); // [0, 1, 2, 3]
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr) // true
copiedArr변수는 arr변수의 주소를 참조하고 있기 때문에 copiedArr변수를 변경하게 되면 arr변수도 변경된다.
copiedArr.push(4);
console.log(arr); // [0, 1, 2, 3, 4]
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr === copiedArr) // true
다시 말해, 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사 되었다고 볼 수 없다.
let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하고 있는 주소는 다르다.

spread syntax는 ES6에서 새롭게 추가된 문법으로, spread라는 단어의 뜻처럼 배열을 펼칠 수 있다. 펼치는 방법은 배열이 할당된 변수명 앞에
...을 붙여주면 된다. 배열을 펼치면 배열의 각 요소를 확인할 수 있다.
let arr = [0, 1, 2, 3];
console.log(...arr); // 0 1 2 3
새로운 배열 안에 원본 배열을 펼쳐서 전달하면 해보자.
원본 배열과 같은 요소를 가지고 있지만 각각 다른 주소를 참조하게 된다.
결과적으로 slice() 메서드를 사용한 것과 동일하게 동작한다.
let arr = [0, 1, 2, 3];
let copiedArr = [...arr];
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = Object.assign({}, obj);
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
const target = { a: 1, b: 2}
const source = { c: 3, d: 4}
const returnedTarget = Object.assign(target, source);
console.log(target) // { a: 1, b: 2, c: 3, d: 4 }
console.log(source) // { c: 3, d: 4 }
console.log(returnedTarget) // { a: 1, b: 2, c: 3, d: 4 }
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우,
slice(),Object.assign(),spread 문법을 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없다.
한 단계까지만 복사할 수 있다.
유저의 정보를 담고 있는 객체를 요소로 가지고 있는 배열 users를 slice() 메서드를 사용하여 복사했다.
let users = [
{
name: "kimcoding",
age: 26,
job: "student"
},
{
name: "parkhacker",
age: 29,
job: "web designer"
},
];
let copiedUsers = users.slice();
console.log(users === copiedUsers); // false
console.log(users[0] === copiedUsers[0]); // true

이처럼 slice(), Object.assign(), spread syntax 등의 방법으로 참조 자료형을 복사하면, 중첩된 구조 중 한 단계까지만 복사합니다. 이것을 얕은 복사(shallow copy)라고 한다.
반면, 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것은 깊은 복사(deep copy)라고 한다. 그러나 JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법은 없다.
단, JavaScript의 다른 문법을 응용하면 깊은 복사와 같은 결과물을 만들어 낼 수 있다.
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
완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치하면 됩니다. lodash와 ramda는 각각 방법으로 깊은 복사를 구현해 두었습니다. 다음은 lodash의 cloneDeep을 사용한 깊은 복사의 예시입니다.
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
예제 2) 콘솔에 변수
for(var i = 0; i < 5; i++) {
console.log(i);
}
console.log('final i:'. i); // 5
block 범위를 벗어나도 (같은 function scope에서는) 사용이 가능하다.
var 키워드는 블록 스코프를 무시하고, 함수 스코프만 따른다.
변수를 정의하는 또다른 키워드 var
let 키워드의 사용이 권장된다.

MDN은 클로저를 아래와 같이 정의합니다.
클로저는 함수와 그 함수 주변의 상태의 주소 조합이다.
클로저는 함수와 그 함수가 접근할 수 있는 변수의 조합입니다.
const globalVar = '전역 변수';
function outerFn() {
const outerFnVar = 'outer 함수 내의 변수';
const innerFn = function() {
return 'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.';
}
return innerFn;
}
위 코드에서 클로저는 두 조합을 찾을 수 있다.
outerFn과 outerFn에서 접근할 수 있는 globalVarinnerFn과 innerFn에서 접근할 수 있는 globalVar, outerFnVar
변수의 접근 범위인 스코프와 비슷한 개념인데, 왜 따로 클로저만 구분할까?
클로저의 함수는 어디에서 호출되느냐와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해지기 때문이다.
디버거에서도 아래와 같이 클로저이기 때문에 접근할 수 있었던 outerFnVar는 따로 분류하고 있는 모습을 확인할 수 있다.

실제 클로저를 사용할 때는 outerFn, innerFn처럼 함수가 함수를 리턴하는 패턴을 자주 사용하고, outerFn을 외부 함수, innerFn을 내부 함수라고 통칭.
클로저에 대해 추가 학습 시 “외부 함수의 변수에 접근할 수 있는 내부 함수”등의 표현을 자주 접할 수 있으니 참고.
function createFoodRecipe (foodName) {
const getFoodRecipe = function (ingredient1, ingredient2) {
return `${ingredient1} + ${ingredient2} = ${foodName}!`;
}
return getFoodRecipe;
}
const highballRecipe = createFoodRecipe('하이볼');
highballRecipe('콜라', '위스키'); // '콜라 + 위스키 = 하이볼!'
highballRecipe('탄산수', '위스키'); // '탄산수 + 위스키 = 하이볼!'
highballRecipe('토닉워터', '연태고량주'); // '토닉워터 + 연태고량주 = 하이볼!'
highballRecipe 함수는 문자열 ‘하이볼’을 보존하고 있어서 전달인자를 추가로 전달할 필요가 없고, 다양한 하이볼 레시피를 하나의 함수로 제작할 수 있다.
커링은 여러 전달인자를 가진 함수를 연속적으로 리턴하는 함수로 변경하는 행위이다.
function sum(a, b) {
return a + b;
}
function currySum(a) {
return function(b) {
return a + b;
};
}
console.log(sum(10, 20) === currySum(10)(20)) // true
언뜻 봐서는 일반 함수와 커링 함수의 차이가 느껴지지 않지만, 커링은 전체 프로세스의 일정 부분까지만 실행하는 경우 유용하다.
아래 makePancake 함수는 팬케이크 제작 과정을 커링 함수로 만들었다. 팬케이크는 팬케이크 믹스를 만들어두었다가, 나중에 다시 만들 수도 있다. 반면, 커링이 적용되지 않은 makePancakeAtOnce 함수는 일부 조리 과정이 생략된 모습을 표현할 수 없다.
function makePancake(powder) {
return function (sugar) {
return function (pan) {
return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
}
}
}
const addSugar = makePancake('팬케이크가루');
const cookPancake = addSugar('백설탕');
const morningPancake = cookPancake('후라이팬');
// 잠깐 낮잠 자고 일어나서 ...
const lunchPancake = cookPancake('후라이팬');
function makePancakeAtOnce (powder, sugar, pan) {
return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
}
const morningPancake = makePancakeAtOnce('팬케이크가루', '백설탕', '후라이팬')
// 잠깐 낮잠 자고 일어나서 만든 팬케이크를 표현할 방법이 없다.
이와 같이 커링은 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장하기에 용이하다.
displayValue는 makeCalculator의 코드 블록 외에 다른 곳에서는 접근이 불가능하지만, cal의 메서드는 모두 클로저의 함수로서 displayValue에 접근할 수 있다. 이렇게 데이터를 다른 코드 실행으로부터 보호하는 개념을 정보 은닉(information hiding)이라고 한다. 이는 캡슐화(encapsulation)의 큰 특징이다.
function makeCalculator() {
let displayValue = 0;
return {
add: function(num) {
displayValue = displayValue + num;
},
subtract: function(num) {
displayValue = displayValue - num;
},
multiply: function(num) {
displayValue = displayValue * num;
},
divide: function(num) {
displayValue = displayValue / num;
},
reset: function() {
displayValue = 0;
},
display: function() {
return displayValue
}
}
}
const cal = makeCalculator();
cal.display(); // 0
cal.add(1);
cal.display(); // 1
console.log(displayValue) // ReferenceError: displayValue is not defined
이와 같이 클로저는 특정 데이터를 다른 코드의 실행으로부터 보호해야 할 때 용이합니다.
주로 배열을 풀어서 인자로 전달하거나, 배열을 풀어서 각각의 요소로 넣을 때에 사용한다.
function sum(x, y, z) {
return x + y + z;
}
const numbers = [1, 2, 3];
sum(...numbers) // 질문: 어떤 값을 리턴하나요?
let arr = [10, 30, 40, 20]
let value = Math.max(arr) // value = NaN
Math.max를 MDN에서 검색해보면, 숫자인 인자들을 비교하여 가장 큰 수를 리턴하는 메서드인데, 여기서는 배열을 전달하여 실행하였기 때문에 NaN이 나온다.
let arr = [10, 30, 40, 20]
let value = Math.max(...arr) // value = 40
여기서는 spread syntax가 사용되었다. spread syntax는 iterable 한 모든 것의 (대표적으로 문자열, 배열) 요소를 "펼쳐"주는 문법을 의미한다.
파라미터를 배열의 형태로 받아서 사용할 수 있다. 파라미터 개수가 가변적일 때 유용하다.
function sum(...theArgs) {
return theArgs.reduce((previous, current) => {
return previous + current;
});
}
sum(1,2,3) // 질문: 어떤 값을 리턴하나요? 6
sum(1,2,3,4) // 질문: 어떤 값을 리턴하나요? 10
spread 문법은 배열에서 강력한 힘을 발휘한다.
1. 배열 합치기
let parts = ['shoulders', 'knees'];
let lyrics = ['head', ...parts, 'and', 'toes'];
// 질문: lyrics의 값은 무엇인가요
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1 = [...arr1, ...arr2]; // 참고: spread 문법은 기존 배열을 변경하지 않으므로(immutable), arr1의 값을 바꾸려면 새롭게 할당해야 합니다.
// 질문: arr1의 값은 무엇인가요?
2. 배열 복사하기
let arr = [1, 2, 3];
let arr2 = [...arr]; // arr.slice() 와 유사
arr2.push(4); // 참고: spread 문법은 기존 배열을 변경하지 않으므로(immutable), arr2를 수정한다고, arr이 바뀌지 않습니다.
// 질문: arr와 arr2의 값은 각각 무엇인가요? arr: [1, 2, 3] | arr2: [1, 2, 3, 4]
let obj1 = { foo: 'bar', x: 42 };
let obj2 = { foo: 'baz', y: 13 };
let clonedObj = { ...obj1 };
let mergedObj = { ...obj1, ...obj2 };
// 질문: clonedObj와 mergedObj의 값은 각각 무엇인가요?
// cloneObj => {foo: 'bar', x: 42}
// mergedObj => {foo: 'baz', x: 42, y: 13}
function myFun(a, b, ...manyMoreArgs) {
console.log("a", a);
console.log("b", b);
console.log("manyMoreArgs", manyMoreArgs);
}
myFun("one", "two", "three", "four", "five", "six");
// 질문: 콘솔은 순서대로 어떻게 찍힐까요?
// a one
// b two
// manyMoreArgs (4) ['three', 'four', 'five', 'six']
const [a, b, ...rest] = [10, 20, 30, 40, 50];
// a = 10, b = 20, rest = [30, 40, 50]
const {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40}
// a = 10, b = 20, rest = {c: 30, d: 40}
function whois({displayName: displayName, fullName: {firstName: name}}){
console.log(displayName + " is " + name);
}
let user = {
id: 42,
displayName: "jdoe",
fullName: {
firstName: "John",
lastName: "Doe"
}
};
whois(user) // 질문: 콘솔에서 어떻게 출력될까요? jdoe is John
함수를 정의하는 방식에는 함수 선언문과 함수 표현식이 있다.
// 함수선언문
function sum (x, y) {
return x + y;
}
// 함수표현식
const subtract = function (x, y) {
return x - y;
}
ES6가 등장하면서 함수를 정의하는 방법이 하나 더 생겼는데 그게 바로 화살표 함수(arrow function)이다. 함수 표현식으로 함수를 정의할 때 function키워드 대신 화살표(⇒)를 사용한다.
// 화살표 함수
const multiply = (x, y) => {
return x * y;
}
이처럼 화살표 함수를 사용하면 함수 표현식으로 함수를 정의할 때보다 간략하게 함수를 정의할 수 있다. 또한 화살표 함수를 정의할 때 생략할 수 있는 몇가지 규칙이 있다.
())를 생략할 수 있다.// 매개변수가 한 개일 때, 소괄호를 생략할 수 있습니다.
const square = x => { return x * x }
// 위 코드와 동일하게 동작합니다.
const square = ( x ) => { return x * x }
// 단, 매개변수가 없는 경우엔 소괄호를 생략할 수 없습니다.
const greeting = () => { return 'hello world' }
{})를 생략할 수 있다. 이때 코드 블록 내부의 문이 값으로 평가될 수 있으면 return키워드를 생략할 수 있다.const squre = x => x * x
// 위 코드와 동일하게 동작합니다.
const square = x => { return x * x }
// 위 코드와 동일하게 동작합니다.
const square = function (x) {
return x * x
}
일반 함수
function BlackDog() { this.name = '흰둥이'; return { name: '검둥이', bark: function() { console.log(this.name + ': 멍멍!); } } } const blackdog = new BlackDog(); blackDog.bark(); // 검둥이: 멍멍!검둥이: 멍멍
일반 함수는 자신이 종속된 객체를 this로 가리킨다.
화살표 함수
function WhiteDog() { this.name = '흰둥이'; return { name: '검둥이', bark: () => { console.log(this.name + ': 멍멍!); } } } const whiteDob = new WhiteDog(); whiteDob.bark(); // 흰둥이: 멍멍!흰둥이: 멍멍!
화살표 함수는 자신이 종속된 인스턴스를 가리킨다.
자바스크립트의 배열 [ ] 의 경우에는 length를 통하여 길이를 간단하고 손쉽게 가져 올 수 있다.
하지만 obj의 경우에는 length를 지정하게 되면 undefined가 나타난다.

obj 의 경우 length의 길이를 구하기 위해서는 Object.keys를 이용해야 합니다.

많은 도움이 되었습니다, 감사합니다.