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

내부 슬롯과 내부 메서드

  • 내부 슬롯 internal slot과 내부 메서드 internal method는 자바스크립트 엔지의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에 사용하는 의사 프로퍼티 pseudo property와 의사 메서드 pseudo method
  • ECMASciprt 사양에 등장하는 이중 대괄호([[...]])로 감싼 이름에 해당
  • 자바스크립트의 내부 로직이므로 원칙적으로 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지는 않음 (일부에 한하여 간접적으로 접근 가능)
const o = {};
// 내부 슬롯은 직접 접근 불가능
o.[[Prototype]] // SyntaxError
// 일부에 한해 간접적으로 접근 가능
o.__proto__; // Object.prototype

프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

  • 자바스크립트 엔진은 프로퍼티를 생성할 때, 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의
  • 프로퍼티 어트리뷰트는 직접 접근할 수 는 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인 가능
  • 내부 상태 값 meta-property: 프로퍼티 상태
    • [[Value]]: 프로퍼티 값
    • [[Writable]]: 값의 갱신 여부
    • [[Enumerable]]: 열거 가능 여부
    • [[Configurable]]: 재정의 가능 여부
const person = {
  name: "Jin",
};

// Object.getOwnPropertyDescriptor 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체 반환
console.log(Object.getOwnPropertyDescriptor(person, "name")); 
// { value: 'Jin', writable: true, enumerable: true, configurable: true }
const person = {
  name: "Jin",
};
person.location = "Seoul";

// ES8에 도입된 Object.getOwnPropertyDescriptors 메서드는 모든 프로퍼티 어트리뷰트 정보 제공
console.log(Object.getOwnPropertyDescriptors(person));
// {
//   name: {
//     value: 'Jin',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   location: {
//     value: 'Seoul',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   }
// }

데이터 프로퍼티와 접근자 프로퍼티

  • 데이터 프로퍼티 data property: 키와 값으로 구성된 일반적인 프로퍼티
  • 접근자 프로퍼티 accessor property: 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수 accessor function으로 구성된 프로퍼티

데이터 프로퍼티

프로퍼티
어트리뷰트
프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Value]] value - 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값
- 프로퍼티 키를 통해 프로퍼티 값을 변경하면 [[Value]]에 값을 재할당. 이때 프로퍼티가 없으면 프로퍼티를 동적 생성하고 생성된 프로퍼티의 [[Value]]에 값저장
[[Writable]] writable - 프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값
- [[Writable]] 값이 false인 경우 해당 프로퍼티의  [[Value]] 값을 변경할 수 없는 읽기 전용 프로퍼티가 됨
[[Enumerable]] enumerable - 프로퍼티의 열거 가능 여부를 나타내며 불리언 값
- [[Enumerable]] 값이 false인 경우 해당 프로퍼티는 for ... in 문이나 Object.keys 메서드 등으로 열거 불가능
[[Configurable]] configurable - 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값
- [[Configurable]] 값이 false인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지 됨. 단, [[Writable]] 값이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것은 허용

접근자 프로퍼티

  • 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티
  • 접근자 함수는 getter / setter 함수라고도 부름
프로퍼티
어트리뷰트
프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Get]] get 접근자 프로퍼티를 통해 데이터 프로퍼티 값을 읽을 때 호출되는 접근자 함수. 즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환
[[Set]] set - 접근자 프로퍼티를 통해 데이터 프로퍼티 값을 저장할 때 호출되는 접근자 함수. 즉, 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환
[[Enumerable]] enumerable - 프로퍼티의 열거 가능 여부를 나타내며 불리언 값
- [[Enumerable]] 값이 false인 경우 해당 프로퍼티는 for ... in 문이나 Object.keys 메서드 등으로 열거 불가능
[[Configurable]] configurable - 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값
- [[Configurable]] 값이 false인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지 됨
const person = {
  // 데이터 프로퍼티
  firstName: "HongYeop",
  lastName: "Jin",

  // fullName은 접근자 함수로 구성된 접근자 프로퍼티
  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(" ");
  },
};

