(JS) 모던 자바스크립트 자바스크립트 객체지향 프로그래밍

호두파파·2021년 3월 30일
0

해당 글은 포에마 웹의 자바스크립트 객체지향 프로그래밍
의 일부를 토대로 작성되었습니다.

원문보기

자바스크립트 : 프로토타입 기반 언어

자바스크립트는 멀티-패러다임 언어로 명령형, 함수형, 프로포토타입 기반 객체지향 언어다. 비록 다른 객체지향 언어들과의 차이점에 대한 논쟁들이 있긴 하지만, 자바스크립트는 강력한 객체지향 프로그래밍 능력들을 지니고 있다.

자바스크립트는 클래스 개념이 없고 별도의 객체 생성방법이 존재한다.

  • 객체 리터럴
  • Object() 생성자 함수
  • 생성자 함수
var obj1 = {};
obj1.name = 'Lee';

//Object() 생성자 함수
var obj2 = new Object();
obj2.name = 'lee';

//생성자 함수 
function F() {};
var obj = new F();
obj3.name = 'lee';

자바스크립트는 이미 생성된 인스턴스의 자료구조와 기능을 동적으로 변경할 수 있다는 특징이 있다. 객체 지향의 상속, 캡슐화(정보은닉) 등의 개념은 프로토타입 체인과 클로저 등으로 구현할 수 있다.

클래스 기반 언어에 익숙한 프로그래머들은 이러한 프로토타입 기반의 특성으로 인해 혼란을 느낀다. 자바스크립트에서는 함수 객체로 많은 것을 할 수 있는데 클래스, 생성자, 메소드도 모두 함수로 구현이 가능하다.

ECMAScript 6에서 새롭게 클래스가 도입되었다. 다만 새로군 객채지향 모델을 제공하는 것이 아니기 때문에 기존 prototype 기반 패턴의 Syntatic sugar라는 의견이 있다.

생성자 함수와 인스턴스의 생성

자바스크립트는 생성자 함수와 new 연산자를 통해 인스턴스를 생성할 수 있다. 이때 생성자 함수는 클래스이자 생성자의 역할을 한다.

function Person(name) {
  //프로퍼티 
  this.name = name;
  
  //메소드
  this.setName = function(name) {
    this.name = name;
  };
  //메소드 
  this.getName = function() {
    return this.name;
  };
}
//인스턴스의 생성 
var me = new Person('lee');
console.log(me.getName());//lee

//메소드 호출 
me.setName('Kim');
console.log(me.getName());//kim

위 예제에서 만든 Person 생성자 함수로 여러개의 인스턴스를 만들어보면 다음과 같다.

var me  = new Person('Lee');
var you = new Person('Kim');
var him = new Person('Choi');

console.log(me);  // Person { name: 'Lee', setName: [Function], getName: [Function] }
console.log(you); // Person { name: 'Kim', setName: [Function], getName: [Function] }
console.log(him); // Person { name: 'Choi', setName: [Function], getName: [Function] }

위와 같이 인스턴스를 생성하면 각각의 인스턴스에 setName, getName이 중복되어 생성된다. 즉, 각 인스턴스가 내용이 동일한 메소드를 각자 소유한다. 이는 메모리 낭비인데 생성되는 인스턴스가 많아지거나 메소드가 크거나 많다면 무시할 수 없는 문제이다.

메모리 누수를 해결하기 위해 프로토타입을 이용할 수 있다.

프로토타입 체인과 메소드의 정의

모든 객체는 프로토타입이라는 다른 객체를 가리키는 내부링크를 가지고 있다. 즉, 프로토타입을 통해 직접 객체를 연결할 수 있는데 이를 프로토타입 체인이라고 한다.

프로토타입을 이용해 생성자 함수 내부의 메소드를 생성자 함수의 prototype 프로퍼티가 가리키는 프로로타입 객체로 이동시키면 생성자 함수에 의해 생성된 모든 인스턴스는 프로토타입 체인을 통해 프로토타입 객체의 메소드를 참조할 수 있다.

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

// 프로토타입 객체에 메소드 정의 
Person.prototype.setName = function(name) {
  this.name = name;
};

// 프로토타입 객채에 메소드 정의 
Person.prototype.getName = function() {
  return this.name;
};

let me = new Person('Lee');
let you = new Person('kim');
let him = new Person('choi');

console.log(Person.prototype);
// Person { setName: [Function], getName: [Function] }

console.log(me);  // Person { name: 'Lee' }
console.log(you); // Person { name: 'Kim' }
console.log(him); // Person { name: 'choi' }  


출처 : poiemaweb web, 프로토 타입 체인과 메소드 정의

