스코프(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은 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킵니다.
Math.random() 함수는 0이상 1미만의 구간에서 부동소숫점 의사 난수를 반환합니다. 난수 생성 알고리즘에 사용되는 초기값은 사용자가 선택하거나 초기화할 수 없습니다.
두 값 사이의 정수 난수 생성하기
function getRandomInt(min, max) {
min = Math.ceil(min); // 소수값이 존재할 경우 올림
max = Math.floor(max); // 소수값이 존재할 경우 버림
return Math.floor(Math.random() * (max - min)) + min; //최댓값은 제외, 최솟값은 포함
}
최솟값 이상 최댓값 미만의 정수인 난수를 구하는 함수입니다.
최댓값을 포함하는 정수 난수 생성하기
function getRandomInt(min, max) {
min = Math.ceil(min); // 소수값이 존재할 경우 올림
max = Math.floor(max); // 소수값이 존재할 경우 버림
return Math.floor(Math.random() * (max - min + 1)) + min; //최댓값은 제외, 최솟값은 포함
}
최댓값을 포함하는 정수인 난수를 구하는 함수입니다. min = 1, max = 2일 경우 1이상 2이하의 정수가 return 됩니다.
Math.random() 사용시 주의할 점
MDN에서 Math.random()은 이와 같이 명시하고 있습니다. 'Math.random()은 암호학적으로 안전한 난수를 제공하지 않으므로, 보안과 관련된 어떤 것에도 이 함수를 사용해서는 안 된다.' 그렇기 때문에 crypto.getRandomValues()를 사용해야 안전한 난수를 얻을 수 있습니다.
내부 슬롯( internal slot )과 내부 메서드( internal method )란?
내부 슬롯과 내부 메서드는 자바스크립트 엔진 내부에서 사용되는 비공개 요소로서, ECMAScript 사양에서 사용하는 의사 프로퍼티( pseudo property )와 의사 메서드( pseudo method )입니다. ECMAScript 사양에 등장하는 이중 대괄호([[...]])로 감싼 이름들이 내부 슬롯과 내부 메서드입니다.
내부 슬롯과 내부 메서드 특징
내부 슬롯과 내부 메서드는 자바스크립트 엔진에서 실제로 동작하지만 개발자가 직접 접근할 수는 없습니다. 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공하고 있습니다. 모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는데 __proto__를 통해서 [[Prototype]]에 간접적으로 접근할 수 있습니다.
하지만, __proto__는 비표준으로써 권장되지 않기 때문에 Object.getPrototypeOf()와 Object.setPrototypeOf() 메서드를 사용하여 [[Prototype]] 내부 슬롯에 간접적으로 접근하는 것이 권장됩니다
자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의합니다. 프로퍼티 상태란 프로퍼티의 값( value ), 값의 갱신 가능 여부( writable ), 열거 가능 여부( enumerable ), 재정의 가능 여부( configurable )를 말합니다.
프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값인 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]이다. 따라서 프로퍼티 어트리뷰트에 직접 접근할 수 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 return 받은 프로퍼티 디스크립터 객체를 통해 간접적으로 확인할 수 있습니다.
데이터 프로퍼티와 접근자 프로퍼티
프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있습니다.
데이터 프로퍼티와 접근자 프로퍼티를 구분하는 방법은 Object.getOwnPropertyDescriptor 메서드를 사용하여 return된 객체의 프로퍼티를 통해 구분할 수 있습니다.
Object.defineProperty, Object.defineProperties를 통해서 프로퍼티와 프로퍼티 어트리뷰트를 정의할 수 있습니다.
객체 변경 방지
구분
메서드
프로퍼티 추가
프로퍼티 삭제
프로퍼티 값 읽기
프로퍼티 값 쓰기
프로퍼티 어트리뷰트 재정의
객체 확장 금지
Object.preventExtensions
X
O
O
O
O
객체 밀봉
Object.seal
X
X
O
O
X
객체 동결
Object.freeze
X
X
O
X
X
조건에 따라서 Object.preventExtensions, Object.seal, Object.freeze를 이용해서 객체의 변경을 방지할 수 있습니다. 다만 프로퍼티가 객체일 경우 해당 객체까지 적용되지 않기 때문에 해당 프로퍼티를 대상으로 추가적으로 호출해 주어야 합니다.
Test 함수 안의 x 변수는 현재 스코프와 전역 스코프에 선언되어있지 않기 때문에 자바스크립트 엔진에 의해서 전역 객체에 x 프로퍼티를 동적으로 생성하며, 이를 암묵적 전역( implicit global ) 이라 합니다. 암묵적 전역은 가독성과 유지보수성에 안 좋은 영향을 미치기 때문에 그렇기 때문에 반드시 var, let, const 키워드를 사용해야 한다.
객체 리터럴을 이용해서 객체를 생성하는 방법은 간편하지만, 여러개의 동일한 객체를 생성 하기에는 매우 비효율적입니다. 그래서 생성자 함수를 정의해 놓으면 이를 이용해서 동일한 구조를 가지는 여러 개의 객체를 간편하게 생성할 수 있습니다.
생성자 함수 정의 방법
function Circle(radius) {
// new 연산자를 사용하여 생성자를 호출했는지 확인
if (!new.target) {
return new Circle(radius);
}
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}
const circle = Circle();
this
생성자 함수가 new 연산자를 통해 호출될 경우 암묵적으로 빈 객체가 생성되며 this에 바인딩됩니다. 만약, new 연산자 없이 호출할 경우 브라우저라면 window, node.js라면 global 전역 객체를 가리키게 됩니다.
인스턴스 반환
new를 이용하여 생성자 함수를 호출하였을 경우 return문이 없더라도 암묵적으로 자바스크립트 엔진이 this 인스턴스를 return 합니다. 만약 원시 값 return일 경우 해당 return은 무시되지만 임의의 객체를 return 할 경우 this가 아닌 임의의 객체가 return되기 때문에 생성자 함수 내부에서는 return문은 작성하지 않는 편이 좋습니다.
파스칼
일반 함수와 생성자 함수를 구분 짓는 형식적 차이는 없기 때문에 첫 문자를 대문자로 기술하는 파스칼 코딩 컨벤션을 사용해서 구별할 수 있도록 노력합니다.
내부 메서드 [[Call]]과 [[Construct]]
함수는 객체이므로 일반 객체가 가지고 있는 모든 내부 슬롯 및 내부 메서드를 가지고 있습니다. 그리고 추가적으로 [[Call]], [[Construct]] 등의 내부 메서드를 가지고 있으며, 일반 함수로서 호출될 경우 내부 메서드 [[Call]]이 호출되고 new 연산자와 함께 생성자 함수로서 호출되면 내부 메서드 [[Construct]]가 호출됩니다.
constructor vs non-constructor
constructor : 함수 선언문, 함수 표현식, 클래스
non-constructor : 메서드(ES6 메서드 축약 표현), 화살표 함수
생성자 함수로서 호출할 수 있는 함수는 [[Constructor]] 내부 메서드를 가지고 있으며 이러한 함수 객체를 constructor라 부르며 [[Constructor]] 내부 메서드를 가지고 있지 않은 함수 객체를 non-constructor라고 부릅니다.
new 연산자와 new.target
new를 이용해서 constructor 생성자 함수를 호출할 경우 [[Call]] 내부 메서드가 호출되는 것이 아닌 [[Construct]] 내부 메서드가 호출됩니다.
new.target
ES6에서는 생성자 함수를 사용하는 쪽에서 new를 사용하지 않고 호출하는 실수를 방지하기 위해서 new.target을 이용해 내부에서 new 사용 여부를 판단할 수 있습니다. 만약 new를 사용하였다면 new.target은 함수 자신을 가리키며, new 연산자 없이 일반 함수로서 호출된 경우 undefined입니다.
new.target은 실수를 방지할 수도 있고, new를 사용하지 않고 String, Number, Boolean을 호출하였을 때 캐스팅 용도로 사용하는 것처럼 new 사용 여부에 따라서 다른 방식의 결과값을 return 하도록 로직을 구현할 수 있습니다.