JS STUDY - 2주차 ( 객체지향 자바스크립트 )

👊0👊·2020년 1월 19일
0

es6 스터디

목록 보기
4/8

프로토타입 기반 객체지향 언어

기존 다른 언어를 사용하던 개발자들은 처음에 js를 접했을 때, class가 없어서 매우 당황했을 것이다. 이런 점 덕분에 객체지향 언어가 아니라고 생각될 수 있다. 하지만 js는 class 기반 객체지향이 아니고 prototype 기반 객체지향 언어라고 할 수 있다.

JavaScript에서 class는 ECMAScript 2015을 통해 소개되었다. 하지만 Class 문법이 새로운 객체지향 상속 모델을 제공하지 않는다. 다만 prototype 기반 상속을 간단하게 사용할 수 있을 뿐입니다.

프로토타입이란? : 매우 객체지향적인 시스템

프로토타입 기반 프로그래밍은 객체 지향 프로그래밍 스타일 중 하나이다.
프로토타입이 제공하는 위임(delegation)으로 기존 객체를 재사용합니다.
다른 말로는 prototypal, prototype-oriented, classless, 또는 instance-based 프로그래밍이라고도 합니다.

위임이란 프로토타입 기반 프로그래밍에서 지원하는 언어적 기능입니다.

더글라스 클락포트드는 프로토타입 상속을 이렇게 말했습니다.

"프로토타입 객체를 만든 다음에, ... 새로운 인스턴스를 생성한다". 자바스크립트에서 객체들은 변경되므로, 새로운 인스턴스를 확장하여 새로운 필드와 메서드를 제공 할 수 있습니다. 그러면 더 새로운 객체의 프로토타입 역할을 할 수 있습니다. 비슷한 객체를 많이 만들기 위해 클래스가 필요하지 않습니다. 객체는 객체에서 상속됩니다. 이보다 더 객체지향적일 수 있을까요?

프로토타입 기반 시스템은 런타임 동안 프로토타입의 변경을 권장하지만 클래스 기반 객체 지향 시스템에서는 실행 중에 클래스 변경을 할 수 없습니다.

  • 프로토타입 객체지향 시스템
  • 위임으로 기존 객체를 재사용
  • 런타임 동안 프로토타입 변경

객체 구성

프로토 타입 기반 언어에서는 명시적인 클래스가 없습니다.(ES6는 class는 설탕입니다.) 객체는 프로토 타입 속성을 통해 다른 객체로부터 직접 상속됩니다.

클래스 기반 언어에서 새 인스턴스는 객체의 멤버에 대한 메모리 블록을 예약하고 해당 블록에 대한 참조를 반환하는 특수 함수인 클래스의 생성자 함수를 통해 구성됩니다. 그 결과 인스턴스는 클래스에 정의된 모든 메서드와 속성을 상속받습니다. 이것은 유사한 형식의 객체를 구성할 수 있는 일종의 템플릿 역할을 합니다.
// 그림 있으면 좋을듯

image.png

image.png

많은 프로토타입 언어에서는 종종 Object라고하는 루트객체가 있습니다.(JS도 마찬가지) 루트 객체는 런타임에 생성된 다른 모든 객체의 기본 프로토타입으로 설정되며 객체에 대한 설명을 반환하는 toString() 함수와 같이 일반적으로 필요한 메소드를 운반합니다.

image.png

복제(Cloning)은 기존 객체(는 Prototype)의 동작을 복사하여 새 객체를 구성하는 프로세스를 말합니다. 그런 다음 새 객체는 원본의 모든 자질(qualities)을 전달합니다.

// 리터럴로 객체 생성
var foo = { name : "foo", one : 1, two : 2 };

var bar = { two : "two", three : 3 };

// 실제로는 이렇게 하면 안된다. 
// 매우 느린 작업으로, Object.create()를 사용해서 상속하는 것을 권장
// var bar = Object.create(foo);
// bar.two = "two";
// bar.three = 3;
Object.setPrototypeOf(bar, foo); // foo는 지금 bar의 포로토 타입이다. 

