[JS] 객체지향 프로그래밍

·2022년 10월 26일
0

JavaScript

목록 보기
23/25

객체지향? 프로그래밍 방법론중에 하나
프로그램을 작성할 때 객체들을 만들어 서로 소통하도록하는 방법

객체 : 데이터의 묶음
객체 지향의 객체 : 우리가 표현하고자 하는 구체적인 사물을 추상적으로 표현한것

객체는 행동과 상태를 가진다.
행동은 메소드, 상태는 프로퍼티 정도

// 나라는 사람 추상화 하기
const kon = {
    name : '곤',
    mbti : 'ENFJ',
	says : function() {
		console.log(`나는 ${this.name}을 위해 살아간다!`)
    },

//행동과 상태 부여하기 
    isStudying : function(javascript){
      	console.log('오늘도 가보자고')
    	javascript.levelup();
    }
}

//새로운 객체 만들기 (상호작용)
const javascript = {
    level: 1,
    levelup: function(){
        this.level++
    }
}

//능력 발휘
kon.isStudying(javascript); // 오늘도 가보자고
javascript // {level: 2, levelup: ƒ}

객체와 객체가 서로 메소드를 통해 상호작용하게 하는것이 바로 객체지향 프로그래밍!
하 지 만
우리가 만들어낸 객체는 한번 생성하고 나면 끝 . . .
kon과 같은 프로퍼티를 가지는 객체를 생성하려면 작성했던 코드를 반복해야한다.
어떻게 효율적으로 객체를 만들 수 있을까?

생성자 (constructor)

생성자? 객체를 만들 때 new 연산자와 함께 사용하는 함수

  • 생성자 장점 :
    생성자를 통해 생성된 객체는 같은 프로퍼티메서드를 공유한다!

  • 생성자 함수 특징 :
    대문자로 시작(약속)
    new 키워드를 통해 객체를 생성
    생성자는 함수이기 때문에 기본적으로 함수가 필요

        function NewFactory(name){
            this.name = name;
            this.sayYourName = function() {
                console.log('삐리비리 제 이름은 ${this.name}입니다.');
            }
        }

Factory 생성자 함수는 따로 return 값을 가지지 않지만 new 키워드가 앞에 붙게되면 실행되었을 때 자동적으로 객체를 생성하고 반환
이렇게 반환되어 만들어진 객체 <- 인스턴스(instance)
생성자 함수와 객체의 관계는 instanceof 로 확인 가능

함수안에서의 this는 함수를 호출한 객체를 참조
생성자 함수 앞에 new 연산자가 사용되었네? 함수안의 this는 생성자가 만들어낸 객체인 인스턴스를 참조!

이제 객체 즉, 인스턴스를 생성해보자~

const robot1 = new NewFactory('게리');
const robot2 = new NewFactory('뮤라');
const robot3 = new NewFactory('빙키');

필요에 따라서 배열(Array), 객체(Object)를 사용하는 것처럼
필요할 때 사용할 수 있는 우리만의 객체를 만들어서 사용할 수 있도록 도와주는 것이 바로 생성자!

  • 실습 : 음식 이름의 배열을 전달하면 배열안에서 랜덤하게 메뉴를 뽑아내는 로봇객체의 생성자를 만들어보세요.
function NewFactory(food) {
  	const this.chosenFood = food[Math.floor(Math.random() * food.length)];
  	this.saysYourFood = function() {
      	console.log(`오늘의 메뉴는 ${this.chosenFood}입니다.`);
    }
}

const food = ["김치볶음밥", "떡볶이", "버터갈릭감자튀김"]

const foodBot = new NewFactory(food);

foodBot.saysYourFood(); // 오늘의 메뉴는 떡볶이입니다. (셋 중에 하나 랜덤!)

