[S1U9] JavaScript 핵심 개념과 주요 문법

👽·2024년 1월 24일
0
post-thumbnail

CH1. 참조 자료형과 원시 자료형

🔸 자료형: 값(value)의 종류. 각각의 자료형은 고유한 속성과 메서드를 가짐. 크게 두 가지로 구분하며 원시 자료형과 참조 자료형이 존재.

📌 원시 자료형(primitive data type)

🔸 number, string, boolean, undefined, null, symbol.
🔸 원시자료형은 고정된 저장 공간을 차지.

  • 변수에 값을 할당하면 메모리 공간에 값 자체가 저장됨.
  • 원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달됨.
  • 변경 불가능한 값(immutable value)임. 한 번 생성된 원시 자료형은 읽기 전용(read only) 값임.

📌 참조 자료형(reference data type)

🔸 원시 자료형이 아닌 모든 자료형. 여러 데이터를 한 번에 다룰 수 있는 array, object, function

  • 변수에 할당하면 메모리 공간에 주소값이 저장됨.
  • 참조 값을 갖는 변수를 다른 변수에 할당하면 주소값이 복사되어 전달됨.
  • 변경 가능한 값(mutable value)임.

1. 값 자체를 저장 vs 주소값을 저장

🔸 원시 자료형을 변수에 할당하면 값 자체가 할당됨.

let num = 20;

  • 변수 num을 선언하면 컴퓨터는 num이라는 이름의 공간을 확보, 20이라는 원시 값 자체를 그 공간에 저장함.
    🔸 원시 자료형의 데이터가 저장되는 공간 스택(stack)

🔸 JavaScript에서는 특별한 저장 공간에 참조 자료형을 저장한 후, 그 저장공간을 참조할 수 있는 주소값을 변수에 저장.

let arr = [0, 1, 2, 3]

  • 변수 arr을 선언하면 arr에 해당하는 저장공간에는 주소값이 저장됨.
  • 이때 참조 자료형은 힙(heap)이라는 특별한 저장 공간에 저장됨.
  • 따라서 변수 arr에 해당하는 저장공간에는 주소값이 저장되어 있고, 그 주소값을 통해 참조 자료형에 접근, 이를 참조한다(refer)라고 함.

2. 원시 값 자체를 복사 vs 주소값을 복사

let num = 20;
let copiedNum = num;

🔸 원시 자료형은 값 자체가 복사되므로, 변수 num과 변수 copiedNum은 동일하게 20이라는 값을 가짐.
🔸 원본에 다른 값을 재할당해도 복사본에는 영향을 미치지 않음.

let arr = [0, 1, 2, 3];
let copiedArr = arr;

🔸 참조 자료형은 주소값을 복사하므로, 할당된 변수를 다른 변수에 할당하면, 두 변수는 같은 주소를 가리킴.
🔸 같은 주소를 참조하고 있기 때문에 원본을 변경하면 복사본도 영향을 받음.

3. 변경 불가능한 값 vs 변경이 가능한 값

🔸 한 번 생성된 원시 값은 변경할 수 없음. 변수에 다른 값을 재할당해도 원시 값 자체가 변경된 것이 아니라 새로운 원시 값을 생성하고, 변수가 다른 메모리 공간을 참조함.

let num = 20;
num = 30; 
  • num이라는 변수를 선언하고 숫자 20을 할당한 뒤, 30을 변수에 재할당하면, 메모리 내부에서는 30이라는 원시 값을 저장하기 위한 새로운 공간을 확보.
  • 이후 그 공간에 num이라는 이름을 붙이고 30을 저장함. 남아 있는 값 20은 JavaScript 엔진이 사용하지 않는 값을 자동으로 메모리에서 삭제함.

🔸 이렇게 사용하지 않는 값을 자동으로 메모리에서 삭제하는 기능을 가비지 콜렉터(garbage collector) 라고 함 (하지만 이러한 동작이 어느 시점에 진행되는지 예측할 수 없음).
🔸 따라서 원시 자료형은 어떤 상황에서도 불변하는 읽기 전용 데이터로 높은 신뢰성을 가질 수 있음.

