제대로 배우자, 자바스크립트(Javascript) #5 – 생성자와 프로토타입

자바스크립트에는 클래스가 없기 때문에 똑같은 특성을 갖춘 여러 객체를 만들 때는 생성자 프로토타입을 사용한다.

I. 생성자

생성자(constructor)는 객체를 만들 때 new 연산자와 함께 사용하는 함수다. 생성자를 사용할 때 얻을 수 있는 장점은 같은 생성자를 사용해 만드 객체는 같은 프로퍼티와 메소드를 갖는다는 것이다.

생성자 함수이므로 함수와 같은 방식으로 정의한다. 다른 점이 있다면 평범한 함수와 구분하기 위해 생성자의 이름은 보통 대문자로 시작한다는 것이다.

인스턴스 타입은 instanceof를 통해 확인할 수 있다.

function Person() { }
var person = new Person();
console.log(person instance Person);

인스턴스 타입은 모든 객체 인스턴스에 자동으로 추가되는 constructor 프로퍼티로도 확인할 수 있다. 이 프로퍼티는 인스턴스를 생성할 때 사용했던 생성자 함수를 참조한다. 기본 객체의 경우 constructor는 Object를 참조한다.

생성자를 호출할 때는 반드시 new 연산자를 사용해야 한다. 그렇지 않음녀 객체가 생성되는 대신 전역 컨텍스트 객체(global object)가 의도치 않게 수정되는 일이 발생할 수 있다.

끝으로 생성자를 사용하면 같은 프로퍼티를 가진 객체 인스턴스를 여러 개 만들 수 있지만 생성자만으로는 코드 중복까지 제거할 수 없다. 예를 들어, 생성자에 어떤 메소드가 있다면 생성된 각각의 인스턴스는 완전히 똑같이 작동하는 함수를 별개로 갖고 있게 된다. 모든 인스턴스가 하나의 메소드를 공유하도록 만들려면 프로토타입을 사용해야 한다.

II. 프로토타입

프로토타입(prototype)은 객체를 위한 레시피다.

모든 인스턴스는 [[Prototype]] 이라는 내부 프로퍼티를 통해 프로토타입의 변화를 추적한다. 이 프로퍼티는 인스턴스가 사용하고 있는 프로토타입 객체를 가리킨다. new 연산자를 사용해 새 객체를 생성할 때 생성자의 prototype 프로퍼티가 새로 생성된 객체의 [[Prototype]] 프로퍼티에 할당된다. 자바스크립트에서는 프로토타입을 사용해 코드 중복을 줄일 수 있다.

[[Prototype]] 프로퍼티의 값은 Object.getPrototypeOf() 메소드를 객체에 사용하면 읽을 수 있다.

var object = {};
var prototype = Object.getPrototypeOf(object);

console.log(prototype === Object.prototype);

일반 객체의 [[Prototype]]은 언제나 Object.prototype을 참조한다.

일부 자바스크립트 엔진에서는 모든 객체가 __proto__라는 프로퍼티를 가지고 있는데, 이 프로퍼티를 사용하면 [[Prototype]] 프로퍼티를 읽거나 쓸 수 있다. 파이어폭스, 사파리, Node.js는 모두 __proto__ 프로퍼티를 지원하고 있으며 ECMAScript 6에서는 __proto__에 대한 표준화 작업을 진행하고 있다.

또한 모든 객체에서 사용 가능한 isPrototypeOf() 메소드를 사용하면 어떤 객체가 다른 객체의 프로토타입인지 확인할 수 있다.

var object = {};

console.log(Object.prototype.isPrototypeOf(object));

자바스크립트는 객체의 프로퍼티 값을 다음과 같이 가져온다.

객체 프로퍼티의 값을 가져올 때 자바스크립트 엔진은 먼저 해당 이름을 가진 고유 프로퍼티가 있는지 확인한다. 고유 프로퍼가 있다면 프로퍼티의 값을 반환한다. 고유 프로퍼티를 찾지 못했다면 [[Prototype]] 객체에서 해당 프로퍼티를 검색하고, 해당 이름을 가진 프로토타입 프로퍼티가 있다면 그 프로퍼티의 값을 반환한다. 만약 Prototype 프로퍼티에서도 이름을 찾을 수 없다면 undefined를 반환한다.

III. 생성자와 프로토타입 함께 사용하기

여러 객체에 공유된다는 프로토타입의 특성을 활용하면 타입이 같은 모든 객체가 같이 사용할 메소드를 한 번만 정의해도 된다.

