7 분 소요

프로토타입

자바스크립트는 프로토타입 개체와 체인으로 연결되어 있으며, 자신에게 해당 속성/메서드가 없는 경우 프로토타입 개체의 속성/메서드를 이용합니다. 마치 디자인 패턴중 Chain of Responsibility 처럼요.

image

다음과 같이 console.dir()을 사용하면 브라우저 개발자 도구에서 개체프로토타입 개체([[Prototype]])를 확인할 수 있습니다.

1
2
3
4
const user = {
    name: 'Lee'
};
console.dir(user);

image

[[Prototype]]__proto__ 속성으로 접근할 수 있으며, 초기에는 Object.prototype을 가리킵니다.

1
2
3
4
5
const user = {
    name: 'Lee'
};

console.log('프로토타입 user.__proto__ === Object.prototype', user.__proto__ === Object.prototype); // true

[[Prototype]]과 __proto__와 prototype과 constructor

생성자 함수로 생성한 개체프로토타입 개체를 체인으로 연결하기 위해 각 개체는 다음과 같이 구성됩니다.

1
2
3
4
5
6
7
8
function User(name) { // 생성자 함수
    this.name = name;
}

const user = new User('Lee'); // 생성자 함수로 생성한 개체

console.log('User.prototype과 user.__proto__ 는 동일한 프로토타입 개체를 가리킵니다.', User.prototype === user.__proto__); 
console.log('프로토타입 개체의 constructor는 생성자 함수입니다.', user.__proto__.constructor === User); 

image

프로토타입 체인을 이용한 속성 참조

다음 예는 개체의 속성/메서드에 접근할때 해당 속성/메서드가 없으면 프로토타입 개체의 속성/메서드에 접근하는 것을 보여줍니다.

Useraddr 속성이 없고 Userprototypeaddr속성이 추가되었으므로, user1.addr이나 user2.addrUser.prototype.addr에 접근하여 'Seoul'이 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function User(name) {
    this.name = name;
}
User.prototype.addr = 'Seoul'; // 프로토타입에 addr 속성을 추가합니다.

const user1 = new User('Kim');
const user2 = new User('Lee');

console.log('user1과 user2는 프로토타입이 같습니다.', user1.__proto__ === user2.__proto__); 

// user1.name, user2.name은 addr 속성이 없으므로 User.prorotype.addr을 사용합니다.
console.log('addr은 프로토타입의 속성입니다', user1.name === 'Kim' && user1.addr === 'Seoul'); 
console.log('addr은 프로토타입의 속성입니다', user2.name === 'Lee' && user2.addr === 'Seoul'); 

상기를 그림으로 보면 다음과 같습니다.

image

만약 프로토타입 개체의 속성값을 수정하면, 이를 참조하는 모든 개체에서 수정된 값을 사용하게 됩니다.

다음 예에서 user1.__proto__.addr를 수정했지만, user1user2는 같은 프로토타입 개체를 공유하므로 함께 수정되어 Pusan이 출력됩니다.

1
2
3
4
5
6
7
// __proto__의 addr을 수정합니다. 
// User.prototype.addr = 'Pusan'; 과 동일합니다.
user1.__proto__.addr = 'Pusan'; 

// user1, user2는 같은 __proto__를 공유하므로 Pusan 입니다.
console.log('user1.addr은 Pusan 입니다', user1.addr === 'Pusan'); 
console.log('user2.addr은 Pusan 입니다', user2.addr === 'Pusan'); 

image

만약 user2.addr = 'Seoul'와 같이 user2 개체에 값을 설정한다면, user2.__proto__.addr을 수정하는게 아니라 user2 개체addr속성을 추가하고 수정합니다.(개체 속성 추가/삭제 참고)

따라서, 이후로 user2.addr은 자신의 속성값을 리턴하고, user1.addr프로토타입 체인을 통해 프로토타입 개체의 값을 리턴하기 때문에, 서로 다른 값을 출력하게 됩니다.

1
2
3
user2.addr = 'Seoul'; // user2에 addr 속성을 추가합니다.
console.log('user1.addr은 Pusan 입니다', user1.addr === 'Pusan');
console.log('user2.addr은 Seoul 입니다', user2.addr === 'Seoul'); 

image

사용자가 만든 개체 뿐만 아니라 자바스크립트에서 기본으로 제공하는 기본 타입프로토타입 개체도 수정할 수 있습니다. 다음은 기본 타입Stringprototypeadd 메서드를 추가한 예입니다.

