Front/js

javaScript 클래스(+ 프로퍼티 getter와 setter / 상속)

읽히는 블로그 2024. 7. 4. 17:08

▤ 목차

     
     
    함수형 프로그래밍 언어인 javascript에서 최근 클래스의 개념이 나왔다.
    java에서 클래스와는 차이가 있다. 그 차이를 위주로 정리해보려고한다.
    java가 익숙한 사람은 java와 비슷한 부분이 있기에 금방 익숙해 질 수 있다고 개인적으로 생각한다.
     
     

    ✔ 클래스

    클래스는 OOP에서 특정 객체를 생성하기 위해 멤버를 정의하는 일종의 틀(설계도)을 말한다.
    기존의 PROTOTYPE을 기반으로 객체를 생성하는 것보다 명료하게 객체 작성이 가능하다.
    클래스도 함수와 같이 호출하기 전까지는 코드가 실행되지 않는다.

     

    ⌨ 형식

    class 클래스명{
    	멤버 변수;
        
        constructor(){} // 생성자
        
        메소드명(){}; //메서드
    }

     
    JAVA와 비교했을때 다른 차이점

    • 멤버변수는 let, const와 같은 데이터 타입을 정의하지 않는다.
    • 생성자로 변수를 정의한다면 멤버 변수에 정의하지 않아도 된다.(가독성을 위해 써도 된다.)
    • (위의 연장선으로) 생성자에서 선언하면 각 인스턴스 멤버 변수로 만들어진다.
    • 오버로딩(overloading)을 할 수 없다. (오버라이딩은 가능)
    • (아래에서 설명) 상속 시 super 키워드가 최상단에 선언되지 않아도 된다.

     

    💻 코드로 보기

    class Class1{
    	addr = '서울';
    	//name;
    	
    	constructor(name){
    		this.name = name; //각 인스턴스의 멤버 변수로 만들어짐
    	}
    	sayHi(){ //멤버 메소드
    		document.write("<br>",this.name);
    		document.write("<br>",this.addr);
    		let msg = "프로그래머"; //지역 변수
    		return "<br> 이름은 " +this.name + " "+ msg;
    	}
    }

    Class1이라는 클래스를 선언했다.
    위의 클래스를 선언해보자.

    let c1 = new Class1('신기해');
    document.write("<br>",c1," ",typeof Class1," ",typeof c1);//[object Object] function object
    document.write("<br> 주소는 ", c1.addr);
    document.write(c1.sayHi());

     
    typeof 키워드를 사용하여 Class1과 호출한 c1(인스턴)의 타입을 알아봤다.
    객체 자체를 보면 function 타입이다.
    그 인스턴스의 타입은 object이다. 즉, 참조형이다.
    변수에 할당할때 값이 아닌 데이터의 주소를 저장한다.
     
    java와 같이 new로 생성하면 바 constructor가 호출된다.
     

    👏 prototype으로 바꾼다면

    function Class1(name){
    	this.name = name;
    };
    
    Class1.prototype.sayHi = function(){
    	document.write("<br>",this.name);
    	document.write("<br>",this.addr);
    	let msg = "프로그래머"; //지역변수
    	return "<br> 이름은 " +this.name + " "+ msg;
    };

    prototype으로 class함수를 바꾼다면 이와 같다.
    개인적으로 java가 익숙해서 class형식이 보기 편하다. 하지만 관련 기능이 묶여있어 응집도 높은 코드로 보인다는 점에서 좋다.
     
    *****
    해당 프로퍼티가 원형 객체의 프로퍼티가 맞는지 확인하기 위해서는 hasOwnPreperty를 사용해야한다.
    사용된 프로퍼티에는 false를 반환한다.
     
     

    클래스는 javaScript 선언과 달리 호이스팅이 되지 않는다.
    클래스를 사용하려면 미리 선언해야한다.

     

     


     
     
     

    ✔ private 멤버

    객체의 프로퍼티는 2종류가 있다.
    데이터 프로퍼티(data property)그리고 접근자 프로퍼티(accessor property)이다.
    그중 오늘은 접근자 프로퍼티의 본질은 함수이다. 이 함수는 값을 획득(get)하고 설정(set)하는 역할을 담당한다. 외부 코드에서는 일반적 프로퍼티처럼 보인다.
     
    javascript에는 getter와 setter가 있다.
     
    class를 사용하기 전까지는 아래와 같이 코드로 값을 직접 넣고 가져왔다.

    test1.name = '홍길동'; //setter역할 
    document.write("<br>",test1.name); //getter역할

     
    지금까지 선언한 변수는 모두 public이였다. 
    때문에 getter 혹은 setter는 의미가 없었다. 
     
    javascript의 private를 정리해보고자한다.
     

    💻 코드로 보기

    class Class3{
    	#name;  // private 멤버 변수. 모던자바스크립만 지원한다.
    	age;  // public 멤버변수
    	static addr = '서울';//static로 선언
    	
    	constructor(name, age){
    		this.#name = name;
    		this.age = age;
    	}
    }

    이렇게 #를 사용해서 private 변수로 만들 수 있다.
    즉 외부에서 직접 접근하지 못하고 getter를 사용해서 접근할 수 있다.
     

    메서드의 기본 return값은 undefined이다.
    즉, return를 생략해도 undefined를 반환한다.

     
     

    ⌨ getter와 setter

    • getter/setter의 사용이유 : private 멤버를 처리하기 위함이다.
    • 이름은 동적이다. (이름을 get으로 시작하지 않아도 된다.)
    class Class3{
    	#name;  // private 멤버 변수. 모던자바스크립만 지원한다.
    	age;  // public 멤버변수
    	static addr = '서울';//static로 선언
    	
    	constructor(name, age){
    		this.#name = name;
    		this.age = age;
    	}
    	get getName(){ //보통은 get name(){} 선언
    		return this.#name;
    	}
        
    	set setName(){
    		this.name = name;
    	}
    }

     
    getter의 이름을 보통 변수명 그대로 사용한다.
    하지만 가독성을 위해서 get을 앞쪽에 붙였다. 정해진 이름의 규칙은 없다.
     
    값을 대입해보자.

    const person = new Class3('이기자',23);
    document.write("<br",person); //[object Object]
    document.write("<br>",person.age); //23

     
    변수  person을 선언하고 Class3 생성자를 호출했다.
    인스턴스했다고도 표현하는데 해당 객체를 만들어 person객체에 대입했다. 
     
    " 아래 상속 파트에서 한번 더 강조하겠지만 javascript에는 변수 타입이 따로 정해져있지 않다.
    즉, 상속한 값에 대한 타입변환의 과정이 필요없다. 이 부분은 아래에서 자세히 정리해보겠다. " 
     
    일단 위의 값처럼 person과 person.age의 값을 출력하면 해당 타입과 전달한 값인 23이 출력된다.
     
    private로 선언된 변수를 출력해보자.

    document.write('<br>',person.name);

    해당 값은 나오지 않는다. 즉, 변수에 직접 접근이 불가능하다.
    혹시 위의 값이 잘못 작성되어서 그렇다고 생각하는가?
    다시 아래와 같이 선언해보겠다.
     

    document.write('<br>',person.#name);

    돌리기전에 이미 컴파일 오류가 발생한다. 오류 메세지는 다음과 같이 나온다.

    Property '#name' is not accessible outside class 'Class3' because it has a private identifier.
    '#name' 속성에는 개인 식별자가 있으므로 'Class3' 클래스 외부에서 액세스할 수 없습니다.

    그렇다면 어떻게 접근해야하는가?
    이럴때 getter가 사용된다. 개발자는 변경되면 안되는 값을 숨기고 가공을 한 변수를 사용자들이 접근할 수 있도록 만들어야하는 것이다.

    document.write("<br>",person.getName); //이기자

    이렇게 사용해야한다.
     
    그럼 주소에는 어떻게 접근할까? static으로 선언했다.
    즉, 힙메모리에 저장되어 있는 값이 아닌 static 영역에 저장되어 있다. 다시 말해, 클래스에 직접 접근할 수 있다는 뜻이다.

    document.write("<br>",person.addr); //undefined
    document.write("<br>",Class3.addr); //서울

    위와 같이 person 객체 명으로 접근하면 undefined 값이 나온다. 
    클래스명.변수명 형식으로 직접 접근하자.
     
    setter 사용방법

    person.setName = '홍길동';
    document.write('<br><br>',person.getName);

    값은 주는 방식은 = 을 사용해서 전달한다. 
    java의 문법은 setName('홍길동');이지만 js는 setName='홍길동'으로 부여한다.
     
     

    👏 

    밑줄 문자를 사용하는 것은 JavaScript에서 getter/setter를 사용할 때 일반적인 관행이지만 필수는 아니다.

    원하는 대로 이름을 지정할 수 있지만 속성 이름과 동일하지는 않다.
     


     
     
     

    ✔상속(inheritance)

    java와 동일하게 extends를 키워드를 사용한다.
    클래스 상속으로 생성된 클래스는 다른 클래스의 모든 메서드를 상속한다.
    상속을 하면 부모 클래스에서 정의된 멤버 필드나 메서드를 사용할 수 있다.
    상속받는 클래스를 자식 클래스라고한다.
    부모의 속성이나 기능을 상속받을때, 자식 클래스에서 상속받은 클래스를 변경하고 싶다면 변경이 가능하다.
    즉, 오버라이딩이 가능하다.
     

    ⌨ 다중상속은 불가능하다

    우선 상속의 코드를 보기 전 알아두면 좋은 것이 있다. 다중 상속이다.
    다중 상속(Multiple inheritance)이란 객체 지향 프로그래밍의 특징 중 하나이며 어떤 클래스가 하나 이상의 상위 클래스로부터 여러가지 행동이나 특징을 상속받을 수 있는 것을 말한다.
     
    파이썬, C++ 등 언어는 다중 상속을 허용한다.  하지만 java를 포함해 javaScript 또한 다중 상속이 불가능하다.
    즉, 부모 클래스를 2개를 선택할 수 없다.
     

    	class Animal{
                move="움직이자!";
                constructor(name, speed){
                    document.write(`<br> Animal 생성자`);
                    this.name = name;
                    this.speed = 0;
                }
    
                run(speed){
                    this.speed = speed;
                    document.write(`<br>${this.name} : ${this.speed}`);
                    document.write(`<br>${this.move}`);
                }
            }
            class Cat extends Animal{
                constructor(){};
            }
            
            class Dog extends Animal,Cat{
                constructor(){};
            }

    코드 가장 아랫쪽에 정의된 클래스는 오류가 발생한다. 

    Classes can only extend a single class.

    코드 오류 메세지이다. 
    SyntaxError가 발생해 다중 상속이 되지 않는다.
    프로토타입으로 체인 연결을 해주면 다중 상속한 것처럼 보일 순 있지만 상속은 불가능하다.
     
    이제 상속에 대해 알아보자.
     

    💻 코드 보기

    • js 상속에서 가장 중요한것은 생성자, 메서드 오버로딩이 안된다는 점이다.
    더보기

    오버로딩이 안된다?

    > 자식 클래스에서 부모 클래스의 생성자와 다른 파라미터, 메서드와 다른 파라미터의 값으로 불러올 수 없다.

    > sunper(); this(); 를 부를 수 없다.

    구조적으로 불가능하다.

     

    	class Animal{
                move="움직이자!";
                constructor(name){
                    document.write(`<br> Animal 생성자`);
                    this.name = name;
                    this.speed = 0;
                }
    
                run(speed){
                    this.speed = speed;
                    document.write(`<br>${this.name} : ${this.speed}`);
                    document.write(`<br>${this.move}`);
                }
                
                stop(){
                    this.speed = 0;
                    document.write(`<br> 멈추기 : ${this.speed}`)
                }
    
                
            }
    
            class Cat extends Animal{
                leg = 4;
                constructor(name, leg){
                    document.write(`<br> 고양이 생성자`);
                    this.leg = leg;
                    super(name); //부모의 변수에 초기화된다.
                };
    
                stop(speed){ //method overriding
                    this.speed = 0;
                    document.write(`<br> 멈추기 : ${this.speed}`)
                };
    
                slogan(){ //슬로건
                    document.write(`<br> 고양이 만세`);
                };
            }

     
    위와 같이 정의된 코드가 잇다고 해보자. 동물이란 클래스의 기본 생성자는 name, speed로 파라미터로 들어온 값을 초기화하고 있으며 run 메서드와 stop 메서드를 정의하고 있다.
    cat  클래스는 동물 클래스를 상속 받고 있으며 부모의 멤버 변수 name과 stop()메서드를 사용한다.
    그리고 자체적으로 slogan()메서드를 가지고 있다.
     
    잘 작동하는지 확인해보자.

     	window.onload = function(){
                const parents = new Animal('동물');
                parents.run(10);
                parents.move;
                parents.stop;
            }

     

     
    javaScript는 super가 상단에 위치하지 않아도
    오류가 발생하지 않는다. 

     


     
     

    ✔ super ()와 다형성

     

    ⌨ 코드

    지금까지의 코드이다. 다른 클래스를 한개 더 만들어보려고한다.

     

    	class Animal{
                move="움직이자!";
                constructor(name){
                    document.write(`<br> Animal 생성자`);
                    this.name = name;
                    this.speed = 0;
                }
    
                run(speed){
                    this.speed = speed;
                    document.write(`<br>${this.name} : ${this.speed}`);
                    document.write(`<br>${this.move}`);
                }
                
                stop(){
                    this.speed = 0;
                    document.write(`<br> 멈추기 : ${this.speed}`)
                }
    
                
            }
    
            class Cat extends Animal{
                leg = 4;
                constructor(name, leg){
                    document.write(`<br> 고양이 생성자`);
                    this.leg = leg;
                    super(name); //부모의 변수에 초기화된다.
                };
    
                stop(speed){ //method overriding
                    this.speed = 0;
                    document.write(`<br> 멈추기 : ${this.speed}`)
                };
    
                slogan(){ //슬로건
                    document.write(`<br> 고양이 만세`);
                };
            }

     

     

    class Dog extends Animal{
    //	constructor(name){
    //		super(name);
    //	}
    	disp(){//method overriding
    		document.write(`<br> 댕댕이 화이팅 : 열심히 ${this.move}`);
    	}
    }

    만약 클래스 자체적인 생성자가 없다면 자동으로 부모의 생성자를 호출한다.

    이때 생성자에도 해당 인수를 모두 전달한다.

     

    만약 자식클래스 생성자 정의를 아래와같이 한다면 어떤 결과가 나올까?

    constructor(name){
    		//super(name);
    		this.name = name;
    		this.speed =0;
    		this.leg = leg;
    	}
    const dog = new Dog('댕댕이');

     

    Must call super constructor in derived class before accessing 'this' or returning from derived constructor

     

    상속클래스의 생성자에서는 super()를 반드시 호출해야한다.

    super()는 this를 사용하기 전에 반드시 호출해야한다.

     

    일반 클래스 new와 함께 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당
    상속 클래스 생성자 함수는 빈 객체를 만들고
    this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리하길 기대하고 있다.

     

     

    class Dog extends Animal{
    	constructor(name){
    		super(name); //this보다 먼저 사용되어야한다.
    		this.leg = leg;
    	}
    	disp(){//method overriding
    		document.write(`<br> 댕댕이 화이팅 : 열심히 ${this.move}`);
    	}
    }
    
    const dog = new Dog('댕댕이');
    	dog.disp();
    	dog.run(5); //부모의 메서드를 찾아가 실행한다.
    	dog.stop(); //부모의 메서드

     

     

     


    부모 생성자는 자식 클래스에서 오버라이딩한 값이 아닌, 부모 클래스 안의 필드 값을 사용한다

     

    //코드1
    class Animal {
      name = 'animal'
    
      constructor() {
        console.log(this.name);
      }
    }
    
    class Rabbit extends Animal {
      name = 'rabbit';
    }
    
    new Animal(); // animal
    new Rabbit(); // animal

     

    코드를 작성하면서 원하던 출력값을 생각해보자.

     

     

    Rabbit()을 인스턴스하면 해당 클래스로 들어가서 name 초기화가 되어 rabbit이 되어야한다고 생각했을 것이다.

    하지만 둘다 animal이라는 결과를 출력했다. 즉, 부모의 필드 값을 사용했다.

    우리가 원하는 결과를 위해서는 아래와 같이 작성되어야한다.

     

    //코드2
    <script>
    "use strict";
    
    class Animal {
      showName() { 
        alert('animal');
      }
    
      constructor() {
        this.showName();
      }
    }
    
    class Rabbit extends Animal {
      showName() {
        alert('rabbit');
      }
    }
    
    new Animal(); // animal
    new Rabbit(); // rabbit
    </script>

     

     

     

    우리가 원하는 결과이다. 

     

    두 코드의 결과에 차이가 생기는 이유는 필드 초기화 순서때문이다.

    • 상속받지 않은 기본 클래스는 생성자 실행 이전에 초기화된다.
    • 상속을 받은 클래스는 super()실행 직후에 초기화가 된다. 

    코드2의 Rabbit 클래스를 보자.

    constructor()가 정의되어 있지 않는다.

    이런 경우 생성자가 생략되어 있는 것이다. 

    class Rabbit extends Animal {
    	constructor(){ //생성자는 생략이 가능하다.
    		super();
    	}; 
    	showName() {
    		alert('rabbit');
    	}
    }

    이런 방식으로 코드가 진행이 되는 것이다.

     

     new Rabbit()을 실행하면

    1. 생략되어 있던 constructor()가 실행되면서 super()가 호출된다.
    2. 그 결과 부모 생성자가 실행된다. 
    3. 필드 초기화 순서에따라 Rabbit의 필드는 super() 실행 후 에 초기화된다. (부모 생성자가 실행되는 시점에서는 Rabbit의 필드는 존재하지 않는다.)

    필드 오버라이딩이 문제가 되는 상황이 발생하면 필드 대신 메서드를 사용하거나 getter나 setter를 사용해 해결하면 된다.

     

     

     

    💻 다형성(Polymorphism)

    다형성은 객체 지향의 주요 개념중 하나로 , 같은 이름의 메서드나 연산자가 다른 클래스에 대해 다른 동작을 하도록 하는 것을 말한다.

    • 메서드 오버라이딩
    • 메서드 오버로딩

    👏 코드로 보기

    //고객(부모클래스) 
    	class User{
                constructor(userId, birthdate){
                    this.userId = userId;
                    this.birthdate = birthdate;
                }
    
                buy(item){
                    let msg = `${this.userId}고객님 구매 : ${item.name}`;
                    console.log(msg);
                    //document.querySelector("#show").innerText = msg;
                }
            }

     

    	class NewUser extends User{ //신규 고객
                constructor(userId, birthdate, level){
                    super(userId, birthdate);
                    this.level = level;
                };
                buy(item){
                    let msg = `신규 이벤트 ${this.userId}님 ${item.name} 5% 할인 쿠폰까지`;
                    console.log(msg);
                };
            }
            const item ={
                name : "청바지",
                price : 32000,
            }

     

    신규 고객은 User의 클래스를 상속받았다.

    신규 고객이기에 할인 쿠폰을 증정하는 메시지로 바꿔보았다.

     

    	const user1 = new User('hihi','2000-12-01'); //인스턴스
            const user2 = new User('haha','1996-10-21');
    
            const NewUser1 = new NewUser('kim','1999-12-02');
            const NewUser2 = new NewUser('pack','1995-09-15');
    
            const members = [user1, user2, NewUser1, NewUser2];
            members.forEach((user)=>{
                user.buy(item); //****
            });

     

    js는 대입하는 값에 따라 변수의 타입이 결정된다.

    때문에 다형성을 구현할때 묵시적/강제 형변환이 일어나지 않는다.

     

    ****) 이 부분이 다형성이다. user.buy메서드를 동일하게 호출하지만 각 객체에 따라서 호출되는 메서드가 달라진다.

     


     
     

    😊

    클래스와 생성자 함수의 차이에 대한 블로그 글을 찾아봤다.
    한번 참고하기 좋은 것같다.
    https://velog.io/@iberis/JavaScript-%ED%81%B4%EB%9E%98%EC%8A%A4class%EC%99%80-%EA%B0%9D%EC%B2%B4object%EC%9D%98-%EC%B0%A8%EC%9D%B4

     

    [JavaScript] 클래스(class)와 생성자 함수 의 차이

    ES6 이후 클래스(class) 문법이 생기면서, 그 이전까지 생성자 함수로 만든 객체와 3가지 차이점이 생겼다.for... in... 문으로 조회했을 때 매서드의 노출 여부new 없이 객체를 생성했을 때 오류 여부cl

    velog.io