🔸 원시 자료형의 경우 값의 크기가 거의 일정하기 때문에 새로운 공간을 확보하여 값을 복사하는 방법이 유용. 하지만, 크기가 일정하지 않는 참조 자료형의 경우 매번 값을 복사하면, 그만큼 효율성이 떨어질 수 밖에 없음.
🔸 이러한 이유로 참조 자료형은 변경이 가능하도록 설계됨.

📌 얕은 복사 (shallow copy)

🔸 원시 자료형을 할당한 변수를 다른 변수에 할당하면, 값 자체의 복사가 일어나기 때문에, 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않음.
🔸 참조 자료형이 할당된 변수를 다른 변수에 할당하면 주소가 복사되어 원본과 복사본이 같은 주소를 참조하고, 주소값을 복사한 변수에 요소를 추가하면 같은 주소를 참조하고 있는 원본에도 영향을 미침.

참조 자료형을 복사하여, 똑같은 요소와 프로퍼티를 가지지만 원본과 복사본이 서로 영향을 미치지 않도록 할 수는 없을까?

배열 복사하기

🔸 slice() : 배열 내장 메서드로 원본 배열을 복사할 수 있음.

let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
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]
  • 새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하고 있는 주소는 다름. 따라서 복사한 배열에 요소를 추가해도 원본 배열에는 추가되지 않음.

🔸 spread문법 : ES6에서 새롭게 추가된 문법으로, 배열을 펼칠 수 있음. 펼치는 방법은 배열이 할당된 변수명 앞에 ...을 붙이며, 배열을 펼치면 배열의 각 요소를 확인할 수 있음.

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]
  • 새로운 배열 안에 원본 배열을 펼쳐서 전달하면 원본 배열과 같은 요소를 가지고 있지만 각각 다른 주소를 참조. 결과적으로 slice() 메서드를 사용한 것과 동일하게 동작.

객체 복사하기

🔸 Object.assign(target, 복사할 값)

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 = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);

console.log(target);
// Expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget === target);
// Expected output: true

🔸 spread syntax

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 syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없음.
🔸 참조 자료형이 몇 단계로 중첩되어 있던지, 위에서 설명한 방법으로는 한 단계까지만 복사할 수 있음.

let copiedUsers = users.slice();
console.log(users === copiedUsers); 
// false;  각각 다른 주소를 참조하고 있기 때문
console.log(users[0] === copiedUsers[0]); 
// true; users[0]과 copiedUsers[0]는 여전히 같은 주소값을 참조하고 있기 때문

🔸 이처럼 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의 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

CH2. 스코프 (Scope)

📌 스코프의 주요 규칙

🔸 스코프 (Scope) : 변수가 정의될 때 해당 변수가 접근 가능한 범위.
🔸 바깥쪽 스코프에서 선언한 변수는 안쪽 스코프에서 사용 가능하지만, 안쪽 스코프에 선언한 변수는 바깥쪽 스코프에서 사용 불가능 (이 경우 RefereneceError가 나타남).
🔸 중첩이 가능함.

🔸 가장 바깥의 스코프는 전역 스코프(Global scope)라고 부름. 반대는 지역 스코프(Local scope).
🔸지역 변수는 전역 변수보다 더 높은 우선순위를 가짐.
🔸 동일한 변수 이름으로 인해 바깥쪽 변수가 안쪽 변수에 의해 가려지는(shadow) 현상을 쉐도잉(variable shadowing)이라고 부름.

📌 변수 선언과 스코프

블록 스코프 (block scope)

🔸 중괄호({})로 둘러싼 범위 (if, for, switch 등). 블록 스코프 안에서 정의된 변수는 블록 범위를 벗어나는 즉시 접근할 수 없음.
🔸 그러나 var 키워드는 블록 스코프를 무시하기 때문에, 블럭 범위를 벗어나도 사용 가능.
🔸 화살표 함수는 블록 스코프로 취급됨.

함수 스코프 (function scope)

🔸 함수 function으로 둘러싼 범위 (함수 표현식, 함수 선언식).
🔸 var 키워드는 함수 스코프를 따르고 (화살표 함수, 함수 표현식, 함수 선언식), 함수 스코프의 최상단에 선언됨.
🔸 함수 스코프는 함수의 실행부터 종료까지이고, 함수 내에서 선언 키워드 없는 선언은 최고 스코프에 선언되며, 함수의 실행 전까지 선언되지 않은 것으로 취급.

var 키워드와 let 키워드