프로토타입 또한 객체 리터럴로 초기화 할 수 있다. 이 때 주의할 점이 있다. 객체 리터럴 표기법을 사용해서 초기화하면 constructor 프로퍼티도 바뀐다. 이 문제는 constructor 프로퍼티가 객체 인스턴스가 아닌 프로토타입에 정의되어 있어서 발생한다. constructor 프로퍼티는 함수를 만들 때 함수의 prototype 프로퍼티에 정의되면서 만들어진 함수를 참조한다. 이 문제를 우회하려면 constructor 프로퍼티의 값을 아래와 같이 적절하게 설정하면 된다.

function Dream(goal) {
this.goal = goal;
};

Dream.prototype = {
    constructor: Dream,

sayDream: function() {
console.log(this.goal);
},

toString: function() {
return “[Dream ” + this.goal + “]”;
}
};

var dream = new Dream(“Do what I love and love what I do!”);

console.log(dream instanceof Dream);
console.log(dream.constructor === Dream);
console.log(dream.constructor === Object);

생성자, 프로토타입, 인스턴스의 관계에서 아마 가장 흥미로운 부분은 인스턴스와 생성자 사이에 직접적인 연결이 없다는 것이다. 하지만 인스턴스와 프로토타입, 프로토타입과 생성자는 서로 직접적으로 연결되어 있다.

생성자, 프로토타입, 인스턴스

이러한 관계 때문에 인스턴스와 프로토타입 사이의 연결이 끊어지면 생성자와 인스턴스 간의 연결도 끊어지게 된다.

IV. 프로토타입 체이닝

프로토타입에는 언제든 새 프로퍼티나 메소드를 추가할 수 있으며 이러한 변화는 이미 만들어진 인스턴스에도 즉시 반영된다.

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

Person.prototype = {
constructor: Person,

sayName: function() {
console.log(this.name);
},

toString: function() {
return “[Person ” + this.name + “]”;
}
};

var person1 = new Person(“Sung Am YANG”);
var person2 = new Person(“Ryan YANG”);

console.log(“sayHi” in person1);
console.log(“sayHi” in person2);

Person.prototype.sayHi = function() {
console.log(“Hi”);
}

person1.sayHi();
person2.sayHi();

V. 내장 객체의 프로토타입

프로토타입을 사용하면 자바스크립트 엔진에서 기본으로 제공하는 내장 객체도 수정할 수 있다.

Array.prototype.sum = function() {
return this.reduce(function(previous, current) {
return previous + current;
});
};

var numbers = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var result = numbers.sum();

console.log(result);

아래는 원시 래퍼 타입의 프로토타입에 새로운 기능을 추가하는 코드다.

String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.substring(1);
};

var message = “hello world!”;
console.log(message.capitalize());

VI. 요약

생성자는 평범한 함수지만 new 연산자와 함께 호출된다는 점이 다르다. 동일한 프로퍼티를 가진 객체를 여러 개 작성하고 싶을 때는 직접 생성자를 정의해서 사용하면 된다. 객체를 만들 때 사용한 생성자는 instanceof 연산자를 사용하거나 constructor 프로퍼티를 비교해서 확인할 수 있다.

모든 함수에는 prototype 프로퍼티가 있는데, 이 프로퍼티는 특정 생성자를 사용해 생성된 객체들이 공유할 프로퍼티를 정의한다. 일반적으로 프로토타입에는 공동으로 사용할 메소드와 원시 값 프로퍼티를 정의하고 그 밖의 프로퍼티는 생성자 안에서 정의한다. constructor 프로퍼티는 모든 객체 인스턴스가 공유하는 프로퍼티이므로 프로토타입에 정의되어 있다.

객체의 프로토타입은 내부적으로 [[Prototype]]이라는 프로퍼티 참조 값에 저장된다. 프로토타입을 변경하면 변경된 사항이 모든 인스턴스에 적용된다. 객체의 프로퍼티에 접근하면 처음에는 해당 이름을 가진 고유 프로토타입에서 찾는다. 이 같은 탐색 방식 때문에 객체 인스턴스에 참조하는 프로토타입이 변경되면 변경사항이 인스턴스에도 즉시 반영된다.

내장 객체도 프로토타입을 갖고 있으며 이를 수정해 기능을 추가할 수 있지만 실제 제품에는 사용하지 않는 게 좋다.

제대로 배우자, 자바스크립트(Javascript) #4 – 객체

JavaScript 객체는 동적이라 언제든지 바뀔 수 있다는 것을 기억하자. 반면에 Java 같은 언어에서는 한 번 정의한 클래스는 객체를 수정할 수 없도록 만든다.

I. 프로퍼티 정의

객체를 만드는 방법에는 Object 생성자를 사용하는 방법과 객체 리터럴을 사용하는 방법이 있다.

  1. Object 생성자 사용

