[서적 정리] 모던 자바스크립트 Deep Dive 정리하기

박하민·2023년 8월 18일
post-thumbnail

++ 2023.08.28
모던 자바스크립트 Deep Dive 책을 혼자서 읽던 중 우연히 스터디에 가입할 기회가 생긴 관계로 앞으로는 Github와 블로그에 동일하게 정리한 내용을 업로드할 생각이다. 기존 내용들은 정리가 끝나면 지울 계획이다.

사실 책의 내용을 정리한다는 것 자체가 과연 좋은 효율을 낼 수 있을지 의문이긴 하다. 책으로 배운 내용을 실전에 활용하는 것이 가장 좋지만 제가 정리를 시작하게 된 계기는 스스로를 귀찮게 하기 위해서다. 이 두꺼운 책을 읽으면서 아이패드에 적고 블로그에 정리하는 과정에서 매우 많은 시간이 소모될 것으로 예상되지만, 이 과정에서 득이 되는 부분이 더 많다고 생각하기 때문이다. 하지만 너무 시간이 소모되면 안 되기 때문에 적절한 시간 배분이 중요할 것 같다.



04장 변수

변수 - 변하는 값 <-> 상수

  • 하나의 값을 저장하기 위해 확보한 메모리 공간 또는 메모리 공간 식별을 위해 붙인 이름
  • 변수 이름으로 참조를 요청하면 JS 엔진은 변수 이름과 매핑된 메모리 주소를 통해 저장된 값을 반환
  • 개발자의 의도를 나타내는 명확한 네이밍이 필요하다.
  • 변수의 이름은 식별자의 역할을 수행하며, 값이 아닌 메모리 주소를 기억한다.

예시 - var name = hamin

주소
0x000000000000 0000
  • name 변수는 주소 0x00000000와 매핑되고 값에 할당된 값(hamin)을 저장
  • 변수(식별자)를 참조하면 값(hamin)을 반환한다.

변수 선언

  • 변수를 생성하는 것이며 값을 저장하기 위해 메모리를 확보
  • 확보된 메모리는 해제되기 전까지 보호됨
  • 선언 키워드로 var, let, const가 존재한다.


변수 선언 단계

  1. 선언 단계
  • 변수 이름을 등록해 JS 엔진에 변수 존재를 알린다.

  1. 값 저장을 위해 메모리 공간을 확보하고 undefined를 할당해 초기화
  • 값 초기화를 단계가 없을 경우 메모리 공간에 이전 값이 남아있을 수 있다(쓰레기 값)




📌 var 키워드의 변수 선언은 선언 단계와 초기화 단계가 동시에 진행

  • 선언과 동시에 변수 이름 등록과 undefined로 초기화된다.



호이스팅 - 변수 선언문이 코드 선두로 끌어 올려진 것처럼 동작하는 JS 고유의 특징

 console.log(hamin); // undefined   -> 런타임 전에 변수 hamin을 undefined로 초기화
 var hamin = 'man';
  • JS의 변수 선언은 런타임 이전 단계에서 실행
  • 소스코드 위치의 영향 받지 않음

📍 소스코드 평가 과정에서 JS 엔진은 변수 선언을 포함한 모든 선언문(변수, 함수 선언문 등)을 소스코드 내에서 찾아 실행

  • 이 과정에서 undefined로 초기화된다.

📍 평가 과정이 끝나면 소스코드를 한 줄씩 순차적으로 실행

  • 런타임 때 코드가 순차적으로 실행되며 할당돼야 할 값이 있다면 사전에 undefined로 할당된 값에 재할당.

image

  • ⭐️기존 메모리 주소에 재할당 되는 것이 아닌 새로운 메모리 주소에 저장된다.⭐️
  • 어떤 식별자와도 연결되지 않는 기존 값들은 가비지 콜렉터에 의해 메모리에서 자동 해제된다.



05장 표현식과 문

값과 리터럴

📍 값

  • 식(표현식)이 평가되어 생성된 결과

📍 리터럴

  • 사람이 이해할 수 있는 문자 또는 약속된 기호를 사용해 값을 생성하는 표기법
  • 아라비아 숫자, 알파벳, 한글 또는 약속된 기호 ('', "", [], {} 등)
  • 값으로 평가됨

표현식

  • 표현식은 값으로 평가될 수 있는 문을 말한다.
var score = 100; // JS엔진에 의해 리터럴 100이 값으로 평가된다.

var score = 50 + 50; // 50 + 50도 평가되어 숫자 100을 생성하는 표현식

score; // 식별자 참조 -> 값은 없지만 값으로 평가

📌 0개 이상의 문을 중괄호로 묶은 코드 블록({...}) 뒤에는 세미콜론 X

  • 자체 종결성을 가진다.
  • if 문, for 문, 함수 등
변수 선언문
var x;

할당문
x = 5;

함수 선언문
function foo () {}

조건문
if()

반복문
for()

표현식 문과 표현식이 아닌 문

  • 표현식인 문은 값으로 평가된다. 즉, 가장 간단한 구별 방법은 변수에 할당하면 된다.
var x;  // 표현식이 아닌 선언문

var foo = var x // SyntaxError: Unexpected token var 즉, 선언문은 값처럼 사용 불가

완료 값 - feat. 개발자 도구

  • 크롬 개발자 도구에서 표현식 문은 완료 값을, 아닌 문은 undefined를 출력한다.
    📌 선언문, 조건문



    📌 표현식인 문



06장 데이터 타입

📍 데이터 타입 === 값의 종류

구분데이터 타입설명
원시 타입숫자숫자, 정수, 실수
문자열문자열
불리언참(true)와 거짓(false)
undefiendvar 키워드로 선언된 변수에 암묵적 할당되는 값
문자열문자열
문자열문자열
----------------------------------------------------------
객체 타입객체, 배열, 함수

1과 '1'

  • 목적과 용도가 다른 엄연히 다른 값이다.
  • 저장 방식의 차이가 존재
    🔍 확보해야 할 메모리 공간의 크기, 저장되는 2진수도 다르다.

숫자 타입

📌 숫자 타입의 값은 배정밀도 64비트 부동소수점 형식을 따른다.

  • 모든 수를 실수로만 처리한다.
    -> 정수로 표시된다 해도 실수이다.
    -> 정수로 표시되는 수끼리 나눠도 실수가 나올 수 있다.

  • 정수, 실수, 2진수, 8진수, 16진수 리터럴은 메모리에 배정밀도 64비트 부동소수점 형식으로 저장

  • 2진수, 8진수, 16진수 표현을 위한 데이터 타입 존재 x -> 참조 시 10진수로 해석

📌 세 가지 특별한 값

Infinitity: 양의 무한대
-Infinitity: 음의 무한대
NaN: 산술 연산 불가


문자열 타입

  • JS 문자열은 원시 타입 -> 변경이 불가능한 값
  • 줄바꿈(개행) 허용 x

템플릿 리터럴

  • 멀티라인 문자열
var name = `ha
min`
  • 표현식 삽입
var name = 'hamin';

console.log(`My name is ${name}`) // My name is hamin

console.log(`1 + 2 = ${1 + 2}`) // 3

📌 자주 사용되는 이스케이프 시퀸스

  • \n: 줄바꿈
  • \t: 탭(수평)
  • \': 작은 따옴표
  • \": 큰 따옴표
  • \: 백슬래시

불리언 타입

  • 논리적 참과 거짓을 나타내는 true와 false 존재
  • 조건문에서 주로 사용

undefined 타입

  • 유일한 값 undefined만 존재
  • var 키워드로 선언한 변수에 초기화 값으로 할당됨
  • 의도적으로 초기 비어있는 값을 설정한다면 null 사용할 것

null 타입

  • 대소문자 구분 (null, Null, NULL과 다르다.)
  • 변수에 값이 없다는 걸 의도적으로 명시할 때 사용

심벌 타입

  • 다른 값과 중복되지 않는 유일한 값
    -> 이름 충돌 위험이 적어 객체의 유일한 프로퍼티 키를 만들 때 사용

  • 함수를 호출해서 생성된다.
    -> 생성된 값은 외부에 노출 x
    -> 다른 값과 중복 x

var key = Symbol('key');
console.log(typeof key); // symbol

데이터 타입의 필요성

  • 값을 메모리에 저장할 때
    📍 저장을 위해서는 확보할 공간 결정이 필요하다. (낭비와 손실을 줄이기 위해)
    📍 데이터 타입에 따라 메모리 저장 크기가 다르다.
var score = 100;
  1. 100을 저장하기 위한 메모리 확보
  2. 100을 2진수의 형태로 저장
  • 값을 메모리에서 참조할 때
    📍 한 번에 읽어 들어야 할 메모리 공간의 크기, 즉 메모리 셀의 개수(바이트 수)가 필요하다
    📍 JS는 숫자 타입을 값으로 가지기 때문에 숫자 타입으로 인식되고
    📍 숫자 타입의 경우 8바이트 단위로 저장되기 때문에 참조 시 8바이트 단위로 메모리에 공간에 저장된 값을 읽어 들인다.

  • 참조한 값(2진수)를 해석할 때
    📍 score에 저장된 값의 2진수 형태 = 0100 0001이라고 가정하면,
    문자열로 해석하면 'A', 숫자로 해석하면 65가 된다.
    📍 이 경우에도 score 변수의 타입을 통해 어떤 타입으로 해석할지 결정한다.


📌 데이터 타입의 필요성 정리
1. 저장할 때 메모리 공간 크기 결정을 위해
2. 참조할 때 읽어 들일 메모리 공간 크기 결정을 위해
3. 읽어들인 2진수의 메모리 값 해석 결정을 위해


동적 타이핑

정적 타입 언어동적 타입 언어
C, 자바, 코틀린, Go자바스크립트, 파이썬
  • 정적타입 언어

    📍 변수 선언 시 변수에 할당할 수 있는 값의 데이터 타입을 사전 선언 필요 -> 명시적 타입선언
    📍 타입 변경 불가
    📍 컴파일 시점에 타입 체크 -> 런타임에 발생하는 에러 줄임
  • 동적 타입 언어

    📍 변수 타입 선언은 하지 않는다.
    📍 다른 데이터 타입의 값도 할당 가능

  • 동적 타입 언어 JS

    📍 JS는 var, let, const 키워드와 함께 변수 선언
    📍 변수 선언이 아닌 할당에 의해 타입 결정(타입추론)됨.
    📍 편리할수도 있지만, 변화하는 변수 값 추적이 어려운 위험성이 있다
    -> 유연성은 높지만 신뢰성이 낮다.


📌 typeof 연산자

  • 변수에 할당된 값의 데이터 타입 반환
    console.log(typeof 변수명) // 변수의 타입 출력



07장 연산자

📌연산자
하나 이상의 표현식을 대상으로 산술, 할당, 비교, 논리, 타입, 지수 연산등을 수행하여 하나의 값을 만든다.

산술 연산자

  • 피연산자를 대상으로 수학적 계산을 수행해 새로운 값을 만든다.
  • 산술 연산이 불가능한 경우 NaN 반환

이항 산술 연산자

  • 2개의 피연산자를 사용

    +, -, *, /, %


단항 산술 연산자

📍 1개의 피연산자를 사용
📍 증가/감소 연산자는 피연산자의 값을 변경하는 부수 효과가 있다.

++, --, +, -


타입 변환

📍 암묵적 타입 변환(타입 강제 변환)

+, - 단항 연산자

var x = '1';

문자열을 숫자로 타입 변환
console.log(+x); // 1

x = true;

불리언 타입도 변환
console.log(+x); // 1

x = false;

console.log(+x); // 0

x = 'hamin';

console.log(+x); // NaN

console.log(-(-10)); // 10

문자열 연결 연산자

'1' + 2 = '12'

1 + true = 2

1 + undefined // NaN

할당 연산자

할당 연산자동일 표현
=x = 5x = 5
+=x += 5x = x + 5
-=x -= 5x = x - 5
*=x *= 5x = x * 5
/=x /= 5x = x / 5
%=x %= 5x = x % 5

연쇄 할당

  • 오른쪽에서 왼쪽으로 진행
a = b = c = 0;

console.log(a, b, c); // 0 0 0

비교 연산자

동등 비교 (==) vs 일치 비교 (===)

📍 일치 비교와 동등 비교는 연산자는 좌항과 우항의 피연산자를 비교는 동일하다.
📍 동등 비교는 암묵적 타입 변환을 통해 타입을 일치 시킨 후 값을 비교한다. 정확한 타입과 값을 비교하기 위해서는 반드시 일치 비교를 사용해야 한다.

5 == 5 // true
5 == '5' // true 

false == 'false' // true

5 === 5 // true
5 === '5' // false

false === 'false' // false

📌 NaN - 자신과 일치하지 않는 유일한 유형
📍NaN을 조사하기 위해서는 Number.isNaN을 사용해야 한다.

NaN === NaN; // false
Number.isNaN(NaN); // true
Number.isNaN(10) // false

📌 0 - 양의 0과 음의 0이 존재하고 이둘을 비교하면 true를 반환

0 === -0 // true
0 == -0 // true

삼항 조건 연산자

  • 값으로 평가할 수 있는 표현식인 문

