본문 바로가기
JavaScript Deep Dive 스터디

[모던 자바스크립트 Deep Dive] - 19~21장 정리

by 림졍 2026. 4. 29.
728x90
반응형

[모던 자바스크립트 Deep Dive] - 19~21장 정리

마참내! 드디어 본론이다.

 

용두사망이 아닌게 어디야 ^0^

 

앞서 적어놓은 19장 맛뵈기가 무려 딥다이브 한페이지라는거 ^-^;;;;

좀 현타가 오지만? 그래도 정리는 해야겠지 않겠습니까요....

빠르게 가...가볼께욧...

 

19장 - 프로토타입

상속 — "왜 필요한지"부터

상속이 왜 필요한지 코드로 보면 바로 납득이 된다.

// 상속 없이 — getArea가 인스턴스마다 따로 만들어짐
function Circle(radius) {
  this.radius = radius;
  this.getArea = function() {
    return Math.PI * this.radius ** 2;
  };
}

const c1 = new Circle(1);
const c2 = new Circle(2);
console.log(c1.getArea === c2.getArea); // false — 다른 함수 객체
// 인스턴스가 10000개면 getArea도 10000개 생김 → 메모리 낭비
// 프로토타입에 정의하면 — 하나만 만들고 전부 공유
function Circle(radius) {
  this.radius = radius;
}
Circle.prototype.getArea = function() {
  return Math.PI * this.radius ** 2;
};

const c1 = new Circle(1);
const c2 = new Circle(2);
console.log(c1.getArea === c2.getArea); // true — 같은 함수를 공유
// 인스턴스가 10000개여도 getArea는 메모리에 딱 하나

 

c1.getArea === c2.getAreatrue라는 게 핵심이다.
각자 들고 있는 게 아니라, Circle.prototype이라는 공용 창고에서 꺼내 쓰는 것이기 때문이다.

 

 

프로토타입 객체 — 모든 객체의 부모

 

프로토타입은 객체 간 상속을 구현하기 위해 사용되는 부모 객체다.
프로토타입이 가진 프로퍼티와 메서드를 상속받은 객체는 자신의 것처럼 자유롭게 쓸 수 있다.

모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다.
여기에 자기 부모 객체(프로토타입)의 참조가 저장된다.

focaccia 객체
  └─ [[Prototype]] ──▶ Recipe.prototype
                          └─ [[Prototype]] ──▶ Object.prototype
                                                  └─ [[Prototype]] ──▶ null

 

이 연결이 이어진 구조가 프로토타입 체인이다.

책에서는 이 체인을 "단방향 링크드 리스트로 구현된다"고 설명한다. 방향이 있고 끊기지 않아야 한다는 것이 핵심이다.

 

+) 어제 스터디에서 스코프 체인이랑 비슷한 메커니즘이라는 얘기가 나왔는데, 찾아봤더니 MDN에서 이렇게 정리해놨다:

"an unqualified identifier is resolved by searching the scope chain, while a qualified identifier is resolved by searching the prototype chain." — MDN, with statement

 

즉, 스코프 체인은 변수/식별자(focaccia 같은 것)를 찾고, 프로토타입 체인은 객체의 프로퍼티(focaccia.cook 같은 것)를 찾는다.

구조는 비슷하지만 담당이 다른 것이다.

둘이 서로 협력해서 동작한다 — 자세한 건 아래 프로토타입 체인 섹션에서 다뤄보는거로-!

 

생성자 함수, 프로토타입, 인스턴스는 항상 삼각형으로 연결되어 있다

Recipe (생성자 함수)
  ├─ .prototype ──▶ Recipe.prototype
  └─ Recipe.prototype.constructor ──▶ 다시 Recipe

focaccia (인스턴스)
  └─ [[Prototype]] ──▶ Recipe.prototype

 

 

 

[[Prototype]] — 직접 접근이 안 된다

 

[[Prototype]]은 이중 대괄호로 표기하는 내부 슬롯이다.
스펙 표기일 뿐, JS 코드에서 직접 쓸 수 있는 문법이 아니다.

obj.[[Prototype]]; // SyntaxError — 이런 문법 없음

 

대신 __proto__ 접근자 프로퍼티를 통해 간접적으로 접근할 수 있다.

__proto__는 상속을 통해 사용된다.

__proto__Object.prototype에 있는 접근자 프로퍼티다.
내 것이 아니라 상속받아 쓰는 것이다.