🔸 블록 단위로 스코프를 구분했을 때, 훨씬 더 예측 가능한 코드를 작성할 수 있으므로 var보단 let 키워드 사용이 권장됨.
🔸 var를 사용하지 않는다 해도, 함수 스코프는 let으로 선언된 변수의 접근 범위를 제한함.
🔸 var 키워드는 변수를 재선언해도 아무런 에러도 내지 않지만, let 키워드는 재선언하는 경우, 대부분 버그를 일으킴.

const

🔸 블록 스코프를 따름
🔸 값의 변경을 최소화하여 보다 안전한 프로그램을 만들 수 있음. 값을 새롭게 할당할 일이 없다면, const 키워드의 사용이 권장됨.
🔸 값의 재할당이 불가능. 값을 재할당할 경우, TypeError를 내므로, 의도하지 않은 값의 변경을 막을 수 있음.

📌 변수 선언할 때 주의할 점

window 객체 (브라우저 only)

  • 브라우저에는 window라는 객체가 존재함. 브라우저 창을 대표하는 객체로 브라우저 창과 관계없이 전역 항목도 담고 있음.
  • var로 선언된 전역 변수와 전역 함수가 window 객체에 속함. 함수 선언식으로 함수를 선언하거나, var로 전역 변수를 만들면, window 객체에서도 동일한 값을 찾을 수가 있습니다.
var myName = '김코딩';
console.log(window.myName); // 김코딩

function foo() {
	console.log('bar');
}
console.log(foo === window.foo); // true;

전역 변수는 최소화하는 것이 좋다.

  • 전역 변수는 가장 바깥 스코프에 정의한 변수이므로, 어디서든 접근이 가능.
  • 편리한 대신, 다른 함수 혹은 로직에 의해 의도되지 않은 변경이 발생할 수 있음. 부수 효과(side effect) 발생

let, const를 주로 사용하라.

  • var는 블록 스코프를 무시하며, 재선언을 해도 에러를 내지 않음 (같은 스코프에서 동일한 이름의 변수를 재선언하는 것은 버그를 유발).
  • 전역 변수를 var로 선언하는 경우 문제가 될 수 있음 (var로 선언한 전역 변수가 window 기능을 덮어씌워서 내장 기능을 사용할 수 없게 만들 수 있음).

선언 없는 변수 할당 금지.

  • 선언 키워드(var, let, const)없이 변수를 할당할 시, 해당 변수는 전역 변수처럼 작동함.
  • Strick Mode는 브라우저가 엄격하게 작동하도록 만들어, "선언 없는 변수 할당"의 경우 에러로 판단함. 이를 적용하려면 js 파일 상단에 'use strict'라고 입력하면 됨 (문법적으로 실수할 수 있는 부분들을 에러로 판단함).

CH3. 클로저 (closure)

📌 클로저 기초

🔸 함수와 함수가 선언된 어휘적(lexical) 환경의 조합. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성됨. (함수와 그 함수가 접근할 수 있는 변수의 조합)

🔸 자바스크립트는 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경, 즉 어휘적 환경을 기준으로 변수를 조회하려고 함.
🔸 이와 같은 이유로 "외부 함수의 변수에 접근할 수 있는 내부 함수"를 클로저 함수라고 함.
🔸 함수가 실행되는 동안 함수 내부에서 정의된 변수들을 계속 유지하고, 함수 외부에서도 접근 가능하게 만들어 줌.

클로저
1. 함수 outerFnouterFn에서 접근할 수 있는 globalVar
2. 함수 innerFninnerFn에서 접근할 수 있는 globalVar, outerFnVar

🔸 클로저가 중요한 이유 : 클로저의 함수는 어디에서 호출되느냐와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해지기 때문

const globalVar = '전역 변수';
function outerFn() {
  const outerFnVar = 'outer 함수 내의 변수';
  const innerFn = function () {
    return (
      'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.'
    );
  };
  return innerFn;
}
const innerFnOnGlobal = outerFn();
const message = innerFnOnGlobal();
console.log(message); // ?
  • innerFnOnGlobalouterFn 내부의 innerFn의 주소값을 가짐.
  • 이때, innerFnOnGlobalinnerFn 밖에 있기 때문에 outerFnVar에는 접근하지 못한다고 생각할 수 있지만, innerFnOnGlobalinnerFn의 주소값을 가지고 있고, innerFn은 클로저로서 outerFnVar에 접근할 수 있기 때문에 innerFnOnGlobalouterFnVar에 접근할 수 있음.
  • 이 “환경”을 어휘적 환경(Lexical Environment)라고 함.
  • 만약 클로저가 JS에 없는 개념이라면, outerFnVar에 접근할 수 없어 에러가 남.

