• 클로저는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어 등에서 사용되는 중요한 특성
  • 클로저는 자바스크립트 고유의 개념이 아니므로 ECMAScript 사양에 들어가지 않음
  • MDN에서 정의한 클로저
    • "A closure is the combination of a function and the lexical environment within which that function was declared"
    • "클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다"
// innerFunc 함수가 outerFunc 함수의 내부에서 정의된 중첩 함수가 아니라면 
// innerFunc 함수를 outerFunc  함수의 내부에서 호출하더라라도 outerFunc 함수의 변수에 접근 불가
const x = 1;

function outerFunc() {
  const x = 10;
  innerFunc();
}

function innerFunc() {
  console.log(x); // 1; innerFunc은 전역에서 정의되었기 때문
}

outerFunc();

렉시컬 스코프

  • 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정
  • 스코프의 실체는 실행 컨텍스트의 렉시컬 환경. 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조 Outer Lexical Environment Reference를 통해 상위 렉시컬 환경과 연결. 이것이 스코프 체인
  • 렉시컬 스코프: 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값. 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정
const x = 1;

function foo() {
  const x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // 결과는?
bar(); // 결과는?

함수 객체의 내부 슬롯 [[Environment]]

  • 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장
  • 해당 함수가 호출되었을 때 생성될 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조 값
  • 외부 렉시컬 환경에 대한 참조에는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당 됨

함수 객체의 내부 슬롯 [[Environment]]에는 상위 스코프가 저장

 

클로저와 렉시컬 환경

  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조 가능. 이러한 중첩 함수를 클로저 closure라고 부름
  • 실행 컨텍스트가 실행 컨텍스트 스택에서 제거되어도 다른 곳에서 참조되어지고 있다면 해당 렉시컬 환경은 소멸하지 않음
  • 클로저는 중첩 함수가 상위 스코프 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적 (자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로는 모든 함수는 클로저)
  • 자유 변수 free variable: 클로저에 의해 참조되는 상위 스코프의 변수, 클로저는 함수가 자유 변수에 닫혀있다라는 의미
const x = 1;

function outer() {
  const x = 10;
  const inner = function () {
    console.log(x);
  };
  return inner;
}

// outer 함수를 호출하면 중첩함수 inner를 반환
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텐스트에서 팝되어 제거됨
const innerFunc = outer();
innerFunc(); // 10

전역 함수 객체의 상위 스코프 결정

 

중첩 함수의 상위 스코프 결정
outer 함수의 실행 컨텍스트가 제거되어도 outer 함수의 렉시컬 환경은 유지
외부 함수가 소멸해도 반환된 중첩 함수는 외부 함수의 변수를 참조할 수 있음

클로저의 활용

  • 클로저는 상태 state를 안전하게 변경하고 유지하기 위해 사용. 상태를 은닉 information hiding , 특정 함수에게만 상태 변경을 허용하게 함
// 카운트 상태 변수
let num = 0;

// 카운트 상태 변경 함수
const increase = function () {
  // 카운트 상태를 1만큼 증가시킴
  return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

// 오류 가능성
// 1. 카운트 상태(num변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야 함
// 2. 이를 위해 카운트 상태(num변수의 값)는 increase 함수만이 변경할 수 있어야 함
// 카운트 상태 변경 함수
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

  // 카운트 상태를 1만큼 증가시킴
  return ++num;
};

// 이전 상태를 유지하지 못함 - 함수가 호출될 때마다 새로운 렉시컬 환경 생성
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
// 카운트 상태 변경 함수
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;
  // 클로저
  return function () {
    return ++num;
  };
})();

// 카운트 증가 성공
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
const counter = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저인 메서드를 갖는 객체를 반환
  // 객체 리터럴은 스코프를 만들지 않음
  // 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경
  return {
    // num:0 - 프로퍼티는 public하므로 은닉되지 않음
    increase() {
      return ++num;
    },
    decrease() {
      return num > 0 ? --num : 0;
    },
  };
})();

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
console.log(counter.decrease()); // 0
// 위 코드를 생성자 함수로 표현할 경우
const Counter = (function () {
  // 카운트 상태 변수
  let num = 0;
  function Counter() {
    // this.num = 0 - 프로퍼티는 public하므로 은닉되지 않음
  }

  Counter.prototype.increase = function () {
    return ++num;
  };

  Counter.prototype.decrease = function () {
    return num > 0 ? --num : 0;
  };

  return Counter;
})();

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
console.log(counter.decrease()); // 0
// 함수를 인수로 전달받고 함수를 반환하는 함수
// 이 함수는 카운트 상태를 유지하기 위한 counter를 기억하는 클로저를 반환
function makeCounter(aux) {
  // 카운트 상태를 유지하기 위한 자유 변수
  let counter = 0;

  // 클로저 반환
  return function () {
    // 인수로 전달받은 보조 함수에 상태 변경을 위임
    counter = aux(counter);
    return counter;
  };
}

// 보조 함수
function increase(n) {
  return ++n;
}

function decrease(n) {
  return --n;
}

const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// makeCounter 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖음
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

makeCounter 함수를 두 번째 호출했을 때 생성된 렉시컬 환경

// 독립된 카운터가 아니라 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 함
// 이를 위해서는 makeCounter함수를 두 번 호출하지 말아야 함

// 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환
const counter = (function () {
  // 카운트 상태를 유지하기 위한 자유 변수
  let counter = 0;

  // 함수를 인수로 전달 받는 클로저를 반환
  return function (aux) {
    // 인수로 전달받은 보조 함수에 상태 변경을 위임
    counter = aux(counter);
    return counter;
  };
})();

// 보조 함수
function increase(n) {
  return ++n;
}

function decrease(n) {
  return --n;
}

// 보조 함수를 전달하여 호출
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2

// 자유 변수 공유
console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0

캡슐화와 정보 은닉

  • 캡슐화 encapsulation는 객체의 상태 state를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작 behavior인 메서드를 하나로 묶는 것. 이를 통해 객체의 특성 프로퍼티나 메서드를 감추는 목적(정보 은닉 information hiding)으로 사용하기도 함
  • 정보 보호 및 객체 간의 상호 의존성 (결합도 coupling)을 낮추는 효과가 있음
  • 대부분의 객체지향 프로그래밍 언어는 public, private, protectedt 같은 접근 제한자 access modifier를 선언하여 공개 범위를 한정 가능
  • 자바스크립트는 이제 private를 필드를 정의할 수 있는 새로운 사양을 도입하려는 듯? 기본적으로는 지원하지 않음
  • 흉내내는 방법은 한계가있어 근본적인 해결은 아님
function Person(name, age) {
  this.name = name; // public
  let _age = age; // private

  // 인스턴스 메서드
  this.sayHi = function () {
    console.log(`Hi! my name is ${this.name}. I am ${_age}`);
  };
}

const me = new Person("Jin", 29);
me.sayHi(); // Hi! my name is Jin. I am 29
console.log(me.name); // Jin
console.log(me._age); // undefined

// sayHi 메서드는 인스턴스 메서드이므로 Person 객체가 생성될 때마다 중복 생성됨
function Person(name, age) {
  this.name = name; // public
  let _age = age; // private
}
// 프로토타입 메서드
Person.prototype.sayHi = function() {
  // Person 생성자 함수의 지역 변수 _age를 참조할 수 없음
  console.log(`Hi! my name is ${this.name}. I am ${_age}`)
}
// 즉시 실행 함수를 사용하여 Person 생성자 함수와 prototype 메서드를 하나의 함수에 모아보자
const Person = (function () {
  let _age = 0;

  // 생성자 함수
  function Person(name, age) {
    this.name = name; // public
    _age = age;
  }

  // 프로토타입 메서드
  Person.prototype.sayHi = function () {
    console.log(`Hi! my name is ${this.name}. I am ${_age}`);
  };

  // 생성자 함수 반환
  return Person;
})();

const me = new Person("Jin", 29);
me.sayHi(); // Hi! my name is Jin. I am 29
console.log(me.name); // Jin
console.log(me._age); // undefined

const you = new Person("Park", 20);
you.sayHi(); // Hi! my name is Park. I am 20
console.log(you.name); // Park
console.log(you._age); // undefined

// 문제점
me.sayHi(); // Hi! my name is Jin. I am 20
// Person.prototype.sayHi 메서드가 단 한번 생성되는 클로저
// Person.prototype.sayHi 메서드는 모든 인스턴스가 하나의 동일한 상위 스코프를 사용

자주 발생하는 실수

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = function () {
    return i;
  };
}

for (var j = 0; j < 3; j++) {
  console.log(funcs[j]());
}

// 3
// 3
// 3

// var는 블록 레벨 스코프가 아닌 함수 레벨 스코프를 갖기 때문에 전역 변수
var funcs = [];

for (let i = 0; i < 3; i++) {
  funcs[i] = function () {
    return i;
  };
}

for (var j = 0; j < 3; j++) {
  console.log(funcs[j]());
}

// 0
// 1
// 2

// let은 블록 레벨 스코프를 가져, 매 반복마다 렉시컬 스코프를 생성
// 고차 함수와 화살표 함수를 사용한 방법
const funcs = Array.from(new Array(3), (_, i) => () => i);

funcs.forEach((f) => console.log(f()));
// 0
// 1
// 2
  • ECMAScript 사양은 소스코드를 4가지 타입으로 구분. 4가지 타입의 소스코드는 실행 컨텍스트를 생성
  • 소스코드의 타입에 따라 실행 컨텍스트르 생성하는 과정과 관리 내용이 다르기 때문
소스코드의 타입 설명
전역 코드 global code - 전역에 존재하는 소스코드. 전역에 정의된 함수, 클래스 등의 내부 코드는 포함되지 않음
- 전역 변수를 관리하기 위해 최상위 스코프인 전역 스코프 생성
- var키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수를 전역 개체의 프로퍼티와 메서드로 바인딩하고 참조하기 위해 전역 변수와 연결
- 전역 코드가 평가되면 전역 실행 컨텍스트 생성 
함수 코드 function code - 함수 내부에 존재하는 소스코드. 함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않음
- 지역 스코프를 생성하고 지역변수, 매개변수, arguments 객체를 관리
- 생성한 지역 스코프를 전역 스코프에서 시작하는 스코프 체인의 일원으로 연결
- 함수 코드가 평가되면 함수 실행 컨텍스트 생성
eval 코드 eval code - 빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 코드
- strict mode에서 자신만의 독자적인 스코프 생성
- eval코드가 평가되면 eval 컨텍스트가 생성
모듈 코드 module code - 모듈 내부에 존재하는 소스코드. 모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않음
- 모듈별로 독립적인 스코프 생성
- 모듈 코드가 평가되면 모듈 실행 컨텍스트 생성

소스코드의 평가와 실행

  • 자바스크립트는 "소스코드의 평가"와 "소스코드의 실행" 과정으로 나누어 처리
  • 소스코드의 평가: 실행 컨텍스트를 생성하고 변수, 함수 등의 선언문만 먼저 실행하여 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프 (렉시컬 환경의 환경 레코드)에 등록
  • 소스코드의 실행: 선언문을 제외한 소스코드가 순차적으로 실행 (런타임). 소스코드 실행에 필요한 정보, 즉 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색하여 취득. 변수 값의 변경 등 소스코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록 

소스코드의 평가와 실행
소스 코드의 평가와 실행 예시

