[TIL] Javascript 개념

JISU·2021년 11월 24일
0

TIL

목록 보기
4/27

일기


오늘 배운 것

Javascript 개념

Class

class란

속성과 메서드를 한데 묶어 놓은 것.
객체를 생성할 때 사용하는 template이라고 보면 됨.

Class는 그냥 찍어내는 도구이기 때문에 정의만 되어 있지, data를 가지고 있지 않기 때문에 메모리에 올라가지 않는다.

<-> Object란
instance of a class.
class를 이용해서 만든 객체(instance).

객체는 class를 이용해서 만든 실물이고, data가 들어있기 때문에 생성하면 메모리에 올라가게 된다.

저번에 prototype을 공부하면서 알았던 사실!
javascript는 객체 지향 언어가 아니고, 프로토타입 기반의 언어이기 때문에 class 개념이 없다는 것이다.

근데, 내가 여태까지 사용하던 class는 무엇인지..?

class 문법은 ES6에서 추가된 문법이다.

앗..? 그럼 그 전에는 어떻게 객체 지향적인 프로그래밍을 했지?
상속이랑 객체 만드는 건 어떻게 한거?

ES5까지는 함수를 이용해서 상속의 개념을 사용하고 있었다.

밑에서 ES5 문법에서 사용한 객체 지향 프로그래밍을 볼 것이다.

어찌됐든, 원래부터 프로토타입 기반 언어에서 함수와 프로토타입을 이용해서 객체 지향적인 프로그래밍을 잘 사용하고 있었다.
때문에, 추가된 class는 문법적 설탕(syntatical sugar)라고 한다.
(왜냐하면, 원래 있던 기능인데 문법적으로만 조금 더 간편하도록 만든 것이므로)

class 예제를 다음과 같이 알아보도록 한다.

1) ES5와 ES6에서 문법 차이
2) 상속과 다양성 (ft. 오버라이딩)
3) getter와 setter
4) public, private fields
  1. ES5와 ES6에서 문법 차이

    1) 생성자

  • ES5
const User = function (name, age) {
  this.name = name
  this.age = age
  this.showName = function () {
    console.log(this.name)
  }
}

const mike = new User("Mike", 30)

기존 function과 prototype을 이용해서 class 처럼 동작하는(이를 이용해서 객체/자식을 만드는) 객체 지향적인 프로그래밍을 했다.

-ES6 (class)

class User2 {
  // 객체를 만들어 주는 생성자 메서드
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  // User2 객체의 메서드
  // User2의 prototype에 저장됨!
  showName() {
    console.log(this.name)
  }
}

const tom = new User2("Tom", 19)

new를 통해 생성자(User)를 호출하면 constructor가 실행되며 object(instance)가 생성된다.

여기서 차이점,

mike.showName() // showName이 User의 객체 내에 있다.
tom.showName() // showName이 User2의 proto 내부에 있다.


function 생성자는 showName(메서드)가 User 객체 내에 있다.
반면,
class 생성자는 showName(메서드)가 User2의 proto 내부에 있다.

따라서,
User 객체에도 똑같이 showName을 proto에 담으려고 하려면 prototype 속성에 showName을 정의해주면 된다.

User.prototype.showName = function () {
  console.log(this.name)
}

이렇게 함으로써 class 문법 없이도 함수와 prototype을 이용해서 여느 언어에서의 class 처럼 생성자를 구현할 수 있었다.
그렇다면, 단지 문법적 설탕일까?
함수 생성자에는 한가지 문제점은 있긴 하다.

2) 함수 생성자의 문제점

const mike2 = User("Mike", 30)
const tom2 = User2("Tom", 19)

console.log(mike2) // undefined
console.log(tom2) // Error 발생

new로 호출해주지 않아도 함수 생성자에서는 오류가 발생하지 않는다.
나도 모르게 실수로 undefined 값이 들어갔는데 오류없이 동작하게 된다는 것.
반면, class 문법으로 구현한 생성자는 constructor가 Class로 명시되어 new로 호출하지 않으면 Error가 발생되도록 설계되어있다.

3) 또 한가지 차이점
for in문에서도 차이가 난다.

// 함수 생성자 (User)
for (const p in mike) {
  console.log(p)
} // name age showName undefined

// class 생성자 (User2)
for (const p in tom) {
  console.log(p)
} // name age undefined

for in문 에서는 객체가 가진 속성만 보여주는데,

