자바스크립트 객체 지향 프로그래밍

Kim Jin Hyeok·2021년 3월 1일
0

클래스, 생성자, 메서드

C++이나 Java 같은 언어는 class 라는 키워드를 제공하여 개발자는 클래스를 만들 수 있다. 클래스와 같은 이름의 메서드로 생성자를 구현한다. 하지만 자바스크립트는 이러한 개념이 없다(ECMAScript 5 기준). 자바스크립트는 거의 모든 것이 객체이고 특히 함수 객체로 많은 것을 구현한다. 클래스, 생성자, 메서드도 모두 함수로 구현할 수 있다.

function Person(name) {
    this.name = name;
    
    this.getName = function() {
        return this.name;    
    }
 
    this.setName = function(name) {
        this.name = name;
    }
}

var kim = new Person('kim');
console.log(kim.getName()); // kim

var lee = new Person('lee');
console.log(lee.getName()); // lee

위 예제에서 Person 함수가 클래스이자 생성자 역할을 하며, 내부에 구현된 get set 함수들이 메서드 역할을 한다.
그리고 new 키워드로 인스턴스를 생성하여 사용할 수 있다.

다만 위 예제는 겉으로는 문제가 없지만 자원이 낭비된다는 문제가 있다.
만들어진 인스턴스들이 공통적으로도 사용할 수 있는 함수들(get,set)을 각각 따로 생성한다.

이러한 문제를 해결하기 위해 프로토타입을 이용할 수 있다.

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

Person.prototype.getName = function() {
	return this.name;
}

Person.prototype.setName = function(name) {
	this.name = name;
}

위와 같이 작성함으로서 인스턴스(객체)들은 각자 함수 객체를 생성할 필요가 없다.

더글라스 크락포드는 아래와 같이 함수를 제시하면서 메서드를 정의할 수 있는 방법을 소개한다.

Function.prototype.method = function(name, func) {
    if(!this.prototype[name]) {
    	this.prototype[name] = func;	
    }
}

위 방법을 예제에 적용한다면 아래와 같이 바꿀 수 있다.

Function.prototype.method = function(name, func) {
    if(!this.prototype[name]) {
    	this.prototype[name] = func;	
    }
}

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

Person.method('setName', function(name) {
    this.name = name;
});

Person.method('getName', function() {
	return this.name;
});

상속

자바스크립트에선 클래스 기반의 전통적인 상속은 지원하지 않지만, 객체 프로토타입 체인을 이용하여 상속을 구현할 수 있다. 이런 상속은 크게 두 가지로 구분할 수 있다. 하나는 클래스 기반의 전통적인 상속을 흉내내는 것과 다른 하나는 클래스 개념 없이 객체의 프로토타입으로 상속을 구현하는 방식이다.

프로토타입을 이용한 상속 prototype based inheritance

다음은 더글라스 크락포드가 소개하는 객체를 상속하는 방법이다.

function create_object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

사실 위 코드는 ES5에서 Object.create() 함수로 제공된다. 이해를 위해 사용했다.

create_object() 함수는 인자로 들어온 객체를 부모로 하는 자식 객체를 생성하여 반환한다. 이렇게 반환된 객체는 부모 객체의 프로퍼티에 접근 할 수도 있고 자신만의 프로퍼티를 만들 수도 있다.

새로운 객체에 프로퍼티나 메서드를 재정의하거나 추가할 때 직접 접근하는 방법도 있으나 관례상 extend 라는 이름의 함수로 사용한다.

var person = { // 부모 객체 정의
    name: 'kim',
    getName: function() {
        return this.name;
    },
    setName: function(name) {
    	this.name = name;
    }
}

function create_object(o) { // 상속 함수
    function F() {}
    F.prototype = o;
    return new F();
}

function extend(obj,prop) { // 확장 함수
    if(!prop) { prop = obj; obj = this; }
    for (var i in prop) obj[i] = prop[i];
    return obj;
}

var student = create_object(person); // 자식 객체를 만들고 상속

var added = { // 추가(확장)할 메서드를 담은 객체
    setAge = function(age) {
        this.age = age;
    },
    getAge = function() {
    	return this.age;
    }
};

extend(student, added); // 추가

student.setAge(20); // 추가된 메서드 사용
console.log(student.getAge()); // 20

클래스 기반의 상속 class based inheritance

프로토타입을 이용한 상속과 거의 같다. 다만 클래스 역할을 하는 함수가 있을 뿐이다.

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