실행 컨텍스트의 역할

  • 실행 컨텍스트는 소스코드를 실행하는데 필요한 환경을 제공하고 실행 결과를 실제로 관리하는 영역
    • 선언에 의해 생성된 모든 식별자(변수, 함수, 클래스 등)를 스코프를 구분하여 등록하고 상태 변화(식별자에 바인딩된 값의 변화)를 지속적으로 관리해야 함
    • 스코프는 중첩 관계에 의해 스코프 체인을 형성해야 함. 즉 스코프 체인을 통해 상위 스코프로 이동하며 식별자를 검색할 수 있어야 함
    • 현재 실행 중인 코드의 실행 순서를 변경(함수의 호출에 의한 변경 등)할 수 있어야 하며 다시 되돌아갈 수도 있어야 함
  • 실행 컨텍스트는 식별자(변수, 함수, 클래스 등의 이름)를 등록하고 관리하는 스코프와 코드 실행 순서를 관리를 구현한 내부 메커니즘. 모든 코드는 실행 컨텍스트를 통해 실행되고 관리
    • 식별자와 스코프: 실행 컨텍스트의 렉시컬 환경
    • 코드 실행 순서: 실행 컨텍스트 스택

실행 컨텍스트 스택

  • 실행 컨텍스트 스택은 코드의 실행 순서를 관리
  • 실행 컨텍스트는 스택 stack 자료 구조로 관리
  • 실행 중인 실행 컨텍스트 running execution context: 실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트. 현재 실행 중인 코드의 실행 컨텍스트
const x = 1;

function foo() {
  const y = 2;

  function bar() {
    const z = 3;
    console.log(x + y + z);
  }

  bar();
}

foo(); // 6

실행 컨텍스트 스택

렉시컬 환경

  • 렉시컬 환경 lexical environment는 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 차몾를 기록하는 자료구조로 실행 컨텍스트르 구성하는 컴포넌트
  • 렉시컬 환경은 스코프와 식별자를 관리
  • 실행 컨텍스트는 LexicalEnvironment 컴포넌트와 VariableEnvironment로 구성되어 있고, 초기에는 하나의 동일한 렉시컬 환경을 참조하나, 몇 가지 상황에 달라지는 경우가 있으나 이번 정리에서는 구분하지 않음
  • 렉시컬 환경은 두개의 컴포넌트로 구성
    • 환경 레코드 environment record
      • 스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소
      • 환경 레코드는 소스코드의 타입에 따라 관리하는 내용에 차이가 있음
    • 외부 렉시컬 환경에 대한 참조 outer lexical environment reference
      • 외부 렉시컬 환경에 대한 참조는 상위 스코프를 가리킴
      • 상위 스코프란 외부 렉시컬 환경, 즉 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위 코드의 렉시컬 환경을 뜻함
      • 외부 렉시컬 환경에 대한 참조를 통해 단방향 링크드 리스트인 스코프 체인을 구현

렉시컬 환경과 스코프 체인
렉시컬 환경의 구성 컴포넌트

실행 컨텍스트의 생성과 식별자 검색 과정

var x = 1;
const y = 2;

function foo(a) {
  var x = 3;
  const y = 4;

  function bar(b) {
    const z = 5;
    console.log(a + b + x + y + z);
  }
  bar(10);
}

foo(20); // 42

전역 객체 생성

전역 코드 평가

  1. 전역 실행 컨텍스트 생성
  2. 전역 렉시컬 환경 생성
    1. 전역 환경 레코드 생성
      1. 객체 환경 레코드 생성
      2. 선언적 환경 레코드 생성
    2. this 바인딩
    3. 외부 렉시컬 환경에 대한 참조 결정

전역코드 실행

  • 식별자 결정 indentifier resolution: 어느 스코프의 식별자를 참조하면 되는지 결정, 실행 중인 실행 컨텍스트에서 식별자 검색 시작

전역 실행 컨텍스트와 렉시컬 환경

foo 함수 코드 평가

  1. 함수 실행 컨텍스트 생성
  2. 함수 렉시컬 환경 생성
    1. 함수 환경 레코드 생성
    2. this 바인딩
    3. 외부 렉시컬 환경에 대한 참조 결정
      • 함수를 어디서 호출했는지가 아니라 어디에 정의했는지에 따라 상위 스코프 결정

foo 함수 코드 실행

  • 식별자 결정을 위해 실행 컨텍스트의 렉시컬 환경에서 식별자를 검색하기 시작
  • 만약 실행 중인 실행 컨텍스트의 렉시컬 환경에서 식별자를 검색할 수 없으면 외부 렉시컬 환경에 대한 참조가 가리키는 렉시컬 환경으로 이동하여 식별자를 검색

foo 함수 코드의 실행

bar 함수 코드 평가

  • foo 코드 평가와 같은 실행 컨텍스트와 렉시컬 환경의 생성 과정 진행

bar 함수 코드 실행

  1. console 식별자 검색
  2. log 메서드 검색
  3. 표현식 a + b + x + y + z의 평가
    • 현재 실행 중인 실행 컨텍스트의 렉시컬 환경에서 시작하여 외부 렉시컬 환경에 대한 참조로 이어지는 렉시컬 환경의 연속에서 검색
  4. console.log 메서드 호출

bar 함수 코드의 실행

bar 함수 코드 실행 종료

bar 함수 코드 종료

foo 함수 코드 실행 종료

foo 함수 코드 종료

전역 코드 실행 종료

  • 실행 컨텍스트 스택에는 아무것도 남지 않게 됨

실행 컨텍스트와 블록 레벨 스코프

  • let, const 키워드로 선언한 변수는 모든 코드 블록(함수, if 문, for 문, while문, try/catch 문 등)을 지역 스코프로 인정하는 블록 레벨 스코프 block-level scope를 따름
  • 해당 코드 블록이 실행되면 해당 코드 블록을 위한 블록 레벨 스코프가 생성하고, 선언적 환경 레코드를 갖는 렉시컬 환경을 새롭게 생성하여 기존의 전역 렉시컬 환경을 교체
  • 이 때 새롭게 생성된 코드 블록을 위한 외부 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 해당 코드 블록이 실행되기 이전의 전역 렉시컬 환경을 가리킴
  • 참고: var 키워드로 선언한 변수는 오로지 함수의 코드 블록만 지역 스코프로 인정하는 함수 레벨 스코프를 따름
let x = 1;

if (true) {
  let x = 10;
  console.log(x); // 10
}
console.log(x); // 1

if 문의 코드 블록이 실행되면 새로운 렉시컬 환경을 생성하여 기존의 렉시컬 환경을 교체
if 문의 코드 블록을 위한 렉시컬 환경에서 이전 렉시컬 환경으로 복귀

this 키워드

  • this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수 self-referencing variable
  • this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있음
  • this를 가리키는 값, this 바인딩은 함수 호출 방식에 의해 동적으로 결정 / 클래스 기반 언어인 자바나 c++에서는 언제나 클래스가 생성하는 인스턴스를 가리킴
  • this는 객체의 프로퍼티나 메서드를 참조하기 위한 자기 참조변수이므로 일반 함수에서는 의미가 없음. strict mode가 적용된 일반 함수의 내부의 this에는 undefined가 바인딩
// this는 어디서든지 참조 가능
// 전역에서 this는 전역 객체 window를 가리킴
console.log(this); // Window

function square(number) {
  // 일반 함수 내부에서 this는 전역 객체 window를 가리킴
  console.log(this); // Window
  return number * number;
}
console.log(square(2)); // 4

function strictFn() {
  "use strict"; // srict mode 적용된 일반 함수 내부의 this는 undefined가 바인딩
  console.log(this); // undefined
}
strictFn();

const person = {
  name: "Jin",
  getName() {
    // 메서드 내부에서 this는 메서드를 호출한 객체를 가리킴
    console.log(this); // {name: 'Jin', getName: ƒ}
    return this.name;
  },
};
console.log(person.getName()); //Jin

function Person(name) {
  this.name = name;
  // 생성자 함수 내부에서 this는 생성자 함수가 생성할 인스턴스를 가리킴
  console.log(this); // Person {name: 'Jin'}
}

const me = new Person("Jin");

함수 호출 방식과 this 바인딩

  • this 바인딩(this에 바인딩될 값)은 함수 호출 방식, 즉 함수가 어떻게 호출되었는가에 따라 동적으로 결정
  • 비교: 함수의 상위 스코프를 결정하는 방식인 렉시컬 스코프는 함수 정의가 평가되어 함수 객체가 생성되는 시점에 상위 스코프 결정
  • 함수 호출하는 방식
    • 일반 함수 호출
    • 메서드 호출
    • 생성자 함수 호출
    • Function.prototype.apply/call/bind 메서드에 의한 간접 호출
// this 바인딩은 함수 호출 방식에 따라 동적으로 결정
const foo = function () {
  console.dir(this);
};

// 일반 함수 호출
// foo 함수 내부의 this는 전역 객체 window를 가리킴
foo(); // Window

// 메서드 호출
// foo 함수 내부의 this는 호출한 객체 obj를 가리킴
const obj = { foo };
obj.foo(); // obj

// 생성자 함수 호출
// foo 함수 내부의 this는 생성자 함수가 생성한 인스턴스를 가리킴
new foo(); // foo

// Function.prototype.apply/call/bind 메서드에 의한 간접 호출
// foo 함수 내부의 this는 인수에 의해 결정
const bar = { name: "bar" };
foo.call(bar); // bar
foo.apply(bar); // bar
foo.bind(bar)(); // bar

일반 함수 호출

  • 일반 함수로 호출된 모든 함수(중첩 함수, 콜백 함수 포함) 내부의 this에는 전역 객체가 바인딩
    • this 대신 콜백함수 내에서 this를 할당한 변수 사용
    • Function.prototype.apply/call/bind 메서드를 사용하여 명시적으로 this 바인딩
    • 화살표 함수 (권장)
  • strict mode가 적용된 일반 함수의 내부에는 undefined 바인딩
var value = 1;

const obj = {
  value: 100,
  foo() {
    console.log("foo's this: ", this); // foo's this:  {value: 100, foo: ƒ}
    setTimeout(function () {
      console.log("callback's this: ", this); // callback's this:  Window
      console.log(this.value); // 1
    }, 100);
  },
};

obj.foo();
// this 대신 콜백함수 내에서 this를 할당한 변수 사용
var value = 1;

const obj = {
  value: 100,
  foo() {
    const that = this;
    console.log("foo's this: ", that); // foo's this:  {value: 100, foo: ƒ}
    setTimeout(function () {
      console.log("callback's this: ", that); // callback's this:  {value: 100, foo: ƒ}
      console.log(that.value); // 100
    }, 100);
  },
};

obj.foo();
// Function.prototype.apply/call/bind 메서드를 사용하여 명시적으로 this 바인딩
var value = 1;

const obj = {
  value: 100,
  foo() {
    console.log("foo's this: ", this); // foo's this:  {value: 100, foo: ƒ}
    setTimeout(
      function () {
        console.log("callback's this: ", this); // callback's this:  {value: 100, foo: ƒ}
        console.log(this.value); // 100
      }.bind(this),
      100
    );
  },
};

obj.foo();
// 화살표 함수

const obj = {
  value: 100,
  foo() {
    console.log("foo's this: ", this); // foo's this:  {value: 100, foo: ƒ}
    setTimeout(() => {
      console.log("callback's this: ", this); // callback's this:  {value: 100, foo: ƒ}
      console.log(this.value); // 100
    }, 100);
  },
};

obj.foo();

메서드 호출

  • 메서드 내부의 this는 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩 됨
function Person(name) {
  this.name = name;
}

Person.prototype.getName = function () {
  return this.name;
};

const me = new Person("Jin");

// getName 메서드를 호출한 객체는 me
console.log(me.getName()); // Jin

Person.prototype.name = "Park";

// getName 메서드를 호출한 객체는 Person.prototype
console.log(Person.prototype.getName()); // Park

