코린이의 기록

[ES6] 객체와 객체지향 프로그래밍 본문

javascript,HTML,CSS

[ES6] 객체와 객체지향 프로그래밍

코린이예요 2019. 6. 30. 23:44
반응형

클래스는 함수다. 

ES6에 생겨난 클래스. 

Javascript에서 class는 클래스 생성자로 사용할 함수를 만든다는 의미이다. 주의할점은 클래스 문법이 추가되었다는 것이지, 자바스크립트가 클래스 기반으로 바뀌었다는 것은 아니다.

function FuncCar() {
  this.make = "Telsa";
  this.model = "Model S";
}
const car1  = new FuncCar();

console.log(car1.make);  // => Telsa
console.log(car1.model);  // => Model S
class ClassCar() {
  this.make = "Telsa";
  this.model = "Model S";
}
const car1  = new ClassCar();

console.log(car1.make);  // => Telsa
console.log(car1.model);  // => Model S

typeOf FuncCar // "function"

typeOf ClassCar // "function"

 

짚고 넘어갈 OOP기본 용어

(ex 자동차)

- 클래스 : 자동차 (추상적이고 범용적인것)

- 인스턴스 : 특정 자동차 (구체적이고 한정적인것)

- 메서드 : 자동차의 기능 (ex. 시동을 거는기능)

- 서브클래스 : 클래스가 '운송수단'이라면, 서브클래스는 '자동차, 보트, 비행기..'  (클래스에 상대적임)

 

클래스 만들기

class Car {
   constructor () {
   }
 }

인스턴스 만들기

인스턴스를 만들때는 new를 사용한다.

const car1 = new Car();
const car2 = new Car();

 

(참고)

위에서 생성된 car1, car2 두객체의 클래스를 확인해보기 위해서 instanceof을 사용해본다.

car1 instanceof Car

car2 instanceof Car

 

클래스와 인스턴스 예시

클래스 ex.

class Car {
    constructor(make, model) {
        this.make = make;
        this.model = model;
        this.userGears = ['P', 'N', 'R', 'D'];
        this.userGear = this.userGears[0];
    }
    shift(gear) {
        if (this.userGears.indexOf(gear) < 0)
            throw new Error(`Invalid gear: ${gear}`);
        this.userGear = gear;

    }
}

여기서 this는 메서드를 호출하는 시점에 알 수 있다.

인스턴스 ex.

const car1 = new Car("Telsa", "Model S");
const car2 = new Car("Maxda", "3i");
car1.shift('D');
car2.shift('R');

console.log("user1 Gear : " + car1.userGear);
console.log("user2 Gear : " + car2.userGear);

car1은 Telsa제조사고 모델은 Model S이며 변속기능은 'D' 이다.

car2는 Maxda제조사고 모델은 3i이며 변속기능은 'R'이다.

 

Result 

user1 Gear : D
user2 Gear : R

만약에 직접 car1의 shift를 바꾸면 어떻게될까?

car1.userGear = 'X';
console.log("user1 Gear : " + car1.userGear);

Result 

user1 Gear : X

프로퍼티에 직접 접근이 가능하여 실수가 발생할 수 있다. 

이를 막을 방법으로 아래와 같이 구현해보자

방법 1

class Car {

   constructor(make, model) {

      this.make = make;

      this.model = model;

      this._userGears = ['P', 'N', 'R', 'D'];

      this._userGear = this._userGears[0];

   }

   get userGear(){

      return this._userGear;

   }

   set userGear(value){

      if(this._userGears.indexOf(value) < 0)

          throw new Error(`Invalid gear : ${value}`);

      this._userGear = value;

   }

   shift(gear){

      this.userGear = gear;

   }

}

 

const car1 = new Car("Telsa", "Model S");

const car2 = new Car("Maxda", "3i");

 

car1.userGear = 'X';

console.log("user1 Gear : " + car1.userGear);

차이점이 있다면 get, set 메서드를 만들어서 프로퍼티에 직접 접근하지 못하도록 하였다. 

Result 

Error: Invalid gear : X
    at Car.set userGear [as userGear] (c:\Users\Desktop\Untitled-1.js:13:15)
    ....
    ....

근데 이것은 근본적인 해결방법이 아니다. 

car1._userGear = 'X'; 로 한다면??

결국 car1의 기어변속 값이 바뀌는 것이다. 

