8/16 js 기본 문법 종합 5주차

성준호·2024년 8월 16일
0
  • 클래스와 생성자 함수
    기존 개발자들이 js에서도 class 형식으로 개발을 하다보니 ES6에서 클래스를 도입하였다.
// 클래스라는 설계도 만들어보기
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억

  • getter와 setter
// 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가 호출되지 않는다.

  • ES13 (2022) 공식 프라이빗 필드
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가 호출되게끔 하였다.

  • 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씩 증가시킬 수 있다.

profile
안녕하세요

0개의 댓글