학습 자료

this, 프로토타입, 클래스


Article

11강에서 화살표 함수는 this를 갖지 않는다고 했고, 17강에서 메서드 안의 this를 그냥 넘겼다.

이제 정면으로 본다. this는 자바스크립트에서 가장 많이 틀리는 개념이고, 규칙은 딱 한 문장이다.

this는 함수가 어디에 쓰였는지가 아니라, 어떻게 호출됐는지로 정해진다.

같은 함수, 다른 this

function whoAmI() {
  console.log(this);
}

const obj = { name: "지원", whoAmI };

obj.whoAmI();
whoAmI();
콘솔
  • { name: '지원', whoAmI: f }
  • undefined (strict mode) 또는 window

똑같은 함수인데 this가 다르다. 함수를 정의한 자리가 아니라 부른 방식이 달랐기 때문이다.

네 가지 호출 방식

this가 정해지는 규칙은 네 개뿐이다. 위에서부터 우선한다.

호출 방식생김새this 는
new 호출new Person()새로 만들어지는 객체
명시적 바인딩fn.call(obj)인자로 준 obj
메서드 호출obj.method()점 앞의 obj
일반 호출fn()undefined (strict) 또는 전역

핵심은 세 번째다. 점 앞에 있는 것이 this다.

const counter = {
  count: 0,
  increase() {
    this.count++;
    return this.count;
  },
};

console.log(counter.increase());
콘솔
  • 1

counter.increase()에서 점 앞은 counter다. 그래서 this.countcounter.count다.

점이 사라지면 this 도 사라진다

여기서 사고가 난다.

const counter = {
  count: 0,
  increase() {
    this.count++;
    return this.count;
  },
};

const fn = counter.increase;
fn();
콘솔
  • TypeError: Cannot read properties of undefined (reading 'count')

fn에 함수를 담는 순간 counter와의 연결이 끊겼다. fn()은 그냥 일반 호출이라 thisundefined다.

연결이 끊기는 과정
  1. 01

    counter.increase 는 함수를 가리킬 뿐

    객체에 함수가 들어 있는 게 아니라, 객체가 함수를 가리키고 있다.

  2. 02

    fn 도 같은 함수를 가리킨다

    함수 자체는 자기가 어느 객체에 담겼는지 모른다.

    const fn = counter.increase;
  3. 03

    점 없이 부르면 this 를 정할 근거가 없다

    일반 호출로 취급되어 thisundefined 가 된다.

    fn();  // this === undefined

콜백으로 넘길 때 똑같은 일이 벌어진다.

const counter = { count: 0, increase() { this.count++; } };

setTimeout(counter.increase, 100);

setTimeout은 함수만 받아서 나중에 점 없이 부른다. this가 끊긴다.

해결책은 세 가지다.

const counter = { count: 0, increase() { this.count++; return this.count; } };

console.log(counter.increase.call(counter));
const bound = counter.increase.bind(counter);
console.log(bound());
setTimeout(() => counter.increase(), 0);
콘솔
  • 1
  • 2

callthis를 지정해 즉시 부르고, bindthis를 고정한 새 함수를 만든다. 세 번째가 가장 흔하다. 화살표 함수로 감싸서 점을 다시 만들어준다.

화살표 함수는 this 를 갖지 않는다

11강에서 예고한 부분이다.

const timer = {
  seconds: 0,

  startBad() {
    setInterval(function () {
      this.seconds++;
      console.log(this.seconds);
    }, 1000);
  },

  startGood() {
    setInterval(() => {
      this.seconds++;
      console.log(this.seconds);
    }, 1000);
  },
};

startBad의 콜백은 일반 함수라 자기만의 this를 갖는데, 점 없이 불리므로 undefined다. startGood의 화살표 함수는 this를 만들지 않고 바깥에서 물려받는다. 바깥은 startGood이고 거기서 thistimer다.

this 를 정하는 시점

일반 함수

호출할 때 정해진다

  • 부를 때마다 달라진다
  • 점 앞의 객체를 본다
  • 콜백으로 넘기면 끊긴다
  • 객체의 메서드에 알맞다

화살표 함수

쓰여진 위치로 정해진다

  • 자기 this 가 아예 없다
  • 바깥 스코프의 this 를 쓴다
  • 어떻게 불러도 안 바뀐다
  • 콜백에 알맞다