const recipe = { name: '포카치아' };
console.log(recipe.hasOwnProperty('__proto__')); // false — 내 것 아님
console.log({}.__proto__ === Object.prototype);  // true — 상속받아 씀
const obj = {};
const parent = { x: 1 };

obj.__proto__ = parent; // 프로토타입 교체
console.log(obj.x);    // 1 — parent에서 상속받아 씀

 

 

왜 직접 접근 못 하게 막았냐? — 순환 참조 방지

const parent = {};
const child = {};

child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
// 서로가 서로의 부모면 → 체인에 끝이 없음 → 무한 루프 → 막음

 

프로토타입 체인은 반드시 한 방향으로만 가야 한다.
__proto__를 통해 교체하게 만들어야 이 순환 참조를 감지해서 막을 수 있다.

 

+) 코드에서 __proto__ 직접 쓰는 건 권장하지 않는다.
Object.create(null)로 만든 객체처럼 Object.prototype을 상속받지 않는 객체는

__proto__ 자체가 없기 때문이다.

 

대신 이걸 쓰자

Object.getPrototypeOf(obj);         // 프로토타입 취득
Object.setPrototypeOf(obj, parent); // 프로토타입 교체

 

 

 

__proto__ vs prototype 프로퍼티 — 이름이 비슷해서 헷갈리는 그것

 

함수 객체는 __proto__ 말고도 prototype이라는 프로퍼티를 따로 갖는다.
이 둘이 처음엔 진짜 헷갈렸다.

({}).hasOwnProperty('prototype');            // false — 일반 객체엔 없음
(function() {}).hasOwnProperty('prototype'); // true  — 함수 객체엔 있음
  __proto__ prototype 프로퍼티
소유 모든 객체 생성자 함수만
용도 내 프로토타입에 접근/교체 내가 만들 인스턴스의 프로토타입 지정

 

결국 같은 객체를 양쪽에서 바라보는 것이다

function Recipe(name) { this.name = name; }
const focaccia = new Recipe('포카치아');

Recipe.prototype === focaccia.__proto__; // true — 같은 걸 가리킴
  • Recipe.prototype: 생성자 함수 쪽에서 "내가 만들 인스턴스의 프로토타입은 이거야"
  • focaccia.__proto__: 인스턴스 쪽에서 "내 프로토타입은 이거야"

화살표 함수와 ES6 메서드 축약 표현은 prototype 프로퍼티가 없다.
[[Construct]]가 없어서 new로 인스턴스를 만들 수 없기 때문이다.

const arrow = () => {};
arrow.hasOwnProperty('prototype'); // false

const obj = { foo() {} };
obj.foo.hasOwnProperty('prototype'); // false

 

 

 

constructor — "나를 만든 생성자 함수가 누구야"

 

모든 프로토타입은 constructor 프로퍼티를 갖는다.
이 프로퍼티는 자신을 생성한 생성자 함수를 가리킨다.

function Recipe(name) { this.name = name; }
const focaccia = new Recipe('포카치아');

// focaccia 자신에게 constructor가 없다
// focaccia.__proto__ (= Recipe.prototype)에 있는 걸 빌려 씀
console.log(focaccia.constructor === Recipe); // true

 

 

focacciaconstructor를 직접 들고 있는 게 아니라
프로토타입 체인으로 Recipe.prototype.constructor를 빌려 쓰는 것이다.

 

 

리터럴로 만든 객체의 constructor — 약간 미묘한 부분

const obj = {};
console.log(obj.constructor === Object); // true

 

objObject 생성자로 만든 게 아니라 리터럴로 만들었다.
근데 constructor를 확인하면 Object가 나온다. 왜?

스펙을 찾아봤더니, 객체 리터럴을 평가할 때 내부적으로 OrdinaryObjectCreate라는 추상 연산을 호출하고
이 과정에서 Object.prototype을 프로토타입으로 갖는 객체가 만들어지기 때문이다.

리터럴로 만들어도 → OrdinaryObjectCreate → Object.prototype 상속
new Object()로 만들어도 → OrdinaryObjectCreate → Object.prototype 상속
→ 결과: 둘 다 같은 프로토타입 체인

 

엄밀히 말하면 리터럴 객체 ≠ Object 생성자가 만든 객체지만,
프로토타입 체인 관점에서는 사실상 같다.

리터럴 표기법 생성자 함수 프로토타입
객체 리터럴 Object Object.prototype
함수 리터럴 Function Function.prototype
배열 리터럴 Array Array.prototype

 

 

 

