Skip to content

Commit e2dd807

Browse files
committed
✏️ 5회차 문서 추가 : this
1 parent bb2f7bd commit e2dd807

1 file changed

Lines changed: 389 additions & 0 deletions

File tree

Javascript/this.md

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
객체는 상태(state)를 나타내는 프로퍼티(Property)와 동작(behavior)을 나타내는 메서드(Method)를 하나의 논리적 단위로 묶은 복합적 자료구조라 했죠? 이때 메서드가 내부 프로퍼티를 제어하기 위해선 자신이 속한 객체를 가리키는 `무언가`가 존재해야 합니다. 저희는 이 무언가인 **this**에 대해 알아봅니다.
2+
3+
## this 키워드
4+
> 자기 참조 변수(self-referencing variable)로 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리킵니다.
5+
6+
잠깐 객체 리터럴의 경우를 생각해봅시다. 우리는 객체를 생성하여 내부 메서드를 정의하면 메서드 내부에서 자신이 속한 객체의 식별자를 참조할 수 있었습니다.
7+
8+
- `객체 리터럴 방식`으로 선언한 경우
9+
```js
10+
const circle = {
11+
// 프로퍼티: 객체 고유의 상태 데이터
12+
radius: 5,
13+
// 메서드: 상태 데이터를 참조하고 조작하는 동작
14+
getDiameter() {
15+
// 자신이 속한 객체인 circle을 참조합니다.
16+
return 2 * circle.radius;
17+
}
18+
};
19+
20+
console.log(circle.getDiameter()); // 10
21+
```
22+
- `getDiameter`를 호출하면 circle을 재귀적으로 호출하죠? 즉, 이 참조 표현식이 평가되는 시점은 getDiameter 메서드가 호출되어 함수 몸체가 실행되는 시점입니다.
23+
24+
객체 리터럴은 변수에 할당되기 전에 평가되는 것은 계속 언급해왔죠? 즉 getDiameter가 호출되는 시점에는 객체가 생성된 시점이므로 메서드 내부에서 해당 식별자(위 예제의 경우 circle)를 참조할 수 있는 것이죠. 하지만 내가 속한 객체를 재귀적으로 참조한다는 것은 당연히 권장되지 않습니다.
25+
26+
- `생성자 함수 방식`으로 선언한 경우
27+
```js
28+
function Circle(radius) {
29+
// 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없죠?
30+
????.radius = radius;
31+
}
32+
33+
Circle.prototype.getDiameter = function () {
34+
// 여기도 마찬가지입니다.
35+
return 2 * ????.radius;
36+
};
37+
38+
// 생성자 함수로 인스턴스를 생성합니다.
39+
const circle = new Circle(5);
40+
```
41+
- 생성자 함수 내부에서는 자신이 생성한 인스턴스를 참조할 수 있어야 합니다. 그러나 생성자 함수가 호출되지 않는 한 인스턴스가 생성되지 않죠.
42+
- 결국 생성자 함수를 정의하는 시점에는 이 생성자 함수가 어떤 인스턴스를 생성하는지, 식별자는 무엇인지 알 수 없습니다.
43+
44+
결국 자신이 속한 객체나 생성한 인스턴스를 가리킬 식별자가 필요하다는 것입니다. 이게 바로 `this`에요. 자기 참조 변수(self-referencing variable)로서 this를 통해 해당 객체나 인스턴스의 프로퍼티와 메서드를 참조할 수 있게 되는 것입니다. 그 특징은 아래와 같습니다.
45+
1. 자바스크립트 엔진에 의해 암묵적으로 생성됩니다.
46+
2. 따라서 어디서든 참조할 수 있습니다.
47+
3. 함수를 호출하면 arguments 객체와 this가 암묵적으로 전달됩니다.
48+
4. this도 지역변수처럼 사용할 수 있습니다.
49+
5. **this가 가리키는 값, `this 바인딩`은 함수 호출 방식에 따라 동적으로 결정됩니다**.
50+
51+
앞선 예제를 this로 수정해볼까요?
52+
53+
- `객체 리터럴 방식`으로 선언한 경우
54+
```js
55+
const circle = {
56+
// 프로퍼티: 객체 고유의 상태 데이터
57+
radius: 5,
58+
// 메서드: 상태 데이터를 참조하고 조작하는 동작
59+
getDiameter() {
60+
// 자신이 속한 객체인 circle을 참조합니다.
61+
return 2 * this.radius;
62+
}
63+
};
64+
65+
console.log(circle.getDiameter()); // 10
66+
```
67+
68+
- `생성자 함수 방식`으로 선언한 경우
69+
```js
70+
function Circle(radius) {
71+
// 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없죠?
72+
this.radius = radius;
73+
}
74+
75+
Circle.prototype.getDiameter = function () {
76+
// 여기도 마찬가지입니다.
77+
return 2 * this.radius;
78+
};
79+
80+
// 생성자 함수로 인스턴스를 생성합니다.
81+
const circle = new Circle(5);
82+
```
83+
84+
this 바인딩이란 식별자와 값을 연결하는 과정으로, this(키워드이나 식별자로 분류돼요)와 this가 가리킬 객체를 연결하는 것입니다. `클래스 기반 언어`는 이 this가 가리키는 대상이 `클래스가 생성하는 인스턴스`이지만 자바스크립트는 함수 호출 방식에 따라 다르다는 것을 유의해주세요.
85+
86+
> strict mode는 this 바인딩에도 영향이 있습니다. [여기](https://github.com/FECrash/JavascriptCrash/blob/main/Javascript/strict_mode.md#%EC%9D%BC%EB%B0%98-%ED%95%A8%EC%88%98%EC%9D%98-this)를 참조해주세요.
87+
88+
this는 자기 참조 변수입니다. 일반 함수 내부에서는 사용할 필요가 없기 떄문에 undefined가 할당되는게 정상(strict mode처럼요)이지만 기본 자바스크립트 엔진은 전역 객체에 바인딩된 상태이므로 `window`를 출력한다는 것을 알아두세요.
89+
90+
<br>
91+
92+
자바스크립트에서는 동일한 함수도 다양한 방식으로 호출할 수 있습니다. 함수가 동일한데 다양한 방식으로 호출할 수 있다니요...? 무슨 종류가 있을까요? 여기서는 종류와 바인딩이 어떻게 결정되는지 자세히 알아봅니다.
93+
94+
<br>
95+
96+
## 함수 호출 방식과 this 바인딩
97+
> this 바인딩은 함수가 **어떻게** 호출 되었는지에 따라 동적으로 결정됩니다.
98+
99+
> 잠깐! ✋ **렉시컬 스코프와 this 바인딩은 결정 시기가 달라요.**
100+
> - 렉시컬 스코프(Lexical Scope)는 함수 정의가 평가되고 함수 객체가 생성되는 시점에 상위 스코프를 결정합니다.
101+
> - this 바인딩은 함수 호출 시점에 결정됩니다.
102+
103+
함수를 호출하는 방식과 this 바인딩은 아래처럼 다양합니다.
104+
105+
<div align=center>
106+
107+
|함수 호출 방식|this 바인딩|
108+
|----|----|
109+
|`일반 함수` 호출|**전역** 객체|
110+
|`메서드` 호출|메서드를 **호출**한 객체|
111+
|`생성자 함수` 호출|생성자 함수가 **미래에 생성할 인스턴스**|
112+
|Function.prototype.apply/call/bind<br>메서드에 의한 `간접` 호출|Function.prototype.apply/call/bind<br>메서드에 첫 번째 인수로 전달된 객체|
113+
114+
</div>
115+
116+
<br>
117+
118+
### 일반 함수 호출
119+
> 기본적으로 **전역 객체(global object)** 에 this가 바인딩됩니다.
120+
121+
전역 함수와 내부 함수 상관 없이 일반 함수로 호출하면 **함수 내부의 this에는 전역 객체가 바인딩**됩니다. 그것이 메서드 내에서 정의한 내부 함수나 콜백 함수라도 동일합니다.
122+
123+
```js
124+
// 1. 일반 함수에 정의한 내부 함수
125+
function foo() {
126+
console.log(this); // window
127+
function bar() {
128+
console.log(this); // window
129+
}
130+
"use strict";
131+
function bar2(){
132+
console.log(this); // undefined
133+
}
134+
bar();
135+
}
136+
foo();
137+
138+
// 전역 객체의 프로퍼티로서 할당되는 var 키워드
139+
var value = 1;
140+
141+
// 2. 메서드에서 정의한 내부 함수
142+
const obj = {
143+
value: 100,
144+
foo() {
145+
console.log(this); // {value: 100, foo: ƒ}
146+
console.log(this.value); // 100
147+
148+
function bar() {
149+
console.log(this); // window
150+
console.log(this.value); // 1
151+
}
152+
153+
bar();
154+
}
155+
};
156+
157+
obj.foo();
158+
159+
// 3. 메서드 내의 콜백 함수
160+
const obj = {
161+
value: 100,
162+
foo() {
163+
console.log(this); // {value: 100, foo: ƒ}
164+
setTimeout(function () {
165+
console.log(this); // window
166+
console.log(this.value); // 1
167+
}, 100);
168+
}
169+
};
170+
171+
obj.foo();
172+
```
173+
174+
결국 **일반 함수로 호출된 모든 함수(내부, 콜백 등) 내부의 this는 전역 객체가 바인딩**됩니다.
175+
176+
당연히 this가 전역 객체에만 바인딩 되는 것은 문제가 있으므로, 메서드의 내부 함수나 콜백 함수의 this 바인딩을 메서드의 this와 일치시키기 위해서는 `this 식별자를 변수에 할당`하거나 `apply/call/bind 메서드`, `화살표 함수`를 사용해주세요.
177+
178+
- this를 변수에 할당하기
179+
```js
180+
var value = 1;
181+
182+
const obj = {
183+
value: 100,
184+
foo() {
185+
const that = this;
186+
187+
setTimeout(function () {
188+
console.log(that.value); // 100
189+
}, 100);
190+
}
191+
};
192+
193+
obj.foo();
194+
```
195+
196+
- apply/call/bind 메서드 사용하기
197+
```js
198+
var value = 1;
199+
200+
const obj = {
201+
value: 100,
202+
foo() {
203+
setTimeout(function () {
204+
console.log(this.value); // 100
205+
}.bind(this), 100);
206+
}
207+
};
208+
209+
obj.foo();
210+
```
211+
212+
- 화살표 함수 사용하기 : 화살표 함수의 this는 상위 스코프의 this와 바인딩됩니다.
213+
```js
214+
var value = 1;
215+
216+
const obj = {
217+
value: 100,
218+
value2 : this.value // 1
219+
foo() {
220+
setTimeout(() => console.log(this.value), 100); // 100
221+
}
222+
};
223+
224+
obj.foo();
225+
```
226+
227+
<br>
228+
229+
### 메서드 호출
230+
> 메서드를 호출할 때 마침표 연산자 앞에 기술한 객체가 바인딩됩니다.
231+
232+
메서드 내부의 this는 프로퍼티로 메서드를 가리키는 객체와는 **관계가 없고**, 메서드를 호출한 객체에 바인딩 되는 것입니다.
233+
```js
234+
// 메서드 내부의 this는 메서드를 호출한 객체에 바인딩됩니다.
235+
const person = {
236+
name: 'amy',
237+
getName() {
238+
return this.name;
239+
}
240+
};
241+
242+
// 메서드 getName을 호출한 객체는 person이므로
243+
console.log(person.getName()); // amy
244+
245+
const anotherPerson = {
246+
name: 'james'
247+
};
248+
249+
// getName 메서드를 anotherPerson 객체의 메서드로 할당
250+
anotherPerson.getName = person.getName;
251+
252+
// getName 메서드를 호출한 객체는 anotherPerson입니다.
253+
console.log(anotherPerson.getName()); // james
254+
255+
// getName 메서드를 변수에 할당
256+
const getName = person.getName;
257+
258+
// getName 메서드를 일반 함수로 호출
259+
console.log(getName()); // ''
260+
```
261+
262+
일반 함수로 호출된 `getName` 함수 내부의 `this.name`은 전역 객체의 프로퍼티 `window.name`과 같고, 브라우저의 window.name은 브라우저 창의 이름을 나타내는 빌트인 프로퍼티입니다. 따라서 기본값은 `''`이 돼요(Node.js는 `undefined`).
263+
264+
<br>
265+
266+
이는 프로토타입 내부에 사용된 this 바인딩도 동일합니다.
267+
```js
268+
function Person(name) {
269+
this.name = name;
270+
}
271+
272+
Person.prototype.getName = function () {
273+
return this.name;
274+
};
275+
276+
const me = new Person('amy');
277+
console.log(me.getName()); // amy
278+
279+
Person.prototype.name = 'james';
280+
console.log(Person.prototype.getName()); // james
281+
```
282+
283+
<br>
284+
285+
### 생성자 함수 호출
286+
> 생성자 함수가 **미래에 생성할 인스턴스**가 바인딩됩니다.
287+
288+
```js
289+
function Circle(radius) {
290+
this.radius = radius;
291+
this.getDiameter = function () {
292+
return 2 * this.radius;
293+
};
294+
}
295+
296+
const circle1 = new Circle(5);
297+
console.log(circle1.getDiameter()); // 10
298+
299+
const circle2 = new Circle(10);
300+
console.log(circle2.getDiameter()); // 20
301+
```
302+
303+
단, new 연산자 없이 호출하면 일반 함수이므로... 어떻게 동작하게 되는진 예상되시죠?
304+
305+
<br>
306+
307+
### Function.prototype.apply/call/bind 메서드에 의한 간접 호출
308+
> Function.prototype의 메서드로 모든 함수가 상속받아 사용할 수 있습니다.
309+
310+
apply, call은 `this로 사용할 객체와 인수 리스트`를, bind는 `this`을 인수로 받습니다.
311+
312+
- apply : `Function.prototype.apply(thisArg[, argsArray])`
313+
- thisArg : this로 사용할 객체
314+
- argsArray : 함수에 전달할 인수 리스트
315+
316+
- call : `Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])`
317+
- thisArg : this로 사용할 객체
318+
- arg1, arg2, ... : 함수에 전달할 인수 리스트
319+
320+
- bind : `Function.prototype.bind(thisArg)`
321+
- thisArg : this로 사용할 객체
322+
323+
> 자세한 사용법은 MDN을 참고해주세요.
324+
> - [apply()](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)
325+
> - [call()](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/call)
326+
> - [bind()](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)
327+
328+
<br>
329+
330+
apply와 call 메서드는 본래 함수를 호출하는 것이지만, 첫 번째 인수로 전달한 특정 객체를 호출한 함수의 this에 바인딩하죠. 인수를 전달하는 방식이 다를 뿐이지 동작 방식은 같습니다.
331+
332+
- 다음은 getThisBinding 함수를 호출하면서 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩시키는 코드입니다.
333+
```js
334+
function getThisBinding() {
335+
console.log(arguments);
336+
return this;
337+
}
338+
339+
// this로 사용할 객체
340+
const thisArg = { a: 1 };
341+
342+
// 1. apply 메서드는 호출할 함수의 인수를 배열로 묶어 전달
343+
console.log(getThisBinding.apply(thisArg, [1, 2, 3]));
344+
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
345+
// {a: 1}
346+
347+
// 2. call 메서드는 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달
348+
console.log(getThisBinding.call(thisArg, 1, 2, 3));
349+
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
350+
// {a: 1}
351+
```
352+
- 이들의 대표적인 용도는 **유사 배열 객체에 배열 메서드를 사용하는 경우**입니다.
353+
```js
354+
// 1. arguments 객체를 배열로 변환
355+
function convertArgsToArray() {
356+
console.log(arguments);
357+
358+
// Array.prototype.slice를 인수없이 호출하면 배열의 복사본을 생성
359+
const arr = Array.prototype.slice.call(arguments);
360+
// const arr = Array.prototype.slice.apply(arguments);
361+
console.log(arr);
362+
363+
return arr;
364+
}
365+
366+
convertArgsToArray(1, 2, 3); // [1, 2, 3]
367+
```
368+
369+
<br>
370+
371+
bind 메서드는 메서드의 this와 메서드의 내부/콜백 함수의 this가 불일치하는 문제를 해결하기 위해 유용하게 사용됩니다.
372+
373+
- 다음은 bind 메서드로 this를 일치시키는 코드입니다.
374+
```js
375+
const person = {
376+
name: 'amy',
377+
foo(callback) {
378+
// bind 메서드로 callback 함수 내부의 this 바인딩 전달
379+
setTimeout(callback.bind(this), 100);
380+
}
381+
};
382+
383+
person.foo(function () {
384+
console.log(`Hi! ${this.name}.`); // Hi! amy.
385+
});
386+
```
387+
388+
<hr>
389+
<br>

0 commit comments

Comments
 (0)