헷갈렸던 부분!
food[Math.floor(Math.random() * food.length)

  1. Math.random()은 0~1까지 숫자 중에 랜덤으로 내준다.

  2. food.length는 메뉴 숫자만큼 3

  3. Math.random() food.length 해보면
    0.111111
    3 = 0.333333
    0.999999 * 3 = 2.99....

  4. Math.floor는 주어진 수 이하의 가장 큰 정수 반환.

  5. food[Math.floor(Math.random() * food.length)
    food 어레이에서 인덱스 0,1,2 중에 랜덤 뽑기 가능!

프로토타입 (prototype)

손쉽게 객체를 생산할 수 있긴 한데, 사실 객체의 메서드를 등록할 때마다 새로운 함수를 생성하는 것임 . . .
그럼 어떡하지?
자원 낭비 해결을 위해 프로토타입 등장!

프로토타입?
prototype은 특정 객체에 대한 참조.
즉 어떠한 공간을 가르키고 있다

생성자 함수가 인스턴스 생성
-> 그 안에는 숨겨진 프로퍼티인 [[Prototype]] 존재, 코드상에서는 __proto__로 표현
__proto__ 프로퍼티는 자신을 만든 생성자 함수의 prototype을 참조
즉, new 키워드를 통해 생성자 함수의 prototype과 인스턴스의 __proto__ 가 연결

프로토타입 - 생성자 함수 안에! (함수는 프로토타입, 프로토 가지고 있음)
프로토 - 객체 안에! (객체는 프로토만 가지고 있음)

function Test(){
};
const obj = new Test();
obj.__proto__ === Test.prototype // true

다른 예시로 연결 확인하기

function NewFactory(name){
    this.name = name;
}

const robot1 = new NewFactory('뮤라');

NewFactory.prototype.sayYourName = function(){
    console.log(`삐리비리. 제 이름은 ${this.name}입니다.`);
}

robot1.__proto__ === NewFactory.prototype; // true

  • 생성자함수에서 Prototype은 공간이다.
    생성자 함수로 만든 인스턴스들은 그 Prototype 공간 이용이 가능하다.

  • 이전에는 애초에 다른 공간상에 함수를 여러번 만든것 vs 지금은 처음부터 함수를 생성할 때 같은 공간에 등록해놓고 사용

  • 메서드를 생성할 때 생성자 함수 내에 작성하는 것 보다 prototype으로 작성해서 메모리를 아낄 수 있다!

  • 실습 1 : 우리가 만들었던 음식 로봇객체의 메서드를 프로토타입으로 분리해보세요.
    그리고 객체의 메서드가 서로 동일한 주소를 참조하는지 확인해보세요.
const food = ["김치볶음밥", "떡볶이", "버터갈릭감자튀김"]

function NewFactory(food) {
      this.chosenFood = food[Math.floor(Math.random() * food.length)];
}

NewFactory.prototype.saysYourFood = function() {
    console.log(`오늘의 메뉴는 ${this.chosenFood}입니다.`);
}

const foodBot = new NewFactory(food);

foodBot.saysYourFood();
foodBot.__ptoro__ === NewFactory.prototype
  • 실습2: 우리가 객체지향 개념에서 만들었던 ‘나’ 와 ‘대상’ 객체를 생성자를 통해서 만들어 볼 수 있도록 코드를 수정해봅시다.
    이전 코드
const kon = {
    name : '곤',
    mbti : 'ENFJ',
	says : function() {
		console.log(`나는 ${this.name}을 위해 살아간다!`)
    },

    isStudying : function(javascript){
      	console.log('오늘도 가보자고')
    	javascript.levelup();
    }
}

const javascript = {
    level: 1,
    levelup: function(){
        this.level++
    }
}

kon.isStudying(javascript); // 오늘도 가보자고
javascript // {level: 2, levelup: ƒ}

생성자를 이용해 바꾼 코드

function Me(name, mbti) {
    this.name = name,
    this.mbti = mbti
}

Me.prototype.says = function() {
      	console.log(`나는 ${this.name}을 위해 살아간다!`)
}

Me.prototype.isStudying = function (javascript) {
        console.log('오늘도 가보자고')
    	javascript.levelup();
}

const kon = new Me('곤','ENFJ');
kon // Me {name: '곤', mbti: 'ENFJ'}

function Language(level) {
	this.level = level;
}

Language.prototype.levelup = function() {
	console.log('오늘도 가보자고')
	this.level++;
}

const javascript = new Language(1);

javascript // Language {level: 1} 인스턴스 생성!
javascript.levelup() // Language {level: 2} 렙업 함수 발동!
javascript.__proto__ === Language.prototype // true

객체의 상속

자바스크립트의 상속은 기본적으로 prototype을 통해 일어난다.

const obj = {
  name: 'test'
}
console.log(obj.hasOwnProperty('name'));
const arr = [1,2,3];
console.log(arr.hasOwnProperty('name'));

Array 함수의 proto 가 Object 함수의 prototype을 참조하고 있기 때문에 Array의 prototype에 존재하지 않는 Object 객체의 프로퍼티와 메서드를 사용할 수 있다.
자신에게 존재하지 않는 프로퍼티나 메서드를 프로토타입을 통해 추적하는 과정 ㅡ 프로토타입 체이닝

class

ES6 부터 class 등장!

class의 사용법

클래스의 결과물은 인스턴스를 생성하는것입니다. 생성자를 이용한 타입 생성과 그 결과가 정확하게 일치합니다.

// function Robot(name) {
//     this.name = name;
// }

// Robot.prototype.sayYourName = function () {
//     console.log(`삐리비리. 제 이름은 ${this.name}입니다.`);
// }


class Robot {
// 클래스의 생성자 함수입니다. 하나의 클래스는 하나의 생성자만 정의할 수 있습니다. 
// 그리고 생성자 함수는 new 키워드가 호출될때 자동으로 실행됩니다.
constructor(name) {
	this.name = name;
}

// 메소드를 정의합니다. 메소드는 클래스가 생성한 인스턴스를 통해 사용할 수 있습니다.
sayYourName() {
	console.log(`삐리비리. 제 이름은 ${this.name}입니다.`);
    }
}

내부적인 동작은 동일하지만 더 보기 좋고 편리하게 개선된 문법 ㅡ 슈가신텍스 (Syntactic sugar)

  • 실습 : 우리가 위에서 만들어본 여러분 자신의 생성자 함수를 class 문법으로 변경해봅시다.

class 상속받기

클래스의 상속은 extends 키워드 사용

상속을 받는 클래스 : ‘파생 클래스’(derived classes)
부모 클래스의 프로퍼티를 상속받기 위해 super 함수를 사용, 이때 super는 부모 생성자를 참조

super 사용시 주의할 점

  • 파생 클래스에 생성자 함수를 사용하고 싶다면 반드시 super 함수 사용
  • 파생클래스에 생성자 함수가 없다면 super 함수가 자동으로 호출되어 부모 클래스의 프로퍼티를 상속 받게 함
  • 생성자 함수에서 this 값을 사용할 경우 super 함수는 반드시 this 보다 먼저 실행
  • 파생 클래스가 아닌 클래스에서 사용하려고 해도 에러가 발생
        // class 문법은 상속입니다.
        class Robot {
            constructor(name) {
                this.name = name;
            }
            sayYourName() {
                console.log(`삐리비리. 제 이름은 ${this.name}입니다.`)
            }
        }

        class BabyRobot extends Robot {
            constructor(name) {
                super(name);
            //     this.ownName = '베이비봇';
            }

            sayBabyName() {
                // 상속을 받게되면 부모 클래스의 메소드를 사용할 수 있게 됩니다. 때문에 this로 접근 할 수 있습니다.
                this.sayYourName();
                console.log('Suceeding you, Mother!');
            }
        }
        
        const babyRobot = new BabyRobot('베이비봇');
        babyRobot.sayBabyName(); 
		// 삐리비리. 제 이름은 베이비봇입니다.
		// Suceeding you, Mother!

상속을 통해 BabyRobot내에 sayYourName이 들어와있으므로 해당 인스턴스의 메서드를 사용한다

  • 실습 : 야채구이를 만드는 클래스를 정의해봅시다.
    1. 생성자 함수는 두 가지 매개변수를 전달 받을 수 있으며 전달되는 매개변수에 따라 야채구이의 맛이 결정됩니다.
    2. 야채구이 객체는 taste 라는 메서드가 존재합니다. 생성자함수에서 전달받은 재료에 따라 맛을 나타내는 콘솔로그를 출력하는 역할을 합니다. (예시 : ‘양배추’ 와 ‘버섯’ 을 매개변수로 전달하였을 경우, ‘양배추와 버섯 맛이 아주 좋아!’ 는 콘솔 메세지를 출력합니다.)
      // 3. 야채구이 클래스를 상속받는 AddTruffle 파생클래스를 생성해봅니다. 파생클래스의 taste 메서드를 실행하면 콘솔 메세지에 트러플맛이 나기 시작합니다.
        class GrilledVege {
            constructor(ingredient1, ingredient2) {
                this.ingredient1 = ingredient1,
                this.ingredient2 = ingredient2
            }

            taste() {
                console.log(`${this.ingredient1}${this.ingredient2} 맛이 아주 좋아!`)
            }
        }

        class AddTruffle extends GrilledVege {
            taste() {
                super.taste() + console.log("트러플 맛도 나네요.")
            }
        }

        const truffleVege = new AddTruffle('양배추','버섯');
        truffleVege.taste(); // 

비공개(private) 프로퍼티

비공개 프로퍼티? 객체 안의 중요한 정보를 안전하게 보호해서 프로그램이 뜻하지 않게 변경되는 것을 막는 역할을 한다

class를 통해 인스턴스를 만들었을 때 보통 아무런 제약없이 인스턴스의 프로퍼티에 접근하는 것이 가능하고, 프로퍼티의 값을 수정하는 것도 가능

class Robot {
          
    constructor(name) {
        this.name = name;
    }

    sayYourName() {
        console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
    }
}

const bot = new Robot('smith');

bot.name // smith <- 인스턴스의 프로퍼티에 접근
bot.name = 'jay';
bot.name // jay <- 프로퍼티의 값 수정

하지만 중요한 데이터를 조심스럽게 다뤄야 한다면?
데이터를 외부에서 함부로 수정할 수 없게 만들고 싶을 때!
비공개 프로퍼티로 데이터를 변경할 수 있다.

#달고 프로퍼티를 비공개로 전환
이제 #password 의 값에 접근하고 수정하려면 반드시 해당하는 기능을 하는 메서드를 사용해야함
값을 불러오는 메서드: getter 메서드
값을 수정하는 메서드: setter 메서드

class Robot {
    #password

    constructor(name, pw) {
        this.name = name;
        this.#password = pw;
    }

    sayYourName() {
        console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
    }

    getPassword() {
        return this.#password
    }

    setPassword(pw) {
        this.#password = pw;
    }
}

아래처럼 get, set 키워드를 이용해 아래처럼 코드를 단순화!

class Robot {
    #password

    constructor(name, pw) {
        this.name = name;
        this.#password = pw;
    }


    sayYourName() {
        console.log(`삐리비리. 제 이름은 ${this.name}입니다.`);
    }

    get password() {
        return this.#password
    }

    set password(pw) {
        this.#password = pw;
    }
}

const bot = new Robot('곤', 1234);

get, set은 프로퍼티를 가공해야할 때 편리하다!
마치 객체의 프로퍼티에 접근하듯 값을 다룰수 있다.

get 과 set을 사용할 때 주의할 점!
get과 set을 사용하면 마치 객체의 프로퍼티를 수정하는것 같아서 간편!
하지만 해당 코드를 직접 작성하지 않은 협업자들에게는 오해를 일으킬 수 있다.(get, set 안에 어떤 로직이 들어있는지 파악하지 못하고 단순히 객체의 프로퍼티를 수정한다는 착각을 일으킬 수 있음)
때문에 협업 시에는 주석이나, 가이드 문서를 만들어 충분한 정보를 제공할 것

profile
주니어 프론트엔드 웹 개발자 🐛

0개의 댓글