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;
});
자바스크립트에선 클래스 기반의 전통적인 상속은 지원하지 않지만, 객체 프로토타입 체인을 이용하여 상속을 구현할 수 있다. 이런 상속은 크게 두 가지로 구분할 수 있다. 하나는 클래스 기반의 전통적인 상속을 흉내내는 것과 다른 하나는 클래스 개념 없이 객체의 프로토타입으로 상속을 구현하는 방식이다.
다음은 더글라스 크락포드가 소개하는 객체를 상속하는 방법이다.
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
프로토타입을 이용한 상속과 거의 같다. 다만 클래스 역할을 하는 함수가 있을 뿐이다.
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 는 상속받을 클래스에 넣을 변수 및 메서드가 담긴 객체를 인자로 받아 부모 함수를 상속받는 자식 클래스를 만든다.
여기서 부모 함수는 subClass() 함수를 호출할 때 this 객체를 의미한다.
var SuperClass = subClass(obj);
var SubClass = SuperClass.subClass(obj);
함수 구조는 다음과 같다.
참고: 송형주, 고현준, 인사이드 자바스크립트(2014)