1
2
3
4
String.prototype.add = (a, b) => {
    return a + b;
};
console.log('기본타입인 String에 add 함수를 추가했습니다. String.add(1, 2)', 'test'.add(1, 2)); // 3

프로토타입은 여러 개체가 공통된 메서드를 사용할때 메서드의 중복 선언을 최소화 할 수 있어 좋긴 합니다만(생성자 함수 참고), 메서드 호출 과정에 있어 해당 메서드가 있는지 개체에서 싹 뒤진뒤 없다면 호출하는 것이기 때문에, 메서드 호출 부하가 있을 수 있습니다.

따라서 어지간하면(개체 지향의 다형성을 배제한다면), obj.func()형태의 메서드 호출보다는 func(obj)형태의 함수 호출 구문을 사용하시는게 좋습니다.

프로토타입 변경

프로토타입을 다른 개체로 변경할 수도 있습니다. 이때 constructor가 덮어써지므로, 다음 예의 #2와 같이 생성자 함수로 설정해 줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function User(name) { // #1. 생성자 함수입니다.
    this.name = name;
}
User.prototype = { // 프로토타입을 다른 개체로 변경할 수도 있습니다.
    constructor: User, // #2. #1의 생성자 함수로 설정합니다.
    addr: 'Seoul'
};

const user1 = new User('Kim');
const user2 = new User('Lee');

// user1.name, user2.name은 addr 속성이 없으므로 User.prototype.addr을 사용합니다.
console.log('addr은 프로토타입의 속성입니다', user1.name === 'Kim' && user1.addr === 'Seoul'); 
console.log('addr은 프로토타입의 속성입니다', user2.name === 'Lee' && user2.addr === 'Seoul'); 

// prototype 값을 수정하면, 생성한 모든 개체에 반영됩니다.
User.prototype.addr = 'Busan'; 
console.log(user1.name === 'Kim' && user1.addr === 'Busan');
console.log(user2.name === 'Lee' && user2.addr === 'Busan'); 

프로토타입을 이용한 메서드 구현

생성자 함수를 이용하면 메서드가 중복 생성된다고 언급했었는데요(개체의 생성자 함수 참고),

1
2
3
4
5
6
7
8
9
fuction User(name) {
    this.name = name; 
    this.getName = function() { // 생성하는 개체마다 메서드 선언이 중복됩니다.
        return this.name; 
    };
}

const user1 = new User('Kim'); 
const user2 = new User('Lee');

프로토타입을 이용하면, 메서드가 한개만 선언됩니다. 다만, 호출 부하가 생길 수 있죠.(프로토타입 체인을 이용한 속성 참조 참고)

메서드 중복 생성을 할 것이냐, 호출 부하가 큰 것을 사용할 것이냐 고민되실텐데, 그냥 가독성 좋은 것 쓰시고, 메모리가 부족해 지거나 속도가 느려진다면 개선을 고민하시기 바랍니다.

1
2
3
4
5
6
7
8
9
10
11
12
function User(name) {
    this.name = name;
}
User.prototype.getName = function() { // 프로토타입 개체에 메서드를 한번만 선언합니다.
    return this.name;
};

const user1 = new User('Kim');
const user2 = new User('Lee');

console.log('프로토타입 메서드 호출', user1.getName()); // Kim
console.log('프로토타입 메서드 호출', user2.getName()); // Lee

함수 호출 방식에 따른 this 변경

this를 사용하여 개체 자신을 나타낼 수 있습니다만, 함수 호출 방식에 따라 달라집니다. this가 달라지니 참 어처구니 없는 일(설계상의 오류라고 하네요)인데요, 주의해서 잘 사용해야 합니다.

  1. 일반 함수 : this는 전역 개체입니다.
  2. call(), apply(), bind() : this는 지정한 개체입니다.
  3. 개체 메서드 : this는 호출한 개체입니다.
  4. 생성자 함수 : this는 리턴하는 개체입니다.
  5. 중첩 함수 : this는 전역 개체입니다. 바깥 함수의 this를 사용하려면, #5-2와 같이 바깥 함수의 this를 저장하고 사용해야 합니다.
  6. 화살표 함수 : 화살표 함수this가 없습니다. 상위 환경의 this가 있다면 이를 따릅니다.

    • 메서드의 this는 호출한 개체이므로 #3-1에서 user1이었지만, #6-1에서는 메서드를 화살표 함수로 선언했으므로 상위 환경의 this를 따릅니다. 즉 전역 개체 입니다.

    • 중첩 함수this는 전역 개체이므로, #5-1에서 전역 개체 였지만, #6-2에서는 중첩 함수 arrow()화살표 함수로 선언했습니다. arrow()의 상위 환경이 function으로 선언된 getArrowNestName() 메서드이고, #3에서처럼 개체 메서드의 this는 호출한 개체이므로 getArrowNestName()thisuser4입니다. 따라서, #6-2 화살표 함수arrow()this는 상위 환경 getArrowNestName()thisuser4입니다.

    function으로 선언했는지, 화살표 함수로 선언했는지에 따라 this가 다릅니다. 혼란스럽죠. 따라서, this를 사용한다면, 메서드와 생성자 함수는 아예 화살표 함수로 선언하지 않는게 좋습니다.

  7. prototype : this는 호출한 개체입니다. 프로토타입 개체가 아니고요.
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
var name = 'Global'; // 전역 변수