프로토타입 메서드와 this 바인딩

생성자 함수 호출

  • 생성자 함수 내부의 this에는 생성자 함수가 (미래에) 생성할 인스턴스가 바인딩
  • new 연산자와 함께 호출하지 않으면 생성자 함수가 아닌 일반 함수의 호출임
// 생성자 함수
function Circle(radius) {
  // 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킴
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

const circle1 = new Circle(5);
const circle2 = new Circle(10);
// new 연산자와 함께 호출하지 않으면 생성자 함수로 동작 x, 일반 함수 호출임
const circle3 = Circle(15);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20
// 일반 함수로 호출된 Circle 내부의 this는 전역 객체를 가리킴
console.log(radius); // 15

Function.prototype.apply/call/bind 메서드에 의한 간접 호출

  • apply, call, bind 메서드는 Function.prototype의 메서드이므로 모든 함수가 상속 받아 사용 가능
  • apply와 call의 본질적인 기능은 함수를 호출하는 것
    • apply와 call은 호출 함수에 인수를 전달하는 방식만 다를 뿐 동일하게 동작
      • apply 메서드: 호출할 함수의 인수를 배열로 묶어 전달
      • call 메서드: 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달
    • 대표적인 용도는 arguments 같은 유사 배열 객체에 배열 메서드를 사용하는 경우
  • bind는 첫 번째 인수로 전달한 값으로 this 바인딩이 교체된 함수를 새롭게 생성해 반환. apply와 call과 달리 함수를 호출하지 않기 때문에 명시적으로 호출해줌
function getThisBinding() {
  console.log(arguments);
  return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

// getThisBinding 함수를 호출하면서 인수로 전달된 객체를 getThisBinding 함수의 this에 바인딩
// apply 메서드는 호출할 함수의 인수를 배열로 묶어 전달
console.log(getThisBinding.apply(thisArg, [1, 2, 3]));
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// {a: 1}

// call 메서드는 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달
console.log(getThisBinding.call(thisArg, 1, 2, 3));
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// {a: 1}
function convertArgsToArray() {
  console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3 }

  // arguments 객체를 배열로 변환
  // Array.prototype.slice를 인수 없이 호출하면 배열의 복사본 생성
  const arr = Array.prototype.slice.apply(arguments);
  console.log(arr); // [ 1, 2, 3 ]
  return arr;
}

convertArgsToArray(1, 2, 3); // [ 1, 2, 3 ]
function getThisBinding() {
  return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

// bind 메서드는 첫 번째 인수로 전달한 thisArg로 this바인딩이 교체된 함수를 새롭게 생성해 반환
console.log(getThisBinding.bind(thisArg)); // [Function: bound getThisBinding]
// bind 메서드는 함수를 호출하지는 않으므로 명시적으로 호출해야 함
console.log(getThisBinding.bind(thisArg)()); // { a: 1 }
// 메서드 내부의 중첩 함수 또는 콜백 함수의 this가 불일치하는 문제
const person = {
  name: "Jin",
  foo(callback) {
    setTimeout(callback, 100);
  },
};

person.foo(function () {
  console.log(`Hi! my name is ${this.name}.`); // Hi! my name is . ; 브라우저 환경의 window.name은 ""
});
// bind 메서드는 this와 메서드 내부의 중첩 함수 또는 콜백 함수의 this가 불일치하는 문제를 해결하기 위해 사용됨
const person = {
  name: "Jin",
  foo(callback) {
    // bind 메서드로 callback 함수 내부의 this 바인딩을 전달
    setTimeout(callback.bind(this), 100);
  },
};

person.foo(function () {
  console.log(`Hi! my name is ${this.name}.`); // Hi! my name is Jin.
});
함수 호출 방식 this 바인딩
일반 함수 호출  전역 객체
메서드 호출 메서드를 호출한 객체
생성자 함수 호출 생성자 함수가 (미래에) 생성할 인스턴스
Function.prototype.apply/call/bind 메서드에 의한 간접 호출 메서드에 첫 번째 인수로 전달한 객체

자바스크립트 객체의 분류

  • 표준 빌트인 객체 standard built-in objects / native objects / global objects
    • ECMAScript 사양에 정의된 객체 (브라우저 또는 Node.js환경에 관계 없이 사용 가능 )
    • 애플리케이션 전역 공통의 기능 제공
    • 전역 객체의 프로퍼티로 제공, 별도의 선언 없이 전역 변수처럼 언제나 참조 가능
  • 호스트 객체 host objects
    • ECMAScript 사양에 정의되지 않았지만 실행ㄹ 환경에서 추가로 제공하는 기능
    • 브라우저 환경: DOM, BOM, Canvas, XMLHttpRequest, fetch, requestAnimationFrame, SVG, Web Storage, Web Component, Web Worker  같은 클라이언트 사이드 Web API
    • Node.js: Node.js의 고유의 API 제공
  • 사용자 정의 객체  user-defined objects
    • 기본 제공되는 객체가 아닌 사용자가 직접 정의한 객체

표준 빌트인 객체

  • 자바스크립트는 Object, String, Number, Boolean, Symbol, Date, Math, RegExp, Array, Map/Set, WeakMap, WeakSet, Function, Promise, Reflect, Proxy, JSON, Error 등 40여개의 표준 빌트인 객체 제공
  • Math, Reflect, JSON을 제외한 표준 빌트인 객체는 모두 인스턴스를 생성할 수 있는 생성자 함수
  • 생성자 함수인 표준 빌트인 객체가 생성한 인스턴스의 프로토타입은 표준 빌트인 객체의 prototype  프로퍼티에 바인딩
  • 표준 빌트인 객체는 인스턴스 없이도 호출 가능한 빌트인 정적 메서드 제공 
// String 생성자 함수에 의한 String 객체 생성
const strObj = new String("Jin");
console.log(typeof strObj); // object

// String 생성자 함수를 통해 생성한 strObj 객체의 프로토타입은 String.prototype
console.log(Object.getPrototypeOf(strObj) === String.prototype); // true
// Number 생성자 함수에 의한 Number 객체 생성
const numObj = new Number(1.5);

// toFixed는 Number.prototype의 프로토타입 메서드
console.log(numObj.toFixed()); // 2;

// isInteger는 Number의 정적 메서드
console.log(Number.isInteger(3)); // true

원시 값과 래퍼 객체

  • 문자열이나, 숫자, 불리언 등의 원시값이 있는데 왜 String, Number, Boolean 같은 생성자 함수가 존재할까?
  • 문자열, 숫자, 불리언, 심벌은 암묵적으로 생성되는 래퍼 객체에 의해 마치 객체처럼 사용할 수 있음
  • 래퍼 객체 wrapper object: 문자열, 숫자, 불리언 값에 대해 객체처럼 접근하면 생성되는 임시 객체
  • 래퍼 객체의 처리가 종료되면 래퍼 객체의 [[StringData]], [[Number Data]] 등 내부 슬롯에 할당된 원시 값으로 되돌리고 래퍼 객체는 가비지 컬렉션의 대상이 됨
  • 래퍼 객체 참조용이므로 new 연산자와 함께 호출하여 문자열, 숫자, 불리언, 객체 인스턴스 생성하는 것은 권장하지 않음
  • null과 undefined는 생성자 함수가 없어 래퍼 객체가 없으므로 객체처럼 생성하면 에러 발생
const str = "hi";

// 원시 타입이 문자열이 래퍼 객체인 String 인스턴스로 변환
console.log(str.length); // 2
console.log(str.toUpperCase()); // HI

// 래퍼 객체로 프로퍼티에 접근하거나 메서드를 호출한 후, 다시 원시 값으로 되돌림
console.log(typeof str); // string

문자열 래퍼 객체의 프로토타입 체인

전역 객체

  • 전역 객체 global object는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체, 어떤 객체에도 속하지 않은 최상위 객체
  • 브라우저 환경에는 window(또는 self, this, frames) / Node.js 환경에는 global 전역 객체를 가리킴
  • 전역 객체는 개발자가 의도적으로 생성 불가능, 전역 변수를 생성할 수 있는 생성자 함수 제공 x
  • 전역 객체의 프로퍼티를 참조할 때 window(또는 global) 생략 가능
  • 전역 객체는 모든 표준 빌트인 객체를 프로퍼티로 가지고 있음
  • 자바스크립트 실행환경(브라우저 or Node.js)에 따라 추가적으로 프로퍼티와 메서드를 가짐
  • var 키워드로 선언한 전역 변수와 선언하지 않은 변수에 변수에 값을 할당한 암묵적 전역, 전역 함수는 전역 객체의 프로퍼티가 됨
  • let, const 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아닌 보이지 않는 개념적 블록(전역 렉시컬 환경의 선언적 환경 레코드) 내에 존재
  • 브라우저 환경의 모든 자바스크립트 코드는 하나의 전역 객체 window를 공유. 여러 개의  script 태그를 통해 자바스크립트 코드를 분리해도 하나의 전역 객체를 공유함

빌트인 전역 프로퍼티

  • 빌트인 전역 프로퍼티 built-in global property는 전역 객체의 프로퍼티 의미, 즉 애플리케이션 전역에서 사용하는 값 제공

Infinity

  • Infinity는 프로퍼티는 무한대를 나타내는 숫자값 Infinity를 갖음
// 전역 프로퍼티는 window를 생략하고 참조 가능
console.log(window.Infinity === Infinity); // true

// 양의 무한대
console.log(1 / 0); // Infinity
// 음의 무한대
console.log(-1 / 0); // -Infinity
// Infinity는 숫자 값
console.log(Infinity); // Infinity

NaN

  • NaN 프로퍼티는 숫자가 아님(Not-a-Number)을 나타내는 숫자값 NaN을 갖음
  • Number.NaN 프로퍼티와 같음
console.log(window.NaN); // NaN

console.log(Number("xyz")); // NaN
console.log(1 * "string"); // NaN
console.log(typeof NaN); // "number"

undefined

  • unfined 프로퍼티는 원시 타입 undefined를 값으로 갖음
console.log(window.undefined); // undefined

var foo;
console.log(foo); // undefined
console.log(typeof undefined); // "undefined"

빌트인 전역 함수

  • 빌트인 전역 함수 built-in global function은 애플리케이션 전역에서 호출할 수 있는 빌트인 함수로서 전역 객체의 메서드

eval

  • eval 함수는 자바스크립트 코드를 나타내는 문자열을 인수로 전달 받음
    • 전달 받은 문자열 코드가 표현식: 문자열 코드를 런타임에 평가하여 값 생성
    • 전달 받은 문자열 코드가 표현식이 아닌 문: 문자열 코드를 런타임에 실행
    • 문자열 코드가 여러 개의 문: 모든 문 실행
    • eval 함수는 기존의 스코프를 런타임에 동적으로 수정
      • strict mode에서는 기존의 스코프를 수정하지 않고 eval 함수 자신의 자체적인 스코프 생성
      • 인수로 전달 받은 문자열 코드가 let, const 키워드를 사용한 변수 선언문이면, 암묵적으로 strict mode 적용
    • eval 함수 사용은 금지해야 함
      • eval 함수를 통해 사용자로부터 입력 받은  콘텐츠 untrusted data를 실행하는 것은 보안에 취약
      • eval 함수를 통해 실행되는 코드는 자바스크립트 엔진에 의해 최적화가 수행되지 않으므로 처리가 느림
// 표현식인 문
console.log(eval("1 + 2;")); // 2
// 표현식이 아닌 문
console.log(eval("var x = 5;")); // undefined
// eval 함수에 의해 런타임에 변수 선언문이 실행되어 x 변수가 선언
console.log(x); // 5

// 객체 리터럴, 함수 리터럴은 반드시 괄호로 둘러쌈
const o = eval("({a: 1})");
console.log(o); // { a: 1 }

const f = eval("(function() {return 1;})");
console.log(f()); // 1

// eval 함수는 자신이 호출된 위치에서 해당하는 기존의 스코프를 런타임에 동적으로 수정

const z = 1;

function foo() {
  console.log(z); // 1 - 선언 단계에 실행되었다면 undefined가 되었어야 함
  // eval 함수는 런타임에 foo 함수의 스코프를 동적으로 수정
  eval("var z = 2;");
  console.log(z); // 2
}

foo();
console.log(z); // 1

isFinite

  • 전달받은 인수가 유한수이면 true / 무한수이면 false를 반환
  • 전달 받은 인수의 타입이 숫자가 아닌 경우, 숫자로 타입을 변환 후 검사 실행 (NaN은 false 반환, null은 true 반환)

inNaN

  • 전달 받은 인수가 NaN인지 검사하여 결과를 불리언으로 반환
  • 전달 받은 인수의 타입이 숫자가 아닌 경우, 숫자로 타입을 변환 후 검사 실행

 parseFloat

  • 전달받은 문자열 인수를 부동 소수점 숫자 floating point number, 즉 실수로 해석 parsing하여 반환

parseInt

  • 전달받은 문자열 인수를 정수 integer로 해석하여 반환
  • 전달받은 인수가 문자열이 아니면 문자열로 변환한 다음 정수로 해석하여 반환
  • 두 번째 인수로 진법을 나타내는 기수 (2 ~ 36) 전달 가능, 이때 인수로 전달된 문자열을 해당 기수의 숫자로 해석하여 10진수로 해석하여 반환
    • 10진수를 해당 기수의 문자열로 변경하고 싶을 경우 Number.prototype.toString 메서드 사용
  • 첫 번째 인수로 전달된 문자열이 "0x" 또는 "0X"로 시작하는 16진수 리터럴이면 16진수로 해석하여 10진수로 반환(2진수와 8진수 리터럴은 해당 안됨) 
  • 앞 뒤 공백은 무시하고 중간에 공백이 있으면 공백으로 구분된 앞에만 해석하여 반환
// 전달 받은 문자열 인수를 문자열이 아니면 문자열로 변환하고 정수로 해석하여 반환
console.log(parseInt(10.123)); // 10

// "10"을 8진수로 해석하고, 그 결과를 10진수 정수 반환
console.log(parseInt("10", 8)); // 8
// Number.prototype.toString 메서드를 사용하여 10을 8진수로
console.log((10).toString(8)); // 12

// 16진수 리터럴 자동으로 16진수로 해석하여 10진수로 반환
console.log(parseInt("0x0000000f")); // 15

// 앞뒤 공백 무시, 중간에 공백이 있을 경우 공백으로 구분된 앞에만 해석하여 반환
console.log(parseInt(" 0x0000000f 34 ")); // 15

encodeURI / decodeURI

  • encodetURI 함수는 완전한 URI Uniform Resource Identifier를 문자열로 전달받아 이스케이프 처리를 위해 인코딩
  • URI는 인터넷에 있는 자원을 나타내는 유일한 주소를 말함. 하위 개념으로 URL, URN이 있음
  • URI 문법 형식 표준 RFC3986에 따르면 URL은 아스키 문자 셋으로만 구성되어야 하며, 쿼리 스트링 구분자로 사용되는 (=, ? , &)을 제외하고 이스케이프 처리 

URI

const uri = "http://example.com?name=진홍엽&status=jobless&hopeful";
// encodeURI 함수는 완전한 URI를 전달받아 이스케이프 처리를 위해 인코딩
const enc = encodeURI(uri);
console.log(enc); // http://example.com?name=%EC%A7%84%ED%99%8D%EC%97%BD&status=jobless&hopeful

// decodeURI 함수는 인코딩된 URI를 전달받아 이스케이프 처리 이전으로 디코딩
const dec = decodeURI(enc);
console.log(dec); // http://example.com?name=진홍엽&status=jobless&hopeful

encodeURIComponent / decodeURIComponent

  • URI 구성요소를 인수로 전달받아 인코딩
  • 쿼리 스트링 구분자로 사용되는 =, ?, &까지 인코딩하는 차이가 있음
const uriComp = "name=진홍엽&status=jobless&hopeful";

let enc = encodeURIComponent(uriComp);
console.log(enc); // name%3D%EC%A7%84%ED%99%8D%EC%97%BD%26status%3Djobless%26hopeful
let dec = decodeURIComponent(enc);
console.log(dec); // name=진홍엽&status=jobless&hopeful

enc = encodeURI(uriComp);
console.log(enc); // name=%EC%A7%84%ED%99%8D%EC%97%BD&status=jobless&hopeful
dec = decodeURI(enc);
console.log(dec); // name=진홍엽&status=jobless&hopeful

암묵적 전역

  • 암묵적 전역 implicit global: 선언하지 않은 식별자에 값을 할당하면 전역 객체의 프로퍼티를 동적 생성하여 전역 변수처럼 동작
  • 전역 객체의 프로퍼티이므로 변수가 아님. 따라서 변수호이스팅이 발생하지 않음
  • 변수가 아니라 단지 프로퍼티이므로 delete로 삭제 가능, 전역 변수는 프로퍼티이지만 delete로 삭제 불가능
// 전역 변수 x는 호이스팅 발생
console.log(x); // undefined
// 전역 객체의 프로퍼티 y는 호이스팅 발생 x
// console.log(y); // ReferenceError: y is not defined

var x = 10; // 전역 변수

function foo() {
  y = 20; // 선언하지 않은 식별자에 값을 할당 (전역 객체의 프로퍼티로 y 추가)
}

foo();

// 선언하지 않은 식별자 y를 전역에서 참조할 수 있음
console.log(x + y); // 30

strict mode란?

  • ES5부터 추가된 strict mode(엄격 모드)
  • strict mode는 자바스크립트 언어의 문법을 좀 더 엄격히 적용하여 오류를 발생싴닐 가능성이 높거나 자바스크립트 엔진의 최적화 작업에 문제를 일으킬 수 있는 코드에 대해 명시적인 에러를 발생시킴
  • ESLint 같은  린트 도구를 사용해도 유사한 효과를 얻을 수 있음. 린트 도구는 정적 분석 기능을 통해 소스코드를 실행하기 전에 소스코드를 실행하여 문법적 오류뿐만 아니라 잠재적 오류까지 찾아내고 오류의 원인을 리포팅해주는 도구
  • DeepDive 필자는 린트 도구가 strict mode에서 제한하는 오류는 몰론 코딩 컨벤션을 설정 파일 형태로 정의하고 강제하는 등 더 강력한 효과를 얻을 수 있기 때문에 strict mode의 사용보다 린트 도구 사용을 더 선호
  • ES6에서 도입된 클래스와 모듈은 모두 strict mode가 적용
// 오류를 발생시킬 것 같은 코드
function foo() {
  x = 10; // x 선언이 없어서 foo 스코프 내에서 검색실패 -> 전역 객체에 암묵적으로 x 프로퍼티 동적 생성
}
foo();

console.log(x); // 10

// 개발자의 의도와 상관없이 발생한 암묵적 전역은 오류를 발생시킬 수 있다

strict mode의 적용

  • 전역의 선두 또는 함수 몸체 선두에 "use strict" 를 추가
// 전역의 선두에 추가하면 스크립트 전체에 strict mode가 적용
"use strict";

function foo() {
  x = 10; // ReferenceError: x is not defined
}
foo();
// 함수 몸체의 선두에 추가하면 해당 함수와 중첩 함수에 strict mode 적용
function foo() {
  "use strict";

  x = 10; // ReferenceError: x is not defined
}
foo();
// 코드의 선두에 "use strict"; 를 위치시키지 않으면 strict mode가 제대로 작동하지 않음
function foo() {
  x = 10; // 에러를 발생시키지 않음
  "use strict";
}
foo();

전역에 strict mode를 적용하는 것은 피하자

  • 전역에 적용한 strict mode는 스크립트 단위로 적용 (다른 스크립트에 영향 주지 않음)
  • strict mode 스크립트와 non-strict mode 스크립트를 혼용하는 경우, 외부 서드파티 라이브러리를 사용하는 경우 해당 라이브러리가 non-strict인 경우 등 예상치 못한 에러가 발생할 수 있으므로 전역에 strict mode를 사용하는 것은 지양
  • 즉시 실행 함수로 스크립트 전체를 감싸서 스코프를 구별하는 방법 권장
<!DOCTYPE html>
<html>
  <body>
    <script>
      "use strict";
    </script>
    <script>
      x = 1; // 에러 발생하지 않음
      console.log(x); // 1
    </script>
    <script>
      "use strict";
      y = 1; // ReferenceError: y is not defined
      console.log(y);
    </script>
  </body>
</html>
// 즉시 실행 함수의 선두에 strict mode 적용
(function () {
  "use strict";
  // Do something...
})();

함수 단위로 strict  mode를 적용하는 것도 피하자

  • 함수마다 다르게  strict  mode를 설정하는 건 바람직 하지 않음
  • strict mode가 적용된 함수가 참조할 함수 외부의 컨텍스트에 strict mode가 적용하지 않는 것도 문제가 될 수 있음
  • 결론: strict mode는 즉시 실행함수로 감싼 스크립트 단위로 적용하자
(function () {
  // non-strict mode
  var let = 10; // 에러가 발생하지 않음
  function foo() {
    "use strict";

    let = 20; // SyntaxError: Unexpected strict mode reserved word
  }
  foo();
})();

strict mode가 발생시키는 에러

암묵적 전역

  • 선언하지 않은 변수를 참조하면 ReferenceError가 발생
(function () {
  "use strict";

  x = 1; // ReferenceError: x is not defined
  console.log(x);
})();

변수, 함수, 매개변수의 삭제

  • delete 연산자로 변수, 함수, 매개변수를 삭제하면 SyntaxError 발생 (아닐 경우 Error 발생안되고 무시됨)
(function () {
  "use strict";

  var x = 1;
  delete x; // SyntaxError: Delete of an unqualified identifier in strict mode.

  function foo(a) {
    // delete a; // SyntaxError: Delete of an unqualified identifier in strict mode.
  }
  delete foo; // SyntaxError: Delete of an unqualified identifier in strict mode.
})();

매개변수 이름의 중복

  • 중복된 매개변수 이름을 사용하면 SyntaxError 발생
  • 참고: 화살표 함수는 중복된 매개변수 이름 사용 불가능
(function () {
  "use strict";

  function foo(x, x) { // SyntaxError: Duplicate parameter name not allowed in this context
    return x + x;
  }
  console.log(foo(1, 2)); // 참고: strict mode가 아닐 때 결과 값은 4
})();

with 문의 사용

  • with 문을 사용하면 SyntaxError 발생
  • with 문은 전달된 객체를 스코프 체인에 추가함
  • with 문은 객체의 이름을 생략할 수 있어서 코드가 간단해지지만 성능과 가독성이 나빠져서 사용 지양
(function () {
  "use strict";

  with ({ x: 1 }) { // SyntaxError: Strict mode code may not include a with statement
    console.log(x);
  }
})();

strict mode 적용에 의한 변화

일반 함수의 this

  • strict mode에서 함수를 일반 호출하면 this에 undefined가 바인딩 됨 (기존에는 전역객체가 바인딩)
  • 생성자 함수가 아닌 일반 함수 내부에서는 this를 사용할 필요가 없음
(function () {
  "use strict";

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

  function Foo() {
    console.log(this); // Foo {}
  }
  new Foo();
})();

// non-strict mode
function foo() {
  console.log(this); // Window (브라우저 환경)
}
foo();

arguments 객체

  • strict mode에서는 매개변수에 전달된 인수를 재할당하여 변경해도 arguments 객체에 반영되지 않음
(function (a) {
  "use strict";
  // 매개 변수에 전달된 인수를 재할당하여 변경
  a = 2;

  // 변경된 인수가 arguments 객체에 반영되지 않음
  console.log(arguments); // [Arguments] { '0': 1 }
})(1);

// non-strict mode
(function (a) {
  // 매개 변수에 전달된 인수를 재할당하여 변경
  a = 2;

  // 변경된 인수가 arguments 객체에 반영
  console.log(arguments); // [Arguments] { '0': 2 }
})(1);
  • 자바스크립트는 명령형 imperative, 함수형 functional, 프로토타입  기반 prototype-based, 객체지향 프로그래밍 OOP; Object Oriented Programming을 지원하는 멀티 패러다임 프로그래밍 언어
  • ES6에서 클래스 도입. 클래스는 생성자 함수보다 엄격하며 생성자 함수에서는 제공하지 않는 기능도 제공

객체지향 프로그래밍

  • 객체지향 프로그래밍은 프로그램을 명령어 또는 함수의 목록으로 보느 전통적인 명령형 프로그래밍 imperative programming의 절차지향적 관점에서 벗어나 여러 개의 독립적인 단위, 즉 객체 object의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임
  • 실체는 특징이나 성질을 나타내는 속성 attribute/property를 가지고 있고, 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현하는 것을 추상화 abstraction라고 함
  • 객체: 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조
  • 객체 object
    • 프로퍼티  property: 객체의 상태 state를 나타내는 데이터
    • 메서드 method: 객체의 상태 데이터를 조작할 수 있는 동작 behavior 
const circle = {
  radius: 5,
  getDiameter() {
    return 2 * this.radius;
  },
};

console.log(circle); // { radius: 5, getDiameter: [Function: getDiameter] }
console.log(circle.radius); // 5
console.log(circle.getDiameter()); // 10

상속과 프로토타입

  • 상속 inheritance: 어떤 객체의 프로퍼티 또는 메소드를 다른 객체가 상속 받아 그대로 사용할 수 있는 것
  • 자바스크립트는 프로토타입 prototype 기반으로 상속을 구현
  • 상속 받을 경우 생성자 함수가 생성한 모든 인스턴스는 자신의 프로토타입, 즉 상위(부모) 객체 역할을 하는 생성자.prototype의 모든 프로퍼티와 메서드를 상속 받음
  • 상속은 코드의 재사용 관점에서 매우 유용
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return Math.PI * this.radius ** 2;
  };
}