📌 클로저 활용

데이터를 보존하는 함수

🔸 일반적으로 함수 내부에 선언한 변수와 매개변수에 접근할 수 없음.

function getFoodRecipe (foodName) {
  let ingredient1, ingredient2;
  return `${ingredient1} + ${ingredient2} = ${foodName}!`;
}

console.log(ingredient1); // ReferenceError: ingredient1 is not defined (함수 내부에 선언한 변수에 접근 불가)
console.log(foodName); // ReferenceError: foodName is not defined (매개변수에 접근 불가)

🔸 클로저를 응용하면, 함수 내부에 선언한 변수와 매개변수에 접근가능하여, 클로저의 함수 내에 데이터를 보존해 두고 사용할 수 있음.
🔸 기존 함수 내부에서 새로운 함수를 리턴하면 클로저로서 활용할 수 있음. 즉, 리턴한 새로운 함수의 클로저에 데이터가 보존됨.

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
  • sumcurrySum이 같은 값을 리턴하기 위해서는 currySum 함수에서 리턴한 함수에 두 번째 전달인자 20을 전달하여 호출.
  • 이렇게 커링을 활용한 currySum과 같은 함수를 커링 함수라고 부름.

🔸 전체 프로세스의 일정 부분까지만 실행하는 경우 유용.
🔸 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장하기에 용이.

function makePancakeAtOnce (powder, sugar, pan) {
  return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
}

const morningPancake = 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('후라이팬');
  • 커링이 적용되지 않은 makePancakeAtOnce 함수는 일부 조리 과정이 생략된 모습을 표현할 수 없음.
  • 반연에 makePancake 함수는 팬케이크 제작 과정을 커링 함수로 만들어 일부 조리 과정을 표현할 수 있음.

모듈 패턴

🔸 모듈: 하나의 기능을 온전히 수행하기 위한 모든 코드를 가지고 있는 코드 모음

  • 하나의 단위로서 역할을 함.
  • 기능 수행을 위한 모든 기능을 갖추고 있음. (다른 모듈에 의존적이지 않고 독립적)
  • 외부 코드 실행을 통해서 모듈의 속성이 훼손받지 않아야 함.

🔸 JS에 class 키워드가 없었을 때, 모듈 패턴을 구현하기 위해 클로저를 사용했음.
🔸 특정 데이터를 다른 코드의 실행으로부터 보호해야 할 때 용이.

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
  • displayValuemakeCalculator의 코드 블록 외에 다른 곳에서는 접근이 불가능하지만, cal의 메서드는 모두 클로저의 함수로서 displayValue에 접근할 수 있음.
  • 이렇게 데이터를 다른 코드 실행으로부터 보호하는 개념을 정보 은닉(information hiding)이라고 함.
  • 이는 캡슐화(encapsulation)의 큰 특징.

Summary

🔸 클로저를 이용하면 특정 함수가 데이터를 보존할 수 있음.
🔸 커링을 이용하면 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장할 수 있음.
🔸 모듈 패턴을 이용하면 특정 데이터를 다른 코드의 실행으로부터 보호할 수 있음.

CH4. ES6 주요 문법

현재까지 가장 최신 버전은 2019년에 출시된 ES2019이지만, 2015년에 출시된 ES6(ECMA Script6)에서 가독성과 유지보수성을 획기적으로 향상할 수 있는 문법이 많이 추가됨.

📌 spread/rest 문법

spread 문법

🔸 주로 배열을 풀어서 인자로 전달하거나, 배열을 풀어서 각각의 요소로 넣을 때에 사용. 객체 혹은 배열을 펼칠 수 있음.
🔸 기존 배열을 변경하지 않으므로(immutable) 새롭게 할당해야 함.

function sum(x, y, z) {
  return x + y + z;
}

const numbers = [1, 2, 3];

sum(...numbers) // 6;

rest 문법

🔸 파라미터를 배열의 형태로 받아서 사용할 수 있음. 파라미터 개수가 가변적일 때 유용.