논리 연산자

|| - 논리합(OR)
&& - 논리곱(AND)
! - 부정(NOT


쉼표 연산자

  • 왼쪽 피연산자부터 차례대로 피연산자를 평가하고 마지막 피연산자 평가가 끝나면 반환
var x, y, z;

x = 1, y = 2, z = 3   // 3

typeof 연산자

📍 JS 첫 번째 버전의 버그 null타입 확인을 위해서는 일치 연산자 === 필요

typeof null // object

📍 배열, 객체, new Date()의 경우 typeof 연산자를 사용하면 object를 반환한다. 이 부분 때문에 정밀한 타입 검증을 위해서는 다른 방법이 필요하다.

📌 선언하지 않은 식별자에 typeof 연산자를 사용하면 ReferenceError가 발생하지 않고 undefined 반환


옵셔널 체이닝 ?.

📍 좌항의 피연산자가 null or undefined이면 undefined 반환 아니면 우항 반환
📍 단 null과 undefined만 잡고 false 값은 정상 작동되어 우항의 값 출력됨


null 병합 연산자

📍 좌항이 null or undefined 이면 우항 반환 아니면 좌항 반환
📍 단 null과 undefined만 잡고 false 값은 정상 작동되어 우항의 값 출력됨
📍 변수 선언에 사용되기도 함


연산자의 부수 효과

📍 할당 연산자(=), 증가/감소 연산자(++/--)는 변수의 값이 변하거나 피연산자의 값을 변경한다.
📍 delete 연산자의 경우는 객체 프로퍼티를 삭제하는 부수효과가 있다.


연산자 우선순위 및 결합 순서

📍 우선순위를 모두 기억하기에는 어려움이 많다. 상위그룹 연산자들을 기억해 두고, 우선 순위가 가장 높은 연산자를 사용하여 우선순위를 명시적으로 조절할 필요가 있다.

10 * (2 + 3); // 50



08장 제어문

조건에 따라 코드 블록을 실행(조건문)하거나 반복 실행(반복문)할 때 사용


블록문

📍 0개 이상의 문을 중괄호로 묶은 것, 코드 블록 or 블록이라고 부른다.
📍 JS는 블록문을 하나의 실행 단위로 취급
📍 단독으로 사용할 수도 있으나 일반적으로 제어문 or 함수 정의할 때 사용
📍 블록문은 언제나 문의 종료를 의미(자체 종결성을 가진다.) -> ⭐️세미콜론을 붙이지 않는다.⭐️

// 블록문
{
	var name = 'hamin';
}

// 제어문
var age = 24;

if(age === 24){
	age++;
}

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

조건문

📍 주어진 조건식의 평가 결과에 따라 코드 블록의 실행을 결정
📍 조건에 따라서 값을 결정하여 변수를 할당하는 경우 삼항연산자를 사용하면 가독성이 좋다.
📍 조건에 따라서 실행해야 할 내용이 복잡하여 여러 줄의 문이 필요하다면, if ... else 문이 적합하다.

if ... else 문

📍 else if 문과 else문은 옵션이다.
📍 코드 블록 내의 문이 하나라면 중괄호 생략 가능

if(조건식 1) {
	// 조건식1이 참이면 실행
} else if(조건식2){
	// 조건식2가 참이면 실행
} else{
	조건식1과 조건식2가 모두 거짓이면 실행
}

switch

📍 표현식을 평가하여 그 값과 일치하는 표현식을 갖는 case 문을 실행한다.
📍 case문 실행 뒤 break 코드가 없다면, 다음 case가 실행된다.
📍 조건이 많은 경우 if ... else 문 보다 가독성이 좋을 수 있다.

switch (표현식) {
	case 표현식1:
    	switch 문의 표현식과 표현식1이 일치하면 실행될 문;
    	break;
    case 표현식2:
    	switch 문의 표현식과 표현식1이 일치하면 실행될 문;
    	break;
    default:
        switch 문의 표현식과 일치하는 case 문이 없을 때 실행될 문;
}

반복문

📍 조건식의 평과 결과가 참인 경우 코드 블록 실행하며, 조건식이 거짓일 때까지 반복

for 문

📍 선언문, 조건식, 증감식은 모두 옵션 단, 어떤 식도 선언하지 않으면 무한루프가 된다.
📍 중첩 사용이 가능하다.

for (변수 선언문 or 할당문; 조건식; 증감식) {
	조건식이 참인 경우 반복 실행될 문
}

=========================ex=========================

for(var i = 0; i < 2; i++) {
	console.log(i);
}

// 0
// 1

while 문

📍 반복횟수가 불명확할 때 주로 사용
📍 주어진 조건식의 평과 결과가 참이라면 계속해서 반복 실행된다.
📍 주어진 평과 결과가 언제나 참이면 탈출 문이 필요하다. (ex. break)

do ... while 문

📍 코드 블록을 먼저 실행하고 조건식 평가 -> 무조건 한 번 이상 코드가 실행된다.

break 문

📍 레이블 문, 반복문, switch 문에서 코드 블록을 탈출한다.
📍 불필요한 반복을 회피할 수 있다.

continue 문

📍 반복문의 코드 블록 실행을 현 지점에서 중단하고 반복문의 증감식으로 실행 흐름을 이동시킨다.
(반복문 진행 중 continue 문을 만나면 아래 코드를 진행하지 않고 증감식으로 이동한다.)



09장 타입 변환과 단축 평가

타입 변환

1. 명시적 타입 변환(타입 캐스팅)

📌 JS의 모든 값은 타입이 있고 개발자의 의도에 따라 타입 변환이 가능하다. 이를 명시적 타입 변환 혹은 타입 캐스팅이라 한다. ( keyword -> 의도적인 변환 )


🟢 명시적으로 타입을 변경하는 방법 🟢
1. 표준 빌트인 생성자 함수(String, Number, Boolean)를 new 연산자 없이 호출
2. 빌트인 메서드를 사용
3. 암묵적 타입 변환을 이용

📌 문자열로 명시적으로 타입을 변경하는 방법

숫자로 타입 변경하는 방법

불리언 타입으로 변환


2. 암묵적 타입 변환(타입 강제 변환)

📌 개발자 의도와는 상관없이 표현식 평가 도중 JS 엔진에 의해 암묵적으로 타입이 자동 변경되는 경우가 있다. 이를 암묵적 타입 변환 혹은 타입 강제 변환이라 한다.

문자 타입으로 변환

숫자 타입으로 변환
  • 문자열의 형태지만 그 값이 숫자일 경우 숫자 타입으로 암묵적 변환이 발생한다.
  • 정확한 비교를 수행할 경우 동등 연산자(==)가 일치 연산자(===)가 필요한 이유다.
1 - '1'   // 0
'1' > 0  // true
불리언 타입으로 변환
  • JS 엔진은 타입이 아닌 값을 두 가지로 구분한다.

📍 Truthy(참으로 평가되는 값) ex. 1, true

📍 Falsy(거짓으로 평가되는 값) ex. false, 0, -0, null, undefined, NaN, ' '(빈 문자열)
(💡 false가 되는 값들을 유의하자 특히 undefined, null 앞으로 데이터 검증 등 자주 사용된다.)


🤔 타입 변환 발생 이유

  • JS엔진이 표현식을 에러 없이 평가하기 위해 한 번 사용하고 버린다.
  • 타입의 자동 변환이 이루어지면 예측하지 못한 결과들이 발생할 수 있다. 그러므로 타입 변환을 정확히 이해하고 사용할 필요가 있다.


단축 평가

📌 단축 평가
논리 연산의 결과를 결정하는 피연산자를 타입 변환 없이 그대로 반환하는 것

/* 예제 */
'Cat' && 'Dog' // "Dog"

📍 논리곱(&&)

  • 두 개의 피연산자가 모두 true로 평가되면 true 반환
  • 좌항 -> 우항으로 평가 ( 좌항 Cat이 true면 논리 연산 결과를 결정하는 우항을 평가하고 그 결과를 반환한다.)
/* 예제 */
'Cat' || 'Dog' // "Cat"

📍 논리합(||)

  • 두 개의 피연산자가 하나라도 true로 평가되면 true 반환
  • 좌항 -> 우항으로 평가 ( 논리 연산 결과를 결정하는 좌항 Cat이 true -> 결과 반환)



객체를 가리키기를 기대하는 변수가 null or undifined가 아닌지 확인하고 프로퍼티를 참조할 때 유용하다.

📍 객체를 가리키기를 기대하는 변수가 null or undifined인 경우 이 변수를 참조하면 오류를 발생시키고 프로그램이 종료된다. 이 경우 단축 평가를 사용하면 Falsy 값일 경우와 Truthy 값일 경우의 분기를 주면 에러가 발생되지 않는다

/* 예제 */
var elem null;

var value = elem && elem.value;

함수 매개변수에 기본값을 설정할 때

📍 함수 호출 시 인수를 전달하지 않으면 매개변수가 undefined로 할당된다. 이때 단축 평가를 사용해 매개변수의 기본값을 설정하면 에러를 방지할 수 있다.

function getStringLength(str){
	str = str || '';
    
    return str.length;
}

getStringLength(); // 0
getStringLength('hi'); // 2

옵셔널 체이닝 연산자( ?. )

📌 좌항의 피연산자가 null or undefined인 경우 undefined 반환, 그렇지 않으면 우항 프로퍼티 참조를 이어감.

  • 객체가 가리키기를 기대하는 변수의 값이 null or undefined인지 확인하는데 유용하다.
var elem null;

var value = elem?.value; // elem이 null or undefined이면 elem의 값을 반환 아니면 value 값 반환
console.log(value); // null

⭐️ null or undefined은 평가되지만, Falsy 값은 평가하지 않는다. ⭐️

📍 즉 null or undefined 값만 잡기 위해 사용해야한다.


null 병합 연산자 ( ?? )

📌 좌항의 피연산자가 null or undefined인 경우 우항의 피연산자를 반환하고, 그렇지 않으면 좌항 피연산자 반환

  • 변수 기본값 설정 시 유용하다.
var foo = null ?? 'default string';
console.log(foo); // "default string"

⭐️ 마찬가지로 null or undefined은 평가되지만, Falsy 값은 평가하지 않기 때문에 Falsy 값 평가로는 적합하지 않다.⭐️

📍 null or undefined 값 평가만 위해서 사용하자



10장 객체 리터럴

JS 객체

  • JS는 객체 기반 언어이며 JS를 구성하는 거의 "모든 것"이 객체다.
  • 다양한 타입의 값(원시 값 혹은 다른 객체)을 하나의 단위로 구성한 복합적 자료구조
  • 변경 가능한 값이다.
  • 0개 이상의 프로퍼티로 구성되며, 프로퍼티는 키(key)와 값(value)으로 구성
var counter = {
	num = 0;				// 프로퍼티
	increase: functin () {  // 메서드
    	tihs.num++;
    }
}

프로퍼티

  • 객체의 상태를 나타내는 값

  • JS에서 사용하는 모든 값은 프로퍼티 값이 될 수 있다.

  • JS 함수는 일급 객체 -> 값 취급 가능 -> 함수도 프로퍼티 값으로 사용 가능

📍 프로퍼티 키

  • 키에는 빈 문자열을 포함하는 모든 문자열 또는 심벌 값 ( 빈 문자열도 사용이 가능하지만 키로서 의미 갖지 않으므로 사용 권장 x)
  • 프로퍼티 값에 접근할 수 있는 이름으로서 식별자 역할
  • 심벌 값도 사용 가능하지만 일반적으로 문자열 사용
  • 식별자 네이밍 규칙을 준수하는 이름, 즉 JS에서 사용 가능한 유효한 이름인 경우 따옴표 생략 가능
    (반대로 말하면 식별자 네이밍 규칙을 따르지 않는 이름에는 반드시 따옴표를 사용해야함)
  • 심벌 값 외의 다른 값을 키로 사용하면 따옴표는 붙지 않지만 암묵적 타입 변환으로 문자열로 바꾼다
var foo = {
	0: 1,
    1: 2,
    2: 3
}

console.log(foo) // {0: 1, 1: 2, 2: 3}
  • var, function 같은 예약어를 프로퍼티 키로 사용해도 되지만 권장 x
  • 중복 선언 시 나중에 선언된 프로퍼티가 먼저 선언된 프로퍼티를 덮어쓴다. (에러 발생 x)

📍 프로퍼티 접근
🟢 마침표 프로퍼티 접근 연산자(.)를 사용하는 마침표 표기법

🟢 대괄호 프로퍼티 접근 연산자([ ... ] )를 사용하는 대괄호 표기법

  • 접근 연산자 내부에 지정하는 프로퍼티 키는 반드시 따옴표로 감싼 문자열이어야 한다.
  • 따옴표로 감싸지 않은 이름을 프로퍼티 키로 사용하면 JS 엔진이 식별자로 해석할 수 있다.
  • 객체에 존재하지 않는 프로퍼티에 접근하면 undefined를 반환 (ReferenceError)
  • 숫자로 이루어진 문자열은 생략 가능하다. 그 외의 경우 대괄호 내에 들어가는 프로퍼티 키는 반드시 따옴표로 감싼 문자열이어야 한다.
var person = {
	'last-name': 'Lee'
    1: 10
};

person.'last-name'; // SyntaxError: Unexpected string


person.last-name;   // NaN
/* 브라우저 환경: undefined - '' -> NaN 
	-로 인해 last가 없으니까 undefined
    name은 전역 객체 ''가 되어 NaN이 된다.
*/

person[last-name];  // ReferenceError: last is not defined
person['last-name'] // Lee

/* 프로퍼티 키가 숫자로 이루어진 문자열인 경우 생략 가능 */
person.1;           // SyntaxError: Unexpected number
person.'1';         // SyntaxError: Unexpected string
person[1];          // 10 : person[1] -> person['1']
person['1']         // 10

person.last-name // 

📌 브라우저 환경에서는 name이라는 전역 변수가 암묵적으로 존재한다.
전역 변수 name은 창(name)의 이름을 가리키고 기본값은 빈 문자열이다


📌 식별자 규칙을 미준수 case

var person = {
	firstName: 'hi', // 올바른 형식
    last-name: 'Lee' // -을 연산자로 해석해 에러 발생
}

📍 프로퍼티 값 갱신

var person = {
	name: 'Lee'
};

person.name = "Kim";

console.log(person); // {name: "Kim"}

📍 프로퍼티 동적 생성

var person = {
	name: 'Lee'
};

person.age = 20;

console.log(person);

📍 프로퍼티 삭제

var person = {
	name: 'Lee'
};

person.age = 20;

delete person.age;

delete person.address;

console.log(person); // {name: "Lee"}

ES6에서 추가된 객체 리터럴의 확장 기능
  • 프로퍼티 키가 동일한 이름일 때 프로퍼티 키를 생략 가능
  • 이때 프로퍼티 키는 변수 이름으로 자동 생성된다.
/* 프로퍼티 축약 표현 */
let x = 1, y = 2;

const obj = {x, y};

console.log(obj); // {x: 1, y: 2}

계산된 프로퍼티 이름
// ES5
var prefix = 'prop';
var i = 0;

var obj = {};

obj[prefix + '-' + ++i] = i;
obj[prefix + '-' + ++i] = i;
obj[prefix + '-' + ++i] = i;

console.log(obj); { prop-1: 1, prop-2: 2, prop-3: 3}
// ES6
var prefix = 'prop';
var i = 0;

var obj = {
	[`${prefix}-${++i}`]: i,
    [`${prefix}-${++i}`]: i,
    [`${prefix}-${++i}`]: i
};

console.log(obj); { prop-1: 1, prop-2: 2, prop-3: 3}

메서드 축약

/* ES5 */
var obj = {
	name: 'Lee',
    sayHi: function() {
    	console.log('Hi ' + this.name);
    }
};

obj.sayHi(); // Hi Lee


/* ES6 */
var obj = {
	name: 'Lee',
    sayHi() {
    	console.log('Hi ' + this.name);
    }
};

obj.sayHi(); // Hi Lee

메서드

  • 프로퍼티 값이 함수일 경우 일반 함수와 구분하기 위해 메서드라고 부른다.
  • 프로퍼티를 참조하고 조작할 수 있는 동작(behavior)

📌 클래스 기반 객체지향 언어 vs 프로토타입 기반 객체지향 언어

클래스 기반 객체지향 언어

  • C++, 자바
  • 클래스를 사전 정의하고 필요한 시점에 new 연산자와 함께 생성자를 호출하여 인스턴스 생성

프로토타입 기반 객체지향 언어

  • 다양한 방법 지원
    📍 객체 리터럴, Object 생성자 함수, 생성자 함수, Object.create 메서드, 클래스

객체 생성 방법 - 객체 리터럴

  • 중괄호 ({...}) 내에 0개 이상의 프로퍼티 정의 (중괄호 내에 프로퍼티 정의 안 하면 빈 객체 생성)
  • 변수 할당 시점에 JS 엔진이 객체 리터럴을 해석해 객체 생성
  • 객체 리터럴의 중괄호는 코드 블록을 의미 x -> 세미콜론을 붙인다.

📌 new 연산자와 함께 행성자 호출 필요 x -> JS의 유연함과 강력함을 나타내는 객체 생성 방식!

11장 원시 값과 객체의 비교

📍 원시 타입의 값 - 변경 불가능한 값 ( 원시 값을 변수에 할당 시 실제 값 저장 )

  • 원시 값을 갖는 변수를 다른 변수에 할당하면 원본의 원시 값이 복사되어 전달 -> 값에 의한 전달
  • 읽기 전용 값으로 변경 불가능하다.
  • 데이터의 신뢰성 보장!
  • 값의 변경(상태 변경) 추적에 유리하다.

📌 상수와 변경 불가능한 값은 다르다.

  • 상수는 재할당이 금지된 변수일 뿐, 원시 값은 값 변경은 불가능하지만 변수는 변경 가능하다.

불변성

  • 재할당 하게되면 메모리의 값이 변한다. -> 불변성을 갖는 원시값을 변경 불가능하기 때문에

문자열의 불변성

  • 문자열은 유사 배열이다.
  • 재할당은 가능하지만 문자열은 변경 불가능한 값으로 변경사항이 반영되지 않는다.
var str = 'string';

str[0] = 'S';

console.log(str); // string (값 변경 x)
값에 의한 전달
var score = 80;

var copy = score;

console.log(score, copy); // 80 80
console.log(score === copy); // true
  • 80이라는 값을 가진다는 것은 동일하지만 다른 메모리 공간에 저장된 별개의 값이다.

💡 변수에 원시 값을 갖는 변수를 할당하는 시점에는 두 변수가 같은 원시 값을 참조하다가 어느 한쪽의 변수에 재할당이 이뤄졌을 때 비로소 새로운 메모리 공간에 재할당된 값을 저장하도록 동작할 수도 있다.

var score = 80;  
var copy = score; // 이 시점에는 같은 주소를 참조하고 있을 수도 있다.

score = 100; // 다른 주소 참조

📌 식별자 === 메모리 주소에 붙인 이름

  • 값에 의한 전달도 사실은 값을 전달하는 것이 아닌 메모리 주소를 전달한다.
  • 메모리 주소를 통해 메모리 공간에 접근하면 값을 참조할 수 있다.


📍 객체(참조) 타입의 값 - 변경 가능한 값 ( 객체를 변수에 할당 시 참조 값이 저장 )

  • 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달 -> 참조에 의한 전달
  • 동적으로 추가 및 삭제가 가능함
  • 복잡한 방식(불확실하고 일정하지 않은 크기)으로 비용이 많이 든다. -> 메모리의 효율적 소비가 어렵고 성능이 나빠진다.
  • 원시 값을 할당한 변수를 참조하면 메모리에 저장되어 있는 원시 값에 접근한다. 하지만 객체를 할당한 변수를 참조하면 메모리에 저장되어 있는 참조 값을 통해 실제 객체에 접근한다.
  • ⭐️여러 개의 식별자가 하나의 객체를 공유할 수 있다⭐️ -> 원시 값은 새롭게 생성하지만 객체는 아니다.

얕은 복사(shallow copy) vs 깊은 복사(deep copy)

얕은 복사

  • 객체의 최상위 수준만 복사하므로 중첩된 객체가 변경될 경우 원본도 영향을 X
const obj1 = {
  a: 1,
  b: { c: 2 }
};

const obj2 = { ...obj1 };  // 얕은 복사

obj2.b.c = 3;

console.log(obj1.b.c);  // 출력: 3

💡 최상위 수준이란?
객체 안에 중첩된 객체나 배열이 없는 첫 번째 레벨의 속성들을 지칭

예를 들어,

const obj = {
  a: 1,
  b: "text",
  c: {
    d: 2,
    e: [3, 4],
    f: {
      g: 5
    }
  }
};

여기서 a, b, c는 obj의 최상위 수준의 속성이고 d, e, f는 c 속성 내의 중첩된 속성이므로 최상위 수준의 속성이 아니다.

얕은 복사를 할 경우 a와 b 속성은 원본 객체와 복사본 객체에서 독립적이게 됩니다. 그러나 c는 참조형 데이터이기 때문에 얕은 복사를 통해 복사된 객체에서 c를 수정하면 원본 객체의 c 속성도 변경됩니다.

깊은 복사

  • 객체의 모든 수준을 복사하여 원본과 복사본이 독립적 복사본의 변경이 원본에 영향을 주지 않는다.
const obj1 = {
  a: 1,
  b: { c: 2 }
};

const obj2 = JSON.parse(JSON.stringify(obj1));  // 깊은 복사

obj2.b.c = 3;

console.log(obj1.b.c);  // 출력: 2

참조에 의한 전달

  • 여러 개의 식별자가 하나의 객체를 공유하면 한 식별자의 값이 추가, 삭제 등의 변경이 있을 때 다른 식별자 또한 영향을 받게 된다.
  • 값에 의한 전달참조에 의한 전달은 식별자가 기억하는 메모리 공간에 저장되어 있는 값을 복사해서 전달하는 면에서 동일하다. 그러나 변수에 저장되어 있는 값이 원시 값이냐 참조 값이냐의 차이만 있을 뿐이다.

📌 따라서 JS에는 참조에 의한 전달은 존재하지 않고 값에 의한 전달만 존재한다고 말할 수 있다.



12장 함수

📌 프로그래밍 언어의 함수

  • 함수는 객체다.
  • 일련의 과정을 문으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의하는 것
  • 함수 내부로 입력을 전달받는 변수를 매개변수, 입력을 인수, 출력을 반환값이라 한다.
  • 함수는 값이며, 여러 개 존재할 수 있으므로 특정 함수 구별을 위해 식별자인 함수 이름을 사용한다.-> 코드 가독성 증가
  • 몇 번이든 호출할 수 있으므로 코드의 재사용성이 높다.
  • 코드의 중복을 억제하고 재사용성을 높인다 -> 유지보수의 편의성을 높이고 실수를 줄여 코드 신뢰성이 높아진다.
  • 함수 정의를 통해 생성한다.
function add(x, y) {
	return x + y;
}

함수 호출

  • 인수를 매개변수를 통해 함수로 전달하면서 함수의 실행을 명시적으로 지시하는 것
var result = add(2, 5);

함수 리터럴

📌 리터럴 - 사람이 이해할 수 있는 문자 또는 약속된 기호를 사용해 값을 생성하는 표기 방식

  • JS 함수는 객체 타입의 값이다. -> 함수 리터럴로 생성할 수 있다.
  • 함수 리터럴도 평가되어 값을 생성, 이 값은 객체다.
  • function 키워드, 함수 이름, 매개 변수 목록, 함수 몸체로 구성
변수에 함수 리터럴을 할당
var f = function add(x + y) {
	return x + y;
}

구성 요소설명
함수 이름함수 이름은 식별자다. -> 네이밍 규칙 준수 해야함
함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자다.
함수 이름은 생략 가능, 이름이 있는 함수를 기명 함수, 이름 없는 함수를 무명/익명 함수라고 한다.
매개변수 목록0개 이상의 매개변수를 소괄호로 감싸고 쉼표로 구분
각 매개변수에는 함수를 호출할 때 지정한 인수가 순서대로 할당 즉, 매개변수 목록 순서도 의미가 있다.
매개변수는 함수 몸체 내에서 변수와 동일 취급, 따라서 매개변수도 네이밍 규칙 준수해야함
함수 몸체함수가 호출되었을 때 일괄적으로 실행될 문들을 하나의 실행 단위로 정의한 코드
함수 호출에 의해 실행됨
일반 객체 vs 함수 객체
  • 일반 객체는 호출할 수 없지만 함수는 호출할 수 있다.
  • 함수 객체는 일반 객체에는 없는 함수 객체만의 고유 프로퍼티를 가진다.

함수 정의

함수 선언문

  • 함수 이름 생략할 수 없다.
  • 표현식이 아닌 문이다.
function add(x, y) {
	return x + y;
}

/* 생략 불가 */
function (x, y) {
	return x + y;
}
  • 함수 선언문은 표현식이 아닌 문이므로 변수에 할당할 수 없지만 함수 선언문이 변수에 할당되는 것처럼 보이는 경우가 있다.
  • JS 엔진이 코드 문맥에 따라 동일한 함수 리터럴을 표현식이 아닌 문인 함수 선언문으로 해석하는 경우와 표현식으로 해석하기 때문이다.
  • 함수 이름을 생략할 수 없다는 점을 제외하면 함수 리터럴과 동일하다.
  • 기명 함수 리터럴은 함수 선언문 또는 함수 리터럴 표현식으로 해석될 가능성이 있다는 의미다.

📌 기명 함수 리터럴은 중의적인 코드다. -> 문맥에 따라 해석이 다르다.

  • JS 엔진은 함수 이름이 있는 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석하고
  • 함수 리터럴이 값으로 평가되어야 하는 문맥 예를 들면 함수 리터럴을 변수에 할당하거나 피연산자로 사용하면 함수 리터럴로 해석
  • 두 방법 모두 함수가 생성되는 것은 동일하지만 함수 생성하는 내부 동작에 차이가 있다.
// 기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석
var foo = function add(x,y) {
	return x + y;
};

// 함수 리터럴을 피연산자로 사용하면 함수 선언문이 아니라 함수 리터럴 표현식으로 해석
(var add = function add(x,y) {
	return x + y;
});

add(); // ReferenceError: add is not defined

💡 에러 발생 이유

  • 함수의 이름(add)은 내부에서만 참조할 수 있는 식별자이다. 외부에서 호출했기 때문에 에러가 발생한다.

  • foo를 호출하게 된다면 오류가 발생하지 않는다. -> JS 엔진이 암묵적으로 함수 이름을 식별자로 생성하고 거기에 함수 객체를 할당했기 때문이다.

  • ⭐️ 지금까지 우리가 호출한 함수 이름은 사실 객체를 가리키는 식별자를 호출한 것이다.

/* 함수를 선언을 코드로 표현하면 이러한 형태이다. */
var add = function add(x,y){    // 앞에 add는 식별자 이름 뒤에 add는 함수 이름이다
	return x + y;
};

console.log(add(2,5)) // 함수의 식별자(add)를 호출한 것이다.

함수 표현식

📌 일급 객체 - JS 함수는 값처럼 변수에 할당할 수도 있고 프로퍼티 값, 배열의 요소가 될 수도 있다.

  • 함수는 일급 객체이므로 함수 리터럴로 생성한 함수 객체를 변수에 할당 가능하다.
  • 이러한 함수의 정의 방식을 함수 표현식이라고 한다.
/* 함수를 선언과 동일해 보이지만 함수 이름 생략이 가능하다. */
var add = function(x,y){
	return x + y;
};

console.log(add(2,5))

🟢 함수 표현식과 함수 선언문은 동일하게 보이지만 그렇지 않다.

  • 함수 표현식은 "표현식인 문"이고, 함수 선언문은 "표현식이 아닌 문이다."

함수 생성 시점과 호이스팅

함수 선언문과 함수 표현식으로 정의한 함수의 생성 시점은 다르다.

함수 선언문 - 런타임 이전에 함수 객체 생성(함수 이름과 동일한 이름의 식별자를 암묵적으로 생성 및 할당)
  • 함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출할 수 있다. 런타임 이전에 JS 엔진에서 먼저 실행
  • 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 JS 고유의 특징을 함수 호이스팅이라고 한다.

💡 변수 호이스팅과 함수 호이스팅은 런타임 이전에 식별자를 생성한다는 부분은 동일하지만 변수 호이스팅은 undefined로, 함수 호이스팅은 함수 객체가 할당된다.

함수 표현식 - 할당문이 실행되는 시점, 즉 런타임에 평가되어 함수 객체가 생성 된다.
  • 함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출할 수 없다.
  • 런타임 이전에 undefined로 초기화된다.
  • ⭐️함수 표현식으로 함수를 정의하면 함수 호이스팅이 아닌 변수 호이스팅이 발생한다.⭐️

함수 선언문은 이전에 호출해도 정상적으로 사용 가능하지만, 함수 표현식은 undefined로 초기화된 값이므로 사용에 제약이 있다.

Function 생성자 함수

  • JS에서 기본 제공하는 빌트인 Function 생성자 함수에 매개변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하면 함수 객체를 생성해서 반환 (new 연산자가 없어도 결과는 동일하다.)
  • 클로저를 생성하지 않는 등, 기존 다른 함수와 다르게 작동한다.
  • 일반적이지 않고 바람직하지 않은 생성 방식이다.
var add = new Funtion('x', 'y', 'return x + y');

console.log(add(2,5))

화살표 함수

  • function 키워드를 축약해서 사용, 익명 함수로 정의한다.
  • 기존 함수와 this 바인딩 방식, prototype 프로퍼티가 없고 arguments 객체를 생성 x
const add = (x + y) => x + y;

함수 호출

  • 0개 이상의 인수를 쉼표로 구분해서 나열한다.
  • 함수 호출 시 현재 실행 흐름을 중단하고 함수로 실행 흐름을 옮긴다.
  • 매개변수에 인수가 순서대로 할당되고 함수 몸체 문들이 실행됨.

매개변수

  • 함수 정의할 때 선언, 함수 몸체 내부에서 변수와 동일하게 취급됨 (함수 호출 시 함수 몸체 내부에서 암묵적으로 매개변수가 생성되고 undefined로 초기화된 이후 인수가 순서대로 할당
  • 스코프 유효 범위는 함수 내부
  • 인수가 부족해 인수가 할당되지 않은 매개변수는 undefined이다.(처음 초기화 값)
  • 초과된 경우는 암묵적으로 arguments 객체 프로퍼티로 보관된다.
function add(x,y) {
	console.log(arguments); // Arguments(3) [2, 5, 10]
    
    return x + y;
}

add(2, 5, 10) // 10은 arguments에 저장

인수 확인

  1. JS 함수는 매개변수와 인수의 개수가 일치하는지 확인 x
  2. JS는 동적 타입 언어이다. 따라서 JS 함수는 매개변수의 타입을 사전에 지정 불가능하다.
📍 인수 전달을 확인할 필요성이 있다.
  • 부적절한 호출을 줄여 런타임 에러를 방지
typeof를 사용해 타입 검증 -> 숫자가 아닌 값을 찾는다.
function add(x,y){
	if(typeof x !== 'number' || typeof y !== 'number'){
    	throw new TypeError('인수는 모두 숫자만!')'
    }
}
arguments 객체를 통해 인수 개수를 확인
function add(a, b, c){
	a = a || 0
    b = b || 0;
    c = c || 0;
    
    return a + b + c;
}

console.log(add(1, 2, 3)); // 6
console.log(add(1, 2));    // 3
console.log(add(1));       // 1
console.log(add());        // 0
매개변수 기본 값
  • 매개변수의 기본 ㄱ밧을 지정한다.
function add(a = 0, b = 0, c = 0) {
   return a + b + c;
}

console.log(add(1, 2, 3)); // 6
console.log(add(1, 2));    // 3
console.log(add(1));       // 1
console.log(add());        // 0

매개변수의 최대 개수

  • 함수는 한 가지 일만 해야 하며 가급적 작게 만들어야한다.
  • 코드의 가독성, 유지보수성, 역할을 고민했을 때 이상적인 매개변수는 최대 3개 이상 넘지 않도록 권장 그 이상의 경우 하나의 매개변수를 선언하고 객체를 인수로 전달하는 것이 유리하다.
객체를 인수로 사용하기
  • 프로퍼티 키만 정확하게 지정하면 매개변수 순서를 고려하지 않아도 된다.
  • 명시적으로 인수의 의미 설명 시 프로퍼티 키를 사용하므로 코드의 가독성도 좋아진다.

반환문

  • 함수 호출은 표현식이다. -> 함수 호출 표현식은 return 키워드가 반환하는 표현식의 평가 결과이다.
  • return에 명시적 값을 지정하지 않으면 undifend가 반환된다.
  • 반환문은 생략 가능하다. 함수의 마지막 문까지 실행 뒤 암묵적으로 undefined 반환

참조에 의한 전달과 외부 상태의 변경

function changeVal(primitive, obj) {
	primitive += 100;
    obj.name = 'Kim';
}

var num = 100;
var person = { name: 'Lee' }

console.log(num); 
console.log(person)

changeVal(num, person);

console.log(num); // 100
console.log(person) // {name: "Kim"}

📍원시 값은 값 자체가 복사되어 전달되므로 기존 값이 변경되지 않는다.

📍 객체의 경우 참조 값이 복사되어 전달되므로 값이 변경되는 부수효과가 발생한다.

  • 외부 상태를 변경하면 값이 변경될 경우 상태 변화를 추적하기 어렵고 코드의 복잡성을 증가시키며 가독성을 해치게 된다.

💡 해결방법 - 객체를 불변 객체로 만들어 사용한다.(객체의 복사본을 만든다. (새롭게 생성하는 것처럼))

  • 객체 변경이 필요한 경우 객체의 방어적 복사를 통해 원본 객체를 완전히 복제(깊은 복제)를 통해 새로운 객체를 생성하고 재할당을 통해 교체한다. -> 이를 통해 부수효과 방지

즉시 실행 함수

  • 함수 정의와 동시에 즉시 호출되는 함수
  • 단 한 번만 호출된다.
  • 기본적으로 무명으로 사용되나, 기명으로도 사용이 가능하지만 다시 호출은 불가능하다.
  • (...)로 감싸지 않으면 에러가 발생한다.
  • 변수, 함수 이름의 충돌 방지할 수 있다.

💡 그룹 연산자로 묶은 이유

  • 먼저 함수 리터럴을 평가해서 함수 객체를 생성하기 위해서
(function() {
	var a = 3;
    var b = 5;
    return a * b;
}());

일반 함수처럼 값 반환, 인수 전달이 가능하다.

var res = (function() {
	var a = 3;
    var b = 5;
    return a * b;
}());

console.log(res); // 15

res = (function (a, b) {
	return a * b;
}(3, 5));

console.log(res); // 15

재귀 함수

  • 함수 자기 자신을 호출하는 것을 재귀 호출이라고 한다.
  • 재귀 함수는 자기 자신을 호출하는 함수이다.
  • 함수 내부에서 호출가능한 함수 이름을 이용해 자기 자신을 계속 호출한다. (단, 함수 외부에서 호출 시 식별자를 가리키도록 해야함)
  • 팩토리얼 같은 반복되는 처리 구문에 사용한다.
  • 자신을 무한 재귀 호출한다. -> 탈출 조건이 없다면 스택 오버플로 에러가 발생한다.

중접 함수

  • 함수 내부에 정의된 함수를 중첩 함수 또는 내부 함수라고한다.
  • 중첩 함수를 포함하는 함수를 외부 함수라고 한다.
  • 중첩 함수는 자기 자신을 포함하는 외부 함수를 돕는 헬퍼 함수 역할
function outer() {
	var x = 1;
    
    // 중첩함수
    function inner () {
    	// 외부 함수의 변수 참조 가능
	    console.log(x + y); // 3
    }
    inner();
}

outer();

콜백 함수

  • 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수
  • 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수라고 한다.
  • 외부에서 로직의 일부분을 함수로 전달받아 수행 -> 유연한 구조를 갖는다.
  • 고차 함수에 전달되어 헬퍼 함수의 역할 수행 -> 고차 함수는 콜백 함수를 자신의 일부분으로 합성
  • 고차 함수는 매개변수를 통해 전달받은 콜백 함수의 호출 시점을 결정해서 호출
  • 함수형 프로그래밍 패러다임뿐 아니라 비동기 처리(이벤트 처리, Ajax 통신, 타이머 함수 등), 배열 고차 함수에서 활용된다.

📌 콜백 함수는 고차 함수에 의해 호출, 이때 고차 함수는 필요에 따라 콜백 함수에 인자를 전달


순수 함수와 비순수 함수

📍외부 상태를 변경하지 않고 외부 상태에 의존하지도 않는 함수를 순수 함수라고 한다.

  • 동일한 인수가 전달되면 동일한 값 반환
  • 함수 내부로 전달된 인수에만 의존해 값 생성 및 반환
  • 내부 상태에만 의존한다고 해도 호출될 때마다 변화하는 값(현재 시간)이라면 순수 함수 x
  • 최소 하나 이상의 인수를 전달 받음 -> 최소 하나도 없으면 존재 의미가 없다.
  • 인수 변경시키지 않는 것이 기본 -> 인수 불변성 유지
var cnt = 0;

// 언제나 동일한 인수가 전달되고 동일한 값 반환
function increase(n) {
	return ++n;
}

cnt = increase(cnt);
console.log(cnt); // 1

cnt = increase(cnt);
console.log(cnt); // 2

📍 외부 상태에 의존하거나 외부 상태를 변경하는, 즉 부수 효과가 있는 함수를 비순수 함수라고 한다.

💡 외부 상태 - 전역 변수, 서버 데이터, 파일, console, DOM 등

  • 함수 외부 상태를 변경하는 부수 효과가 있다.
  • 외부 상태에 의존하게 되어 반환 값과 외부 상태를 변경할 수도 있고, 상태 변화 추적이 어렵다.
var cnt = 0;

// 언제나 동일한 인수가 전달되고 동일한 값 반환
function increase(n) {
	return ++cnt;       // 외부 상태에 의존하여 외부 상태를 변경
}

// 비순수 함수는 외부 상태를 변경하므로 상태 변화 추적이 어렵다.
increase(cnt);
console.log(cnt); // 1

increase(cnt);
console.log(cnt); // 2

함수형 프로그래밍

  • 순수 함수와 보조 함수 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화해서 불변성을 지향하는 프로그래밍 패러다임
  • 로직 내에 조건문과 반목문을 제거해 복잡성을 해결, 변수 사용을 억제하거나 생명주기를 최소화해서 상태 변경을 피해 오류를 최소화하는 것을 목표로 한다.



13장 스코프

식별자가 유효한 범위

  • 다른 언어와 구별되는 특징
  • var, let, const 키워드 변수 스코프는 다르게 작동한다.
  • JS 엔진이 식별자를 검색할 때 사용하는 규칙
  • 스코프가 없다면 동일한 이름을 가지는 변수들이 충돌할 가능성이 있다.
  • 모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효범위가 결정된다. 이를 스코프라고 한다.

식별자 결정

var x = 'global';

function foo() {
	var x = 'local';
    console.log(x) // 1
}

foo();

console.log(x); // 2
  • 두 개의 x 변수가 선언됐고 1, 2번에서 x를 참조한다.
  • JS 엔진은 두 개의 변수 중 어떤 것을 참조할지 결정한다 이것을 식별자 결정이라고 한다.
  • 두 개의 x 변수는 식별자 이름이 동일하지만 유효범위, 즉 스코프가 다르다.
  • 식별자의 값은 값을 구별할 수 있는 유일한 값이므로 중복이 불가능하다. 즉, 하나의 값은 유일한 식별자에 연결되어야 한다.

📌 변수 중복 선언을 방지하는 방법

  • var 키워드가 아닌 let, const 키워드를 사용하면 중복 선언이 불가능하다.

스코프의 종류 - 전역과 지역

구분설명스코프함수
전역코드의 가장 바깥 영역전역 스코프전역 변수
지역함수 몸체 내부지역 스코프지역 변수
  • 변수는 자신이 선언된 위치(전역 or 지역)에 의해 스코프가 결정된다. 즉, 전역에 선언되면 전역 스코프를 갖게 되고 지역에서 선언하면 지역 스코프를 가지게 된다.

전역 스코프

  • 전역에 변수를 선언하면 전역 스코프를 가지는 전역 변수가 되고 어디서든지 참조할 수 있다.

지역 스코프

  • 지역에 변수를 선언하면 지역 스코프를 가지는 지역 변수가 된다.
  • 지역 변수는 자신이 선언된 지역과 하위 지역(중첩 함수)에서만 참조 가능하다. -> 지역 스코프와 하위 지역 스코프에서만 유효
var x = 'global';

function foo() {
	var x = 'local';
    console.log(x) // 1
}

foo();

console.log(x); // 2
  • 1번 x 변수는 자신의 지역에 있는 x를 참고해 local이 되고, 2번 x 변수는 전역 변수이므로 global 값을 출력하게 된다.

스코프 체인

  • 함수는 중첩될 수 있으므로 함수의 지역 스코프도 중첩될 수 있다. 이는 스코프가 함수 중첩에 의해 계층적 구조를 갖는다는 것을 의미한다..
    📍 중첩 함수의 지역 스코프는 중첩 함수를 포함하는 외부 함수의 지역 스코프와 계층적 구조를 갖는다. 이때 외부 함수의 지역 스코프를 중첩 함수의 상위 스코프라 한다.

  • 모든 지역 스코프의 최상위 스코프는 전역 스코프다.
  • 변수를 참조할 때 JS 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다. 이를 통해 상위 스코프에서 선언한 변수를 하위 스코프에서 참조가 가능하다.
  • 스코프 체인은 물리적인 실체로 존재한다. JS 엔진은 코드(전역 코드와 함수 코드)를 실행하기 전 위 그림과 유사한 자료구조인 렉시컬 환경을 실제로 생성한다. 변수가 선언이 실행되면 변수 식별자가 렉시컬 환경(자료구조)에 key로 등록되고 변수 할당이 일어나면 렉시컬 환경의 변수 식별자에 해당하는 값을 변경한다. 변수 검색도 렉시컬 환경 상에서 이뤄진다.

스코프 체인에 의한 변수 검색

  • 변수가 참조되면 해당 지역에서 변수가 선언되었는지 확인하고 없다면 상위 스코프로 이동한다. 선언된 변수를 찾지 못한다면 상위로 계속 이동하게 된다. 선언이 존재한다면 변수를 참조하고 검색을 종료한다.
  • 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 이용 가능하지만 하위 스코프에서 유효한 값을 상위 스코프에서 참조는 불가능하다.
  • 상속과 유사한 개념이다.

스코프 체인에 의한 함수 검색

  • 함수는 함수 이름과 동일한 이름의 식별자를 암묵적으로 선언하고 생성된 객체를 할당한다.
  • 함수도 식별자에 할당되기 때문에 스코프를 갖는다. 따라서 스코프를 "변수를 검색할 때 사용하는 규칙"이라고 표현하기 보다는 "식별자를 검색하는 규칙"으로 표현하는 것이 더 적합하다.

함수 레벨 스코프

  • 지역은 함수 몸체 내부를 말하고 지역은 지역 스코프를 만든다. -> 코드 블록이 아닌 함수에 의해서만 지역 스코프가 생성된다.
  • C, Java는 함수 몸체만이 아닌 모든 코드 블록(if, for 등)이 지역 스코프를 만든다. 이러한 특성을 블록 레벨 스코프라고 한다. JS에서는 var 키워드로 선언된 변수는 오로지 함수의 코드 블록(함수 몸체)만 지역 스코프로 인정한다. 이러한 특성을 함수 레벨 스코프라고 한다.

💡 var 키워드

  • 함수의 코드 블록(함수 몸체)만 지역 스코프로 인정
  • 함수 밖에서 var 키워드로 선언된 변수는 코드 블록 내에서 선언되었다 할지라도 모두 전역 변수가 된다.
  • 의도치 않게 변수 값이 변경되는 부작용이 발생한다.
var x = 1;

if(true) {
	var x = 10;
}

console.log(x); // 10



렉시컬 스코프

var x = 1;

function foo() {
	var x = 10;
    bar();
}

function bar(){
	console.log(x);
}

foo();    1. ?
bar();    2. ?

📌 두 가지 패턴으로 예측할 수 있다.

1. 함수를 어디서 호출했는지에 따라 함수 상위 스코프 결정

  • 동적 스코프 방식이다.
  • 정의하는 시점에는 함수가 어디서 호출될지 알 수 없다. 따라서 호출되는 시점에 동적으로 상위 스코프를 결정한다.

2. 함수를 어디서 정의했는지에 따라 함수의 상위 스코프 결정

  • 렉시컬 스코프 or 정적 스코프라고 한다.
  • 동적 스코프 방식처럼 상위 스코프가 동저긍로 변하지 않고 함수 정의가 되는 시점에 상위 스코프를 결정한다.
  • 함수의 상위 스코프는 언제나 자신이 정의된 스코프다.
  • 위 예제의 경우 실행 전에 먼저 평가되어 함수 객체를 생성한다. 이떼 bar 힘수 객체는 자신의 스코프에 x 변수 값이 없으므로 전역 스코프를 기억하게 된다. 따라서 1을 두 번 출력하게 된다.



14장 전역 변수의 문제점

  • 변수는 선언에 의해 생성되고 할당을 통해 값을 갖는다.
  • 변수는 자신이 선언된 위치에서 생성되고 소멸한다.
  • 전역 변수의 생명 주기 == 애플리케이션의 생명 주기
  • 지역 변수는 함수가 호출되면 생성되고 종료되면 소멸 (지역 변수 생명 주기 == 함수의 생명 주기)
  • 참조되지 않을 경우 가비지 콜렉터에 의해 해제됨 (스코프도 동일)
💡 변수의 생명 주기

메모리 공간 확보(스코프 등록) -> 메모리 공간이 해제되어 가용 메모리 풀에 반환되는 시점(스코프 소멸)

전역 변수

  • 전역 코드는 명시적인 호출 없이 실행 -> 코드 로드 시 곧바로 해석 및 실행
  • 마지막 문이 실행되어 더 이상 실행할 문이 없을 때 종료
  • var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다. (전역 변수의 생명 주기 == 전역 객체의 생명 주기)
📌 전역 객체
  • 코드 실행 이전 단계에 JS 엔진에 의해 가장 먼저 생성되는 특수한 객체
  • 클라이언트 사이드 환경에서는 window, 서버 사이드 환경에서는 global 객체를 의미
  • 표준 빌트인 객체(Object, String, Number ...)와 환경에 따른 호스트 객체(클라이언트 API or Node.js 호스트 API, var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖는다.
  • 전역 객체 window는 웹페이지를 닫기 전까지 유효

전역 변수의 문제점

  1. 암묵적 결합
  • 모든 코드가 전역 변수를 참조하고 변경할 수 있는 암묵적 결합을 허용한다.
  • 변수의 유효 범위가 크면 클수록 코드의 가독성은 나빠지고 의도치 않은 상태 변경의 위험성이 높다.
  1. 긴 생명 주기
  • 메모리 리소스도 오랜 기간 소비한다.
  • 전역 변수의 상태를 변경할 시간도 길고 기회도 많다.
  1. 스코프 체인 상에서 종점에 존재
  • 변수 검색 시 전역 변수가 가장 마지막에 검색된다. -> 검색 속도가 가장 느리다.
  1. 네임스페이스 오염
  • JS는 파일이 분리되어도 하나의 전역 스코프를 공유한다.
  • 다른 파일 내에서 동일한 이름의 전역 변수나 전역 함수가 존재하면 예상치 못한 오류가 발생할 수 있다.

전역 변수 사용 억제

  1. 지역 변수 사용 지향하기
  2. 즉시 실행 함수 사용 (단 한 번만 호출)
  3. 네임스페이스 객체
  • 전역에 네임스페이스 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가
  • 식별자 충돌 방지 효과가 있다.
  • 유용한 방법은 아니다.
var MYAPP = {}; // 전역 네임스페이스 객체

MYAPP.name = 'LEE';

console.log(MYAPP.name) // LEE
  1. 모듈 패턴
  • 관련 있는 변수와 함수를 모아 즉시 실행함수로 감싸 하나의 모듈을 만든다.
  • 클로저 기반으로 작동
  • 전역 변수 억제 및 캡슐화까지 구현 가능
  • 전역 네임스페이스의 오염을 막는 기능과 한정적이긴 하지만 정보 은닉을 구현하기 위해 사용

📌 캡슐화 - 객체의 상태를 나타내는 프로퍼티와 메서드를 하나로 묶는 것, 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용(은닉)

  1. ES6 모듈
  • ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공
  • var 키워드로 선언한 변수도 window 객체의 프로퍼티가 아니게 된다.
  • script 태그에 type = "module" 어트리뷰트를 추가하면 모듈로서 동작하게 된다.
  • 확장자는 mjs를 권장



15장 let, const, 키워드와 블록 레벨 스코프

var 키워드의 문제점

  1. 변수 중복 선언 허용
  2. 함수 레벨 스코프
  3. 변수 호이스팅

📌 공통적으로 에러를 발생시키지는 않지만 가독성과 오류를 발생시킬 여지를 남긴다


let 키워드

변수 중복 선언 금지

let name = '하민';

let name = '박하'; // SyntaxError Identifier 'name' has already been declared

블록 레벨 스코프

  • let 키워드로 선언된 변수는 모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등)을 지역 스코프로 인정
  • 전역에 생성된 변수와 지역 변수 안에 선언된 변수의 이름이 같아도 별개의 변수다.
let foo = 1; // 전역변수

{
	let foo = 2; // 지역 변수
    let bar = 3; // 지역 변수
}

conosle.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined

변수 호이스팅

console.log(foo); // ReferenceError: foo is not defined

let foo; // 변수 선언문에서 초기화 단계가 실행
console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1
  • var 키워드의 경우 선언과 초기화 단계가 한번에 진행된다. ( <=> let은 선언과 초기화가 분리되어 진행)
  • let 키워드는 변수 호이스팅이 발생하지 않는 것처럼 동작하지만 런타임 전에 JS 엔진에 의해 암묵적으로 선언 단계가 되어 실행되지만 초기화 단계는 변수 선언문에 도달했을 때 실행된다. -> 초기화 이전에 변수 접근하려고 하면 참조 에러 발생(호이스팅은 발생하기 때문에 참조 에러가 발생한다.)
  • let 키워드 선언 변수는 스코프 시작 지점부터 초기화 단계 시작 지점(변수 선언문)까지 변수를 참조할 수 없다 이 구간을 일시적 사각지대(TDZ)라고 부른다.
선언단계ReferenceError
일시적 사각지대(TDZ)ReferenceError
초기화 단계foo === undefined
할당 단계foo === 1

const 키워드

  • 보통 상수 선언을 위해 사용(상수는 상태 유지와 가독성. 유지보수의 편의를 위해 적극 사용 권장, 상수 이름은 대문자로 선언해 상수임을 명확히 나타내고 여러 단어로 이뤄진 경우 언더스코어(_)로 구분해 스네이크 케이스로 표현하는 것이 일반적)

  • 선언과 동시에 초기화가 필요하고 재할당이 불가능하다.

const foo = 1;

const foo; // SyntaxError: Missing initializer in const declaration

foo = 2; // TypeError: Assignment to constant variable.
  • 변수 호이스팅이 발생하지 않는 것처럼 동작한다.
  • const 키워드로 선언한 변수에 원시 값을 할당한 경우 값 변경이 불가능하지만 객체를 할당한 경우 값 변경이 가능하다. -> 객체는 재할당 없이도 직접 변경이 가능하다. const 키워드는 재할당을 금지하지만 "불변"을 의미하지는 않는다.

💡 var vs let vs const

  • ES6 사용 시 var 키워드는 사용하지 않는다.
  • 재할당 필요 여부를 모를 경우 const를 우선 사용하고 상황을 지켜보는 편이 좋다.



16장 프로퍼티 어트리뷰트

내부 슬롯과 내부 메서드

  • JS 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript에서 사용하는 의사 프로퍼티와 의사 메서드

  • 이중 대괄호 ([[...]])로 감싼 이름들이 내부 슬롯과 내부 메서드다.

  • JS 엔진에서 실제로 동작하지만 개발자가 직접 접근할 수 있도록 외부에 공개된 객체의 프로퍼티는 아니다. (JS 엔진 내부 로직으로 원칙적이므로 JS는 내부 슬롯과 내부 메서드에 직접적으로 접근 및 호출 방법 제공 x)

  • 일부 내부 슬롯과 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공

    📌 ex

  • 모든 객체는 [[Prototype]] 내부 슬롯을 갖는다.

  • 원래 JS 엔진 내부로직에 접근 불가능하지만 __proto__를 통해 간접 접근 가능


프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

  • JS 엔진은 프로퍼티 생성 시 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의
  • 프로퍼티 상태란
  1. 프로퍼티의 값(value)
  2. 값의 갱신 가능 여부(writable)
  3. 열거 가능 여부(enumerable)
  4. 재정의 가능 여부(configurable)

프로퍼티 어트리뷰트

  • JS 엔진이 관리하는 내부 상태 값인 슬롯
    [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]

  • 직접 접근이 불가능하지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적 확인 가능 (첫 번째 매개변수에 객체 참조 전달, 두 번째 매개변수에 프로퍼티 키를 문자열로 전달)

  • Object.getOwnPropertyDescriptor 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환, 존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한 프로퍼티 디스크립터 요구 시 undefined 반환

const person = {
	name: 'Lee'
};

console.log(Object.getOwnPropertyDescriptor(person, 'name');
// {value: "Lee", writable: true, enumerable: true, configurable: true}

프로퍼티

데이터 프로퍼티

  • 키와 값으로 구성된 일반적인 프로퍼티
  • JS 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의
  • 프로퍼티가 생성될 때 [[Value]]의 값은 프로퍼티 값으로 초기화
  • 나머지 값은 true로 초기화
  • 동적 추가도 초기화 값은 동일하다.
프로퍼티 어트리뷰트프로퍼티 디스크립터 객체의 프로퍼티설명
[[Value]]vale프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값, 프로퍼티 키로 프로퍼티 값 변경 시 [[Value]]에 값을 재할당 이때 프로퍼티가 없으면 프로퍼티를 동적 생성하고 [[Value]]값을 저장
[[Writable]]writable프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다. false인 경우 [[Value]] 값을 변경할 수 없는 읽기 전용이 됨
[[Enumerable]]enumerable프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다. false인 경우 해당 프로퍼티는 for ... in 문이나 Object.keys 메서드 등으로 열거 불가
[[Configurable]]configurable프로퍼티 재정의 가능여부를 나타내며 불리언 값을 갖는다. false인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰터 값의 변경이 금지된다. 단 [[Writable]]이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것을 허용

접근자 프로퍼티

  • 자체적으로 값[[Value]]을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성
  • 프로퍼티 키가 유효한지 확인 -> 프로토타입 체인에서 프로퍼티 검색 -> 데이터 프로퍼티인지 접근자 프로퍼티 인지 확인 -> 해당 함수 호출 및 결과 반환
프로퍼티 어트리뷰트프로퍼티 디스크립터 객체의 프로퍼티설명
[[Get]]get접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수, getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환
[[Set]]set접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수, setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장
[[Enumerable]]enumerable데이터 프로퍼티의 [[Enumerable]]과 동일
[[Configurable]]configurable데이터 프로퍼티의 [[Configurable]]과 동일

접근자 프로퍼티와 데이터 프로퍼티는 프로퍼티 디스크립터 객체의 프로퍼티가 다르다
// 일반 객체의 __proto__는 접근자 프로퍼티다.

Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// {get: f, set: f, enumerable: false, configurable: true}

// 함수 객체의 prototype은 데이터 프로퍼티다.

Object.getOwnPropertyDescriptor(function() {}, 'prototype');
// {value: {...}, writable: true, enumerable: false, configurable: false}

프로퍼티 정의

  • 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것
  • 프로퍼티 값 갱신 가능 여부, 프로퍼티 열거 가능 여부, 프로퍼티 재정의 가능 여부를 정의 가능
  • 이를 통해 객체의 프로퍼티가 어떻게 동작해야 하는지 명확한 정의 가능
  • Object.defineProperty 메서드를 사용해서 정의 가능하며 인수로 객체의 참조와 데이터 프로퍼티의 키인 문자열, 프로퍼티 디스크립터 객체를 전달
  • Object.defineProperties 메서드는 한 번에 여러 개 정의 가능
  • value, get, set 프로퍼티의 경우 생략했을 때 기본값은 undefined, wirtable, enumerable, configurable은 false

객체 변경 방지

  • 객체는 변경 가능한 값이므로 재할당 없이 직접 변경이 가능하다.
  • 객체 변경 방지 메서드가 존재하며 각각 강도가 다르다.
구분메서드프로퍼티 추가프로퍼티 삭제프로퍼티 값 읽기프로퍼티 값 쓰기프로퍼티 어트리뷰트 재정의
객체 확장 금지Object.preventExtensionsXOOOO
객체 밀봉Object.sealXXOOX
객체 동결Object.freezeXXOXX

객체 확장 금지(Object.preventExtensions)

  • 프로퍼티 추가가 금지된다.
  • 확장 가능한 객체인지 여부는 Object.isExtensible 메서드로 확인할 수 있다.
const person = {name: 'Lee'};

console.log(Object.isExtensible(person)); // true

// 객체 확장 금지
Object.preventExtensions(person);

console.log(Object.isExtensible(person)); // false

객체 밀봉(Object.seal)

  • 읽기와 쓰기만 가능
  • Object.isSealed 메서드로 확인 가능하다.
  • 밀봉된 객체는 configurable이 false다.
const person = {name: 'Lee'};

// 밀봉된 객체 아님
console.log(Object.isSealed(person)); // false

// 객체 확장 금지
Object.seal(person);

console.log(Object.isSealed(person)); // true

객체 동결(Object.freeze)

  • 읽기만 가능
  • Object.isFrozen 메서드로 확인 가능
const person = {name: 'Lee'};

// 동결된 객체가 아니다
console.log(Object.isFrozen(person)); // false

// 객체 동결
Object.freeze(person);

console.log(Object.isFrozen(person)); // true

불변 객체

  • 지금까지 살펴본 변경 방지 메서드들은 얕은 변경 방지로 직속 프로퍼티만 변경 방지되고 중첩 객체까지는 영향을 줄 수 없다.
  • 불변 객체를 구현하기 위해서는 모든 프로퍼티를 순회하며 재귀적으로 동결하는 방법을 사용해야 한다.
  • Object.keys 메서드로 열거 가능한 프로퍼티 키를 배열로 반환하고 각 요소를 콜백함수로 freeze 시킨다.



17장 생성자 함수에 의한 객체 생성

Object 생성자 함수

  • new 연산자와 함께 Object 생성자 함수를 호출하면 빈 객체를 생성 및 반환 이후 프로퍼티 또는 메서드를 추가하여 객체 완성
  • 생성자 함수란 new 연산자와 함께 호출하여 객체(인스턴스)를 생성하는 함수 생성자 함수로 생성된 객체를 인스턴스라고 한다.
  • Object 외에도 String, Number 등의 빌트인 생성자 함수를 제공한다.
  • 특별한 이유가 없다면 유용하지 않은 방식
const person = new Object();

person.name = 'Lee';
person.hi = function() {
	console.log('hi');
}

console.log(person) // {name: "Lee", hi: f}
person.hi(); // "hi"

생성자 함수

  • 객체 리터럴에 의한 객체 생성의 경우 매번 같은 프로퍼티를 기술해야하는 비효율적인 문제가 발생한다. 생성자 함수에 의한 객체 생성의 경우 객체를 생성하기 위한 템플릿처럼 동일한 객체를 여러 개 간편하게 생성 가능하다.
  • new 연산자와 함께 호출하면 해당 함수는 생성자 함수로 동작하고 없다면 일반 함수로 동작한다.
  • 인스턴스를 생성하는 것과 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기값 할당)하는 생성 과정을 거친다. 인스턴스 생성은 필수지만 초기화는 옵션이다
function Circle(radius) {
	this.radius = radius; 	        // 초기화는 선택이다.
    this.getDiameter = function() {
    	return 2 * this.radius;
    };
}

생성자 함수의 인스턴스 생성 과정

  • new 연산자로 생성한 생성자 함수는 반환(return)하는 코드가 없어도 JS 엔진에서 암묵적으로 인스턴스를 생성 및 초기화 후 암묵적으로 인스턴스를 반환한다.
  1. 인스턴스 생성과 this 바인딩
  • 암묵적으로 빈 객체가 생성되는데(아직 완성되지 않은) 이 빈 객체가 생성자 함수가 생성한 인스턴스이다.
  • 그리고 이 빈 객체, 즉 인스턴스는 this에 바인딩 된다. -> 생성자 함수 내부의 this가 생성자 함수가 생성한 인스턴스를 가리키는 이유
  • 런타임 이전에 실행된다.

📌 바인딩

  • 식별자와 값을 연결하는 과정

  1. 인스턴스 초기화
  • 생성자 함수의 기술되어 있는 코드를 한 줄씩 실행하며 this에 바인딩되어 있는 인스턴스를 초기화 -> 생성자 함수가 인수로 전달받은 초기값을 프로퍼티에 초기화 or 고정값 할당, 인스턴스에 프로퍼티나 메서드 추가
  1. 인스턴스 반환
  • 생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적 반환(return 없이)
  • tihs가 아닌 다른 객체를 명시적으로 반환하면 return 문에 명시한 객체 반환, 원시 값 반환 시 값은 무시되고 암묵적으로 this 반환
  • 생성자 함수 내부에서 return 문은 반드시 생략해야 한다.

내부 메서드 [[Call]], [[Construct]]

  • 함수 선언문 또는 함수 표현식으로 정의한 함수는 일반적인 함수로서 호출할 수 있고 생성자 함수로도 호출 가능
  • 함수는 객체이므로 객체와 동일하게 동작, 함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드를 모두 가진다.
  • 일반 객체는 호출 불가능하지만 함수 객체는 가능하므로 일반 객체가 가지는 내부 슬롯과 메서드 외에 함수로서 동작하기 위해 함수 객체만을 위한 [[Environment]], [[FormalParameters]] 등의 내부 슬롯과 [[Call]], [[Construct]] 같은 내부 메서드를 추가로 가진다.

💡 함수가 일반 함수로서 호출되면 함수 객체의 내부 메서드 [[Call]]이 호출되고 new 연산자와 함께 생성자를 함수로서 호출되면 내부 메서드 [[Construct]]가 호출된다.

  • 내부 메서드 [[Call]]을 갖는 함수 객체를 callable, 내부 메서드 [[Construct]]을 갖는 함수 객체를 constructor, [[Construct]]를 갖지 않는 함수 객체를 non-constructor 라고 부른다.
  • 즉 함수 객체는 반드시 callable이면서 constructor이거나 callable이면서 non-constructor이다.

constructor, non-constructor 구분

  • JS 엔진은 함수 정의를 평가하여 함수 객체를 생성할 때 함수 정의 방식에 따라 함수를 구분
  • constructor - 함수 선언문, 함수 표현식, 클래스
  • non-constructor - 메서드(ES6 메서드 축약 표현), 화살표 함수

생성자 함수와 일반 함수 구분

  • 두 함수는 사실 호출 방법이 다를 뿐 큰 차이가 없다.
  • 생성자 함수는 일반적으로 첫 문자를 대문자로 기술하는 파스칼 케이스를 사용한다.
  • new.target을 통해 함수 내부에서 어떤 함수로 호출되었는지 구분이 가능하다. new 연산자와 함께 호출되면 함수 내부의 new.targe은 함수 자신을 가리킨다. 일반 함수로 호출될 경우 undefined로 호출된다.
if(!new.target) // 생성자 함수로 호출되지 않은 경우를 방지가 가능하다.
  • 대부분의 빌트인 생성자 함수는 new 연산자와 호출되었는지를 확인 후 적절한 값을 반환
    (Object와 function 생성자 함수는 new 연산자 없이도 new 연산자와 함께 호출된 결과와 동일하게 작동)
  • 나머지 빌트인 생성자 함수들은 new 연산자가 없다면 문자열, 숫자, 불리언 값을 반환한다. 이를 통해 데이터 타입을 변환하기도 한다.



18장 함수와 일급 객체

📌 일급 객체의 조건

  1. 무명의 리터럴로 생성할 수 있다. (런타임에 생성 가능)
  2. 변수나 자료구조(객체, 배열 등)에 저장 가능하다.
  3. 함수의 매개변수로 전달 가능하다.
  4. 함수의 반환값으로 사용 가능하다.

  • JS의 함수는 위 조건을 모두 만족하므로 일급 객체다.
  • 함수는 값을 사용할 수 있는 곳(변수 할당문, 객체의 프로퍼티 값, 배열의 요소, 함수의 호출의 인수, 함수 반환문)이라면 어디서든지 리터럴로 정의할 수 있고 런타임에 함수 객체로 평가된다.
  • 함수는 객체지만 일반 객체와 달리 호출이 가능하고 함수 고유의 프로퍼티를 소유한다.

// 1. 함수는 무명의 리털로 생성할 수 있다.
// 2. 함수는 벼수에 저장할 수 있다.
// 런타임(할당 단계)에 함수 리터럴이 평가되어 함수 객체가 생성되고 변수에 할당된다.
const increase = function(num) => {
	return ++num;
};

// 2. 함수는 객체에 저장할 수 있다.
const auxs = { increase };

// 3. 함수의 매개변수에 전달할 수 있다.
// 4. 함수의 반환값으로 사용할 수 있다.
function makeCounter(aux) {
	let num = 0;
 	
    return function() {
    	num = aux(num);
        
        return num;
    };
}

// 3. 함수는 매개변수에게 함수를 전달할 수 있다.
const increase = makeCounter(auxs.increase);
console.log(increase()); // 1

함수 객체의 프로퍼티

function square(number){
	return number * number;
}

conosole.log(Object.getOwnPropertyDescriptors(square));
/*
{
	length: {valye: 1, writable: false, enumerable, configuarable: true},
    name: {valye: "square", writable: false, enumerable, configuarable: true},
    arguments: {valye: null, writable: false, enumerable, configuarable: false},
    caller: {valye: null, writable: false, enumerable, configuarable: false},
    prototype: {valye: {}, writable: true, enumerable, configuarable: true}
}
*/

arguments 프로퍼티

  • 함수 객체의 arguments 프로퍼티 값은 arguments 객체다.
  • arguments 객체는 함수 호출 시 전달된 인수들의 정보를 담고 있는 순회 가능한 유사 배열 객체이며 함수 내부에서 변수처럼 사용된다. -> 외부에서 참조 불가
  • 매개변수의 개수보다 인수를 더 많이 전달하면 무시되지만 버려지지 않고 암묵적으로 arguments 객체의 프로퍼티로 보관한다. arguments 객체는 인수를 프로퍼티 값으로 소유하고 프로퍼티 키는 인수의 순서를 나타낸다. arguments 객체의 callee 프로퍼티는 호출되어 arguments 객체를 생성한 함수 즉 함수 자신을 가리킨다.
  • 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하다.
  • 배열의 형태로 인자를 담고있지만 실제 배열이 아닌 유사 배열 객체다.
  • 유사 배열 객체란 length 프로퍼티를 가진 객체로 for문으로 순회할 수 있는 객체이다. 배열 메서드를 사용할 경우 에러 발생함 그렇기 때문에 Function.prototype.call, Function.prototype.apply를 간접 호출해야하는 번거로움이 있다.

caller 프로퍼티

  • 함수 객체의 caller 프로퍼티는 함수 자신을 호출한 함수를 가리킨다.
  • 비표준 프로퍼티로 참고만하자

length 프로퍼티

  • 함수 객체의 length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킨다.
  • arguments 객체의 length 프로퍼티와 함수 객체의 length 프로퍼티의 값은 다를 수 있다. arguments 객체의 length 프로퍼티는 인자의 개수를 가리키고 함수 객체의 length는 프로퍼티 매개변수의 개수를 가리킨다.

proto 접근자 프로퍼티

  • 모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다. [[Prototype]] 내부 슬롯은 객체지향 프로그래밍 상속을 구현하는 프로토타입 객체를 가리킨다.
  • [[Prototype]] 내부 슬롯을 가리키는 프로토타입 객체에 접근하기 위해 사용하는 접근자 프로퍼티다.
  • 직접 접근은 불가능하고 간접적인 접근 가능하다.
const obj = { a: 1 };

console.log(obj.__proto__ === Object.prototype); // true

// hasOwnproperty 메서드는 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티인지 판단한다.
console.log(obj.hasOwnproperty('a')); // true
console.log(obj.hasOwnproperty('__proto__')); // false

prototype 프로퍼티

  • 생성자 함수로 호출할 수 있는 함수 객체 -> constructor만이 소유하는 프로퍼티다.
  • non-constructor에는 prototype 프로토타입이 없다.
  • 함수가 객체를 생성하는 생성자 함수로 호출될 때 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.



프로토타입

상속 - 객체지향 프로그래밍의 핵심 개념으로 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것

  • JS는 프로토타입으로 상속을 구현해 코드를 재사용함으로써 중복을 줄인다.
  • 생성자 함수로 여러 개의 객체 생성 시 동일한 메서드를 중복 생성하고 소유하는 경우가 발생한다. -> 메모리 낭비, 퍼포먼스 악영향
    (만약 인스턴스 10개 생성하면 동일한 메서드가 10개 생성됨)
function Circle(radius) {
	this.radius = radius;	
}

Circle.prototype.getArea = function() {
	return Math.PI * this.radius ** 2;
};

const circle1 = new Circle(1);
const circle2 = new Circle(2);

console.log(circle1.getArea === circle2.getArea); // true
  • Circle 생성자 함수가 생성한 모든 인스턴스는 자신의 프로토타입 즉, 상위(부모) 객체 역할을 하는 Circle.prototype의 모든 프로퍼티와 메서드를 상속받는다.
  • 생성자 함수가 생성할 모든 인스턴스는 별도의 구현 없이 상위 객체인 프로퍼티타입의 자산을 공유 가능

프로토타입 객체

  • 모든 객체는 [[Prototype]] 슬롯을 가지게 되고, 생성 방식에 따라 프로토타입이 결정 및 저장 된다.
  • 생성자 함수는 prototype을 통해 프로토타입에 접근 가능하고 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근 가능하다.

__proto__ 접근자 프로퍼티

  • 모든 객체는 __proto__ 접근자 프로퍼티를 통해 [[Prototype]] 내부 슬롯에 접근 가능

  • getter/setter 함수라고 부르는 접근자 함수 ([[Get]], [[Set]] 프로퍼티 어트리뷰트에 할당된 인수)를 통해 [[Prototype]] 내부 슬롯의 값 즉 프로토타입을 취득하거나 할당
  • __proto__ 접근자 함수를 통해 프로토타입에 접근하면 내부적으로 __proto__ 접근자 프로퍼티인 getter 함수인 [[Get]]이 호출되고 새로운 프로토타입을 할당하면 setter 함수인 [[Set]]이 호출된다.
const obj = {};
const parent = { x: 1};

// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입 취득
obj.__proto__

// setter 함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;

console.log(obj.x); // 1
  • __proto__ 프로퍼티는 객체가 직접 소유하지 않고 Object.prototype의 프로퍼티다.
  • 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티 사용 가능

💡 모든 객체는 프로토타입의 계층 구조인 프토토타입 체인에 묶여있다. JS 엔진이 프로퍼티, 메서드에 접근할 때 해당 객체에 접근하려는 프로퍼티가 없다면 __proto__ 접근자 프로퍼티가 가리키는 참조를 따라 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 프로토타입 체인의 종점은 Object.prototype이고 이 객체의 프로퍼티와 메서드는 모든 객체에 상속됨


  • __proto__ 접근자 프로퍼티 사용 이유는 상호 참조 방지를 위해서이다.
const parent = {};
const child = {};

// child의 프로토타입을 Parent로 설정
child.__proto__ = parent;

// parent의 프로토타입을 child로 설정
parent.__proto__ = child // TypeError
  • 서로가 자신의 프로토타입이 되는 비정상적인 프로토타입 체인이 만들어지게 된다. 프로토타입은 단방향 링크드 리스트로 구현되어야 한다. 순환 참조를 하게될 경우 체인의 종점이 존재하지 않기 때문에 검색 시 무한 루프에 빠진다. 따라서 __proto__ 접근자 프로퍼티를 통해서만 프로토타입에 접근하고 교체하도록 구현되어 있다.

  • Object.prototype을 상속받지 않는 객체도 존재하기 때문에 __proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장 x 대신 프로토타입의 참조를 취득하고 싶은 경우에는 Object.getPrototypeOf 메서드를 사용하고, 프로토타입 교체를 원하는 경우 Object.setPrototypeOf 메서드 사용을 권장

  • 생성자 함수로서 호출할 수 없는 함수, 즉 non-constructor인 화살표 함수와 ES6 메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성 하지 않는다.


프로토타입의 constructor 프로퍼티와 생성자 함수

  • 모든 프로토타입은 constructor 프로퍼티를 갖는다. 이 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자를 가리킨다.
  • 이 연결은 생성자 함수(객체)가 생성될 때 이뤄진다.
  1. Person 생성자 함수는 me 객체를 생성
  2. me 객체는 constructor 프로퍼티를 통해 생성자 함수와 연결
  3. me 객체에는 constructor 프로퍼티가 없지만 me 객체의 프로토타입인 Person.prototype에는 constructor 프로퍼티가 있기 때문에 me 객체는 프로토타입인 Person.prototype의 constructor 프로퍼티를 상속받아 사용 가능

리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

  • 리터럴 표기법에 의해 생성된 객체도 프로토타입이 존재하지만 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라는 보장이 없다.
const onj = {};

// obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true
  • Object 생성자 함수에 인수를 전달하지 않거나 null, undefiend를 인수로 전달하면서 호출하면 내부적으로 추상연산 OrdinaryObjectCreate를 호출하여 Object.prototype을 프로토타입으로 갖는 빈 객체를 생성하고 프로퍼티를 추가한다.
  • Object 생성자 함수가 생성한 객체가 아니지만 생성자 함수로 생성된 객체와 본질적인 면에서 큰 차이는 없다. 차이점이 있다면 객체 리터럴 방식은 내부에 프로퍼티를 추가하지만 생성자 함수 방식은 일단 빈 객체를 생성한 이후 프로퍼티를 추가해야한다.

추상 연산

  • ECMAScript 사양에서 내부 동작의 구현 알고리즘을 표현한 것

리터럴 표기법생성자 함수프로토타입
객체 리터럴ObjectObject.prototype
함수 리터럴FunctionFunction.prototype
배열 리터럴ArrayArray.prototype
정규 표현식 리터럴RegExpRegExp.prototype

프로토타입의 생성 시점

  • 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성 - 언제나 쌍으로 존재
  • 생성자 함수는 사용자가 직접 정의한 생성자 함수와 JS가 기본 제공하는 빌트인 생성자 함수로 구분

사용자 정의 생성 함수

  • 생성자 함수로서 호출할 수 있는 함수 즉, constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
// 함수 정의와 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
console.log(Person.prototype); // {constructor: f}

function Person(name){
	this.name = name;
}
  • 함수 선언문은 런타임 이전에 JS엔진에 의해 먼저 실행 이때 함수 선언문으로 정의된 Person 생성자 함수는 먼저 함수 객체가 되고 프로토타입도 더불어 생성

  • 생성된 프로토타입은 Person 생성자 함수의 prototype 프로퍼티에 바인딩된다.

  • 모든 객체가 프로토타입을 가지므로 프로토타입도 자신의 프로토타입을 갖는다.

  • 객체는 다른 객체로부터 상속을 받는다. 이 때 상속받는 객체를 '프로토타입'이라고 한다.

  • prototype 프로퍼티는 해당 생성자 함수로 생성될 객체 프로토타입을 참조한다.

function Person(name) {
    this.name = name;
}

let john = new Person("John");
  • Person.prototype은 john 객체의 프로토타입이 되고 person.prototype의 프로토타입은 Object.prototype이다. (JS의 모든 객체는 Object의 인스턴스)
  • Object.prototype은 모든 객체의 최종 프로토타입이다.

빌트인 생성자 함수와 프로토타입 생성 시점

  • Object, String, Number, Function 등과 같은 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 생성되는 시점에 프로토타입 생성되고 생성된 프로토타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩됨
  • 모튼 빌트인 생성자 함수는 전역객체가 생성되는 시점에 생성된다.

> 📌 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재하고 이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당된다.

생성자 함수에 의해 생성된 객체 프로토타입

  • new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하면 다른 객체 생성방식과 동일하게 추상 연산자 OrdunaryObjectCreat가 호출된다. 이때 전달되는 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어있는 객체다.
  • 프로토타입에 프로퍼티를 추가하여 하위(자식) 객체가 상속 받을 수 있도록 구현이 가능하다. 일반 객체와 같이 프로토타입에도 프로퍼티를 추가/삭제 가능하고 이 프로퍼티는 프로토타입 체인에 즉각 반영된다.

프로토타입 체인

  • JS 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다.
  • 상속과 프로퍼티 검색을 위한 메커니즘이다.
  • 프로토타입 체인의 종점에서도 프로퍼티 검색이 안 될 경우 undefiend를 반환한다.
// hasOwnProperty는 Object.prototype의 메서드다.
// me 객체는 프로토타입 체인을 따라 hasOwnProperty 메서드를 검색하여 사용한다.
me.hasOwnProperty('name'); // true
참조 과정
  1. hasOwnProperty 메서드를 호출한 me 객체에서 hasOwnProperty 메서드를 검색하고 메서드가 없다면 [[Prototype]] 내부 슬롯에 바인딩되어 있는 프로토타입으로 이동해 hasOwnProperty 메서드 검색
  2. hasOwnProperty 메서드가 존재한다면 호출하고 Object.prototype.hasOwnProperty 메서드의 this에 me 객체가 바인딩 된다 (hasOwnProperty 경우에는 Object.prototype에 존재)
//call 메서드는 this로 사용할 객체를전달하는 함수를 호출

Object.prototype.hasOwnProperty.call(me, 'name');
  • 프로토타입 체인의 최상위 객체는 언제나 Object.prototype(모든 객체에 상속)이며 프로토타입 체인의 종점이라고 한다.
  • Object.prototype의 프로토타입, 즉 [[Prototype]] 내부 슬롯의 값은 null이다.
스코프 체인과 프로토타입 체인
me.hasOwnProperty('name');
  • 위 예제의 경우 스코프 체인에서 me 식별자를 검색하고 me 객체의 프로토타입 체인에서 hasOwnProperty 메서드를 검색한다. 이처럼 스코프 체인과 프로토타입 체인은 서로 협력하여 식별자와 프로퍼티 검색에 사용된다.

오버라이딩과 프로퍼티 섀도잉

  • 프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스 프로토타입에 추가하면 프로퍼티를 덮어쓰는 것이 아닌 인스턴스 프로퍼티를 새로 추가하게 된다. 이를 오버라이딩(상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식)이라고 한다.
  • 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라고 한다.
  • 하위 객체에서 프로토타입의 프로퍼티를 변경 또는 삭제할 수 없다. (Get은 허용하나 Set은 허용 x)

생성자 함수에 의한 프로토타입의 교체

  • 새로운 프로토타입을 동적으로 변경하기 위해 프로토타입을 교체할 경우 constructor가 없게 된다.(JS가 암묵적 생성) 따라서 constructor 프로퍼티와 생성자 함수간의 연결이 파괴되고 검색 시 Object가 나온다.
  • 연결을 위해서는 교체한 객체 리터럴에 constructor 프로퍼티를 추가하여 프로토타입의 consturctor 프로퍼티를 되살린다.
const Person = (function () {
	function Person(name) {
    	this.name = name;
    }
    
    // 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
    Person.prototype = {
    	// constructor 프로퍼티와 생성자 함수 간의 연결을 설정
       	constructor: Person,
        ~
    }
    ~~
    
    const me = new Person('Lee');
    
    // constructor 프로퍼티가 생성자 함수를 가리킨다.
    console.log(me.constructor === Person); // true
    console.log(me.constructor === Object); // false 
}

인스턴스에 의한 프로토타입 교체

function Person(name){
	this.name = name;
}

const me = new Person('Lee');

// 프로토타입으로 교체할 객체
const parent = {
	sayHello() {
    	console.log(`hi my name is ${name}`);
    }
};

// me 객체의 프로토타입을 Parent로 교체
Object.setPrototypeOf(me, parent);
// 위 코드는  me.__proto__ = parent와 동일하게 동작

me.sayHello(); // hi my name is Lee
  • 위와 같은 경우도 검색 시 parent가 아닌 Object가 검색되고 constructor 프로퍼티와 생성자 함수간의 연결이 파괴된다.

  • 연결을 위해서는

// constructor 프로퍼티와 생성자 함수를 연결해주고
constructor: Person

// 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결을 설정해준다
Person.Prototype = parent;

// me 객체의 프로토타입을 Parent로 변경
Object.setPrototypeOf(me, parent);

console.log(me.constructor === parent); // true
console.log(me.constructor === Object); // false

// 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리킨다
console.log(Person.prototype === Object.getPrototypeOf(me)) // true


instanceof 연산자

  • 이항 연산자로서 좌변에 객체를 가리키는 식별자, 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받는다.
  • 만약 우변의 피연산자가 함수가 아닌 경우 TypeError가 발생한다.
  • 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지 확인한다.
  • 프로토타입 교체 시 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴되어도 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결은 파괴되지 않으므로 instanceof는 아무런 영향을 받지 않는다.
프로토타입 변경 시 체인 상에 존재하는지 확인 가능하다.

// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하지 않기 때문에 false
console.log(me instanceof Person); // false

// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instance Object); // true

직접 상속

Object.create에 의한 직접 상속
  • Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체 생성
  • 다른 객체 생성 방식과 마찬가지로 추상 연산 OrdinaryObjectCreate를 통해 호출
  • 첫 번째 매개변수로 생성할 객체의 프로토타입으로 지정할 객체를, 두 번째 매개변수로 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체를 전달(생략 가능)
  • 첫 번째 매개변수에 전달한 객체의 프로토타입 체인에 속하는 객체를 생성하여 상속을 구현한다.
Object.create 메서드의 장점
  1. new 연산자 없이도 객체 생성 가능
  2. 프로토타입을 지정하면서 객체 생성 가능
  3. 객체 리터럴에 의해 생성된 객체도 상속받을 수 있다.

💡 Object.prototype의 빌트인 메서드를 객체가 직접 호출하는 것은 권장하지 않는다.

  • Object.create 메서드를 통해 프로토타입 체인의 종점에 위치하는 객체를 생성할 수 있다. 프로토타입의 종점에 위치하는 객체는 Object.prototype의 빌트인 메서드를 사용할 수 없다.
// 프로토타입이 null인 객체, 즉 프로토타입 체인의 종점에 위치하는 객체를 생성한다.
const obj = Object.create(null);
obj.a = 1;

console.log(Object.getPrototypeOf(obj) === null); // true

// obj는 Object.prototype의 빌트인 메서드를 사용할 수 없다.
console.log(obj.hasOwnProperty('a')); // TypeError

// 해당 방법을 권장한다.
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); // true

객체 리터럴 내부에서 __proto__에 의한 직접 상속

  • ES6에서는 객체 리터럴 내부에서 __proto__ 접근자 프로퍼티를 사용하여 직접 상속 구현이 가능하다.
const myProto = { x: 10 };

// 객체 리터럴에 의해 객체를 생성하면서 프로토타입을 지정하여 직접 상속받을 수 있다.
const obj = {
	y: 20,
    // 객체를 직접 상속받는다.
    // obj -> myProto -> Object.prototype -> null
    __proto__: myProto
};

/* 위 코드와 동일하다. 기존에는 아래처럼 정의해야 했다.
	const obj = Object.create(myProto, {
    	y: { value: 20, writable: true, enumerable: true, configurable: true }
    });
*/

console.log(obj.x, obj.y); // 10 20
console.log(Object.getPrototypeOf(obj) === myProto); // true

정적 프로퍼티/메소드

  • 정적 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출 가능한 프로퍼티/메서드를 말한다.
// 정적 프로퍼티
Person.staticProp = 'static prop';

// 정적 메서드
Person.staticMethod = function () {
	console.log('staticMethod');
};

const me = new Person('Lee');

// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMethod(); // staticMethod

// 인스턴스로 참조/호출은 불가능하다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError
  • Object.create 메서드는 Object 생성자 함수의 정적 메서드이므로 생성된 인스턴스에서는 접근이 불가능하고

  • Object.prototype.hasOwnProperty 메서드는 Object.prototype의 메서드로 모든 객체의 프로토타입 체인의 종점 Object.prototype의 메서드이므로 모든 객체가 호출 가능하다.

  • this를 참조하지 않는 프로토타입 메서드의 경우 정적 메서드로 변경해도 동일한 효과를 얻을 수 있다.

function Foo() {}

Foo.prototype.x = function() {
	console.log('x');
};

const foo = new Foo();
// 프로토타입 메서드를 호출하려면 인스턴스를 생성해야 한다.
foo.x(); // x

// 정적 메서드
Foo.x = function () {
	console.log('x');
};

// 정적 메서드는 인스턴스를 생서하지 않아도 호출할 수 있다.
Foo.x(); // x

프로토타입 프로퍼티/메서드를 표기할 때 prototype을 #으로 표기(ex. Object.prototype.isPrototypeOf를 Object#isPrototypeOf으로 표기)하는 경우도 있다.


프로퍼티 존재 확인

in 연산자
  • 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인
  • 확인 대상 객체의 프로퍼티뿐만 아니라 확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인한다.(ex. toString이라는 프로퍼티가 없지만 체인 상에서 검색한다)
const person = {
	name: 'Lee',
    address: 'Seoul'
};

console.log('name' in person); // true

Reflect.has 메서드
  • in 연산자와 동일하게 동작
const person = { name: 'Lee' };

console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.has(person, 'toString')); // true

Object.prototype.hasOwnProperty 메서드
  • 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true 반환 상속받은 프로토타입일 경우 false 반환
console.log(person.hasOwnProperty('toString')); // false

프로퍼티 열거

for ... in 문
  • 객체의 모든 프로퍼티를 순회하며 열거
  • 객체의 프로퍼티 개수만큼 순회하며 for ... in 문의 변수 선언문에서 선언한 변수에 프로퍼티 키를 할당 후 코드 블록 실행
  • 상속받은 프로토타입의 프로퍼티까지 열거한다.
  • [[Enumerable]]이 false인 경우에는 열거되지 않는다. (ex. toString)
  • 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않는다.
  • 배열에는 for 문 or for ... of 문 or Array.prototype.forEach 메서드 사용을 권장한다.
const person = {
	name: 'Lee',
    address: 'Seoul'
};

for(const ket in person) {
	console.log(key + ': ' + person[key]);
}

// name: Lee
// address: Seoul

Object.keys/values/entries 메서드

  • 객체 자신의 고유 프로퍼티 열거를 위해서는 위 메서드 사용이 권장된다.
Object.keys
  • 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환한다.
Object.values
  • 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환한다.
Object.entries
  • 객체 자신이 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환
console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]]