// 데이터 프로퍼티를 통한 프로퍼티 값의 참조 (setter함수 호출)
console.log(person.firstName + " " + person.lastName); // HongYeop Jin

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
person.fullName = "Ungmo Lee";
console.log(person); // { firstName: 'Ungmo', lastName: 'Lee', fullName: [Getter/Setter] }

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조 (getter함수 호출)
console.log(person.fullName); // Ungmo Lee

// 데이터 프로퍼티 어트리뷰트 확인
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
// {value: 'Ungmo', writable: true, enumerable: true, configurable: true}

// 접근자 프로퍼티 어트리뷰트 확인
descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log(descriptor);
// {get: [Function: get fullName], set: [Function: set fullName], enumerable: true, configurable: true}
// 일반 객체의 __proto__는 접근자 프로퍼티
Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");
// {enumerable: false, configurable: true, get: ƒ, set: ƒ}

// 함수 객체의 prototype은 데이터 프로퍼티
Object.getOwnPropertyDescriptor(function () {}, "prototype");
// {value: {…}, writable: true, enumerable: false, configurable: false}

프로퍼티 정의

  • Object.defineProperty 메서드를 사용하여 프로퍼티의 어트리뷰트를 정의 가능
  • Object.defineProperties 메서드를 사용하여 여러 개의 프로퍼티 한 번에 정의 가능
const person = {};

Object.defineProperty(person, "firstName", {
  value: "HongYeop",
  writable: true,
  enumerable: true,
  configurable: true,
});

Object.defineProperty(person, "lastName", {
  value: "Jin",
});

let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
// {value: 'HongYeop', writable: true, enumerable: true, configurable: true}

// 디스크립터 객체의 프로퍼티를 누락시키면 undefined, false가 기본 값
descriptor = Object.getOwnPropertyDescriptor(person, "lastName");
console.log(descriptor); // {value: 'Jin', writable: false, enumerable: false, configurable: false}

// [[Enumerable]] 값이 false인 경우
// 해당 프로퍼티는 for ... in 문이나 Object.keys 등으로 열거 불가능
// lastName 프로퍼티는 [[Enumerable]] 값이 false이므로 열거되지 않음
console.log(Object.keys(person)); // ['firstName']

// [[Writable]] 값이 false인 경우 해당 프로퍼티의 [[Value]] 값을 변경할 수 없음
// lastName 프로퍼티는 [[Writable]] 값이 false이므로 값을 변경할 수 없음
// 이때 값을 변경하면 에러는 발생하지 않고 무시
person.lastName = "Park";
console.log(person); // {firstName: 'HongYeop', lastName: 'Jin'} lastName이 변경되지 않음

// [[Configurable]] 값이 false인 경우 해당 프로퍼티를 삭제할 수 없음
// lastName 프로퍼티는 [[Configurable]] 값이 false이므로 값을 삭제할 수 없음
// 이떄 프로퍼티를 삭제하면 에러는 발생하지 않고 무시
delete person.lastName;
console.log(person); // {firstName: 'HongYeop', lastName: 'Jin'} lastName이 삭제되지 않음

// [[Configurable]] 값이 false인 경우 해당 프로퍼티를 재정의할 수 없음
// Object.defineProperty(person, "lastName", { enumerable: true });
// Uncaught TypeError: Cannot redefine property: lastName

// 접근자 프로퍼티 정의
Object.defineProperty(person, "fullName", {
  // getter 함수
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set(name) {
    [this.firstName, this.lastName] = name.split(" ");
  },
  enumerable: true,
  configurable: true,
});

descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log("fullName", descriptor); // fullName {enumerable: true, configurable: true, get: ƒ, set: ƒ}

person.fullName = "Ungmo Lee";
console.log(person); // {firstName: 'Ungmo', lastName: 'Jin'}
const person = {};

Object.defineProperties(person, {
  // 데이터 프로퍼티 정의
  firstName: {
    value: "HongYeop",
    writable: true,
    enumerable: true,
    configurable: true,
  },
  lastName: {
    value: "Jin",
    writable: true,
    enumerable: true,
    configurable: true,
  },
  // 접근자 프로퍼티 정의
  fullName: {
    // getter 함수
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    // setter 함수
    set(name) {
      [this.firstName, this.lastName] = name.split(" ");
    },
    enumerable: true,
    configurable: true,
  },
});

person.fullName = "UngMo Lee";
console.log(person); // {firstName: 'UngMo', lastName: 'Lee'}

