Study Record

[JavaScript] 객체지향 기초(ES2015 이전) 본문

웹/Javascript

[JavaScript] 객체지향 기초(ES2015 이전)

초코초코초코 2025. 3. 17. 22:28
728x90

 

 

ES2015 이전의 객체지향

자바스크립트는 인스턴스화 및 인스턴스라는 개념이 존재하나 이른바 클래스가 없고, 프로토타입이 존재했다. 프로토타입이란 어떤 객체의 원본이 되는 객체로 자바스크립트에서는 이것을 이용하여 새로운 객체를 생성한다. 따라서 JavaScript 의 객체지향은 프로토타입 베이스의 객체지향이라고 불린다.

 

 

가장 간단한 클래스 정의하기

자바스크립트의 세계에서는 엄미한 의미의 클래스가 존재하지 않기 때문에 함수(Function 객체)에 클래스의 역할을 부여한다. 간단한 클래스의 정의는 다음과 같다. 일반 함수와 구분하기 위해 첫글자를 대문자로 기술하는 것이 일반적이다.

var Member = function() {};

 

new 연산자로 인스턴스화가 가능하다.

var Member = function() {};
var mem = new Member();

 

 

 생성자로 초기화하기

생성자는 인스턴스를 생성할 때 객체의 초기화 처리를 기술하기 위한 특수한 메소드를 말한다. 

 

① 프로퍼티와 메소드

this 키워드는 생성자에 의해 생성되는 인스턴스(자기자신)이다. this 키워드에 변수를 저장함으로써 인스턴스의 프로퍼티를 설정할 수 있다. 자바스크립트에서는 면밀히 하자면 메소드라는 개념이 없어 함수 객체로써의 값이 프로퍼티의 메소드로 간주된다.

var Member = function(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.getName = function() {
        return `${this.firstName} ${this.lastName}`;
    }
}

var mem = new Member('길동', '홍');

 

 

 동적으로 메소드 추가하기

동적으로 인스턴스에 메소드나 혹은 프로퍼티(변수)도 추가할 수 있다. 인스턴스만의 메소드 혹은 프로퍼티가되기 때문에 다른 인스턴스에는 영향을 끼치지 않는다.