20장 strict mode

function foo () {
	x = 10;
}
foo();

console.log(x) // ??
  • JS 엔진에 의해 상위 스코프로 x 변수 선언 검사가 시작된다. 에러가 발생할 것 같지만 JS 엔진이 암묵적으로 x 프로퍼티를 동적 생산해 마치 전역 변수처럼 사용되는 암묵적 전역이 발생한다.
  • 안정적인 코드를 위해 실수를 방지하고 오류를 줄이기 위해 ES5부터 strict mode(엄격 모드)가 추가됨
  • 전역의 선두 또는 함수 몸체의 선두에 'use strict';를 추가하면 스크립트 전체에 strict mode가 적용된다. ( 반드시 선두에 지정해야한다. )

전역에 strict mode 적용을 피하자.

<!DOCTYPE html>
<html>
~
~
<script>
'use strict';
</script>
  • 다른 스크립트에 영향을 주지 않고 해당 스크립트에만 한정 적용된다.
  • strict mode 스크립트와 non-strict mode 스크립트를 혼용하는 것은 오류를 발생시킬 가능성이 높다. ( 서드파티 라이브러리 사용하는 경우 라이브러리가 non-strict mode인 경우도 있기 때문에 전역은 피하자 )

즉시 실행 함수에 적용하는 방법