함수 생성자에서는 가지고 있는 속성(property)과 프로토타입에 있는 showName 메서드까지 다 나온다.

따라서 class의 메서드는 for in문 에서 제외된다.
만약 prototype에 있는 속성을 보려면 hasOwnProperty라는 메서드를 사용해야한다.

  1. 상속과 다양성 (ft. 오버라이딩)
  • 상속
class Car {
  constructor(color) {
    this.color = color
    this.wheels = 4
  }
  drive() {
    console.log("drive~~")
  }
  stop() {
    console.log("STOP!")
  }
}

class Bmw extends Car {
  park() {
    console.log("parked.")
  }
}

const z4 = new Bmw("blue")

z4는 Bmw의 상속을 받고 그 Bmw는 Car의 상속을 받으므로
z4는 Car의 속성 또한 사용할 수 있다.

z4의 속성을 살펴보면 다음과 같다.

z4는 Bmw 클래스로 만들어졌다(Bmw의 instance이다/(z4의 construtor는 Bmw이고, Bmw의 instance는 z4)).
때문에 상속받은 color와 wheels를 속성으로 가지고 있다.
또 z4의 prototype에는 constructor(instance를 만든 원형)와 park 메서드(Bmw 클래스가 가지고 있는 메서드)가 들어있다.
-> park는 Bmw의 메서드이기 때문에 만들어진 객체 z4에는 기본 속성에 없는 것!

또 Bmw의 prototype에는 constructor가 Car이고, Car에서 상속받은 속성 drive, stop이 있다.

똑같이 Car의 prototype은 Object가 된다. (Javascript에서 만드는 모든 객체는 Object 객체의 instance)
Javscript에서 제공하는 Object의 메서드는 저렇게 다양하다.

결론적으로 z4는 Bmw, Car, Object의 메서드들을 모두 상속받고 있다.

  • 메서드 오버라이딩
    만약 상속받는 자식 객체에서 메서드를 생성하려고 하는데 부모(생성자) 객체의 메서드 이름과 겹치면??
class Car {
  constructor(color) {
    this.color = color
    this.wheels = 4
  }
  drive() {
    console.log("drive~~")
  }
  stop() {
    console.log("STOP!")
  }
}

class Bmw extends Car {
  park() {
    console.log("parked.")
  }
  stop() {
    console.log("OFF")
  }
}

const z4 = new Bmw("blue")

z4.stop() // OFF

Car에 stop이라는 메서드가 있지만, Bmw에 stop이라는 똑같은 이름의 메서드를 정의하면
하위 객체 메서드로 덮어씌어진다. (당연, z4에 없는 메서드 찾으러 갔는데 Bmw에 바로 있으니 굳이 Car에 있는 메서드를 찾을 필요가 없다.)

이렇게 자식 객체의 메서드가 사용되므로써 메서드가 덮어 씌어지는 것을 메서드 오버라이딩(Method Overriding)이라고 한다.

그렇다면, 만약 똑같은 이름을 쓰면서도 부모 객체에 있는 메서드를 사용하고 싶다면?
상속받은 메서드의 기능에다가 그 객체 하나에만 추가 기능을 달고 싶다면?

  • 다양성
    살리고 싶은 부모 객체 메서드의 내용을 다 쓸 일은 없을 테고, 어떤 것이 이걸 가능하게 할까?
class Car {
  constructor(color) {
    this.color = color
    this.wheels = 4
  }
  drive() {
    console.log("drive~~")
  }
  stop() {
    console.log("STOP!")
  }
}

class Bmw extends Car {
  park() {
    console.log("parked.")
  }
  stop() {
    super.stop() // 부모 클래스에 정의된 메서드를 불러와서 사용
    console.log("OFF")
  }
}

const z4 = new Bmw("blue")

z4.stop()
// STOP!
// OFF

바로 super를 사용한다.
부모 객체(Car)에 있는 stop 메서드를 super를 이용해서 부르고, 추가하고 싶은 다른 기능을 추가하면 된다.

이렇게 객체마다 다양성을 줄 수 있다.

  • 생성자 오버라이딩
class Car {
  constructor(color) { // {}
    this.color = color
    this.wheels = 4
  }
  drive() {
    console.log("drive~~")
  }
  stop() {
    console.log("STOP!")
  }
}

class Bmw extends Car {
  constructor(color) {
    super(color)
    this.navigation = 1
  }
  park() {
    console.log("parked.")
  }
  stop() {
    super.stop()
    console.log("OFF")
  }
}