프로토타입의 생성 시점 — 닭이 먼저냐 알이 먼저냐

// 선언 전에 이미 prototype 존재
console.log(Recipe.prototype); // {constructor: ƒ} — 이미 있음!

function Recipe(name) { this.name = name; }

 

생성자 함수와 프로토타입은 항상 쌍으로 존재한다.
함수 선언문은 런타임 이전에 평가되어 함수 객체가 되는데, 이 시점에 프로토타입도 함께 만들어진다.

화살표 함수 / ES6 메서드 축약 표현은 [[Construct]]가 없어서 프로토타입도 생성되지 않는다.

빌트인 생성자 함수(Object, String, Array 등)는 전역 객체가 생성되는 시점에 프로토타입이 함께 만들어진다.
내 코드가 실행되기 훨씬 전에 이미 준비되어 있다는 뜻이다.

 

 

객체 생성 방식과 프로토타입

 

JS에서 객체를 만드는 방법은 여러 가지지만
모두 OrdinaryObjectCreate라는 추상 연산을 통해 만들어진다는 공통점이 있다.

const a = { x: 1 };               // 객체 리터럴 → Object.prototype이 프로토타입
const b = new Object();            // Object 생성자 → Object.prototype이 프로토타입
const c = new Recipe('포카치아');  // 생성자 함수 → Recipe.prototype이 프로토타입
const d = Object.create(myProto); // → myProto가 프로토타입

 

이 추상 연산이 "어떤 프로토타입을 줄 것인가"를 결정한다.

 

 

프로토타입 체인 — "없으면 위로 올라가는 것"

 

JS 엔진이 프로퍼티를 찾는 방식이 바로 프로토타입 체인이다.

function Recipe(name) { this.name = name; }
Recipe.prototype.cook = function() { return `${this.name} 굽는 중`; };
const focaccia = new Recipe('포카치아');

focaccia.hasOwnProperty('name'); // true

 

focaccia에는 hasOwnProperty가 없다. 근데 쓸 수 있다. 왜?

1. focaccia 자신에서 hasOwnProperty 검색 → 없음
2. focaccia.__proto__ (= Recipe.prototype)에서 검색 → 없음
3. Recipe.prototype.__proto__ (= Object.prototype)에서 검색 → 있음! 실행

 

Object.prototype이 프로토타입 체인의 종점이다.
Object.prototype[[Prototype]]null이다.
여기서도 없으면 undefined를 반환한다 — 에러가 아니라 undefined라는 점 주의.

focaccia.foo; // undefined — 없어도 에러 안 남

 

스코프 체인 vs 프로토타입 체인

  프로토타입 체인 스코프 체인
목적 프로퍼티/메서드 검색 변수/식별자 검색
구조 객체 간 연결 렉시컬 환경의 계층

 

둘은 별도로 동작하는 게 아니라 협력한다.
focaccia.hasOwnProperty('name') 실행 시

1. 스코프 체인에서 focaccia 식별자 검색
2. focaccia 객체의 프로토타입 체인에서 hasOwnProperty 메서드 검색

 

 

 

오버라이딩과 프로퍼티 섀도잉 — "앞에 새 거 놓으면 뒤에 게 가려진다"

function Recipe(name) { this.name = name; }
Recipe.prototype.cook = function() { return `${this.name} 기본 방식으로`; };

const focaccia = new Recipe('포카치아');

// 인스턴스에 같은 이름 추가 — 오버라이딩
focaccia.cook = function() { return `${this.name} 특별한 방식으로`; };

focaccia.cook(); // '포카치아 특별한 방식으로' — 인스턴스 것이 먼저

 

Recipe.prototype.cook이 사라진 게 아니라 가려진 것이다. 이게 프로퍼티 섀도잉.

인스턴스 메서드를 지우면 다시 프로토타입 메서드가 나온다

delete focaccia.cook;
focaccia.cook(); // '포카치아 기본 방식으로' — 다시 나옴

 

단, 인스턴스를 통해 프로토타입 메서드를 삭제하는 건 안 된다.
하위 객체에서 프로토타입으로의 get은 허용되지만 set(삭제 포함)은 허용되지 않는다.

delete focaccia.cook; // focaccia 자신의 cook만 지워짐, 프로토타입은 그대로

// 프로토타입 메서드를 바꾸거나 지우려면 프로토타입에 직접 접근해야 함
delete Recipe.prototype.cook;
focaccia.cook(); // TypeError — 이제 진짜 없음

 

 