// 반지름이 1인 인스턴스 생성
const circle1 = new Circle(1);
// 반지름이 2인 인스턴스 생성
const circle2 = new Circle(2);

/* 
Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는 getArea 메서드를 중복 생성, 모든 인스턴스가 중복 소유
각 Circle의 특이한 속성인 radius가 아닌, 공통으로 갖는 getArea 메서드는 공유해서 사용하는 것이 바람직
*/
console.log(circle1.getArea === circle2.getArea); // false
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
}

// Circle생성자 함수가 생성한 모든 인스턴스가 getArea메서드를 공유해서 사용할 수 있도록 프로토타입에 추가
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있음
Circle.prototype.getArea = function () {
  return Math.PI * this.radius ** 2;
};

// 인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);

/*
Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체 역할을 하는 프로토타입 Circle.prototype으로부터 getArea 메서드 상속
즉, Cirlce 생성자 함수가 생성한 모든 인스턴스는 하나의 getArea 메서드를 공유
*/
console.log(circle1.getArea === circle2.getArea); // true

프로토타입 객체

  • 프로토타입은 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용
  • 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토 타입의 참조
  • 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고  [[Prototype]]에 저장
    • 객체 리터럴에 의해 생성된 객체의 프로토타입: Object.prototype
    • 생성자 함수에 의해 생성된 객체의 프로토타입: 생성자 함수의 prototype 프로퍼티에 바인딩 되어 있는 객체
  • 모든 객체는 하나의 프로토타입을 갖음
  • 모든 프로토타입은 생성자 함수와 연결되어 있음
  • [[Prototype]] 내부 슬롯에는 직접 접근할 수 없지만 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입,  즉 자신의 [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근 가능
  • 프로토타입은 자신의 contructor 프로퍼티를 통해 생성자 함수에 접근 가능
  • 생성자 함수는 자신의  prototype 프로퍼티를 통해 프로토타입에 접근 가능

객체와 프로토타입과 생성자 함수 연결 관계

__proto__ 접근자 프로퍼티

  • 모든 객체는 __proto__  접근자 프로퍼티를 통해 자신의 프로토 타입, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근 가능

크롬 브라우저 콘솔에서 출력한 객체의 프로퍼티

__proto__는 접근자 프로퍼티다

  • 내부 슬롯은 프로퍼티가 아니며 원칙적으로 직접적으로 접근할 수 없음
  • 접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터의 값을 읽거나 저장할 때 사용하는 접근자 함수, 즉  [[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성된 프로퍼티

Object.prototype.__proto__는 접근자 프로퍼티다

 

const obj = {};
const parent = { x: 1 };

// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입을 취득
console.log(obj.__proto__); // [Object: null prototype] {}

// setter 함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;

console.log(obj.x); // 1

__proto__ 접근자 프로퍼티는 상속을 통해 사용

  • __proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티
  • 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티 사용 가능
const person = { name: "Jin" };

// person 객체는 __proto__ 프로퍼티를 소유하지 않음
console.log(person.hasOwnProperty("__proto__")); // false

// __proto__ 프로퍼티는 모든 객체의 ㅡㅍ로토타입 객체인 Object.prototype의 접근자 프로퍼티
console.log(Object.getOwnPropertyDescriptor(Object.prototype, "__proto__"));
// {enumerable: false, configurable: true, get: ƒ, set: ƒ}

// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용 가능
console.log({}.__proto__ === Object.prototype); // true

__proto__  접근자 프로퍼티를 통해 프로토타입에 접근하는 이유

  • 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위함
  • 프로토타입은 단방향 링크드 리스트로 구현되어야 함
const parent = {};
const child = {};

// child의 프로토타입을 parent로 지정
child.__proto__ = parent;

// parent의 프로토타입을 child로 지정 (상호 참조에 의한 프로토타입 체인 생성)
parent.__proto__ = child; // TypeError: Cyclic __proto__ value

__proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않는다

  • 모든 객체가 __proto__ 접근자 프로퍼티를 사용할 수 있는 것은 아님
  • 직접 상속을 통해 Object.prototype을 상속 받지 않는 객체를 생성할 수도 있기 때문에 __proto__ 접근자 프로퍼티를 사용할 수 없는 경우가 있음
  • __proto__  접근자 프로퍼티 대신
    • Object.getPrototypeOf 메서드: 프로토타입의 참조를 취득하고 싶은 경우 (ES5)
    • Object.setPrototypteOf 메서드: 프로토 타입을 교체하고 싶은 경우 (ES6)
// obj는 프로토타입 체인의 종점
const obj = Object.create(null);

// obj는 Object.__proto__를 상속받을 수 없음
console.log(obj.__proto__); // undefined

// 따라서 __proto__보다 Object.getPrototypeOf 메서드를 사용 권장
console.log(Object.getPrototypeOf(obj)); // null
const obj = {};
const parent = { x: 1 };

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

console.log(obj.x); // 1
console.log(obj.__proto__ === parent); // true

함수 객체의 prototype 프로퍼티

  • 함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로타입을 가리킴
  • prototype 프로퍼티는 생성자 함수가 생성할 객체(인스턴스)의 프로타입을 가리키므로 non-constructor인 화살표 함수와 ES6 메서드 축약 표현을 정의한 메서드는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않음
  • 모든 객체가 가지고 있는 (엄밀히 말하면 Object.prototype으로부터 상속받은) __proto__ 접근 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킴. 하지만 이들 프로퍼티를 사용하는 주체가 다름
// 함수 객체는 prototype 프로퍼티를 소유
console.log(function () {}.hasOwnProperty("prototype")); // true

// 일반 객체는 prototype 프로퍼티를 소유 x
console.log({}.hasOwnProperty("prototype")); // false
// 화살표 함수는 non-constructor
const Person = (name) => {
  this.name = name;
};

// non-constructor는 prototype 프로퍼티 소유하지 않음
console.log(Person.hasOwnProperty("prototype")); // false

// non-constructor는 프로토타입을 생성하지 않음
console.log(Person.prototype); // undefined

// ES6 메서드 축약 표현으로 정의한 메서드는 non-constructor
const obj = {
  foo() {},
};

// non-constructor는 prototype 프로퍼티 소유하지 않음
console.log(obj.foo.hasOwnProperty("prototype")); // false

// non-contructor는 프로토타입을 생성하지 않음
console.log(obj.foo.prototype); // undefined
구분 소유 사용 주체 사용 목적
__proto__
접근자 프로퍼티
모든 객체 프로토타입의 참조 모든 객체 객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용
prototype
프로퍼티
constructor 프로토타입의 참조 생성자 함수 생성자 함수가 자신이 객체(인스턴스)의 프로토타입을 할당하기 위해 사용
// 생성자 함수
function Person(name) {
  this.this = name;
}

const me = new Person("Jin");

// 결국 Person.prototype과 __proto__는 결국 동일한 가리킴
console.log(Person.prototype == me.__proto__); // true

객체 __proto__ 접근자 프로퍼티와 함수 객체의 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킴

프로토타입의 constructor 프로퍼티와 생성자 함수

  • 모든 프로토타입은 constructor 프로퍼티를 갖음
  • contructor 프로퍼티는 prototype 프로토타입 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킴
  • 이 연결은 생성자 함수가 생성될 때, 즉 함수 객체가 생성될 때 이뤄짐
  • me 객체에는 constructor가 없지만 프로토타입인 Person.prototype의 constructor 프로퍼티를 상속 받아 사용 가능

프로토타입의 constructor 프로퍼티

리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

  • 리터럴 표기법에 의해 생성된 객체는 생성자 함수가 생성한 객체는 아님
  • 하지만 객체 리터럴 contructor 프로퍼티로 확인한 생성자 함수는 Object 생성자 함수
  • 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재
  • 리터럴 표기법으로 생성한 객체도 생성자 함수로 생성한 객체와 본질적인 면에서 큰 차이는 없음
// obj1 객체를 생성한 생성자 함수는 Object
const obj1 = new Object();
console.log(obj1.constructor === Object); // true

// obj2 객체는 객체 리터럴로 생성했으나 생성자 함수는 Object
const obj2 = {};
console.log(obj2.constructor === Object); // true
리터럴 표기법 생성자 함수 프로토타입
객체 리터럴 Object Object.prototype
함수 리터럴 Function Function.prototype
배열 리터럴 Array Array.prototype
정규 표현식 리터럴 RegExp RegExp.prototype

프로토타입의 생성 시점

  • 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성 (프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재)

사용자 정의 생성자 함수와 프로토타입 생성 시점

  • 생성자 함수로서 호출할 수 있는 함수, 즉 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
  • 생성자 함수로서 호출할 수 없는 함수, 즉 non-constructor는 프로토타입이 생성되지 않음
// 함수 정의(constructor)가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
console.log(Person.prototype); // {constructor: ƒ}

// 생성자 함수
function Person(name) {
  this.name = name;
}

생성된 프로토타입의 프로토타입은 Object.prototype

// 화살표 함수는 non-constructor
const Person = (name) => {
  this.name = name;
};

// non-constructor는 프로토타입이 생성되지 않음
console.log(Person.prototype); // undefined

 

Person.prototype의 프로토타입

빌트인 생성자 함수와 프로토타입 생성 시점

  • Object, String, Number, Function, Array, RegExp, Date, Promise 같은 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 실행되는 시점에 프로토타입이 생김
  • 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성
  • 생성된 프로토타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩
  • 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재
  • 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토 타입은 생성된 객체의  [[Prototype]] 내부 슬롯에 할당
// 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재
// 브라우저 환경에서
console.log(window.Object === Object) // true

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

  • 객체의 생성 방식
    • 객체 리터럴
    • Object 생성자 함수
    • 생성자 함수
    • Object.create 메서드
    • 클래스(ES6)
  • 세부적인 객체 생성 방식의 차이는 있으나 추상 연산 OrdinaryObjectCreate에 생성된다는 공통점이 있음
  • OrdinaryObjectCreate
    • 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 전달 받고, 추가할 프로퍼티 목록을 옵션으로 전달
    • 빈 객체를 생성한 후, 객체에 추가할 프로퍼티 목록이 인수로 전달될 경우 프로퍼티를 객체에 추가
    • 인수로 전달 받은 프로토타입을 자신이 생성한 객체의 [[Prototype]] 내부 슬롯에 할당
    • 객체 반환

객체 리터럴에 의해 생성된 객체의 프로토타입

const obj = { x: 1 };

// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속 받음
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty("x")); // true

객체 리터럴에 의해 생성된 객체의 프로토타입

Object 생성자 함수에 의해 생성된 객체의 프로토타입

  • 객체 리터럴 방식은 객체 리터럴 내부에 프로퍼티를 추가하지만, Object 생성자 함수 방식은 일단 빈 객체를 생성한 후 프로퍼티 추가
const obj = new Object();
obj.x = 1;

// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속 받음
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty("x")); // true

Object 생성자 함수에 의해 생성된 객체의 프로토타입

생성자 함수에 의해 생성된 객체의 프로토타입

  • 생성자 함수에 의해 생성되는 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체
  • 표준 빌트인 객체인 Object 생성자 함수와 더불어 생성된 프로토타입 Object.prototype은 다양한 빌트인 메서드 (hasOwnProperty, etc) 등 을 가짐 / 사용자 정의 생성자 함수 Person과 더불어 생성된 Person.prototype의 프로퍼티는 constructor 뿐임
function Person(name) {
  this.name = name;
}

const me = new Person("Jin");

생성자 함수에 의해 생성된 객체의 프로토타입

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

Person.prototype.sayHello = function () {
  console.log(`Hi my name is ${this.name}`);
};

const me = new Person("Jin");
const you = new Person("Park");

me.sayHello(); // Hi my name is Jin
you.sayHello(); // Hi my name is Park

프로토타입의 확장과 상속

프로토타입 체인

  • 프로토타입 체인: 자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티가 없으면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색
  • 프로토타입 체인은 객체 지향 프로그래밍의 상속과 프로퍼티 검색을 위한 메카니즘
  • Object.prototype은 프로토타입 체인의 종점 end of prototype chain
  • Object.prototype의 [[Prototype]] 내부 슬롯 값은 null
  • 프로토타입 체인의 종점에서도 프로퍼티 값을 검색할 수 없는 경우 undefined 반환 (에러는 발생 x)
  • 참고: 스코프 체인은 식별자 검색을 위한 메카니즘
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function () {
  console.log(`Hi my name is ${this.name}`);
};

const me = new Person("Jin");

// hasOwnProperty는 Object.prototype의 메서드인데 동작
console.log(me.hasOwnProperty("name")); // true

// 이것은 me 객체가 Person.prototype뿐만 아니라 Object.prototype도 상속 받았다는 것을 나타냄
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true

프로토타입 체인

오버라이딩과 프로퍼티 섀도잉

  • 프로퍼티 섀도잉: property shadowing: 상속 관계에 의해 프로퍼티가 가려지는 현상
  • 오버라이딩 overriding: 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
  • 오버로딩 overloading: 함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식. 자바스크립트는 오버로딩을 지원하지는 않지만 arguments 객체를 통하여 구현 가능
  • 하위 객체를 통해 프로토타입의 프로퍼티를 변경 또는 삭제하는 것은 불가능, (get 엑세스는 허용되나 set 엑세스는 허용되지 않음)
  • 프로토타입 프로퍼티를 변경하려면 하위 객체를 통해 프로토타입 체인으로 접근하는 것이 아니라 프로토타입에 직접 접근해야 함
const Person = (function () {
  // 생성자 함수
  function Person(name) {
    this.name = name;
  }
  // 프로토 타입 메서드
  Person.prototype.sayHello = function () {
    console.log(`Hi! my name is ${this.name}`);
  };

  // 생성자 함수를 반환
  return Person;
})();

const me = new Person("Jin");

// 인스턴스 메서드
me.sayHello = function () {
  console.log(`안녕! 내 이름은 ${this.name}`);
};

// 인스턴스 메서드가 호출됨. 프로토타입 메서드는 인스턴스 메서드에 의해 가려짐
me.sayHello(); // 안녕! 내 이름은 Jin

// 인스턴스 메서드 삭제
delete me.sayHello;
me.sayHello(); // Hi! my name is Jin

// 하위 객체를 통하여 프로토타입 프로퍼티를 변경 또는 삭제는 불가능
delete me.sayHello;
me.sayHello(); // Hi! my name is Jin 여전히 프로토타입 메서드 호출

// 프로토타입 프로퍼티를 변경하려면 프로토타입에 직접 접근
delete Person.prototype.sayHello;
me.sayHello(); // TypeError: me.sayHello is not a function

오버라이딩과 프로퍼티 섀도잉

프로토타입의 교체

  • 프로토타입은 임의의 다른 객체로 변경 가능
  • 객체의 간의 상속 관계를 동적으로 변경 가능
const Person = (function () {
  function Person(name) {
    this.name = name;
  }
  // 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
  // 자바스크립트가 암묵적으로 생성한 prototype을 객체 리터럴로 교체
  Person.prototype = {
    sayHello() {
      console.log(`Hey, my name is ${this.name}`);
    },
  };
  return Person;
})();

const me = new Person("Jin");

생성자 함수를 통한 프로토타입의 교체

// constructor 와 생성자 함수간의 연결을 살려보자
const Person = (function () {
  function Person(name) {
    this.name = name;
  }
  // 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
  // 자바스크립트가 암묵적으로 생성한 prototype을 객체 리터럴로 교체
  Person.prototype = {
    // constructor 프로퍼티와 생성자 함수간의 연결 설정
    constructor: Person,
    sayHello() {
      console.log(`Hey, my name is ${this.name}`);
    },
  };
  return Person;
})();

const me = new Person("Jin");

// constructor 프로퍼티가 생성자 함수를 가리킴
console.log(me.constructor === Person); // true
console.log(me.constructor === Object); // false

인스턴스에 의한 프로토타입의 교체

  • 생성자 함수의 prototype 프로퍼티에 다른 임의의 객체를 바인딩 하는 것은 미래에 생성할 인스턴스의 프로토타입을 교체하는 것
  • 인스턴스의 __proto__ 접근자 프로퍼티를 통해 프로토타입을 교체하는 것은 이미 생성된 객체의 프로토타입을 교체하는 것
  • Object .setPrototypeOf 메서드 또는 인스턴스의 __proto__ 접근자 프로퍼티를 통해 인스턴스의 프로토타입 교체 가능
  • 프로토타입은 직접 변경하지 않는 것을 권장 (객체 관의 상속 관계를 동적으로 관리하는 것은 번거로움)
  • 상속 관계를 인위적으로 설정하려면 직접 상속 권장
  • ES6에 도입된 클래스를 사용하면 간편하고 직관적으로 상속 관계 구현 가능
function Person(name) {
  this.name = name;
}

const me = new Person("Jin");

// 프로토 타입으로 교체할 객체
const parent = {
  sayHello() {
    console.log(`Hi! my name is ${this.name}`);
  },
};

// me 객체의 프로토타입을 parent로 교체
// me.__proto__ = parent; 와 동일하게 동작
Object.setPrototypeOf(me, parent);

me.sayHello(); // Hi! my name is Jin

// 당연히 생성자 함수 Person의 prototype은 parent가 아닌 Person.prototype임
console.log(Person.prototype);
/*
{constructor: ƒ}
constructor: ƒ Person(name)
[[Prototype]]: Object
*/

// 인스턴스 prototype이 교체되어서 생성자함수와 prototype간의 연결이 끊어짐
// 인스턴스의 constructor는 프로토타입 체인을 따라 Object 됨
console.log(me.constructor === Object); // true

instance of 연산자

  • 객체 instance of 생성자 함수
  • 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true, 그렇지 않은 경우 false 반환
  • 생성자 함수를 찾아주는 것이 아니라 생성자 함수의 prototype이 프로토타입 체인 상에 존재하는지 확인
const Person = (function () {
  function Person(name) {
    this.name = name;
  }
  // 프로토타입 교체
  Person.prototype = {
    sayHello() {
      console.log(`Hi! my name is ${this.name}`);
    },
  };
  return Person;
})();

const me = new Person("Jin");

// constructor 프로퍼티와 생성자 함수 간의 연결이 파괴되어도 intanceof 에는 영향 x
console.log(me.constructor === Person); // false
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true
console.log(me instanceof Person); // true
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true
console.log(me instanceof Object); // true

직접 상속

Object.create에 의한 직접 상속

  • Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체 생성
    • 첫 번째 매개변수: 생성할 객체의 프로토타입으로 지정할 객체 (null로 할 경우 프로토타입 종점에 위치)
    • 두 번재 매개변수: 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터객체로 이뤄진 객체 (옵션; 생략가능)
  • 장점
    • new 연산자가 없어도 객체 생성 가능
    • 프로토타입을 지정하면서 객체 생성 가능
    • 객체 리터럴에 의해 생성된 객체도 상속 가능
  • 참고: ESLint에서는 Object.prototype 빌트인 메서드를 직접 호출하는 것을 권장하지 않음. Object.create(null)처럼 프로토타입 종점에 위치하는 객체를 생성 가능하기 때문.  간접적으로 호출 권장
// 프로토타입이 null인 객체 생성. 생성된 객체는 프로토타입 체인의 종점에 위치
let obj = Object.create(null);
console.log(Object.getPrototypeOf(obj) === null); // true
// Object.prototype을 상속 받지 못함
// console.log(obj.toString()); // TypeError: obj.toString is not a function

// obj = {}; 와 동일
obj = Object.create(Object.prototype);
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// obj = {x: 1} 와 동일
obj = Object.create(
  Object.create(Object.prototype, {
    x: { value: 1, writable: true, enumerable: true, configurable: true },
  })
);

// 임의의 객체 직접 상송
const parent = { x: 10 };
obj = Object.create(parent);
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === parent); // true