여기서 "_"를 붙인다는 것은 단지 외부에서 접근해서는 안되는 프로퍼티 이름 이라고 단지 주의를 주는것이다. 

 

프로퍼티를 완벽하게 보호하기 위해서는 아래와 같이 수정할 수 있다.

방법 2

const Car = (function(){

     const carProps = new WeakMap();

 

     class Car {

           constructor(make, model) {

              this.make = make;

              this.model = model;

              this._userGears = ['P', 'N', 'R', 'D'];

              carProps.set(this, {userGear: this._userGears[0]});

           }

          get userGear(){

          return carProps.get(this).userGear;

          }

          set userGear(value){

             if(this._userGears.indexOf(value) < 0)

                throw new Error(`Invalid gear : ${value}`);

             carProps.get(this).userGear = value;

          }

          shift(gear){

             this.userGear = gear;

          }

     }

     return Car;

})();

 

여기서 사용한 방식은 WeakMap이라는인스턴스를 이용하였다. 이 방법은 javascript의 특징인 스코프 개념을 이용하여 보호하는 방법이다. (WekMap에 대한 개념 및 Map 대신 WeakMap을 쓰는 이유는 아래 블로그를 참고하도록.

https://m.blog.naver.com/1ilsang/221305941903)

WeakMap은 클래스(함수) 외부에서 접근할 수 없는 객체이므로 프로퍼티를 보호할 수 있다. 

 

 

 

 

 

 

 

프로토타입

자바스크립트는 프로토타입 기반 언어다. 

클래스의 인스턴스가 사용할 수 있는 메서드가 프로토타입 메서드를 말한다. 위 "Car"의 예제에서 보면 shift가 프로토타입 메서드가 된다.

 

함수를 정의하면 (function Car{})  함수만 생성되는것이 아니라 "Prototype Object"라는 것도 생성이된다. 이 Prototype Object라는 녀석은 기본적으로 "constroctor(상속자)"와 "__proto__" 프로퍼티를 가지고있다. constroctor 자격이 부여되어야 new를 통해 객체를 만들 수 있다. 

var Car = {};
var car1 = new Car();

Result

Uncaught Type Error: Car is not a constructor..

 

constructor와 __proto__을 이해해보자.

function Car() {};
var car1 = new Car();
console.log(car1);

Car라는 함수를 정의하면 Car 함수 뿐만 아니라 Car Prototype Object라는 것이 생성되는데 이 Prototype Object에는 "constructor"와 "__proto__"를 가지고 있다. __proto__는 Car라는 새로운 객체를 만들어내기 위해 사용된 Prototye Link이다. 즉 자기 자신을 만들어낸 객체의 원형을 의미한다. 

 

proto (숨은링크)는 상위에서 물려받은 객체의 프로토타입에 대한 정보이고 prototype 프로퍼티는 자신을 원형으로 만들어질 새로운 객체들 즉 하위로 물려줄 연결에 대한 속성이다.

ex. 

constrouctor : function Car () { ... }

__proto__ : object {constructor: , ...}

function Car() {};

Car.prototype.make = "Telsa";
Car.prototype.model = "Model S";

var car1 = new Car();
var car2 = new Car();

console.log(car1.make);

Car의 Prototype 속성으로 Prototype Object에 접근할 수 있다. new Operator를 통해 새 인스턴스를 만들었다. 새로 생성된 Object는 생성자의 prototype 프로퍼티에 접근할 수 있다. 객체 인스턴스는 생성자의 prototype 프로퍼티를 __proto__에 저장한다.

 

prototype 에서의 동적 디스패치 개념 

위에서 구현한 Car class를 이용하여 Object를 생성하고 console.log로 프러퍼티 및 메서드 접근에 대해 출력해본다.

Ex 1.

const car1 = new Car();
const car2 = new Car();

console.log("car1 : " + car1);
console.log("car1.shift === Car.prototype.shift : " + car1.shift === Car.prototype.shift);

car1.shift('D');

console.log("car1.userGear : " + car1.userGear);
//car1.shift('p'); // error
console.log("car2.userGear : " + car2.userGear);
console.log("car1.shift === car2.shift : " + car1.shift === car2.shift);
console.log("car1.shift : " + car1.shift);
console.log("car2.shift : " + car2.shift);

Result

car1 : [object Object]
false
car1.userGear : D
car2.userGear : P
false
car1.shift : shift(gear) {
            this.userGear = gear;
        }
car2.shift : shift(gear) {
            this.userGear = gear;
        }

첫번째 예제에서는 car1객체에 shift 메서드가 없다. car1.shift('D')를 호출하면 car1의 prototype에 shift라는 이름의 매서드를 검색하여 호출한다. 이때 car1에 shift라는 메서드를 추가해주면 어떻게 될까?

Ex 2. 

car1.shift = function(gear) {
    this.userGear = gear.toUpperCase();
}

console.log("car1.shift === Car.prototype.shift : " + car1.shift === Car.prototype.shift);
console.log("car1.shift === car2.shift : " + car1.shift === car2.shift);
console.log("car1.shift : " + car1.shift);
console.log("car2.shift : " + car2.shift);
car1.shift('p');
console.log("car1.userGear : " + car1.userGear);

Result

false
false
car1.shift : function(gear) {
    this.userGear = gear.toUpperCase();
}
car2.shift : shift(gear) {
            this.userGear = gear;
        }
car1.userGear : P

car1과 car1의 protototype은 같은 메소드 shift를 갖게된다. 이때 car1.shift('p')를 호출하면 car1의 메서드가 호출되고 프로토타입의 메서드는 호출되지 않는다.

 

Mixin (다중상속)

자바스크립트에서는 오직 한개의 object만 상속할 수 있다. 한개의 object에는 한개의 prototype이 존재하기 때문이다. 또한 class도 마찬가지로 오직 한개의 class만 상속할 수 있다. 하지만 이는 한계가 있다. 대신 Mixin 패턴을 사용하여 이를 해결할 수 있다. 

 

Mixin? 

Mixin은 다른 클래스의 부모2 클래스가 아니어도 다른 클래스에서 사용할 수있는 메서드가 포함 된 클래스이다.

 

다시 말해, Mixin은 특정 동작을 구현하는 메서드를 제공하지만 우리는이 메서드를 단독으로 사용하지 않고 다른 클래스에 동작을 추가하는 데 사용한다.

 

자바 스크립트에서 Mixin을 만드는 가장 간단한 방법은 유용한 메소드로 객체를 만드는 것이다. 그 객체를 어떤 클래스의  prototype으로 쉽게 병합할 수 있다. 

 

Mixin example

// mixin
let sayHiMixin = {
    sayHi() {
        console.log(`Hello ${this.name}`);
    },
    sayBye() {
        console.log(`Bye ${this.name}`);
    }
};

// usage:
class User {
    constructor(name) {
        this.name = name;
    }
}

// copy the methods
Object.assign(User.prototype, sayHiMixin);

// now User can say hi
new User("Dude").sayHi(); // Hello Dude!

여기서 mixin인 sayHiMixin은 User에 대한 "sayHi"와 "sayBye"와 같은 메소드를 추가하는 데 사용된다. 상속은 없지만 간단한 방법으로 메소드를복사 할 수 있다. 따라서 User는 다른 클래스를 상속 할 수 있으며  아래와 같은의미로 추가 메소드를 "혼합(mix)"하기 위해 mixin을 포함 할 수 있다.

class User extends Person {
  // ...
}

Object.assign(User.prototype, sayHiMixin);

Mixins는 내부에서 상속을 사용할 수 있다.

For example

let sayMixin = {
    say(phrase) {
      console.log(phrase);
    }
  };
  
  let sayHiMixin = {
    __proto__: sayMixin, // (or we could use Object.create to set the prototype here)
  
    sayHi() {
      // call parent method
      super.say(`Hello ${this.name}`);
    },
    sayBye() {
        super.say(`Bye ${this.name}`);
    }
  };
  
  class User {
    constructor(name) {
      this.name = name;
    }
  }
  
  // copy the methods
  Object.assign(User.prototype, sayHiMixin);
  
  // now User can say hi
  new User("Dude").sayHi(); // Hello Dude!

sayHiMixin은 sayMixin으로부터 상속 받는다. sayHiMixmin에서 상위 메소드인 super.say를 호출하면 클래스가 아닌 해당 mixin의 프로토타입의 메소드가 실행이 된다. 

https://javascript.info/mixins

 

프로토타입 개념 Reference

Javascript 기초 - Object Property 이해하기 : http://insanehong.kr/post/javascript-prototype/

[javascript] 프로토타입 이해하기 : https://medium.com/@bluesh55/javascript-prototype-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-f8e67c286b67

 

Reference : Learning Javascript (이선 브라운 지음)

반응형
Comments