본문 바로가기

프로그래밍 언어 (プログラミング言語)/javascript

javascript Feat. Code Factory

https://www.miyazaki-city.tourism.or.jp/spot/10137

 

鬼の洗濯板

宮崎のパワースポット青島をとりまく「鬼の洗濯板」は大昔に海面下にあった岩が地質変動によって水面上に現れ、波や海水に浸食されて現在のような形ができ上がりました。 青島から南に

www.miyazaki-city.tourism.or.jp

 

/**
 * 변수(variable) 선언
 * 
 * 1) var - 더 이상 사용하지 않는다고 함(let,const로 전부 대체되었다고 함)
 * -> 레거시 프로그램에서는 여전히 사용되고 있어서 이해하고는 있어야 됨. 
 * -> 그리고 var의 어떤 점 때문에 let,const로 대체되었는 지도 알아야 함. 
 * 2) let
 * 3) const
 */


// var
var name='코드 팩터리';
console.log(name);

var age = 10;
console.log(age);

// let
let ive = '아이브';
console.log(ive);

/**
 * let,var의 공통 특징 : 값 변경 가능(이게 나중에 문제가 많이 된다고 함)
 */
name='코드 팩터링2';
ive='안유진';
console.log(name);
console.log(age+'\n'.repeat(2));

// const - 값 변경 불가
const newJeans = '뉴진스';
console.log(newJeans);

//newJeans='소녀시대';// 에러
//console.log(newJeans+'\n'.repeat(2)); // 에러


/**
 * 변수 선언 : 변수 선언 ex) var name;
 * 변수 할당 : 변수에 값 입력 ex) var name='kim';
 */

// 변수를 선언만 하는 경우
let girlFriend;
console.log(girlFriend+'\n'.repeat(1)); // undefined : 할당x

// const는 선언과 동시에 할당을 해야 한다. 
// const girlFriend2; // 에러

 

javascript의 데이터 타입
 
 1] Primitive type : 
   a-1) Number

// a) Number
const age = 32;
const pi = 3.14;

console.log(typeof age);
console.log(typeof age);
console.log("----------------------------------");

const infinity = Infinity; // 양수의 무한대
const nInfinity = -Infinity; // 음수의 무한대

console.log(infinity); // 출력값 : Infinity라는 문자열
console.log(nInfinity); // 출력값 : -Infinity라는 문자열
console.log(typeof infinity); // number
console.log(typeof nInfinity); // number

Q. console.log(infinity)의 출력값이 왜 숫자가 아닌 Infinitty라는 문자열 형태로 출력이 되는 걸까?

A. Javascript에서는 Infinity와 -Infinity의 데이터 타입은 number이지만 특정한 숫자 크기아 아닌 무한대의 개념을 나타내

는 값이다. 

Infinity(Number Type) : number 타입이며 어떤 유한한 숫자보다 큼을 의미

-Infinity(Number Type) : number 타입이며 어떤 유한한 숫자보다 작음을 의미

 

console.log(Number.MAX_VALUE);  // 1.7976931348623157e+308 (자바스크립트에서 표현 가능한 가장 큰 숫자)
console.log(Infinity > Number.MAX_VALUE); // true

덧붙여서 재미있는 예:

console.log(1 / 0);  // Infinity
console.log(-1 / 0); // -Infinity

 

Q. C/C++,JAVA에서는 0으로 나누면 Exception이 터진다. 근데 왜 javascript에서는 에러가 나지 않고 마치 정상적인 연산인 것처럼 Infinity를 출력할까?

A.  아래의 2가지 이유 때문이다. 

1] Javascript는 IEEE 754 표준을 따르기 때문이다. 

-> Javascript는 IEEE 754 부동 소수점 표준을 따르는데, 이 표준에서는 다음과 같이 정의돼 있다. 

  • 1 / 0 → Infinity
  • -1 / 0 → -Infinity
  • 0 / 0 → NaN (Not a Number) - 이거에 대해서는 아래에서 설명을 해 놓음

즉, "에러" 대신 특수한 값으로 결과를 표현하는 방식이야.

2] 오류 없이 계산을 계속하기 위함

->실제 프로그램에서는 수백, 수천 번의 연산이 돌아가는데 그 중 하나에서만 에러 나도 전체가 멈추면 불편하다.

그래서 이런 특수한 상황에서도 프로그램이 멈추지 않게 Infinity, -Infinity, NaN 같은 값을 반환하게 설계되어 있다.

조금 더 구체적으로 설명을 하겠다. 

이를 위해서는 약간의 수학적 접근이 필요하다. 

Q. 1 / 0은 왜 Infinity?

자바스크립트는 "양수(or 음수)를 0에 가까운 아주 작은 수로 나누면 점점 커지니까, 그걸 무한값이라는 하나의 숫자값으로  간주"해.

1 / 0.000000....00001 → 점점 커짐 → 결국 Infinity로 처리

-1 / 0.00000000001 → 점점  작아짐 → 결국 -Infinity로 처리

NaN(Not a Number, Number Type)

-> 결론부터 말하자면 0/0과 같이 결과값이 무한히 많은 계산일 때, 즉 결과값를 하나로 정할 수 없을 때NaN으로 처리

console.log(0 / 0); // NaN

왜 똑같이 0으로 나누는데 이번에는 Infinity or -Infinity가 나오지 않고 NaN이라는 값으로 나오는 걸까?

먼저 왜 수학적으로 0으로 나누는 것이 안 되는 것인지부터 알아야 한다. 

일반적으로 a / b = c 라면 →b × c = a 가 되어야 한다. 

근데 0 / 0 = x 라고 해보자.

그럼 0 × x = 0이라는 하나의 결과값이 나와야 하는데.....

 

이건 문제임.

  • 왜냐면 x에 어떤 값을 넣든 0 × x = 0 이잖아?
  • 즉, 무한히 많은 x 값이 성립 가능해.
    • 예: 0 / 0 = 1, 0 / 0 = 100, 0 / 0 = -9999... 다 돼버려(즉, 0/0의 결과값은 1/0과는 다르게 하나의 무한값이 아닌 모~~~든 무한값을 포함한 모든 실수값이 다 성립이 되어 버린다)

※이렇게 0/0의 결과가 하나로 정해질 수 없기 때문에 하나의 무한값으로 처리할 수가 없다. 

이와 같은 연산 결과에 대해서 javascript는 IEEE 754 부동 소수점 표준에 따라 NaN으로 처리한다. 

   b) String- 하나 하나의 문자를 리스트 타입으로 묶은 타입.

/**
 * String 타입
 */

const codeFactory = '코드 팩토리';
console.log(typeof codeFactory);

const ive="'안유진' 사랑해";
console.log(ive); // ''이 출력된다. 

const ive2='"안유진" 사랑해';
console.log(ive2); // ""이 출력된다. 

/**
 * escaping charater
 * 1) new Line : \n
 * 2) tab      : \t
 * 3) 백슬래시  : \\
 */
console.log("안유진\t사랑해요\\"+"\n".repeat(2));

/**
 * template literal : escaping charater이 불편해서 탄생
 */
const ive3 = `아이브 ''   / 
사랑해`; // ``와 같은 백 태그로 시작하면 escaping character를 사용하지 않아도 된다. 
console.log(ive3);/** [출력 결과]
                    * 아이브 ''   / 
                      사랑해       
                    */
const ive4 = '아이브';
console.log(`${ive4} 안유진`); // 출력 결과 : 아이브 안유진 
//  백태그를 사용하면 ``안에 변수도 집어 넣을 수가 있다.
//  백태그를 사용하지 않고 single quatation을 사용하면 변수가 문자열로 인식이 되버림.
//  문자열은 되도록 백태그를 사용하자!!!

 

  a-1) BigInt

Number과의 주요 차이점

0] 표기법

const _number = 123; // Number
const _bigInt = 123n; // bigInt는 'n'을 붙여 준다. 

console.log(typeof _number); // number
console.log(typeof _bigInt); // bigint

 

1] 최대 표현 가능 실수

Number.MIN_SAFE_INTEGER  // -9007199254740991  (-2⁵³ + 1)
Number.MAX_SAFE_INTEGER  //  9007199254740991  (2⁵³ - 1)

Number은 최대 표현 실수에 한계가 있는 반면, BigInt에는 최대 표현 실수(정수)에 한계가 없다. 

메모리가 허락하는 하는 범위 내의 정수를 표현 가능. 

console.log(9007199254740991 + 2); // Number : 9007199254740992 (틀림!)
console.log(9007199254740991n + 2n); // BigInt : 9007199254740993n (정확함)

고로, Number 타입은 -9007199254740991 ~ + 9007199254740991까지만 계산이 정확하지만, 

BigInt 타입은 메모리 크기 범위 내에서 무한정 크기의 정확한 계산이 가능하다. 

2] 소수점 지원 가능 여부

const _number = 123.3333; // 소수점 -> 정상
const _bigInt = 123.3333n; // 소수점 -> 에러

-> BigInt 형은 오로지 정수만 입력 가능하다. 

 

3] 타입 섞기 

In JavaScript, you can't mix number and BigInt in arithmetic operations directly.

If you try to add, subtract, multiply, etc. using both at the same time, you will get a TypeError.

❌ Example (this causes an error):

const a = 5;     // number
const b = 5n;    // BigInt

console.log(a + b); // ❌ TypeError: Cannot mix BigInt and other types

✅ Solution – manual conversion:

  1. Convert number → BigInt:
console.log(BigInt(a) + b); // 10n

 

2. convert BigInt → number (⚠ might lose precision!): Not recommended

console.log(a + Number(b)); // 10

 

 

   c) Boolean

/**
 * Boolean : true | false
 */

const isTrue = true;
const isFalse = false;
console.log(typeof isTrue); // boolean
console.log(typeof isFalse); // boolean

 

아래의 리스트는 javascript에서 false 취급하는 값들이다. 

falsy 값들:

  1. false – 불리언 false
  2. 0 – 숫자 0
  3. -0 – 음수 0
  4. 0n – BigInt의 0 -> BigInt는 아래에서 설명
  5. '' – 빈 문자열
  6. null – 값이 없다는 의미
  7. undefined – 정의되지 않음
  8. NaN – 숫자가 아님
if (!false) console.log("false는 falsy");
if (!0) console.log("0은 falsy");
if (!'') console.log("빈 문자열은 falsy");
if (!null) console.log("null은 falsy");
if (!undefined) console.log("undefined는 falsy");
if (!NaN) console.log("NaN은 falsy");

 

Q. BigInt란 뭘까??

A. 일단 BigInt는 Number과는 전혀 다른 데이터 타입이다. 

 

 

 

   d) undefined : 선언만 하고 사용자가 값을 초기화하지 않은 상태

let noInit=undefined;
console.log(noInit); // 출력 : undefined

Q. undefined는 분명 위에서 [데이터 타입]이라고 했는데 왜 변수명에 할당이 가능하지??

A. 그 이유는 바로 undefined는 데이터 타입임과 동시에 을 의미한다. 

고로 let noInit = undefined 에서 이 "undefined"는 데이터 타입이 아닌 값이다. 

이 이외에도 아래 2가지 상황일 때 undefined가 나옴

함수가 아무것도 리턴하지 않을 때

function test() {}
console.log(test()); // undefined

객체에 존재하지 않는 프로퍼티에 접근할 때

const obj = {};
console.log(obj.missing); // undefined

참고로 아래와 같이 변수를 undefined로 초기화하는 것은 미친 짓이다. 

let noInit=undefined; // 이건 미친 짓!!

나중에 아래에서 null 데이터 타입에 대해서 다루겠지만 null와 undefined의 차이는 둘다 변수에 값이 없음을 의미를 하지만 null은 개발자가 명시적으로 변수를 초기화하는 목적으로 주로 사용되며 undefined는 let noInit;과 같이 개발자가 변수를 초기화하지 않았음을 알리기 위한 목적으로 사용된다. 

 

재미있는 javascript의 예시

let noInit2 = Number; // 데이터 타입
console.log(noInit2); // 출력 결과 : [Function: Number]
console.log(typeof noInit2); // 출력 결과 : function

위 코드의 첫째 줄에는 변수에 데이터 타입을 할당하는 듣도 보도 못한 코드가 있다. 

에러가 날 것 같지만 javascript에서는 놀랍게도 지극히 정상적인 코드이다. 

우선 javascript에서는 "함수도 객체(일급 객체)"라는 특징을 알아야 한다(일급 객체는 밑에서 설명)

Number을 예시로 설명하면 Number는 데이터 타입임과 동시에 객체라는 javascript 특징을 이해해야 한다. 

function hello(){

console.log('hello');

}

console.log(typeof hello); // function 출력
console.log(hello instanceof Object); // true 출력 : javascript에서는 【함수==객체】이다.

javascript에서 함수는 객체의 특별한 형태야.

정확히는 함수는 javascript에 내장돼 있는 Function Object(=클래스)의 생성자(new Function(,,,))로 만들어진 객체

그리고 이 Function Object(=클래스) 또한 Object를 상속받아 구현된 것임. 상속 구조는 아래와 같아.

hello 함수
  ↓
Function.prototype  // prototype이 뭔지에 대해서는 지금 단계에서는 넘어가라. 나중에 개념 설명 함.
  ↓
Object.prototype

(javascript는 참 어려운 것 같다. hello가 함수 임과 동시에 instance라니....)

그래서 hello.someProp = 123 같은 식으로 프로퍼티도 붙일 수 있음!

hello.greeting = "안녕~";
console.log(hello.greeting); // "안녕~"

➡ 즉, 함수는 함수이면서도 객체처럼 동작 가능
그래서 **"함수는 (일급)객체"**라는 말이 자연스럽게 나오는 거야.

함수가 객체처럼 동작하는 여러 예시

예시1. 함수에 설정값 저장하기 (config처럼)

function greet(name) {
  console.log(`${greet.prefix} ${name}`);
}

greet.prefix = "Hello";

greet("Alice"); // Hello Alice
greet.prefix = "Hi";
greet("Bob");   // Hi Bob

예시2. 함수 내부 상태 저장용으로 사용하기

function makeLogger(name) {
  function logger(message) {
    console.log(`[${logger.prefix}] ${name}: ${message}`);
  }

  logger.prefix = "LOG";
  return logger;
}

const logAlice = makeLogger("Alice");
logAlice("hello"); // [LOG] Alice: hello

logAlice.prefix = "DEBUG";
logAlice("testing"); // [DEBUG] Alice: testing

예시3. 함수에 메타정보 붙이기

function add(a, b) {
  return a + b;
}

add.description = "두 수를 더하는 함수";
add.version = "1.0.0";

console.log(add.description); // 두 수를 더하는 함수
console.log(add.version);     // 1.0.0

예시4. 생성자 호출

function hello(name){

    // 생략

}


hello.constructor    // ƒ Function()

 

-> 여튼 객체가 할 수 있는 모든 걸 다 할 수가 있다. 왜냐?? javascript에서는 함수는 객체이니깐!!!!!

 

✅ 정리: 함수가 객체처럼 행동하는 이유?

자바스크립트에서 함수는 사실 Function 객체의 인스턴스야.
→ 즉, typeof myFunc === 'function'이지만,
→ myFunc instanceof Object도 true야!

function test() {}
console.log(typeof test); // "function"
console.log(test instanceof Object); // true

Q. 함수가 객체(일급 객체)이라고??

A. 일단 Number는 데이터 타입임과 javascript에서 기본적으로 제공하는 내장 함수이자 객체이다. 

const sayHello = function() { // 함수라는 객체를 대입
  console.log("Hello!");
};

sayHello(); // Hello!

그렇다면 sayHello 변수 메모리에는 어떤 값이 들어가 있을까?

javascript에서는 함수가 [객체]라는 것에 힌트가 있다. 바로 함수가 저장돼 있는 메모리 주소, 즉 [레퍼런스 값]이다.

sayHello 변수에 함수가 저장돼 있는 레퍼런스 값이 들어가 있기 때문에 예를 들어 sayHello()처럼 함수를 호출하거나

만약 sayHello 함수에 매개변수가 있다면 sayHello("Hello World")와 같이 호출할 수 있는 것이다. 

그림으로 상상하면 아래와 같다. 

┌────────┐       ┌──────────────────────────┐
│ sayHello│ ───▶  │ [Function: ]           │
└────────┘       │ 내부 구현: Hello 출력     │
                 └──────────────────────────┘

Q. 일급 객체(First-Class Object)라고????? 먹는 거야 그거??

A. 일급 객체의 정의부터 살펴보자. 아래와 같다.

 

일급 객체 성럽의 3가지 조건

변수에  할당이 가능 && 함수의 인자로 전달 가능 && 함수에서 리턴될 수 있는 무언가를 의미

(javascript에서는 함수는 위 조건을 모두 충족하므로 함수임과 동시에 객체(일급 객체)이다)

(참고로 일급 객체는 타입이 아니라 개념)

1] 변수에 함수 할당

// 함수
function sayHello() {
  console.log("Hello");
}

let greet = sayHello; // [변수에] 함수 자체를  [할당]
greet(); // Hello

✔️ 변수에 함수를 할당하였고, 그 변수로 호출도 가능!

2] 함수의 인자 전달

function runTwice(fn) {
  fn(); 
  fn(); 
}

runTwice(function() { // runTwice의 [인자(변수)]에 함수 자체를 전달
  console.log("Go!");
});

함수가 다른 함수의 인자로 전달됨(= 값처럼 다뤄졌다는 뜻)

3] 함수에서 함수를 리턴할 수 있어야 한다

function makeGreeting() { 

  함수내에서 함수의 [리턴값]으로 전달됨
  return function() { 
    console.log("Hi from returned function!");
  };

}

let greet = makeGreeting(); // 변수에 함수 자체를 [할당]
greet();

 

 

위 예제에서는 리턴된 함수를 변수에 [할당]을 하였지만 리턴된 함수를 변수에 할당할지 말지는 일급 객체의 조건에 들어가