Person 생성자 함수의 Prototype 프로퍼티가 가리키는 프로토타입 객체로 이동시킨 setName, getName 메소드는 프로토타입 체인에 의해 모든 인스턴스가 참조할 수 있다. 프로토타입 객체는 상속할 것들이 저장되는 장소이다

/**
 * 모든 생성자 함수의 프로토타입은 Function.prototype이다. 따라서 모든 생성자 함수는 Function.prototype.method()에 접근할 수 있다.
 * @method Function.prototype.method
 * @param ({string}) (name) - (메소드 이름)
 * @param ({function}) (func) - (추가할 메소드 본체)
 */
Function.prototype.method = function (name, func) {
  // 생성자함수의 프로토타입에 동일한 이름의 메소드가 없으면 생성자함수의 프로토타입에 메소드를 추가
  // this: 생성자함수
  if (!this.prototype[name]) {
    this.prototype[name] = func;
  }
};

/**
 * 생성자 함수
 */
function Person(name) {
  this.name = name;
}

/**
 * 생성자함수 Person의 프로토타입에 메소드 setName을 추가
 */
Person.method('setName', function (name) {
  this.name = name;
});

/**
 * 생성자함수 Person의 프로토타입에 메소드 getName을 추가
 */
Person.method('getName', function () {
  return this.name;
});

var me  = new Person('Lee');
var you = new Person('Kim');
var him = new Person('choi');

console.log(Person.prototype);
// Person { setName: [Function], getName: [Function] }

console.log(me);  // Person { name: 'Lee' }
console.log(you); // Person { name: 'Kim' }
console.log(him); // Person { name: 'choi' }

상속(Inheritance)

Java같은 클래스 기반 언어에서 상속(또는 확장)은 코드 재사용의 관점에서 매우 유용하다. 새롭게 정의할 클래스가 기존에 있는 클래스와 매우 유사하다면, 상속을 통해 다른 점만 구현하면 된다. 코드 재사용은 개발 비용을 현저히 줄일 수 있는 잠재력이 있기 때문에 매우 중요하다.

클래스 기반 언어에서 객체는 클래스의 인스턴스이며 클래스는 다른 클래스로 상속될 수 있다. 자바스크립트는 기본적으로 프로토타입을 통해 상속을 구현한다.

이것은 프로토타입을 통해 객체가 다른 객체로 직접 상속된다는 의미이다.

상속구현방식은 두 가지로 구분할 수 있다.

  • 의사 클래스 패턴 상속 : 클래스 기반 언어의 상속 방식을 흉내내는 것
  • 프로토타입 패턴 상속: 프로토타입 상속을 구현하는 것

1) 의사 클래스 패턴 상속(Pseudo-classical Inheritance)

의사 클래스 패턴은 자식 생성자 함수의 Prototype 프로퍼티를 부모 생성자 함수의 인스턴스로 교체해 상속을 구현하는 방법이다.

부모와 자식 모두 생성자 함수를 정의해야 한다.

var parents = (function () {
  // constructor
  function Parent(name) {
    this.name = name;
  }
  
  // method
  Parent.prototype.sayhi = function() {
    console.log('hi!' + this.name);
  };
  
  // return constructor
  return Parent;
}());

// 자식 생성자 함수 
var child = (function() {
  // constructor
  function Child(name) {
    this.name = name;
  }
  
  // 자식 생성자 함수의 프로토타입 객체를 부모 생성자 함수의 인스턴스로 교체.
  child.prototype = new Parents();//
  
  // 메소드 오버라이드 
  child.prototype.sayHi = function() {
    console.log('안녕하세요! ' + this.name);
  };
  
  // saybye메소드는 Parents 생성자 함수의 인스턴스에 위치된다. 
  child.prototype.sayBye = function() {
    console.log('안녕히 가세요! '+this.name);
  };
  // return constructor
  return child;
}());
var child = new Child('child'); // ①
console.log(child);  // Parent { name: 'child' }

console.log(Child.prototype); // Parent { name: undefined, sayHi: [Function], sayBye: [Function] }

child.sayHi();  // 안녕하세요! child
child.sayBye(); // 안녕히가세요! child

console.log(child instanceof Parent); // true
console.log(child instanceof Child);  // true

child 생성자 함수가 생성한 인스턴스 child의 프로토타입의 객체는 Parents 생성자 함수가 생성한 인스턴스이다. 그리고 Parent 생성자 함수가 생성한 인스턴스의 프로토타입 객체는 Parent.prototype이다.

이로써 child는 프로토타입 체인에 의해 Parent 생성자 함수가 생성한 인스턴스와 Parents.prototype의 모든 프로퍼티에 접근할 수 있게 되었다.

출처 : poiemaweb web, 의사 클래스 패턴 상속 (Pseudo-classical Inheritance)