객체 변경 방지

  • 객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있음
  • 자바스크립트는 객체의 변경을 방지하는 다양한 메서드 제공
구분 메서드 프로퍼티
추가
프로퍼티
삭제
프로퍼티
값 읽기
프로퍼티
값 쓰기
프로퍼티 어트리뷰트 재정의
객체 확장 금지 Object.preventExtensions X O O O O
객체 밀봉 Object.seal X X O O X
객체 동결 Object.freeze X X O X X

객체 확장 금지

  • Object.preventExtensions 메서드는 객체의 확장 금지
  • 확장이 금지된 객체는 프로퍼티 추가가 금지
  • 프로퍼티의 추가는 동적추가와 Object.defineProperty 두가지 방법 모두 금지
  • 확장 가능 여부는 Object.isExtensible 메서드로 확인 가능
const person = { name: "Jin" };

// person 객체는 확장 가능 객체
console.log(Object.isExtensible(person)); // true

// person 객체 확장 금지
Object.preventExtensions(person);

// person 객체는 확장 불가능 객체
console.log(Object.isExtensible(person)); // false

// 프로퍼티 추가 금지됨
person.age = 29; // 무시 strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 추가 금지되지만 삭제 가능
delete person.name;
console.log(person); // {}

// 프로퍼티 정의에 의한 프로퍼티 추가도 금지
Object.defineProperty(person, "age", { value: 29 });
// TypeError: Cannot define property age, object is not extensible

객체 밀봉

  • Object.seal 메서드는 객체를 밀봉
  • 객체 밀봉이란 프로퍼티 추가 및  삭제와 프로퍼티 어트리뷰트 재정의 금지
  • 즉, 밀봉된 개체는 읽기와 쓰기만 가능
  • 밀봉 여부는 Object.isSealed 메서드로 확인 가능
const person = { name: "Jin" };

// person 객체는 밀봉안된 객체
console.log(Object.isSealed(person)); // true

// person 객체 밀봉
Object.seal(person);

// person 객체는 밀봉된 객체
console.log(Object.isSealed(person)); // false

// 밀봉된 객체는 configurable이 false
console.log(Object.getOwnPropertyDescriptors(person));
// {name: {value: 'Jin', writable: true, enumerable: true, configurable: false}}

// 프로퍼티 추가 금지됨
person.age = 29; // 무시 strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 삭제 금지
delete person.name; // 무시 strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 값 갱신 가능
person.name = "Park";
console.log(person); // { name: 'Park' }

// 프로퍼티 재정의 금지
Object.defineProperty(person, "name", { configurable: true });
// TypeError: Cannot redefine property: name

객체 동결

  • Object.freeze 메서드는 객체를 동결
  • 객체 동결은 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트의 재정의 금지, 프로퍼티 값 갱신 금지 의미
  • 즉, 동결된 개체는 읽기만 가능
  • 동결 여부는 Object.isFrozen 메서드로 확인 가능
const person = {
  name: "Jin",
};

// person 객체는 동결된 객체 아님
console.log(Object.isFrozen(person)); // false

// person 객체 동결. 프로퍼티 추가, 삭제, 재정의, 쓰기 금지
Object.freeze(person);

// person 객체는 동결된 객체
console.log(Object.isFrozen(person)); // true

// 동결된 객체는 writable과 configurable이 false
console.log(Object.getOwnPropertyDescriptors(person));
// {name: {value: 'Jin', writable: false, enumerable: true, configurable: false}}

// 프로퍼티 추가 금지
person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 삭제 금지
delete person.name; // 무시. strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 값 갱신 금지
person.name = "Park"; // 무시. strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 어트리뷰트 재정의 금지
Object.defineProperty(person, "name", { configurable: true });
// TypeError: Cannot redefine property: name

불변 객체

  • 지금까지 변경 방지 메서드들은 얕은 변경 방지 shallow only로 직속 프로퍼티만 변경 방지
  • 객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 대해서 재귀적으로 Object.freeze 메서드를 호출해야 함
const person = {
  name: "Jin",
  address: { city: "Seoul" },
};

// 얕은 객체 동결
Object.freeze(person);

