본문으로 건너뛰기

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를 돌려줘서, 오류 대신 결과를 비교할 수 있게 해줍니다.

코드 실험 · 호출과 this

같은 read를 다른 식으로 호출하면 무엇을 읽을까요?

결과를 먼저 고른 뒤 실행하면 코드 옆에 관찰값이 나타나요.

두 사례는 같은 read 함수예요. 호출식과 함수 안의 this를 비교해 보세요. 이 함수는 오류보다 this의 차이를 보기 위해 ?.를 사용해요.

실행할 JavaScript · strict 기준
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를 지정해서 바로 실행할지, 나중에 호출할 함수를 만들어 둘지 생각해 보세요.

참고 문서​