6 분 소요

[TIL]

Javascript

Class 문법

Class의 정의 (ft. Object)

Class 란?

자바스크립트에서 Class는 ES6 에서 도입되었으며 클래스 기반의 객체 지향 프로그래밍을 가능하도록 한 객체 생성 메커니즘이다.
객체를 생성할 때 사용하는 template이다.

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

Javascript는 객체 지향 언어가 아니고, prototype 기반의 언어이기 때문에 class 개념이 없다.

우리가 지금까지 사용하던 class는 무엇이었던건가?


그렇다면, 이전에는 어떻게 객체 지향적인 프로그래밍을 했을까?


따라서 요약하자면

ES6 이전부터 프로토타입 기반 언어에서 함수와 prototype을 이용해서 객체 지향적인 프로그래밍을 잘 사용하고 있었긴 하다. 때문에, 추가된 class는 문법적 설탕(syntatical sugar)이라고 한다. (원래 있던 기능이나 문법적으로만 조금 더 간편하도록 만든 것이므로)
하지만, 자바스크립트에서의 새로운 객체 생성 메커니즘으로도 볼 수 있다. (클래스와 생성자 함수가 가지는 차이가 있기 때문에)

하지만, 완전히 문법적 설탕이라고 보기도 어렵다.

왜냐하면, 클래스와 생성자 함수가 가지는 차이가 있기 때문이다.

  • 클래스와 생성자 함수의 차이
  1. new 연산자로만 호출할 수 있다. (생성자 함수는 new 없이 호출하면 일반 함수로 호출)
  2. extends와 super 키워드를 제공
  3. 클래스는 호이스팅이 발생하지 않는 것 처럼 동작. (생성자 함수는 함수 호이스팅/변수 호이스팅 일어남)
  4. 클래스 내부는 무조건 strict mode가 적용
  5. 클래스의 contstructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]] 값이 false이다.


예제로 보는 Class

다음과 같은 순서로 class의 예제를 알아본다.

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

    • 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 내부에 있다.
      

      생성자 함수는 showName(메서드)가 User 객체 내에 있다. 반면, class 생성자는 showName(메서드)가 User2의 proto 내부에 있다. (상속이 될 수 있다) 따라서, User에도 똑같이 showName을 proto에 담으려고 하려면 prototype 속성에 showName을 정의해주면 된다.

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

      이렇게 함으로써 class 문법 없이도 함수와 prototype을 이용해서 여느 언어에서의 class 처럼 생성자를 구현할 수 있다. (프로토타입 체인이 가능해지면서 상속이 되는)

  • 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문 에서 제외된다.

    -> [[Enumerable]] 프로퍼티 어트리뷰트가 false이기 때문이다.

    따라서, 만약 prototype에 있는 속성을 보려면 hasOwnProperty라는 메서드를 사용해야한다.

  • 정적 메서드와 프로토타입 메서드

    • 정적 메서드

      class Square {
        // 정적 메서드
        static area(width, height) {
          return width * height;
        }
      }
      
      console.log(Square.area(10, 10)); // 100
      

      정적 메서드는 클래스로 호출한다.

    • 프로토타입 메서드

      class Square {
        constructor(width, height) {
          this.width = width;
          this.height = height;
        }
      
        // 프로토타입 메서드
        area() {
          return this.width * this.height;
        }
      }
      
      const square = new Square(10, 10);
      console.log(square.area()); // 100
      

      메서드에서 this는 호출한 함수 객체에 바인딩이 되기 때문에 인스턴스를 가리킨다.

      따라서, 프로토타입 메서드는 인스턴스로 호출해야 하므로 인스턴스를 생성해서 그 메서드를 호출해야 한다.

  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의 속성 또한 사용할 수 있다.

  • 메서드 오버라이딩

    만약 상속받는 자식 객체에서 메서드를 생성하려고 하는데 부모(생성자) 객체의 메서드 이름과 겹치면??

    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를 먼저 호출해야한다.

  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 // 값을 변경할 수도, 읽을 수도 없다
    

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

댓글남기기