function sum(...theArgs) {
  return theArgs.reduce((previous, current) => {
    return previous + current;
  });
}

sum(1,2,3) // 6;
sum(1,2,3,4) // 10;
  • Array.reduce(초기값, 현재 처리할 요소) : 배열의 모든 요소에 대해 주어진 함수를 순차적으로 실행하면서 하나의 결과값을 생성.

배열에서 사용하기 (spread 문법)

🔸 배열 합치기

let parts = ['shoulders', 'knees'];
let lyrics = ['head', ...parts, 'and', 'toes'];
console.log(lyrics); // (5) ['head', 'shoulders', 'knees', 'and', 'toes']
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1 = [...arr1, ...arr2]; // (6) [0, 1, 2, 3, 4, 5]

🔸 배열 복사

let arr = [1, 2, 3];
let arr2 = [...arr]; // arr.slice() 와 유사
arr2.push(4); 
// arr1 = [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 };
// clondedObj = { foo: 'bar', x: 42 };
// mergeObj = { foo: 'baz', x: 42, y: 13 };

함수에서 나머지 파라미터 받아오기

function myFun(a, b, ...manyMoreArgs) {
  console.log("a", a); // a one
  console.log("b", b); // b two
  console.log("manyMoreArgs", manyMoreArgs); //manyMoreArgs [ 'three', 'four', 'five', 'six' ]
}

myFun("one", "two", "three", "four", "five", "six");
// a one
// b two
// manyMoreArgs (4) ['three', 'four', 'five', 'six']

전개 구문

Rest 파라미터

📌 구조분해할당

🔸 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식.
🔸 spread 문법을 이용하여 값을 해체한 후, 개별 값을 변수에 새로 할당하는 과정.

분해 후 새 변수에 할당

🔸 배열

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};
  • 객체에서 구조 분해 할당을 사용하는 경우, 선언(const, let, var)과 함께 사용하지 않으면 에러가 발생할 수 있음.

선언 없이 할당할 경우, 해당 문서 참고

var a, b;
({ a, b } = { a: 1, b: 2 });
  • {a, b} = {a:1, b:2}는 유효한 독립 구문이 아님. 좌변의 {a, b}이 객체 리터럴이 아닌 블록으로 간주되기 때문.
  • 하지만 () 내에 작성할 경우, var {a, b} = {a:1, b:2}와 같음.

함수에서 객체 분해

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 Doe

📌 화살표 함수 (arrow function)

// 함수선언문
function sum (x, y) {
	return x + y;
}

// 함수표현식
const subtract = function (x, y) {
	return x - y;
}

// 화살표 함수
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
}

fe-sprint-javascript-koans

📌 자바스크립트 테스트 프레임워크 Chai

it("테스트 케이스 설명", function () {
	// 테스트 코드
    // 예상 결과와 실제 결과를 비교하여 테스트 수행
  }
)

🔸 expect(테스트하는값).matcher : 테스트하는 값과 기대값을 비교.

  • matcher : 기대하는 조건
    • .equal : 두 값이 타입까지 엄격하게 같은지 검사.

📌 const 사용 이유?

🔸 불변셩(immutability) 유지 : const는 변수에 재할당을 허용하지 않기 떄문에, 선언된 이후 값이 변경되지 않음. 이로인해, 코드의 예측 가능성이 높아지고, 코드를 이해하고 유지보수하기 쉬워짐.
🔸 실수 방지 : 실수로 값이 변경되는 경우를 방지해 코드의 안정성을 높임.
🔸 코드 최적화 : 일부 JS 엔진은 const를 사용한 변수를 상수로 취급하고 더 효율적으로 최적화할 수 있음. 이는 런타임 최적화와 관련이 있으며, 코드 실행 속도를 높일 수 있음.
🔸 버그 예방 : 의도치 않은 값 변경으로 인한 버그를 방지할 수 있음.
🔸 의미 전달 : 해당 변수는 변경되지 않을 것이라는 의미를 코드에서 명확하게 전달하여 변수의 용도와 특성을 명확히 전달하는데 도움을 줌.

📌 클로저

🔸 자바스크립트는 함수가 호출되는 환경와 별개로, 기존에 선언되어 있던 환경(어휘적 환경)을 기준으로 변수를 조회하려고 함.
🔸 이는 외부함수의 변수에 접근할 수 있는 내부함수를 클로져 함수로 부르는 이유가 됨.