// 생성자 함수
function Person(name) {
  this.name = name;
}
obj = Object.create(Person.prototype);
obj.name = "Jin";
console.log(obj.name); // Jin
console.log(obj.__proto__ === Person.prototype); // true
// 프로토타입이 null인 객체 생성
const obj = Object.create(null);
obj.a = 1;

// console.log(obj.hasOwnProperty("a"));
// TypeError: obj.hasOwnProperty is not a function

// 간접 호출
console.log(Object.prototype.hasOwnProperty.call(obj, "a")); // true

객체 리터럴 내부에서 __proto__에 의한 직접 상속

  • ES6에서는 __proto__ 접근자 프로퍼티를 사용하여 직접 상속 구현
const myProto = { x: 10 };

// 객체 리터럴에 의해 객체를 생성하면서 프로토타입을 지정하여 직접 상속 받을 수 있음
const obj = {
  y: 20,
  // 객체 직접 상속
  __proto__: myProto,
};

/* 아래 코드와 동일하게 동작
const obj = Object.create(myProto, {
  y: { value: 20, writable: true, enumerable: true, configurable: true },
});
*/

정적 프로퍼티 / 메서드

  • 정적 static 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 뜻함
  • 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출 당연히 불가능
  • 사실상 일반 함수의 프로퍼티랑 메서드
  • 프로토타입 프로퍼티/메서드를 표기할 때 prototype을 #으로 표기하는 경우도 있음
    • 예시: Object.prototype.isPrototypeOf를 Object#isPrototypeof 로 표기