지 않음(개발자의 자유). 함수에서 함수를 리턴만 가능하면 됨. 

 

🎯 자, 여기서 핵심 정리!

조건예시설명
변수에 할당 가능 let f = sayHi; 값을 다루듯 변수에 넣음
인자로 전달 가능 run(sayHi); 다른 함수에 넘김
리턴값으로 사용 return function() {...} 함수가 함수를 리턴함

➡ 이 3가지를 만족하면 우리는 그걸 **"일급 객체"**라고 부르는 거야!

참고로...

자바스크립트에서는

  • 함수(Number,String.Boolean,Symbol 등)
  • 객체(Obejct)
  • 배열
    전부 다 이 세 가지 조건을 만족해 → 다 일급 객체
let number = Number;
let string = String;
let boolean = Boolean;

console.log(typeof number); // function
console.log(typeof string); // function
console.log(typeof boolean); // function


   e) null : 값이 할당 안 된 상태

undefined VS null

null은 개발자가 명시적으로 없는 값으로 초기화하고 싶을 때 사용!

let noInit=null;
console.log(typeof noInit); // 출력 결과 : Object?????????
console.log(noInit); // 출력 결과 : null

Q. typeof noInit의 데이테 타입이 null아니라 Object라고??

A. 그.... 그렇다. 

이건 단순한 javascript 개발자의 실수에 불과하다. 

그러나 이미 【typeof null == Object】로 개발된 레거시 프로젝트들이 너무 많기에 지금까지도 고치지 않고 있음에 불과.

참고로 null 또한 데이터 타입임과 동시에 을 의미한다. 

let noInit=null; // 데이터 타입임과 동시에 null이라는 【값】
console.log(noInit); // 출력 결과 : null


   f)  Symbol

-> 고유하고 유일한 값을 생성할 때 사용! 말로서는 도저히 이해가 안 되니 코드를 보자. 

const test1 = '1';
const test2 = '1';
console.log(test1 === test2); // true

위 코드는 당연히 결과가 true가 출력이 된다. 

하지만 똑같은 '1'이라도 비교 연산 시 false를 나오게 할 수는 없을까??

그것을 가능하게 하는 것이 바로 Symbol이라는 데이터 타입이다. 

const _test1 = Symbol('1');
const _test2 = Symbol('1');
console.log(_test1 === _test2); // false


Symbol은 호출할 때마다 새로운 심볼을 반환하기에 내부적으로는 매번 완전히 다른 유일한 값이 들어가 있다. 

Q. 그럼 진짜 값("1")을 확인할 수 있을까??

A. description이라는 Symbol 함수(객체)의 Property를 사용하면 된다. 

console.log(_test1.description); // '1' 출력

.description은 label값('1')을 확인 가능하게 한다. 

하지만 실제 변수에 들어 있는 심볼 값 자체는 숨겨져 있음 (고유 ID)

Q. Symbol을 왜 쓰냐???

A. 결론부터 먼저 말을 하자면, 객체 Property의 key를 Symbol로 만들게 되면 외부 코드에서는 해당 key에 접근이 불가능

하기 때문에 [우연히 or 악의적으로] 객체의 중요한 정보에 해당하는 Property 값을 변경할 수 없게 되기 때문.

자자 예시를 통해서 구체적으로 알아 보자. 

구체적 설명에 들어 가기 전에 앞서

javascript에서는 객체의 키String 데이터 타입이라는 것을 알고 가자. 

const obj = {
  name: "Alice"
};

console.log(obj["name"]); // 일반적으로 문자열("name")을 key로 사용

다른 코드에서도 "name"만 알면 누구나 이 값에 접근 가능.

// 어떤 모듈 내부 코드
const secretKey = Symbol("mySecret");

const obj = {
  [secretKey]: "비밀 값"
};

이제 이 코드 바깥에 있는 외부 코드가 secretKey라는 변수명을 안다고 해도…

console.log(obj[secretKey]); // ❌ 안 됨, secretKey is not defined

secretKey라는 변수(Symbol 변수)는 내부 스코프에서만 유효하기 때문에외부 코드에서는 변수명을 알아 내서 위와

같이 접근을 하려고 해도 불가능하다. 

 

*Export 키워드를 사용하여 Symbol 객체를 외부에 노출(공유)시키면 접근이 가능하다. 

// A.js
export const secretKey = Symbol("hi");

export const obj = {
  [secretKey]: "hello"
};
// B.js
import { secretKey, obj } from "./A.js";

console.log(obj[secretKey]); // ✅ hello

위와 같이 export 키워드를 이용하여 secretKey 객체와 obj 객체를 외부에 노출(공유)시키면 외부 코드에서도 접근이 가능.

그래서 진짜 중요한 객체의 property라서 외부 코드에서 열거(출력)도 접근도 못 하게 하려면 Symbol 객체를 아래와 같이 

해당 모듈 스코프 안에서만 보관하고 있으면 된다.

// 모듈명 : myLib.js 
const _internal = Symbol("internal"); // 외부 코드 노출(공유)시키면 안 되기 때문에 export 금지

export const obj = {
  [_internal]: "비밀" // 객체의 매우 중요한 property
};

// 외부에서는 _internal에 접근할 수 있는 방법이 없어!

 

Symbol의 2가지 특징

a] Symbol 키는 일반적인 방법으로는 열거(출력)이 안된다.

const secret = Symbol("secret");
const obj = {
  [secret]: "숨겨진 값",
  visible: "보이는 값"
};

console.log(Object.keys(obj));       // ["visible"]
console.log(JSON.stringify(obj));    // {"visible":"보이는 값"}
for (let key in obj) {
  console.log(key);                  // 출력 : "visible" 
}                                    // 위에서 언급하였듯이 javascript는 key의 데이터 타입은 문자열

Object.keys(), JSON.stringify()는 객체의 key(or key-value)를 순차적으로 열거(출력)하는 함수이다. 

그러나 위 결과를 보면 알겠지만 Symbol로 만들어진 key(or key-value)는 열거(출력)이 되지 않고 있는 것을 알 수 있다

Symbol을 이용하면 객체의 중요한 property를 비공개 속성처럼 사용 가능하다. 

b] Symbol 객체의 참조값(레퍼런스 값)을 알면 접근 가능

const s = Symbol("hi"); // Symbol 객체 참조값(레퍼런스 값)
const obj = {
  [s]: "안녕!"
};

console.log(obj[s]); // "안녕!" : 변수 s == Symbol 객체 참조값(레퍼런스 값)
console.log(obj[Symbol("hi")]); // undefined ❌ (다른 심볼이니까)

 

Global Symbol Registry(전역 Symbol Registry) 

"label 값이 붙은 Symbol 객체들을 저장하는 Global Registry"이다. 쉽게 말해서, 공용 Symbol 저장소이다. 

Global Symbol( Symbol.for() / Symbol.keyFor() ) 만들기

필요한 이유 : 여러 파일이나 모듈에서 같은 키를 공유하고 싶을 때!

 

🔹 Symbol() vs Symbol.for()

사용법특징
Symbol("id") 항상 새로운 고유 Symbol 생성
Symbol.for("id") Global Symbol 레지스트리에서 같은 키면 재사용
const x = Symbol("hello");
const y = Symbol("hello");

console.log(x === y); // ❌ false (항상 새로 만들어짐)

반면에

const a = Symbol.for("hello"); // Global Symbol Registry에 저장
const b = Symbol.for("hello"); // Global Symbol Registry에 저장

console.log(a === b); // ✅ true (같은 전역 심볼!)

🔍 레지스트리에서 심볼의 "label"을 찾고 싶을 땐:

const sym = Symbol.for("myKey");

console.log(Symbol.keyFor(sym)); // "myKey"

 

 

2] Object type(Map이랑 매우 비슷)

*Function(함수)과 Array(배열)은 모두 Object 데이터 타입 - Function에 대해서는 깊이 파고 들 것이 너무나도 많기에 

별도의 시간에서 제대로 포스팅하겠다. 

아래와 같이 key : value로 구성돼 있다. 

const dictionary = {
    
    red:'ㅇㅇㅇ',
    blue : 'aaaa',
    green: 'bbbb'
}
console.log(dictionary); // 출력 : { red: 'ㅇㅇㅇ', blue: 'aaaa', green: 'bbbb' }
console.log(typeof dictionary); // Object 데이터 타입

// key로 접근
for(let key in dictionary)
  console.log(dictionary[key]);

 

*Array type(배열)

-> Object 타입은 Map이랑 매우 유사하였다면, Array 타입은 List와 매우 비슷하다. 

const _arrayList = [

    `안유진1`,
    `장원영`,
    {red:"red"},
    12,
    true,
    Symbol("hello"),
    null,
    undefined
]; 

// index로 접근
for(let index=0; index < _arrayList.length; index++)
        console.log(_arrayList[index]); 
/*[출력 결과] - 문자열?처럼 출력이 된다.
안유진1
장원영
{ red: 'red' }
12
true
Symbol(hello)
null
undefined
        
        */
 
for(let index=0; index < _arrayList.length; index++)
    console.log(typeof _arrayList[index]);       
 /*[출력 결과] - 그러나 데이터 타입은 정상. 아마 데이터 타입을 변환하는 과정이 필요
string
string
object
number
boolean
symbol
object
undefined
        
        */

Object와 차이점은 

일단 첫째는 Object는 Map, Array는 List 구조와 매우 비슷하다. 

둘째 Objects는 key값을 기반으로 접근, Array는 index를 기반으로 접근. 

그러나 배열도 결국에 Object 데이터 타입이다.

const _arrayList = [

    `안유진1`,
    `장원영`,
    {red:"red"},
    12,
    true,
    Symbol("hello"),
    null,
    undefined
]; 

console.log(typeof _arrayList); //  Object 데이터 타입

더 정확히는, 배열(Array)은 javascript가 제공하는 Object를 상속받아 구현된 Array라는 기본 내장 클래스이다.  

상속 구조는 아래와 같다. 

배열 (arr) // new Array(,,,)로 생성됨. 
  ↓
Array.prototype
  ↓
Object.prototype
  ↓
null (끝)

 

const arr = [1, 2, 3];

console.log(arr instanceof Array);    // true
console.log(arr instanceof Object);  // true

console.log(typeof arr);             // "object" : 여기가 함수와 다른 점. typeof arr의 출력결과로
                                     //  Array가 출력되지는 않는다.

 

정리

Function(함수)과 Array(배열)는 전부 Object 데이터 타입이다. 

 

Static Typing VS Dynamic Typing

Static Typing : C언어와 Java언어와 같이 변수를 선언함과 동시에 데이터 타입을 명시

ex) int c_language = 123;

Dynamic Typing : JS와 같이 변수의 선언과 동시에 데이터 타입을 명시하지 않고 변수에 대입되는 값에 의해 데이터 타입

을 【추론】

ex) let java_script = '문자열로 추론'; // 변수에 대입되는 값에 의해 java_script 변수의 데이터 타입을 【추론】

*중요

let,var,const 키워드는 변수를 선언하는 키워드이며 데이터 타입과는 관계가 없어. 

또한 이 키워드들은 【변수의 스코프】,【재할당 가능 여부(변수에 값 변경)】, 【호이스팅(Hoisting) 방식】 등에 영향을 준다(아직 안 배움)

 

 

 

 

Hositing(호이스팅) 

console.log('hello');
console.log('world');

// 출력 결과 : hello
               world

위 코드를 실행을 하면 당연히 hello가 먼저 출력이 되고 그 다음에 world가 출력이 될 것이다. 

(만약 결과가 다르다면 그건 인공지능이 지구를 지배하고 있다는 증거???ㅋㅋㅋ)

var name = '코드 팩토리'
console.log(name); // 출력 결과 : 코드 팩토리

이 또한 var 키워드로 변수를 선언한 다음 '코드 팩터리'로 초기화 한 다음에 name을 출력하면, 출력 결과가 당연히

'코드 팩토리'로 출력이 된다. 

하지만 아래의 코드의 경우 상식적으로 이해가 안 되는  출력 결과가 나온다. 

console.log(name); // 출력 결과 : 코드 팩토리 - 응????
var name = '코드 팩토리';
console.log(name); // 출력 결과 : 코드 팩토리

맨 처음의 console.log(name)에서 name이 사용될 수 있는 것과 또한 정상적으로 출력되는 것이 프로그래밍 언어적 관점

에서는 있을 수 없는 일이다. 위 코드는 마치 아래의 코드처럼 동작한다. 

var name = '코드 팩토리'; // name 변수가 최상단으로 이동!!(실제로 이동하지는 않음) 
console.log(name); // 출력 결과 : 코드 팩토리
console.log(name); // 출력 결과 : 코드 팩토리

여기서 Hoisting(호이스팅)이 무엇인지 말 할 수가 있다. 

Hoisting은 모든 변수 선언문이 마치 코드의 최상단으로 이동되는 것"처럼" 느껴지는 현상

위 예시에서는 var의 예시이다. 그럼 let과 const도 hoisting 현상이 일어날까?

console.log(name)
let name = "코드 팩토리";
console.log(name);

위 코드를 실행하면 아래와 같은 에러 메시지가 나온다. 

위 에러 메시지를 보면 조금 이상하다. name이라는 변수가 초기화(initialization)되지 않았다고 나온다. 

즉 name이라는 변수가 hoisting이 되어 일단 【선언】은 된 것이다.(선언만 되어 있으므로 name에는 undefined가 할당)

그러나 var과의 차이점은 var은 hoisting이 되어 초기화된 값이 출력이 되었지만

let은 초기화문(let name="코드 팩토리")이 오기 전에는 변수에 접근이 불가능하다 (const도 똑같은 결과)

(그래서 에러 메시지가 "Cannot Access ''name'' before initialization"으로 나오는 것이다)

(hoisting 현상에 원인에 대해서는 추후에 다룸)

let과 const는 변수가 선언되고 초기화 되기 이전에 해당 변수에 접근하려고 하면 에러를 내준다. 

그러나 var은 그렇지 않다. 이러한 이유로 var을 사용하지 말고 let과 const를 사용하는 것이다. 

왜냐하면 변수가 선언되기 이전에 해당 변수에 접근하는 버그를 var은 막아 주지를 못하기 떄문!!!

 

연산자(Operator)

Q. Number 타입이 아닌 타입에 +,- 연산자를 사용하면 어떻게 될까??

1. String 타입의 경우

let sample='99'; // String 

console.log(+sample); // 99
console.log(-sample); // -99
console.log(typeof +sample); // Number
console.log(typeof -sample); // Number

console.log(sample); // "99"
console.log(typeof sample); // String

sample 자체는 string 타입이지만 +,-를 붙이면 Number 타입이 되어 99,-99가 출력이 된다. 

let sample='숫자가 아닌 문자열의 경우'; // String 

console.log(+sample); // Nan
console.log(-sample); // Nan

 

 

2. Boolean 타입의 경우

let sample=true;

console.log(+sample); // 1
console.log(-sample); // -1
console.log(typeof +sample); // Number
console.log(typeof -sample); // Number

console.log(sample);
console.log(typeof sample); // Boolean
let sample=false;

console.log(+sample); // 0
console.log(-sample); // -0
console.log(typeof +sample); // Number
console.log(typeof -sample); // Number

console.log(sample);
console.log(typeof sample); // Boolean

 

 

비교 연산자 

1] 값을 비교 : 절대 사용해서는 안 되는 JS의 문법

console.log(5 == 5);
console.log(5 == '5'); // true
console.log(0 == ''); //  true

console.log(true == 1); // true
console.log(true == '1'); // true

console.log(false == 0); //true
console.log(false == '0'); // true

 

console.log(5 != 5);
console.log(5 != '5'); // false
console.log(0 != ''); //  false

console.log(true != 1); // false
console.log(true != '1'); // false

console.log(false != 0); //false
console.log(false != '0'); // false

'==' 연산자는 데이터 타입까지는 비교 하지 않는다. 

고로 0 == ''과 같은 것이 true가 되버린다. 

코드를 봐서 알겠지만 절~~~대 사용하면 안 되는 JS의 최악의 스펙이다. 

 

2] 값과 데이터 타입을 동시에 비교 : JS에서는 무조건 이 연산자를 사용해야 한다. 

console.log(5 === 5); // true 
console.log(5 === '5'); // false
console.log(0 === ''); //  false

console.log(true === 1); // false
console.log(true === '1'); // false

console.log(false === 0); //false
console.log(false === '0'); // false
console.log(5 !== 5); // false 
console.log(5 !== '5'); // true
console.log(0 !== ''); //  true

console.log(true !== 1); // true
console.log(true !== '1'); // true

console.log(false !== 0); //true
console.log(false !== '0'); // true

"===" 연산자는 값과 동시에 데이터 타입도 비교를 한다. 

비교 연산자는 무조건 "==="과 "!=="을 사용해야 한다. 

 

short circuit evaluation(단축 평가)

-> short curcuit evaluation을 설명하기에 앞서 &&와 || 연산자의 작동 방식에 대해 먼저 이해를 해야 한다. 

(A 조건식) && (B 조건식)이 있다고 하자. 만약 A 조건식이 false인 경우 B 조건식을 계산할 필요가 있을까??

없다. && 연산자는 두 조건식 중 1개라도 false이면 true를 반환하기 때문이다. 그래서 많은 프로그래밍 언어에서도 B 조건

식을 계산하지 않는다. 

(A 조건식) || (B 조건식)이 있다고 하자. 만약 A 조건식이 true인 경우 B 조건식을 계산할 필요가 있을까??

없다. || 연산자는 두 조건식 중 1개라도 true이면 true를 반환하기 때문이다. 그래서 많은 프로그래밍 언어에서도 B 조건식

을 계산하지 않는다. 

정리 : 

1] && 연산자의 좌측이 false이면 우측을 계산하지 x. 

2] || 연산자의 좌측이 true이면 우측을 계산하지 x. 