📌 function factories

🔸 함수 내부에서 객체를 동적으로 생성하고 반환하는 함수로, 처음 함수를 호출할 때만 인수를 전달하고 이후 함수 내부에 있는 객체에 접근할 때 인수전달이 필요하지 않고 필요시 프로퍼티에 접근해서 값을 수정할 수 있음.
🔸 객체 생성에 필요한 로직을 모듈화하고, 재사용성을 높일 수 있음.

function createCounter() {
  let count = 0; // 클로저에 의해 기억되는 변수

  return function() {
    count++;
    console.log(count);
  };
}

const counterA = createCounter();
counterA(); // 1
counterA(); // 2

const counterB = createCounter();
counterB(); // 1 (새로운 클로저 생성)

📌 namespacing private variables/functions (정리중..)

🔸 네임스페이스(namespace) : 구분이 가능하도록 정해놓은 범위나 영역. 이름 공간을 선언하여 다른 공간과 구분하도록 하는 것.
🔸 네임스페이싱(namespacing) : 객체나 변수가 겹치지 않는 안전한 소스코드를 만드는 것.

  • 외부에서 접근이 불가능한 변수와 함수를 정의하여, 전역 스코프의 오염을 방지하는 데 사용.
const myNamespace = (function () {
  // 프라이빗 변수
  let privateVar = "I am private!";

  // 프라이빗 함수
  function privateFunction() {
    console.log("This is a private function!");
  }

  // 네임스페이싱된 오브젝트를 반환
  return {
    publicVar: "I am public!",
    publicFunction: function() {
      console.log("This is a public function!");
      // 프라이빗 변수 및 함수에 접근 가능
      console.log(privateVar);
      privateFunction();
    }
  };
})();

// 외부에서는 privateVar와 privateFunction에 접근할 수 없음
console.log(myNamespace.publicVar); // "I am public!"
myNamespace.publicFunction(); // "This is a public function!" 및 private 변수 및 함수 출력

객체 리터럴 네임스페이싱(Object Literal Namespacing)

🔸 가장 기본적인 방식으로, 하나의 전역 객체를 만든 후 모든 함수, 객체, 변수를 여기에 추가하여 구현.

// 하나의 전역 객체
var MYAPP = {};

MYAPP.Parent = function() { console.log('Parent'); };
MYAPP.Child = function() { console.log('Child'); };
// 객체 컨테이너
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.Parent();                               // Parent 출력
console.log(MYAPP.modules.module1.data.a);    // 1 출력
MYAPP.Child();                                // Child 출력

🔸 단점

  • 모든 변수와 함수에 상위 객체 명을 모두 붙여야하기 때문에 소스 코드량이 늘어나 파일의 크기가 증가.
  • 전역 인스턴스가 하나뿐이기 때문에 코드의 어느 한 부분이 수정되어도 전역 인스턴스를 수정하게 됨. 계속해서 나머지 기능들도 갱신된 상태를 물려받게 됨.
  • 매번 객체에 접근하며, 이름이 중첩되고 길어지므로 검색이 느려지게 됨.

💡 namespace로 사용되고 있는 객체를 this를 사용하여 참조할 수 없음.
🔸 함수 영역 안에 있는 this 키워드는 부모의 자식으로 불렸을 때만 그 부모객체를 가리키고, 직접 호출하였을 때는 더 이상 부모객체가 아닌 전역 객체(브라우저 환경에서는 window)를 가리켜 undefined가 반환됨.

var MYAPP = {};
MYAPP.message = "Hi";
MYAPP.sayHello = function() {
	// this를 사용하여 리턴
	return this.message;
};
console.log(MYAPP.sayHello());    // Hi 출력
var direct = MYAPP.sayHello;
console.log(direct());            // undefined 출력

✔️ 이러한 단점을 해결하기 위해 샌드반스 패턴(Sandbox Pattern)을 사용함

샌드박스 패턴(Sandbox Pattern)

🔸 생성자를 유일한 전역으로 사용하며, 유일한 전역인 생성자에게 콜백 함수(Callback function)를 전달해 모든 기능을 샌드박스 내부 환경으로 격리 시키는 방법을 사용.

범용 네임스페이스 함수