bar.one // 1

bar.three // 3

bar.two // "two"

bar.name // "foo"
foo.name // "foo"

프로토타입 위임

디스패치 추천글

dispatch는 프로그램이 어떤 메서드를 호출할 것인가를 결정하여 메서드를 실행하는 과정을 말합니다. 크게 정적(Static)과 동적(Dynamic)이 있습니다.

위임(delegation)을 사용하는 프로토타입언어는 런타임 중에 디스패칭(동적디스패칭)으로 적합한 메서드를 찾거나, 위임 포인터를 따라가며 올바른 데이터 조각을 찾습니다.(객체에서 프로토타입까지)
여기서 객체간의 메서드 공유를 설정하는데 필요한 것이 위임 포인터입니다.(크롬에서 __proto__, ES [[Prototype]])

클래스 기반 객체 지향 언어에서 클래스와 인스턴스 간의 관계와 달리 프로토타입(prototype) 그 파생물(offshoots)간의 관계는 메모리 또는 구조적 유사성이 필요하지 않습니다.

따라서 클래스 기반 시스템에서와 같이 관련 프로토타입의 구조를 재정렬하지 않고도 시간이 지남에 따라 하위 오브젝트를 계속 수정할 수 있습니다. 데이터뿐만 아니라 메서드도 추가하거나 변경할 수 있습니다.

JS OOP

생성자 함수

JS도 생성자 함수가 있다. ES5까진 따로 명시가 되어있지 않았을 뿐이다.

function Person(name) {
  this.name = name;
  this.greeting = function() {
    alert('Hi! I\'m ' + this.name + '.');
  };
}

생성자 함수는 클래스의 JS버전이다. 생성자함수는 프로퍼티와 메서드를 제어할 뿐 아무것도 리턴하지 않는다. 다만 class처럼 구분되는 키워드가 없으니 함수명을 대문자로 시작한다는 약속을 할 뿐이다.

자바스크립트 언어는 Marsupial 함수를 생성자 함수(new 키워드와 함께 사용하려고 작성한 함수)로 사용하라고 강요하지 않는다. 즉, new 키워드 없이 생성자 함수를 사용해도 이를 못하게 막을 보호 체계가 없다. 그래서 더러 개발자들은 파스칼 표기법(PascalCase)으로 생성자 함수를 따로 표기하여 구분하기도 한다.
더글라스 크락포드 같은 사람은 생성자 함수 호출 시 new를 빠뜨리면 ‘나쁜 일’이 생길 수 있으니 아예 생성자 함수를 쓰지 말라고 권한다(《자바스크립트 핵심 가이드(한빛미디어, 2008)》 참고).

function Marsupial(name, nocturnal) {
	if (!(this instanceof Marsupial)) {
	  throw new Marsupial(name, nocturnal);
	}
	this.name = name;
	this.isNocturnal = nocturnal;
}

저번 시간 공부한 것처럼 greeting같은 메서드에는 arrow function을 쓰면 안된다. 유의하자

프로토타입의 장점 사용하기

함수 프로퍼티를 생성자 함수의 프로토타입에 붙일 수도 있다. 생성자 함수의 프로토타입에 함수를 정의하면 객체 인스턴스를 대량 생성할 때 함수 사본 개수를 한 개로 제한하여 메모리 점유율을 낮추고 성능까지 높이는 추가 이점이 있다.

function Marsupial(name, nocturnal) {
if (!(this instanceof Marsupial)) {
  throw new Error(“이 객체는 new를 사용하여 생성해야 합니다”);
}
this.name = name;
this.isNocturnal = nocturnal;
}
// 각 객체는 isAwake 사본 하나를 공유한다.
Marsupial.prototype.isAwake = function(isNight) {
return isNight === this.isNocturnal;
}
var maverick = new Marsupial(‘매버릭’, true);
var slider = new Marsupial(‘슬라이더’, false);