프로토타입의 교체 — 가능은 한데 권장은 안 함

function Recipe(name) { this.name = name; }
Recipe.prototype = {
  cook() { return `${this.name} 굽는 중`; }
};

const focaccia = new Recipe('포카치아');
console.log(focaccia.constructor === Recipe); // false ← constructor 연결이 파괴됨!
console.log(focaccia.constructor === Object); // true

 

교체한 객체 리터럴에 constructor가 없으니까
프로토타입 체인을 타고 Object.prototype.constructorObject가 나오는 것이다.

 

복원하려면 이 방식을 사용하면 되는데... 쬠 번거롭돠;

Recipe.prototype = {
  constructor: Recipe, // 명시적으로 연결 복원
  cook() { return `${this.name} 굽는 중`; }
};


상속 관계를 바꾸려면 직접 상속(Object.create)이나 class를 쓰는 게 훨씬 낫다.

 

 

instanceofconstructor 아니고 프로토타입 체인을 확인하는 것

const focaccia = new Recipe('포카치아');
focaccia instanceof Recipe; // true
focaccia instanceof Object; // true

 

instanceof가 확인하는 건 constructor 프로퍼티가 아니다.
"오른쪽 생성자 함수의 prototype이 왼쪽 객체의 프로토타입 체인 어딘가에 있는가?" 가 전부다.

focaccia.__proto__ === Recipe.prototype; // true → instanceof Recipe가 true인 이유

 

그래서 constructor 연결이 파괴돼도 instanceof는 영향을 받지 않는다:

// constructor 파괴됨
console.log(focaccia.constructor === Recipe); // false

// 하지만 Recipe.prototype은 체인에 있으니까
console.log(focaccia instanceof Recipe); // true — 영향 없음

 

반대로, 프로토타입을 통째로 교체하면 기존 인스턴스의 instanceof 결과가 갑자기 바뀐다:

function Recipe(name) { this.name = name; }
const focaccia = new Recipe('포카치아');
console.log(focaccia instanceof Recipe); // true

Recipe.prototype = { newMethod() {} }; // 프로토타입 교체

console.log(focaccia instanceof Recipe); // false!
// focaccia는 여전히 옛날 Recipe.prototype을 가리키고 있고,
// Recipe.prototype은 이제 다른 객체라서

 

instanceof가 "타입 검사"처럼 쓰이지만 실제론 프로토타입 참조 비교라는 걸 알면
이런 이상한 동작이 왜 생기는지 바로 납득이 된다.

 

 

직접 상속 — Object.create로 프로토타입을 지정해서 생성

// null을 넣으면 프로토타입 체인의 종점 (Object.prototype도 없음)
const obj1 = Object.create(null);
obj1.toString(); // TypeError — Object.prototype 메서드 못 씀

// Object.prototype을 상속받는 객체 — {}와 동일
const obj2 = Object.create(Object.prototype);

// 임의의 객체를 프로토타입으로
const baseRecipe = { describe() { return `${this.name}은 요리다.`; } };
const focaccia = Object.create(baseRecipe);
focaccia.name = '포카치아';
focaccia.describe(); // '포카치아은 요리다.' — baseRecipe에서 상속

 

Object.create의 장점:

  • new 없이도 객체 생성 가능
  • 프로토타입을 명시적으로 지정하면서 생성 가능
  • 클래스나 생성자 함수 없이 객체 간 상속 가능

+) Object.create(null)로 만든 객체는 Object.prototype 메서드를 못 쓴다.

const obj = Object.create(null);
obj.name = '포카치아';
obj.hasOwnProperty('name'); // TypeError — 메서드 자체가 없음!

// ES2022에서 추가된 Object.hasOwn()을 쓰자
Object.hasOwn(obj, 'name'); // true — 이건 됨

 

ES6부터는 객체 리터럴 안에서 __proto__로 직접 상속도 가능하다

const baseRecipe = { describe() { return `${this.name}은 요리다.`; } };
const focaccia = {
  name: '포카치아',
  __proto__: baseRecipe // 직접 상속
};
focaccia.describe(); // '포카치아은 요리다.'

 

 

 

정적 프로퍼티/메서드 — 인스턴스 게 아닌 생성자 함수 자체의 것

function Recipe(name) { this.name = name; }
Recipe.defaultTemp = 200;           // 정적 프로퍼티
Recipe.createDefault = function() { // 정적 메서드
  return new Recipe('기본 레시피');
};
Recipe.prototype.cook = function() { return `${this.name} 굽는 중`; };

