// 클래스라는 설계도 만들어보기
class Person {
// 우리는 사람이기 때문에 필수요소
// name, age
constructor(name, age) {
this.name = name;
this.age = age;
}
// 메서드형태로 동사 표현
sayHello() {
console.log(`${this.name} 님 안녕하세요`);
}
// 내 나이는 ~살이에요 라고 출력하는 메서드
sayAge() {
console.log(`내 나이는 ${this.age}살이에요`);
}
}
// 설계도를 통해 인스턴스를(실제 사물) 만들어보기
// 이름은 홍길동이고 나이는 30살인 사람 하나를 만들어줘
const person1 = new Person("홍길동", "30");
const person2 = new Person("홍길순", "25");
person1.sayHello();
person1.sayAge();
person2.sayHello();
person2.sayAge();
홍길동 님 안녕하세요
내 나이는 30살이에요
홍길순 님 안녕하세요
내 나이는 25살이에요
=> 클래스는 객체를 만들기 위한 설계도이다. 클래스 안에 constructor 생상자 함수를 정의하여 생성할 인스턴스의 속성을 전달 받을 수 있다. 여기서 생성자 함수의 this는 생성할 인스턴스를 참조한다. 또 메서드 형태로 동작을 표현할 수 있다. 클래스 안에서 메서드를 정의할 때 function 키워드는 쓰지 않는다.
// 클래스 연습해보기
// [요구사항]
// 1. Car라는 새로운 클래스를 만들되, 처음 객체를 만들 때는 다음 4개의 값을 필수로 입력돼야 한다
// (1) modelName
// (2) modelYear
// (3) type
// (4) price
// 2. makeNoise() 메서드를 만들어 클락션을 출력해주세요.
// 3. 이후 자동차를 3개 정도 만들어주세요.
class Car {
constructor(modelName, modelYear, type, price) {
this.name = modelName;
this.year = modelYear;
this.type = type;
this.price = price;
}
makeNoise() {
console.log(`${this.name}, ${this.year}, ${this.type}, ${this.price}`);
}
}
let car1 = new Car("아반떼", "2017", "세단", "3억");
let car2 = new Car("투싼", "2018", "SUV", "1억");
let car3 = new Car("다마스", "2020", "화물차", "30억");
car1.makeNoise();
car2.makeNoise();
car3.makeNoise();
아반떼, 2017, 세단, 3억
투싼, 2018, SUV, 1억
다마스, 2020, 화물차, 30억
// Getters와 Setters
// 객체지향 프로그래밍 언어 -> G, S
// 클래스 --> 객체 (인스턴스)
// 프로퍼티(constructor)
// new Class(a, b, c)
class Rectangle {
constructor(height, width) {
// underscore: private (은밀하고, 감춰야 할때)
this._height = height;
this._width = width;
}
// width를 위한 getter
get width() {
return this._width;
}
// width를 위한 setter
set width(value) {
// 검증 1: value가 음수이면 오류!
if (value <= 0) {
console.log("가로 길이는 0보다 커야 합니다!");
} else if (typeof value !== "number") {
console.log("가로 길이로 입련된 값이 숫자 타입이 아닙니다!");
}
this._width = value;
}
// height를 위한 getter
get height() {
return this._height;
}
// height를 위한 setter
set height(value) {
if (value <= 0) {
console.log("세로 길이는 0보다 커야 합니다!");
} else if (typeof value !== "number") {
console.log("세로 길이로 입련된 값이 숫자 타입이 아닙니다!");
}
this._height = value;
}
// getArea: 가로 * 세로 -> 넓이
getArea() {
const a = this._width * this._height;
console.log(`넓이는 => ${a}입니다`);
}
}
// instance 생성
const rect1 = new Rectangle(10, 20);
const rect2 = new Rectangle(10, 30);
const rect3 = new Rectangle(15, 20);
rect1.getArea();
넓이는 => 200입니다
=> getter와 setter는 각각 속성의 값을 읽을 때, 수정할 때 호출되는 메소드이다.
const rect1 = new Rectangle(10, 20); 여기서 생성자 함수가 호출된다.
다만 여기서 클래스의 속성명이 this.height, this._width = width;로 가 붙어있는데 이는 개발자들 사이에서 관습적으로 표기하는 프라이빗 필드이다. 이는 클래스 외부에서 접근하지 말라는 의미이다. 또한 생성자에서 프라이빗 필드를 사용하여 값을 할당할 경우 getter와 setter가 호출되지 않는다.
class Rectangle {
#height;
#width;
constructor(height, width) {
this.height = height;
this.width = width;
}
// width를 위한 getter
get width() {
return this.#width;
}
// width를 위한 setter
set width(value) {
// 검증 1: value가 음수이면 오류!
if (value <= 0) {
console.log("가로 길이는 0보다 커야 합니다!");
} else if (typeof value !== "number") {
console.log("가로 길이로 입련된 값이 숫자 타입이 아닙니다!");
}
this.#width = value;
}
// height를 위한 getter
get height() {
return this.#height;
}
// height를 위한 setter
set height(value) {
if (value <= 0) {
console.log("세로 길이는 0보다 커야 합니다!");
} else if (typeof value !== "number") {
console.log("세로 길이로 입련된 값이 숫자 타입이 아닙니다!");
}
this.#height = value;
}
// getArea: 가로 * 세로 -> 넓이
getArea() {
const a = this.#width * this.#height;
console.log(`넓이는 => ${a}입니다`);
}
}
// instance 생성
const rect1 = new Rectangle(-1, 20);
const rect2 = new Rectangle(10, 30);
const rect3 = new Rectangle(15, 20);
rect1.getArea();
세로 길이는 0보다 커야 합니다!
넓이는 => -20입니다
=> ES13 (2022)에서 공식적으로 프라이빗 필드가 추가되었다. 그리고 생성자 함수에서 프라이빗 필드를 사용하지 않고 일반 속성을 정의하여 getter와 setter가 호출되게끔 하였다.
class Car {
#name;
#year;
#type;
#price;
constructor(modelName, modelYear, type, price) {
this.name = modelName;
this.year = modelYear;
this.type = type;
this.price = price;
}
// name의 getter
get name() {
return this.#name;
}
// name의 setter
set name(value) {
if (value.length <= 0) {
console.log("모델명이 입력되지 않았습니다");
return;
} else if (typeof value !== "string") {
console.log("입력된 모델명이 문자형이 아닙니다");
return;
}
this.#name = value;
}
// year의 getter
get year() {
return this.#year;
}
// year의 setter
set year(value) {
this.#year = value;
}
// type의 getter
get type() {
return this.#type;
}
// type의 setter
set type(value) {
this.#type = value;
}
// price의 getter
get price() {
return this.#price;
}
// price의 setter
set price(value) {
this.#price = value;
}
makeNoise() {
console.log(`${this.#name}, ${this.#year}, ${this.#type}, ${this.#price}`);
}
}
// 자동차 인스턴스 생성
let car1 = new Car(12, "2017", "세단", "3억");
let car2 = new Car("투싼", "2018", "SUV", "1억");
let car3 = new Car("다마스", "2020", "화물차", "30억");
// 출력
car1.makeNoise();
car2.makeNoise();
car3.makeNoise();
// setter를 이용한 값 설정
car1.name = "setter로 바꾼 아반떼";
car1.makeNoise();
입력된 모델명이 문자형이 아닙니다
undefined, 2017, 세단, 3억
투싼, 2018, SUV, 1억
다마스, 2020, 화물차, 30억
setter로 바꾼 아반떼, 2017, 세단, 3억
=> car1 인스턴스의 name 속성에 12를 넣었다. setter가 호출되고 유효성 검사에 따라 오류 메시지가 출력된다.
// 상속
// Class -> 유산으로 내려주는 주요 기능!!
// 부모 <-> 자식
// 동물 전체에 대한 클래스
class Animal {
// 생성자
constructor(name) {
this.name = name;
}
// 메서드 (짖다)
speak() {
console.log(`${this.name} says!`);
}
}
const me = new Animal("man");
me.speak();
class Dog extends Animal {
// 부모에게서 내려받은 메서드를 재정의할 수 있음
// overriding... 부모에게서 상속 받아 재정의함
speak() {
console.log(`${this.name} barks!`);
}
}
const puppy1 = new Dog("뽀삐");
puppy1.speak();
man says!
뽀삐 barks!
=> 클래스와 생성자 함수를 통해 me 인스턴스를 생성하였다. 인스턴스의 name 속성을 출력한다.
여기서 Animal의 자식 클래스인 Dog 클래스를 추가하였다. extends 키워드를 통해 부모 클래스를 설정할 수 있다. 자식 클래스는 부모로부터 속성, 메서드, 생성자, 정적 요소들을 상속 받는다. 이때 변경하고 싶은 부분을 재정의할 수 있다. 이를 overriding이라 한다. 여기서는 메소드를 재정의하였다.
// 클래스 연습해보기
// [요구사항]
// 1. Car라는 새로운 클래스를 만들되, 처음 객체를 만들 때는 다음 4개의 값을 필수로 입력돼야 한다
// (1) modelName
// (2) modelYear
// (3) type
// (4) price
// 2. makeNoise() 메서드를 만들어 클락션을 출력해주세요.
// 3. 이후 자동차를 3개 정도 만들어주세요.
// [추가 요구사항]
// 1. 전기차 클래스 <= Car 클래스의 상속을 받음
class Car {
constructor(modelName, modelYear, type, price) {
this.name = modelName;
this.year = modelYear;
this.type = type;
this.price = price;
}
makeNoise() {
console.log(`${this.name}, ${this.year}, ${this.type}, ${this.price}`);
}
}
// 전기차 Class 정의
class ElectronicCar extends Car {
#chargeTime;
constructor(modelName, modelYear, price, chargeTime) {
// Car (부모 class)에게도 알려주기!!
super(modelName, modelYear, "e", price);
this.chargeTime = chargeTime;
}
set chargeTime(value) {
this.#chargeTime = value;
}
get chargeTime() {
return this.#chargeTime;
}
}
const eleCar1 = new ElectronicCar("테슬라", "2023", 9000, 60);
eleCar1.makeNoise();
console.log("----------------------");
console.log(eleCar1.chargeTime);
eleCar1.chargeTime = 20;
console.log(eleCar1.chargeTime);
테슬라, 2023, e, 9000
-=-=-=-=-=-=-=-=-=-=
60
20
=> 자식 클래스 EletronCar를 생성하여 생성자를 재정의하였다. 이때 부모 클래스의 생성자를 super로 호출하여 입력받은 속성값을 할당한다. 이때 type 속성은 "e"로 할당하였다. 마지막으로 chargeTime 속성을 정의하였다.
// Static Method 정적 메소드
class Calculator {
static add(a, b) {
console.log("더하기");
return a + b;
}
static subtract(a, b) {
console.log("빼기");
return a - b;
}
}
console.log(Calculator.add(3, 5));
console.log(Calculator.substract(3, 5));
더하기
8
빼기
-2
=> 정적 메소드는 클래스 자체에서 호출할 수 있는 메소드이다. 따로 인스턴스를 만들지 않고 클래스 이름으로 접근할 수 있다. 정적 메소드를 정의할 땐 앞에 static 키워드를 붙인다.
Calculator.add(3, 5) -> Calculator 클래스에 정의된 add 정적 메소드를 호출하였다.
// JS 엔진은 함수를 어디서 호출했는지가 아니라
// 어디에 정의했는지에 따라서 스코프(상위 스코프)를 결정한다
// 외부 렉시컬 환경에 대한 참조값 => outer
// 함수 정의가 평가되는 시점
const x = 1;
// ousterFunc 내에 innerFunc가 호출되고 있음에도 불구하고
function outerFunc() {
const x = 10;
innerFunc();
}
// innerFunc와 outerFunc는 서로 다른 스코프를 갖고 있다.
function innerFunc() {
console.log(x);
}
outerFunc();
1
=> outerFunc 스코프 안에서 innerFunc를 호출하고 있다. 그러나 두 함수는 서로 다른 스코프를 갖고있기 때문에 innerFunc는 전역 변수로 선언된 1의 값을 출력한다. 함수 호출이 아닌 정의(선언)를 기준으로 스코프를 결정한다.
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
// outer 함수를 실행해서 innerFunc에 담음
// outer 함수의 return 부분을 innerFunc에 담는다
const innerFunc = outer();
// ------- 여기서 outer 함수의 실행 컨텍스트는?
innerFunc();
10
=> 외부 함수 outer 안에 중첩 함수 inner가 정의돼있다. 그리고 outer 함수는 inner 함수를 반환한다.
밑에서 함수 표현식으로 이 outer 함수의 반환값(inner)을 innerFunc에 할당하였다. 할당과 동시에 outer 함수의 실행은 종료된다.
이 innerFunc를 호출하면 종료된 outer 함수의 const x = 10을 여전히 참조할 수 있다.
// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터 구현
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가
return ++num;
};
console.log(increase());
num = 100;
console.log(increase());
console.log(increase());
// 보완해야 할 사항
// 1. 카운트 상태 (num 변수 값)
// => increase 함수가 호출되기 전까지는 변경되면 안 된다
// 2. 이를 위해서 count 상태는 increase 함수만이 변경
// 3. 전역 변수 num이 문제다 -> 지역변수?
1, 101, 102
=> 호출될 때마다 호출 횟수를 누적하여 출력하는 카운터를 구현하는 코드이다.
그런데 밑에서 num = 100으로 인해 갑자기 횟수가 바뀌는 문제가 있다.
// 카운트 상태 변경 함수 #2
const increase = function () {
let num = 0;
return ++num;
};
console.log(increase());
console.log(increase());
console.log(increase());
// [리뷰]
// 1. num 변수는 increase 함수의 지역변수로 선언 -> 변경은 방지
// -> num 변수는 오직 increase 함수만이 변경할 수 있었음
// 2. 하지만 increase()가 호출될 때마다 num이 초기화되는 이상한 코드...
// * 의도치 않은 변경은 방지하면서 + 이전 상태를 유지해야 함
1, 1, 1
=> 우선 num 변수를 오직 increase 함수 내에서 제어할 수 있도록 지역 변수로 선언하였다.
그러나 호출할 때마다 let num = 0으로 초기화 되어 항상 1을 출력한다.
// 카운트 상태 변경 함수 #3
const increase = (function () {
let num = 0;
// 클로저
return function () {
return ++num;
};
})();
console.log(increase());
num = 100;
console.log(increase());
console.log(increase());
// [코드 설명]
// 1. 위 코드가 실행되면 즉시 실행 함수가 호출 -> 함수가 반환(inner) -> increase에 할당
// 2. increase 변수에 할당된 함수는 자신이 정의된 위치에 의해서 결정된 사우이 스코프인 즉시 실행 함수의 '렉시컬' 환경을 기억하는 클로저 -> let num = 0을 기억한다
// 3. 즉시 실행 함수는 -> 즉시 소멸된다.
// * 결론: num은 초기화 X. 외부에서 접근할 수 없는 은닉된 값. 의도하지 않은 변경도 걱정할 필요가 없다.
// => increase에서만 변경할 수 있기 때문에
1, 2, 3
=> 이를 클로저를 활용하여 개선할 수 있다. 우선 함수 표현식으로 increase 함수를 선언하였다. 이는 즉시 실행 함수로 return 값인 function () { return ++num; };을 반환한다.
이 함수는 즉시 실행 함수 내부에 정의돼있다. 따라서 let num = 0을 참조한다.
이를 consoloe.log로 실행하여 출력하면 즉시 실행 함수의 실행은 끝났으나 increase 함수는 클로저로서 계속 num을 참조한다.
결론: 최초 호출된 즉시 실행 함수에서 let num = 0으로 초기화되고 이후 클로저로서 반환된 함수는 num을 계속 참조할 수 있어 1씩 증가시킬 수 있다.