Class를 기반으로 상속을 구현하는 다른 언어와 달리 JavaScript는 Prototype을 기반으로 상속을 구현합니다.
즉, Class를 생성해서 상속받는 것은 원론적으로 불가능합니다.
자바 언어에서 클래스를 생성하는 예제를 보겠습니다.

// Java
// User Class를 생성합니다.
public class User {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(string name) {
        this.name = name;
    }
}
// User 클래스를 상속받는 YunaKim 인스턴스를 생성하였습니다.
User YunaKim = new User();
YunaKim.setName("김연아");

Java는 클래스를 기반으로 상속을 받기 때문에 위와 같이 클래스를 만들고,
그 인스턴스를 만들어냅니다.
User 클래스의 인스턴스들은 모두 이름(name), 나이(age)라는 private 필드와 함께,
getName(), setName() 메소드를 가지게 됩니다.
비슷한 예제를 자바스크립트로 만들어보도록 하겠습니다.

// 생성자 함수로 사용할 함수 User를 정의합니다.
var User = function(sName, nAge) {
    this._sName = sName || "이름을 등록해주세요";
    this._nAge = nAge || 0;
};
// User의 prototype에 getName 메소드와 setName 메소드를 가지는 객체를 삽입합니다.
User.prototype = {
    getName : function() {
        return this._sName;
    },
    setName : function(sName) {
        this._sName = sName;
    }
}
// User의 prototype을 객체로 변경했기 때문에 생성자 함수가 User임을 명시하기 위해 prototype의 constructor에 User함수를 넣습니다.
User.prototype.constructor = User;
// User 함수를 생성자 함수로 하는 YunaKim 인스턴스를 생성합니다.
var YunaKim = new User();
YunaKim.setName("김연아");

기본적으로 자바스크립트는 생성자 함수(Constructor)를 사용해 새로운 객체를 생성합니다.
각 함수는 prototype 속성을 가지고 있으며, 새로운 객체가 생성될 때 객체의 [[proto]] 속성이 생성자 함수의 prototype 속성의 주소를 참조하게 되면서 상속을 구현합니다.
위의 예제에서 YunaKim 인스턴스를 생성하기 위한 생성자 함수는 User 함수이며, User의 prototype은 바로 아래에 있는 User.prototype입니다.
User 생성자 함수를 이용해 생성된 객체는 모두 User.prototype을 상속받게 됩니다.
그리고 JavaScript에서도 Java와 마찬가지로 인스턴스를 생성할 때에는 new 연산자로 생성합니다.
new 연산자를 붙임으로써 자바스크립트 내부에서 다음 동작을 수행하게 됩니다.

  1. 함수 실행 공간에서 임의의 객체 obj를 생성합니다.
  2. obj의 기본 메소드들을 정의하고, obj의 [[proto]] 속성에 constructor의 prototype 속성을 대입시킵니다.
  3. this 키워드에 obj를 대입합니다.
  4. this를 리턴합니다.

만약 생성자 함수에서 다른 객체를 의도적으로 반환하고 있을 경우에는 new키워드를 활용한 객체생성이 다르게 동작하게 됩니다.
간단히 정리해보겠습니다.

  • 자바스크립트는 클래스 기반 상속이 아니기 때문에 클래스가 없습니다. 그래서 프로토타입을 이용해서 상속을 구현합니다.
  • 자바스크립트의 함수와 프로토타입의 조합으로 클래스 비슷한 걸 만들어볼 수 있습니다.

위의 예제를 아주 약간 변경해보겠습니다.

