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.count는 counter.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()은 그냥 일반 호출이라 this가 undefined다.
- 01
counter.increase 는 함수를 가리킬 뿐
객체에 함수가 들어 있는 게 아니라, 객체가 함수를 가리키고 있다.
- 02
fn 도 같은 함수를 가리킨다
함수 자체는 자기가 어느 객체에 담겼는지 모른다.
const fn = counter.increase; - 03
점 없이 부르면 this 를 정할 근거가 없다
일반 호출로 취급되어
this는undefined가 된다.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
call은 this를 지정해 즉시 부르고, bind는 this를 고정한 새 함수를 만든다. 세 번째가 가장 흔하다. 화살표 함수로 감싸서 점을 다시 만들어준다.
화살표 함수는 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이고 거기서 this는 timer다.
일반 함수
호출할 때 정해진다
- 부를 때마다 달라진다
- 점 앞의 객체를 본다
- 콜백으로 넘기면 끊긴다
- 객체의 메서드에 알맞다
화살표 함수
쓰여진 위치로 정해진다
- 자기
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
- Array.prototype
- Object.prototype
- null
- Object.prototype
- Array.prototype
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
greet는 a에 없고, a와 b가 같은 함수 하나를 쓴다. 프로토타입에 얹혀 있기 때문이다.
new를 붙이면 네 가지 일이 벌어진다.
- 01
빈 객체를 만든다
{}하나가 생긴다. - 02
프로토타입을 연결한다
그 객체가
Person.prototype을 가리키게 한다. - 03
constructor 를 실행한다
이때
this는 새로 만든 객체다. 그래서this.name = name이 통한다. - 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를 본다.