자바스크립트에서 거의 모든 객체는 Object라는 객체의 인스턴스이다. 일반적으로 객체는 Object.prototype에서 프로퍼티와 메서드를 상속받는다. 즉, 대부분의 객체는 prototype을 원형으로 만들어진 것이다. 따라서 자바스크립트를 프로토타입 기반의 언어라고 부른다. 자바스크립트 함수도 객체이므로 기본적으로 prototype을 통해 만들어진다고 할 수 있다.
이러한 내용을 머릿속에 생각하고 함수를 만든다고 하자.
먼저, 함수를 선언하게 되면 함수 객체와 함께 해당 함수 이름의 prototype 객체가 별도로 생성된다. 아래 그림처럼 말이다.
F1함수 객체 내부에는 prototype 이름을 가진 프로퍼티가 존재하는데 이 프로퍼티 값은 F1 프로토타입 객체를 참조한다. 반대로 F1 프로토타입 객체에는 constructor 이름을 가진 프로퍼티가 존재하고 F1함수 객체로 접근할 수 있다.
즉, prototype 프로퍼티가 가리키는 객체를 그 함수의 프로토타입 객체라고 부른다.
// prototype 프로퍼티
function F1() {}
console.log(F1.prototype); // {constructor: ƒ}
프로토타입 객체의 프로퍼티를 추가하거나 프로토타입 객체의 constructor 프로퍼티를 통해 F1 함수 객체에 프로퍼티를 추가할 수 있다.
// 프로토타입 객체의 프로터티 추가
F1.prototype.humanName = "탱구링";
// 프로토타입 객체로 F1 함수의 프로퍼티 추가
F1.prototype.constructor.age = 30;
결과를 출력해보면 다음과 같다.
console.log("F1.prototype.humanName:", F1.prototype.humanName); // F1.prototype.humanName: 탱구링
console.log("F1.age:", F1.age); // F1.age: 30
console.log("F1.humanName:", F1.humanName); // F1.humanName: undefined
첫 번째 로그값을로그 값을 보면 F1 함수 프로토타입 객체의 프로퍼티로 잘 추가된 것을 알 수 있다. 두 번째 로그 값을 보면 F1 함수 객체의 프로퍼티로 잘 추가된 것을 알 수 있다. 그러나 세 번째는 undefined가 나왔다. 위에 설명한 것처럼 prototype과 constructor라는 두 객체의 프로퍼티들을 통해 서로 참조가 가능한 거면 세 번째 로그 값도 탱구링으로 나와야 하지 않겠냐는 의문이 생긴다.
이러한 결과가 나온 이유는 F1 함수 객체에 humanName이라는 프로퍼티를 추가한 것이 아니라 F1 함수의 프로토타입에 추가한 것이기 때문이다.
따라서, 이 F1 프로토타입 객체의 프로퍼티를 사용하고 싶으면 new 생성자로 생성한 인스턴스를 만들어야 한다.
let a = new F1();
a; // F1 {}
new 생성자를 통해 생성한 인스턴스인 a는 F1 함수 객체를 참조하며 이 F1 함수 객체는 프로퍼티로 __proto__ 가 있다.
바로 __proto__ 프로퍼티가 F1 함수의 프로토타입 객체를 나타낸다.
추가적으로 자바스크립트 객체의 원형은 Object라는 것을 다음 객체 상속 구조 그림에서 확인할 수 있다.
그렇기 때문에 이제는 아래와 같은 코드에서 값을 출력할 수 있다.
let a = new F1();
console.log("a.humanName:", a.humanName); // a.humanName: 탱구링
그러나 인스턴스가 __proto__ 프로퍼티를 통해 해당 프로토타입 객체를 참조할 수 있고 그 프로퍼티를 읽을 수 있지만, 그 프로퍼티에 새로운 값을 대입한다고 해서 기존 프로토타입 객체의 프로퍼티가 수정되는 것은 아니라는 것이다.
// 인스턴스에서 프로토타입 객체의 프로퍼티를 수정
a.humanName = "탱구링2";
console.log("prototype:", F1.prototype.humanName); // prototype: 탱구링
프로토타입 객체는 변화가 없고 오히려 아래와 같이 해당 인스턴스의 프로퍼티로 추가된 것을 알 수 있다.
console.log("a:", a.humanName); // a: 탱구링2
이처럼 인스턴스에서 프로토타입 객체의 인스턴스를 참조할 수 있는 상황을 '인스턴스가 프로토타입 객체를 상속하고 있다'라고 말하며 이는 프로토타입 체인이라는 메커니즘에 기반한다.
만약에 프로토타입 객체의 프로퍼티 값을 변경하거나 삭제하고 싶으면 결국 아래와 같이 함수 객체의 prototype 프로퍼티를 통해 접근해야한다.
// 수정
F1.prototype.humanName = "탱구링3";
console.log("prototype:", F1.prototype.humanName); // prototype: 탱구링3
// 삭제
delete F1.prototype.humanName; // true
console.log("prototype:", F1.prototype.humanName); // prototype: undefined
자바스크립트에서 생성자를 통해 인스턴스를 만들어 사용할 때 문제점이 있다. 바로 생성자 안에서 this 뒤에 메서드를 정의하게 되면 해당 생성자를 통해 만들어진 모든 인스턴스 수만큼 동일한 메서드가 생성된다는 것이다. 이는 그만큼 메모리 낭비가 발생한다. 이와 같은 문제를 프로토타입 객체를 통해 해결할 수 있다.
// 생성자에서 공통 메서드 정의하기
function Calc(a, b) {
this.a = a;
this.b = b;
this.sum = function () {
return this.a + this.b;
}
}
// 동일한 기능을 하는 메서드가 인스턴스 마다 생성
let calc1 = new Calc(1, 2); // Calc {a: 1, b: 2, sum: ƒ}
let calc2 = new Calc(3, 4); // Calc {a: 3, b: 4, sum: ƒ}
위 코드를 아래와 같이 프로토타입 객체를 사용해 변경할 수 있다.
// 프로토타입 객체를 통한 메서드 정의 후 인스턴스에서 참조하기
function Calc2(a, b) {
this.a = a;
this.b = b;
Calc2.prototype.sum = function () {
return this.a + this.b;
}
}
Calc2;
let calc3 = new Calc2(5, 6); // Calc2 {a: 5, b: 6}
let calc4 = new Calc2(7, 8); // Calc2 {a: 7, b: 8}
일반 인스턴스에는 따로 메서드가 정의되지 않고 아래와 같이 해당 함수의 프로토타입 객체 프로퍼티로 등록된다. 그리고 인스턴스에서 프로토타입 객체를 상속받아서 사용하는 것이다.
자바스크립트의 상속은 객체를 상속하며 프로토타입 체인으로 부르는 객체의 자료 구조로 구현되어있다. 상속을 통해 다른 언어와 마찬가지로 상속받은 대상의 프로퍼티나 메서드를 재사용할 수 있고 거기에 새로운 메서드를 추가해 확장된 객체를 생성할 수 있다. 자바의 override와 유사하다고 할 수 있다.
모든 객체는 아래와 같이 내부 프로퍼티로 [[Prototype]]을 가지고 있다. 아래 함수 객체의 prototype 프로퍼티와는 다른 프로퍼티이다.
아래는 일반 객체의 내부 구조이다.
ECMAScript 5까지는 사용자가 임의로 내부 프로퍼티인 [[Prototype]]을 참조할 수 없었지만 ECMAScript 6부터 __proto__라는 이름을 가진 프로퍼티에 [[Prototype]]의 값이 저장되어 참조할 수 있게 되었다. 사용자는 웹 브라우저 개발자 도구에서 쉽게 확인할 수 있다. 위 사진도 크롬 기반의 웹 브라우저 개발자 도구에서 확인한 것이다.
함수 객체의 prototype 프로퍼티에는 그 함수 이름으로 된 프로토타입 객체를 참조값으로 가지지만 객체의 __proto__ 프로퍼티에는 그 객체에게 상속을 해 준 부모 객체를 참조값으로 가진다. 위 사진에서 a라는 이름을 가진 객체는 Object를부모 객체로 갖고 있다.
console.log(a.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
이러한 특성을 이용해 자신의 부모 객체의 프로퍼티까지 올라가면서 검색하는 객체의 연결 고리를 프로토타입 체인이라고 한다.
let a = {
name: "탱구링",
age: 30,
sayHello: function () {
return `안녕 내 이름은 ${this.name}이고 나이는 ${this.age}야 잘 부탁해`;
}
};
let b = {
name: "준영"
};
b.__proto__ = a;
let c = {};
c.__proto__ = b;
c.sayHello(); // 결과는??
위 코드를 설명하면 다음과 같다.
객체 c는 sayHello 프로퍼티 가지고 있지 않지만 위 그림처럼 __proto__ 프로퍼티가 가리키는 객체로 거슬러 올라가 객체 a가 해당 프로퍼티를 소유하고 있는 것을 확인하고 사용한다.
이때 객체 c의 __proto__프로퍼티가 가리키는 객체는 객체 b이고 이 객체를 객체 c의 프로토타입이라고 부른다.
객체의 프로토타입은 Object.getPrototypeOf(변수명)으로 가져올 수 있다.
// 프로토타입 가져오기
function Calc(a, b) {
this.a = a;
this.b = b;
this.sum = function () {
return this.a + this.b;
}
}
let calc5 = new Calc(1, 2);
console.log(Object.getPrototypeOf(calc5)); // {constructor: ƒ}
생성자를 new 연산자로 호출하면 객체의 생성, 프로토타입의 설정, 객체의 초기화를 수행한다.
// new 연산자로 인스턴스 생성시 내부 프로세스
function Calc2(a, b) {
this.a = a;
this.b = b;
}
Calc2.prototype.sum = function () {
return this.a + this.b;
}
let a = new Calc2({x: 10, y: 30}, 20);
// 내부 프로세스
// 1. 빈 객체 생성
let newObj = {};
// 2. Calc2.prototype을 생성된 객체의 프로토타입으로 설정
// 만약 Calc2.prototype이 가리키는 값이 객체가 아니라면 Object.prototype을 프로토타입으로 설정
newObj.__proto__ = Calc2.prototype;
// 3. Calc2 생성자를 실행하고 newObj를 초기화. 이때 this는 1에서 생성한 객체로 설정됨
// 인수는 new 연산자와 함께 사용한 인수를 사용
Calc2.apply(newObj, arguments);
// 4. 완성된 객체를 결괏값으로 반환
return newObj;
함수 객체와 프로토타입 객체의 관계에서 인스턴스를 추가한 그림이다. new 연산자로 생성한 인스턴스는 생성될 때 내부 프로퍼티에 프로토타입 객체의 참조만 가지고 있고 생성자인 F1 함수 객체를 직접 참조할 수 있는 연결고리가 없다.
__proto__ 프로퍼티는 상속받은 객체의 참조를 가리킨다. 상속 받은 객체는 프로토타입이기 때문에 프로토타입 객체의 프로토타입은 Object.prototype이다. 아래 사진에서 f1 함수의 프로토타입 객체가 Object.prototype의 프로퍼티들을 가지고 있는 것을 알 수 있다.
인스턴스의 프로토타입은 new 연산자를 통해 생성자로 인스턴스를 생성할 때 같이 만들어진 프로토타입 객체이다.
따라서 인스턴스 생성 후 생성자의 prototype 프로퍼티 값을 다른 객체로 변경해도 인스턴스의 프로토타입은 변경되지 않는다. 즉, 인스턴스의 프로퍼티는 생성되는 시점의 프로토타입에서 상속받는다.
// 인스턴스 프로퍼티 생성 시간
function F1(a, b) {
this.a = a;
this.b = b;
}
let a = new F1(1, 2);
// 프로토타입 프로퍼티를 다른 객체로 변경
F1.prototype = {
constructor: F1,
sum: function () {
return this.a + this.b;
}
}
a.sum(); // Uncaught TypeError: a.sum is not a function
위와 같이 sum 메서드는 함수가 아니라고 나온다. 그러나 아래와 같이 생성자로 인스턴스를 생성할 때 프로토타입에 단순히 프로퍼티를 추가한다면 생성자와 인스턴스 사이에 연결고리가 끊기지 않는다.
let a = new F1(1, 2);
F1.prototype.sum = function () {
return this.a + this.b;
}
a.sum(); // 3
function F() {}
let obj = new F();
console.log(obj instanceof F); // true
console.log(obj instanceof Object); // true
console.log(obj instanceof Date); // false
function F() {}
let obj = new F();
console.log(F.prototype.isPrototypeOf(obj)); // true
console.log(Object.prototype.isPrototypeOf(obj)); // true
console.log(Date.prototype.isPrototypeOf(obj)); // false
Object 생성자는 내장 생성자로 일반적인 객체를 생성한다. 자세한 스펙은 아래 링크를 참고하자.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object
Object.prototype의 메서드는 모든 내장 객체로 전파되며 모든 인스턴스에서 사용 가능하다. 참고로 Object.prototype의 프로토타입은 null이다. 따라서 Object.prototype은 인스턴스에서 프로토타입 체인의 마지막 단계에 있는 객체를 의미한다.
console.log(Object.prototype.__proto__); // null
자세한 스펙은 아래 링크를 참고하자.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype
Object.create 메서드를 사용하면 명시적으로 프로토타입을 지정해서 객체를 생성할 수 있다. 이를 통해 가장 간단하게 상속을 표현할 수 있다.
[첫 번째 인수]
let apple = {
name: "사과",
brix: 10,
describe: function () {
return `과일 이름은 ${this.name}이고 당도는 ${this.brix}브릭스이다.`
}
};
apple.describe(); // "과일 이름은 사과이고 당도는 10브릭스이다."
let orange = Object.create(apple);
orange.name = "오렌지";
orange.describe(); // "과일 이름은 오렌지이고 당도는 10브릭스이다."
orange객체는 apple객체의 name, brix 프로퍼티와 describe 메서드를 상속받았다. 그러나 orange객체는 이미 name 프로퍼티를 가지고 있으므로 기존 자기 자신의 name 프로퍼티 값을 사용한다.
인수에 null을 넘기면 프로토타입이 없는 객체를 생성할 수 있다. 이를 활용하면 순수한 프로퍼티 집합(해시 테이블)을 만들 수 있다.
그러나 Object.prototype 조차 상속받지 못하였으므로 해당 prototype의 기본 메서드도 사용할 수 없다.
따라서 객체 리터럴로 생성한 빈 객체를 생성하려면 인수로 Object.prototype을 넘기면 된다.
[두 번째 인수]
첫 번째 인수와 함께 두 번째 인수에 생성할 객체에 프로퍼티를 추가해서 객체를 만들 수 있다.
// 두번째 인수 : 객체의 프로퍼티 지정
let team = {
teamName: "고척동 스쿠버다이빙",
introduce: function () {
return `역사를 자랑하는 ${this.teamName}에 초대합니다!`
}
};
let member = Object.create(team, {
name: {
value: "준영",
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 30,
writable: true,
enumerable: true,
configurable: true
},
sayHello: {
value: function () {
return `내 이름은 ${this.name}이고 ${this.age}살이야. 잘 부탁해`;
},
writable: true,
enumerable: false,
configurable: true
}
});
위 객체의 프로퍼티를 다음과 같이 출력해보았다.
console.log(member); // {name: "준영", age: 30, sayHello: ƒ}
console.log(member.teamName); // 고척동 스쿠버다이빙 => 프로토타입 객체 프로퍼티 상속
member.introduce(); // "역사를 자랑하는 고척동 스쿠버다이빙에 초대합니다!" => 프로토타입 객체 프로퍼티 상속
member.sayHello(); // "내 이름은 준영이고 30살이야. 잘 부탁해"
지금까지 자바스크립트 프로토타입과 관련된 내용을 살펴보았다. 자바스크립트의 뼈대가 되는 내용들이니 반복해서 봐야겠다.
[JavaScript] 자바스크립트 이벤트 객체 (0) | 2020.08.31 |
---|---|
[JavaScript] 자바스크립트 이벤트 핸들러와 리스너 (0) | 2020.08.30 |
[JavaScript] 자바스크립트 화살표 함수(Arrow Function) (0) | 2020.08.26 |
[JavaScript] 자바스크립트 클로저 (0) | 2020.08.24 |
[JavaScript] 자바스크립트 네임스페이스(Namespace, 이름 공간) (0) | 2020.08.22 |