// 생성자 함수로 사용할 함수 User를 Create 함수로 감싸줍니다.
// 이제 외부에서 접근할 때에는 Create 함수로 접근하면 됩니다.
var Create = function(oClassMember) {
    // Create 함수 내부에서 임의의 함수 User를 생성합니다. User는 초기화 및 생성자 함수입니다.
    var User = function(sName, nAge) {
        this._sName = sName || "이름을 등록해주세요";
        this._nAge = nAge || 0;
    };
    User.prototype = oClassMember;
    User.prototype.constructor = User;
    return new User;
};
var userInfo = {
    getName : function() {
        return this._sName;
    },
    setName : function(sName) {
        this._sName = sName;
    }
}
var YunaKim = Create(userInfo);
YunaKim.setName("김연아");

위의 예제에서 인스턴스를 생성하던 생성자함수 User와, User의 prototype을 설정하는 부분을 create라는 함수로 감쌌습니다.
그리고, 파라미터로 넘어온 객체를 prototype으로 하는 새로운 객체를 생성하여 return해줍니다.
이렇게 하면 조금 더 보기 좋아진 것 같습니다. 새로운 함수 생성 및 프로토타입 지정이 모두 create 함수 내부에 포함되어 있어서 함수의 가독성이 조금 더 증가합니다.
이제 이 함수를 조금 더 개선시켜보도록 하겠습니다.
위의 함수는 현재 User라는 생성자함수를 가지고서만 생성이 가능하도록 되어 있습니다.
이번에는 User함수도 빼보도록 하겠습니다. 여기에 Create라는 생성자함수 명도 별로인 거 같으니,
제가 생성자함수 명을 바꿀 수 있도록 아주 약간 코드를 변경하겠습니다.

var $Class = function(oClassMember) {
    var origin = function() {
        this.$init.apply(this, arguments);
        // apply 메소드는 this바인딩을 변경할 때 도움을 줍니다.
        // 1번째 파라미터(this)는 바인딩할 객체이고, 2번째 파라미터(arguments)는 배열로 함수에 넘겨줄 파라미터입니다.
        // 이때 배열은 자기순환을 하며 매개변수에 값을 넣습니다.
        // 이 예제에서 sName : arguments[0], nAge : arguments[1]이 됩니다. this는 마찬가지로 origin입니다.
    };
    origin.prototype = oClassMember;
    origin.prototype.constructor = User;
    return origin;
};
var User = $Class({
    $init : function(sName, nAge){
        this._sName = sName;
        this._nAge = nAge;
    },
    getName : function() {
        return this._sName;
    },
    setName : function(sName) {
        this._sName = sName;
    }
});
var YunaKim = new User("김연아", 40);

코드가 꽤 좋아졌습니다.
이제 생성자 함수에서 초기화 함수($init)도 내가 지정해줄 수 있어 확장성이 좋아지고, 생성자 함수의 이름(User)도 마음대로 지정해줄 수 있어서 생성자 함수의 이름을 명확히 나타낼 수 있게 되었습니다.

var $Class = function(oClassMember) {
    function ClassOrigin() {
        this.$init.apply(this, arguments);
    }
    ClassOrigin.prototype = oClassMember;
    ClassOrigin.prototype.constructor = ClassOrigin;
    return ClassOrigin
}
// 예제 1
var User = $Class({
    _name : "김연아",
    $init : function(sName){
        this._name = sName;
    },
    printName : function(){
        console.log(this._name);
    }
});
// 예제 2
var User2 = $Class({
    $init : function(sName){
        this._name = sName || "아이유";
    },
    printName : function(){
        console.log(this._name);
    }
});
var YunaKim = new User("김연아");
var VictorAn = new User("빅토르 안");
var GracieGold = new User("그레이시 골드");
var IU = new User2("아이유");
var Suji = new User2("수지");
var Tanggu = new User2("태연");