const focaccia = new Recipe('포카치아');
focaccia.cook();          // 작동 — 프로토타입 메서드
Recipe.createDefault();   // 작동 — 정적 메서드
focaccia.createDefault(); // TypeError — 인스턴스에서 접근 불가

 

정적 프로퍼티/메서드는 프로토타입 체인에 없다.
생성자 함수 자체에 바인딩된 것이라 인스턴스에 상속되지 않는다.

Array.from(), Object.keys(), Math.max() 같은 게 다 정적 메서드다.

MDN 문서에서는 정적 메서드와 프로토타입 메서드를 표기법으로 구분한다:

  • Object.create() → 정적 메서드
  • Object.prototype.hasOwnProperty() → 프로토타입 메서드 (줄여서 Object#hasOwnProperty로 쓰기도 함)

 

프로퍼티 존재 확인 — in이랑 hasOwnProperty가 다른 이유

 

- in 연산자

const recipe = { name: '포카치아' };

console.log('name' in recipe);     // true
console.log('toString' in recipe); // true ← Object.prototype 것도 잡힘!

 

in 연산자는 프로토타입 체인 전체에서 프로퍼티를 검색한다.
recipetoString은 없지만 Object.prototype에 있으니까 true가 나온다.

 

ES6에서 도입된 Reflect.hasin이랑 동일하게 동작한다.

Reflect.has(recipe, 'toString'); // true — in과 동일

 

- Object.prototype.hasOwnProperty 메서드

console.log(recipe.hasOwnProperty('name'));     // true
console.log(recipe.hasOwnProperty('toString')); // false — 자신의 것만

 

hasOwnProperty는 객체 자신의 프로퍼티만 확인한다.
상속받은 프로퍼티는 false를 반환한다.

 

 

프로퍼티 열거 — for...in 말고 Object.keys를 써라

 

- for...in

const recipe = { name: '포카치아', temp: 220 };

for (const key in recipe) {
  console.log(key); // name, temp
}

 

for...in은 상속받은 것까지 열거한다.
toString 같은 Object.prototype의 메서드가 안 나오는 건 [[Enumerable]]false이기 때문이다.

 

상속받은 프로퍼티가 아닌 자신의 것만 열거하려면

for (const key in recipe) {
  if (recipe.hasOwnProperty(key)) {
    console.log(key);
  }
}


- Object.keys/values/entries — 자신의 것만

const recipe = { name: '포카치아', __proto__: { temp: 220 } };

Object.keys(recipe);    // ['name'] — 자신의 열거 가능한 프로퍼티 키
Object.values(recipe);  // ['포카치아']
Object.entries(recipe); // [['name', '포카치아']]

 

for...in은 상속받은 프로퍼티까지 나와서 의도치 않은 버그가 생기기 쉽다.
자신의 프로퍼티만 열거하려면 Object.keys/values/entries를 쓰는 게 권장된다.

배열에는 for...in 대신 for...offorEach를 쓰자.
배열도 객체라 for...in을 쓰면 예상치 못한 프로퍼티가 같이 나올 수 있다.

 

 

V8이 프로토타입 체인을 최적화하는 방식

 

이건 ECMAScript 스펙 얘기가 아닌, V8 엔진의 구현 최적화 얘기다.
스펙은 "어떻게 찾아라"를 정의하고, V8은 그걸 빠르게 하는 방법을 알아서 구현한다.

V8은 인라인 캐시(IC) 를 통해 "이 객체에서 이 메서드를 찾으려면 몇 단계 올라가면 있다"를 기억해둔다.
같은 형태의 객체라면 다음번에는 체인을 다시 순회하지 않고 캐시에서 바로 꺼낸다.

그래서 "프로토타입 체인을 너무 길게 만들지 마라"는 조언이 나온다.
체인이 깊어질수록 검색 비용이 커지고 인라인 캐시가 효과를 못 발휘하기 때문이다.

 

 

20장 — strict mode

프론트엔드 입장에서는 이미 자동 적용되어있는거시다!!!! (뭣)

React/Next.js에서는 class와 ES 모듈이 기본값이라 strict mode가 자동으로 적용된다.

별도로 'use strict'를 선언할 필요가 없다는 뜻이다.

그래도 왜 생겼는지는 알고 있어야 하니까 적어보도록,,,,하겠읍니다.

 

 

왜 필요한가

 

function foo() {
  x = 10; // 선언 없이 할당
}
foo();
console.log(x); // 10 — 에러가 안 남!!
 

선언 안 한 변수에 값을 할당해도 JS 엔진이 전역 객체에 x를 동적으로 생성해버린다.

에러가 안 나니까 찾기도 어렵다. 이게 암묵적 전역이다.

 

strict mode를 켜면:

'use strict';

function foo() {
  x = 10; // ReferenceError: x is not defined
}
 
 

적용할 때는 즉시 실행 함수로 감싸자

 

전역에 바로 적용하면 non-strict mode인 외부 라이브러리랑 충돌할 수 있다.

(function() {
  'use strict';
  // 여기서만 적용
}());
 
 
 

주요 동작 변화 — 프론트에서 마주칠 수 있는 것

 

암묵적 전역 차단 — 선언 안 한 변수 할당하면 ReferenceError

일반 함수에서 this가 undefined가 된다

(function() {
  'use strict';
  function foo() { console.log(this); } // undefined
  foo();
}());
 

non-strict mode에서는 일반 함수의 this가 window였는데, strict mode에서는 undefined가 된다.

React 클래스 컴포넌트에서 bind(this)를 붙이는 이유도 이것과 연관이 있다 — 22장 this에서 제대로 다룰 예정.

 

21장 — 빌트인 객체

세 종류가 있다

- 표준 빌트인 객체 (Standard Built-in Objects)

 

ECMAScript 사양에 정의된 객체. 브라우저든 Node.js든 실행 환경 상관없이 항상 쓸 수 있다.
Object, String, Number, Array, Math, Promise, Date, RegExp... 40여 개.

 

- 호스트 객체 (Host Objects)

 

실행 환경이 추가로 제공하는 객체.
브라우저 → DOM, BOM, fetch, XMLHttpRequest
Node.js → fs, path, http

 

- 사용자 정의 객체 (User-defined Objects)

 

우리가 직접 만든 것들.

 

 

 

표준 빌트인 객체 — Math, Reflect, JSON 빼고 전부 생성자 함수

const strObj = new String('hello');
Object.getPrototypeOf(strObj) === String.prototype; // true

const numObj = new Number(1.5);
numObj.toFixed(); // 2 — Number.prototype의 메서드

Number.isInteger(0.5); // false — Number의 정적 메서드

 

생성자 함수인 표준 빌트인 객체는 프로토타입 메서드정적 메서드를 모두 제공한다.
Math, Reflect, JSON은 생성자 함수가 아니라 정적 메서드만 제공한다.
new Math()TypeError다.

 

 

래퍼 객체 — "잠깐 옷 입혀드렸다가 다시 벗는 거"

 

원시값인데 어떻게 메서드를 쓸 수 있는지 궁금해서 찾아봤는데...

const str = 'hello';
str.toUpperCase(); // 'HELLO' — str은 원시값인데 어떻게?

 

원시값에 마침표 표기법으로 접근하면 JS 엔진이 일시적으로 래퍼 객체를 생성하기 때문이다.

① str = 'hello'  (원시값)
② str.toUpperCase() 접근
   → String 래퍼 객체 생성, 'hello'를 [[StringData]] 슬롯에 저장
   → String.prototype의 메서드 사용 가능
③ 처리 완료
   → 래퍼 객체 폐기 (가비지 컬렉션 대상)
   → str은 다시 원시값 'hello'

 

래퍼 객체가 매번 새로 생성되기 때문에 뭔가 저장하려고 해도 못 한다:

const str = 'hello';
str.custom = 'test';          // 래퍼 객체 생성 → custom 추가 → 폐기
console.log(str.custom);      // undefined — 새 래퍼 객체라 custom 없음
console.log(typeof str, str); // string hello — 여전히 원시값

 

그래서 new String('hello'), new Number(1) 같이 명시적으로 래퍼 객체를 만드는 건 권장 안 한다

const str1 = 'hello';            // 원시값
const str2 = new String('hello'); // 래퍼 객체

console.log(typeof str1); // 'string'
console.log(typeof str2); // 'object' — 의도하지 않은 동작의 원인

 

nullundefined는 래퍼 객체를 생성하지 않는다.

그래서 null.something이 TypeError인 것이다.

숫자도 마찬가지다.

const num = 1.5;
console.log(num.toFixed());  // '2' — 래퍼 객체 Number 인스턴스가 임시 생성됨
console.log(typeof num);     // 'number' — 여전히 원시값

 

 

 

전역 객체 — 모든 것의 최상위

 

코드가 실행되기 이전에 JS 엔진이 가장 먼저 만드는 특수한 객체.

  • 브라우저: window
  • Node.js: global
  • ES11+: globalThis (표준화된 이름)

전역 객체가 갖는 것들

  • 표준 빌트인 객체 (Object, Array, Math...)
  • 호스트 객체 (DOM, fetch...)
  • var로 선언한 전역 변수, 전역 함수
var foo = 1;
console.log(window.foo); // 1 — var 전역변수는 전역 객체의 프로퍼티

bar = 2; // 암묵적 전역 — window.bar = 2
console.log(window.bar); // 2

 

let/const는 전역 객체의 프로퍼티가 아니다.

let foo = 123;
console.log(window.foo); // undefined

 

let/const는 전역 렉시컬 환경의 선언적 환경 레코드에 들어가기 때문이다.

 

 

빌트인 전역 프로퍼티

console.log(3 / 0);        // Infinity
console.log(-3 / 0);       // -Infinity
console.log(typeof Infinity); // 'number'

console.log(Number('xyz')); // NaN
console.log(typeof NaN);    // 'number' — "숫자가 아님"인데 타입이 숫자 ㅋㅋ
// IEEE 754 부동소수점 표준에서 NaN은 숫자 타입의 특수한 값이라 맞는 동작이다.

 

 

 

빌트인 전역 함수들 — 자주 쓰는데 정확히는 모르는 것들

 

- eval — 쓰지 말자

eval('var x = 2;');
console.log(x); // 2 — 런타임에 스코프를 동적으로 수정

 

보안에 취약하고, JS 엔진 최적화가 불가능해서 성능도 나쁘다. 쓸 이유가 없다.

 

- isFinite

isFinite(10);        // true
isFinite(Infinity);  // false
isFinite(NaN);       // false
isFinite(null);      // true — null을 0으로 변환하기 때문

 

isFinite(null)true라는 게 좀 의외였다.

null이 숫자로 변환되면 0이고, 0은 유한수니까 true. 이런 암묵적 변환이 JS다.

 

- isNaN vs Number.isNaN — 이름이 같은데 동작이 다르다

isNaN('hello');        // true — '문자열을 숫자로 바꾸면 NaN이니까 NaN이다'
isNaN(undefined);      // true
isNaN(null);           // false — 'null → 0 → NaN 아님'

Number.isNaN('hello'); // false — '문자열은 NaN이 아니다'
Number.isNaN(NaN);     // true  — 'NaN은 NaN이다'

 

전역 isNaN은 인수를 먼저 숫자로 변환한 다음 NaN인지 확인한다.
Number.isNaN은 변환 없이 "이 값 자체가 NaN인가"만 확인한다.
isNaN('hello')true라고 해서 'hello'가 NaN인 건 아니다.

실무에서는 Number.isNaN이 더 직관적이다.

 

- parseInt / parseFloat

parseInt('10', 2);   // 2 — '10'을 2진수로 해석해서 10진수 반환
parseInt('10', 16);  // 16
parseInt('0xf');     // 15 — 0x 접두어는 자동으로 16진수 해석
parseInt('0b10');    // 0 — 2진수 리터럴(0b)은 자동 해석 안 됨, 주의

const x = 15;
x.toString(2);        // '1111' — 10진수를 2진수 문자열로 변환
parseInt('1111', 2);  // 15 — 다시 2진수 문자열을 10진수로

 

- encodeURI / encodeURIComponent

const uri = 'http://example.com?name=림졍&job=developer';
encodeURI(uri);          // = ? & 인코딩 안 함 — 완전한 URI로 간주
encodeURIComponent(uri); // = ? & 까지 전부 인코딩 — 쿼리스트링 일부로 간주

 

실무에서 쿼리 파라미터를 직접 붙일 때 encodeURIComponent를 써야 한다.
encodeURI=, ?, &를 인코딩 안 해서 쿼리 파라미터 값에 이 문자들이 포함되면 의도치 않은 결과가 나온다.

Bean Tag 프로젝트에서 쿼리 파라미터 처리할 때 encodeURIComponent를 쓴 이유가 이번에 명확해졌다.

 

 

암묵적 전역 — 20장 내용이랑 연결되는 지점

var x = 10;

function foo() {
  y = 20; // 선언 없이 할당 — window.y = 20으로 해석됨
}
foo();

console.log(x + y); // 30

delete x; // 전역 변수는 delete 불가 (변수이기 때문)
delete y; // 프로퍼티는 삭제 가능

console.log(window.x); // 10
console.log(window.y); // undefined

 

y는 변수가 아니라 전역 객체의 프로퍼티로 추가된 것이기 때문에

  • 변수 호이스팅이 발생하지 않는다
  • delete로 삭제할 수 있다

20장에서 배운 strict mode가 이걸 ReferenceError로 잡아준다.
class와 모듈이 기본으로 strict mode를 적용하는 이유 중 하나가 암묵적 전역 방지다.

 

 

 

 

 

마무리 - 방금 죽은 림졍 대령이요.

 

19장만? 다 읽고 나니 솔직히 현타가 좀 왔다.

딥다이브에서 가장 긴 장이 괜히 그런 게 아니었음을 온몸으로 체감했다...

근데 파다 보니까  모든 게 프로토타입 체인으로 연결되는, 결국 하나의 패턴으로 수렴됐다.

hasOwnProperty를 왜 쓸 수 있는지, constructor가 어디서 오는지, instanceof가 어떻게 동작하는지

전부 "체인 타고 올라가서 찾는다"는 하나의 원리로 설명이 됐다.

 

스터디 발표에서 나왔던 스코프 체인이랑 프로토타입 체인 비교를 찾아봤더니 MDN이 딱 한 줄로 정리해놨다.

"unqualified identifier는 스코프 체인으로, qualified identifier는 프로토타입 체인으로." 이걸 찾는 재미가 있었다.

(나머지도 더 찾아봐야하는데.. ㄱ...그건 미래의 림졍에게 맡길래)

 

발표 때 class 슈거코드 얘기 제대로 못 했던 게 찜찜했는데,

이번에 파면서 "큰 틀은 맞는데 의도적으로 더 엄격하게 설계된 것들이 있다"는 결론까지 가게 됐다.

그냥 맞다 틀리다가 아니라 "절반은 맞고 절반은 더 정확한 말이 있다"를 스스로 정리한 게 뿌-듯하다.

 

이제 22장 this가 기다리고 있는데... 아직 개념이 머릿속에서 제대로 안 잡혀서 이번 주 내내 붙잡을 것 같다.

그나마 프로토타입이랑 스코프 체인을 제대로 짚고 가는 거니까 다행이라 생각하기로 했다.

아 얼레벌레 딥다이브 -완-.

 

 

 

 

 

앗차차, 추가로 삽질할 리스트

  • Symbol과 래퍼 객체 관계 — Symbol도 래퍼 객체를 생성하긴 하는데, 다른 원시값과 달리 리터럴로 만들 수 없고 Symbol()로만 생성 가능하다. 왜 이렇게 설계됐는지 아직 모름.
  • Number.isFinite vs 전역 isFinite — isNaN vs Number.isNaN이랑 똑같은 패턴인데, 전역 버전은 숫자로 변환 후 검사하고 Number. 버전은 변환 없이 검사한다. 이번 장에서 패턴은 봤는데 왜 ES6에서 이런 "엄격한 버전"들을 따로 추가했는지 더 파보고 싶다.
  • 래퍼 객체 V8 최적화 — 원시값에 마침표 찍을 때마다 래퍼 객체가 생성/폐기된다는데, 매번 힙에 할당하면 성능 이슈가 있지 않을까? 발표하신 분도 궁금해하셨는데 아직 답이 없는 것 같아서 나도 일단 미뤄두기로...
  • instanceof 내부 알고리즘 — "프로토타입 체인에 있는지 확인한다"는 건 알겠는데, ECMAScript 스펙에서 실제로 어떻게 구현되어 있는지 따라가보고 싶다.
  • Object.hasOwn()이 ES2022에 추가된 이유 — hasOwnProperty가 있었는데 왜 새 메서드가 필요했는지. Object.create(null) 케이스 때문이라는 건 알겠는데, 그것만으로 새 스펙을 추가할 이유가 됐는지 tc39 proposal 찾아보면 재밌을 것 같다.
  • 클래스 기반 언어(Java, C++) 비교 — 노트 쓰면서 "클래스 기반 언어는 이렇다더라"는 말을 계속 했는데, 정작 Java나 C++를 본 적이 없다. 언젠가 간단한 Java 코드를 직접 봐보면 JS 프로토타입이 뭐가 다른지 더 체감이 될 것 같다.

참고 자료

ECMAScript Living Standard

MDN Web Docs

728x90
반응형