위 1~2]의 성질을 이용하여 만들어 진 것이 바로 short circuit evaluation(단축 평가)이다. 

console.log( true && "우측 값 반환");  // 이때, 우측 조건식까지 계산하는 && 연산자의 성질을 이용!
console.log( false && "좌측 값 반환"); // 이때, 우측 조건식까지 계산하지 않는 && 연산자의 성질을 이용!

console.log( true || "좌측 값 반환");  // 이때, 우측 조건식까지 계산하지 않는 || 연산자의 성질을 이용!
console.log( false || "우측 값 반환"); // 이때, 우측 조건식까지 계산하는 || 연산자의 성질을 이용!

 

short circuit evaluation의 장점이자 단점은 "계속 연결"이 가능하다는 것이다. 

console.log(조건식1 && 조건식2 && 조건식3 && ... && 조건식n); // 계속 연결

(조건식 2~n)의 연산을 조건식 1이 true이냐 false이냐에 따라 시키고 싶을 때 사용하면 굉장히 유용하다. 

 

null 연산자

let name; // name === undefined
console.log(name); // 출력 : undefined

name = name ?? '코드 팩토리';
console.log(name); // 출력 : 코드 팩토리 

name = name ?? '김진영';
console.log(name); // 출력 : 코드 팩토리

?? : 왼쪽이 null이거나 undefined의 경우 우측값이 대입된다. 

 

타입 변환(Type Conversion)

1] 명식적 변환

let age = 32;

let toStringAge = age.toString(); // 명시적 타입 변환
console.log(typeof toStringAge, toStringAge); // String, 32

근데 이상한 점이 있다. age.【toString()】에서 보면 알겠듯이, age는 primitive data type인데 마치 Object 타입처럼 사용되

고 있다. 이건 javascript 의 동작 원리를 알아야 한다. 

결론부터 말을 하자면 javascript는 개발자가 변수를 메서드 혹은 프로퍼티로 접근을 할 때 일시적으로 해당 변수의

데이터 타입에 대응되는 객체형으로 Wrapping을 하기 떄문이다. 

예시를 아래에서 들어보자. 

let age = 32;

let toStringAge = age.toString(); // primitive 타입 변수를 메서도로 접근 시도
console.log(typeof toStringAge, toStringAge);

age.toString()으로 age 변수를 메서드로 접근하려고 할 때 javascript는 내부적으로 아래의 3 단계 동작을 한다. 

let temp = new Number(age);      // step 1 : age가 Number type이니 Number 객체(함수)로 Wrapping
let toStringAge = temp.toString(); // step 2 : Number 객체에 내장돼 있는 toString() 메서드 호출
temp = null;                     // step 3 : temp=null을 함으로써 GC의 제거 대상으로 만들어 버림

또 다른 명시적 변환의 예시

// 문자열 타입으로 변환
console.log(typeof (99).toString(), (99).toString());
console.log(typeof (true).toString(), (true).toString());
console.log(typeof (Infinity).toString(), (Infinity).toString());

// 숫자 타입으로 변환
console.log(typeof parseInt('0'), parseInt('0'));
console.log(typeof parseFloat('0.99'), parseFloat('0.99'));

Boolean으로의 타입 변환이 있는데 이건 실무에서 많이 쓰이며 약간의 이해가 필요하다. 

// Boolean 타입으로의 변환
console.log(!'abc'); // false
console.log(!!'abc'); // true

console.log(!''); // true : '' == true이다. 
console.log(!!''); // false

Boolean은 문자열 안에 공백을 포함한 그 어떠한 데이터도 입력되지 않을 떄를 제외하고는 모두 true로 판다. 

(c.java에서 0이외의 숫자를 모두 true로 보는 것과 비슷한 원리???)

 

 

 

2] 암묵적 변환(절대 실무에서는 사용 금지!! 가독성 매우 떨어짐)

let test = age + '';
console.log(typeof test, test); // string, 32

console.log('32' + 2); // 322
console.log('32' - 2); // 30 : string('32')에는 - 연산자가 적용 되지 않기 때문
console.log('32' * 2); // 64 : string('32')에는 * 연산자가 적용 되지 않기 때문
console.log('32' / 2); // 16 : string('32')에는 / 연산자가 적용 되지 않기 때문
console.log('32' % 2); // 0 : string('32')에는 % 연산자가 적용 되지 않기 때문

-,*,/,% 연산자에 대해서는 string(''32'')가 Number로 암묵적 데이터 타입 변환이 일어남

 

 

함수(Function Basic)

일반 함수는 쉬우므로 건너 뛰고 Arrow 함수(화살표 함수)에 대해 알아 보겠다. 

(그러나 함수에 대해서는 javascript는 조금 깊은 이해가 필요하다. 자세한 함수에 대한 내용은 지금은 넘어 가고 클래스나 객체의 개념을 알고 나면 원리가 이해가 될 것이다. 지금은 사용법 위주로만 익히자)

const arrowFunction1 = (x,y)=>{return x*y};
console.log(arrowFunction1(2,3)); 

// Arrow 함수의 정의가 한 줄이며 동시에 return인 경우 아래와 같이 변형이 가능
const arrowFunction2 = (x,y)=> x*y;
console.log(arrowFunction2(2,3));

const arrowFunction3 = (x) => x*2;
console.log(arrowFunction3(3));

// Arrow 함수의 매개변수가 1개인 경우 괄호 생략 가능
const arrowFunction4 = x => x*2;
console.log(arrowFunction4(3));


const arrowFunction5 = x => y => z => `x : ${x}, y : ${y}, z:${z}`;
console.log(arrowFunction5(1)(2)(3));

//위 arrowFunction5 함수를 일반 함수로 고치면 아래와 같다. 
function _arrowFunction5(x){

    return function (y){

        return function(z){
            return `x : ${x}, y : ${y}, z:${z}`;
        
        }
    }
}
console.log(_arrowFunction5(1)(2)(3));

 

다음 개념을 설명하기에 앞서 우리는 함수의 paramater과 arguments의 차이를 알아야 한다. 

function mul(x,y=1){ // x,y이 【paramater】
    return (x*y).toString();
}

mul(2,4); // 2와 4가 [arguments]

arguments : 실제 함수의 paramter에 들어 가는 값. 

 

arguments 키워드 in JS

아래와 같이 함수를 하나 정의를 했다고 하자. 

const argument_using = function (x,y,z){

    return x*y*z;

}

근데 개발자가 함수의 매개변수에 어떤 값이 들어 왔는 지, 즉 arguments 값을 알고 싶어서 출력하려고 할 때,

어떻게 해야 할까?? 일일이 console.log(x), console.log(y),,, 로 하는 방식도 있겠지만 javascript에서는 arguments 키워드

를 사용하면 된다.

const argument_using = function (x,y,z){

    console.log(arguments); // 출력 : [Arguments] { '0': 1, '1': 2, '2': 3 }
	console.log(arguments[0]); // x의 arguments가 궁금할 떄!!!
    return x*y*z;

}

argument_using(1,2,3);

Object.values(객체)는 key-value로 이루어진 객체를 Array 형태로 변환을 해주는 역할을 한다. 

예시 코드는 아래와 같다. 

const user = {
  name: "Bob",
  hobbies: ["reading", "gaming"],
  isAdmin: false
};

console.log(Object.values(user)); 
// 👉 출력: ["Bob", ["reading", "gaming"], false]

 

 

 

또한 만약에 함수의 paramater를 정해진 개수가 아닌 무한대로 받고 싶을 떄는 어떻게 해야 할까??

이떄도 arguments 키워드를 사용하면 된다. 

const argument_using2 = function (...arguments){ // 매개변수가 동적 할당된다. 

    let result = 1;

    for(let index = 0; index  < arguments.length; index++)
            result *= arguments[index];

    return result;
}

const result = argument_using2(1,2,3,4,5,6,7,8,9);
console.log(result);

아래 코드는 위의 코드를 reduce 함수로 다르게 표현한 것이다. reduce 함수에 대해서는 현 단계에서는 이해가 불가능 하

니, 그냥 아~~ 이런 코드도 있구나 정도로 이해하고 넘어 가자(Array 함수 설명할 때 자세하게 배울 예정)

const argument_using2 = function (...arguments){ // 매개변수가 동적 할당된다. 

    return Object.values(arguments).reduce((a,b)=>a*b,1);
}

const result = argument_using2(1,2,3,4,5,6,7,8,9);
console.log(result);

 

즉시 실행 함수(immediately invoked function)

-> 정의와 동시에 실행을 해버리는 함수이다. 

(function (x,y){
    console.log(x*y);
})(2,3);

 

 

Array의 여러 메서드

const ive = ['안유진','장원영','레이','가을','이서','리즈'];

// push() : 맨 끝에 추가
console.log(ive.push('김진영'));
console.log(ive);

// unshift() : 맨 앞에 추가
console.log(ive.unshift('김나리'));
console.log(ive);

// pop() : 맨 끝 요소 삭제
console.log(ive.pop());
console.log(ive);

// shift() : 맨 앞 요소 삭제
console.log(ive.shift());
console.log(ive);

// splice(index1, index2) : index1 ~ index2 요소 삭제
console.log(ive.splice(0,2));
console.log(ive);

강사 왈 : 위 메서드는 되도록 안 쓰는 것이 좋다. 요즈음에는 immutable programming이라고 해서 한 번 선언한 변수는

되도록 변경하지 않는 것이 좋다고 여겨진다고 함. 

그래서 아래와 같은 방식으로 위 메서드를 대체한다고 한다. 

 

concat() : push()를 대체

const ive =[

  '안유진',
  '장원영',
  '레이',
  '리즈',
  '가을',
  '이서',
]

// concat()
console.log(ive.concat('김진영')); // [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ,'김진영']
console.log(ive); // [ '안유진', '장원영', '레이', '리즈', '가을', '이서']

ive.concat('김진영')의 출력 결과 마치 ive.push('김진영')과 똑같은 결과를 출력한다. 

그러나 다른 점이 있다. 그 아래의 코드 console.log(ive)의 출력 결과를 보면 알겠지만 ive Array에는 변환가 없다. 

즉, concat()은 새롭게 객체를 만들어서 반환을 한 것이다.(기존의 변수를 변경하지 않은 immutable programming의 일환)

 

concat() : unshift()를 대체

const ive =[

  '안유진',
  '장원영',
  '레이',
  '리즈',
  '가을',
  '이서',
]

// concat() : unshift() 대체
const newArr = ['김진영'].concat(ive); // index 0이 생략된 거임임
console.log(newArr); // [ '김진영','안유진','장원영', '레이', '리즈', '가을', '이서']
console.log(ive); // [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]

 

 

 

slice() : splice()를 대체

const ive =[

  '안유진',
  '장원영',
  '레이',
  '리즈',
  '가을',
  '이서',
]
// slice()
console.log(ive.slice(0,3)); // [ '안유진', '장원영', '레이' ]
console.log(ive); // [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]

이 또한 concat()과 마찬가지로 ive Array를 변경하지 않고 새로운 Array를 만들어서 반환을 한다. 

slice() : pop()를 대체

const ive =[

  '안유진',
  '장원영',
  '레이',
  '리즈',
  '가을',
  '이서',
]

// slice() : pop() 대체
const newArr = ive.slice(0,-1);
console.log(newArr); // [ '안유진', '장원영', '레이', '리즈', '가을']
console.log(ive); // [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]

slice()에서 음수 인덱스 사용법

음수 인덱스의미
-1 배열의 마지막 요소
-2 배열의 끝에서 두 번째 요소
-3 배열의 끝에서 세 번째 요소
... 계속해서 끝에서부터 셈

 

slice() : shift() 대체

const ive =[

  '안유진',
  '장원영',
  '레이',
  '리즈',
  '가을',
  '이서',
]

// slice() : shift() 대체
const newArr = ive.slice(1); // index 0이 생략된 거임임
console.log(newArr); // [  '장원영', '레이', '리즈', '가을', '이서']
console.log(ive); // [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]

 

spread operator(전개 연산자) : Array와 Object의 값을 그대로 유지한 채로 복사하고 싶을 때 사용

(복사라는 용어에서 힌트가 있듯이 spread operator는 값을 복사하여 새로운 객체를 반환)

// spread operator을 사용한 경우 
let ive2 = [  // 새로운 Array 반환

    ...ive

]
console.log(ive2); // [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]


// spread operator를 사용하지 않은 경우
let ive3 = [ // spread operator를 사용하지는 않지만 새로운 Array 반환

    ive

]
console.log(ive3); // [ [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ] ]


// spread operator을 사용한 경우 
let ive4 = [ // 새로운 Array 반환
 
    ...ive3

]
console.log(ive4); // [ [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ] ]\

// spread operator를 사용하지 않은 경우
let ive5 = [

    ive3

]
console.log(ive5); // [ [ [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ] ] ]

위 출력 결과에서 보면 알겠듯이, spread operator을 차원하면 Array의 차원을 유지한 채로 복사가 되지만, 

spread operator를 사용하지 않으면 차원이 1 차원 증가돼서 출력이 된다. 

아래의 코드는 Object 타입에 spread Operator를 적용한 예시 코드이다. 

const ives1 = {

    name : `안유진`,
    group : `ive`,

};

// 새로운 객체로 반환환
const ives2 = {
    ...ives1 
}

console.log(ives1); // 출력 결과 : { name: '안유진', group: 'ive' }
console.log(ives2); // 출력 결과 : { name: '안유진', group: 'ive' }
console.log(ives1 === ives2) // false

 

*spread operator의 속성값 추가는 넣는 순서가 매우 중요하다. 

-> 엥?? 뭔 말?? ㅋㅋㅋ 일단 코드를 예시로 하여 천천히 이해해 보자. 

일단 아래의 예시 코드는 spread operator를 사용함과 동시에 array의 속성값 or Object의 key-value을 추가할 수 있음을 나타내는 코드이다. 

const ives1 = {

    name : `안유진`,
    group : `ive`,

};

// 새로운 Object로 반환
const ives2 = {

    birth_day : `02/24`, // spread operator 앞에서 추가
    ...ives1,
    
}
console.log(ives2); // 출력 결과 : { birth_day: '02/24', name: '안유진', group: 'ive' };


// 새로운 Object로 반환
const ives3 = {
 
    ...ives1,
	 birth_day : `02/24`,  // spread operator 뒤에서 추가    
}
console.log(ives3); // 출력 결과 : {  name: '안유진', group: 'ive', birth_day: '02/24'};

위 코드를 보면 알겠듯이 spread operator 앞에 key-value를 추가한 것과 뒤에 key-value를 추가한 코드이다. 

console.log로 출력한 결과 추가한 순서를 반영하여 객체에 새롭게 key-value가 추가가 된다. 

자 만약 ive1에 있는 name 값을 변경하고 싶은 경우에는 어떻게 해야 할까???

// 새로운 객체로 반환환
const ives2 = {
    
    ...ives1,
    name : `김진영`, // spread operator [뒤]에서 변경한 경우
}
console.log(ives2); // 출력 결과 : { name: '김진영', group: 'ive' }

위 코드는 spread operator 뒤에서 기존의 name 값을 변경하였다. 

출력 결과도 우리가 원하는 출력 결과일 것이다. 

그러나 spread operator 뒤가 아닌 앞에서 기존의 name 값을 변경하면 어떻게 될까?

const ives1 = {

    name : `안유진`,
    group : `ive`,

};

// 새로운 객체로 반환환
const ives2 = {

    name : `김진영`,  // spread operator [앞]에서 변경한 경우
    ...ives1,
    
}
console.log(ives2); // 출력 결과 : { name: '안유진', group: 'ive' }

name 값을 넣는 순서가 반대로 되었다. 

근데 문제가 우리가 원하는 출력 결과는 name : '김진영'으로 name 값을 변경되는 것인데 실제로는 변경되지 않고 있다. 

그 이유는 name 값을 넣은 순서에 있다. 

name : '김진영'이 실행된 다음에 [...ives1] spread operator가 실행이 되므로 '김진영'이라는 값을 '안유진'으로 덮어 써버린

다.

이런 의미에서 spread operator와 함께 기존의 값을 변경할 때에는 그 값을 넣는 순서가 중요하다는 것이다. 

 

 

다음 코드를 보기 이전에 === 연산자에 대해 다시 짚고 넘어 가야 할 것이 있다. 

결론부터 말하자면, primitive type Object type(Function,Array 포함)에서의 '===' 연산자의 동작 방식은 다르다

primitive type : 값과 데이터 타입을 비교

1 === 1         // true (값 같고 타입도 둘 다 number)
'1' === 1       // false (값은 같지만 타입이 다름: string vs number)
true === true   // true
false === 0     // false (값도 다르고 타입도 다름)

 

Object type(Function,Arryay 포함) : 참조값, 즉 레퍼런스 값을 비교

const a = [1, 2, 3];
const b = a;
const c = [1, 2, 3];

console.log(a === b); // true → 같은 주소를 가리킴
console.log(a === c); // false → 내용 같아도 서로 다른 배열 (다른 주소)

 

그럼 이제 === 연산자에 대해 깊게 알아 봤으니 본 내용으로 돌아 가자

일단 아래 코드를 보자 

const ive =[

  '안유진',
  '장원영',
  '레이',
  '리즈',
  '가을',
  '이서',
]

let ive6 = ive; // 참조값 공유

console.log(ive6);
console.log(ive6 === ive); // true : 참조값 비교 결과 같음
console.log( [...ive] === ive); // false : [...ive]는 값을 복사하여 생성된 새로운 객체!!

=== 연산자가 Object type일 때는 참조값(레퍼런스 값)을 비교한다는 사실을 알았으니 위 코드의 결과가 이해가 될 것이다.

 

Array.join()

-> 결론부터 말하면 반환값은 String이고, Array 객체 안의 모든 value들을 하나의 문자열로 합쳐(join)준다. 