var obj = new Object();
obj.name = “라이언”;

2. 객체 리터럴 사용

var obj = {
name: “라이언”
};

JavaScript는 프로퍼티를 처음 추가할 때 객체에 있는 [[Put]]이라는 내부 메소드를 호출한다. [[Put]] 메소드는 객체에 프로퍼티 저장 공간을 생성한다. 이 과정은 해시 테이블에 처음 키를 추가하는 것과 비슷하다. 이 동작은 수행하면 초기값은 물론 프로퍼티의 속성도 설정한다.

[[Put]]을 호출하면 객체에 고유 프로퍼티(own property)가 만들어진다. 고유 프로퍼티는 객체의 특정 인스턴스에 속해있으며 인스턴스에 바로 저장된다. 또한 프로퍼티에 동작을 수행하려면 소유 객체를 거쳐야 한다. 참고로 프로토타입 프로퍼티(prototype property)는 특정 객체에 소유되지 않아 동일한 객체가 공유하는 프로퍼티를 뜻한다.

기존 프로퍼티에 새 값을 할당하면 [[Set]]이 호출된다. [[Set]]은 프로퍼티의 현재 값을 새 값으로 교체한다.

II. 프로퍼티 탐지

프로퍼티의 존재여부를 탐지하려면 in 연산자를 사용한다. 이 방식을 사용하면 실제 프로퍼티의 값을 확인하지 않아 성능에 영향을 끼치지 않는다.

console.log(“name” in person1);
console.log(“age” in person1);

참고로 in 연산자는 고유 프로퍼티(own property)와 프로토타입 프로퍼티(prototype property)을 모두 찾기 때문에 고유 프로퍼티를 확인하려면 hasOwnProperty() 메소드를 활용한다.

프로퍼티 검토 in 연산자

III. 프로퍼티 제거

객체에서 프로퍼티를 완전히 제거할 때는 delete 연산자를 사용한다. 객체 프로퍼티에 delete 연산자를 사용하면 내부적으로 [[Delete]]가 호출된다. 이 동작은 해시 테이블에서 키/값 쌍을 없애는 것으로 볼 수 있다. delete 연산자는 호출 성공 시 true를 반환한다.

IV. 열거

객체를 추가하는 프로퍼티는 기본적으로 열거(enumerable)가 가능하다. 즉 for-in 반복문을 사용해 훑을 수 있다. 열거 가능 프로퍼티에는 [[Enumerable]]이라는 내부 속성이 true로 설정되어 있다.

for-in 반복문에서는 열거 가능한 프로토타입 프로퍼티도 반환하지만 Object.keys()는 고유 프로퍼티만 반환한다. 참고로 네이티브 프로퍼티는 대부분 열거가능하지 않다.

모든 프로퍼티를 열거할 수 있는 것은 아니다. 객체의 네이티브 메소드는 대부분 [[Enumerable]] 속성이 false로 설정되어 있다. 특정 프로퍼티가 열거 가능한지 확인할 때는 propertyIsEnumerable() 메소드를 사용하면 된다.

V. 프로퍼티 종류

프로퍼티에는 값을 포함하고 있는 데이터 프로퍼티(Data Property)와 값을 포함하지 않는 대신 프로퍼티를 읽었을 때 Getter와 Setter를 포함하는 접근자 프로퍼티(Accessor Property)가 있다.

VI. 프로퍼티 속성

데이터 프로퍼티와 접근자 프로퍼티의 공통 속성은 [[Enumerable]], [[Configurable]] 속성이다. 프로퍼티 속성은 Object.defineProperty() 메소드로 변경할 수 있는데 이 메소드는 아래 인수 세 개를 전달한다.

  1. 프로퍼티를 소유하고 있는 객체
  2. 프로퍼티 이름
  3. 설정할 프로퍼티 속성 값을 갖고 있는 프로퍼티 서술자(Property Descriptor)

var person = { name: “Ryan” };

Object.defineProperty(person, “name”, { enumerable: false });

데이터 프로퍼티에는 접근자 프로퍼티에는 없는 [[Value]], [[Writable]] 속성이 있다. [[Value]] 속성은 프로퍼티의 값을 저장하고 있고 [[Writable]] 속성은 프로퍼티에 값을 쓸 수 있는지 여부를 설정한다.

반면에 접근자 프로퍼티에는 [[Get]], [[Set]] 속성이 있다.

여러 프로퍼티를 설정하려면 Object.defineProperties() 메소드를 사용한다. 이 메소드는 아래 두 개 인수를 전달한다.

  1. 대상 객체
  2. 정의할 프로퍼티의 정보를 담고 있는 객체

var person = {};