var isNightTime = true;

console.log(maverick.isAwake(isNightTime)); // true
console.log(slider.isAwake(isNightTime)); // false

// 객체들은 isAwake의 단일 인스턴스를 공유한다.
console.log(maverick.isAwake === slider.isAwake); // true



![성능비교.png](https://velog.velcdn.com/post-images%2Fgtah2yk%2F4bc9e980-3aa5-11ea-842f-05c915697c3e%2Fimage.png)
### 고전적 상속 모델
만약 유대류(marsupial) 중 캥거루란 객체를 만들어 hop이란 메서드를 추가하고 싶어지면 어떻게 해야할까? marsupial에 추가하가하면 생성한 모든 인스턴스에 hop을 만들 것이므로 바람직하지 않다.

간단한 해결책은 Kangaroo 생성자함수를 만들고 hop메서드를 추가하고 prototype에 Marsupial을 바인딩하는 것이다.

```js
function Marsupial(name, nocturnal) {
	if (!(this instanceof Marsupial)) {
	  throw new Error(“이 객체는 new를 사용하여 생성해야 합니다”);
	}
	this.name = name;
	this.isNocturnal = nocturnal;
}

Marsupial.prototype.isAwake = function(isNight) {
	return isNight == this.isNocturnal;
};
 
function Kangaroo(name) {
	if (!(this instanceof Kangaroo)) {
	  throw new Error(“이 객체는 new를 사용하여 생성해야 합니다”);
	}
	this.name = name;
	this.isNocturnal = false;
}
 
Kangaroo.prototype = new Marsupial();
Kangaroo.prototype.hop = function() {
	return this.name + ” 가 껑충 뛰었어요!”;
};

var jester = new Kangaroo(‘제스터’);
console.log(jester.name);
 
var isNightTime = false;
console.log(jester.isAwake(isNightTime)); // true
console.log(jester.hop()); // ‘제스터가 껑충 뛰었어요!’
 
console.log(jester instanceof Kangaroo); // true
console.log(jester instanceof Marsupial); // true

고전적 상속 모델의 문제점

문제는 위와 같은 방법은 코드 반복과 메모리 점유를 피하기 어렵다는 점이다.

Kangaroo.prototype = new Marsupial();

Marsupial 생성자 함수에 인자가 하나도 없다. Kangaroo의 프로토타입을 지정하는 시점은 물론이고 Kangaroo 인스턴스가 만들어지기 전까지 어떤 인자가 올지 알 길이 없다.

프로토타입 지정 시 인자를 알 수 없으므로 Marsupial 함수의 프로퍼티 할당 작업은 Kangaroo 함수에서도 되풀이된다.

function Marsupial(name, nocturnal) {
	if (!(this instanceof Marsupial)) {
	  throw new Error(“이 객체는 new를 사용하여 생성해야 합니다”);
	}
	this.name = name;
	this.isNocturnal = nocturnal;
}
 
function Kangaroo(name) {
	if (!(this instanceof Kangaroo)) {
 	 throw new Error(“이 객체는 new를 사용하여 생성해야 합니다”);
	} 
	// name과 isNocturnal 프로퍼티를 반복해서 할당한다!
	this.name = name;
	this.isNocturnal = false;
}

해결책 : 함수형 상속

함수형 상속(functional inheritance)을 하면 데이터를 숨긴 채 접근을 다스릴 수 있다.

var AnimalKingdom = AnimalKingdom | | {};
 
AnimalKingdom.marsupial = function(name, nocturnal) {
 
	var instanceName = name,
 	   instanceIsNocturnal = nocturnal;
 
	return {
 	 getName: function() {
 	   return instanceName;
 	 },
 	 getIsNocturnal: function() {
 	   return instanceIsNocturnal;
 	 }
 	};
};
 
AnimalKingdom.kangaroo = function(name) {
	var baseMarsupial = AnimalKingdom.marsupial(name, false);
 
	baseMarsupial.hop = function() {
	  return baseMarsupial.getName() + ‘가 껑충 뛰었어요!;
	};
	return baseMarsupial;
};

var jester = AnimalKingdom.kangaroo(‘제스터’);
console.log(jester.getName()); // ‘제스터’
console.log(jester.getIsNocturnal()); // false
console.log(jester.hop()); // ‘제스터가 껑충 뛰었어요!’

ES6의 객체 생성

es6의 생긴 class를 사용하여 객체를 만들고 상속을 해보자.
새로 생긴 키워드는 class, constructor, static, extends, super이다.

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

안티패턴

프로토타입 체인 검색

프로토타입 체인에 걸친 속성 검색으로 성능에 나쁜 영향을 줄 수 있으며, 때때로 치명적일 수 있다. 또한 존재하지도 않는 속성에 접근하려는 시도는 항상 모든 프로토타입 체인인 전체를 탐색해서 확인하게 만든다.

객체의 속성에 걸쳐 루프를 수행 하는 경우 프로토타입 체인 전체의 모든 열거자 속성에 대하여 적용된다. 객체 개인 속성인지 프로토타입 체인상 어딘가에 있는지 확인하기 위해서는 Object.prototype에서 모든 오브젝트로 상속된 hasOwnProperty 메소드를 이용할 필요가 있다. 다음 코드를 통하여 구체적인 예를 확인하여 보자.

console.log(g.hasOwnProperty('vertices'));
// true

console.log(g.hasOwnProperty('nope'));
// false

console.log(g.hasOwnProperty('addVertex'));
// false

console.log(g.__proto__.hasOwnProperty('addVertex'));
// true

hasOwnProperty 메소드만이 속성을 확인하고 프로토타입 체인 전체를 훑지 않게 할 수 있다.

참고: undefined인지 여부만 확인하는 것으로는 충분하지 않다. 여전히 속성이 존재할 수도 있는데 단지 그 값에 undefined가 할당되어 있을 수도 있기 때문이다.

좋지 않은 사례: 기본 프로타입의 확장 변형

Object.prototype 혹은 빌트인 프로토타입의 확장은 종종 이용되지만 오용이다.

이 기법은 Monkey patching으로 불리며 캡슐화를 망가뜨린다. Prototype.js와 같은 유명한 프레임워크에서도 사용되지만, 빌트인 타입에 비표준 기능을 추가하는 것은 좋은 생각이 아니다.

유일하게 좋은 사용 예라면, 새로운 자바스크립트 엔진에 Array.forEach등의 새로운 기능을 추가하면서 빌트인 프로토타입을 확장하는 것 정도다.

문제

1. 무슨 차이가 있을까요?


//1 
var person1 = new Object();

person1.name = 'Chris';
person1['age'] = 38;
person1.greeting = function() {
  alert('Hi! I\'m ' + this.name + '.');
};

var person2 = new Object({
  name: 'Chris',
  age: 38,
  greeting: function() {
    alert('Hi! I\'m ' + this.name + '.');
  }
});

function Person1(name,age) {
  return {
  	name : name,
    age : age,
    greeting () {
      alert('Hi! I\'m ' + this.name + '.');
    }
  }
}


function Person2(name,age) {
  this.name = name;
  this.age = age;
  this.greeting = function() {
    alert('Hi! I\'m ' + this.name + '.');
  };
}

function Person3(name,age) {
  return Object.create({
  	name : name,
    age : age,
    greeting () {
      alert('Hi! I\'m ' + this.name + '.');
    }
  });
}

var person1 = Person('Chris',38);
var person2 = Person('Chris',38);
var person3 = Person('Chris',38);

참고자료

profile
ㅎㅎ

0개의 댓글