const ive =[

    '안유진',
    '장원영',
    '레이',
    '리즈',
    '가을',
    '이서',
  ]
  
console.log(ive.join('/')); // 출력 결과 : 안유진/장원영/레이/리즈/가을/이서
console.log(ive.join('+')) // 출력 결과 : 안유진+장원영+레이+리즈+가을+이서
console.log(ive.join());  // 출력 결과 : 안유진,장원영,레이,리즈,가을,이서

console.log(typeof ive.join('/')); // string
console.log(typeof ive.join('+')) // string
console.log(typeof ive.join());  // string

만약 join() 안에 어떠한 delimeter를 넣지 않게 되면 default로 콤마(,)가 삽입된다.

 

Array.sort(),reverse()

const ive =[

    '안유진',
    '장원영',
    '레이',
    '리즈',
    '가을',
    '이서',
  ]


// sort() : 오름차순 정렬
ive.sort(); // [ '가을', '레이', '리즈', '안유진', '이서', '장원영' ]
console.log(ive);

// reverse() : ive를 거꾸로 출력
ive.reverse();
console.log(ive); // [ '장원영', '이서', '안유진', '리즈', '레이', '가을' ]

console.log(ive)의 결과 오름차순/반대순대로 정렬이 잘 된 것을 확인할 수가 있다. 

그런데 나는 문득 이런 생각이 들었다. 

이 코드를 immutable하게 만들 수는 없을까?

const immutable1 = [...ive].sort();
console.log(ive === immutable1); // false
console.log(ive); // 출력 결과(원본) : [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]
console.log(immutable1); // 출력 결과 : [ '가을', '레이', '리즈', '안유진', '이서', '장원영' ]


const immutable2 = [...ive].reverse();
console.log(ive === immutable2); // false
console.log(ive); // 출력 결과(원본) :[ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]
console.log(immutable2); // 출력 결과 : [ '이서', '가을', '리즈', '레이', '장원영', '안유진' ]

 

compare function(비교 함수)

const number = [1,4,6,8,4,3,100,34,20]
console.log(number);

console.log(number.sort(
    (a,b)=>{return a > b ? 1 : -1} // 큰 수를 뒤로 두겠다는 의미 
));							       // return 생략 가능

console.log(number); // [1,  3,  4,   4, 6,8, 20, 34, 100]

 (a,b)=>{return a > b ? 1 : -1}의 의미는 

-> a가 b보다 크면(a>b === true이면) 양수를 반환하여 a를 뒤에 정렬한다. 만약 그렇지 않은 경우 음수를 반환하며 a를 앞앞에 둔다(아래의 규칙표를 보면 이해가 될 듯)

a와 b를 비교했을 때
case 1 - a를 b보다 나중에 정렬하려면(뒤에 두려면) 양수 반환
case 2 - a를 b보다 먼저 정렬하려면(앞앞에 두려면) 음수를 반환
case 3 - 원래 순서를 유지하려면 0을 반환 // a와 b를 비교했을 때

아래 코드는 immuatable한 코드로 수정해 본 것이다. 

// immutable programming
const immutable_number = [...number];
immutable_number.sort(
    (a,b)=>{ a > b ? 1 : -1} // return이 생략돼 있음. 
);

 

Array.map() - Array의 모든 값들을 순회한다. 

// map() - mapping
console.log(ive.map(valueOfArray => valueOfArray)); // return 생략됨
                       // 출력 결과 :  [ '안유진', '장원영', '레이', '리즈', '가을', '이서' ]


console.log(ive.map(valueOfArray => `아이브 : ${valueOfArray}`)); // return이 생략됨
/*출력 결과 : 
[
    '아이브 : 안유진',
    '아이브 : 장원영',
    '아이브 : 레이',
    '아이브 : 리즈',
    '아이브 : 가을',
    '아이브 : 이서'
  ]*/

또 다른 예시 코드를 보자

console.log(ive.map(valueOfArray => {

    if(valueOfArray === '안유진')
        return `아이브 : ${valueOfArray}`;
    else
        return valueOfArray;
})) 
// 출력 결과 : [ '아이브 : 안유진', '장원영', '레이', '리즈', '가을', '이서' ]

map()은 immutable programming 방식으로 구현이 돼 있어서 Array.map()는 새로운 객체로 반환이 된다. 

const result = ive.map(valueOfArray => {

    if(valueOfArray === '안유진')
        return `아이브 : ${valueOfArray}`;
    else
        return valueOfArray;
});

console.log(typeof result); // Object
console.log(result === ive); // false

 

Array.filter() - true이면 element 반환, false이면 element 반환x

// filter() - true이면 element를 반환,false이면 element 반환x
const num = [1,8,6,4];

const newNum1 = num.filter(vauleOfArray => true);
const newNum2 = num.filter(vauleOfArray => false);

console.log(newNum1); // 출력 결과 : [ 1, 8, 6, 4 ]
console.log(newNum2); // 출력 결과 : []

//filter()는 immutable progamming 방식
console.log(newNum1 === num); // false
console.log(newNum2 === num); // false

Array.map()과 달리 true,false 여부에 따라 반환될지 안 될지 여부가 결정. 

// 출력 결과 : [ 8, 6, 4 ]
console.log(num.filter(vauleOfArray => (vauleOfArray % 2 === 0))); // (vauleOfArray % 2 === 0)가 true일때만 valueOfArray반환

 

Array.find(), findIndex()

// find()
const newFind = num.find(vauleOfArray => (vauleOfArray % 2 === 0)); 
console.log(newFind); // 출력 결과 : 8

// findIndex()
const newFindIndex = num.findIndex(vauleOfArray => (vauleOfArray % 2 === 0));
console.log(newFindIndex); // 출력 결과 : 1

기본적으로 find()는 filter()와 같이 true일 때에만 반환을 한다.

그러나 다른 점은 조건식이 true인 첫 번째 element를 만나면 해당 element를 반환을 하고, 더 이상 다른 element를 

순환하지 않는다는 점이다. 

findIndex()는 조건식이 true인 첫 번째 element의 index을 반환하는 것이다. 

 

Array.reduce()

const num = [1,8,6,4];

// reduce()
const newReduce = num.reduce((previous,next)=> previous + next ,0);
console.log(newReduce); // 출력 결과 : 19(=1+8+6+4)

우선 reduce()함수는 기본적으로 2개의 매개변수를 갖는다. 

Array.reduce(함수,시작값)이다. 

reduce((previous,next)=> previous + next,0) 코드를 예시로 살펴 보자. 

reduce()의 첫번 째 매개변수인 함수, 즉 (previous,next)=> previous + next의 의미를 뜯어 보자. 

num Array의 모든 element를 순환을 하는데 순환을 할 떄 마다 매개변수 next에 그 값이 들어 간다. 

그리고 previous에는 그 이전에 실행된 함수로 부터 받은 반환값이 들어 간다.

(내가 생각하기에는 이렇게 이전 함수의 반환값을 가지고 있어야 할 때 reduce 함수를 사용하는 게 아닐까 싶다)

그리고 제일 첫번쨰 함수 실행 시에는 반환값이 없으니, reduce()의 두 번째 매개변수 부분에 반환값을 미리 

정해 둔 것이다. 고로 숫자 0은 (previous,next)=> previous + next 함수가 첫 번째 실행될 때 previous에 대입이 된다. 

눈치를 챘겠지만 위 reduce() 함수는 num Array에 있는 모든 element을 더하는 함수이다. 

 

 

객체(Object) 

const object = {

    name : `안유진`,
    group : `아이브`,

    // 함수도 정의 가능
    dance : function(){
        return `안유진이 춤을 춥니다`;
    }
}

console.log(object.name); // 출력 결과 : 안유진
console.log(object['name']); // 출력 결과 : 안유진
 
const name = 'name';
console.log(name); // 출력 결과 : 안유진




console.log(object.dance()); // 출력 결과 : `안유진이 춤을 춥니다`

const func = object.dance;
console.log(func === object.dance); // true
console.log(func());  // 출력 결과 : `안유진이 춤을 춥니다`

 

javascript에서 객체는 key-value로 구성이 되어 있다. 

위 코드에서 봐서 알겠듯이 property 이외에도 key-value 형식으로 함수도 정의가 가능하다. 

 

this 키워드

그러나 위 코드는 뭔가가 아쉽다. object 객체 안의 dance 메서드에는 `안유진`이라는 부분이 있는데 

이 부분은 object 객체의 name : `안유진`과 중복이 된다. 

프로그래밍에서 중복은 최대한 피해야 하는 버그와 맞먹은 존재이다. 

고로, 아래와 같이 수정할 수가 있다. 

const object = {

    name : `안유진`,
    group : `아이브`,

    // this 이용
    dance : function(name){
        return `${this.name}이 춤을 춥니다. `+`${name}은 매개 변수입니다.`;
    }
}

console.log(object.dance("김진영")); 
// 출력 결과 : 안유진이 춤을 춥니다. 김진영은 매개 변수입니다

대부분의 언어에서 this 키워드가 존재하는데, javascript에 this 키워드는 상황마다 다르게 동작한다. 

이 상황에서 this는 객체 자신을 가르키게 동작한다고 이해하고, 다른 동작에 대해서는 추후에 자세히 설명하겠다. 

 

* this 값은 함수를 호출할 때 동적으로 결정된다!!!!

아래의 코드를 일단 먼저 보자.

const object = {

    name : `안유진`,
    group : `아이브`,

    // 함수도 정의 가능
    dance : function(name){
        return `${this.name}이 춤을 춥니다. `+`${name}은 매개 변수입니다.`;
    }
}


const func = object.dance;
console.log(func(`김진영`));

console.log(func(`김진영`))의 출력 결과는 과연 무엇일까?

바로, 아래와 같은 결과를 출력한다. 

undefined이 춤을 춥니다. 김진영은 매개 변수입니다.

왜 this.name이 undefined로 되어 있는 걸까?

그건, this의 동작 방식에 비밀이 있다. 

기본적으로 함수는 this 값을 내부적으로 기억(저장)하고 있지 않다. 

함수 호출 시점에 어떤 객체로 호출되었는 지를 보고 동적으로 this 값이 할당이 된다

const func = object.dance 이 시점에 this(or this.name) 값은 아무런 값이 할당되지 않은 undfined 상태이다. 

그 다음에 func("김진영")이라는 호출 시점에 객체의 메서드로 호출된 것이 아닌 일반 함수로 호출이 되었다. 

그러니 당연히 this 값은 여전히 값을 가지고 있지 않는 상태이다. 

만약 object.func("김진영")의 경우, 호출 시점에 object 라는 객체로 호출을 하였으므로 this에는 동적으로 

object의 참조값이 할당되며 this.name = '안유진'이라는 값이 초기화가 된다. 

const nameKey = "name";
const nameValue = "안유진";

const groupKey = "group";
const groupValue = "아이브";

const object = {

    [nameKey] : nameValue, // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 
    [groupKey] : groupValue,  // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 
	
    // 대괄호 생략 가능 : 단지, [] 대괄호를 붙여 줌으써 변수로 선언함을 명시적으로 의미 전달 가능 
    
    dance : function(name){
        return `${this.name}이 춤을 춥니다. `+`${name}은 매개 변수입니다.`;
    }
}


console.log(object.dance(`김진영`));

 

 

속성(property)의 추가/삭제

// 속성 추가
object.bigFan1 = "김진영1";
object['bigFan2'] = "김진영2";

console.log(object); 
/* 출력 결과 : {
    name: '안유진',
    group: '아이브',
    dance: [Function: dance],
    bigFan1: '김진영1',        // 추가된 속성
    bigFan2: '김진영2'        // 추가된 속성
  }
*/

// 속성 삭제
delete object.bigFan1;
delete object['bigFan2'];
console.log(object); // 출력 결과 :{ name: '안유진', group: '아이브', dance: [Function: dance] }

 

그러나 객체 part의 예시 코드들을 보면서 객체를 모조리 const로 선언을 하였다. 

const로 선언을 하면 변경이 불가능하다고 들었는데, 어떻게 해서 객체의 속성(property)를 추가/삭제가 가능한 것일까?

javascript에서 const로 선언한 객체의 2가지 특징이 있다. 

1] const로 선언을 한 경우, 한 번 초기화된 객체 자체를 재정의 x. 

const object = { // const

    [nameKey] : nameValue, // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 
    [groupKey] : groupValue,  // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 

    dance : function(name){
        return `${this.name}이 춤을 춥니다. `+`${name}은 매개 변수입니다.`;
    }
}

object = {...........}; // 이렇게 객체를 재정의하는 것은 불가능.

 

2] 그러나 객체 안의 속성(property)나 메서드는 변경(기존 value 변경, property 추가/삭제) 가능

const object = { // const

    [nameKey] : nameValue, // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 
    [groupKey] : groupValue,  // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 

    dance : function(name){
        return `${this.name}이 춤을 춥니다. `+`${name}은 매개 변수입니다.`;
    }
}

//object = {...........}; // 이렇게 객체를 재정의하는 것은 불가능.

// 아래의 방식은 가능
obejct['name'] = "김진영";
obejct["dance"] = function(name){....}

 

JS 최신 문법에서의 객체 선언

const name = "안유진";

const object3 = {
    name,
}
console.log(object3); // 출력 결과 : { name: '안유진' }

const object3 = { name,}은 const object2 = {[name] : name} or const object3 = {name: name}과 똑같다. 

 

객체의 유틸리티(utility) 기능

1] Object.keys() - Array 타입으로 새로운 객체 반환

const nameKey = "name";
const nameValue = "안유진";

const groupKey = "group";
const groupValue = "아이브";

const object = {

    nameKey : nameValue, // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 
    groupKey : groupValue,  // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 

    dance : function(name){
        return `${this.name}이 춤을 춥니다. `+`${name}은 매개 변수입니다.`;
    }
}

const ObjectToArray = Object.keys(object); // Array 타입으로 새롭게 반환
console.log(ObjectToArray === object); // false
console.log(ObjectToArray instanceof Array); // true


for(const key of ObjectToArray) {

    console.log(key);

}
for(const index in ObjectToArray){

    //console.log(object[index]); // object는 Array 타입이 아니므로 object[key]로 접근해야 함. 
    console.log(ObjectToArray[index]);
}

 

2] Object.values()  - Array 타입으로 새로운 객체 반환

const nameKey = "name";
const nameValue = "안유진";

const groupKey = "group";
const groupValue = "아이브";

const object = {

    nameKey : nameValue, // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 
    groupKey : groupValue,  // 변수를 key로 사용할 때에는 [] 대괄호가 필요. 

    dance : function(name){
        return `${this.name}이 춤을 춥니다. `+`${name}은 매개 변수입니다.`;
    }
}

const ObjectToArray = Object.keys(object); // Array 타입으로 새롭게 반환
console.log(ObjectToArray === object); // false
console.log(ObjectToArray instanceof Array); // true

for(const value of ObjectToArray){
    console.log(value); // 출력 결과 : [ 안유진, 아이브, [Function : dance] ]
}
for(const index in ObjectToArray){

    console.log(ObjectToArray[index]);

}
/**출력 결과
 * 안유진
   아이브
   [Function: dance]
 * 
 */

 

클래스 기본기(Class Basics)

class IdolModel{

    name = `안유진`; // Property
    birth = `02/24`; // Property
}

const ins = new IdolModel();
console.log(ins); // 출력 결과 : IdolModel { name: undefined, birth: undefined }
console.log(typeof ins); // object
console.log(ins instanceof Object); // true
console.log(ins instanceof IdolModel); // true

JAVA,C++과 같은 언어를 해 본 사람이라면 위 코드를 이해하는 데 문제가 없을 것이므로 넘어 간다. 

그러나 하나 알아 둘 것이 있다. 

console.log(ins), 이 코드의 출력 결과가 왜  IdolModel { name: undefined, birth: undefined } 이와 같이 

Object의 key-value 형태로 출력되는 것일까?

그 이유에 대해서는 추후에 클래스를 더 깊게 다룰 때 배우게 될 것이다.

지금은 객체를 출력하면 [클래스명][key-value 형태의 객체 속성/메서드]와 같은 형식으로 출력이 된다는 것만

알아 두면 된다. 

 

생성자(constructor)

class IdolModel{

    name; 
    birth; 

    constructor(name,birth){

        this.name = name;
        this.birth = birth;
    }
}

const ins = new IdolModel("안유진","02/24");
console.log(ins); // 출력 결과 : IdolModel { name: '안유진', birth: '02/24' }

위 내용도 JAVA,C++을 해본 사람이라면 이해하는 데 아무 문제가 없을 것이다. 

그러나 아래 코드가 정상적으로 동작한다는 데에는 의문이 들 것이다. 

class IdolModel{

    name; 
    birth; 

    constructor(name,birth){

        this.name = name;
        this.birth = birth;
    }
}

const ins = new IdolModel();
console.log(ins); // 출력 결과 : IdolModel { name: undefined, birth: undefined }

JAVA, C++에서는 매개변수가 있는 생성자를 만들게 되면 더 이상 컴파일러가 default constructor를 자동으로 생성해 주지

않기 때문에 개발자가 직접 default constructor를 생성하지 않은 채 호출을 하게 되면 에러가 당연히 발생한다. 

그러나 javascript에서는 문법 상 default constructor를 생성하지 않아도 에러가 나지 않는다. 

그리고 javascript는 개박살난? 언어이기에 아래와 같이 클래스 안에 속성 값을 지정하지 않아도 된다. 

class IdolModel{

    constructor(name,birth){

        this.name = name;
        this.birth = birth;
    }
}

const ins = new IdolModel("안유진",2002);
console.log(ins);  // IdolModel { name: '안유진', birth: 2002 }

 

메서드 생성 예시

class IdolModel{

    name;
    year;

    constructor(name,birth){

        this.name = name;
        this.birth = birth;
    }

    sayName() {
        return `Hello ${this.name}`;
    }
}