// #1. 일반 함수에서의 this
function getName() {
    return this.name; // this는 전역 개체입니다. 
}
console.log('일반 함수에서 this는 전역 개체입니다', getName() === 'Global');  

// #2. this를 다른 개체에 연결
const obj = {
    name: 'Kim'
};
console.log('this를 다른 개체에 바인딩하여 사용할 수 있습니다', getName.call(obj) === 'Kim'); // getName 함수의 this를 obj에 바인딩합니다. 
console.log('this를 다른 개체에 바인딩하여 사용할 수 있습니다', getName.apply(obj) === 'Kim'); // call()과 유사하며, 추가 인수로 배열을 사용합니다.
const bindFunc = getName.bind(obj);
console.log('this를 다른 개체에 바인딩하여 사용할 수 있습니다', bindFunc() === 'Kim');

// #3. 개체 메서드에서의 this
const user1 = {
    name: 'Lee',
    getName: function() {
        return this.name; // 3-1. this는 user1입니다.
    }
};
console.log('개체 메서드에서 this는 user1입니다', user1.getName() === 'Lee'); 

// #4. 생성자 함수의 this
function User(name) {
    this.name = name;
    this.getName = function() {
        return this.name; // this는 생성자 함수가 리턴하는 개체입니다.
    };
}
const user2 = new User('Kim');
console.log('생성자 함수의 this는 리턴하는 개체입니다', user2.getName() === 'Kim'); 

// #5. 중첩 함수에서의 this
const user3 = {
    name: 'Park',
    getNestName: function() {
        function f() {
            return this.name; // #5-1. this는 전역 개체입니다. 
        }
        return f();
    },
    getName: function() {
        var that = this; // #5-2. 클로저를 활용하여 that으로 저장해 둡니다.
        function f() {
            return that.name; // that은 바깥 함수의 this입니다.
        }
        return f();
    }
};

console.log('중첩 함수에서 this는 전역 개체입니다', user3.getNestName() === 'Global'); 
console.log('that을 사용할 수 있습니다', user3.getName() === 'Park'); 

// #6 화살표 함수의 this
const user4 = {
    name: 'Park',
    getArrowName: () => { // 메서드를 화살표 함수로 선언했습니다.
        return this.name; // #6-1. this는 전역 개체입니다. 
    },        
    getNestName: function() {
        function f() {
            return this.name; // #5-1. this는 전역 개체입니다. 
        }
        return f();
    },
    getArrowNestName: function() {
        const arrow = () => {
            // #6-2. 화살표 함수에서는 this가 없어 상위 환경에서 찾습니다.
            // 상위 환경인 getArrowNestName()은 function으로 선언되었기 때문에 #3에서 처럼 this가 있고,
            // user4입니다.
            return this.name; 
        }
        return arrow();
    },
}
console.log('메서드를 화살표 함수로 선언했습니다. this는 상위 환경인 전역 개체입니다', user4.getArrowName() === 'Global');
console.log('중첩 함수에서의 this는 전역 개체입니다', user4.getNestName() === 'Global'); 
console.log('화살표 함수에서의 this는 상위 환경에서 찾습니다', user4.getArrowNestName() === 'Park');

// #7. prototype에서의 this
function PrototypeUser(name) {
    this.name = name;
}
PrototypeUser.prototype.getName = function() {
    return this.name; // 프로토타입 개체가 아닙니다. 해당 메서드를 호출한 개체입니다.
};
var user5 = new PrototypeUser('Kim');
var user6 = new PrototypeUser('Lee');

console.log('프로토타입 메서드에서 this는 user5입니다', user5.getName() === 'Kim'); // this는 user5입니다.
console.log('프로토타입 메서드에서 this는 user6입니다', user6.getName() === 'Lee'); // this는 user6입니다.

댓글남기기