JavaScript 2주차 — 함수 호출과 this
장바구니의 가격을 읽는 함수를 변수에 담아 실행했더니 가격이 사라졌어요. 함수 코드는 그대로인데 말이에요. 같은 함수라도 어떻게 부르느냐에 따라 사용하는 대상이 달라질 수 있어요. 그 대상을 함수 안에서는 this라고 부릅니다.
이 글에서는 두 가지를 확인해요. this가 언제 달라지는지, 그리고 call·apply·bind로 this를 어떻게 지정하는지예요. 먼저 연결 지도로 전체를 보고, 같은 함수를 두 방식으로 실행해 볼게요.
함수 값에서 호출 방식으로
함수 값을 전달하는 것과 호출하는 것을 나눈 뒤, 일반 함수·화살표 함수·생성자 호출을 비교합니다.
- 함수 값 — 변수 저장 · 인자로 전달. 함수 값: 변수 저장 · 인자로 전달
- 호출식 — fn() · obj.fn(). 호출식: fn() · obj.fn()
- 일반 함수의 this — 이번 호출의 수신자. 일반 함수의 this: 이번 호출의 수신자
- call · apply · bind — 즉시 호출 / 새 함수. call · apply · bind: 즉시 호출 / 새 함수
- Arrow — 바깥 this 사용. Arrow: 바깥 this 사용
- new — 새 객체 생성. new: 새 객체 생성
함수 값을 전달하는 것과 호출하는 것을 나눈 뒤, 일반 함수·화살표 함수·생성자 호출을 비교합니다.
- 함수 값 — 변수 저장 · 인자로 전달. 함수 값: 변수 저장 · 인자로 전달
- 호출식 — fn() · obj.fn(). 호출식: fn() · obj.fn()
- 일반 함수의 this — 이번 호출의 수신자. 일반 함수의 this: 이번 호출의 수신자
- call · apply · bind — 즉시 호출 / 새 함수. call · apply · bind: 즉시 호출 / 새 함수
- Arrow — 바깥 this 사용. Arrow: 바깥 this 사용
- new — 새 객체 생성. new: 새 객체 생성
그림의 앞부분은 함수 값 → 호출식 → this를 나눠 보는 순서예요. call·apply·bind는 this를 지정하는 방법이고, Arrow와 new는 다른 호출 규칙을 가진 경우예요. 이번에는 일반 함수와 call·apply·bind에 집중해 볼게요.
이름과 역할을 먼저 나눠 볼까요?
왼쪽은 코드를 읽는 기본 용어, 오른쪽은 this를 지정하는 세 가지 방법이에요.
기본 용어
- this
- 함수가 이번 호출에서 사용하는 대상이에요.
- 메서드
- 객체에 들어 있는 함수예요. 장바구니 객체의
read처럼요. - 호출식
cart.read()나fn()처럼 함수를 실행하는 표현이에요.- 인자
- 함수를 호출할 때 넘기는 값이에요.
call · apply · bind
- call · 지금 실행
- this와 인자를 하나씩 넘겨요.
fn.call(cart, 1, 2) - apply · 지금 실행
- call과 같지만, 인자를 배열로 넘겨요.
fn.apply(cart, [1, 2]) - bind · 새 함수 만들기
- this를 기억하는 새 함수를 만들어요. 그 함수를 부를 때 실행해요.
const bound = fn.bind(cart)
실습은 엄격 모드(strict mode) 기준이에요. 이 기준에서 객체 없이 fn()으로 부르면 this는 undefined예요. ?.는 this가 없을 때 읽기를 멈추고 undefined를 돌려줘서, 오류 대신 결과를 비교할 수 있게 해줍니다.
같은 read를 다른 식으로 호출하면 무엇을 읽을까요?
결과를 먼저 고른 뒤 실행하면 코드 옆에 관찰값이 나타나요.
두 사례는 같은 read 함수예요. 호출식과 함수 안의 this를 비교해 보세요. 이 함수는 오류보다 this의 차이를 보기 위해 ?.를 사용해요.
function read() { return this?.price;}const cart = { price: 12000, read };const fn = cart.read;cart.read();함수 안에서 관찰한 값
아직 실행 전이에요. 예상 결과를 고르면 실행할 수 있어요.
this는 이번 호출에서 사용하는 대상이에요. 함수가 저장돼 있던 객체와 같을지는 호출식에 따라 달라져요.
같은 함수 값, 다른 호출식
const fn = cart.read는 함수 값을 꺼내 저장해요. fn === cart.read는 true지만, fn이 cart와의 호출 관계까지 보관하지는 않습니다.
const fn = cart.read;
cart.read(); // read의 this: cart
fn(); // strict 일반 호출의 this: undefined
실습의 fn()은 undefined를 반환해요. this?.price 대신 this.price였다면 this가 undefined일 때 TypeError가 발생합니다. 비엄격 일반 함수는 undefined나 null을 전역 객체로 바꾸는 규칙이 있으므로, 이 결과를 모든 script 환경에 그대로 적용하지는 않아요.
프로퍼티를 읽는 단계와 함수를 호출하는 단계를 나누면 같은 함수인데 결과가 달라진 이유를 확인할 수 있어요.
즉시 호출과 나중에 호출하기
call과 apply는 this를 지정하면서 함수를 지금 실행하는 도구예요. call은 인자를 하나씩, apply는 배열로 묶어 넘겨요. bind는 this와 일부 인자를 기억하는 새 함수를 돌려줍니다. 원래 함수는 그 새 함수를 호출할 때 실행돼요.
| 표현식 | 원래 함수가 실행되는 시점 | 전달할 일반 인자 |
|---|---|---|
fn.call(other, a, b) | 지금 | a, b를 나란히 전달 |
fn.apply(other, [a, b]) | 지금 | 배열 같은 객체로 묶어 전달 |
fn.bind(cart, a) | 나중에 만들어진 함수를 호출할 때 | a를 앞에 고정 |
bind는 만드는 시점과 실행하는 시점을 나눠 보면 쉬워요.
const bound = fn.bind(cart); // 새 함수만 만들어요.
bound(); // 이때 read가 실행되고 cart의 가격을 읽어요.
같은 함수를 꺼내 쓰더라도, bind로 만든 함수는 일반 호출에서 지정한 cart를 기억해요. call과 apply는 바로 실행, bind는 새 함수를 만들어 나중에 실행한다는 차이를 먼저 잡으면 됩니다.
코드로 돌아갈 때
함수 값이 어디에 저장됐는지보다 실제 호출식을 먼저 확인해요. cart.read()와 fn()을 비교한 뒤, this를 지정해서 바로 실행할지, 나중에 호출할 함수를 만들어 둘지 생각해 보세요.