// 생성자 함수
function Person(name) {
  this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function () {
  console.log(`Hi! my name is ${this.name}`);
};

// 정적 프로퍼티
Person.staticProp = "static prop";

// 정적 메서드
Person.staticMethod = function () {
  console.log("static method");
};

const me = new Person("Jin");

// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출
Person.staticMethod(); // static method

// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출 불가
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 함
me.staticMethod(); // TypeError: me.staticMethod is not a function

정적 프로퍼티 메서드

프로퍼티 존재 확인

in 연산자

  • in 연산자는 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인
  • in 연산자는 확인 대상 객체의 프로퍼티 뿐만 아니라 확인 대상 객체가 상속 받은 모든 포로토타입의 프로퍼티를 확인
  • ES6에서 도입된 Reflect.has 메서드는 in 연산자와 동일하게 동작
const person = {
  name: "Jin",
  address: "Seoul",
};

// person 객체에 name 프로퍼티가 존재
console.log("name" in person); // true
// person 객체에 address 프로퍼티가 존재
console.log("address" in person); // true
// person 객체에 age 프로퍼티가 존재?
console.log("age" in person); // false

// person에는 Object.prototype에서 상속받은 toString이라는 프로퍼티 존재
console.log("toString" in person); // true

// ES6에 도입된 Relect.has 메서드는 in과 동일하게 동작
console.log(Reflect.has(person, "name")); // true
console.log(Reflect.has(person, "toString")); // true

Object.prototype.hasOwnProperty 메서드

  • 인수로 전달 받은 프로퍼티가 객체 고유의 프로퍼티 키인 경우에만 true 반환 (상속 받은 프로퍼티는 false)
const person = {
  name: "Jin",
  address: "Seoul",
};

// Object.prototype.hasOwnProperty는 객체 고유의 프로퍼티인 경우에만 true 반환
console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("age")); // false
console.log(person.hasOwnProperty("toString")); // false

