enum은 명명된 숫자 상수의 집합으로 열거형이라 불립니다. C/C++, 자바, 파이썬 등 여러 프로그래밍 언어에서는 enum을 지원하지만 자바스크립트에서는 지원하지 않습니다. 그래서 위와 같이 심볼과 Object.freeze 메서드를 이용해서 enum을 구현할 수 있습니다.
ES6 이전에서는 모든 함수가 callable이며 constructor입니다. 하지만 ES6 이후부터는 메서드(ES6에서의 메서드)와 화살표 함수는 callable이면서 non-constructor입니다. 이러한 구분이 중요한 이유는 호출 용도로만 사용될 함수가 constructor이면 불필요한 prototype 프로퍼티를 가지며, 프로토타입 객체도 생성하는 불필요한 작업이 추가되기 때문입니다.
메서드
const player = {
name: "devhun",
// 메서드 축약표현
sayHello() {
console.log(`my name is ${name}`);
},
};
과거에는 메서드에 대한 명확한 정의가 없었지만, ES6 이후부터 메서드는 메서드 축약 표현으로 정의된 함수만을 의미합니다. ES6 사양에서 정의한 메서드는 non-constructor로서 new 연산자와 함께 호출될 경우 TypeError가 발생됩니다.
super 키워드로 부모 클래스의 프로퍼티 및 메서드를 참조하려면 반드시 ES6에서 정의한 메서드 축약표현으로 정의된 함수로만 참조할 수 있습니다.
화살표 함수
화살표 함수는 표현만 간략한 것이 아니라 내부 동작도 기존의 함수보다 간략합니다. 화살표 함수는 non-constructor이며 prototype 프로퍼티도 없으며 프로토타입도 생성하지 않습니다.
화살표 함수는 함수 내부에서 this, arguments, super new.target을 참조하면 스코프 체인을 통해 상위 스코프의 this, arguments, super, new.target을 참조합니다.
화살표 함수는 콜백 함수로서 사용하기에 적합하며 this를 가지지 않기 때문에 인스턴스의 메서드나 프로토타입 메서드로서는 사용하기에 적합하지 않습니다.
Rest 파라미터
function foo(...args) {
console.log(args);
}
foo(1, 2, 3, 4);
Rest 파라미터는 매개변수 이름 앞에 세개의 점 ... 을 붙여서 정의한 매개변수를 의미합니다. Rest 파라미터는 함수에 전달된 인수들의 목록을 배열로 전달받습니다.
Rest 파라미터는 이름 그대로 먼저 선언된 매개변수에 할당된 인수를 제외한 나머지 인수들로 구성된 배열이 할당됩니다. 따라서 Rest 파라미터는 반드시 마지막 파라미터여야 합니다.
Rest 파라미터는 함수 정의 시 선언한 매개변수 개수를 나타내는 함수 객체의 length 프로퍼티에 영향을 주지 않습니다.
Rest 파라미터는 배열이지만, arguments는 유사 배열 객체이며 메서드와 화살표 함수에서는 사용할 수 없습니다.
const Person = function () {
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log(`name : ${this.name}`);
};
return Person;
};
let me = new Person();
me.sayHi();
자바스크립트는 프로토타입 기반의 객체지향 언어입니다. 클래스가 없어도 위와같이 생성자 함수와 프로토타입을 통한 상속을 구현할 수 있습니다.
ES6 클래스
ES6에서 C++, Java, C#과 같은 클래스 기반 객체지향 언어들과 비슷한 방식의 객체 생성 메커니즘이 도입되었습니다. 이는 자바스크립트의 프토토타입 기반 객체지향 모델을 폐지하는 것이 아니며 프로토타입 기반 패턴을 클래스 기반 패턴처럼 사용할 수 있도록 하는 새로운 객체 생성 메커니즘이다.(자바스크립트에서는 클래스 또한 객체입니다.)
자바스크립트 클래스와 생성자의 차이점
1. new 사용여부
클래스를 new 연산자 없이 호출할 경우 에러가 발생하지만, 생성자는 new 없이 일반 함수로 호출할 수 있습니다.
2. extends, super 키워드
클래스는 상속을 지원하는 extends와 super 키워드를 제공합니다.
3. strict mode
클래스 내의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되며 strict mode를 해제할 수 없습니다. 생성자는 strict mode가 암묵적으로 적용되지 않습니다.
4. [[Enumerable]]
클래스의 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트로 [[Enumerable]]의 값이 false로 적용됩니다.
클래스 정의
클래스 정의
// 선언문
class Person {}
// 익명 클래스 표현식
class Person = class {};
// 기명 클래스 표현식
class Person = class MyClass{};
클래스는 클래스 키워드를 사용해서 정의합니다. 클래스 이름은 생성자 함수와 마찬가지로 파스칼 케이스를 사용하는 것이 일반적입니다.
클래스는 constructor, 프로토타입 메서드, 정적 메서드 3가지를 정의할 수 있습니다.
클래스 호이스팅
class Person {}
console.log(typeof Person);
클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정, 즉 런타임 이전에 먼저 평가되어 함수 객체를 생성합니다. 이때 클래스가 평가되어 생성된 함수 객체는 생성자 함수로서 호출할 수 있는 constructor입니다. 생성자 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됩니다. 다만, 클래스는 클래스 정의 이전에 참조할 수 없습니다.
호이스팅 테스트
const Person = "";
{
// 호이스팅이 발생하지 않는다면 ""를 출력해야 합니다.
console.log(Person);
class Person {}
}
위 코드를 테스트하면 클래스 또한 호이스팅 대상이 된다는 것을 알 수 있습니다. 다만 클래스는 let, const 키워드와 같이 호이스팅되며 일시적 사각지대로 정의 이전에 참조할 경우 참조 에러가 발생됩니다.
인스턴스 생성
class Person {}
const me = new Person();
console.log(me); // Person {}
클래스는 생성자 함수이며 new 연산자와 함께 호출되어 인스턴스를 생성합니다. 클래스는 오로지 인스턴스를 생성하기 위해서 존재하며 반드시 new 연산자와 함께 호출해야 합니다.
메서드
과거에는 클래스의 프로퍼티 정의는 constructor 내부에서만 가능했지만 추후 클래스 몸체에 메서드뿐만 아니라 프로퍼티를 직접 정의할 수 있는 표준안이 새롭게 추가될 예정입니다..(이미 모던 브라우저에서는 추가되어있습니다.)
constructor
constructor는 인스턴스를 생성하고 초기화하기 위한 특수한 메서드입니다. constructor는 이름을 변경할 수 없습니다.
클래스도 함수 객체의 고유 프로퍼티를 모두 갖고있는 객체입니다. 그리고 모든 함수 객체가 가지고 있는 prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor 프로퍼티는 클래스 자신을 가리키고 있습니다.
constructor 내부 this에 추가한 프로퍼티는 생성한 인스턴스의 프로퍼티가 되며 초기화됩니다.
클래스 내부의 constructor 메서드는 클래스가 평가되어 함수 객체 일부로 됩니다.
constructor는 클래스 내에 최대 한 개만 존재할 수 있으며, 2개 이상 정의될 경우 문법 에러가 발생됩니다. 원한다면 constructor를 생략하여 이를 이용해 빈 인스턴스를 생성할 수 있습니다.
constructor 함수는 생성자 함수처럼 return문을 넣지 않고 정의해야 합니다.
프로토타입 메서드
class Person {
constructor(name) {
this.name = name;
}
// 프로토타입 메서드
sayHi() {
console.log(`name : ${name}`);
}
}
클래스의 프로토타입 메서드는 생성자 함수와는 다르게 클래스 내부에 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으로 프로토타입 메서드가 됩니다.
프로토타입 체인은 기존의 객체 리터럴, 생성자 함수, Object.create 메서드 등과 같이 클래스에 의해 생성된 인스턴스에도 동일하게 적용됩니다. 결국 클래스는 생성자 함수와 마찬가지로 프로토타입 기반의 객체 생성 메커니즘입니다.
정적 메서드
class Person {
constructor(name) {
this.name = name;
}
static sayHello() {
console.log("Hello World");
}
}
정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있는 메서드를 말합니다. 그리고 클래스 정의가 평가되는 정적 메서드는 클래스에 바인딩된 메서드가 됩니다.
정적 메서드는 생성자 함수로 생성한 인스턴스와 마찬가지로 인스턴스 프로토타입 체인 상에 존재하지 않기 때문에 인스턴스로는 정적 메서드를 호출할 수 없습니다.
정적 메서드와 프로토타입 메서드의 차이
정적 메서드와 프로토타입 메서드는 자신이 속해 있는 프로토타입 체인이 다릅니다.
정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출합니다.
정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있습니다.
클래스에서 정의한 메서드 특징
function 키워드를 생략한 메서드 축약 표현을 사용합니다.
객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요 없습니다.
암묵적으로 strict mode로 실행됩니다.
for...in 문이나 Object.keys 메서드 등으로 열거할 수 없습니다. 즉, 프로퍼티 어트리뷰트의 [[Enumerable]]이 false입니다.
내부 메서드 [[Construct]]를 갖지 않는 non-constructor입니다.
클래스의 인스턴스 생성 과정
1. 인스턴스 생성과 this 바인딩
new 연산자와 함께 클래스를 호출하면 constructor 내부 코드가 실행되기에 앞서 암묵적으로 빈 객체가 생성되며, 해당 인스턴스의 프로토타입으로 클래스의 prototype이 가리키는 객체로 바인딩됩니다. 그리고 해당 인스턴스는 this에 바인딩됩니다.
2. 인스턴스 초기화
constructor의 내부 코드가 실행되어 this에 바인됭되어 있는 인스턴스를 초기화합니다.
3. 인스턴스 반환
클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환됩니다.
프로퍼티
class Person {
constructor(name) {
this.name = name;
}
}
인스턴스 프로퍼티는 constructor 내부에서 정의해야합니다. 해당 클래스로 생성한 인스턴스의 프로퍼티는 모두 동일한 프로퍼티를 가집니다. 하지만, 다른 객체지향 언어와는 다르게 public, protected, private와 같은 접근 제한자를 지원하진 않습니다.
접근자 프로퍼티
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
set fullName(name) {
[this.firstName, this.lastName] = name.split(" ");
}
}
접근자 프로퍼티는 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티입니다.
프로토타입 기반 상속은 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 개념이지만, 상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것입니다.
클래스는 생성자와 달리 extends 키워드를 통해서 직관적이고 간편하게 클래스를 상속받아 확장할 수 있습니다.
extends
extends 키워드를 이용해서 상속 관계를 구현한다면은 부모 클래스와 자식 클래스의 인스턴스 프로토타입 체인뿐만 아니라 클래스간의 프로토타입 체인도 생성합니다.
동적 상속
function Base1() {}
class Base2 {}
let condition = true;
class Derived extends (condition ? Base1 : Base2) {}
const derived = new Derived();
extends 키워드 앞에는 반드시 클래스가 와야합니다. 하지만, extends 키워드 뒤에는 클래스뿐만이 아니라 [[Construct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있습니다.
서브클래스의 constructor
// 평가 전
class Base {}
class Derived extends Base {}
// 평가 후
class Base {
constructor(...args) {}
}
class Derived extends Base {
constructor(...args) {
super(...args);
}
}
클래스에서 constructor를 생략하면 ...args 인자를 받은 constructor가 암묵적으로 정의됩니다. 또한 자식 클래스에서는 super()를 통해서 부모 클래스의 constructor를 호출하도록 암묵적으로 정의됩니다.
super 키워드
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
class Derived extends Base {
constructor(a, b, c) {
super(a, b);
this.c = c;
}
}
super 키워드는 함수처럼 호출할 수도 있고 this와 같이 식별자처럼 참조할 수 있는 특수한 키워드입니다. super를 호출하면 부모 클래스의 constructor를 호출 수 있고, super를 참조하면 부모 클래스의 메서드를 호출할 수 있습니다.
자식 클래스에서 constructor를 생략하지 않는다면 super는 반드시 호출해야하며, super를 호출하기 전에는 this를 참조할 수 없습니다. 또한 자식 constructor 외에는 super를 호출할 수 없습니다.
super 참조
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi ${this.name}`;
}
}
class Derived extends Base {
sayHi() {
return `${super.sayHi()}. how are you doing?`;
}
}
메서드 내에서 super를 참조하면 부모 클래스의 메서드를 호출할 수 있습니다.
자식 클래스 정적 메서드 내부에서 부모 클래스 정적 메서드를 super를 통해 참조가 가능합니다.
상속 클래스의 인스턴스 생성 과정
1. 자식 클래스의 super 호출
자바스크립트 엔진은 클래스를 평가할 때 부모와 자식 클래스를 구분하기 위해 "base" 또는 "derived"를 값으로 갖는 내부 슬롯인 [[ConstructorKind]]을 가지고 있습니다. 이를 통해 부모 클래스와 자식 클래스는 new 연산자와 함께 호출되었을 때의 동작이 구분됩니다.
자식 클래스는 자신이 직접 인스턴스를 생성하지 않고 부모 클래스에게 인스턴스 생성을 위임합니다. 이것이 바로 자식 클래스 constructor 내부에서 반드시 super를 호출해야 하는 이유입니다.
2. 부모 클래스의 인스턴스 생성과 this 바인딩
비록 인스턴스는 부모 클래스가 생성하지만 this에 바인딩된 값은 생성된 인스턴스 즉, 부모 클래스를 상속받은 자식 클래스 인스턴스입니다.
3. 부모 클래스의 인스턴스 초기화
부모 클래스의 constructor가 실행되어 this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 인자로 전달된 값을 통해서 초기화합니다.
4. 자식 클래스 constructor로의 복귀와 this 바인딩
super는 호출을 완료하면 생성한 인스턴스가 바인딩된 this를 return합니다. 이러한 메커니즘 때문에 super를 호출하기 전에는 this를 참조할 수 없습니다.
5. 자식클래스의 인스턴스 초기화
super 호출 이후, 자식 클래스의 constructor에 기술되어 있는 로직과 전달받은 인자로 인스턴스의 프로퍼티를 초기화합니다.
const x = 1;
function outer() {
const x = 10;
const inner function () {
console.log(x);
};
return inner;
}
const innerFunc = outer();
innerFunc();
outer 함수 실행 후 innerFunc를 실행하면 전역의 x가 아닌 outer 함수 내부에 있는 x 변수가 출력됩니다. 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 중기가 종료한 외부 함수의 변수를 참조할 수 있는데 이러한 중첩 함수를 클로저(closure)라고 부릅니다.
함수는 평가되고 함수 객체가 생성될 때 현재 렉시컬 환경을 [[Environment]] 내부 슬롯으로 참조하고 있다가 해당 함수가 호출되어 실행 컨텍스트를 생성한 후 실행 컨텍스트의 외부 렉시컬 환경에 대한 참조값으로 사용한다. 이러한 메커니즘을 통해서 클로저가 외부 함수의 생명주기가 종료되어도 외부 렉시컬 환경에 대해 참조가 가능하며 가비지 컬렉터 또한 해당 렉시컬 환경을 컬렉팅 하지 않습니다.
클로저와 자바스크립트 최적화
사례 1
function foo() {
const x = 1;
const y = 2;
function bar() {
const z = 3;
console.log(z);
}
return bar;
}
const bar = foo();
bar();
위 코드를 본다면은 bar 중첩 함수가 return되어 호출되기 때문에 foo의 렉시컬 환경이 유지될것 같지만 모던 브라우저에서 실행한다면은 최적화를 통해 상위 렉시컬 환경인 foo의 렉시컬 환경을 기억하지 않습니다. 그래서 bar는 클로저라 할 수 없습니다.
사례 2
function foo() {
const x = 1;
function bar() {
console.log(x);
}
bar();
}
foo();
위 코드같은 경우는 bar는 중첩 함수이지만 foo 함수의 생명주기보다 짧기 때문에 클로저라고 하지 않습니다.
사례 3
function foo() {
const x = 1;
const y = 2;
function bar() {
console.log(x);
}
return bar;
}
const bar = foo();
bar();
위 예제를 보면은 bar는 상위 스코프의 식별자를 참조하고 있으므로 클로저입니다. 위와 같이 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에만 클로저 라고 합니다.
위 코드에서 x만을 클로저가 참조하고 있습니다. 이러한 경우 자바스크립트 엔진의 최적화를 통해서 y 변수는 제외하고 x만을 참조하여 기억합니다. x와 같이 클로저에 의해 참조되어 기억되는 변수를 자유 변수(free variable)이라고 합니다.
위 코드는 클로저 메커니즘을 활용하여 number를 다른 외부에서 참조하지 못하도록하고 오로지 return된 객체의 메서드를 통해서만 값의 변경을 줄 수 있도록하여 코드의 안정성을 높일 수 있습니다.
예제 2
const Counter = function () {
let number = 0;
function Counter() {}
Counter.prototype.increment = function () {
return ++number;
};
Counter.prototype.decrement = function () {
return --number;
};
return Counter;
};
const co = new Counter();
console.log(co.increment());
생성자 함수를 생성할 때 외부에 공개하지 않을 데이터 만들 때 클로저 메커니즘을 활용할 수 있습니다.
예제 3
function makeCounter(aux) {
let number = 0;
return function () {
return aux(number);
};
}
function increment(number) {
return ++number;
}
const func = makeCounter(increment);
func();
클로저 메커니즘을 활용해서 위와 같은 프로그래밍 또한 가능합니다.
캡슐화와 정보은닉
const Player = (function () {
let mAge = 0;
function Player(name, age) {
this.name = name;
mAge = age;
}
Player.prototype.sayHi = function () {
console.log(`name : ${this.name}, age : ${mAge}`);
};
return Player;
})();
const me1 = new Player("devhun1", 20);
me1.sayHi();
const me2 = new Player("devhun1", 30);
me2.sayHi();
me1.sayHi(); // mAge의 값이 30으로 바뀜
클로저를 활용해서 정보 은닉성을 구현할 수 있지만, C/C++, Java 같은 언어에서 제공하는 완벽한 정보 은닉성을 구현할 수는 없습니다.
전역에 존재하는 소스코드를 말합니다. 전역에 정의된 함수, 클래스 등의 내부 코드는 포함되지 않습니다.
함수 코드
함수 내부에 존재하는 소스코드를 말합니다. 함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않습니다.
eval 코드
빌트인 전역 함수인 eval 함수에 전달되어 실행되는 소스코드를 말합니다.
모듈 코드
모듈 내부에 존재하는 소스코드를 말합니다. 모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않습니다.
ECMAScript 사양은 위와같이 소스코드를 4가지 타입으로 구분합니다.
소스코드를 4가지 타입으로 구분하는 이유는 소스코드의 타입에 따라 실행 컨텍스트를 생성하는 과정과 관리 내용이 다르기 때문입니다.
소스코드 타입마다 실행 컨텍스트가 필요한 이유
전역 코드
전역 코드는 전역 변수를 관리하기 위해 최상위 스코프인 전역 스코프를 생성해야합니다. 그리고 var로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수를 전역 객체의 프로퍼티와 메서드로 바인딩하고 참조하기 위해 전역 객체와 연결되어야 합니다. 이를 위해 전역 코드가 평가되면 전역 실행 컨텍스트가 생성됩니다.
함수 코드
함수 코드는 지역 스코프를 생성하고 지역 변수, 매개변수, arguments 객체를 관리해야 합니다. 그리고 생성한 지역 스코프를 전역 스코프에 연결해야 하며, 이를 위해 함수 코드가 평가되면 함수 실행 컨텍스트가 생성됩니다.
eval 코드
eval 코드는 strict mode(엄격 모드)에서 자신만의 독자적인 스코프를 생성합니다. 이를 위해 eval 코드가 평가되면 eval 실행 컨텍스트가 생성됩니다.
모듈 코드
모듈 코드는 모듈별로 독립적인 모듈 스코프를 생성합니다. 이를 위해 모듈 코드가 평가되면 모듈 실행 컨텍스트가 생성됩니다.
소스코드 평가와 실행
모든 소스코드는 실행에 앞서 평가 과정을 거치며 코드를 실행하기 위한 준비를 합니다. 즉, 자바스크립트 엔진은 "소스코드 평가", "소스코드 실행" 2개의 과정으로 나누어 처리합니다.
소스코드 평가
소스코드 평가 과정에서는 실행 컨텍스트를 생성하고 변수, 함수 등의 선언문만 먼저 실행하여 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록합니다.
소스코드 실행
소스코드 평가 과정이 끝나면 선언문을 제외한 소스코드가 순차적으로 실행되는 런타임이 시작됩니다. 소스코드를 실행하면서 필요한 변수, 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 취득합니다. 그리고 변수 값의 변경 등 소스코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록합니다.
실행 컨텍스트의 역할
const x = 1;
const y = 2;
function test(a) {
const x = 10;
const y = 20;
console.log(a + x + y);
}
sayHello(50);
console.log(x + y);
자바스크립트 엔진이 아래 코드를 평가하고 실행하는 방식은 아래와 같습니다.
1. 전역 코드 평가
전역 코드 평가를 통해 변수 선언문과 함수 선언문이 실행되고, 그 결과 생성된 전역 변수와 전역 함수가 실행 컨텍스트가 관리하는 전역 렉시컬 환경의 환경 레코드에 등록됩니다. 이 때 var 키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수는 전역 객체의 프로퍼티와 메서드가 됩니다.
2. 전역 코드 실행
전역 코드 평가 과정이 끝나면 런타임이 시작되어 전역 코드가 순차적으로 실행됩니다. 이 떄 전역 변수에 값이 할당되고 함수가 호출됩니다. 함수가 호출되면 순차적으로 실행되던 전역 코드의 실행을 일시 중단하고 코드 실행 순서를 변경하여 함수 내부로 진입합니다.
3. 함수 코드 평가
함수 호출에 의해 함수 내부로 진입하면 함수 내부의 문들을 실행하기에 앞서 함수 코드 평가 과정을 거치며 함수 코드를 실행하기 위한 준비를 합니다. 이 때 지역 실행 컨텍스트가 생성되고 매개변수와 지역 변수 선언문이 실행되어 지역 실행 컨텍스트의 환경 레코드에등록됩니다. 또한 함수 내부에서 지역 변수처럼 사용할 수 있는 arguments 객체가 생성되어 지역 실행 컨텍스트의 환경 레코드에 등록되고 this 바인딩도 결정됩니다.
4. 함수 코드 실행
함수 코드 평가 과정이 끝나면 런타임이 시작되어 함수 코드가 순차적으로 실행되기 시작합니다. 이 때 매개변수와 지역 변수에 값이 할당되면서 코드가 실행됩니다. console.log() 함수를 실행할 때 console은 스코프 체인을 통해서 전역 스코프에서 참조하며 .log는 프로토타입 체인으로 검색합니다. 그리고 a,x,y 식별자는 스코프 체인을 통해 검색하여 console.log() 함수를 실행 후 함수 코드를 모두 실행하였다면 다시 전역 코드로 돌아가서 이후 코드를 마저 실행합니다.
최종 분석
위와 같은 메커니즘을 구성하기 위해서는 실행 컨텍스트는 실별자와 스코프를 렉시컬 환경으로 관리하고 코드 실행 순서는 실행 컨텍스트 스택으로 관리합니다.
실행 컨텍스트 스택
자바스크립트 엔진은 가장 먼저 전역 코드를 평가하고 전역 실행 컨텍스트를 생성하여 실행 컨텍스트 스택에 push하고 이후 코드를 실행하면서 함수를 호출할 경우 지역 함수를 평가 및 실행 컨텍스트를 생성하고 실행 컨텍스트 스택에 push 하고 모두 실행했다면 pop을 합니다. 이러한 메커니즘을 통해서 코드의 실행 순서를 관리합니다. 실행 컨텍스트 스택에 가장 최상위에 존재하는 실행 컨텍스트를 실행 중인 실행 컨텍스트라 부릅니다.
렉시컬 환경
렉시컬 환경은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트입니다. 실행 컨텍스트 스택이 코드의 실행 순서를 관리한다면 렉시컬 환경은 스코프와 식별자를 관리합니다.
렉시컬 환경은 환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성됩니다.
환경 레코드(Environment Record)
스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소입니다. 환경 레코드는 소스코드의 타입에 따라 관리하는 내용의 차이가 있습니다.
외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference)
외부 렉시컬 환경에 대한 참조는 상위 스코프를 가리킵니다. 외부 렉시컬 환경에 대한 참조를 통해 단방향 링크드 리스트 자료구조의 스코프 체인을 구현합니다.
실행 컨텍스트 생성과 식별자 검색 과정
1. 전역 객체 생성
전역 객체는 전역 코드가 평가되기 이전에 생성됩니다. 이때 전역 객체에는 빌트인 전역 프로퍼티와 빌트인 전역 함수, 그리고 표준 빌트인 객체가 추가됩니다.
2. 전역 코드 평가
소스코드가 로드되면 자바스크립트 엔진은 전역 코드를 평가합니다. 전역 코드 평가는 다음과 같은 순서로 진행됩니다.
2.1 전역 실행 컨텍스트 생성
전역 실행 컨텍스트를 생성하여 실행 컨텍스트 스택에 푸시합니다.
2.2 전역 렉시컬 환경 생성
전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩합니다.(렉시컬 환경은 환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성됩니다.)
ES6 이후부터는 전역 환경 레코드는 기존의 전역 객체가 관리하던 var 전역 변수와 전역 함수 선언문으로 정의한 함수, 빌트인 전역 프로퍼티와 빌트인 전역 함수, 표준 빌트인 객체를 관리하는 객체 환경 레코드와 const와 let을 관리하는 선언적 환경 레코드와 this가 바인딩된 [[GlobalThisValue]] 내부슬롯으로 구성됩니다.
var 전역 변수와 다르게 전역 함수 선언문은 소스코드 평가 과정에서 객체 환경 레코드에 식별자가 등록되고 함수 객체가 생성되어 등록됩니다. 이러한 메커니즘으로 인해서 함수 선언문은 함수 선언문 이전에 호출할 수 있으며 변수 호이스팅과 함수 호이스팅의 차이점입니다.
선언적 환경 레코드는 let과 const 변수를 관리하는 환경 레코드로서 변수 호이스팅은 발생되지만 var 키워드와 달리 undefined로 초기화하지 않고 를 할당하여 변수 할당문 이전까지는 일시적 사각지대로서 해당 변수를 참조하면 참조 에러가 발생됩니다.
외부 렉시컬 환경을 통해 상위 렉시컬 환경에 접근할 수 있습니다. 전역 렉시컬 환경의 외부 렉시컬은 null이며 스코프 체인의 종점을 의미합니다.
3. 전역 코드 실행
위의 과정이 끝났다면 전역 코드를 순차적으로 실행합니다. 전역 코드가 실행되어 변수에 접근할 때 전역 실행 컨텍스트의 환경 레코드를 통해 참조합니다. 실행 컨텍스트에서 접근할 식별자를 결정하는 과정을 식별자 결정이라고 합니다. 만약, 실행 컨텍스트의 환경 레코드에 변수를 찾지 못했다면 상위 스코프의 실행 컨텍스트의 환경 레코드에 접근해야 합니다. 하지만, 전역 실행 컨텍스트는 스코프 체인의 종점이기 때문에 여기서 찾지 못했다면 참조 에러가 발생됩니다.
4. 함수 코드 평가
전역 코드 실행 중 함수를 실행하게 될 경우 해당 함수로 코드의 제어권이 이동합니다. 그리고 함수 코드 평가를 시작합니다.
함수 코드 평가는 함수 '실행 컨텍스트 생성' -> '함수 렉시컬 환경 생성' -> '함수 환경 레코드 생성' -> 'this 바인딩' -> '외부 렉시컬 환경에 대한 참조 결정' 순으로 평가합니다.
함수 실행 컨텍스트를 생성 합니다. 그리고 함수 렉시컬 환경을 생성하면 함수 실행 컨텍스트에 바인딩하여 실행 컨텍스트 스택에 push 합니다.
함수 렉시컬 환경의 환경 레코드에는 매개변수, arguments 객체, 지역 변수와 중첩 함수를 등록하고 관리합니다.
함수가 일반 함수로 호출되었을 때 [[ThisValue]] 내부 슬롯에 전역 객체가 바인딩되며, this를 호출하였을 때 전역 객체를 가리킵니다.
전역 선언문 함수의 외부 렉시컬 환경에 대한 참조는 함수 선언문이 평가된 시점에 결정됩니다. 자바스크립트는 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정합니다. 이러한 메커니즘은 함수 객체를 생성할 때 함수 객체의 [[Environment]] 내부 슬롯의 현재 실행 컨텍스트에 대한 참조를 저장하는 방식으로 구현됩니다. 즉, 해당 함수가 호출되어 실행 컨텍스트를 생성하고 렉시컬 환경의 외부 렉시컬 환경에 대한 참조 값을 [[Environment]] 내부 슬롯에 있는 값으로 셋팅합니다.
실행 컨텍스트와 렉시컬 환경 반환
실행 컨텍스트가 실행 완료되어 실행 컨텍스트 스택에서 pop 되어도 실행 컨텍스트의 렉시컬 환경은 가비지 컬렉팅 대상이 되지 않을 수 있습니다. 렉시컬 환경은 독립적인 객체로서 참조 여부에 따라서 가비지 컬렉팅 대상이 됩니다.
실행 컨텍스트와 블록 레벨 스코프
var 키워드는 함수 레벨 스코프만을 따르지만, let과 const는 모든 코드 블록을 지역 스코프로 인정합니다. 만약, 코드 실행 과정에서 if문과 같은 블록을 만날 경우 선언적 환경 레코드를 가지는 렉시컬 환경을 생성합니다. 그리고 외부 렉시컬 환경에 대한 참조는 if문이 실행되기 이전의 렉시컬 환경을 참조합니다.
스코프(Scope)는 모든 식별자가 선언된 위치에 의해 해당 식별자의 유효 범위가 결정되는데 이 유효범위를 스코프(Scope)라고합니다.
식별자 결정
const number = 15;
function PrintNumber() {
const number = 20;
console.log(number);
}
console.log(number);
PrintNumber();
자바 스크립트 엔진은 console.log(number);를 실행할 때와 PrintNumber() 함수를 실행할 때 식별자 이름이 같은 전역, 지역 number 변수중 어떤것을 참조해야 할것인지를 결정해야 합니다. 이를 식별자 결정이라고 합니다. 또한 자바스크립트는 스코프를 통해 어떤 변수를 참조해야할 것인지를 결정합니다. 따라서 스코프란 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고 할 수 있습니다.
코드 문맥과 환경
"코드가 어디서 실행되며 주변에 어떤 코드가 있는지"를 렉시컬 환경(lexical environment)이라고 부릅니다. 즉, 코드의 문맥은 렉시컬 환경으로 이뤄집니다. 이를 구현한 것이 "실행 컨텍스트(execution conext)"이며, 모든 코드는 실행 컨텍스트에서 평가되고 실행됩니다.
스코프의 종류
구분
설명
스코프
변수
전역
코드의 가장 바깥 영역
전역 스코프
전역 변수
지역
함수 몸체 내부
지역 스코프
지역 변수
코드는 전역과 지역으로 구분할 수 있습니다. 변수는 선언된 위치에 의해 자신이 유효한 스코프가 결정됩니다. 즉, 전역에 선언된 변수는 전역 스코프를 갖고, 함수 몸체 내부에 선언된 변수는 지역 스코프를 갖습니다.
스코프 체인
let x = "global x";
function outer() {
let x = "outer x";
function inner() {
let x = "inner x";
}
}
위 코드를 보면 함수 내부에 함수가 중첩되어 있는 구조입니다. outer 함수는 inner 함수의 상위 스코프이며, 전역은 outer 함수의 상위 스코프입니다. 이러한 계층적 구조를 통해서 자바스크립트 엔진은 현재 스코프에서 부터 참조할 변수를 상위 스코프로 이동하며 검색합니다. 이를 통해서 상위 스코프에서 선언한 변수를 하위 스코프에서 참조할 수 있습니다.
스코프 체인은 물리적인 실체로 존재합니다. 자바스크립트 엔진은 코드를 실행하기에 앞서 렉시컹 환경을 실제로 생성합니다. 변수 선언이 실행되면 변수 식별자가 렉시켠 환경에 키로 등록되고, 변수 할당이 일어나면 이 자료구조의 변수 식별자에 해당하는 값을 변경합니다.
렉시컬 환경(Lexical Environment)
스코프 체인은 실행 컨텍스트의 렉시컬 환경을 단방향으로 연결한 것입니다. 전역 렉시컬 환경은 코드가 로드되면 곧바로 생성되고 렉시컬 환경은 함수가 호출되면 곧바로 생성됩니다.
렉시컬 스코프(Lexical Scope)
렉시컬 스코프는 정적 스코프라고도 불립니다. 자바스크립트를 비롯한 대부분의 프로그래밍 언어는 렉시컬 스코프라고 합니다. 렉시컬 스코프는 함수를 어디서 호출했는지가 아니라 어디서 정의했는지에 따라 결정됩니다.
표준 빌트인 객체는 ECMAScript 사양에 정의된 객체를 말하며, 애플리케이션 전역의 공통 기능을 제공합니다. 표준 빌트인 객체는 ECMAScript 사양에 정의된 객체이므로 실행 환경과 관계없이 언제나 사용할 수 있습니다.
호스트 객체
호스트 객체는 ECMAScript 사양에 정의되어 있지 않지만 자바스크립트 실행 환경에 따라 추가로 제공하는 객체를 말합니다. 브라우저 환경에서는 DOM, BOM, Canvas, XMLHTTPRequest, fetch와 같은 클라이언트 사이드 Web API 호스트 객체로 제공하고, Node.js 환경에서는 Node.js 고유의 API 호스트 객체로 제공합니다.
사용자 정의 객체
사용자 정의 객체는 표준 빌트인 객체와 호스트 객체처럼 기본 제공되는 객체가 아닌 사용자가 직접 정의한 객체를 말합니다.
원시값과 래퍼 객체
const str = "devhun";
// 원시 타입이지만 프로퍼티와 메서드를 사용할 수 있다.
console.log(str.length);
console.log(str.toUpperCase());
위 코드에서 원시값은 객체가 아니기 때문에 프로퍼티나 메서드를 가질 수 없는데도 원시값인 문자열이 마치 객체처럼 동작합니다. 이는 문자열, 숫자, 불리언 값의 경우 자바스크립트 엔진이 일시적으로 객체로 변환해주기 때문입니다. 그리고 접근이 끝난 후 다시 원시값으로 되돌리는데 이러한 객체를 래퍼 객체(wrapper object)라고 합니다.
래퍼 객체 생성과정
// 1번
const str = "Hello";
// 2번
str.name = "devhun";
// 3번
console.log(str.name); // undefined 출력
// 4번
console.log(typeof str, str); // string Hello 출력
식별자 str은 문자열을 값으로 가지고 있습니다.
str은 암묵적으로 생성된 래퍼 객체를 가리킨다. 그리고 "Hello" 원시 값은 래퍼 객체의 [[StringData]] 내부 슬롯에 할당됩니다.
str에는 [[StringData]] 내부 슬롯에 할당된 원시값으로 할당되며, 래퍼 객체를 참조하는 식별자가 없기 때문에 가비지 컬렉션의 대상이 됩니다.
str은 새롭게 생성된 래퍼 객체를 가리키며, 새롭게 생성된 래퍼 객체는 name 프로퍼티를 가지고 있지 않습니다.
str은 다시 래퍼 객체를 가리키며 string을 출력하고 이후 다시 가리키는 값이 원시 값으로 바뀌면서 가비지 컬렉션의 대상이 됩니다.
전역 객체
전역 객체는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체입니다. 환경에 따라 전역 객체는 다르며 브라우저 환경에서는 window, Node.js 환경에서는 global 입니다.
전역 객체의 특징
전역 객체는 생성자가 제공되지 않으며 개발자가 의도적으로 생성할 수 없습니다.
전역 객체의 프로퍼티를 참조할 때 window 또는 global를 생략할 수 있습니다.
전역 객체는 Object, String, Number, Boolean, Function, Array RegExp Date, Math, Promise와 같은 표준 빌트인 객체를 프로퍼티로 가지고 있습니다.( 전역 객체가 최상위 객체라고 해서 프로토타입 상속 관계에서 최상위라는 의미가 아닙니다. 표준 빌트인 객체와 호스트 객체를 프로퍼티로 소유하고 있을 뿐입니다. )
자바스크립트 실행 환경에 따라 추가적으로 프로퍼티와 메서드를 갖습니다.
var 키워드로 선언한 전역 변수와 선언하지 않은 변수에 값을 할당한 암묵적 전역 그리고 전역 함수는 전역 객체의 프로퍼티가 됩니다.( let, const는 전역에 선언되어도 전역 객체의 프로퍼티가 되지 않습니다. )
여러 파일로 분리되어 있더라도 모든 파일은 동일한 하나의 전역 객체를 가리킵니다.
빌트인 전역 프로퍼티
빌트인 전역 프로퍼티는 전역 객체의 프로퍼티를 의미합니다. 주로 어플리케이션 전역에서 사용하는 값을 제공합니다. 빌트인 전역 프로퍼티로는 Infinity, NaN, undefined 등이 있습니다.
빌트인 전역 함수
빌트인 전역 함수는 애플리케이션 전역에서 호출할 수 있는 빌트인 함수로서 전역 객체의 메서드입니다. isFinite, isNaN, parseFloat, parseInt 등이 있습니다.
자바스크립트는 명령형, 함수형 그리고 프로토타입 기반의 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어입니다. 또한 자바스크립트는 원시 타입의 값을 제외한 모든 것이 객체입니다.
상속과 프로토타입
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
};
}
const circle1 = new Circle(1);
const circle2 = new Circle(2);
// return 값은 false
console.log(circle1.getArea === circle2.getArea);
Circle 생성자 내부에 메서드를 정의하여 인스턴스를 생성할 때마다 getArea() 메서드가 생성되어 할당됩니다. 이러한 불필요한 메모리 낭비를 해결하기 위해 상속을 통해서 하나의 메서드를 동일한 타입의 인스턴스가 공유하도록 하는 것이 바람직합니다.
프로토타입 기반 상속으로 메서드 중복 해결
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
};
}
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};
const circle1 = new Circle(1);
const circle2 = new Circle(2);
// return 값은 true
console.log(circle1.getArea === circle2.getArea);
Circle 생성자 함수가 생성한 모든 인스턴스는 상위(부모) 객체 역할을 하는 Circle.prototype의 모든 프로퍼티와 메서드를 상속받습니다. 이러한 특성을 이용해서 getArea 메서드를 Circle.prototype의 메서드로 할당하여 이후 생성된 모든 Circle 인스턴스가 Circle.prototype의 getArea 메서드를 공유하여 사용할 수 있습니다. 이를 통해 getArea 메서드가 중복되어 생성되는 것을 방지할 수 있습니다.
프로토타입 객체
모던 자바스크립트 Deep Dive, 이웅모
프로토타입 객체란 부모 객체의 역할을 하는 객체로서 자식 객체에게 자신의 프로퍼티 및 메서드를 제공합니다. 프로토타입을 상속받은 자식 객체는 프로토타입의 프로퍼티를 프로토타입 체인을 통해서 자신의 프로퍼티처럼 자유롭게 사용할 수 있습니다.
모든 객체는 하나의 프로토타입을 갖으며 모든 프로토타입은 생성자 함수와 연결되어 있습니다. 생성자 함수의 prototype, 프로토타입의 constructor 그리고 인스턴스의 __proto__로 서로 연결되어 있습니다.
__proto__는 접근자 프로퍼티다.
객체의 [[Prototype]] 내부 슬롯에 접근할 수 있는 방법은 __proto__ 접근자 프로퍼티를 사용하여 간접적으로 접근할 수 있습니다. __proto__는 접근자 프로퍼티로서 [[Get]], [[Set]] 프로퍼티 어트리뷰트를 가지며 이를 통해 [[Prototype]] 내부 슬롯의 값, 즉 프로토타입을 취득하거나 변경할 수 있습니다.
__proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티이며, Object.prototype을 프로토타입으로 가지는 모든 객체는 프로토타입 체인을 통해서 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있습니다.
모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여있습니다. 접근하려는 인스턴스의 프로퍼티나 메서드가 없을 때 __proto___ 접근자 프로퍼티가 가리키는 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색합니다.
__proto__ 보다는 getPrototypeOf, setPrototypeOf를 사용하자.
__proto__ 사용은 권장되지 않는다. const obj = Object.create(null);에서 obj는 Object 인스턴스로서 프로토타입이 존재하지 않으며 obj.__proto__ 가 undefined입니다. 이와 같이 프로토타입이 없는 인스턴스도 있기 때문에 __proto__를 사용하기 보다는 getPrototypeOf, setPrototypeOf를 사용하는 것이 바람직합니다.
함수 객체의 prototype 프로퍼티
// true return
(function () {}.hasOwnProperty("prototype"));
// false return
({}.hasOwnProperty("prototype"));
function Person(name) {
this.name = name;
}
const me = new Person("devhun");
// true를 return
console.log(Person.prototype === me.__proto__);
constructor 함수의 prototype 프로퍼티와 constructor로 생성한 인스턴스가 들고 있는 __proto__는 동일한 프로토타입을 가리키고 있습니다.
프로토타입의 constructor 프로퍼티와 생성자 함수
모든 프로토타입은 constructor 프로퍼티를 갖으며 이 constructor 프로퍼티는 자신을 prototype 프로퍼티로 참조하고 있는 생성자 함수를 가리킵니다.
프로토타입이 constructor 프로퍼티를 가지고 있기 때문에 해당 생성자로 생성한 인스턴스는 프로토타입 체인에 의해서 constructor 프로퍼티에 접근할 수 있습니다.
리터럴 표기법으로 생성된 객체와 생성자 함수와 프로토타입
function foo() {}
// return true
console.log(foo.constructor === Function);
리터럴 표기법으로 생성된 객체의 생성자 함수와 프로토타입은 자바스크립트 엔진 내부적으로 추상 연산인 OrdinaryObejctCreate를 호출하고 인스턴스를 셋팅합니다. 이를 통해 리터럴 표기법으로 생성된 객체도 프로토타입이 셋팅되며, 프로토타입의 constructor도 생성자 함수와 연결되어 있습니다.
프로토타입 생성 시점
프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됩니다. 또한 생성자 함수와 프로토타입은 단독으로 존재할 수 없고 언제나 쌍으로 존재합니다.
사용자 정의 생성자 함수와 프로토타입
생성자 함수로서 호출할 수 있는 함수, 즉 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입 객체도 더불어 생성됩니다.
사용자 정의 생성자가 평가되어 생성된 프로토타입은 constructor 프로퍼티를 갖으며, 프로토타입 객체의 프로토타입은 Object.prototype입니다.
빌트인 생성자 함수와 프로토타입 생성 시점
Object, String, Number, Function, Array 등과 같은 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성됩니다. 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성됩니다.
프로토타입 체인
function Player(name) {
this.name = name;
}
Player.prototype.sayHello = function () {
console.log(`Hello ${this.name}`);
};
const me = new Player("devhun");
위와 같은 상황에서 Player.prototype의 프로토타입은 Object.prototype입니다. Object.prototype은 언제나 프로토타입 체인의 최상위에 위치하여 체인의 종점이라 부릅니다.
Player 생성자와 Player.prototype은 prototype과 constructor 프로퍼티로 서로 연결되어 있으며 Player.prototype의 prototype은 Obejct.prototype이기 때문에 Player로 생성한 인스턴스는 Player.prototype 및 Object.prototype과 상속관계로 이루어져 있습니다.
오버라이딩과 프로퍼티 섀도잉
function Player(name) {
this.name = name;
}
// 프로토타입 프로퍼티
Player.prototype.sayHello = function () {
console.log("Hello");
};
const me = new Player("devhun");
// 프로퍼티 섀도잉
me.sayHello = function () {
console.log(`Hello ${this.name}`);
};
// Hello devhun 출력
me.sayHello();
위 코드를 보면 Player.prototype에 sayHello() 라는 메서드를 추가하였지만, Player 생성자를 통해 생성한 인스터스 sayHello 프로퍼티에 새로운 메서드를 할당하여 오버라이딩을 하였습니다. 이로 인해서 프로토타입의 sayHello() 메서드가 가려지는 현상을 프로퍼티 섀도잉이라 합니다.( 자바스크립트에서는 오버로딩은 지원하지 않습니다. )
프로토타입 메서드 삭제
// 인스턴스 프로퍼티 삭제
delete me.sayHello;
// 프로토타입 체인에 의해서 Hello 출력
me.sayHello();
delete me.sayHello;
// Hello가 그대로 출력
me.sayHello();
자식 객체에서는 프로토타입 프로퍼티를 변경 또는 삭제하는 것은 불가능합니다. 프로토타입의 프로퍼티를 삭제하기 위해서는 프로토타입에 직접 접근하여 삭제해야 합니다.
프로토타입 교체
프로토타입은 임의의 다른 객체로 변경할 수 있으며, 이러한 특징을 활용하여 객체 간의 상속 관계를 동적으로 변경할 수 있습니다. 프로토타입은 생성자 함수 또는 인스턴스에 의해 교체할 수 있습니다.
생성자 함수에 의한 프로토타입 교체
const Person = (function () {
function Person(name) {
this.name;
}
Person.prototype = {
// 생성자와 연결을 만듬
// constructor: Person,
sayHello() {
console.log(`Hello ${this.name}`);
},
};
return Person;
})();
const me = new Person("devhun");
// true
console.log(Person.prototype === me.__proto__);
// false
console.log(me.constructor === Person);
// true
console.log(me.constructor === Object);
위와 같이 생성자 함수에서 prototype을 리터럴 객체로 변경하면 이후 Person 생성자 함수로 생성된 모든 인스턴스의 프로토타입은 리터럴 객체로 셋팅됩니다. 하지만, 리터럴 객체는 constructor 프로퍼티가 없기 때문에 Person 생성자 함수는 prototype 프로퍼티를 통해서 객체 리터럴에 접근할 수 있지만, 프로토타입인 리터럴 객체는 constructor 프로퍼티가 없기 때문에 프로토타입 체인에 의해서 Object 생성자와 연결되어 있는 상태입니다. 이를 해결하기 위해서는 직접 constructor 프로퍼티로 연결해주는 작업이 필요합니다.
정적 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조 및 호출할 수 있는 프로퍼티/메서드를 말합니다.
정적 프로퍼티/메서드는 인스턴스로는 호출할 수 없습니다.
프로퍼티 존재 확인
in 연산자 & Object.prototype.hasOwnPrototype 메서드
const person = {
name: "devhun",
};
// true를 return
console.log("toString" in person);
// false를 return
console.log(person.hasOwnProperty("toString"));
in 연산자는 인스턴스의 프로퍼티가 존재하는지 확인하는데, 프로토타입 체인을 따라 확인하기 때문에 부모 프로토타입에 찾으려는 프로토타입이 있을 경우 true를 return 합니다. 그렇기 때문에 경우에 따라서 in 연산자 보다는 Object.prototype.hasOwnPrototype 메서드를 사용해야 합니다.
프로퍼티 열거
for...in 문
const person = {
name: "devhun",
age: 28,
};
for (const key in person) {
console.log(`${key} : ${person[key]}`);
}
for...in 문은 인스턴스가 가지고 있는 enumerable가 true인 프로퍼티 어트리뷰트를 가진 프로퍼티를 대상으로 열거합니다. 하지만, 순회 대상 인스턴스가 상속받은 프로토타입의 프로퍼티까지 포함합니다.
인스턴스의 고유 프로퍼티만 열거하고 싶을 경우에는 Object.keys/values/entries 메서드를 사용하면 됩니다.
// 1. 런타임에 함수 리터럴이 평가되어 함수 객체가 생성되고 변수에 할당할 수 있다.
const increment = function (num) {
return ++num;
};
// 2. 변수나 자료구조(객체, 배열 등)에 저장할 수 있다.
const numObj ={
increment;
}
// 3. 함수의 매개변수로 전달할 수 있다.
// 4. 함수의 반환값으로 사용할 수 있다.
function makeCounter(increment){
return increment;
}
makeCounter(numObj.increment);
함수는 위 코드에 나온 모든 조건에 해당되기 때문에 일급 객체입니다. 일반 객체와 다른점은 호출이 가능하고 함수 객체만의 고유의 프로퍼티를 소유합니다.
함수 객체의 프로퍼티
함수 객체는 length, name, arguments, caller, prototype의 프로퍼티를 가지고 있습니다.
arguments 프로퍼티
함수 객체의 arguments 프로퍼티는 함수 호출 시 전달된 인수들의 정보를 담고 있는 순회 가능한 유사 배열 객체입니다. 함수 내부에서 지역 변수처럼 사용 가능합니다.
함수를 호출할 때 매개변수의 정보는 arguments 객체에 저장되는데, 초과된 매개변수 또한 arguments 객체에 저장됩니다.
arguments 프로퍼티는 가변 인자 함수를 구현할 때 유용합니다. arguments 객체는 배열 형태로 인자 정보를 담고 있지만, 실제 배열이 아닌 유사 배열 객체입니다.( 유사 배열 객체란 배열은 아니지만, length 프로퍼티와 같이 배열처럼 사용할 수 있는 프로퍼티를 가지는 객체로 for 문으로 순회할 수 있는 객체를 말합니다. )
caller 프로퍼티
function foo(func) {
return func();
}
function bar() {
return `caller : ${bar.caller}`;
}
console.log(foo(bar)); // caller : function foo(func){...}
console.log(bar()); // caller : null
caller 프로퍼티는 ECMAScript 사양에 포함되지 않은 비표준 프로퍼티입니다.
caller는 자신을 호출한 함수를 가리킵니다.
length 프로퍼티
함수 객체의 length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킵니다.
name 프로퍼티
함수 객체의 name 프로퍼티는 함수 이름을 나타내며, name 프로퍼티는 ES6 이전까지는 비표준이었다가 ES6에서 정식 표준이 되었습니다.
함수 이름을 가리키는 프로퍼티입니다.
prototype
prototype 프로퍼티는 생성자 함수로 호출할 수 있는 함수 객체인 constructor만이 소유하는 프로퍼티입니다. prototype은 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킵니다.