위 예제는 서로 동일하게 작동하는 것처럼 보이고, 메소드가 동작하지 않는다던가 하는 문제는 발생하지 않습니다.
그러나 내부 동작방식은 다릅니다.
User 생성자함수와 User2 생성자함수를 비교해보겠습니다.
생성자 함수 User와 User2는 각각 프로토타입을 참조합니다.
이 때 프로토타입은 각 함수를 만들때 넘긴 파라미터가 됩니다.
예제 1에서는 생성자 함수를 만들 때 넘긴 객체에 함께 _name을 보내줬습니다.
이렇게하면 해당 생성자함수를 바탕으로 한 모든 인스턴스들이 _name을 상속받습니다.
단, prototype은 읽기전용 속성입니다. 인스턴스에서 prototype을 재정의하는 것은 불가능합니다.
따라서 내가 _name을 정의해주게 되면 prototype의 _name이 덮어지는 것이 아니라, 인스턴스 객체의 멤버로 _name이 다시 추가됩니다.
즉, 불필요한 메모리를 낭비하게 됩니다.
다시 예제 1번을 보겠습니다.

var User = $Class({
    _name : "김연아",
    $init : function(sName){
        this._name = sName;
    },
    printName : function(){
        console.log(this._name);
    }
});
var YunaKim = new User("김연아");
var VictorAn = new User("빅토르 안");
var GracieGold = new User("그레이시 골드");
YunaKim.printName(); // 김연아
VictorAn.printName(); // 빅토르 안
GracieGold.printName(); // 그레이시 골드

위 예제는 얼핏보면 제대로 동작하는 것처럼 보입니다.
그러나 실제 객체구조를 보면,
스크린샷 2014-03-04 오후 3.24.30
name은 인스턴스 객체의 속성으로 들어갔고, 프로토타입에는 계속 초기값인 김연아가 유지됩니다.
불필요한 메모리 공간의 사용은 당연히 지양하는 것이 좋습니다.
그럼 두번째 예제를 보겠습니다.
생성자 함수 User2는 _name 속성을 새로 생성되는 객체(즉 인스턴스)에서 가집니다.
인스턴스별로 _name 속성을 가지기 때문에 1번째 예제에 비해서 메모리를 차지하는 양은 더 많지만,
모든 인스턴스의 공통 속성이 아닌 인스턴스별로 가져야하는 독립속성에 대해서는 이렇게 처리하는 편이 더 깔끔합니다.
다시 두번째 예제의 코드를 보겠습니다.

var User2 = $Class({
    $init : function(sName){
        this._name = sName || "아이유";
    },
    printName : function(){
        console.log(this._name);
    }
});
var IU = new User2("아이유");
var Suji = new User2("수지");
var Tanggu = new User2("태연");
IU.printName(); // 아이유
Suji.printName(); // 수지
Tanggu.printName(); // 태연

얼핏보면 비슷하게 동작하지만,
이번에는 인스턴스별로 _name을 가지기 때문에 각 인스턴스 객체의 _name을 바로 참조하여 값을 수정하게 됩니다.
즉 프로토타입에는 _name을 가지지 않기 때문에 위의 예제보다 조금 더 적은 메모리를 사용하게 됩니다.
인스턴스로 생성된 객체의 구조를 보면,
스크린샷 2014-03-04 오후 3.32.41
이렇듯 프로토타입에 중복되는 값이 없이 깔끔하게 정리되었습니다.
printName과 같은 공통 메서드는 prototype에 넣어두어 모든 곳에서 하나의 속성만을 바라보게 하는 것이 메모리 절약에 도움이 되지만,
_name과 같은 인스턴스별로 다른 멤버는 인스턴스에서 자체적으로 생성할 수 있도록 하는 것이 좋습니다.

카테고리: Research

2개의 댓글

김태훈 · 2015년 7월 16일 1:56 오후

최근에 w3shools보고 독학하고 있는 초보입니다.
$class로 변수를 선언하셨는데 $없이 하는것과 차이가 어떤것이지요..?
$가 변수를 가리키는것 맞나요?(jquery 튜토리얼 할때 $를 쓰던데 이것가 무슨 차이인지..)
$init은 임의로 쓰신건가요?
아니면 저런속성이 따로 있는건가요?

답글 남기기

아바타 플레이스홀더

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다