🔸 프로그램이 복잡해짐에 따라, 코드의 각 부분들이 별개의 파일로 분리되어 선택적으로 문서에 포함되는 경우가 많음.
🔸 네임스페이스로 사용할 객체를 선언할 때나, 그 내부의 프로퍼티를 정의 할 때, 이미 있는 것을 재정의하는 일도 생길 수 있음.
🔸 따라서 미리 선언되었는지를 확인하고 정의해야 함.

if(typeof MYAPP === "undefined") {
	var MYAPP = {};
}
// 혹은
var MYAPP = MYAPP || {};

📌 화살표 함수를 이용해 클로저 표현하기

const adder = x => {
      return y => {
        return x + y
      }
    }
adder(50)(10) // 60

const subtractor = x => y => {
      return x - y
    }  
subtractor(50)(10) // 40

const htmlMaker = tag => textContent => `<${tag}>${textContent}</${tag}>`
const liMaker = htmlMaker('li')
liMaker('1st item') // `<li>1st item</li>`

📌 참조 자료형

const overTwenty = ['hongsik', 'minchul', 'hoyong'];
    let allowedToDrink = overTwenty;

    overTwenty.push('san');
    expect(allowedToDrink).to.deep.equal(['hongsik', 'minchul', 'hoyong', 'san']);
    overTwenty[1] = 'chanyoung';
    expect(allowedToDrink[1]).to.deep.equal('chanyoung');

.equal아닌 .deep.equal을 사용하는 이유?

  • .equal는 간단한 값(원시 자료형)의 비교에 사용되며, 값이 정확하게 일치해야 함.
  • .deep.equal : 객체나 배열과 같은 복합 자료형에 사용되며, 요소나 속성 값들이 일치해야 함.
  • 따라서, 배열에 .equal을 사용하게 되면 두 배열이 정확히 동일한 객체여야 하기 때문에 그 배열의 요소가 같은지 비교하려면 .deep.equal을 사용해야 함.

🔸 JS에서 객체를 비교할 때, === 연산자는 참조(주소)를 비교함. 따라서 객체의 내용이 같더라도 서로 다른 객체일 경우 === 비교연산자는 항상 false를 반환함.
🔸 Array를 함수의 전달인자로 전달할 경우, reference가 전달됨.

call(pass) by value와 call(pass) by reference의 차이

🔸 함수 호출 시 인자가 어떻게 전달되는지를 나타내는 개념으로
🔸 call(pass) by value (값에 의한 호출)

  • 함수에 값을 전달할 때, 해당 값의 복사본이 전달됨.
  • 함수 내에서 매개변수 값을 변경해도 원본 값은 변경되지 않음.
  • JS의 원시 자료형의 방식.

🔸 Call by Reference (참조에 의한 호출) or call by sharing

  • 함수에 변수의 참조(주소)가 전달.
  • 함수 내에서 매개변수를 통해 해당 변수를 직접 변경하면, 원본 값도 변경.
  • JS의 참조 자료형의 방식.

📌 객체

'this'는 method를 호출하는 시점에 결정됨.

🔸 method는 '어떤 객체의 속성으로 정의된 함수' (method는 항상 '어떤 객체'의 method)
🔸 전역 변수에 선언한 함수도 브라우저 환경에서는 window 객체의 속성으로 정의된 함수라고 할 수 있음. window. 접두사 없이도 참조가 가능하기 때문에(window.foo()라고 사용해도 됨), 생략하고 쓰는 것뿐
🔸 따라서 호출될 때마다 어떠한 객체의 method일 텐데, 그 '어떠한 객체'를 묻는 것이 this.

🔸 반면에, 화살표 함수는 자신의 this가 없음. 화살표 함수는 함수가 정의된 시점에서 외부 스코프(lexical context)의 this 값을 가져옴. 이것을 Lexical scoping이라고 함.
🔸 전역 범위에서 정의된 화살표 함수의 this는 전역 객체. (보통 브라우저 환경에서는 window, Node.js 환경에서는 global.)
🔸 이러한 특성 때문에, 화살표 함수는 주로 콜백 함수나 간단한 익명 함수 등에서 사용되며, 자체적인 this가 필요하지 않은 상황에서 효과적으로 사용됨.
🔸 this 참고 자료

얕은 복사(shallow copy)와 깊은 복사(deep copy)에 대해서 학습

🔸1
🔸2

