모던자바스크립트 15~17장

김동현 (마늘향기)·2023년 5월 12일

15장 let, const 키워드와 블록 레벨 스코프

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

  • 변수 중복 선언 허용 초기화문이 있는 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작하고, 초기화문이 없는 변수 선언문은 무시된다. 의도치 않게 먼저 선언된 변수 값이 변경되는 부작용 발생
  • 함수 레벨 스코프
  • 변수 호이스팅 선언과 동시에 undefined값으로 초기화 (선언 단계 + 초기화 단계)

let 키워드

  • 변수 중복 선언 금지
  • 블록 레벨 스코프
  • 변수 호이스팅 “선언 단계”와 “초기화 단계”가 분리되어 진행 일시적 사각지대(TDZ) 발생
  • 전역 객체와 let var 키워드로 선언한 전역 변수와 전역 함수, 암묵적 전역은 객체 window의 프로퍼티가 된다. 하지만 let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니다. let 전역 변수는 보이지 않는 개념적인 블록 내에 존재하게 된다.

const 키워드

  • 선언과 초기화 반드시 선언과 동시에 초기화
  • 재할당 금지
  • 상수 원시 값을 할당하면 변경할 수 없는 값이고 재할당 금지여서 상수로 표현된다.
  • const 키워드와 객체 const 키워드는 재할당을 금지하고 객체는 변경 가능한 값이어서 수정이 가능하다.

var vs. let vs. const

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

일단 const 키워드를 사용하자. 재할당이 필요하면 그때 let으로 바꾸자.

16장 프로퍼티 어트리뷰트

내부 슬롯과 내부 메서드

내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드이다. ECMAScript 사양에 등장하는 이중 대괄호([[…]])로 감싼 이름들이 내부 슬롯과 내부 메서드다.

const o = {};
o.[[Prototype]] //nope.안돼안돼 __proto__와 같이 간접적으로 접근할 수 있다.

o.__proto__ //-> Object.prototype

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

자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다. 프로퍼티의 상태란 프로퍼티의 값(value), 값의 갱신 가능 여부(writable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable)을 말한다.

프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값인 내부 슬롯 아래와 같다.

  • 내부 슬롯: [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]
  • Object.getOwnPropertyDescriptor 메서드로 확인
const person = {
	name: 'Lee'
};

//person의 name 프로퍼티 속사정이 궁금할때 🤩
console.log(Object.getOwnPropertyDescriptor(person, 'name')); 
// {value: 'Lee', writable: true, enumerable: true, configurable: true}

person.age = 20; //프로퍼티 동적 생성!

console.log(Object.getOwnPropertyDescriptors(person)); //s만 붙였을 뿐인데!
/* 
{
	name: {value: 'Lee', writable: true, enumerable: true, configurable: true},
	age: {value: 20, writable: true, enumerable: true, configurable: true}
}
*/

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

프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다.

  • 데이터 프로퍼티
    • 키와 값으로 구성된 일반적인 프로퍼티다. 지금까지 살펴본 모든 프로퍼티는 데이터 프로퍼티다.
  • 접근자 프로퍼티
    • 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티다.

데이터 프로퍼티

데이터 프로퍼티는 다음과 같은 프로퍼티 어트리뷰트를 갖는다. 프로퍼티 생성할 때 자동 정의된다.