const z4 = new Bmw("blue")

상속을 받는 class의 constructor에서는 this를 사용하기 전에 super를 이용해서 부모 class의 constructor를 먼저 호출해야한다.

원래, class의 constructor는 먼저 빈 객체 {} 를 만들고, 그 안에 this로 이 객체를 가리키게 해서 만든다.

그러나,
extends로 상속을 받아 만들어지는 class는 빈 객체를 생성하는 것을 생략한다.
따라서 항상 부모 클래스의 constructor를 먼저 실행해주어야 한다.

하지만 추가적인 문제는,
상속 받는 class에서 constructor안에 super를 실행할때 인자 없이 호출하면
z4에서 인자를 전달해줘도 'blue' Car의 constructor에 전달이 되지 않아 undefined가 나온다.
따라서 constructor에서 super를 호출할 때에는 부모 클래스에서 받는 인자를 전달해주어야한다.

왜냐하면,
constructor가 없으면 javascript에서 알아서
constructor(...args) {
super(...args)
}
를 집어 넣고 동작을 하기 때문에
자식 생성자는 무조건 부모 생성자를 부르게 되어 있다는 것이다.

그래서 따로 constructor를 넣고 super를 호출할 때에는
부모 클래스의 constructor가 받는 인자를 제대로 전달해주어야 잘 동작한다.

  1. getter와 setter
    요약
    getter: 사용자가 함부로 값을 변경하지 못하도록 private하게 해주는 메서드
    setter: 사용자가 잘못 입력했을 경우 방어적으로 값을 set 해주는 메서드

age라는 변수의 무작위 set을 막고자 get / set 메서드를 활용해보자.

class User {
  constructor(firstName, lastName, age) {
    this.firstName = firstName
    this.lastName = lastName
    this.age = age
  }

  // getter
  get age() {
    return this._age
  }

  // setter
  set age(value) {
    // 이런 식으로 잘못된 값을 setter를 통해 조정해줄 수 있다
    // if (value < 0) {
    //   throw Error("age can not be negative")
    // }

    // 아니면 좀 더 부드럽게
    this._age = value < 0 ? 0 : value
  }
}

const user1 = new User("Steve", "Job", -1)
console.log(user1.age) // 0

user1 객체를 만들면서 age 값에 -1을 전달한다.
실제로는 age가 음수인 경우가 없으므로 사용자가 잘못으로라도 음수를 입력했을 때, setter로 처리해줄 수 있다.

tricky park!
잘못된 예

class User {
  constructor(firstName, lastName, age) {
    this.firstName = firstName
    this.lastName = lastName
    this.age = age
  }

  // getter
  get age() {
    return this.age // age와 다른 변수 사용해야함!
  }

  // setter
  set age(value) {
    this.age = value // age와 다른 변수 사용해야함!
  }
}

const user1 = new User("Steve", "Job", -1)

age 변수를 가지는 getter와 setter를 정의한다.

age 변수의 get이 정의가 되면 constructor의 this.age가 호출이 될 때 이 get 메서드를 호출한다.

age 변수의 set이 정의가 되면 constructor에서 값을 어딘가에 할당하려고 할 때(즉, "=" 대입 연산이 실행될 때),
메모리에 있는 값을 바로 사용해서 넣어주는 것이 아니라 setter를 호출하고, 이 안에서 value를 this.속성(age) 에다가 할당하려고 한다.

그런데, 값을 할당하려고 하면 또 set이 호출이 되고, set이 또 할당하려고 하면서 call stack size exceeded가 되어 Error가 발생한다.

따라서, 이를 방지하기 위해서 할당하려고 하는 "age"를 getter와 setter와는 다른 변수명을 사용해야한다.

  1. public, private fields
class Experiment {
  publicField = 2 // public
  #privateField = 0 // private
}

const experiment = new Experiment()

console.log(experiment.publicField) // 2
console.log(experiment.privateField) // undefined // 값을 변경할 수도, 읽을 수도 없다

아주 최근에 추가된 것. 아직 많이 쓰지는 않고, 사파리에서도 지원하지 않아 바벨 사용해야한다.

그냥 이런 것이 있다 정도 알아두면 좋을 것 같다.

[참고 및 출처] https://www.youtube.com/watch?v=OpvtD7ELMQo
[참고 및 출처] https://www.youtube.com/watch?v=_DLhUBWsRtw

profile
블로그 이전

0개의 댓글