var Member = function(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

var mem = new Member('길동', '홍');

mem.getName = function() {
    return `${this.firstName} ${this.lastName}`;
}

var mem2 = new Member('사랑', '유');

console.log(mem.getName());    // 홍길동
console.log(mem2.getName());   // error

 

 

 프로토타입(prototype) 프로퍼티

일반적으로 생성자를 통해 객체를 생성하면 프로퍼티와 메소드 모두 메모리 상에 올라간다. 따라서 여러 인스턴스들을 생성할수록 메모리가 빠르게 소비된다. 메소드나 프로퍼티가 변경되지 않을 경우 모든 인스턴스가 동일한 값을 참조하도록 할 수 있는 방법이 바로 prototype 프로퍼티이다. 프로토타입 프로퍼티는 디폴트로 빈 객체를 참조하고 있는데 이것에 프로퍼티나 메소드를 추가할 수 있다. 프로포타입 프로퍼티에 대해 추가된 멤버는 그 생성자를 기초로 생성된 모든 인스턴스에서 이용할 수 있다. 

 

각각의 인스턴스에는 prototype 프로퍼티를 참조하는 참조값이 저장된다. 프로토타입 프로퍼티를 추가하는 방법은 다음과 같다.

var Member = function(name) {
    this.name = name;
};

var mem = new Member("사랑");

Member.prototype.printName = function() {
    console.log(this.name);
}

var mem2 = new Member("미움");

mem.printName();     // 사랑
mem2.printName();    // 미움

 

프로포타입 프로퍼티라는 객체의 참조값을 각각의 인스턴스마다 갖게 되므로 프로퍼티에 멤버를 추가하기 전에 인스턴스를 생성해도 무방하다.

 

+ 프로토타입 프로퍼티 객체의 변수와 같은 이름으로 인스턴스의 프로퍼티를 지정할 경우

인스턴스가 참조하고 있는 프로토타입 프로퍼티 값도 저장되어 있고 새로운 프로퍼티의 값도 저장된다. 그렇지만 참조할 때는 인스턴스에 있는 프로퍼티값을 참조하므로 프로토타입의 프로퍼티의 값까지 참조하지 않는다. 

var Member = function(name) {
    this.name = name;
};

Member.prototype.sex = "남자";

var mem = new Member("사랑");
var mem2 = new Member("미움");

mem.sex = "여자";

console.log(mem.sex);    // 여자
console.log(mem2.sex);   // 남자

 

 

+ delete 연산자의 프로토타입 객체의 프로퍼티 삭제

delete 연산자가 인스턴스의 프로포타입 객체까지 영향을 끼치지 않는다. 

var Member = function(name) {
    this.name = name;
};

Member.prototype.sex = "남자";

var mem = new Member("사랑");
var mem2 = new Member("미움");

mem.sex = "여자";

console.log(mem.sex);    // 여자
console.log(mem2.sex);   // 남자

delete mem.sex;
delete mem2.sex;

console.log(mem.sex);    // 남자
console.log(mem2.sex);   // 남자

 

+ 프로퍼티 선언은 생성자로 하지만 메소드의 선언은 프로토타입으로 하는 것이 좋다.

 

 

 객체 리터럴로 프로토타입 정의하기

닷 연산자(.)를 통해서 프로토타입에 멤버를 추가했지만 객체 리터럴 표현으로 정의할 수 있다.

var Member = function(name) {
    this.name = name;
};

Member.prototype = {
    getName : function() {
        return this.name;
    },
    printName : function() {
        console.log(this.name);
    }   
}

var mem = new Member('사랑'); 

mem.printName();    // 사랑

 

 

 정적 메소드와 정적 프로퍼티 정의하기

정적 메소드와 프로퍼티는 기본적으로 읽기 전용의 용도로 사용한다. 또한 정적 메소드 안에서 this 키워드를 사용할 수 없다. 그 이유는 인스턴스가 없기 때문에 정적 메소드로부터 인스턴스 프로퍼티 값에 엑세스할 수 없다.

var Love = function() {};

// 정적 프로퍼티 정의
Love.perfectPercent = 100;

// 정적 메소드 정의
Love.getNowLove = function(love) {
    return love * 100;
}

Love.printLove = function(love) {
    console.log(`현재 사랑 게이지 : ${love * 100}`);
}

console.log(Love.perfectPercent);
console.log(Love.getNowLove(0.4));
Love.printLove(0.5);

 

 

객체의 계승(상속)

계승이란 베이스가 되는 객체(클래스 or 프로토타입)의 기능을 계승하여 새로운 클래스(프로토타입)를 정의하는 기능을 말한다. 계승원이 되는 클래스를 슈퍼(상위) 클래스, 계승된 클래스를 서브(하위) 클래스라고 부른다. 

 

자바스크립트에서 계승의 구조를 실현하고 있는 것을 프로토타입 체인이라고 한다. 프로토타입 체인의 종단은 Object.prototype 이다. 자바스크립트에서는 프로토타입에 상위 클래스의 인스턴스를 설정함으로써 인스턴스끼리 암묵의 참조하에 서로 연결되어 계승관계를 갖게 할 수 있다. 이러한 프로토타입 연결을 프로토타입 체인의 실체이다. 

var Animal = function() {};

Animal.prototype = {
    walk : function() {
        console.log('없음.');
    }
}

// Animal 클래스(프로토타입) 계승
var Dog = function() {
    Animal.call();
};

Dog.prototype = new Animal();

// 새로운 프로토타입 프로퍼티 메소드 추가
Dog.prototype.bark = function() {
    console.log('멍멍');
}

// 사용해보기
var d = new Dog();
d.walk();    // 없음.
d.bark();    // 멍멍

이 예시에서는 Dog 객체의 프로토타입이 Animal 객체의 인스턴스로 세트되고 있다. 이로 인해 Dog 객체의 인스턴스로부터 Animal 객체로 정의된 walk 메소드를 호출할 수 있다.

 

 

 계승 관계의 동적 변경

자바스크립트의 프토로타입 체인은 인스턴스가 생성된 시점에서 고정되어 그 후의 변경에는 관여치 않고 보존된다.

// Animal 객체
var Animal = function() {};
Animal.prototype = {
    walk : function() {
        console.log('없음.');
    }
}

// OtherAnimal 객체
var OtherAnimal = function() {};
OtherAnimal.prototype = {
    walk : function() {
        console.log('자주.');
    }
}

// Animal 클래스(프로토타입)를 계승한 Dog 객체 선언
var Dog = function() {
    Animal.call();
};

Dog.prototype = new Animal();

// Dog 인스턴스 생성
var d1 = new Dog();
d1.walk();    // 없음.

// 상위 클래스 변경
Dog.prototype = new OtherAnimal();

var d2 = new Dog();
d2.walk();    // 자주.
d1.walk();    // 없음.

 

 

 객체의 타입 판정하기

 

① instanceof 연산자 - 바탕이 되는 생성자 판정하기

// Animal 객체
var Animal = function() {};
Animal.prototype = {
    walk : function() {
        console.log('없음.');
    }
}

// Animal 클래스(프로토타입)를 계승한 Dog 객체 선언
var Dog = function() {
    Animal.call();
};
Dog.prototype = new Animal();

var d1 = new Dog();
var animal = new Animal();

console.log(animal instanceof Animal);   // true
console.log(d1 instanceof Animal);       // true
console.log(d1 instanceof Dog);          // true

 

 

② isPrototypeOf 메소드 - 참조하고 있는 프로토타입 확인하기

// Animal 객체
var Animal = function() {};
Animal.prototype = {
    walk : function() {
        console.log('없음.');
    }
}

// Animal 클래스(프로토타입)를 계승한 Dog 객체 선언
var Dog = function() {
    Animal.call();
};
Dog.prototype = new Animal();

var d1 = new Dog();
var animal = new Animal();

console.log(Animal.prototype.isPrototypeOf(d1));   // true
console.log(Dog.prototype.isPrototypeOf(d1));       // true
console.log(Animal.prototype.isPrototypeOf(animal));          // true

 

 

③ in 연산자 - 멤버의 유무 판정하기 

var obj = {
    firstName: "haha",
    getName: function() {
        return "Name";
    }    
};

console.log('getName' in obj);      // true
console.log('firstName' in obj);    // true
console.log('lastName' in obj);     // false

 

 

private 멤버 정의하기

private 멤버란 클래스 내부의 메소드에서만 호출할 수 있는 프로퍼티나 메소드를 말한다. 클래스 내부에서만 사용하는 정보나 처리를 정의하고 싶은 경우 사용된다. 외부로부터의 엑세스를 허용하지 않으므로 보안에 유의하다.

 

반대로 클래스 내부, 외부로부터 자유롭게 액세스할 수 있는 멤버를 public 멤버라고 한다. 아무런 고려 없이 멤버를 정의하면 자연스럽게 public 멤버가 된다.

 

자바스크립트에서는 private 멤버를 정의하기 위한 구문을 없으나 클로저를 이용함으로써 유사적으로 private한 멤버를 만들 수 있다.

var Animal = function() {
    var _name;
    var _age;
    var _checkAge = function(age){
        return (typeof age === 'number' && age > -1);
    }
    
    // privileged 메소드 : private 멤버에 접근할 수 있는 메소드
    this.setName = function(name) {
        _name = name;
    }
    this.setAge = function(age) {
        if(_checkAge(age)) _age = age;
    }
    this.getName = () => _name;
    this.getAge = () => _age;
}

var animal = new Animal();

// private 멤버에 직접적인 접근 시도
animal._name = "양이";
animal._age = 13;

console.log(animal.getName());    // undefined
console.log(animal.getAge());     // undefined
console.log(animal._name);     // 양이
console.log(animal._age);      // 13

// private 멤버 올바른 접근 시도
animal.setAge(10);
animal.setName("사랑");

console.log(animal.getName());    // 사랑
console.log(animal.getAge());     // 10
console.log(animal._name);     // 양이
console.log(animal._age);      // 13

고유의 private 멤버를 외부에서 직접적으로 수정하려고 해도 수정할 수 없다. 오직 정해진 루트를 통해서만 수정할 수 있다. 이렇게 프로퍼티 그 자체는 클래스 외부로부터 엑세스할 수 없게 해두고 대신에 프로퍼티에 액세스하기 위한 메소드를 액세서 메소드라고도 부른다. 세분화해서 참조용 메소드를 게터 메소드, 설정용 메소드를 세터 메소드라고 한다.

 

 

Object.defineProperty 메소드에 의한 액세서 메소드 구현

Object.definedProperty 메소드를 이용하면 좀 더 효율적으로 액세서 메소드를 구현할 수 있다.

Object.defineProperty(obj, prop, desc);
// obj : 프로퍼티를 정의하는 객체
// prop : 프로퍼티명
// desc : 프로퍼티의 구성 정보

 

예시)