Object.defineProperties(person, {
_name: {
value: “Ryan”,
enumerable: true,
configurable: true,
writable: true
},

_name: {
get: function() {
console.log(“name 읽는 중”);
return this._name;
},

set: function(value) {
console.log(“name의 값을 %s로 설정하는 중”, value);
this._name = value;
}
});

Object.defineProperties()를 사용하면 프로퍼티를 몇 개든 정의할 수 있으며 기존 프로포티 수정과 새 프로퍼티 추가를 동시에 수행할 수 있다.

프로퍼티 속성을 가져오려면 Object.getOwnPropertyDescriptor() 메소드를 사용한다. 이 메소드는 아래처럼 인수가 두 개다.

  1. 대상 객체
  2. 정보를 가져올 프로퍼티의 이름

VII. 객체 수정 방지

객체에는 객체의 동작을 제어하는 내부 속성이 있다. 그 중 하나인 [[Extensible]]은 객체 자체의 수정 가능 여부를 가리키는 논리값을 갖고 있다. 객체를 수정할 수 없도록 만드는 방법은 크게 세 가지가 있다.

  1. 확장 방지 – Object.preventExtensions(), Object.isExtensible() 사용
  2. 객체 봉인 – Object.seal(), Object.isSeal() 사용
  3. 객체 동결 – Object.freeze(), Object.isFreeze() 사용

VII. 요약

자바스크립트 객체는 프로퍼티가 키/값 쌍으로 되어있는 만큼 해시 맵에 빗대어 생각하면 이해하기 쉽다. 객체 프로퍼티에 접근할 때는 점 표기법 또는 각괄호 표기법 중 무엇을 사용해도 상관없다. 프로퍼티에 값을 할당하면 언제든 객체에 새 프로퍼티를 추가할 수 있으며 delete 연산자를 사용하면 언제든 프로퍼티를 제거할 수 있다. 프로퍼티의 존재 여부는 프로퍼티 이름과 객체를 in 연산자와 함께 사용하면 알 수 있다. 이때 고유 프로퍼티만 확인하고 싶다면 모든 객체에 다 포함되어 있는 hasOwnProperty()를 사용하면 된다. 모든 객체 프로퍼티는 기본적으로 열거 가능하다. 열거 가능하다는 말은 [[Enumerable]] 내부 속성 값이 true 여서 for-in 반복문이나 Object.keys()를 사용할 때 볼 수 있다는 뜻이다.

프로퍼티는 데이터 프로퍼티접근자 프로퍼티로 나눌 수 있다. 데이터 프로퍼티는 값을 담아두기 위한 공간으로 값을 읽거나 저장할 수 있다. 데이터 프로퍼티에 함수를 저장하면 이 프로퍼티는 객체의 메소드로 취급된다. 접근자 프로퍼티는 Getter와 Setter를 조합하여 특정 동작을 수행한다. 데이터 프로퍼티와 접근자 프로퍼티 모두 객체 리터럴 표기법을 사용해 만들 수 있다.

모든 프로퍼티에는 관련된 내부 속성이 몇 가지 있으며 이러한 속성이 프로퍼티가 어떻게 동작하는지 결정한다. 데이터 프로퍼티와 접근자 프로퍼티에는 둘 다 [[Enumerable]]과 [[Configurable]]이라는 속성이 있다. 데이터 프로퍼티에는 [[Writable]]과 [[Value]]라는 속성이 추가로 있는 반면, 접근자 프로퍼티에는 [[Get]]과 [[Set]] 속성이 더 있다. 모든 프로퍼티의 [[Enumerable]]과 [[Configurable]] 속성은 true가 기본 값이며 데이터 프로퍼티의 [[Writable]] 속성 역시 true가 기본 값이다. Object.defineProperty() 또는 Object.defineProperties()를 사용하면 속성을 변경할 수 있으며 Object.getOwnPropertyDescriptor()를 사용하면 설정된 속성 값을 가져올 수 있다.

객체의 프로퍼티를 수정할 수 없게 만드는 방법은 세 가지가 있다. Object.preventExtensions()를 사용하면 객체에 프로퍼티를 추가할 수 없다. Object.seal() 메소드를 사용하면 봉인된 객체를 만들 수 있는데 봉인된 객체는 확장 불가능해지며 객체의 프로퍼티는 설정 불가능해진다. Object.freeze()를 사용해 객체를 동결하면 객체가 봉인되는 것은 물론 데이터 프로퍼티에 값을 저장할 수도 없게 된다. 확장 불가능한 객체를 다룰 때는 잘못된 방식으로 객체에 접근했을 때 에러가 발생할 수 있도록 항상 엄격한 모드를 사용하는 것이 좋다.