프로토타입 — 상속이라 부르지만 사실은 연결

객체 열 개를 만드는데 메서드가 똑같다면, 함수를 열 번 복사해 넣는 건 낭비다.

자바스크립트는 다르게 푼다. 객체마다 다른 객체를 가리키는 숨은 링크가 있고, 자기에게 없는 속성을 찾으면 그 링크를 타고 올라간다.

const arr = [1, 2, 3];

console.log(arr.map);
console.log(Object.hasOwn(arr, "map"));
콘솔
  • [Function: map]
  • false

arr 자신은 map을 갖고 있지 않다. 그런데 쓸 수 있다. 링크를 타고 Array.prototype에서 찾아왔기 때문이다.

프로토타입 체인
  • arr[1, 2, 3] — map 없음
    • Array.prototypemap · filter · push … 여기 있다
      • Object.prototypetoString · hasOwnProperty
        • null체인의 끝

12강의 스코프 체인과 똑같은 모양이다. 자기에게 없으면 위로 올라가며 찾고, 끝까지 없으면 undefined다.

모든 배열이 Array.prototype 하나를 공유한다. map 함수는 메모리에 딱 하나만 존재한다.

클래스 — 프로토타입을 예쁘게 쓰는 문법

프로토타입을 직접 다루는 코드는 읽기 어렵다. ES6에서 익숙한 문법이 생겼다.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `안녕하세요, ${this.name}입니다`;
  }
}

const a = new Person("지원", 27);
const b = new Person("민수", 31);

console.log(a.greet());
console.log(Object.hasOwn(a, "greet"));
console.log(a.greet === b.greet);
콘솔
  • '안녕하세요, 지원입니다'
  • false
  • true

greeta에 없고, ab같은 함수 하나를 쓴다. 프로토타입에 얹혀 있기 때문이다.

new를 붙이면 네 가지 일이 벌어진다.

new Person("지원", 27)
  1. 01

    빈 객체를 만든다

    {} 하나가 생긴다.

  2. 02

    프로토타입을 연결한다

    그 객체가 Person.prototype 을 가리키게 한다.

  3. 03

    constructor 를 실행한다

    이때 this 는 새로 만든 객체다. 그래서 this.name = name 이 통한다.

  4. 04

    그 객체를 돌려준다

    return 을 쓰지 않아도 자동으로 반환된다.

new를 빠뜨리면 this가 새 객체가 아니게 되어 조용히 망가진다. 다행히 클래스는 new 없이 부르면 에러를 낸다.

const c = Person("지원", 27);
콘솔
  • TypeError: Class constructor Person cannot be invoked without 'new'

상속

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name}이(가) 소리를 낸다`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  speak() {
    return `${this.name}이(가) 짖는다`;
  }
}

const dog = new Dog("바둑이", "진돗개");

console.log(dog.speak());
console.log(dog.name);
console.log(dog instanceof Animal);
콘솔
  • '바둑이이(가) 짖는다'
  • '바둑이'
  • true

extends로 잇고, super()로 부모의 constructor를 부른다. super()를 빼먹으면 this를 쓸 수 없다는 에러가 난다.

Dog에도 speak가 있으니 그것이 먼저 잡힌다. 12강의 섀도잉과 같은 원리다. 체인에서 가까운 것이 이긴다.

정리하면

  • this는 호출 방식이 정한다. 함수가 어디에 쓰였는지는 상관없다.
  • **점 앞에 있는 것이 this**다. 점이 사라지면 this도 끊긴다.
  • 끊긴 연결은 bind로 고정하거나, 화살표 함수로 감싸 되살린다.
  • 화살표 함수는 this를 만들지 않고 바깥에서 물려받는다. 콜백에 알맞고, 객체 메서드에는 부적합하다.
  • 객체는 자기에게 없는 속성을 프로토타입 체인을 타고 위로 찾는다. 스코프 체인과 같은 모양이다.
  • class는 프로토타입을 포장한 문법이다. 복사가 아니라 연결이다.

이제 마지막 강의만 남았다. 13강에서 "콜 스택은 한 번에 하나씩만 처리한다"고 했는데, 그렇다면 setTimeout은 어떻게 기다리면서도 화면을 멈추지 않을까. 이벤트 루프async/await를 본다.

this, 프로토타입, 클래스 — 디코드랩(DCODELAB)