JSnote8: Prototypes, inheritance
Prototypes, inheritance
Prototypal inheritance
user라는 객체를 만든 상태에서, 이것을 조금 변형해 admin, guest를 만들고 싶을 때와 같을 때 prototypal inheritance가 사용됨
[[Prototype]]
specification에 명시된 hidden property인 [[Prototype]]은 null 또는 다른 object(prototype이라 불림)의 reference를 저장함
한 객체로부터 property를 읽는데 없다면, JS는 자동으로 prototype 객체로부터 그것을 찾음
⇔ prototypal inheritance(프로토타입 상속)
| javascript.info 참고 |
[[Prototype]]은 숨겨져 있지만, 몇 가지 방법으로 설정 가능함
그 중 하나가 __proto__를 사용하는 것임:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
alert( rabbit.eats );
alert( rabbit.jumps ); // true
rabbit.walk(); // Animal walk
(*)와 같이 prototypal inheritance를 설정함
animal은rabbit의 prototype임 ⇔rabbit은animal을 상속받음
prototypal inheritance의 제한 사항:
- inheritance의 reference는 cycle을 만들면 에러남!
__proto__의 값은Object이거나null이어야 함![[Prototype]]에는 하나의 객체만 들어감
prototype chain이 길어질 수는 있음
__proto__는[[Prototype]]을 위한 getter/setter임
__proto__는[[Prototype]]와 동일한게 아니라,[[Prototype]]의 getter/setter 역할을 함
__proto__는 현재 잘 사용되지 않고, 호환성을 위해서 존재함
최신의 JS에서는Object.getPrototypeOf/Object.setPrototypeOf를 사용하길 권장함
__proto__는 specification에 의하면 browser에서만 지원되어야 하지만, 실제로는 서버를 포함해서 모든 환경에서 지원됨
__proto__가 더 직관적이기 때문에 이 article에서는 이것을 사용함
Writing doesn’t use prototype
prototype은 property를 읽을 때만 사용됨
property를 수정하거나 삭제하는 것은 직접 object를 사용해야 함:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let animal = {
eats: true,
walk() {
/* this method won't be used by rabbit */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
rabbit의 prototype인animal에walk()method가 있지만,rabbit.walk에 함수를 넣으면rabbit에walk()method가 추가됨
=>rabbit.walk()를 호출하면 prototype 대신rabbit에 있는 메소드가 호출됨
Accessor property는 getter/setter 안에 들어가는 property의 존재 여부에 따라서 다른 결과가 나옴:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // (**)
alert(admin.fullName); // Alice Cooper, state of admin modified
alert(user.fullName); // John Smith, state of user protected
(*)에서는admin에name/surname이 없기 때문에 prototype의 그것이 호출됨(**)에서 setter에 의해admin에name/surname이 추가됨
=> 마지막에는this의 값이 다르기 때문에 서로 다른 property가 호출됨!
The value of “this”
this는 prototype에 영향을 받지 않음
method가 object에 선언되든지 prototype에 선언되든지 상관없이, this는 항상 점 이전의 객체를 가리킴!
=> admin.fullName에서는 this가 admin을 가리킴
상속 관계가 복잡해도 이로 인해 편리하게 inherited method 실행 가능
즉, 상속하면 method는 공유되지만 object state는 공유되지 않음(method 내에서 property를 수정하면 this에 해당하는 객체의 상태가 변화함)
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)
- prototype에 정의된 메소드
sleep()을rabbit에서 호출했으므로this가rabbit을 가리켜rabbit에isSleeping이라는 property가 생김
| 위 예시의 object state javascript.info 참고 |
for...in loop
for...in은 상속받은 property도 순회함
obj.hasOwnProperty(key)로 obj가 소유한 key인지 판별할 수 있음:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
alert(Object.keys(rabbit)); // jumps
for(let prop in rabbit) alert(prop); // jumps, then eats
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
Object.keys(obj)는obj의 key만 리턴(key는 method도 포함됨!!)for...in은 inherited key도 리턴- 모든 literal object들은
Object.prototype을 상속받음
Object.prototype은[[Prototype]]이null임 Object.prototype의 모든 property들은enumerable: false이기 때문에for...in에 나열되지 않음
key나 value를 가져오는 거의 모든 method들은 상속받는 property를 제외함
Object.keys/values와 같은 method들은 inherited property를 무시함
즉, 호출된 객체 안의 key에 대해서만 동작하고, prototype의 key들은 가져오지 않음
Summary
| code | description |
|---|---|
obj.hasOwnProperty(prop) |
obj가 prop라는 key를 가지는지 판별 |
- hidden property
[[Prototype]]에 prototype의 reference가 저장됨__proto__를 사용해서 설정 가능
- 상속받은 property들은 수정할 수 없음(대입 연산을 하면 현재 객체에 새 property가 생김)
- 상속받은 method에서도
this는 항상 점 이전의 객체를 가리킴 - 모든 literal object들은
Object.prototype을 상속받음 Object.prototype의 property, method들은 모두enumerable: false임- key나 value를 가져오는 거의 모든 method들은 상속받는 property를 제외함
Tasks
- 최신의 엔진에서 property를 호출할 때 object에서 가져오나 prototype에서 가져오나 성능 차이가 없음
∵ 첫 번째 가져올 때 탐색하고 위치를 저장해놓기 때문에 다음 요청때는 바로 가져올 수 있음
F.prototype
constructor function을 이용해서 new F()와 같이 객체를 생성할 수 있음
만약 F.prototype이 객체라면, new operator로 생성되는 새 객체의 [[Prototype]]에 F.prototype의 reference를 저장함
F.prototype은 단순히 F의 property prototype을 의미하는 것이지, 다른 뜻은 없음!!!
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
alert( rabbit.eats ); // true
Rabbit.prototype = animal은new Rabbit이 생성될 때 그것의[[Prototype]]에animal을 대입해라는 의미임
| javascript.info 참고 |
1
가로 화살표는 평범한 property를 뜻하고 세로 화살표는 inheritance를 뜻함
F.prototype은new F로 호출될 때만 사용됨
new F로 객체가 하나 생성된 다음F.prototype이 바뀐다해도 생성된 객체의[[Prototype]]은 기존의 것으로 유지됨
reference를 복사하는 것이기 때문
Default F.prototype, constructor property
모든 함수들은 우리가 선언해주지 않아도 prototype property를 가짐
default prototype property는 { constructor: F }와 같은 객체임(F는 함수 자기 자신):
1
2
3
4
5
6
7
function Rabbit() {}
/* default prototype
Rabbit.prototype = { constructor: Rabbit };
*/
alert( Rabbit.prototype.constructor == Rabbit ); // true
따라서 new F로 생성된 객체들은 [[Prototype]] property를 통해서 F.constructor에 접근 가능함:
1
2
3
4
function Rabbit() {}
let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}
alert(rabbit.constructor == Rabbit); // true (from prototype)
| javascript.info 참고 |
아래와 같이 기존의 객체의 constructor를 생성자로 사용할 수도 있음:
1
2
3
4
5
6
7
8
function Rabbit(name) {
this.name = name;
alert(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");
- 객체의 생성자를 모르는데(e.g. 써드파티의 객체) 같은 종류의 객체를 생성하고 싶을 때 유용함
F.prototype이 constructor property를 가지는 객체이긴 하지만, 우리가 수정할 수 있기 때문에 없어질 수도 있음:
1
2
3
4
5
6
7
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false
constructor를 유지하면서 F.prototype을 수정하는 방법:
1
2
3
4
5
6
7
8
9
10
11
// (1)
function Rabbit() {}
Rabbit.prototype.jumps = true
// (2)
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
F.prototype전체를 덮어쓰지 않고 property를 하나씩 추가- 덮어쓰는 대신
constructor: Rabbits를 넣어줘야 함
Summary
F.prototype은new F()로 생성된 객체들의[[Prototype]]을 자신이 저장하고 있는 레퍼런스로 설정함F.prototype의 값은 객체이거나null이어야 함([[Prototype]]과 마찬가지)prototypeproperty는 생성자 함수의 property이고, 생성자 함수로 객체가 생성될 때만 위와 같은 기능을 함
cf. 보통의 객체에서는 기본적으로 생성되지도 않으며, 아무런 영향이 없음- 생성자 함수를 포함한 모든 함수들은
F.prototype = { constructor: F }를 가짐
=> 객체가 있다면 해당 객체의 생성자에도 접근할 수 있음- property를 추가할 때 덮어쓰지 않도록 주의해야 함!
Tasks
1
2
3
4
5
6
7
8
9
10
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
Rabbit.prototype = {};
alert( rabbit.eats ); // ?
true
기존의Rabbit.prototype객체는rabbit에서 reference를 사용하기 때문에Rabbit.prototype에서 참조하지 않아도 garbage collected되지 않음
Rabbit.prototype은{}의 reference로 바뀜
=> 이후에new Rabbit으로 만들어지는 객체들은{}의 reference를[[Prototype]]에 저장함- inherited property는 읽기만 가능, 수정, 삭제는 inherited property의 key를 호출하더라도 점 앞의 객체의 property로 간주함(e.g.
delete rabbit.eats;를 하더라도prototype의 property가 지워지지 않음, 대입도 마찬가지로,rabbit.eats가 만들어지고 거기에 값이 대입됨) -
rabbit.prototype는 존재하지 않음!!
∵F.prototype만 위와 같이 사용되기 때문!따라서,
Rabbit.prototype.eats를 상속받은 객체rabbit에서 접근하기 위해선rabbit.__proto__.eats와 같이 접근해야 함!! Rabbit.prototype={};를 실행해서 지운 다음rabbit.constructor()를 호출하면Rabbit의 prototype인Object.prototype의 constructor가 실행됨!
이 constructor는new Object(...)와 같이 사용하지 않음!!
new Object()로 empty object를 생성하기는 함
Native prototypes
모든 내장된 생성자 함수들은 prototype property를 사용함
Object.prototype
1
2
let obj = {};
alert( obj ); // "[object Object]"
obj는 empty object인데"[object Object]"로 바꿔주는toStringmethod이 어디서 나온 걸까?
obj = {}은obj = new Object()의 shorthand이기 때문에Object.prototype에 선언된toString을 사용한 것임
즉,obj.toString과obj.__proto__.toString과Object.prototype.toString은 모두 같은 레퍼런스임
| javascript.info 참고 |
Object.prototype의 [[Prototype]]은 null임!
Other built-in prototypes
Array, Date, Function 등의 다른 내장 객체들도 prototype에 method를 저장함
예를 들어, [1, 2, 3]으로 생성한 array도 new Array()를 사용하는데, constructor function과 다른 method들은 Array.prototype에 정의되어 있음
=> 모든 객체에 각각 method가 정의되어 있는 것보다 메모리를 절약할 수 있음
specification에 의하면, 모든 내장 prototype들은 Object.prototype을 상속받음
| javascript.info 참고 |
1
2
3
4
5
6
7
8
9
10
11
let arr = [1, 2, 3];
alert( arr.__proto__ === Array.prototype ); // true
alert( arr.__proto__.__proto__ === Object.prototype ); // true
alert( arr.__proto__.__proto__.__proto__ ); // null
alert(arr.toString === Array.prototype.toString); // true
alert(arr.toString === Object.prototype.toString); // false
Array.prototype,Object.prototype둘 다toStringmethod를 가지고 있지만,arr에서는 더 가까운Array.prototype.toString을 사용함
console.dir(obj)를 이용하면 obj의 prototype chain을 알 수 있음:
![]() |
|---|
| javascript.info 참고 |
Function도 마찬가지로, call, apply 같은 method들은 Function.prototype에 정의되어 있음
Primitives
String, Number, Boolean은 객체가 아니라 primitive지만, property에 접근할 때 임시적으로 wrapper object가 생성됨(String, Number, Boolean의 생성자 사용해서)
=> 그 wrapper object가 method를 제공한 다음 사라짐
위의 과정이 specification에 명시되어 있는 내용이고, 실제로 wrapper object들은 우리가 볼 수 없고, 대부분의 엔진들이 최적화함
이 wrapper object들의 method들도 prototype(String.prototype, Number.prototype, Boolean.prototype)에 정의되어 있음
null,undefined는 wrapper object가 없음
따라서 사용할 수 있는 method나 property, prototype도 존재하지 않음
Chainging native prototypes
Native prototype을 수정할 수 있음
예를 들어, String.prototype에 method를 추가하면, 그 method는 모든 String에서 사용 가능:
1
2
3
4
5
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
- 수정할 수는 있지만, 좋은 생각이 아님!
주의해야 할 점
prototype은 global하기 때문에 충돌이 일어나기 쉬움
예를 들어 두 라이브러리가String.prototype.show라는 method를 정의하면, 둘 중 하나는 덮어씌워짐
따라서, 일반적으로 native prototype을 수정하는 것은 좋지 않음
최근에는 polyfill을 목적으로 하는 native prototype 수정은 권장됨
(Remind: polyfill은 JS specification에는 명시되어 있지만, 특정한 엔진에서 지원되지 않는 내용을 위한 대체제를 만드는 것임)
Example
1
2
3
4
5
6
7
8
if (!String.prototype.repeat) {
String.prototype.repeat = function(n) {
// repeat the string n times
return new Array(n + 1).join(this);
};
}
alert( "La".repeat(3) ); // LaLaLa
- method가 지원되지 않으면 polyfill을 넣음
Borrowing from prototypes
native prototype의 method를 주로 빌림:
1
2
3
4
5
6
7
8
9
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hello,world!
- 이전 article에서는 empty array를 이용해서
[].join과 같이 method를 빌렸지만, native prototype을 이용해서 method를 빌릴 수도 있음join이 index,length만 사용하기 때문에 array-like object에도 사용할 수 있음
이렇게 data type이 맞지 않아도 사용할 수 있는 경우가 많음
obj.__proto__를Array.prototype으로 설정해서obj.join으로 바로 사용해도 됨
이 방법은 이미obj가 다른 객체를 상속하고 있을 경우 사용할 수 없음
∵ 한 객체는 동시에 하나의 객체로부터만 상속받을 수 있기 때문!
Summary
- 모든 내장 객체들은 같은 패턴을 따름
- method들은 모두 prototype에 저장됨
- 객체 자체는 값(array item, date 등)이나 property만 저장함
- primitive도 method를 wrapper object의 prototype에 저장함
cf.null,undefined는 wrapper object가 없음 - built-in prototype도 변경할 수 있지만, polyfill을 할 때만 권장됨
Tasks
Decorator 역할을 하는 method를 prototype에 선언
1
2
3
4
5
6
7
8
9
function f() {
alert("Hello!");
}
Function.prototype.defer = function(n) {
setTimeout(this, n);
};
f.defer(1000); // shows "Hello!" after 1 second
Function에서this가 함수 자체이기 때문에 가능함
decorating defer를 prototype에 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// (1)
Function.prototype.defer = function(ms) {
let cthis=this;
return function() {
setTimeout(() => cthis.apply(cthis, arguments), ms);
};
};
// (2)
Function.prototype.defer = function(ms) {
return (function() {
setTimeout(() => this.apply(this, arguments), ms);
}).bind(this);
};
// (3)
Function.prototype.defer = function(ms) {
return (...args) => setTimeout(() => this.apply(this, args), ms);
};
function f(a, b) {
alert( a + b );
}
f.defer(1000)(1, 2); // shows 3 after 1 second
(1)은 정석대로 wrapper를 function expression으로 정의하고,cthis로 context를 넘겨줌(2)는 context를 bind해서 따로 넘겨주지 않아도 됨(3)은 wrapper를 rest parameter를 받는 arrow function으로 구현함
arrow function들은 outer context를 사용하기 때문에 nested arrow function은 모두 같은 context를 사용하는 것을 알 수 있음
Prototype methods, objects without proto
__proto__ 대신에 아래와 같은 최신 method들을 사용해야 함:
Object.create(proto[, descriptors]):proto를[[Prototype]]으로 하고descriptor를 적용한 객체 생성Object.getPrototypeOf(obj):obj의[[Prototype]]리턴Object.setPrototypeOf(obj, proto):obj의[[Prototype]]을proto로 설정
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let animal = {
eats: true
};
let rabbit = Object.create(animal);
/*
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
*/
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {}
- 주석과 같이
Object.create에 descriptor를 넣어서 적용 가능함
아래와 같이 for...in으로 property를 복사하는 것보다 더 효과적인 복사를 수행할 수 있음:
1
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
- shallow copy지만,
[[Prototype]], setter/getter, property flag까지 복사함
Brief history
역사적인 이유로, [[Prototype]]에 접근하는 방법은 많음:
- 생성자 함수의
F.prototype은 오래 전부터 사용되어 왔음 - 2012년에
Object.create가 표준으로 정의되어 prototype을 사용해서 객체를 생성할 수 있게 됨
하지만 prototype에 접근하거나 설정할 수 있는 기능을 제공하지 않음
=> browser들이 비표준인__proto__를 구현함 - 2015년에
Object.setPrototypeOf,Object.getPrototypeOf가 표준에 추가됨
__proto__와 같은 역할이지만, 이미__proto__가 사실표준으로 굳어진 상태라__proto__도 표준의 부록에 추가됨
현재 생성된 객체의
[[Prototype]]은 변경하지 않는게 좋음
[[Prototype]]을 언제든지 get/set할 수 있지만, 보통 객체가 생성될 때 한 번만 설정한 후 건드리지 않음JS 엔진들은 이렇게 한 번만 설정하는 것에 최적화되어 있음
Object.setPrototypeOf나obj.__proto__를 사용해서 prototype을 바꾸는 것은 최적화되지 않아서 매우 느림
따라서 속도가 중요할 때는 prototype을 변경하지 않는게 좋음!
“Very plain” objects
computed property는 []를 사용해서 호출할 수 있음
하지만 이 방법은 "__proto__"를 제외한 나머지 property에 대해 적용 가능함:
1
2
3
4
5
6
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // [object Object], not "some value"!
obj["__proto__"]는 대입 연산이 무시됨__proto__property는null이나 객체를 참조해야 하기 때문
원래 의도는
__proto__라는 property에"some value"를 저장하는 것이지만, JS가 이미__proto__를 사용하고 있기 때문에 의도한대로 실행되지 않은 것임 + 객체가 대입된다면 대입 연산이 적용되어 오류가 발생함
현재는 이미__proto__가 property가 아닌Object.prototype에 관한 accessor property로 구현되어 있으므로 객체를 대입해도 잘못된 대입을 피할 수 있음toString과 같은 내장 method도 위와 같이 잘못된 대입이 일어날 수 있음
=> 그냥 객체 대신에Map을 사용하면 해결됨
또는 아래 코드와 같이 [[Prototype]]을 Object.prototype이 아닌 null로 명시해서 위 문제를 해결할 수 있음:
1
2
3
4
5
6
let obj = Object.create(null);
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // "some value"
obj는[[Prototype]]에null이 들어간 상태임
=>__proto__getter, setter가 상속되지 않았으므로__proto__도 평범한 property로 사용됨- 이런 객체를 very plain object 또는 pure dictionary object라고 부름
- 상속받은 것이 없기 때문에
toString같은 다른 내장 method도 사용할 수 없음
=>alert(obj)가 에러를 반환함 - very plain object를 associative array로 사용할 때는 문제가 되지 않음
Object.f()와 같은 method들은 prototype안에 선언된게 아니기 때문에 very plain object에도 사용할 수 있음!
Summary
| code | description |
|---|---|
Object.create(proto[, descriptors]) |
proto를 [[Prototype]]으로 하고 descriptor를 적용한 객체 생성 |
Object.getPrototypeOf(obj) |
obj의 [[Prototype]] 리턴 |
Object.setPrototypeOf(obj, proto) |
obj의 [[Prototype]]을 proto로 설정 |
- 사용자의 입력을 key로 사용하는 경우
__proto__가 호출될 수도 있기 때문에 위험함
=> 이런 경우 very plain object를 사용하거나Map을 사용하는게 좋음 __proto__는[[Prototype]]의 getter/setter이고,Object.prototype에 선언되어 있음
다른 method들:
Object.keys/values/entries(obj): 각각의 enumerable의 array 리턴Object.getOwnPropertySymbols(obj): symbolic key의 array 리턴Object.getOwnPropertyNames(obj): string key의 array 리턴Reflect.ownKeys(obj): 모든 key의 array 리턴obj.hasOwnProperty(key):key라는obj소유의 property가 있는지 판별
object property를 리턴하는 메소드들은 자신 소유의 property만 탐색함
상속받은 property까지 보려면 for...in을 사용하는게 좋음
Tasks
- descriptor를 사용해서 property를 생성할 때 생략된 flag들은 모두
false로 초기화됨!
1
2
3
4
5
6
7
8
9
10
11
12
13
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.sayHi = function() {
alert( this.name );
}
let rabbit = new Rabbit("Rabbit");
rabbit.sayHi(); // Rabbit
Rabbit.prototype.sayHi(); // undefined
Object.getPrototypeOf(rabbit).sayHi(); // undefined
rabbit.__proto__.sayHi(); // undefined

Leave a comment