const ins = new IdolModel("안유진",2002);
console.log(ins.sayName()); // Hello 안유진

 

 

+javascript에서 Class는 Function 타입이다~~~~~~~~~~~~~~~~~~~~~~~!!!!!!!!!!!!!!!!

-> 와우! ㅈ같은 javascript!!! 이젠 하다못해 Class가 Function타입이란다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

그 이유에 대해서는 추후에 자세히 다룸. 일단은 코드로 확인만 하자

class IdolModel{

    name;
    year;

    constructor(name,birth){

        this.name = name;
        this.birth = birth;
    }

    sayName() {
        return `Hello ${this.name}`;
    }
}

const ins = new IdolModel("안유진",2002);
console.log(typeof IdolModel); // Function
console.log(typeof ins); // Object
console.log(ins instanceof Object); // true

 

Getter and Setter

class IdolModel{

    name;
    year;

    constructor(name,year){
        this.name = name;
        this.year = year;
    }

    // 키워드 get
    get getName(){return this.name};
    get getYear(){return this.year};
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin); // 출력 결과 : IdolModel { name: '안유진', year: 2003 } 
console.log(yujin.getName); // 출력 결과 : 안유진
console.log(yujin.getYear); // 출력 결과 : 2003

get 키워드를 앞에 붙여 주는 것 이외에는 다른 언어와 그 용도와 사용법이 다를 것이 없다. 

그러나 하나 주의하고 가자. 

위 코드에서 getter의 호출 방법이다. 우리는 getter를 함수로 정의하였다. 

다른 언어에서 함수를 호출할 때에는 yujin.getName(    )과 같이 괄호 ()를 붙였지만, 

javascript에서는 get 키워드로 정의된 함수는 하나의 key값 처럼 메서드를 반드시 호출해야 한다.

ex) yujin.getName: 클래스의 key 값 호출하듯 함수를 호출 

아래 코드는 괄호 ()를 사용해서 getter를 호출한 에러 코드의 예시이다. 

class IdolModel{

    name;
    year;

    constructor(name,year){
        this.name = name;
        this.year = year;
    }

    // 키워드 get
    get getName(){return this.name};
    get getYear(){return this.year};
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin); // 출력 결과 : IdolModel { name: '안유진', year: 2003 } 
console.log(yujin.getName()); // 에러
console.log(yujin.getYear()); // 에러

 

일반 메서드의 경우는 반드시 괄호 ()를 붙여서 호출해 줘야 한다

class IdolModel{

    name;
    year;

    constructor(name,year){
        this.name = name;
        this.year = year;
    }

    // 키워드 get
    get getName(){return this.name};
    get getYear(){return this.year};

    say(){
        console.log("I should study english harder than now");
    }
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin.say); // 출력 결과 : [Function : Say]
yujin.say() // 출력 결과 : I should study english harder than now

 

자 이제 setter에 대해 배워보자. 아래 코드를 봐라

class IdolModel{

    #name; // # -> private
    #year; // # -> private

    constructor(name,year){
        this.name = name;
        this.year = year;
    }
    
    // 키워드 get
    get getName(){return this.name};
    get getYear(){return this.year};
    
    
    // 키워드 set
    set name(name){
        this.name = name;
    }
    set year(year){
        this.year = year;
    }
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin); // 출력 결과 : IdolModel { name: '안유진', year: 2003 }


yujin.name="김진영"; // 이때 name은 속성(property)의 name x -> setter를 호출한 것이다
yujin.year=1994; // 이때 year 속성(property)의 year x -> setter를 호출한 것이다
console.log(yujin); // 출력 결과 : IdolModel { name: '김진영', year: 1994 }

setter의 설명을 위해 클래스의 속성(property) name, year는 private이라고 가정한다. 

getter와 마찬가지로 괄호 ()로 호출하면 에러가 나고, 반드시 key-value의 key값처럼 호출해야 한다. 

이때의 yujin.name의 name이 속성(property) key name이 아닌 setter의 함수 이름이라는 것은 굳이 설명 안 하겠다

javascript의 set 키워드는 한 가지 주의 사항이 있다. 

java, c/cpp와 달리 set 키워드가 붙은 함수(setter)에 반드시 매개변수가 한 개여야 된다는 것이다. 

그렇지 않으면 에러가 난다. 

(아래의 사진 참조)

 

 

* setter와 getter는 함수(function)이 아니다?????????????

아래의 코드를 실행을 해 보자. 

class IdolModel{

    #name; // # -> private
    #year; // # -> private

    constructor(name,year){
        this.name = name;
        this.year = year;
    }


    // 키워드 get
    get getName(){return this.name};
    get getYear(){return this.year};


    // 키워드 set
    set setName(name){
        this.name = name;
    }
    set setYear(year){
        this.year = year;
    }
}

const yujin = new IdolModel("안유진",2003);


console.log(yujin.getName()); // 에러 메시지 :  yujin.getName is not a function
console.log(yujin.getYear());  // 에러 메시지 : yujin.getYear is not a function


yujin.setName("김진영");  // 에러 메시지 : yujin.setName is not a function
yujin.setYear=(1994);  // 에러 메시지 : yujin.setYear is not a function

getter와 setter를 함수처럼 호출하자 "~~~ is not function", 즉 getter와 setter는 함수가 아니라는 TypeEroor 메세지가 나온

다.

분명 우리는 함수로서 getter와 setter를 정의하였는데 왜 함수가 아닐까?라는 의문이 들었다. 

 결론부터 말하면 javascript에서는 get, set 키워드가 붙은 것은 함수가 아닌 메서드 형태의 속성(property)이다.

yujin.getName()을 예시로 설명을 해보겠다. 

javascript는 괄호 ()가 붙으면 getName이 함수인지 아닌지 확인을 한 후, 만약 함수이면 정상 호출을 하고 

만약 함수가 아니면 에러 메시지(TypeError : "~~~ is not function" )를 발생시킨다. 

get getName() 으로 만든 것은 진짜 함수가 저장된 게 아니라, getter 정의 자체다.

이건 속성처럼 동작하라고 정의된 메서드 형태의 프로퍼티일 뿐, 진짜 함수 객체(Function object)가 들어 있는 건 아니다. 

(만약 함수로서 getter와 setter를 동작시키고 싶다면 일반 메서드로 getter와 setter를 만들면 된다)

 

그러나 setter는 되도록 만들지 않는 것이 좋다. 

왜냐하면 현대의 프로그래밍 방식은 immutable programming 방식을 따르기에 속성값을 변경하는 기능인 setter는

잘 안 만든다. 그냥 "이런 것이 있다"라고 정도만 생각하고 넘어 가자. 

아래 코드 예시는 setter를 제거한 immutable programming의 예시이다. 

class IdolModel {
    #name;
    #year;

    constructor(name, year) {
        this.#name = name;
        this.#year = year;
    }

    get getName() {
        return this.#name;
    }

    get getYear() {
        return this.#year;
    }