var Animal = function() {
    var _name;
    var _age;

    // name 프로퍼티 정의
    Object.defineProperty( this, 'name',
        { 
            get: () => _name,
            set: function(name) {
                _name = name;
            }
        }
    )

    // age 프로퍼티 정의
    Object.defineProperty(this, 'age',
        {
            get: () => _age,
            set: function(age) {
                _age = age;
            }
        }
    )
};

var animal = new Animal();

animal.name = "사랑";
animal.age = 11;

console.log(animal.name);    // 사랑 
console.log(animal.age);     // 11

 

+ Object.defineProperties 메소드 이용하기

var Animal = function() {
    var _name;
    var _age;

    Object.defineProperties(this, {
        name: { 
            get: () => _name,
            set: function(name) {
                _name = name;
            }
        },
        age: {
            get: () => _age,
            set: function(age) {
                _age = age;
            }
        }
    })
};

var animal = new Animal();

animal.name = "사랑";
animal.age = 11;

console.log(animal.name);    // 사랑 
console.log(animal.age);     // 11

 

 

네임스페이스(패키지) 작성하기

클래스의 수가 많아지면 클래스명이 라이브러리끼리 출동하거나 라이브러리와 라이브러리를 이용하고 있는 애플리케이션 사이에서 충돌하는 케이스가 발생할 수 있다. 네임스페이스란 클래스를 정리하기 위한 상자와 같다. 자바스크립트에서는 네임스페이스에 대한 다른 기능이 없기 때문에 빈 객체를 이용해 유사적으로 네임 스페이스와 같은 것을 작성한다.

 

// Wings 가 이미 있으면 패스 없을 경우 빈 객체 생성
var Wings = Wings || {};

Wings.Member = function() {};

Wings.Member.prototype = {
    myPrint: function(name) {
        console.log(name);
    }
}

var mem = new Wings.Member();

mem.myPrint("사랑");     // 사랑
728x90