(function () {
	'use strict';
    
    ~~
}

함수 단위로 strict mode 적용하는 것도 피하자

  • 함수 단위로 적용할 경우 어떤 함수만 적용하고 어떤 함수는 적용하지 않는 것은 바람직 하지 않고 일일이 다 적용하는것은 번거로운 일이다.
  • strict mode는 즉시 실행 함수로 감싼 스크립트 단위로 적용하는 것이 바람직하다.

strict mode가 발생시키는 에러

  1. 암묵적 전역
  • 선언하지 않은 변수를 참조하면 ReferenceError 발생

  1. 변수, 함수, 매개변수의 삭제
  • delete 연산자로 변수, 함수, 매개변수를 삭제하면 SyntaxError 발생

  1. 매개변수 이름의 중복
  • 중복된 매개변수 이름을 사용하면 SyntaxError 발생

  1. with 문의 사용
  • with 문을 사용하면 SyntaxError가 발생한다. (with 문은 전달된 객체를 스코프 체인에 추가한다.)
  • with 문은 동일한 객체의 프로퍼티를 반복해서 사용할 때 객체 이름 생략 가능(코드가 간단해지는 효과가 있지만 가독성이 나빠진다.)
  • 사용하지 않는 것이 좋다.
(function() {
	'use strict'
    
    // SyntaxError
    with({ x: 1 }) {
    	console.log(x);
    }
}());

strict mode 적용에 의한 변화

  1. 일반 함수의 this
  • strict mode에서 함수를 일반 호출하면 this에 undefiend가 바인딩 된다. (생성자 함수가 아닌 일반 함수에서 this를 사용할 필요가 없기 때문에)
  • 에러는 발생하지 않는다.

  1. arguments 객체
  • strict mode에서는 매개변수에 전달된 인수를 재할당하여 변경해도 arguments 객체에 반영 x
(function(a){
	'use strict';
    // 매개변수에 전달된 인수를 재할당하여 변경
    a = 2;
    
    // 변경된 인수가 arguments 객체에 반영 x
    console.log(arguments) // { 0: 1, length: 1 }
}(1));



profile
https://mintmin.dev/ <~~ 블로그 이전했씁니다

0개의 댓글