    // 이름을 바꾼 새 객체 반환
    withName(newName) {
        return new IdolModel(newName, this.#year);
    }

    // 생년을 바꾼 새 객체 반환
    withYear(newYear) {
        return new IdolModel(this.#name, newYear);
    }
}

const yujin1 = new IdolModel("안유진",2003);
console.log(yujin1);


// immutable programming
const yujin2 = yujin1.withName("장원영");
const yujin3 = yujin1.withName("레이")
console.log(yujin2); // 출력 결과 : IdolModel {} -> 엥??????????????????
console.log(yujin3); // 출력 결과 : IdolModel {} -> 엥??????????????????
console.log(yujin1 === yujin2); // false
console.log(yujin1 === yujin3); // false


const yujin4 = yujin2.withYear(2000);
const yujin5 = yujin3.withYear(2002);
console.log(yujin4); // 출력 결과 : IdolModel {} -> 엥??????????????????
console.log(yujin5); // 출력 결과 : IdolModel {} -> 엥??????????????????
console.log(yujin2 === yujin4); // false
console.log(yujin3 === yujin5); // false

withName(newName), withYear(newYear) 함수를 통해 name, year 속성(property)을 변경한 새로운 IdolModel 객체가 

반환되도록 하여 immutable programming 방식을 구현하였다. 

그러나 위 코드에서 한 가지 이상한 결과가 있다. 

console.log(yujin1,2,3,4,5)의 출력 결과가 "IdolModel {}"라는 것이다. 우리 예상에는 당연히 {} 안에 name과 year의 속성값

이 같이 출력되어야 하는데 이게 무슨 일일까?

원인은 바로 name과 year가 private이라는 것이다. 

java의 경우 system.out.println(객체)를 실행할 경우 자동으로 객체.toString()이 호출돼 자동으로 속성값이 출력이 되지만 

javascript의 경우 개발자가 직접 toString()를 만들어 직접 호출하는 방법 밖에 없다. 

private 속성값은 아예 출력해 주지 않는다. 

toString()

class IdolModel{

    #name; // # -> private
    #year; // # -> private

    constructor(name,year){
        this.#name = name;
        this.#year = year; 
    }

    // 키워드 get
    get getName(){return this.#name};
    get getYear(){return this.#year};
    
    withName(newName){
        return new IdolModel(newName,this.#year);
    }

    withYear(newYear){
        return new IdolModel(this.#name,newYear);
    }

    toString(){
        return `IdolModel { name: ${this.#name}, year: ${this.#year}}`
    }

}

const yujin1 = new IdolModel("안유진",2003);
console.log(yujin1);


// immutable programming
const yujin2 = yujin1.withName("장원영");
const yujin3 = yujin1.withName("레이")
console.log(yujin2.toString()); // 출력 결과 : IdolModel { name: 장원영, year: 2003 }
console.log(yujin3.toString()); // 출력 결과 : IdolModel { name: 레이, year: 2003 }
console.log(yujin1 === yujin2); 
console.log(yujin1 === yujin3); 


const yujin4 = yujin2.withYear(2000);
const yujin5 = yujin3.withYear(2002);
console.log(yujin4.toString()); // 출력 결과 : IdolModel { name: 장원영, year: 2000 }
console.log(yujin5.toString()); // 출력 결과 : IdolModel { name: 레이, year: 2002 }
console.log(yujin2 === yujin4); 
console.log(yujin3 === yujin5);

private 필드는 무조건 해당 클래스 내에서만 접근이 가능하므로 toString()은 클래스 내부에서 정의돼야 한다.

 

번외 : Json.stringify() - private 필드(name,year)의 접근은 불가능하다

우선 Json.stringify()의 스펙은 아래와 같다. 

JSON.stringify(value, replacer, space);

 

value JSON 문자열로 변환할 대상 객체
replacer (선택) 어떤 속성을 포함할지 결정하는 함수 또는 배열
space (선택) 들여쓰기(indent) 수준: 보기 좋게 포맷팅할 때 사용

✨ 그럼 이건?

JSON.stringify(yujin3, null, 2)

 

yujin3 JSON으로 바꾸고 싶은 대상 객체
null replacer 없음 → 모든 속성 포함 (기본값)
2 들여쓰기 2칸으로 예쁘게 포맷팅해서 출력해줘!

📄 예시 비교

👉 JSON.stringify(obj)

{"name":"레이","year":2003}

👉 JSON.stringify(obj, null, 2)

{
  "name": "레이",
  "year": 2003
}

 

replacer로 배열을 전달 → 특정 key만 선택( 포함할 key만 지정 )

const idol = {
  name: "안유진",
  year: 2003,
  group: "IVE",
  position: "Leader"
};
// replacer로 배열(Array) [] arguments를 넘김. 
const json = JSON.stringify(idol, ["name", "group"], 2);
console.log(json);

🔍 출력 결과

{
  "name": "안유진",
  "group": "IVE"
}

 

replacer로 함수 전달 → 값을 필터링하거나 수정( key, value를 조작하거나 제거 가능 )

const idol = {
  name: "장원영",
  year: 2004,
  group: "IVE",
  height: 171
};

const replacerFn = (key, value) => {
  if (key === "height") {
    return `${value}cm`;  // cm 붙이기
  }
  if (key === "year") {
    return undefined;  // 이 속성은 제외!
  }
  return value; // 나머지는 그대로
};

const json = JSON.stringify(idol, replacerFn, 2);
console.log(json);

🔍 출력 결과

{
  "name": "장원영",
  "group": "IVE",
  "height": "171cm"
}

🧠 replacer 함수 구조

(key, value) => {
  // key: 현재 키 이름
  // value: 그 키에 해당하는 값
  return 바꾸고 싶은 값 or undefined;
}

🔥 요약

형태                                                    동작
배열 replacer 포함할 key만 지정
함수 replacer key, value를 조작하거나 제거 가능

 

 

static

class IdolModel{

    name;
    year;
    static groupName = "아이브"; // static

    constructor(name,year){
        this.name=name;
        this.year=year;
    }

}

const yujin = new IdolModel("안유진",2003);
console.log(yujin); // 출력 : IdolModel { name: '안유진', year: 2003 }
console.log(yujin.groupName); // 출력 :  undefined
console.log(IdolModel.groupName); // 출력 : 아이브

static 키워드에 대해서는 다른 언어에서도 자주 나왔던 개념이니 구체적인 설명은 하지 않겠다. 

그냥 static 키워드는 객체에 속하지 않는다. 이것만 알아 두면 된다. 

고로 console.log(yujin.groupName)의 출력 결과가 undefined가 나온 것은 결코 groupName이 초기화가 되지 않았다는

의미가 아니라 객체 yujin에는 애초에 groupName이 없기 때문에 undefined가 나오는 것이다. 

그럼 어디에 groupName이 있냐? 바로 클래스 레벨에서 존재한다. 

 

static 함수 내에서의 this 키워드

우선 아래의 코드를 보자

class IdolModel{

    name;
    year;
    static groupName = "아이브"; // static

    constructor(name,year){
        this.name=name;
        this.year=year;
    }


    static returnGroupName(){  // static
        return groupName; // 여기서 에러 발생 : ReferenceError
    }
}

const yujin = new IdolModel("안유진",2003);


console.log(IdolModel.groupName); // 출력 : 아이브
console.log(IdolModel.returnGroupName()); // 에러 : ReferenceError

우선 위 코드는 에러가 나는 코드이다. 

(java에서는 절대 나지 않는 코드인데 javascript는 개박살난 언어이기에 에러가 난다)

그 이유를 설명을 하겠다. 

javascript는 함수 안에서 return groupName과 같이 그냥 변수 이름만 쓰면, 그 변수를 전역 or 지역 변수로 인식.

그러나 groupName은 전역 변수도 아니고 함수 내부에서 선언된 지역 변수도 아니다. 

그럼 어떤 변수냐?? 바로 클래스(객체x)에 속한 static 변수이다. 

그래서 자바스크립트는 "얘 누구야?" 하면서 ReferenceError를 던지는 것이다. 

 고로 [클래스명.static 변수 or this.static 변수] 중 한 가지 방법으로 static 함수 내에서 접근을 해야 한다. 

static returnGroupName() {
    return IdolModel.groupName; // ✅ OK
}
static returnGroupName() {
    return this.groupName; // ✅ OK: static 함수에서의 this는 클래스를 가리킴(JS 특징)
}

 

Static Factory Method - static 메서드를 [객체를 생성하는 '공장(factory)']처럼 사용

우선 아래 코드를 살펴 보자. 

class IdolModel{

    name;
    year;
    static groupName = "아이브";

    constructor(name,year){
        this.name=name;
        this.year=year;
    }

    // static factory method
    static fromObject(object){
       return new IdolModel(object.name,object.year);
    }
}

const yujin1 = IdolModel.fromObject(
    // Object 데이터 타입
    {
     name: "안유진",
     year: 2002  
    }
              )
console.log(yujin1); // IdolModel { name: '안유진', year: 2002 }

이전처럼 new 생성자 방식이 아니라 static 메서드를 통해서 객체를 생성하고 있다. 

fromObject 말고 fromJSON도 만들 수가 있다 

class IdolModel{

    name;
    year;
    static groupName = "아이브";

    constructor(name,year){
        this.name=name;
        this.year=year;
    }

    // static factory method
    static fromJson(json){

        const {name,year} = JSON.parse(json); 
        return new IdolModel(name,year);
    }
    static fromObject(object){
       return new IdolModel(object.name,object.year);
    }
}

const yujin1 = IdolModel.fromObject(
    // Object 데이터 타입입
    {
     name: "안유진",
     year: 2002  
    }
              )
console.log(yujin1); // IdolModel { name: '안유진', year: 2002 }



const yujin2 = IdolModel.fromJson( 
    
  JSON.stringify({ // JSON 문자열로 변환해줘야 한다. 
    name: "장원영",
    year: 1999  
  })
              );
console.log(yujin1); //  IdolModel { name: '안유진', year: 2002 }

나는 이런 의문이 들었다. IdolModel.fromJson(    

    {
     name: "안유진",
     year: 2002  
    }

)

위와 같은 key-value pair가 json 형태이니 매개변수를 위와 같이 넣어도 될 줄 알고 실행을 하였더니 에러가 났다.

그 에러는 아래와 같은 메시지 였다.  

SyntaxError: "[object Object]" is not valid JSON

즉, javascript의 key-value Object를 JSON 형식으로 변환해 주는 JSON.parse()의 매개변수에는 key-value 형태의 Object

가 아닌 JSON 문자열이 들어 가야 했던 것이다.

*JSON 타입은 그냥 하나의 문자열이다.

const json = '{"name": "안유진", "year": 2003}';
console.log(typeof json); // string ✔️

JSON 타입은 위와 같은 특정한 규칙(문법)을 따르는 문자열에 불과하지, key-value pair의 Object와는 전혀 다르다. 

헷갈리지 말자. 

Q. 굳이 static 메서드를 사용하여 객체를 생성해서 좋은 게 뭘까?

A. 결론부터 말하면 첫 번쨰로는 immuatble하게 객체 생성이 가능한 것이고,

두 번째로는 "객체 생성 로직을 생성자로부터 분리"가 가능하다는 점이다. 

-> 이제부터 설명을 할 것인데 이걸 이해하기 위해서는 OOP 지식이 필요하다. 

개발자가 생성자를 통해 직접 객체를 생성하는 경우 만약 생성자의 내부가 변경이 되면 거기에 맞춰서 생성자에 들어갈 매

개변수를 개발자가 변경해야 한다. 이는 OCP 원칙에 위반한다. 

고로, SRI 원칙에  개발자의 new에 의한 객체 생성 로직을 생성자로부터 분리한다 -> Static Factory Method를 통해!!

그렇게 함으로써 클라이언트 코드, 즉 객체를 생성하는 코드는 생성자의 변경(서버 코드의 변경)이 일어나도 그 변경을 알

필요가 없고, 코드 변경도 일어나지 않는다. 

(만약 생성자의 스펙이 변경되도 그건 서버 코드에서 fromObject, fromJson 코드를 변경하면 되는 문제이다)

재미로 fromList()도 만들어 보자. 

class IdolModel{

    name;
    year;
    static groupName = "아이브";

    constructor(name,year){
        this.name=name;
        this.year=year;
    }

    // static factory method
    static fromJson(json){

        const {name,year} = JSON.parse(json); 
        return new IdolModel(name,year);
    }
    static fromObject(object){
       return new IdolModel(object.name,object.year);
    }

    static fromList(list){
        return new IdolModel(
            list[0],
            list[1]
        )
    }

}

const yujin1 = IdolModel.fromObject(
    // Object 데이터 타입입
    {
     name: "안유진",
     year: 2002  
    }
              )
console.log(yujin1); // IdolModel { name: '안유진', year: 2002 }



const yujin2 = IdolModel.fromJson( 
    
  JSON.stringify({ // JSON 문자열로 변환해줘야 한다. 
    name: "장원영",
    year: 1999  
  })
              );
console.log(yujin1); //  IdolModel { name: '안유진', year: 2002 }

const yujin3 = IdolModel.fromList(
    [
       "가을",
        1998 
    ]
)
console.log(yujin3); // 출력 결과 : IdolModel { name: '가을', year: 1998 }

 

 

상속(inheritance) - 프로토 타입 체인 기반!!(프로토 타입은 나중에 자세히 나옴. 지금은 걍 무시)

class IdolModel{

    name;
    year;

    constructor(name,year){
        this.name=name;
        this.year=year;
    }
}

class FemaleIdolModel extends IdolModel{

    dance() {
        return "여자 아이돌이 춤을 춤니다";
    }
}

class MaleIdolModel extends IdolModel{

    sing(){
        return "남자 아이돌이 노래를 부릅니다";
    }
}


const yujin = new FemaleIdolModel("안유진",2003); // 생성자 조차 상속을 받는다 허허허허
console.log(yujin); // 출력 결과 : FemaleIdolModel { name: '안유진', year: 2003 }
console.log(yujin.dance()); // 출력 결과 : 여자 아이돌이 춤을 춤니다
console.log(yujin instanceof IdolModel); // true
console.log(yujin instanceof FemaleIdolModel); // true
console.log(yujin instanceof MaleIdolModel); // false


const jimin = new MaleIdolModel("뷔",1999);
console.log(jimin); // 출력 결과 : MaleIdolModel { name: '뷔', year: 1999 }
console.log(jimin.sing()); // 출력 결과 : 남자 아이돌이 노래를 부릅니다
console.log(jimin instanceof IdolModel); // true
console.log(jimin instanceof MaleIdolModel); // true
console.log(jimin instanceof FemaleIdolModel); // false

상속은 java,cpp에서도 많이 봐 왔기 때문에 자세한 설명은 안 하겠다. 

그러나 JS에만 존재하는 상속 특징이 있다. 

예를 들어 JAVA 같은 경우는 자식 클래스의 생성자의 매개변수를 통하여 부모 클래스의 속성(Property)를 초기화할 때

super.부모 속성 = 매개변수 와 같은 형식으로 super 키워드를 이용해야 했다. 

그러나 javascript의 경우 자식 클래스는 부모 클래스의 생성자도 그대로 상속을 받는다. 

그렇기 때문에 new Female("안유진",2003), new MaleIdolModel("뷔",1999)를 했을 때 IdolModel의 생성자가 호출이 되는

것이다.

 

 앗, 나의 실수... 

javascript의 경우에도 생성자에서 super()를 사용할 수가 있었다. 

class IdolModel{

    name;
    year;

    constructor(name,year){
        this.name=name;
        this.year=year;
    }
}

class FemaleIdolModel extends IdolModel{
    

    group;

    constructor(name,year,group){
        super(name,year);
        this.group = group;
    }

    dance() {
        return "여자 아이돌이 춤을 춤니다";
    }
}

class MaleIdolModel extends IdolModel{

    group;

    constructor(name,year,group){
        super(name,year);
        this.group = group;
    }


    sing(){
        return "남자 아이돌이 노래를 부릅니다";
    }
}


const yujin = new FemaleIdolModel("안유진",2003); 
console.log(yujin); // 출력 결과 : FemaleIdolModel { name: '안유진', year: 2003, group: undefined }



const jimin = new MaleIdolModel("뷔",1999);
console.log(jimin); // 출력 결과 : MaleIdolModel { name: '뷔', year: 1999, group: undefined }

클래스 FemalIdolModel, MaleIdolModel에서 생성자를 선언한 결과 위와 같은 출력값이 나온다. 

예를 들어 new FemaleIdolModel 생성자를 선언한 순간 이제는 FemalIdolModel의 생성자가 호출이 된다. 

그 결과 위 코드에서 group에 해당하는 argument를 넣어 주지 않았기 때문에 group 부분은 당연히 undefined가 출력이 된

다. 여기서 중요한 점은 자식 클래스에서 생성자를 정의한 순간 자식 생성자를 호출하면 새로 정의한 자식 생성자가 호출이

된다는 것이다. 

 

오버라이딩(Overriding)

자 이제 부모 클래스의 메서드를 자식 클래스에서 오버 라이딩을 해보자 

class IdolModel{

    name;
    year;

    constructor(name,year){
        this.name=name;
        this.year=year;
    }

    sayHello(){
        return `안녕하세요!! ${this.name}입니다. 저의 탄생연도는 ${this.year}입니다.`;
    }
}

class FemaleIdolModel extends IdolModel{
    

    group;

    constructor(name,year,group){
        super(name,year);
        this.group = group;
    }

    dance() {
        return "여자 아이돌이 춤을 춤니다";
    }
	
    // 오버 라이딩
    sayHello(){
        return `안녕하세요!! ${super.name}입니다. 저의 탄생연도는 ${super.year}입니다. 그리고 그룹명은 ${this.group}입니다.`
    }

}

class MaleIdolModel extends IdolModel{

    group;

    constructor(name,year,group){
        super(name,year);
        this.group = group;
    }


    sing(){
        return "남자 아이돌이 노래를 부릅니다";
    }
    
    // 오버 라이딩
    sayHello(){
        return `안녕하세요!! ${super.name}입니다. 저의 탄생연도는 ${super.year}입니다. 그리고 그룹명은 ${this.group}입니다.`
    }
}


const yujin = new FemaleIdolModel("안유진",2003,"아이브");
console.log(yujin); // 출력 결과 : FemaleIdolModel { name: '안유진', year: 2003, group: '아이브' }
console.log(yujin.sayHello()); // 출력 결과 : 안녕하세요!! undefined입니다. 저의 탄생연도는 undefined입니다. 그리고 그룹명은 아이브입니다.


const jimin = new MaleIdolModel("뷔",1999,"BTS");
console.log(jimin); // 출력 결과 : MaleIdolModel { name: '뷔', year: 1999, group: undefined }
console.log(jimin.sayHello()); // 출력 결과 : 안녕하세요!! undefined입니다. 저의 탄생연도는 undefined입니다. 그리고 그룹명은 BTS입니다.

당연히 부모 클래스의 속성(property)를 사용하기 위해서 나는 super 키워드를 사용하였다.

이건 java, cpp에서는 당연한 문법이다. 

그러나 아쉽게도 javascript에서는 생성자 이외의 메서드에서는 super 키워드를 사용하지 못하고 부모 클래스의 속성이라

도 this 키워드를 사용해야 한다.

위 코드에서 부모 클래스의 name, year 속성을 사용하기 위하여 super 키워드를 사용하였는데, JS 문법 규칙상 이건 비정

상적인 문법이기에 super.name, super.year 부분은 undefined로 출력이 되었다. 

위 코드를 javascript 문법에 맞게 수정하면 아래와 같다. 

class IdolModel{

    name;
    year;

    constructor(name,year){
        this.name=name;
        this.year=year;
    }

    sayHello(){
        return `안녕하세요!! ${this.name}입니다. 저의 탄생연도는 ${this.year}입니다.`;
    }
}

class FemaleIdolModel extends IdolModel{
    

    group;

    constructor(name,year,group){
        super(name,year);
        this.group = group;
    }

    dance() {
        return "여자 아이돌이 춤을 춤니다";
    }

    sayHello(){
        return `안녕하세요!! ${this.name}입니다. 저의 탄생연도는 ${this.year}입니다. 그리고 그룹명은 ${this.group}입니다.`
    }

}

class MaleIdolModel extends IdolModel{

    group;

    constructor(name,year,group){
        super(name,year);
        this.group = group;
    }


    sing(){
        return "남자 아이돌이 노래를 부릅니다";
    }

    sayHello(){
        return `안녕하세요!! ${this.name}입니다. 저의 탄생연도는 ${this.year}입니다. 그리고 그룹명은 ${this.group}입니다.`
    }
}


const yujin = new FemaleIdolModel("안유진",2003,"아이브");
console.log(yujin); // 출력 결과 : FemaleIdolModel { name: '안유진', year: 2003, group: '아이브' }
console.log(yujin.sayHello()); // 출력 결과 : 안녕하세요!! 안유진입니다. 저의 탄생연도는 2003입니다. 그리고 그룹명은 아이브입니다.


const jimin = new MaleIdolModel("뷔",1999,"BTS");
console.log(jimin); // 출력 결과 : MaleIdolModel { name: '뷔', year: 1999, group: undefined }
console.log(jimin.sayHello()); // 출력 결과 : 안녕하세요!! 뷔입니다. 저의 탄생연도는 1999입니다. 그리고 그룹명은 BTS입니다.

 

위에서 super 키워드에 대해 여러 이야기를 하였는데 여기서 딱 아래 2가지에 대해 정리를 하고 가자. 

1] javascript에서  super 키워드느 부모 클래스의 메서드에만 접근하기 위한 키워드이지, 속성(property) 접근은 x

class IdolModel {
    constructor(name, year) {
        this.name = name;
        this.year = year;
    }

    sayHello() {
        return `안녕하세요. ${this.name}입니다.`;
    }
}

class FemaleIdolModel extends IdolModel {
    constructor(name, year, part) {
        super(name, year); // ✅ OK
        // super.name = name; // ❌ 이렇게 해도 아무 효과 없음
        this.part = part;
    }

    sayHello() {
        return `${super.sayHello()} 저는 ${this.part} 파트입니다.`;
    }
}

2] JavaScript에서도 class 문법을 사용할 때 super()는 생성자(constructor)의 첫 줄에 반드시 위치

    자식 클래스에서 this를 사용하기 전에 super()를 먼저 호출해야 한다 

 

예시로 오류 확인

class IdolModel {
    constructor(name, year) {
        this.name = name;
        this.year = year;
    }
}

class FemaleIdolModel extends IdolModel {
    constructor(name, year, part) {
        // ❌ Error: Must call super constructor before accessing 'this'
        this.part = part; 
        super(name, year);
    }
}

그 이유는 this는 이 상황에서는 자식 클래스를 가리키는데 자식이 생성되기 이전에 반드시 부모 클래스가 먼저 생성이

되어야 하기 때문이다. 

 

올바른 버전

class FemaleIdolModel extends IdolModel {
    constructor(name, year, part) {
        super(name, year);  // ✅ 반드시 첫 줄!
        this.part = part;
    }
}

 

 

객체에 대한 모든 것 - All about Object

객체를 선언할 때 사용할 수 있는 방법 3가지

1] object를 생성해서 객체 생성 - 기본기 {}  # 위에서 배움

2] class를 정의 후 new 키워드를 통해 객체 생성 # 위에서 배움

3]  Function을 사용해서 객체 생성 # 아직 안 배움

여기서부터는 Function을 사용해서 객체를 생성하는 법을 배우고 1]~3] 부분을 더 깊게 알아 볼 것이다. 

 

1],2]의 복습 코드를 보자. 

const yujin = {
    name: "안유진",
    year: 1994
}
console.log(yujin); // 출력 결과 : { name: '안유진', year: 1994 }

class IdolModel{

    name;
    year;

    constructor(name,year){

        this.name=name;
        this.year=year;
    }
}

const yujin = new IdolModel("안유진",1994);
console.log(yujin); // 출력 결과 : IdolModel { name: '안유진', year: 1994 }

뭐 별 거 없을테니 설명을 제끼겠다. 

 

여기서부터가 중요하다. 

생성자 함수 - constructor function

// 생성자 함수 - constructor function
function IdolModel(name,year){
    this.name = name;
    this.year = year;
}

const yujin = new IdolModel("안유진",1994); // function으로 무려 객체를 생성해버렸잖아!!!
console.log(yujin); // 출력 결과 : IdolModel { name: '안유진', year: 1994 }

javascript는 함수를 통해 위 코드와 같이 객체를 생성하는 것이 가능하다. 

단, 아래 2가지 조건을 충족시켜야 한다. 

1] 매개 변수가 0개 이상 존재

2] this 키워드를 사용하여 모든 매개 변수를 초기화 - 매개 변수가 0개 이면 필요 x

-> 위 조건을 충족 시 class의 인스턴스와 같은 객체를 생성 가능하다. 

* 생성자 함수는 일반 함수와 달리 camel 기법이 아니기 때문에 대문자로 선언을 시작하는 게 관례이다

 

함수 선언문( function declaration ) vs 함수 표현식( function expression )

함수 선언문(function declaration) 

// 이건 함수를 【선언】한 것이다
function IdolModel() {
  this.name = '안유진';
  this.year = 1994;
}

const yujin2 = new IdolModel();
console.log(yujin2); // ✅ IdolModel { name: '안유진', year: 1994 }

 

  • 여기서 IdolModel은 함수 선언문 (function declaration)입니다.
  • 함수 선언문은 호이스팅(hoisting) 되어 코드의 어디서든 사용할 수 있어요.
  • new IdolModel()로 인스턴스를 잘 생성할 수 있습니다.

-> 즉 class를 정의한 것과 비슷한 개념이다. 

 

에러 발생 코드

const yujin1 = function IdolModel() {
  this.name = '안유진';
  this.year = 1994;
};

const yujin2 = new IdolModel();  // ❌ ReferenceError: IdolModel is not defined
console.log(yujin2);

 

 

  • 여기서 function IdolModel()은 함수 표현식(function expression)입니다.
  • 그리고 이 표현식은 변수 yujin1에 할당되어 있어요.
  • yujin1이 전역적으로 선언된 것이지 함수가 전역적으로 선언된 것이 아니다 
  • 중요한 차이점:
    function IdolModel()에서 IdolModel은 함수 내부에서만 사용 가능한 이름이에요.
    즉, 바깥에서는 IdolModel이라는 이름이 정의되어 있지 않음

🔍 그래서 어떻게 해야 작동할까?

 

방법 1: 그냥 함수 선언문을 쓰기

function IdolModel() {
  this.name = '안유진';
  this.year = 1994;
}

const yujin2 = new IdolModel(); // 👍 OK

 

 

방법 2: 변수로 만든 경우, 변수 이름으로 호출하기

const yujin1 = function IdolModel() {
  this.name = '안유진';
  this.year = 1994;
};

const yujin2 = new yujin1();  // 👍 OK
console.log(yujin2);

📌 IdolModel은 함수 내부에서만 사용 가능하다는 게 무슨 뜻??

const fn = function Hello() {
    console.log("Inside function name:", Hello.name); // 함수 내부에서는 Hello 사용 가능
  };
  
  fn();             // 출력 결과 : Inside function name: Hello
  console.log(Hello); // ❌ ReferenceError: Hello is not defined

 

그럼 왜 IdolModel은 마치 지역 변수처럼 함수 내부에서만 사용이 가능할까??

그 원인은 자바스크립트 엔진이 이름 있는 함수 표현식을 처리하는 방식에 있다. 

const yujin = function IdolModel() {
  console.log("이 안에서는 IdolModel 이름 쓸 수 있어");
};

IdolModel(); // ❌ ReferenceError

위 코드를 javascript 엔진이 만나면 아래와 같이 코드를 번역한다. 

const yujin = (function() { // 여기를 함수1 이라고 부르자.
  
  
  const IdolModel = function() { // 여기를 함수2 이라고 부르자
  
    console.log("IdolModel 내부");
    
  };
  
  return IdolModel; 
  
})();

IdolModel은 함수1에서 선언된 지역 변수이다. 

스코프 상 IdolModel은 함수 2에서도 사용이 당연히 가능하다. 

이와 같은 자바스크립트 엔진 동작에 의해서 IdolModel은 함수 내부에서는 사용이 가능하지만

외부에서는 사용이 불가능하다.  

 

 

프로퍼티 에트리뷰트(Property Attribute)

우선 Property Attribute가 뭔지를 설명을 하겠다(한글로 된 설명 중에 제대로 된 게 없어서 영어 자료 긁어옴)

In JavaScript, when you create a property inside an object (like obj.name = "value"),

that property is not just a key and value. It also has hidden settings called "property attributes."

These hidden attributes control how the property behaves.

There are three main attributes for normal properties:

Attribute                       Meaning
writable Can the value of the property be changed? (true/false)
enumerable Will the property show up in a for...in loop or Object.keys? (true/false)
configurable Can the property be deleted or its attributes changed? (true/false)

 

🛠 Example

const yujin = {
    name: '안유진'
};

// Let's see the property descriptor
console.log(Object.getOwnPropertyDescriptor(yujin, 'name'));

The output will be

{
    value: '안유진',
    writable: true,
    enumerable: true,
    configurable: true
}

These are called Property Attributye.

  • value: The actual value of the property ('안유진').
  • writable: true: You can change yujin.name.
  • enumerable: true: It will appear in loops like for...in.
  • configurable: true: You can delete name or change its attributes.

 

✨ Special Attributes for Accessor Properties

If the property uses a getter or setter instead of a normal value,
then it has different attributes:

Attribute                                 Meaning
get The function that runs when the property is read
set The function that runs when the property is written
enumerable Same as before
configurable Same as before

Example:

const idol = {

    _name: '안유진', // Data Property
    
    get name() { // Accessor property
        return this._name;
    },
    set name(value) { // Accessor property
        this._name = value;
    }
};

console.log(Object.getOwnPropertyDescriptor(idol, 'name'));

output

{
    get: ƒ name(),
    set: ƒ name(value),
    enumerable: true,
    configurable: true
}

 

Data Property vs Accesstor Property

1. Data Property

  • Stores a value directly.
  • Has the following attributes(Property Attributes of Data Property)
    • value: the actual data
    • writable: can you change the value?
    • enumerable: does it show up in loops?
    • configurable: can you delete or change its attributes?
const idol = {
    name: '안유진' // ← data property
};

Here, name is a data property.

It holds a value directly ('안유진'), and you can check or modify it.

2. Accessor Property

  • Does NOT store a value itself.
  • Instead, it has Property Attribute of Accessor Property
    • get: a function that runs when you read the property
    • set: a function that runs when you write to the property
  • Also has Property Attribute of Accessor Property
    • enumerable
    • configurable

Example

const idol = {
    _name: '안유진',
    get name() {
        return this._name; // runs when you read idol.name
    },
    set name(value) {
        this._name = value; // runs when you assign idol.name = 'something'
    }
};

Here, name is an accessor property.

You don't directly access _name. Instead, the get function returns it, and the set function changes it.

 

Property Attribute 사용법

yujin 객체에 height 속성을 추가해보자

const yujin = {
    name: '안유진',
    year: 1994
}


yujin.height=172; // or yujin["height"] = 172;
console.log(yujin); // { name: '안유진', year: 1994, height: 172 }

그럼 height 속성에 대한 property attribute를 출력해보자

console.log(Object.getOwnPropertyDescriptor(yujin,"height")); 
// 출력 결과 : { value: 172, writable: true, enumerable: true, configurable: true }

writeable,enumrable,configurable 모두 기본값이 true로 설정돼 있는 것을 볼 수가 있다. 

만약 property attribute까지 원하는 설정으로 해서 속성을 추가하고 싶으면 어떻게 해야 할까?

Object.defineProperty()를 이용하면 된다. 

Object.defineProperty(yujin,"height",{

    value: 172,
    writable: true,
    enumerable:true,
    configurable: false

})
console.log(yujin); // { name: '안유진', year: 1994, height: 172 }
console.log(Object.getOwnPropertyDescriptor(yujin,"height")); // { value: 172, writable: true, enumerable: true, configurable: false }

만약 Object.defineProprty()를 사용하고 아래와 같이 property attibute를 설정하지 않으면 모두 기본값이 false가 된다. 

Object.defineProperty(yujin,"height",{

    value: 172,
    //writable: true,
    //enumerable:true,
    //configurable: false

})
console.log(Object.getOwnPropertyDescriptor(yujin,"height")); 
// { value: 172, writable: false, enumerable: false, configurable: false }

 

writable Attribute

// writable
yujin.height=172; // yujin["height"]=172;

Object.defineProperty(yujin,"height",{

        value: 172,
        writable: true,
        configurable: true
})

console.log(yujin); // 출력 결과: { name: '안유진', year: 1994, height: 172 }
console.log(Object.getOwnPropertyDescriptor(yujin,"height")); // { value: 172, writable: true, enumerable: true, configurable: true }


yujin.height=180; // 172에서 180으로 변경
console.log(yujin); // 출력 결과 : { name: '안유진', year: 1994, 【height: 180】}

위 코드는 이해에 문제가 없으니 그냥 넘어 가겠다. 

Object.defineProperty(yujin,"height",{

        writable: false
})
yujin.height=172; // 변경 안됨 -> 에러는 발생 하지 X
console.log(yujin); // 출력 결과 : { name: '안유진', year: 1994, height: 180 }
console.log(Object.getOwnPropertyDescriptor(yujin,"height")); // { value: 180, writable: false, enumerable: true, configurable: true }

writeable: false일 경우 주의해야 할 것이 값 변경은 불가능하지만 코드 에러는 나지 않는다. 

그래서 yujin.height=172에서 런타임 에러는 나지 않는다. 그러나 출력 결과를 보면 알겠지만 변경이 되지 않음을 알 수 있

다. 

 

enumable Attribute

// enumerable
console.log(Object.keys(yujin)); // 출력 결과 :  [ 'name', 'year', 'height' ]
Object.defineProperty(yujin,"name",{
        enumerable:false
})
console.log(Object.keys(yujin)); // 출력 결과 : [ 'year', 'height' ]
for(let key in yujin)
{
        console.log(key);  // 출력 결과 : [ 'year', 'height' ]
}

name 속성에 대해 enumable: false인 경우에는 for..in.. 혹은 Object.keys()에 의해 열거(enum)되지 않는다. 

그러나 주의해야 할 점이 있다. 아래의 코드를 보자. 

console.log(yujin.name);  // 출력 결과 : 안유진 -> 정상적으로 출력!

for..in, Object.keys()에 의해 【열거】가 안 될뿐 name이 사라지거나 하지는 않는다. 

 

 

configurable

//configuarable 
Object.defineProperty(yujin,"height",{
        configurable: false
});

Object.defineProperty(yujin,"height",{ // Error : Can not redefine the property: height
        enumerable: false
})

configurable의 정의 : property attribute의 재정의 가능 여부를 결정.

configuarble이 false인 경우 property의 삭제나 property attribute의 변경이 금지

단, configurable에는 매우 중요한 예외가 있다. 

Object.defineProperty(yujin,"height",{
        writable: true,
        configurable:false
});

Object.defineProperty(yujin,"height",{
        value:170
})
console.log(yujin.height);

예외 1] 기존의 writeable이 true인 경우 value와 writeable의 변경(true->false) 가능

 

Object.defineProperty(yujin,"height",{

		wriable: true // false -> true로는 변경 불가능

})

예외 2] 기존의 writeable이 false인 경우 true로는 변경 불가능

 

 

 

불변 객체(immutable programming)  - 3가지 키워드가 포인트!

1] Extensible - 확장 가능성(그렇게 많이 사용하지는 않는다고 한다)

코드를 먼저 보자

const yujin = {
        name: "안유진",
        year: 2003,


        get age(){
            return new Date().getFullYear - this.year;
        },

        set age(age){
                this.year = new Date().getFullYear - age;
        }
}

yujin["position"]="보컬"; // 속성 추가(확장)
console.log(yujin); // { name: '안유진', year: 2003, age: [Getter/Setter], position: '보컬' }

우리는 여태껏 yujin[key]=value 혹은 yujin.key=value와 같은 형식으로 기존에 정의하지 않은 속성들을 추가(확장)할 수가 

있었다. 이러한 성질을 Extensible하다 라고 말한다. 

객체가 확장가능한 객체인지 아닌지 확인이 가능하다. 

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

그러나 개발자가 객체에 더 이상 어떤 속성 추가(확장)하는 것을 막는 방법도 있다. 

const yujin = {
        name: "안유진",
        year: 2003,


        get age(){
            return new Date().getFullYear - this.year;
        },

        set age(age){
                this.year = new Date().getFullYear - age;
        }
}

Object.preventExtensions(yujin); // 개발자가 확장 불가능하게 설정

yujin["position"]="보컬"; // 확장 불가능. 에러는 나지 않음
console.log(yujin);

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

 

단 속성의 삭제는 가능하다. 

delete yujin["position"];
console.log(yujin); // { name: '안유진', year: 2003, age: [Getter/Setter] }

 

2] Seal - 매우 많이 사용한다고 한다

Seal = (configurable==false) && (속성 추가/삭제 불가능)

const yujin = {
        name: "안유진",
        year: 2003,


        get age(){
            return new Date().getFullYear - this.year;
        },

        set age(age){
                this.year = new Date().getFullYear - age;
        }
}


Object.seal(yujin); 
console.log(Object.isSealed(yujin)); // true



console.log(yujin);  // 출력 결과 : { name: '안유진', year: 2003, age: [Getter/Setter] }

yujin["position"] = "보컬"; // 속성 추가
console.log(yujin);  // 출력 결과 : { name: '안유진', year: 2003, age: [Getter/Setter] }

delete yujin["name"]; // 속성 삭제
console.log(yujin); // 출력 결과 : { name: '안유진', year: 2003, age: [Getter/Setter] }

객체에 sealing을 한 결과 객체에 더 이상 속성의 추가/삭제가 불가능하게 되었다. 

(추가/삭제를 하려고 해도 에러가 나지 않는다는 점에 주의를 하자!)

또한 아래의 property attribute를 살펴보자

const yujin = {
        name: "안유진",
        year: 2003,


        get age(){
            return new Date().getFullYear - this.year;
        },

        set age(age){
                this.year = new Date().getFullYear - age;
        }
}


Object.seal(yujin); 
console.log(Object.isSealed(yujin)); // true



Object.defineProperty(yujin,"name",{
        value: "김진영"
})
console.log(yujin["name"]); // 출력결과 : 김진영

위 코드는 언뜻 보기에 납득이 되지 않는다. 

분명 sealing하였기에 configurable이 false가 되었으므로 value의 변경도 되지 말아야 한다. 

그러나 변경이 잘 되었다. 

여기서 혼동하지 말아야 하는 것이 Obejct.defineProperty()를 통해 최초로 추가된 속성에 대해서는 property attribute의 

디폴트 값은 false이다. 그러나 Obejct.defineProperty() 이전에 먼저 선언된 속성의 property attribute의 디폴트 값은 true이

다. 위 코드에서 name이란 속성은 객체 선언 시에 이미 정의된 변수이기에 configurable은 Object.isSealed()에 의해 false

가 되었어도 여전히 writable은 true이다. 

고로 value의 변경이 가능한 것이다. 

 

3] Freezed - read 외의 모든 기능을 불가능하게 만듬

* 가장 엄격한 immutable 기능이다.

const yujin = {

    name: '안유진',
    
    get getName(){
        return this.name;
    },

    set setName(name){
        return this.name=name;
    }
}

const isFreezed = Object.isFrozen(yujin); // 해당 객체가 freezed돼 있는지 확인
console.log(isFreezed); // false

Object.freeze(yujin);  // 해당 객체를 freezed 상태로 변경!!

//속성 추가
yujin.groupName="ive"; // 에러는 안 남
// Object.defineProperty(yujin,"groupName",{ // 에러 발생

//     value: "ive"

// })
console.log(yujin);

// 속성 변경
yujin.name="jinyoung";
// Object.defineProperty(yujin,"name",{ // 에러 발생
//     value: "jinyoung"
// })
console.log(yujin);

console.log(Object.getOwnPropertyDescriptor(yujin,"name"));

//속성 삭제
delete yujin["name"];
console.log(yujin);

위 코드에서 2가지 주의 깊게 봐야 할 점이 있다. 

1] freezed된 객체에 대해서는 어떠한 방식으로도 속성의 변경/추가/삭제가 불가능

2] Object.define()으로 속성을 변경/추가/삭제를 해도 마찬가지로 불가능하다. 

-> 고맙게도 Object.define()는 에러를 발생시켜 준다. 

3] freezed된 객체의 Property Attribute는 enumarble(read 작업)외에는 false이다.

 

고로 console.log(yujin)의 출력 결과는 항상 아래와 같다. 

{ name: '안유진', getName: [Getter], setName: [Setter] }

 

주의 사항

extensible, seal, freezed는 상위 객체에만 적용이 된다

const yujin = { // 상위 객체

    name: '안유진',
    
    // 하위 객체
    wonYoung: {
        name: "장원영"
    }
}

Object.freeze(yujin);

console.log(Object.isFrozen(yujin)); // true
console.log(Object.isFrozen(yujin.wonYoung)); // false

 

yujin 객체(상위 객체)에 wonYoung(하위 객체)를 선언했다고 하자. 

이떄 yujin 객체를 freezed를 하여 isFrozen함수로 출력을 하면 당연히 true가 나온다. 

그러나 하위 객체에 대해서는 true가 나온다. 

이건 javascript의 문법 규칙이다. 

만약 하위 객체까지도 freezed하고 싶다면 재귀함수 등을 이용하여 적용시키면 된다. 

 

 

생성자 함수 - 심화편

function IdolModel(name,year){
    this.name=name;
    this.year=year;
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin); // 출력 결과

위 코드는 설명을 생략하겠다. 

아래 2개의 코드는 절대 사용해서는 안 되는 패턴이라는 것을 알려 주기 위한 코드이다. 

그리고 쓸 이유도 전혀 없는 코드이다. 

function IdolModel(name,year){
    
    this.name=name;
    this.year=year;
    
    return {};
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin); // 출력 결과 : {}

만약 생성자 함수에서 return 객체를 하게 되면 어떻게 될까?

new 생성자 함수로 만들어진 객체가 반환되는 것이 아니라 return으로 반환한 객체가 출력된다.

그러나 primitive type을 return하게 되면 어떻게 될까? 아래 코드에서 확인해 보자. 

function IdolModel(name,year){
    
    this.name=name;
    this.year=year;
    
    return "안녕";
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin); // 출력 결과 : IdolModel { name: '안유진', year: 2003 }

primitive를 return을 해도 객체를 return한 것과는 달리 new 생성자 함수로 만들어진 객체에 어떠한 영향도 안 미친다.

 

생성자 함수 안에 함수 선언하기

function IdolModel(name,year){
    
    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin.dance()); // 출력 결과 : 안유진이 춤을 춥니다

클래스에서 함수를 선언하는 것과 다름이 없다 

사실 javascript에서 클래스는 나중에 나왔고 원래 function으로 객체를 만들고 OOP를 만들었었다.(ㅡㅡ)

그래서 많은 레거시 프로그램에서 function 기반으로 객체가 만들어 져 있다.

근데 이런 호기심이 생길 수가 있다. new IdolModel()에서 new를 붙이지 않고 그냥 IdolModel()을 하면 어떻게 될까?

function IdolModel(name,year){
    
    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin = IdolModel("안유진",2003);
console.log(yujin); // 출력 결과 : undefined

결론부터 말하지면 IdolMode("안유진",2003)은 아무것도 반환하지 않기에 undefined가 출력이 된다. 

위 코드를 통해 하고 싶은 말은 생성자 함수에는 반드시 new 키워드가 필요하다는 것이다. 

그럼 또 이런 호기심이 생긴다. 생성자 함수에서 선언한 this.name, this.year, this.dance는 메모리에서 사라진 걸까??

결론부터 말하면 javascript가 기본적으로 내장하고 있는 내장 객체 Global에 들어 가 있다. 

우선 코드를 보자

function IdolModel(name,year){
    
    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin = IdolModel("안유진",2003);

console.log(global); // global 객체 출력

출력 결과는 아래와 같다. 

 

global 객체는 추후에 this 키워드를 자세히 다루면서 다시 다룰 것이지만 간단하게 설명을 하자면

node.js를 실행시키기 위해 필요한 값들을 저장하는 곳이며, javascript가 실행되면 자동적으로 

생성이 된다. 

근데 만약 생성자 함수에 new를 붙여 주지 않게 되면 생성자 함수의 this가 붙은 속성/함수들이 

자동적으로 global에 저장되게 된다. 

그럼 new 키워드가 있을 떄와 없을 때 this는 무엇을 가리키는 지 확인해보자

function IdolModel(name,year){
    
    console.log(this); // 추가

    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin1 = new IdolModel("안유진",2003);
const yujin2 = IdolModel("안유진",2003);

생성자 함수 안에 console.log(this)를 추가해 보았다. 

 

new IdolMode()의 출력 결과

IdolModel {}

이때 this는 우리가 원하는대로 new로 생성된 객체를 가리키고 있다. 

* 출력 결과 속성값이 아직 아무것도 안 들어 있는 이유는 javascript가 인터프리터 방식이기 때문에 아직 

this.name,this.year,this.dance를 실행하지 않았기 떄문이다. 

코드 라인이 밑으로 가서 객체가 완성이 되면 {}안에 key-value 속성이 들어 가 있을 것이다. 

 

IdolMode()의 출력 결과

<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  structuredClone: [Getter/Setter],
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [Function: fetch],
  navigator: [Getter],
  crypto: [Getter]
}

new 없이 IdolModel()을 실행하게 되면 이떄의 this는 글로벌 객체(global)를 가리키고 있다.

 

new.target

function IdolModel(name,year){
    
    console.log(new.target); // 추가

    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin1 = new IdolModel("안유진",2003); // 출력 결과 : [Function: IdolModel]
const yujin2 = IdolModel("안유진",2003); // 출력 결과 : undefined

우선 new.target 키워드의 의미를 먼저 설명하겠다. 

new로 인해 생성된 객체가 무엇인지 알고 싶을 때 new.target 키워드를 이용하면 된다. 

그 결과 new IdolMode()의 경우 그 출력 결과가 [Function: IdolModel]이므로 new를 통해 IdolModel의 객체를 생성하고 

있음을 안다. 

그러나 new가 없는 IdolModel()의 경우 객체를 생성하는 것이 아니기 떄문에 당연히 new의 target이 없으므로 undefined

가 출력이 된다. 

여기서 new의 target이 global 객체가 아닌가 라는 생각을 할 수가 있는데 global 객체는 IdolModel()과는 상관없이 

javascript가 실행되자 마자 자동으로 생성되는 객체이다. 개발자가 직접 new를 통해 생성하는 것이 아니므로

this에서 global 객체를 가리켰다고 해서 new의 target이 global 객체를 가리키지는 않는다. 

this 키워드는 단지 현재의 객체가 누구인지를 가리키는 것이고 new.target은 new로 인해 새롭게 생성된 개체를 가리키

는 키워드이다. 

그럼 new.target은 언제 사용하냐?

개발자가 new 키워드를 써주지 않아서 객체가 생성되지 않는 문제를 방지해 준다. 

코드를 보자

function IdolModel(name,year){
    
    // new 누락을 방지
    if(new.target === undefined){
        return new IdolModel(name,year);
    }

    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin = IdolModel("안유진",2003); 
console.log(yujin); // 출력 결과 : IdolModel { name: '안유진', year: 2003, dance: [Function (anonymous)] }

console.log(yujin)의 출력 결과가 new IdolModel()과 똑같음을 알 수가 있다. 

즉 new 키워드를 누락시켰으나 제대로 객체가 생성됨을 확인할 수가 있다. 

 

화살표 함수(arrow function)으로도 생성자 함수를 만들 수가 있을까?

const arrowConstructor = (name,year) =>{
    this.name=name;
    this.year=year;
}


const yujin = new arrowConstructor("안유진",2003);

위 실행 결과는 아래와 같다. 

const yujin = new arrowConstructor("안유진",2003); 
              ^

TypeError: arrowConstructor is not a constructor

즉, 화살표 함수(arrow function)으로는 생성자 함수를 만들지 못하고 오로지 일반 함수로만 생성자 함수를 만들 수가 있

.

 

프로토타입 체인(Prototype Chain)

아래 그림들에서  "???"이라고 표시한 부분에 대해서는 추후에 설명을 하겠다.

그림1

 

아래 그림은 new IdolModel을 통해 객체 생성한 후의 관계를 도식화한 것이다.

그리고 yujin 객체의 __proto__는 IdolModel.prototype 객체의 전체를 참조하고 있는 그림이다.

그림2

지금 위 그림들을 이해하려고 하지 말고 아래 설명을 읽으면서 이해가 안 되면 이 그림을 봐라

-> 이 내용을 이해하면 상속에 대한 이해가 완전히 자기 것이 될 것이다. 다만 많이 헷갈린다. ㅠㅠ

또한 javascript에서는 

1] 클래스 == 함수

2] 함수는 객체(instance)

3] prototype은 메모리 낭비를 줄이기 위하여 탄생! 

이라는 위 3가지 사실을 먼저 짚고 넘어 가자. 그래야 프로토 타입이 더 이해가 잘 될 것이다. 

 

1] 클래스 == 함수