// 직속 프로퍼티만 동결
console.log(Object.isFrozen(person)); // true
// 중첩 객체는 동결하지 못함
console.log(Object.isFrozen(person.address)); // false
// 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Oject.freeze 메서드를 호출하는 함수
function deepFreeze(target) {
  // 객체가 아니거나 동결된 객체는 무시하고 동결되지 않은 객체만 동결
  if (target && typeof target === "object" && !Object.isFrozen(target)) {
    Object.freeze(target);
    // Oject.keys 메서드로 열거 가능한 프로퍼티 키를 배열로 반환,
    // forEach 메서드로 해당 배열을 순회하여 배열의 각 요소에 대해 콜백함수 실행
    Object.keys(target).forEach((key) => deepFreeze(target[key]));
  }
  return target;
}

const person = {
  name: "Jin",
  address: { city: "Seoul" },
};

deepFreeze(person);

console.log(Object.isFrozen(person)); // true
// 중첩 객체는 동결하지 못함
console.log(Object.isFrozen(person.address)); // true

var 키워드로 선언한 변수의 문제점

변수 중복 선언 허용

  • var 키워드로 선언한 변수는 중복 선언 가능
  • 선언한 변수를 중복 선언하면
    • 초기화문이 있는 변수 선언문은 var 키워드가 없는 것처럼 동작
    • 초기화문이 없는 변수 선언문은 무시되고 에러 발생되지 않음
var x = 1;
var y = 1;

// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언 허용
// 초기화문이 있는 경우: var 키워드가 없는 것처럼 동작
var x = 100;
console.log(x); // 100

// 초기화문이 없는 경우: 변수 선언문 무시
var y;
console.log(y); // 1

함수 레벨 스코프

  • var 키워드로 선언한 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정
var x = 1;

// if문의 변수 선언문에서 var 키워드로 선언한 변수도 전역변수가 됨
if (true) {
  var x = 10;
}

console.log(x); // 10

// for문의 변수 선언문에서 var 키워드로 선언한 변수도 전역변수가 됨
for (var x = 0; x < 5; x++) {}

console.log(x); // 5

변수 호이스팅

  • var 키워드로 변수를 선언하면 변수 호이스팅에 의해 변수 선언문이 스코프의 선두로 올려진 것처럼 동작
  • 할당문 이전에 변수를 참조할 경우 undefined 반환
// 이 시점에
// 1. 호이스팅에 의해 변수 foo는 이미 선언
// 2. foo는 undefined로 초기화
console.log(foo); // undefined

foo = 123;

console.log(foo); //123

// 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 실행
var foo;

let 키워드

변수 중복 선언 금지

  • let 키워드로 이름이 같은 변수를 중복 선언하면 문법 에러 발생 SyntaxError
var foo = 123;
// var 키워드 중복 선언 허용
// 아래 변수 선언문은 var 키워드가 없는 것처럼 동작
var foo = 456;

let bar = 123;
// let이나 const 키워드로 선언된 변수는 같은 스코프 내에서 변수 중복 선언 허용 x
let bar = 456; // SyntaxError: Identifier 'bar' has already been declared

블록 레벨 스코프

  • let 키워드로 선언한 변수는 모든 코드 블록 (함수, if 문, for문, while문, try/catch 문 등)을 지역 스코프로 인정하는 블록 레벨 스코프 block-level scope를 따름

블록 레벨 스코프의 중첩

변수 호이스팅

  • var 키워드로 선언한 변수와 달리 let 키워드로 선언한 변수는 변수 호이스팅이 발생하지 않는 것처럼 동작
  • let 키워드로 선언한 변수는 "선언 단계"와 "초기화 단계"가 분리되어 진행
  • 초기화 단계가 실행되기 이전에 변수를 접근하려고 하면 참조 에러 ReferenceError 발생
  • 일시적 사각지대  TDZ; Temporal Dead Zone: 스코프의 시작 시점부터 초기화 시작 시점까지 변수를 참조할 수 없는 구간
  • 호이스팅이 발생하지 않는 것처럼 동작하는 것이지 실제로 호이스팅은 발생
console.log(foo); 
// 브라우저: ReferenceError: foo is not defined
// NodeJs: ReferenceError: Cannot access 'foo' before initialization
let foo;
// var
// 선언한 변수는 런타임 이전에 선언 단계와 초기화 단계가 실행
// 따라서 변수 선언 이전에 변수를 참조 가능
console.log(foo); // undefined

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

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

