JavaScript - Day 5
스코프
스코프란?
모든 식별자는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효범위가 결정된다.
즉, 스코프는 식별자가 유효한 범위를 말한다.
스코프의 종류
지역과 지역 스코프
- 지역이란 함수 몸체 내부를 말한다.
- 지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다.
전역과 전역 스코프
- 전역 변수는 어디서든지 참조할 수 있다.
전역 변수의 문제점
변수의 생명 주기
지역 변수의 생명 주기
지역 변수의 생명 주기는 함수의 생명 주기와 일치한다.
전역 변수의 생명 주기
var 키워드로 선언한 전역 변수의 생명 주기는 전역 객체의 생명 주기와 일치한다.
문제점
- 암묵적 결함 (Implicit Coupling)
- 코드의 가독성은 나빠지고, 의도치 않게 상태가 변경될 수 있는 위험성도 높아진다.
- 긴 생명 주기
- 스코프 체인 상에서 종점에 존재
- 전역 변수의 검색 속도가 가장 느리다.
- 네임스페이스 오염
전역 변수의 사용을 억제하는 방법
전역 변수를 반드시 사용해야 할 이유를 찾지 못한다면 지역 변수를 사용해야 한다.
즉시 실행 함수
(function () {
var foo = 10; // 즉시 실행 함수의 지역 변수
// ...
}());
console.log(foo); // ReferenceError
네임스페이스 객체
전역에 네임스페이스 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방식
var MYAPP = {}; // 전역 네임스페이스 객체
MYAPP.name = 'Lee';
consloe.log(MYAPP.name); // Lee
모듈 패턴
var Counter = (function () {
// private variable
var num = 0;
return {
increase() {
return ++num;
},
decrease() {
return --num;
};
}());
console.log(Counter.num); // undefined, private variable은 외부로 노출X
console.log(Counter.increase()); // 1
console.log(Counter.increase()); // 2
console.log(Counter.decrease()); // 1
ES6 모듈
ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공한다.
<script type="module" src="lib.mjs"></script>
<script type="module" src="app.mjs"></script>
let, const 키워드와 블록 레벨 스코프
var 키워드로 선언한 변수의 문제점
변수 중복 선언 허용
var x = 1;
var y = 1;
...
var x = 100;
var y = 1;
console.log(x); // 100
console.log(y); // 1
함수 레벨 스코프
- var 키워드로 선언한 변수는 코드 블록 내에서 선언해도 모두 전역 변수가 된다.
- for 문의 변수 선언문에서 var 키워드로 선언한 변수도 전역 변수가 된다.
변수 호이스팅
// 이 시점에는 변수 호이스팅에 의해 이미 foo 변수가 선언되었다.
console.log(foo); // undefined
foo = 123;
console.log(foo); // 123
// 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 수행된다.
var foo;
let 키워드
변수 중복 선언 금지
let bar = 123;
let bar = 456; // SyntaxError: Identifier 'bar' has already been declared.
블록 레벨 스코프
let foo = 1; // 전역 변수
{
let foo = 2; // 지역 변수
let bar = 3; // 지역 변수
}
console.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined
변수 호이스팅
let 키워드로 선언한 변수는 변수 호이스팅이 발생하지 않는 것처럼 동작한다.
let 키워드로 선언한 변수는 “선언 단계”와 “초기화 단계”가 분리되어 진행된다.
console.log(foo); // ReferenceError: foo is not defined
let foo;
전역 객체와 let
var 키워드로 선언한 전역 변수와 전역 함수, 그리고 선언하지 않은 변수에 값을 할당한 암묵적 전역은 전역 객체의 프로퍼티가 된다.
let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니다.
const 키워드
선언과 초기화
const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화해야 한다.
const foo = 1;
const foo; // SyntaxError: Missing initializer in const declaration
재할당 금지
const 키워드로 선언한 변수는 재할당이 금지된다.
const foo = 1;
foo = 2; // TypeError: Assignment to const variable.
const 키워드와 객체
const 키워드로 선언된 변수에 원시 값을 할당한 경우 값을 변경할 수 없지만, 객체를 할당한 경우 값을 변경할 수 있다.
var vs. let vs. const
변수 선언에는 기본적으로 const를 사용하고 let은 재할당이 필요한 경우에 한정해 사용하는 것이 좋다.
- ES6를 사용한다면 var 키워드는 사용하지 않는다.
- 재할당이 필요한 경우에 한정해 let 키워드를 사용한다. 이때 변수의 스코프는 최대한 좁게 만든다.
- 변경이 발생하지 않고 읽기 전용으로 사용하는 원시 값과 객체에는 const 키워드를 사용한다.
프로퍼티 어트리뷰트
프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체
자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.
- [[Value]]
- [[Writable]]
- [[Enumerable]]
- [[Configurable]]
const person = {
name: 'Lee'
};
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}
데이터 프로퍼티와 접근자 프로퍼티
- 데이터 프로퍼티
- 키와 값으로 구성된 일반적인 프로퍼티
- 접근자 프로퍼티
- 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티
데이터 프로퍼티
Value, Writable, Enumerable, Configurable
접근자 프로퍼티
Get, Set, Enumerable, Configurable
const person = {
firstName: 'Sung',
lastName: 'Huh',
get fullName(){
return `${this.firstName} ${this.lastName}`;
},
set fullName(name){
[this.firstName, this.lastName] = name.split(' ');
}
};
console.log(person.firstName + ' ' + person.lastName); // Sung Huh
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
console.log(person.fullName); // Heegun Lee
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor);
// {value: "Heegun", writable: true, enumerable: true, configurable: true}
descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor);
// {get: f, set: f, enumerable: true, configurable: true}
프로퍼티 정의
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('firstName', descriptor);
// firstName {value: "Ungmo", writable: true, enumerable: true, configurable: true}
descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log('lastName', descriptor);
// lastName {value: "Lee", writable: false, enumerable: false, configurable: false}
Object.defineProperty 메서드는 한번에 하나의 프로퍼티만 정의할 수 있다.
Object.defineProperties 메서드를 사용하면 여러개 의 프로퍼티를 한번에 정의할 수 있다.
const person = {};
Object.defineProperties(person, {
firstName: {
value: 'Ungmo',
writable: true,
enumerable: true,
configurable: true
},
lastName: {
value: 'Lee',
writable: true,
enumerable: true,
configurable: true
},
fullName: {
get() {
return `${this.firstName} ${this.lastName}`;
},
set() {
[this.firstName, this.lastName] = name.split(' ');
},
enumerable: true,
configurable: true
}
});
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
객체 변경 방지
객체 확장 금지 Object.preventExtensions
프로퍼티 추가가 금지된다.
const person = { name: 'Lee' };
// person 객체는 확장이 금지된 객체가 아니다.
console.log(Object.isExtensible(person)); // true
// person 객체의 확장을 금지하여 프로퍼티 추가를 금지한다.
Object.preventExtensions(person);
// person 객체는 확장이 금지된 객체다.
console.log(Object.isExtensible(person)); // false
person.age = 20; // 무시됨
console.log(person); // {name: "Lee"}
delete person.name;
console.log(person); // {}
// 프로퍼티 정의에 의한 프로퍼티 추가도 금지된다.
Object.defineProperty(person, 'age', { value: 20 });
// TypeError: Cannot define property age, object is not extensible
객체 밀봉 Object.seal
읽기와 쓰기만 가능하다.
const person = { name: 'Lee' };
// person 객체는 밀봉된 객체가 아니다.
console.log(Object.isSealed(person)); // false
// person 객체를 밀봉(seal)하여 프로퍼티 추가, 삭제, 재정의를 금지한다.
Object.seal(person);
// person 객체는 밀봉된 객체다.
console.log(Object.isSealed(person)); // true
person.age = 20; // 무시됨
console.log(person); // {name: "Lee"}
delete person.name; // 무시됨
console.log(person); // {name: "Lee"}
person.name = "Kim"; // 프로퍼티 값 갱신은 가능하다.
console.log(person); // {name: "Kim"}
// 프로퍼티 어트리뷰트 재정의가 금지된다.
Object.defineProperty(person, 'name', { configurable: true });
// TypeError: Cannot redefine property: name
객체 동결 Object.freeze
읽기만 가능하다.
const person = { name: 'Lee' };
// person 객체는 밀봉된 객체가 아니다.
console.log(Object.isFrozen(person)); // false
// person 객체를 밀봉(seal)하여 프로퍼티 추가, 삭제, 재정의를 금지한다.
Object.freeze(person);
// person 객체는 밀봉된 객체다.
console.log(Object.isFrozen(person)); // true
// 동결(freeze)된 객체는 writable과 configurable이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
// name: {value: "Lee", writable: false, enumerable: true, configurable: false},
person.age = 20; // 무시됨
console.log(person); // {name: "Lee"}
delete person.name; // 무시됨
console.log(person); // {name: "Lee"}
person.name = "Kim"; // 무시됨
console.log(person); // {name: "Lee"}
// 프로퍼티 어트리뷰트 재정의가 금지된다.
Object.defineProperty(person, 'name', { configurable: true });
// TypeError: Cannot redefine property: name
불변 객체
객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출해야 한다.
function deepFreeze(target){
if (target && typeof target === 'object' && !Object.isFrozen(target)){
Object.freeze(target);
Object.keys(target).forEach(key => deepFreeze(target[key]));
}
return target;
}
const person = {
name: 'Lee',
address: {city: 'Seoul'}
};
deepFreeze(person);
console.log(Object.isFrozen(person)); // true
console.log(Object.isFrozen(person.address)); // true
person.address.city = 'Busan';
console.log(person); // {name: "Lee", address: {city: "Seoul"}}