javascript에서는 클래스는 결국 함수에 불과하다. 

코드를 보자. 

클래스 선언 (우리가 평소에 쓰는 방식)

class Idol {

  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`Hi, I'm ${this.name}`);
  }
}

const jennie = new Idol('Jennie');
jennie.sayHello(); // Hi, I'm Jennie

위 코드는 class 키워드를 사용하여 인스턴스를 만들었다. 

그러나 javascript는 역사적으로 함수를 이용하여 객체를 만들었고 class 개념은 추후에 도입이 되었다. 

그러한 이유에서인지는 모르겠으나 javascript는 class를 아래와 같이 내부적으로 처리를 한다. 

// 생성자 [함수]
function Idol(name) {
  this.name = name;
}

// 메서드는 prototype에 정의
Idol.prototype.sayHello = function () {
  console.log(`Hi, I'm ${this.name}`);
};

const jennie = new Idol('Jennie');
jennie.sayHello(); // Hi, I'm Jennie

class에서 선언된 constructor() 함수는 생성자 함수로 변환되고

메서드는 prototype에 따로 정의가 된다. 

결국 new 클래스()는 new 생성자 함수()로 실행이 된다. 

 

2] 함수는 객체(instance)

나중 아래 그림 3 부분을 보면 IdolModel 함수의 _proto_는 Function.prototype 객체의 메모리 주소값을 가지고 있다. 

즉, IdolModel 함수는 결국에는 Function.prototype이라는 객체를 상속 받음으로써 탄생한 것이기에

javascript에서는 함수는 객체라는 것이다.

 

3] prototype은 메모리 낭비를 줄이기 위하여 탄생! 

"왜 자바스크립트는 생성자 함수나 클래스를 만들면 자동으로 프로토타입을 만들어놓을까?"
그리고 "대체 프로토타입은 왜 존재할까? 뭘 하려고 만든 걸까?"

이건 자바스크립트의 상속과 메모리 효율성이라는 핵심 개념 때문이다. 

한 문장 요약 : "여러 객체가 같은 기능(메서드)을 공유하도록 하기 위해서"
= 메모리 절약 + 상속 구조를 만들기 위함!

예를 들어, 우리가 메서드 생성자 함수 안에서 만들었다고 해보자

function Idol(name) {
  this.name = name;

  // 메서드를 인스턴스에 직접 붙임 (비효율적)
  this.sayHello = function () {
    console.log(`Hi, I'm ${this.name}`);
  };
}

const jennie = new Idol('Jennie');
const lisa = new Idol('Lisa');

이 경우, sayHello 함수가 각각의 인스턴스마다 메모리에 저장되기에 메모리 낭비가 심하다

javascript의 해결법: "공통 기능은 공유하자!" → 프로토타입 등장

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

Idol.prototype.sayHello = function () {
  console.log(`Hi, I'm ${this.name}`);
};

sayHello()와 같은 공통 기능은 하나의 prototype 객체에 저장하여 필요할 때 프로토타입 체인 탐색을 통해 필요할 때마다

prototype 객체를 탐색하여서 꺼내 쓰는 방식을 채용하였다.

이렇게 하면 상속 시에도 자식 인스턴스가 생성될 때마다 부모 클래스도 같이 메모리를 차지하여 메모리 낭비가 일어나지

않게 된다.

 

이제 prototype에 대한 본격적인 설명을 시작하겠다.

const testObj = {};
console.log(testObj.__proto__); // 언더바가 2개임에 주의!!

우선 출력 결과를 먼저 살펴보자. 출력 결과는 아래와 같다.

[Object: null prototype] {}

우선 proto의 개념에 대해서 먼저 설명을 하겠다(proto에 대해서는 prototype 부분에서 자세히 다룸)

1] proto 객체(속성 x)은 모든 객체(class x)생성되면 자동으로 만들어 지는 객체이다.

2] proto는 상속 관계에서 상위 프로토타입 객체의 메모리 값(참조값) 이다.

*아직 구체적인 것은 몰라도 된다. 추후에 prototype 설명이 계속 될거니 점점 이해하게 될 것이다. 

그럼 이번에는 prototype(!=proto)를 코드에서 출력해보자

function IdolModel(name,year){
    
    if(new.target === undefined){
        return new IdolModel(name,year);
    }

    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

console.log(IdolModel.prototype); // 출력 결과 : {}

우선 prototype은 static 속성이기에 객체를 만들지 않아도 출력이 가능하다.

console.log(IdolModel.prototype)의 출력 결과는 "{}"이다. 즉 key-value 형태의 그 어떠한 속성도 없다. 

그러나 사실 속성이 숨겨져 있을 뿐 속성이 없는 것이 전혀 아니다. 

아래 코드는 IdolModel의 prototype에 숨어 있는 속성들을 출력하는 코드이다. 

console.dir(IdolModel.prototype, {
    showHidden: true
})

출력 결과는 아래와 같다. 

<ref *1> {
  [constructor]: [Function: IdolModel] {
    [length]: 2,
    [name]: 'IdolModel',
    [arguments]: null,
    [caller]: null,
    [prototype]: [Circular *1]
  }
}

 

key(constructor): value(IdolModel 클래스 내에 숨겨진 key-value 형태의 속성)가 {}로 감싸져 있어서 즉 key-value 형태의

객체가 출력이 되는 것을 볼 수가 있다. 위 출력 내용을 자세히 보면 key값이 constructor로 value는 IdolModel의 생성자 

함수의 구성 정보를 담고 있는 것을 알 수가 있다. 즉 위 prototype은 생성자의 구성 정보를 담고 있다.

prototype의 구성은 생성자 이외에도 더 있다. 이거는 아래에서 추후에 설명이 나온다. 

그래서 아래와 같은 코드를 테스트 해볼 수가 있다. 

console.log(IdolModel.prototype.constructor === IdolModel); // 출력 결과: true

결과는 true이다. 

prototype에 대해서 간략하게 설명을 하겠다. 

javascript에서

1] 클래스를 선언

2] 생성자 함수 선언을 하면, 


그 프로토타입 객체(IdolModel.prototype) 안에는 자동으로 constructor라는 속성이 생깁니다.

이 constructor는  그 프로토타입 객체의 생성자 함수(또는 클래스)가 저장된 메모리 주소값(참조값)을 가진다.

말로는 이해가 완전히 안 될테니 javascript 내부에서 어떻게 동작하는지 코드로 이해하자

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

위와 같은 생성자 함수(클래스도 마찬가지)를 만들게 되면 javscript는 자동적으로 아래와 같이 위 코드를 처리한다.

// 내부적으로 자바스크립트가 이렇게 처리함:
IdolModel.prototype = {
  constructor: IdolModel
};

step 1: 먼저 프로토타입 객체 (클래스 x)를 만든다. 

step 2 : constructor라는 key를 만들고 그 value에 생성자 함수(클래스)의 메모리 주소값을 대응시킨다. 

그러니 당연히 console.log(IdolModel.prototype.constructor === IdolModel)의 출력 결과는 true인 것이다. 

Q. 그럼 왜 이런 prototype 객체를 자동으로 만들어 주는 걸까?

A. 결론부터 말하면 3가지 이유가 있다.

1] 객체를 만들 때 나중에 "이 객체는 어떤 생성자에서 왔는가?"를 알 수 있게 하게 위함이다. 

const jennie = new IdolModel("Jennie");

console.log(jennie.constructor === IdolModel); // true
// true라는 결과는 jennie.constructor는 IdolModel 생성자 함수가 저장돼 있는 메모리 주소를
// 공유하고 있다를 의미한다.

마치 java의 instanceof가 어떤 객체/인터페이스를 상속 받았는지 알 수 있게 해 주는 것과 같이 

constructor 속성을 이용하여 해당 객체(jennie)가  어떤 생성함수/클래스로 만들어 졌는지 알 수가 있다. 

2] 인스턴스를 가지고 동일한 클래스의 새로운 인스턴스를 만들 수가 있다.

const wonYoung = new yujin.constructor('장원영');
console.log(another); // 출력 결과 : Idol { name: 'Lisa' }

만약 yujin 인스턴스와 같은 타입의 장원영 인스턴스를 만들고 싶다고 하면 어떻게 해야 할까?

우선 yujin 인스턴스가 어떤 생성자 함수(또는 클래스)인지를 prototype.constructor를 통해서 알아 낸뒤 

해당 생성자(IdolMode())을 이용해서 new를 사용해야 한다. 

그러나 위 코드를 보면 yujin이라는 인스턴스.constructr(..)를 통해 똑같은 IdolModel 인스턴스를 만들어 냈다.

이게 어떻게 가능한 것일까?

프로토타입 체인 탐색 기능이 그것을 가능하게 해준다. 

우선 yujin 인스턴스의 프로토타입 체인은 아래와 같이 구성돼 있다.

yujin → yujin.__proto__ → IdolModel.prototype → Object.prototype → null

javascipt는 해당 객체에 없는 속성에 접근할 때 __proto__를 따라 null을 만날 때지 prototype에 해당 속성을 찾을 떄까지

탐색을 계속한다. 만약 null을 만났을 때에도 발견하지 못하면 undefined를 반환.

이게 바로 프로토타입 체인 탐색이다.

yujin 객체에는 constructor이라는 속성이 존재하지 않는다. 

고로 프로토타입 체인 탐색을 통해, 즉  yujin.__proto__을 통해 IdolModel.prototype을 탐색하게 되고

constructor: IdolModel을 보고 new IdolMode("장원영")을 실행하여 새로운 인스턴스를 만들어 낸다. 

console.log(yujin.__proto__.constructor === IdolModel); // true

 

3]디버깅 / 유지보수에 유용

constructor가 있으면 코드 분석이나 자동화 도구에서 생성자 추적이 쉬워집니다

 

circular reference(순환 참조)

위 prototype을 이해하였다면 아래의 코드도 이해가 될 것이다. 

console.log(IdolModel.prototype.constructor.prototype === IdolModel.prototype); // true

IdolModel.prototype.constructor= IdolModel임을 알았으니 위 결과는 당연한 것이다. 

즉, IdolModel 클래스의 prototype 객체의 constructor는 항상 IdolModel 클래스의 메모리 주소(참조값)을 공유하고 있다. 

그럼 아래와 같은 코드도 결과가 true일 것이다. 

console.log(IdolModel.prototype.constructor.prototype.constructor.prototype === IdolModel.prototype);

즉 같은 생성자 함수/클래스의 prototype 객체는 construct를 key 값으로 항상 IdolModel를 가리키고 있다. 

이제 위에 있는 그림2을 보면 순환 참조라는 것이 무엇인지 확실히 이해가 될 것이다. 

function IdolModel(name,year){
    
    if(new.target === undefined){
        return new IdolModel(name,year);
    }

    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin.__proto__ === IdolModel.prototype); // true

new를 통하여 yujin 객체를 생성한 순간 yujin 객체에는 __proto__라는 key 값이 생기며 해당 key값의 value에는 

IdolModel 클래스의 메모리 주소(참조값)이 자동으로 들어 간다(그림2 참조)

 

proto vs prototype

먼저 그림을 넣겠다. 그림에 대한 설명은 아래에서 이어진다.

그림3

위 그림을 이해하기 위하여 우선 코드를 보자

function IdolModel(name,year){
    
    if(new.target === undefined){
        return new IdolModel(name,year);
    }

    this.name=name;
    this.year=year;
    
    this.dance = function(){
        return `${this.name}이 춤을 춥니다`;
    }
}

const yujin = new IdolModel("안유진",2003);
console.log(yujin.__proto__ === IdolModel.prototype); // true

//추가
const testObj = {};
console.log(testObj.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Object.prototype); // true

자 이제 객체가 생성이 되면 자동적으로 __proto__라는 객체가 생성된다는 것을 알았다. 

여기서 잠깐!! __proto__가 왜 객체인지 알 수 있을까??

__proto__ 메모리에는 prototype이라는 객체의 참조값을 가리키고 있기 때문이다.

 본론으로 돌아 와서 __proto__는 아래와 같이 정의할 수가 있다. 

"객체가 생성되는 순간 자동으로 생성되며 __proto__는 해당 객체를 생성한 생성자 함수/클래스의 prototype 객체의

참조값"이다. 

고로, __proto__를 통해 해당 객체의 상위 프로토타입 객체 를 알아 낼 수가 있다

위 그림과 코드를 통해 알 수가 있듯이 모든 생성자 함수는 Function 프로토타입 객체 를 상속 받고 있으며, 그 Function

프로토타입 객체 는 Obejct 상위 프로토타입 객체 를 상속 받고 있다. 

 

위 그림과 코드를 바탕으로 proto와 prototype의 개념을 정리해 보겠다. 

1] javascript에서는 생성자 함수/클래스(객체x)를 선언함과 동시에 자동적으로 prototype 객체가 생성됨.

2] javascript에서는 모든 객체의 프로토타입 객체는 최상위 프로토타입 객체로 Object 프로토타입 객체 를 상속 받고 있

다.

3] javascript에서 모든 생성자 함수는 Function 프로토타입 객체 를 상속 받고 있으며, 그 Function 프로토타입 객체

한 Object 프로토타입 객체를 상속 받고 있다. 

4] javascript에서 모든 객체는 __proto__ 속성을 자동적으로 가지며 해당 객체가 어떤 생성자 함수/클래스에 의해 

생성된 객체인지를 가리킨다(정확히는 __proto__ 변수에 상위 로토타입 객체 를 가리킴)

5] 모든 prototype은 {constructor,__proto__}을 key값으로 가지고 있다(Object는 최상위 클래스이므로 proto 없음) 

    a) constructor : 해당 prototype 객체에 대응되는 생성자 함수/클래스의 메모리 주소(참조값)

     b) __proto__: 해당 prototype 객체의 [상위 생성자 함수/클래스]의 prototype 객체 메모리 주소(참조값)

 

 

Q. 왜 IdolModel.prototype은 Function.prototype을 상속 받지 않지? 왜 IdolModle.prototype만 상속을 받지?

A. 위에서도 설명을 하였지만 javascript에서는 함수는 결국 객체라는 것과 관련이 있다. 

 

각각이 상속받는 것

IdolModel은 함수이므로...

console.log(IdolModel.__proto__ === Function.prototype); // true

 

  • IdolModel은 함수니까 Function.prototype을 상속받습니다.
  • 즉, IdolModel 함수 자체는 Function의 객체입니다.

 

IdolModel.prototype은 객체이므로...

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

 

 

  • IdolModel.prototype은 일반 객체입니다.
  • 그래서 Object.prototype을 상속받습니다.
  • 이 객체는 인스턴스가 만들어질 때(new IdolModel(...)) 그 인스턴스의 [[Prototype]]이 됩니다.

요약

 

  • JavaScript는 함수 자체도 객체입니다.
  • 따라서 IdolModel은 함수이자 객체이므로 Function.prototype을 상속.
  • 하지만 IdolModel.prototype은 인스턴스의 프로토타입을 위한 일반 객체이기 때문에 Object.prototype을 상속