프로퍼티 열거

for ... in 문

  • for (변수선언문 in 객체) {...}
  • 객체의 모든 프로퍼티를 순회하며 열거 enumeration하려면 for ... in 문을 사용
  • for ... in 문은 객체의 상속 받은 프로퍼티까지 열거
  • [[Enumerable]] 값이 false인 값은 열거하지 않음
  • for ... in 문은 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않음
  • for ... in 문은 프로퍼티를 열거할 때 순서를 보장하진 않음 (대부분의 모던 브라우저는 프로퍼티 키에 대해 정렬을 실시)
  • 배열에는 for 문, for ... of, Array.prototype.forEach 메서드 사용 권장
const Person = (function () {
  const Person = function (name) {
    this.name = name;
  };
  Person.prototype.sayHello = function () {
    console.log(`Hello! my name is ${this.name}`);
  };
  return Person;
})();

const me = new Person("Jin");
me.sayBye = function () {
  console.log("Bye Bye");
};

// for ... in 문의 변수 person 객체의 프로퍼티 키
for (const key in me) {
  console.log(key);
}
/*
name
sayBye
sayHello
true
*/

// me은 프로토타입의 프로퍼티로 toString을 갖고 있음
// toString은 [[Enumberable]] 값이 false라서 열거되지 않음
console.log("toString" in me); // true
console.log(Object.getOwnPropertyDescriptor(Object.prototype, "toString"));
// {writable: true, enumerable: false, configurable: true, value: ƒ}
// 배열
const arr = [1, 2, 3];
arr.x = 10; // 배열도 객체이므로 프로퍼티를 가질 수 있음

for (const i in arr) {
  // 프로퍼티 x도 출력
  console.log(arr[i]); // 1 2 3 10
}

// arr.length는 3
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]); // 1 2 3
}

// forEach 메서드는 요소가 아닌 프로퍼티는 제외
arr.forEach((v) => console.log(v)); // 1 2 3

// for ... of는 변수 선언문에서 선언한 변수에 키가 아닌 값을 할당
for (const value of arr) {
  console.log(value); // 1 2 3
}

Object.keys/values/entries 메서드

  • Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환
  • Object.values 메서드는 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환 (ES8)
  • Object.entries 메서드는 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열 을 배열에 담아 반환 (ES8)
const me = {
  name: "Jin",
  address: "Seoul",
  __proto__: { age: 29 },
};

console.log(Object.keys(me)); // [ 'name', 'address' ]
console.log(Object.values(me)); // [ 'Jin', 'Seoul' ]
console.log(Object.entries(me)); // [ [ 'name', 'Jin' ], [ 'address', 'Seoul' ] ]

Object.entries(me).forEach(([key, value]) => console.log(key, value));
/*
name Jin
address Seoul
*/
  • 일급 객체의 조건
    • 무명의 리터럴로 생성 가능. 즉, 런타임에 생성이 가능
    • 변수나 자료구조(객체, 배열 등)에 저장 가능
    • 함수의 매개변수에 전달 가능
    • 함수의 반환 값으로 사용 가능
// 1. 함수는 무명의 리터럴로 생성 가능
// 2. 함수는 변수에 저장 가능
// 런타임(할당 단계)에 함수 리터럴이 평가되어 함수 객체가 생성되고 변수에 할당
const increase = function (num) {
  return ++num;
};
const decrease = function (num) {
  return --num;
};
// 2. 함수는 객체에 저장 가능
const auxs = { increase, decrease };

// 3. 함수의 매개변수에 전달 가능
// 4. 함수의 반환 값으로 사용 가능
function makeCounter(aux) {
  let num = 0;

  return function () {
    num = aux(num);
    return num;
  };
}

// 3. 함수는 매개변수에 함수 전달 가능
const increaser = makeCounter(auxs.increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

const decreaser = makeCounter(auxs.decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

함수 객체의 프로퍼티

  • 함수는 객체이기 때문에 프로퍼티를 가질 수 있음

함수 객체의 프로퍼티

function square(number) {
  return number * number;
}

console.log(Object.getOwnPropertyDescriptors(square));
/*
{
  arguments: {value: null, writable: false, enumerable: false, configurable: false},
  caller: {value: null, writable: false, enumerable: false, configurable: false},
  length: {value: 1, writable: false, enumerable: false, configurable: true},
  name: {value: 'square', writable: false, enumerable: false, configurable: true},
  prototype: {value: {…}, writable: true, enumerable: false, configurable: false}
}
*/

// __proto__는 square의 프로퍼티가 아님
console.log(Object.getOwnPropertyDescriptor(square, "__proto__")); // undefined

// __proto__는 Object.prototype 객체의 접근자 프로퍼티
// square함수는 Object.prototype 접근자 프로퍼티를 상속 받음
console.log(Object.getOwnPropertyDescriptor(Object.prototype, "__proto__"));
// {enumerable: false, configurable: true, get: ƒ, set: ƒ}

arguments 프로퍼티

  • 함수 객체의  arguments 프로퍼티 값은  arguments 객체
    • 인수를 프로퍼티 값으로 소유하며 프로퍼티 키는 인수의 순서를 나타냄
    • callee 프로퍼티는 호출되어 arguments 객체를 생성한 함수, 즉 함수 자신
    • length 프로퍼티는 인수의 개수를 나타냄
  • arguments 객체는 함수 호출 시 전달된 인수 argument들의 정보를 담고 있는 순회 가능한 iterable 유사 배열 객체이며, 함수 내부에서 지역 변수처럼 사용. 즉, 함수 외부에서는 참조 불가능
    • 유사 배열 객체
      • length 프로퍼티 가짐
      • for 문으로 순회 가능
      • 배열은 아니므로 배열 메서드 사용할 경우 에러 발생
      • Function.prototype.call, Function.prototupe.apply 를 사용해 간접 호출하여 배열 메서드 사용 가능
  • arguments 객체는 매개 변수를 확정할 수 없는 가변 인자 함수 구현할 때 유용

arguments 객체의 프로퍼티

// 가변 인자 함수 구현 
function sum() {
  let res = 0;

  // arguments 객체는 length 프로퍼티가 있는 유사 배열 객체이므로 for문으로 순회 가능
  for (let i = 0; i < arguments.length; i++) {
    res += arguments[i];
  }
  return res;
}

console.log(sum()); // 0
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3)); // 6
// 간접 호출
function sum() {
  // arguments 객체를 배열로 변환
  const array = Array.prototype.slice.call(arguments);
  return array.reduce(function (pre, cur) {
    return pre + cur;
  }, 0);
}

console.log(sum()); // 0
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3)); // 6
// ES6 Rest Parameter
function sum(...args) {
  return args.reduce((pre, cur) => pre + cur, 0);
}

console.log(sum()); // 0
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3)); // 6

caller 프로퍼티

  • ECMAScript 사양에 포함되지 않은 비표준 프로퍼티
  • 함수 객체의  caller 프로퍼티는 함수 자신을 호출한 함수를 가리킴

length 프로퍼티

  • 함수 객체의 length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킴
  • arguments 객체의 프로퍼티 (인자로 받은 수)와 값이 다를 수 있음
function foo() {}
function bar(x) {}
function baz(x, y) {}

console.log(foo.length); // 0
console.log(bar.length); // 1
console.log(baz.length); // 2

name 프로퍼티

  • ES6부터 정식 표준 (ES5와 다르게 동작함)
  • ES6: 함수 객체를 가리키는 식별자를 값
  • ES5: 빈 문자열
// 기명 함수 표현식
var namedFunc = function foo() {};
console.log(namedFunc.name); // foo

// 익명 함수 표현식
var anonymousFunc = function () {};
console.log(anonymousFunc.name); // anonymousFunc (ES6)

// 함수 선언문
function bar() {}
console.log(bar.name); // bar

__proto__ 접근자 프로퍼티

  • 모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖음
  • __proto__는 [[Prototype]] 내부 슬롯이 가리키는 프로토타입 객체에 접근하기 위해 사용하는 접근자 프로퍼티 ( [[Prototype]] 내부 슬롯에 직접 접근 불가능)
const obj = { a: 1 };

// 객체 리터럴 방식으로 생성한 객체의 프로토타입 객체는 Object.prototype
console.log(obj.__proto__ === Object.prototype); // true

// 객체 리터럴 방식으로 생성된 객체는 프로토타입 객체인 Object.prototype 프로퍼티를 상속 받음
// hasOwnProperty 메서드는 인수로 전달 받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true 반환
console.log(obj.hasOwnProperty("a")); // true
console.log(obj.hasOwnProperty("__proto__")); // false
console.log(Object.prototype.hasOwnProperty("__proto__")); // true
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); // true

prototype 프로퍼티

  • prototype 프로퍼티는 생성자 함수로 호출할 수있는 함수 객체, 즉 constructor만이 소유하는 프로퍼티
  • 일반 객체와 생성자 함수로 호출할 수 없는 non-constructor에는 prototype 프로퍼티가 없음
  • prototype 프로퍼티는 함수가 객체를 생성하는 생성자 함수로 호출될 때, 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킴
