thenullpage.com

객체에 분명히 없는 메서드가 불리고 있었다. 콘솔에 찍어보면 속성 두 개가 전부인데 점을 찍고 부르면 멀쩡히 돈다. 자바스크립트 객체는 자기한테 없는 걸 물으면 위쪽에 대신 물어본다는 걸 그때 알았다.

객체는 자기한테 없으면 위에서 찾는다. 두 객체를 연결해두고 찍어보면 바로 보인다.


const animal = { legs: 4 };
const dog = Object.create(animal); // animal을 위에 두고 만든다
dog.name = "바둑이";
console.log(Object.keys(dog)); // ["name"], legs는 없다
console.log(dog.legs); // 4


dog가 가진 건 name뿐인데 legs도 나온다. 없으면 연결된 위쪽 객체를 보고, 거기도 없으면 더 위로 올라간다. 끝까지 없으면 그때 undefined다. 이 줄을 프로토타입 체인이라고 부른다. 늘 쓰던 배열의 map도 배열 안에 들어 있는 게 아니다.


const list = [1, 2];
console.log(Object.keys(list)); // ["0", "1"], map은 안 보인다
console.log(Object.getPrototypeOf(list) === Array.prototype); // true


Object.getPrototypeOf는 이 객체가 누구를 위에 두고 있는지 알려준다. 배열을 몇 개 만들든 map은 저기 한 벌뿐이다.

메서드를 생성자 안에 넣었다가 인스턴스 수만큼 찍어냈다. 처음엔 이렇게 짰다.


function User(name) {
this.name = name;
this.hello = function () { console.log(this.name); };
}
console.log(new User("김").hello === new User("이").hello); // false


목록을 천 개 만들면 똑같은 함수도 천 개 생긴다. prototype에 붙이면 한 벌을 같이 쓴다.


function User2(name) {
this.name = name;
}
User2.prototype.hello = function () { console.log(this.name); };
const c = new User2("김개발");
console.log(c.hello === new User2("이").hello); // true, 하나를 같이 본다
c.hello(); // 김개발


new로 만든 객체는 그 함수의 prototype과 연결된다. hello는 객체 안이 아니라 위에 있고, 부르는 순간 체인을 타고 올라가 찾아온다.

class는 이걸 읽기 좋게 적는 방법일 뿐이다. 다른 물건이 새로 생긴 게 아니다.


class Cart {
size = 0; // 필드는 인스턴스에 생긴다
constructor(owner) {
this.owner = owner;
}
add(item) { // 메서드는 Cart.prototype에 붙는다
this.size = this.size + 1;
console.log(this.owner + " " + item);
}
}
const cart = new Cart("김개발");
cart.add("키보드"); // 김개발 키보드
console.log(cart.size); // 1
console.log(Object.keys(cart)); // ["size", "owner"], add는 없다


결과가 위의 User2와 똑같다. 필드에 적은 값은 인스턴스마다 새로 생기고 메서드는 Cart.prototype에 한 벌 올라간다.

extends는 체인을 한 칸 더 잇는 것이다. 상속이라는 말보다 이 그림이 정확하다.


class Item {
constructor(name) {
this.name = name;
}
info() {
console.log(this.name);
}
}
class Book extends Item {
constructor(name, pages) {
super(name); // 빼면 에러가 난다
this.pages = pages;
}
info() {
super.info(); // 부모 것을 먼저 부른다
console.log(this.pages + "쪽");
}
}
const bk = new Book("자바스크립트", 320);
bk.info(); // 자바스크립트
// 320쪽
console.log(bk instanceof Item); // true, 위쪽도 통과한다


자식 생성자에서는 this를 건드리기 전에 super()를 불러야 한다. 빼먹으면 must call super constructor 에러가 뜬다. bk.info()bk에 없으니 Book.prototype에서 찾아 멈춘다. instanceof도 이 체인을 훑어 오른쪽 prototype이 있는지 보는 게 전부라 부모로 물어도 true다.

static은 인스턴스가 아니라 클래스에 붙는다. 만드는 방법이 여러 가지일 때 쓴다.


class Money {
constructor(won) {
this.won = won;
}
static fromDollar(usd) {
return new Money(usd * 1300);
}
}
const m = Money.fromDollar(10);
console.log(m.won); // 13000
console.log(m.fromDollar); // undefined


인스턴스가 없어도 부를 수 있어서 이름 붙인 입구로 쓴다.

#을 붙이면 밖에서 진짜로 못 건드린다. 밑줄만 붙여두던 시절엔 결국 누군가 직접 고쳤다.


class Account {
#balance = 0;
deposit(won) {
if (won <= 0) return this.#balance;
this.#balance = this.#balance + won;
return this.#balance;
}
get balance() {
return this.#balance;
}
}
const acc = new Account();
console.log(acc.deposit(5000)); // 5000
console.log(acc.deposit(-1)); // 5000, 안 깎인다
console.log(Object.keys(acc)); // [], 밖에서는 안 보인다


get을 붙인 balance는 괄호 없이 속성처럼 읽힌다. 읽기는 열고 쓰기는 deposit을 거치게 만드는 식이다.

프로토타입에 배열을 뒀다가 게시판 글이 전부 섞였다. 이건 진짜 오래 걸렸다.


function Board(name) {
this.name = name;
}
Board.prototype.posts = []; // 위에 하나만 있다
const b1 = new Board("자유");
const b2 = new Board("질문");
b1.posts.push("글1");
console.log(b2.posts); // ["글1"], 건드린 적 없는데 들어 있다


위에 있는 건 모두가 같은 하나를 본다. 메서드는 그게 이득이지만 값은 사고가 된다. 배열이나 객체는 각자 갖게 해야 한다.


function Board2(name) {
this.name = name;
this.posts = []; // 만들 때마다 새 배열
}


class에서 필드에 posts = []라고 적으면 인스턴스마다 새로 생기니 이 함정은 안 밟는다.

돌아보면 규칙은 한 줄이었다. 객체는 자기한테 없으면 위로 올라가 찾는다. newclassextends도 그 줄을 어떻게 잇느냐를 적는 방법일 뿐이다. 그래서 없어야 할 메서드가 불리거나 건드린 적 없는 값이 바뀌어 있으면 이 객체 위에 뭐가 매달려 있는지부터 본다. 거기 값이 올라가 있으면 대체로 범인이었다.