의사 클래스 패턴은 아래와 같은 문제를 가지고 있다.

  1. new 연산자를 통해 인스턴스를 생성한다.
    생성자 함수와 new연산자를 통해 객체를 생성하는 불필요한 간접적인 단계가 있다. 클래스와 비슷하게 보이는 일부 복잡한 구문은 프로토타입 메커니즘을 명확히 나타내지 못한다.

만약 생성자 함수를 호출할 때 new 연산자를 포함하는 것을 잊게되면 this는 새로운 객체와 바인딩되지 않고 전역객체에 바인딩된다.

new 연산자와 함께 호출된 생성자 함수 내부의 this는 새로 생성된 객체를 참조한다.

이런 문제점을 경감시키기 위해 첫글자를 대문자로 표기하는 방식으로 생성자 함수 이름을 표기하는 것이 선호되지만, 더 나은 대안은 new 연산자의 사용을 피하는 것이다.

  1. 생성자 링크의 파괴
    위 그림을 보면 child 객체의 프로토타입 객체는 Parent 생성자 함수가 생성한 new Parent() 객체이다. 프로토타입 객체는 내부 프로퍼티로 constructor를 가지며 이는 생성자 함수를 가리킨다. 하지만 의사 클래스 패턴 상속은 프로토타입 객체를 인스턴스로 교체하는 과정에서 constructor의 연결이 깨지게 된다. 즉, child 객체를 생성한 것은 Child 생성자 함수이지만 child.constructor의 출력 결과는 Child 생성자 함수가 아닌 Parent 생성자 함수를 나타낸다. 이는 child 객체의 프로토타입 객체인 new Parent() 객체는 constructor가 없기 때문에 프로토타입 체인에 의해 Parent.prototype의 constructor를 참조했기 때문이다.

  2. 객체 리터럴
    의사 클래스 패턴 상속은 기본적으로 생성한 함수를 사용하기 때문에 객체 리터럴 패턴으로 생성한 객체의 상속에는 적합하지 않다. 이는 객체리터럴 패턴으로 생성한 객체의 생성하 함수는 Object()이고 이를 변경할 방법이 없기 때문이다.


2) 프로토타입 패턴 상속(Prototype Inheritance)

프로토타입 패턴 상속은 Object.create함수를 사용해 객체에서 다른 객체로 직접 상속을 구현하는 방식이다. 프로토타입 패턴 상속은 개념적으로 의사 클래스 패턴 상속보다 더 간단하다. 또한, 의사 클래스 패턴의 단점인 new 연산자가 필요없으며, 생성자 링크도 파괴되지 않으며 객체리터럴에도 사용할 수 있다.

var Parent = (function() {
  // Constructor
  function Parent(name) {
    this.name = name;
  }
  
  // method
  Parent.prototype.sayHi = function() {
    console.log('hi ' + this.name);
  };
  //return constructor
  return Parent;
}());
// create 함수의 인수는 프로토타입이다.
var child = Object.create(Parent.prototype);
Child.name = 'child';
child.sayHi(); // Hi! child
console.log(child instanceof Parent); // true


출처 : poiemaweb, 프로토타입 패턴 상속: 생성자함수

객체 리터럴 패턴으로 생성한 객체에도 프로토타입 패턴 상속을 사용할 수 있다.

var parent = {
  name : 'parent', 
  sayHi : function() {
    console.log('Hi! ' + this.name);
  }
};

// create 함수의 인자는 객체이다. 
var child = Object.create(parent);
child.name = 'child';

// var child = Object.create(parent, {name: {value: 'child'}});

parent.sayhi(); //hi!parent
child.sayHi(); //hi! child

console.log(parent.isPrototypeOf(child)); // true

  • Object.create 함수는 매개변수에 프로토타입으로 설정할 객체 똔느 인스턴스를 전달하고 이를 상속하는 새로운 객체를 생성한다.
  • Object.create 함수는 표준에 비교적 늦게 추가되어, 크로스 브라우징에 주의하여야 한다.

Object.create() 함수의 폴리필

// Object.create 함수의 폴리필
if (!Object.create) {
  Object.create = function (o) {
    function F() {}  // 1
    F.prototype = o; // 2
    return new F();  // 3
  };
}

위 폴리필은 프로토타입 패턴 상속의 핵심을 담고 있따.
1. 비어있는 생성자 함수 F를 생성한다.
2. 생성자 함수 F의 Prototype 프로퍼티에 매개변수로 전달받은 객체를 할당한다.
3. 생성자 함수 F를 생성자로 해 새로운 객체를 생성하고 반환한다.

출처 : Powema web, Object.create 함수의 폴리필

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

0개의 댓글