프로퍼티 어트리뷰트프로퍼티 디스크립터 객체의 프로퍼티설명
[[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로 변경하는 것은 허용된다.

접근자 프로퍼티

접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.

프로퍼티 어트리뷰트프로퍼티 디스크립터 객체의 프로퍼티설명
[[Get]]get접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수다. 즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
[[Set]]set접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수다. 즉, 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다.
[[Enumerable]]enumerable데이터 프로퍼티의 것과 같다.
[[Configurable]]configurable데이터 프로퍼티의 것과 같다.

접근자 함수는 getter/setter 함수라고도 부른다. 접근자 프로퍼티는 getter와 setter 함수를 모두 정의할 수도 있고 하나만 정의할 수도 있다.

const person = {
	firstName: 'DongHyun',
	lastName: 'Kim',
	
	//getter 함수
	get fullName(){
		return `${this.firstName} ${this.lastName}`;
	}
	
	//setter 함수
	set fullName(name){
		[this.firstName, this.lastName]= name.split(' ');
	}
};

person.fullName = 'Heegun Lee';
console.log(person.fullName);

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

descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
//{get: 𝑓, set: 𝑓, enumerable: true, configurable: true} 

접근자 프로퍼티 fullName으로 프로퍼티 값에 접근하면 내부적으로 [[Get]] 내부 메서드가 호출되어 다음과 같이 동작한다.

  1. 프로퍼티 키가 유효한지 확인한다. 프로퍼티 키는 문자열 또는 심벌이어야 한다. 프로퍼티 키 ‘fullName’은 문자열이므로 유효한 프로퍼티 키다.
  2. 프로토타입 체인에서 프로퍼티를 검색한다. person 객체에 fullName 프로퍼티가 존재한다.
  3. 검색된 fullName 프로퍼티가 데이터 프로퍼티인지 접근자 프로퍼티인지 확인한다. fullName 프로퍼티는 접근자 프로퍼티다.
  4. 접근자 프로퍼티 fullName의 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수를 호출하여 그 결과를 반환한다. 프로퍼티 fullName의 프로퍼티 어트리뷰트 [[Get]]의 값은 Object.getOwnPropertyDescriptor 메서드가 반환하는 프로퍼티 디스크립터 객체의 get 프로퍼티 값과 같다.
🥪 **프로토타입** 프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체다. 프로토타입은 하위(자식) 객체에게 자신의 프로퍼티와 메서드를 상속한다. 프로토타입 객체의 프로퍼티나 메서드를 상속받은 하위 객체는 자신의 프로퍼티 또는 메서드인 것처럼 자유롭게 사용할 수 있다. 프로토타입 체인은 프로토타입이 단방향 링크드 리스트 형태로 연결되어 있는 상속 구조를 말한다.

Object.getOwnPropertyDescriptor 메서드로 접근자 프로퍼티와 데이터 프로퍼티를 구별한다. (찍어 보면 내부 슬롯이 다르니까..ㅎㅎ)

프로퍼티 정의

Object.defineProperty 메서드로 프로퍼티를 정의할 수 있다.

프로퍼티 디스크립터 객체의 프로퍼티대응하는 프로퍼티 어트리뷰트생략했을 때의 기본값
value[[Value]]undefined
get[[Get]]undefined
set[[Set]]undefined
writable[[Writable]]false
enumerable[[Enumerable]]false
configurable[[Configurable]]false
const person = {};

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

Object.defineProperty(person, 'lastName', {
	value: 'Lee'
});

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

descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log(descriptor);
//{value: "Lee", writable: false, enumerable: false, configurable: false}

console.log(Object.keys(person)); //["firstName"]
//enumerable: false인 lastName은 안보인다.ㅎ

person.lastName = 'Kim'; //writable: false라서 무시된다.

delete person.lastName; //configurable: false라서 무시된다.

descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log(descriptor);
//{value: "Lee", writable: false, enumerable: false, configurable: false}
//변한게 없죠? ㅎㅎ

Object.defineProperty(person, 'fullName', {
	get(){
		return `${this.firstName} ${this.lastName}`;
	},
	set(name){
		[this.firstName, this.lastName] = name.split(' ');
	},
	enumerable: true,
	configurable: true
});

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

Object.defineProperties 를 사용하면 여러개를 한번에 정의할 수 있다.

const person = {};

Object.defineProperties(person, {
	firstName: {
		value: 'DongHyun',
		writable: true,
		enumerable: true,
		configurable: true
	},
	fullName: {
		get() {
			return this.firstName;
		},
		set(name) {
			this.firstName = name;
		},
		enumerable: true,
		configurable: true
	}
});

객체 변경 방지

객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있다. 이런 변경을 방지하기 위해 아래 메서드들이 있다.

구분메서드확인 메서드프로퍼티 추가프로퍼티 삭제프로퍼티 값 읽기프로퍼티 값 쓰기프로퍼티 어트리뷰트 재정의
객체 확장 금지Object.preventExtensionsObject.isExtensibleXOOOO
객체 밀봉Object.sealObject.isSealedXXOOX
객체 동결Object.freezeObject.isFrozenXXOXX

객체 확장 금지

프로퍼티 추가 금지

객체 밀봉

읽기와 쓰기만 가능

객체 동결

읽기만 가능

불변 객체

객체의 중첩 객체까지 동결

17장 생성자 함수에 의한 객체 생성

Object 생성자 함수

new Object(); 이외에도 String, Number, Boolean, Function, Array, Date, RegExp, Promise 등의 빌트인 생성자 함수가 있다.

생성자 함수

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

직관적이고 좋지만 단 하나의 객체만 생성한다. 동일한 프로퍼티를 갖는 객체를 여러 개 생성해야 하는 경우 매번 같은 프로퍼티를 기술해야 하기 때문에 비효율 적이다.

객체는 프로퍼티를 통해 고유의 상태를 표현한다. 그리고 메서드를 통해 상태 데이터인 프로퍼티를 참조하고 조작하는 동작을 표현한다.

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

생성자 함수에 의한 객체 생성 방식은 마치 객체(인스턴스)를 생성하기 위한 템플릿(클래스)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성할 수 있다.

function Circle(radius){
	this.radius = radius;
	this.getDiameter = function() {
		return 2 * this.radius;
	};
}
const circle1 = new Circle(5);
const circle2 = new Circle(10);
🥪 **this** this는 객체 자신의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수다. this가 가리키는 값, 즉 this 바인딩은 함수 호출 방식에 따라 동적으로 결정된다.
함수 호출 방식this가 가리키는 값(this 바인딩)
일반 함수로서 호출전역 객체
메서드로서 호출메서드를 호출한 객체
생성자 함수로서 호출생성자 함수가 생성할 인스턴스

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

생성자 함수의 역할은 인스턴스를 생성하는 것과 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기값 할당)하는 것이다.

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

    암묵적으로 빈 객체가 생성되고 this에 바인딩된다.

  2. 인스턴스 초기화

    생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다.

  3. 인스턴스 반환

    this가 반환된다. 다른 객체를 명시적으로 반환하면 this가 반환되지 못한다. 원시 값을 반환하면 무시된다.

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

일반 객체는 호출할 수 없지만 함수는 호출할 수 있다. 따라서 함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드는 물론, 함수로서 동작하기 위해 함수 객체만을 위한 [[Environment]], [[FormalParameters]] 등의 내부 슬롯과 [[Call]], [[Construct]] 같은 내부 메서드를 추가로 지니고 있다.

일반 함수로서 호출되면 [[Call]]이 호출되고 생성자 함수로서 호출되면 [[Construct]]가 호출된다.

함수 객체는 callable이면서 constructor이거나 callable이면서 non-constructor다. 즉, 모든 함수 객체는 호출할 수 있지만 모든 함수 객체를 생성자 함수로서 호출할 수 있는 것은 아니다.

constructor와 non-constructor의 구분

  • constructor: 함수 선언문, 함수 표현식, 클래스
  • non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수

new 연산자

new로 함수 호출하면 constructor가 호출된다.

new.target

new 연산자와 함께 생성자 함수로서 호출되면 함수 내부의 new.target은 함수 자신을 가리킨다. new 연산자 없이 일반 함수로서 호출된 함수 내부의 new.target은 undefined다.

profile
안녕하세요, 김동현입니다. 공부한 내용을 기록하고자 블로그를 생성하였습니다. 감사합니다.

0개의 댓글