// let
// 런타임 이전에 선언 단계가 실행 / 초기화는 진행 x
// 초기화 이전의 일시적 사각 지대에서는 변수 참조 불가능
console.log(bar); // ReferenceError: Cannot access 'bar' before initialization

let bar; // 변수 선언문에서 초기화 실행
console.log(bar); // undefined

bar = 2; // 할당문에서 할당 실행
console.log(bar); // 2

var vs let 변수의 생명주기

// let의 호이스팅
let foo = 1; // 전역 변수

{
  console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
  let foo = 2; // 지역 변수
}

전역 객체와 let

  • var 키워드로 선언한 전역 변수와 전역 함수, 선안하지 않은 변수에 값을 할당한 암묵적 전역은 전역 객체 window의 프로퍼티가 됨
  • 참고: 전역 객체의 프로퍼티를 참조할 때 window를 생략할 수 있음
  • let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아님
  • let 전역 변수는 보이지 않는 개념적인 블록(전역 렉시컬 환경의 선언적 환경 레코드) 내에 존재
// 전역 변수
var x = 1;
// 암묵적 전역
y = 2;
// 전역 함수
function foo() {}

// var키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티
console.log(window.x); // 1
// 전역 객체 window의 프로퍼티는 전역 변수처럼 사용 가능
console.log(x); // 1

// 암묵적 전역은 전역 객체 window의 프로퍼티
console.log(window.y); // 2
console.log(y); // 2

// 함수 선언문으로 정의한 전역 함수는 전역 객체 window의 프로퍼티
console.log(window.foo); // ƒ foo() {}
console.log(foo); // ƒ foo() {}
let x = 1;

// let, const키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티가 아님
console.log(window.x); // undefined
console.log(x); // 1

const 키워드

  • const 키워드는 상수 const를 선언하기 위해 사용
  • 반드시 상수만을 위해 사용하지는 않음

선언과 초기화

  • const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화해야 함
  • let 키워드와 마찬가지로 블록 레벨 스코프, 변수 호이스팅이 발생하지 않는 것처럼 동작
// const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화
const foo = 1;

const bar // SyntaxError: Missing initializer in const declaration

재할당 금지

  • const 키워드로 선언한 변수는 재할당이 금지
  • var 또는 let 키워드로 선언한 변수는 재할당이 자유로움
const foo = 1;
foo = 2; // TypeError: Assignment to constant variable.

상수

  • const 키워드로 선언한 변수에 원시 값을 할당한 경우 변수 값을 변경할 수 없음. 원시 값은 변경 불가능한 값이므로 해당 변수에는 재할당 없이 값을 변경 불가능. 이 특징을 이용해 const 키워드를 상수를 표현하는데 사용
  • 상수: 재할당이 금지된 변수
  • 일반적으로 상수의 이름은 대문자로 선언해 상수임을 명확히 나타냄. 여러 단어로 이뤄진 경우에는 언더스코어(_)로 구분해서 스네이크 케이스로 표현하는 것이 일반적
const TAX_RATE = 0.1;

let preTaxPrice = 100;
let afterTaxPrice = preTaxPrice + preTaxPrice * TAX_RATE;

console.log(afterTaxPrice); // 110

const 키워드와 객체

  • const 키워드로 선언된 변수에 객체를 할당한 경우 값을 변경할 수 있음
  • const 키워드는 재할당을 금지할 뿐, "불변"을 의미하지 않음
const person = {
  name: "Jin",
};

// 객체는 변경 가능한 값. 따라서 재할당 없이 변경 가능
person.name = "Park";

console.log(person); // { name: 'Park' }

var vs. let vs. const

  • ES6를 사용한다면 var 키워드는 사용하지 않음
  • 재할당이 필요한 경우에 한정해 let 키워드 사용. 이때 변수의 스코프는 최대한 좁게
  • 변경이 발생하지 않고 읽기 전용으로 사용하는(재할당이 필요 없는 상수) 원시 값과 객체에는 const 사용
  • ES6 기준: 기본적으로 const 사용하되 재할당이 필요한 경우에만 let 사용

+ Recent posts