arguments와 rest parameter를 통해 배열로 된 전달인자(args)의 차이

// rest parameter
function getAllParamsByRestParameter(...args) {
      return args;
    }

// arguments (spread syntax 도입 이전에 함수의 전달인자들을 다룰 때 사용. 모든 함수의 실행 시 자동으로 생성되는 '객체')
function getAllParamsByArgumentsObj() {
      return arguments;
    }
    
const restParams = getAllParamsByRestParameter('first', 'second', 'third');
const argumentsObj = getAllParamsByArgumentsObj('first', 'second', 'third');

expect(Array.isArray(restParams)).to.deep.equal(true);
expect(Array.isArray(argumentsObj)).to.deep.equal(false);
  • ...args는 rest parameter로 함수에 전달된 모든 인자들을 항상 배열로 수집함. 따라서 함수가 호출될 때 넘겨진 모든 값들이 배열 형태로 args 변수에 저장됨. 반환값은 이 배열.
  • arguments는 함수 내에서 항상 사용 가능한 특별한 객체로, 함수에 전달된 모든 인자들을 수집함. 배열이 아닌 유사 배열 객체로 배열 메서드를 직접 사용할 수 없음. 반환값은 arguments 객체 자체.
  • argument를 배열로 변환하고자 한다면,
    • Array.from(arguments)
    • [...arguments]

구조 분해 할당

🔸 할당하기 전 왼쪽에는, rest 문법 이후에 쉼표가 올 수 없음.

const [first, ...middle, last] = array // X

🔸 객체의 단축(shorthand) 문법


const name = 'John';
const age = 30;

const person = {
  name: name,
  age: age,
};
// 단축 문법
const person = {
  name,
  age,
};
  • 프로퍼티의 이름과 값의 이름이 동일하므로, 단축 문법을 사용할 수 있음.
  • 자동으로 프로퍼티의 이름이 변수의 이름으로 설정되며, 값도 변수의 값으로 설정됨.

추가

호이스팅 (Hoisting)

🔸 변수나 함수를 선언했을 때 코드 범위(scope) 내의 최상단으로 끌어올려지는 현상.
🔸 호이스팅은 스코프 단위로 일어남

var의 생성 과정

  1. 선언 + 초기화
  2. 할당

🔸 선언초기화가 동시에 일어나 할당 전에 호출하면 에러를 내지 않고 undefined가 발생

let의 생성 과정

  1. 선언
  2. 초기화
  3. 할당

🔸 선언 다음에 초기화가 일어나 호이스팅되면서 선언이 일어나지만, 초기화단계는 실제 코드에 도달했을 때 일어나기 때문에 에러를 발생시킴
🔸 변수가 호이스팅 되기는 하지만, 그 값이 할당되지 않을 경우에는 사용불가

  • let은 TDZ의 영향을 받지 않음
let num = 10
function showNum() {
	console.log(num) // 10
}
  • 정상적으로 10을 출력
let num = 10
function showNum() {
	console.log(num) // Error
	let num = 20
}
  • 함수 내부에 있는 let이 호이스팅이 일어나 최상단에 끌어올려지지만, 변수 선언(let num)만 끌어올려지고 값의 초기화는 이루어지지 않았기 때문에 에러를 발생시킴.
  • 만약 호이스팅이 없었으면 함수 바깥에 있는 10을 출력.

const의 생성 과정

  1. 선언 + 초기화 + 할당

🔸 선언과 할당이 동시에 됨. 따라서 재할당시 에러를 일으킴

함수의 생성 과정 (정리중..)

  1. 선언 + 초기화

🔸 함수 표현식 : 호이스팅 ❌. (변수 선언부만 호이스팅함)
🔸 함수 선언문 : 호이스팅 ⭕. (함수 전체를 호이스팅함)

TDZ (Temporal Dead Zone)

🔸 일시적인 사각지대로, 변수를 사용하는 것을 비허용하는 개념상의 공간.
🔸 TDZ에 있는 변수는 선언은 되었지만 아직 초기화가 되지 않아서 변수에 담길 값을 위한 공간이 메모리에 할당되지 않은 상태.
🔸 var는 TDZ에 영향을 받지 않으나, letconst는 영향을 받아 TDZ에 있는 값에 접근하면, ReferenceError 발생

profile
코린이👽

0개의 댓글