Person.prototype.setName = function(name) { 
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

function Student(name) {
    Person.apply(this, arguments);
    // 해석하면 Person 함수를 실행하는데 내부적인 this가 첫 번째 인자 this(new Student의 새로운 인스턴스)를 가리키도록 한다.
    // 부모 클래스의 생성자를 호출할 때 필요한 방식
}

var parent = new Person('parent');
Student.prototype = parent;

var child = new Student();
child.setName('child');
console.log(child.getName());

위 코드는 상속은 이루어졌으나 잘못된 코드인데 자식의 prototype 이 부모의 인스턴스를 가리키기 때문에 자식의 prototype 에 프로퍼티를 추가할 때 문제가 생길 수 있다. 이는 부모의 인스턴스와 자식의 인스턴스가 독립적이어야 함을 의미한다.

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

Function.prototype.method = function(name, func) { // 메서드 추가 메서드
    this.prototype[name] = func;
};

Person.method('getName', function() {
    return this.name;
});

Person.method('setName', function(name) {
    this.name = name;
});

function Student(name) {
    Person.apply(this, arguments);
}

function F() {}; // 중개자 함수
F.prototype = Person.prototype;
Student.prototype = new F();
Student.prototype.constructor = Student;
Student.super = Person.prototype;

var child = new Student();]
child.setName('child');
console.log(child.getName());

위 코드는 자식과 부모 사이에 F 라는 빈 함수를 둠으로서 자식은 부모에게 상속 받은 프로퍼티를 쓸 수도 있고, 빈 함수에 메서드를 추가해도 부모에겐 영향이 없게된다.

스토얀 스테파노프는 상속 관계를 즉시 실행 함수와 클로저를 활용하여 최적화된 함수를 소개하는데 아래와 같다.

var inherit = function(Parent, Child) {
    var F = function() {};
    return function(Parent, Child) {
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.prototype.constructor = Child;
        Child.super = Parent.prototype;
    };
}(); // 즉시 실행 함수(클로저를 반환)

캡슐화

캡슐화 encapsulation 는 기본적으로 관련된 여러 가지 정보를 하나의 틀 안에 담는 것을 의미한다. 여기서 중요한 것은 정보 은닉 information hiding 이다. Java 는 public, private 같은 키워드가 있지만 자바스크립트는 이러한 것들이 없다. 하지만 그렇다고 해서 자바스크립트에서 정보 은닉이 불가능한 것은 아니다.

var Person = function(arg) {
    var name = arg ? arg : 'kim';
    this.getName = function() {
    	return name;
    }
    this.setName = function(name) {
    	name = name;
    }
};

var lee = new Person();
console.log(lee.getName()); // kim
lee.setName('lee');
console.log(lee.getName()); // lee
console.log(lee.name); // undefined

this 객체의 프로퍼티로 선언하면 외부에서 new 키워드로 생성한 객체로 접근할 수 있다. 하지만 var로 선언된 멤버들은 외부에서 접근이 불가능하다. 그리고 public 메서드가 클로저 역할을 하며 private 멤버인 name 에 접근할 수 있다. 이것이 자바스크립트의 기본적인 정보 은닉 방법이다.

주의할 점은 private 멤버가 기본 타입이 아닌 객체나 배열을 반환하는 경우 얕은 복사 shallow copy 가 일어나기 때문에 새로운 객체나 배열을 생성해서 복사해서 반환하는 깊은 복사 deep copy 가 이뤄져야 한다.

객체지향 프로그래밍 예제

기존 클래스의 기능을 가지는 subClass 함수

다음 세 가지를 활용한 subClass 함수를 만들어보는 내용이다.

  • 함수의 프로토타입 체인
  • extend 함수
  • 인스턴스를 생성할 때 생성자 호출(여기서는 생성자를 _init 함수로 정한다.)

subClass 함수 구조

subClass 는 상속받을 클래스에 넣을 변수 및 메서드가 담긴 객체를 인자로 받아 부모 함수를 상속받는 자식 클래스를 만든다.
여기서 부모 함수는 subClass() 함수를 호출할 때 this 객체를 의미한다.

var SuperClass = subClass(obj);
var SubClass = SuperClass.subClass(obj);

함수 구조는 다음과 같다.

  • 자식 클래스 생성
  • 생성자 호출
  • 프로토타입 체인을 이용한 상속
  • 인자 객체를 통해 들어온 변수 및 메서드를 자식 클래스에 추가(확장)
  • 자식 함수 객체 반환

자식 클래스 생성 및 상속



참고: 송형주, 고현준, 인사이드 자바스크립트(2014)

0개의 댓글