-
자바스크립트 객체지향 프로그래밍자바스크립트 2021. 4. 7. 02:23
6.0 객체지향 언어로서 클래스 기반의 언어와 프로토타입 기반의 언어의 차이
6.0.1 클래스 기반의 언어
- 클래스로 객체의 기본적인 형태와 기능을 정의하고, 생성자로 인스턴스를 만들어서 사용할 수 있다.
- 클래스에 정의된 메서드로 여러 가지 기능을 수행할 수 있다.
- 모든 인스턴스가 클래스에 정의된 대로 같은 구조이고 보통 런타임에 바꿀 수 없다.
- Java, C++
6.0.2 프로토타입 기반의 언어
- 객체의 자료구조, 메서드 등을 동적으로 바꿀 수 있다.
- 정확성, 안전성, 예측성 등의 관점에서 클래스 기반 언어보다 떨어진다.
- Javascript
6.1 클래스, 생성자, 메서드
자바스크립트는 거의 모든 것이 객체이고, 특히 함수 객체로 많은 것을 구현해낸다. 클래스, 생성자, 메서드도 모두 함수로 구현이 가능하다. 구체적으로 살펴보기에 앞서, 자바스크립트의 프로토타입과 new 연산자를 다시 복습하자.
function Person(arg){ this.name = arg; this.getName = function(){ return this.name; } this.setName = function(value){ this.name = value; } } var me = new Person("jun"); console.log(me.getName()); // (출력값) jun me.setName("minsu"); console.log(me.getName()); // (출력값) minsu
위의 예제에서 new 키워드로 새로운 객체 me 를 만들었다. 이 형태는 기존 객체지향 프로그래밍 언어에서 한 클래스의 인스턴스를 생성하는 코드와 매우 유사하다. 함수 Person이 클래스이자 생성자의 역할을 한다.
me : Person의 인스턴스, name 변수를 가진다. getName()과 setName() 함수가 있다.
하지만 이 예제는 문제가 많다. 이 Person을 생성자로 하여 여러 개의 객체를 생성한다고 가정해보자.
var me = new Person("me"); var you = new Person("you"); var him = new Person("him");
각 객체는 setName() 함수와 getName() 함수를 따로 생성하고 있다. 이는 불필요하게 중복되는 영역을 메모리에 올려놓고 사용함을 의미하고 자원 낭비를 가져온다.
따라서 앞의 문제를 해결하려면 다른 방식의 접근이 필요한데, 여기서 활용할 수 있는 자바스크립트의 특성이 함수 객체의 프로토타입이다.
fucntion Person(arg) { this.name = arg; } Person.prototype.getName = function() { return this.name; } Person.prototype.setName = function(value) { this.name = value; } var me = new Person("me"); var you = new Person("you");
위 예제에서는 Person 함수 객체의 prototype 프로퍼티에 getName()과 setName() 함수를 정의하였다. 이러면 각 객체는 따로 함수 객체를 생성할 필요 없이 setName()과 getName() 함수를 프로토타입 체인으로 접근할 수 있다. 이와 같이 자바스크립트에서 클래스 안의 메서드를 정의할 때는 프로토타입 객체에 정의한 후, new로 생성한 객체에 접근할 수 있게 하는 것이 좋다. 더글라스 크락포드는 다음과 같은 함수를 제시하면서 메서드를 정의하는 방법을 소개한다.
Function.prototype.method = function(name, func) { if (!this.prototype[name]) this.prototype[name] = func; } }
이 함수를 이용한다면 조금 전의 예제는 다음과 같은 형태가 된다.
Person.prototype.method = function(name, func) { this.prototype[name] = func; } fucntion Person(arg) { this.name = arg; } Person.method("setName", function(value) { this.name = arg; } Person.method("getName", function(value) { return this.name; } var me = new Person("me"); var you = new Person("you");
6.2 상속
- 자바스크립트는 클래스 기반의 전통적 상속 지원 X
- 대신 객체 프로토타입 체인을 이용하여 상속 구현 O
6.2.1 프로토타입을 이용한 상속
function create_object(o) { function F() {} F.prototype = o; return new F(); }
위 코드는 자바스크립트 객체를 상속하는 방법으로 이 세 줄의 코드를 이해하면 다 배운것이나 다름없다.
var person = { name : "zzoon", getName : function() { return this.name; }, setName : function(arg) { this.name = arg; } }; function create_object(o) { function F() {}; F.prototype = o; return new F(); } var student = create_object(person); student.setName("me"); console.log(student.getName()); // (출력값) me
위 예제는 create_object() 함수를 이용하여 상속을 구현한 것으로 person 객체를 상속하여 student 객체를 만들었다.
클래스에 해당하는 생성자 함수를 만들지도 않았고, 클래스의 인스턴스를 따로 생성하지 않았다. 단지 부모 객체에 해당하는 person 객체와 이 객체를 프로토타입 체인으로 참조할 수 있는 자식 객체 student를 만들어서 사용하였다.
여기에서 자식은 자신의 메서드를 재정의 혹은 추가로 기능을 더 확장시킬 수 있어야 한다. 자바스크립트에서는 extend() 함수로 객체에 자신이 원하는 객체 혹은 함수를 추가시킬 수 있다.
var person = { name : "zzoon", getName : function() { return this.name; }, setName : function(arg) { this.name = arg; } }; function create_object(o) { function F() {}; F.prototype = o; return new F(); } var student = create_object(person); var added = { setAge : function(age) { this.age = age; }, getAge : function() { return this.age; } }; extend(student, added); student.setAge(25); console.log(student.getAge()); // (출력값) 25
6.2.2 클래스 기반의 상속
- 원리는 앞의 프토토타입을 이용한 상속과 거의 같다.
- 다만 앞 절에서는 객체 리터럴로 생성된 객체의 상속을 소개했지만, 여기서는 클래스의 역할을 하는 함수로 상속을 구현한다.
function Person(arg) { this.name = arg; } Person.prototype.setName = function(value) { this.name = value; }; Person.prototype.getName = function(value) { return this.name; }; function Student(arg) { } var you = new Person("joo"); Student.prototype = you; var me = new Student("zzoon"); me.setName("zzoon");
Student 함수 객체를 만들어 이 함수 객체의 프로토타입으로 하여금 Person 함수 객체의 인스턴스를 참조하게 만들었다. 이 결과 객체 me는 Person.prototype 프로퍼티에 접근할 수 있고 setName()과 getName()을 호출할 수 있다.
그러나 이 예제는 문제가 있다. 먼저 me 인스턴스를 생성할 때 부모 클래스인 Person의 생성자를 호출하지 않는다.
var me - new Student("zzoon");
이 코드로 me 인스턴스를 생성할 때 "zzoon"을 인자로 넘겼으나, 이를 반영하는 코드는 어디에도 없다. 결국 생성된 me 객체는 빈 객체이다. 이를 해결하려면 Student 함수에 다음 코드를 추가해 부모 클래스의 생성자를 호출해야 한다.
function Student(arg) { Person.apply(this, arguments); }
Student 함수 안에서 새롭게 생성된 객체를 apply 함수의 첫 번째 인자로 넘겨 Person 함수를 실행시킨다. 여기서 조금만 더 발전시켜보자. 현재 자식 클래스의 객체가 부모 클래스의 객체를 프로토타입 체인으로 직접 접근한다. 하지만 부모 클래스의 인스턴스와 자식 클래스의 인스턴스는 서로 독립적일 필요가 있다. 두 클래스의 프로토타입 사이에 중개자를 하나 만들어보자.
function Person(arg) { this.name = arg; } Function.prototype.method = function(name, func) { this.prototype[name] = func; } Person.method("setName", function(value) { this.name = value; }); Person.method("getName", function(value) { return this.name; }); function Student(arg) { } function F() {}; F.prototype = Person.prototype; Student.prototype = new F(); Student.prototype.constructor = Student; Student.super = Person.prototype; var me = new Student("zzoon"); me.setName("zzoon");
빈 함수 F()를 생성하고, 이 F()의 인스턴스를 Person.prototype과 Student 사이에 두었다. 그리고 이 인스턴스를 Student.prototype에 참조하게 된다.
빈 함수의 객체를 중간에 두어 Person의 인스턴스와 Student의 인스턴스를 서로 독립적으로 만들었다. 이제 Person 함수 객체에서 this에 바인딩되는 것은 Student의 인스턴스가 접근할 수 없다.
6.3 캡슐화
- 관련된 여러 가지 정보를 하나의 틀 안에 담는 것
- 예를들면, 멤버 변수와 메서드가 서로 관련된 정보가 되고 클래스가 이것을 담는 하나의 큰 틀
- 중요한 것은 정보의 공개 여부. 정보 은닉의 개념이 이 부분을 담당한다.
- C++이나 Java에서는 public, private 멤버를 선언함으로써 해당 정보를 외부로 노출시킬지를 결정한다.
- 하지만, 자바스크립트에서는 이러한 키워드 자체를 지원하지 않는다. 그렇다고 정보 은닉이 불가능한 것은 아니다.
var Person = function(arg) { var name = arg ? arg : "zzoon" ; this.getName = function(){ return name; } this.setName = function(arg){ name = arg; } }; var me = new Person(); me.setName("joo"); console.log(me.getName); // (출력값) joo console.log(me.name); // (출력값) undefined
private 멤버로 name을 선언하고, public 메서드로 getName()과 setName()을 선언하였다. this 객체의 프로퍼티로 선언하면 외부에서 new 키워드로 생선한 객체로 접근할 수 있다. var로 선언된 멤버들은 외부에서는 접근이 불가능하다. 그리고 public 메서드가 클로저 역할을 하면서 private 멤버인 name에 접근할 수 있다. 여기서 코드를 조금 더 깔끔하게 다듬어보자.
var Person = function(arg) { var name = arg ? arg : "zzoon" ; return { getName : function(){ return name; }, setName : function(arg){ name = arg; } }; } var me = new Person();
이 예제는 Person 함수를 호출하여 객체를 반환받는다. 이 객체에 Person 함수의 private 멤버에 접근할 수 있는 메서드들이 담겨있다. 사용자는 반환받는 객체로 메서드를 호출할 수 있고, private 멤버에 접근할 수 있다. 이렇게 메서드가 담겨있는 객체를 반환하는 함수는 여러 유명 자바스크립트 라이브러리에서 쉽게 볼 수 있는 구조이다. 다만 한 가지 주의할 점이 있다. 접근하는 private 멤버가 객체나 배열이면 얕은 복사로 참조만을 반환하므로 사용자가 이후 이를 쉽게 변경할 수 있다. 다음 예제는 이러한 문제를 잘 보여준다.
var ArrCreate = function(arg) { var arr = [1,2,3]; return { getArr : function() { return arr; } }; } var obj = ArrCreate(); var arr = obj.getArr(); arr.push(5); console.log(obj.getArr()); // (출력값) [ 1,2,3,5 ]
이와 같은 문제가 있으므로 객체를 반환하는 경우 신중해야 한다. 다시 이전의 예제로 가보자. 이 예제에서 사용자가 반환받은 객체는 Person 함수 객체의 프로토타입에 접근할 수 없다는 단점이 있다. 이를 보완하려면 객체를 반환하는 것이 아닌, 함수를 반환하는 것이 좋다.
var Person = function(arg) { var name = arg ? arg : "zzoon" ; var Func = function() {} Func.prototype = { getName : function(){ return name; }, setName : function(arg){ name = arg; } }; return Func; }(); var me = new Person();
클로저를 활용하여 name에 접근할 수 없게 했다. 즉시 실행 함수에서 반환되는 Func이 클로저가 되고 이 함수가 참조하는 name 프로퍼티가 자유 변수가 된다. 따라서 사용자는 name에 대한 접근이 불가능하다.
6.4 객체지향 프로그래밍 응용 예제
6.4.1 클래스의 기능을 가진 subClass 함수
기존 클래스와 같은 기능을 하는 자바스크립트 함수를 만들어보자. 이 함수를 다음 세가지를 활용해서 구현한다.
- 함수의 프로토타입 체인
- extend 함수
- 인스턴스를 생성할 때 생성자 호출(여기서는 생성자를 _init 함수로 정한다.)
6.4.1.1 subClass 함수 구조
subClass는 상속받을 클래스에 넣을 변수 및 메서드가 담긴 객체를 인자로 받아 부모 함수를 상속받는 자식 클래스를 만든다. 여기서 부모 함수는 subClass() 함수를 호출할 때 this 객체를 의미한다.
var SuperClass = subClass(obj); var SubClass = SuperClass.subClass(obj);
이처럼 SuperClass를 상속받는 subClass를 만들고자 할 때, SuperClass, subClass()의 형식으로 호출하게 구현한다. 최상위 클래스인 SuperClass는 자바스크립트의 Function을 상속받게 한다.
함수 subClass 의 구조는 다음과 같이 구성된다.
function subClass(obj) { /* (1) 자식 클래스 (함수 객체) 생성 */ /* (2) 생성자 호출 */ /* (3) 프로토타입 체인을 활용한 상속 구현 */ /* (4) obj를 통해 들어온 변수 및 메서드를 자식 클래스에 추가 */ /* (5) 자식 함수 객체 반환 */ }
6.4.1.2 자식 클래스 생성 및 상속
function subClass(obj) { ....... var parent = this; var F = function() {}; var child = function(){ }; /* 프로토타입 체이닝 */ F.prototype = parent.prototype; child.prototype = new F(); child.prototype.constructor = child; child.parent = parent.prototype; child.parent_constructor = parent; ....... return child; }
자식 클래스는 child라는 이름의 함수 객체를 생성함으로써 만들어졌다. 부모 클래스를 가리키는 parent는 this를 그대로 참조한다.
child.parent_constructor에 부모의 생성자를 참조시켰다. 이렇게 프로토타입 체인을 구성하여 부모를 상속받는 새로운 자식 클래스가 만들어지고 반환된다.
6.4.1.3 자식 클래스 확장
이제 사용자가 인자로 넣은 객체를 자식 클래스에 넣어 자식 클래스를 확장할 차례다.
for (var i in obj) { if (obj.hasOwnProperty(i)) { child.prototype[i] = obj[i]; } } // hasOwnProperty 메서드 // Object.prototype 프로퍼티에 정의되어 있는 메서드로서, // 인자로 넘기는 이름에 해당하는 프로퍼티가 객체 내에 있는지를 판단.
extend() 함수의 역할을 하는 코드를 넣었다. 여기서는 간단히 얕은 복사로 객체의 프로퍼티를 복사하는 방식을 택했다.
6.4.1.4 생성자 호출
클래스의 인스턴스가 생성될 때, 클래스 내에 정의된 생성자가 호출돼야 한다. 물론 부모 클래스의 생성자 역시 호출되어야 한다. 이를 자식 클래스 안에 구현하자.
var child = function() { var parent = child.parent; if (parent.hasOwnProperty("_init")) { parent._init.apply(this, arguments); } if (child.prototype.hasOwnProperty("_init")) { child.prototype._init.apply(this, arguments); } };
이 코드는 부모와 자식이 한 쌍을 이루었을 때만 제대로 동작한다. 자식을 또 다른 함수가 다시 상속 받았을 때는 어떻게 될 것인가?
var SuperClass = subClass(); var SubClass = SuperClass.subClass(); var Sub_SubClass = SubClass.subClass(); var instance = new Sub_SubClass();
이 코드에서 인스턴스를 생성할 때, 그 상위 클래스의 상위 클래스인 SuperClass의 생성자가 호출이 되지 않는다. 따라서 부모 클래스의 생성자를 호출하는 코드는 재귀적으로 구현할 필요가 있다.
var child = function() { var _parent = child.parent_constructor; if (_parent && _parent !== Function) { // 현재 클래스의 부모 생성자가 있으면 그 함수를 호출한다. 다만 부모가 Function인 경우는 최상위 클래스에 도달했으므로 실행하지 않는다.) _parent.apply(this, arguments); // 부모 함수의 재귀적 호출 } if (child.prototype.hasOwnProperty("_init")) { child.prototype._init.apply(this, arguments); } };
6.4.1.5 subClass 보완
parent를 단순히 this.prototype으로 지정해서는 안 된다. 우리는 처음에 최상위 클래스를 Function을 상속받는 것으로 정했는데, 현재 코드에서는 이를 처리하는 코드가 없다. 따라서, 다음 코드를
parent = this;
다음과 같이 수정하자.
if (this === window) { var parent = Function; } else { var parent = this; }
만들어진 subClass 함수의 전체 코드는 다음과 같다.
function subClass(obj) { var parent = this === window ? Function : this; var F = function() {}; var child = function() { var _parent = child.parent; if (_parent & _parent !== Function) { _parent.apply(this, arguments); } if (child.prototype._init) { child.prototype_init.apply(this, arguments); } }; F.prototype = parent.prototype; child.prototype = new F(); child.prototype.constructor = child; child.parent = parent; child.subClass = arguments.callee; // 현재 호출된 함수.(subClass) for (var i in obj) { if (obj.hsaOwnProperty(i)) { child.prototype[i] = obj[i]; } } return child; }
6.4.1.6 subClass 활용
6.4.1.7 subClass 함수에 클로저 적용
6.4.2 subClass 함수와 모듈 패턴을 이용한 객체지향 프로그래밍
'자바스크립트' 카테고리의 다른 글
코딩의 기술 4. 조건문을 깔끔하게 작성하라 (0) 2021.05.22 코딩의 기술 2. 배열로 데이터 컬렉션을 관리하라 (0) 2021.05.14 코딩의기술 1.1 const로 변하지 않는 값을 표현하라 (0) 2021.05.01 ES6 문법 정리 (0) 2021.04.04 자바스크립트 데이터 타입과 연산자(1) (0) 2021.04.02