console.log(function () {}.hasOwnProperty("prototype")); // true
console.log((() => {}).hasOwnProperty("prototype")); // false 화살표함수는 non-constructor

Object 생성자 함수

  • 생성자 함수 constructor란 new 연산자와 함께 호출하여 객체(인스턴스)를 생성하는 함수
  • 생성자 함수에 의해서 생성된 객체를 인스턴스 instance라 함
  • new 연산자와 함께 Object 생성자 함수를 호출하면 빈 객체를 생성하여 반환
  • Object 외에도 String, Number, Boolean, Function, Array, Date, RegExp, Promise 등의 빌트인 생성자 함수 제공
// 빈 객체의 생성
const person = new Object();

// 프로퍼티 추가
person.name = "Jin";
person.sayHello = function () {
  console.log("Hi! My name is " + this.name);
};

console.log(person); // {name: 'Jin', sayHello: ƒ}
person.sayHello(); // Hi! My name is Jin
// String 생성자 함수에 의한 String 객체 생성
const strObj = new String("Jin");
console.log(typeof strObj); // object
console.log(strObj); // String {'Jin'}

// Number 생성자 함수에 의한 Number 객체 생성
const numObj = new Number(123);
console.log(typeof numObj); // object
console.log(numObj); // Number {123}

// Boolean 생성자 함수에 의한 Boolean 객체 생성
const boolObj = new Boolean(true);
console.log(typeof boolObj); // object
console.log(boolObj); // Boolean {true}

// Function 생성자 함수에 의한 Function 객체(함수) 생성
const func = new Function("x", "return x * x");
// Uncaught EvalError: Refused to evaluate a string as JavaScript 
// because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive:

// Array 생성자 함수에 의한 Array 객체(배열) 생성
const arr = new Array(1, 2, 3);
console.log(typeof arr); // object
console.log(arr); // [1, 2, 3]

// RegExp 생성자 함수에 의한 RegExp 객체(정규표현식) 생성
const regExp = new RegExp(/ab+c/i);
console.log(typeof regExp); // object
console.log(regExp); // /ab+c/i

// Date 생성자 함수에 의한 Date 객체 생성
const date = new Date();
console.log(typeof date); // object
console.log(date); // Tue Dec 12 2023 21:46:29 GMT+0900 (한국 표준시)

생성자 함수

객체 리터럴에 의한 객체 생성 방식의 문제점

  • 객체 리터럴에 의한 생성 방식은 단 하나의 객체만 생성하므로 동일한 프로퍼티를 갖는 여러 객체를 생성할 때 비효율적
const circle1 = {
  radius: 5,
  getDiameter() {
    return 2 * this.radius;
  },
};

const circle2 = {
  radius: 10,
  getDiameter() {
    return 2 * this.radius;
  },
};

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20

생성자 함수에 의한 객체 생성 방식의 장점

  • 생성자 함수에 의한 객체 생성 방식은 마치 객체(인스턴스)를 생성하기 위한 템플릿(클래스)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성 가능
  • 자바와 같은 클래스 기반 객체지향 언어의 생성자와는 다르게 그 형식이 정해져 있는 것이 아니라 일반 함수와 동일한 방식으로 생성자 함수를 정의하고 new 연산자와 함께 호출하면 해당 함수는 생성자 함수로 동작
  • new 연산자 없이 호출하면 일반 함수로 동작
  • 일반적으로 생성자 함수로 사용할 함수는 이름을 대문자로 시작
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스의 생성
const circle1 = new Circle(5);
const circle2 = new Circle(10);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20
함수 호출 방식 this가 가리키는 값 (this 바인딩)
일반 함수로 호출 전역 객체
메서드로 호출 메서드를 호출한 객체 (마침표 앞의 객체)
생성자 함수로 호출 생성자 함수가 (미래에) 생성할 인스턴스
// 함수는 다양한 방법으로 호출될 수 있음
function foo() {
  console.log(this);
}

// 일반 함수로 호출
// 전역 객체는 브라우저 환경에서는 window, Node.js 환경에는 global을 가리킴
foo(); // Window

const obj = { foo }; // ES6 프로퍼티 축약 표현
// 메서드로 호출
obj.foo(); // {foo: ƒ} obj

// 생성자 함수로 호출
const inst = new foo(); // foo {} inst
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// new 연산자와 함께 호출하지 않으면 생성자 함수로 동작하지 않음
// 즉, 일반 함수로 호출됨
const circle3 = Circle(15);

// 일반 함수로 호출된 Circle은 반환문이 없으므로 암묵적으로 undefined를 반환
console.log(circle3); // undefined

// 일반 함수로 호출된 Circle의 this는 전역 객체를 가리킴
console.log(radius); // 15

생성자 함수의 인스턴스 생성 과정

  • 생성자 함수 몸체에서 수행해야하는 것
    • 인스턴스를 생성 - 필수
    • 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기 값 할당) - 옵션
  • 자바스크립트는 new 연산자와 함께 생성자 함수를 호출하면 암묵적으로 인스턴스를 생성, 초기화, 반환함
// 생성자 함수
function Circle(radius) {
  // 인스턴스 초기화
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스 생성
const circle1 = new Circle(5)

1. 인스턴스의 생성과 this 바인딩

  • 암묵적으로 빈 객체 생성
  • 암묵적으로 생성된 빈 객체, 즉 인스턴스는 this에 바인딩
  • 이 처리는 함수 몸체의 코드가 한 줄씩 실행되는 런타임 이전에 실행

2. 인스턴스 초기화

  • 생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되어 this에 바인딩 되어 있는 인스턴스를 초기화
  • 즉, this에 바인딩 되어있는 인스턴스에 프로퍼티나 메서드를 추가하고 생성자 함수가 인수로 전달 받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값 할당

3. 인스턴스 반환

  • 생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 전달
  • 생성자 함수 내부에 다른 객체를 명시적으로 반환할 경우,  this가 반환되지 못하고 return문에 명시한 객체가 반환
  • 생성자 함수 내부에 원시 값을 반환할 경우, 원시 값 반환은 무시되고 암묵적으로 this가 반환
  • 따라서 생성사 함수 내부에서 return문은 반드시 생략해야함
// 생성자 함수
function Circle(radius) {
  // 인스턴스 초기화
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스 생성
const circle1 = new Circle(5)
// 생성자 함수 내부에서 다른 객체를 return할 경우
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
  return {};
}
const circle1 = new Circle(5);
console.log(circle1); // {} this가 반환되지 못하고 return문에 명시한 객체가 반환

내부 메서드 [[Call]]과 [[Construct]]

  • 함수는 객체이면서 호출할 수 있다는 추가적인 특징 보유
  • 함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드는 몰론 함수로 동작하기 위해 함수 객체만을 위한 [[Environment]], [[FormalParameters]] 등의 내부 슬롯과 [[Call]], [[Contruct]] 같은 내부 메서드를 추가로 보유
  • 함수가 일반 함수로 호출되면 함수 객체의 내부 메서드 [[Call]] 호출
  • 함수가 new 연산자와 함게 생성자 함수로 호출되면 함수 객체 내부 메서드 [[Construct]] 호출
  • 내부 메서드 [[Call]]을 갖는 함수 객체를 callable
  • 내부 메서드 [[Construct]]를 갖는 함수 객체를 constructor / 안갖는 함수 객체를 non-constructor
  • 모든 함수는 callable이지만 모든 함수 객체가 constructor인 것은 아님

constructor와 non-constructor의 구분

  • constructor: 함수 선언문, 함수 표현식, 클래스(클래스도 함수다)
  • non-constructor: 메서드(ES6 메서드 축약 표현만 인정), 화살표 함수
  • 주의사항: ECMAScript 사양에서 메서드로 인정하는 범위가 일반적인 의미의 메서드보다 좁음
// constructor
// 일반 함수 정의: 함수 선언문, 함수 표현식
function foo() {}
const bar = function () {};
// 프로퍼티 x의 값으로 할당된 것은 일반 함수로 정의된 함수. 이는 메서드로 인정 x
const baz = {
  x: function () {},
};

new foo();
new bar();
new baz.x();

// non-constuctor
// 화살표 함수
const arrow = () => {};
const obj = {
  y() {},
};

new arrow(); // TypeError: arrow is not a constructor
new obj.y(); //TypeError: obj.y is not a constructor

new 연산자

  • 일반 함수와 생성자 함수에 특별한 형식적 차이는 없음
  • new 연산자와 함께 함수를 호출함녀 해당 함수는 생성자 함수로 동작
  • 함수 객체의 내부 메서드 [[Construct]]가 호출 ([[Call]]이 아닌)
  • 단, new 연산자와 함께 호출하는 함수는 constructor여야 함
  • 생성자 함수는 일반적으로 첫 문자를 대문자로 기술하는 파스칼 케이스로 명명하여 일반 함수와 구별할 수 있도록 함

new.target

  • ES6부터 지원, IE는 지원 안함
  • new.target은 this와 유사하게 contructor인 모든 함수 내부에서 암묵적인 지역 변수와 같이 사용되며 메타 프로퍼티라고 부름
  • new 연산자와 함께 생성자 함수로 호출되면 함수 내부의 new.target은 함수 자신을 가리킴
  • new 연산자 없이 일반 함수로 호출되면 함수 내부의 new.target은 undefined
  • 따라서 함수 내부에서 new.target을 사용하여 new 연산자와 생성자 함수로서 호출했는지 확인하여 그렇지 않은 경우 new 연산자와 함께 재귀 호출을 통해 생성자 함수로 호출 가능
  • 참고: 대부분의 빌트인 생성자 함수(Object, String, Number, Boolean, Function, Array, Date, RegExp, Promise 등)는 new 연산자와 함께 호출되었는지를 확인 후 적절한 값을 반환
    • Object와 Function 생성자 함수는 new 연산자 없이 호출해도 new 연산자와 함께 호출했을 때와 동일하게 동작
    • String,  Number, Boolean 생성자 함수는 new 연산자와 함께 호출했을 때 String, Number, Boolean 객체를 생성하여 반환하지만 new 연산자 없이 호출하면 문자열, 숫자, 불리언 값을 반환. 이를 통해 데이터 타입을 변환하기도 함
function Circle(radius) {
  // 이 함수가 new 연산자와 함께 호출되지 않았다면 new.target은 undefined
  if (!new.target) {
    // new 연산자와 함께 생성자 함수를 재귀 호출하여 생성된 인스턴스 반환
    return new Circle(radius);
  }
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}
// new 연산자 호출 없이 생성자 함수를 호출해도 new.target을 통해 생성자 함수로 호출됨
const circle = Circle(5);
console.log(circle.getDiameter()); // 10

// ES6를 지원하지 않는 IE의 경우, 스코프 세이프 생성자 패턴 사용 가능
// Scope Safe Pattern
function Circle2(radius) {
  // 이 함수가 new 연산자와 함게 호출되지 않았다면(일반 함수 호출) 이 시점의 this는 전역 객체 window
  // 즉 this와 Circle은 프로토타입에 의해 연결되지 않는다
  if (!(this instanceof Circle)) {
    return new Circle(radius);
  }
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}
const circle2 = Circle2(5);
console.log(circle2.getDiameter()); // 10
// new 없이 호출해도 동일하게 동작하는 Object
let obj = new Object();
console.log(obj); // {}

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

// new 없이 호출할 경우 다른 데이터 타입을 반환하는 Number
const numObj = new Number("123");
console.log(numObj, typeof numObj); // [Number: 123] object

const num = Number("123");
console.log(num, typeof num); // 123 number

+ Recent posts