ES5까지 변수를 선언할 수 있는 유일한 방법은 var 키워드를 사용하는 것이었다. var 키워드로 선언된 변수는 아래와 같은 특징이 있다. 이는 다른 언어와는 다른 특징으로 주의를 기울이지 않으면 심각한 문제를 일으킨다.
- 함수레벨 스코프
- 함수의 코드 블록만을 스코프로 인정한다. 따라서 전역 함수 외부에서 생성한 변수는 모두 전역변수이다. 이는 전역변수를 남발할 가능성을 높인다.
- for 문의 변수 선언문에서 선언한 변수를 for 문의 코드 블록 외부에서 참조할 수 있다.
- var 키워드 생략 허용 : 암묵적 전역 변수를 양산할 가능성이 크다
- 변수 중복 선언 허용 : 의도하지 않은 변수값의 변경이 일어날 가능성이 크다
- 변수 호이스팅 : 변수를 선언하기 이전에 참조할 수 있다.
대부분의 문제는 전역 변수로 인해 발생하기에 사용이 편리하다는 장점이 있지만 불가피한 상황을 제외하곤 사용을 억제하는것이 좋다. 전역 변수는 스코프가 넓기에 어디서 어떻게 사용될 것인지 파악하기힘들다. (변수의 스코프는 좁을수록 좋다)
ES6는 이러한 var 키워드의 단점을 보완하기 위해 let과 const 키워드를 도입하였다.
let, const
대부분의 프로그래밍 언어는 블록레벨 스코프를 따르지만 JS는 함수레벨 스코프를 따른다.
- 함수레벨 스코프 : 함수 내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다. 즉, 함수 내부에서 선언한 변수는 지역변수이며 함수 외부에서 선언한 변수는 모두 전역 변수이다.
- 모든 코드블록(함수, if 문, for 문, while 문, try/catch 문 등) 내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드 블록 외부에서는 참조할 수 없다. 즉, 코드 블록 내부에서 선언한 변수는 지역변수이다.
let foo = 123;
{
let foo = 456;
let bar = 456;
}
console.log(foo); // 123
console.log(bar); // ReferenceError: bar is not defined
var은 변수 중복선언과 재할당이 가능하며
let은 변수 중복선언은 불가 하지만 재할당은 가능하다.
const는 변수 중복선언 및 재할당이 불가능하다.(변수 선언과 동시에 할당이 이루어져야 한다.)
하지만, 객체의 내용은 변경(프로퍼티의 추가/삭제/변경)은 가능하다.
var와 let, 그리고 const는 다음처럼 사용하는 것을 추천한다.
- ES6를 사용한다면 var 키워드는 사용하지 않는다.
- 재할당이 필요한 경우에 한정해 let 키워드를 사용한다. 이때 변수의 스코프는 최대한 좁게 만든다.
- 변경이 발생하지 않는(재할당이 필요 없는 상수) 원시 값과 객체에는 const 키워드를 사용한다. const 키워드는 재할당을 금지하므로 var, let 보다 안전하다.
호이스팅
JS는 ES6에서 도입된 let, const를 포함하여 모든 선언(var, let, const, function, class)을 호이스팅한다. 호이스팅이란 var 선언문이나 function 선언문 등을 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성을 말한다.
선언단계 → 초기화 단계 → 할당 단계
var 키워드로 선언된 변수는 선언 단계와 초기화 단계가 한번에 이루어진다. 스코프에 변수를 등록하고 메모리에 변수를 위한 공간을 확보한 후, undefined로 초기화 한다. 따라서 변수 선언문 이전에 변수에 접근하여도 스코프에 변수가 존재하기 때문에 에러가 발생하지 않는다. 다만 undefined를 반환한다. 이후 변수 할당문에 도달하면 비로소 값이 할당된다. 이러한 현상을 변수 호이스팅이라고 한다.
클로저
블록 레벨 스코프를 지원하는 let은 var보다 직관적이다.
// 3이 세번 출력된다
var funcs = [];
// 함수의 배열을 생성하는 for 루프의 i는 전역 변수다.
for (var i = 0; i < 3; i++) {
funcs.push(function () { console.log(i); });
}
// 배열에서 함수를 꺼내어 호출한다.
for (var j = 0; j < 3; j++) {
funcs[j]();
}
/*
0 1 2 가 출력된다.
자바스크립트의 함수 레벨 스코프로 인하여 for 루프의 초기화 식에 사용된 변수가
전역 스코프를 갖게 되어 발생하는 문제를 회피하기 위해 클로저를 활용한 방법이다.
*/
var funcs = [];
// 함수의 배열을 생성하는 for 루프의 i는 전역 변수다.
for (var i = 0; i < 3; i++) {
(function (index) { // index는 자유변수다.
funcs.push(function () { console.log(index); });
}(i));
}
// 배열에서 함수를 꺼내어 호출한다
for (var j = 0; j < 3; j++) {
funcs[j]();
}
템플릿 리터럴
ES6 에서의 새로운 문자열 표기법을 도입하였다.
템플릿 리터럴은 백틱(`)을 사용한다.
화살표 함수
function 키워드 대신 화살표(⇒)를 사용하여 보다 간략한 방법으로 함수를 선언할 수 있다.
// 매개변수 지정 방법
() => { ... } // 매개변수가 없을 경우
x => { ... } // 매개변수가 한 개인 경우, 소괄호를 생략할 수 있다.
(x, y) => { ... } // 매개변수가 여러 개인 경우, 소괄호를 생략할 수 없다.
// 함수 몸체 지정 방법
x => { return x * x } // single line block(아래와 동일한 구문이다)
x => x * x // 함수 몸체가 한 줄의 구문이라면 중괄호를 생략할 수 있으며 암묵적으로 리턴된다.
() => {return {a:1};}
() => ({a:1}) // 두 가지 표현 방법은 동일하며, 객체 반환시 소괄호를 사용한다.
() => { // multi line block;
const x = 10;
return x * x;
};
화살표 함수는 익명 함수로만 사용할 수 있다.
// ES5
var pow = function(x) {return x * x;};
console.log(pow(10));
// ES6
const pow = x => x * x;
console.log(pow(10));
콜백 함수로 사용할 수 있다.
// ES5
var arr = [1, 2, 3];
var pow = arr.map(function(x) {
return x * x;
});
// ES6
const arr = [1, 2, 3];
const pow = arr.map(x => x*x);
console.log(pow);
this : function 키워드로 생성한 일반 함수와 화살표 함수의 가장 큰 차이점은 this이다.
/* 일반 함수
일반 함수는 함수를 선언할 때 this에 바인딩할 객체가 정적으로 결정되는 것이 아니고,
함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정된다.
*/
function Prefixer(prefix) {
this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
// (A)
return arr.map(function (x) {
return this.prefix + ' ' + x; // (B)
});
};
var pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));
/*
(A) 지점에서의 this는 생성자 함수 Prefixer가 생성한 객체(생성자 함수의 인스턴스 pre) 이다.
(B) 지점에서의 this는 전역 객체 window를 가리킨다.
생성자 함수와 객체의 메소드를 제외한 모든함수(내부함수, 콜백함수 포함)내부의 this는 전역 객체를 가리킨다.
*/
//콜백 함수 내부의 this가 메소드를 호출한 객체(생성자 함수의 인스턴스)를 가리키는 3가지 방법
// Solution 1: that = this
function Prefixer(prefix) {
this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
var that = this; // this: Prefixer 생성자 함수의 인스턴스
return arr.map(function (x) {
return that.prefix + ' ' + x;
});
};
var pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));
// Solution 2: map(func, this)
function Prefixer(prefix) {
this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
return arr.map(function (x) {
return this.prefix + ' ' + x;
}, this); // this: Prefixer 생성자 함수의 인스턴스
};
var pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));
// Solution 3: bind(this)
function Prefixer(prefix) {
this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
return arr.map(function (x) {
return this.prefix + ' ' + x;
}.bind(this)); // this: Prefixer 생성자 함수의 인스턴스
};
var pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));
화살표 함수의 this
화살표 함수는 함수를 선언할 때 this에 바인딩할 객체가 정적으로 결정된다. 동적으로 결정되는 일반 함수와는 달리 화살표 함수의 this 언제나 상위 스코프의 this를 가리킨다. 이를 Lexical this 라 한다. 화살표 함수는 앞서 살펴본 Solution 3의 Syntactic sugar이다.
화살표 함수로 메소드를 정의할 때는 전역 객체 window를 가리키므로 사용이 바람직 하지 않다.
function Prefixer(prefix) {
this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
// this는 상위 스코프인 prefixArray 메소드 내의 this를 가리킨다.
return arr.map(x => `${this.prefix} ${x}`);
};
const pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));
/*
아래의 예시는 메소드로 정의한 화살표 함수 내부의 this는 메소드를 소유한 객체,
즉 메소드를 호출한 객체를 가리키지 않고 상위 컨택스트인 전역 객체 window를 가리킨다.
따라서 화살표 함수로 메소드를 정의하는 것은 바람직하지 않다.
*/
// Bad
const person = {
name: 'Lee',
sayHi: () => console.log(`Hi ${this.name}`)
};
person.sayHi(); // Hi undefined
// Good : 축약 메소드 표현
const person = {
name: 'Lee',
sayHi() { // === sayHi: function() {
console.log(`Hi ${this.name}`);
}
};
person.sayHi(); // Hi Lee
/*
addEventListener 함수의 콜백 함수를 화살표 함수로 정의하면
this가 상위 컨택스트인 전역 객체 window를 가리킨다.
*/
// Bad
var button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});
/*
따라서 addEventListener 함수의 콜백 함수 내에서 this를 사용하는 경우,
function 키워드로 정의한 일반 함수를 사용하여야 한다.
일반 함수로 정의된 addEventListener 함수의 콜백 함수 내부의 this는 이벤트 리스너에
바인딩된 요소(currentTarget)를 가리킨다.
*/
// Good
var button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});
Rest 파라미터
Rest 파라미터(Rest Parameter, 나머지 매개변수)는 매개변수 이름 앞에 세개의 점 ... 을 붙여서 정의한 매개변수를 의미한다. Rest 파라미터는 함수에 전달된 인수들의 목록을 배열로 전달받는다.
Rest 파라미터는 함수 정의 시 선언한 매개변수 개수를 나타내는 함수 객체의 length 프로퍼티에 영향을 주지 않는다.
ES6에서는 rest 파라미터 를 사용하여 가변 인자의 목록을 배열로 전달받을 수 있다. 이를 통해 유사 배열인 arguments 객체를 배열로 변환하는 번거로움을 피할 수 있다.
하지만 ES6의 화살표 함수 에는 함수 객체의 arguments 프로퍼티가 없다. 따라서 화살표 함수로 가변 인자 함수를 구현해야 할 때는 반드시 rest 파라미터를 사용해야 한다.
// Case 1
function foo(param, ...rest) {
console.log(param); // 1
console.log(rest); // [ 2, 3, 4, 5 ]
}
foo(1, 2, 3, 4, 5);
function bar(param1, param2, ...rest) {
console.log(param1); // 1
console.log(param2); // 2
console.log(rest); // [ 3, 4, 5 ]
}
bar(1, 2, 3, 4, 5);
// Case 2
function sum(...args) {
console.log(arguments); // Arguments(5) [1, 2, 3, 4, 5, callee: (...), Symbol(Symbol.iterator): ƒ]
console.log(Array.isArray(args)); // true
return args.reduce((pre, cur) => pre + cur);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
Spread 문법
Spread 문법(Spread Syntax, ... )는 대상을 개별 요소로 분리한다. Spread 문법의 대상은 이터러블 이어야 한다.
// Case 1
// ...[1, 2, 3]는 [1, 2, 3]을 개별 요소로 분리한다(→ 1, 2, 3)
console.log(...[1, 2, 3]) // 1, 2, 3
// 문자열은 이터러블이다.
console.log(...'Hello'); // H e l l o
// Map과 Set은 이터러블이다.
console.log(...new Map([['a', '1'], ['b', '2']])); // [ 'a', '1' ] [ 'b', '2' ]
console.log(...new Set([1, 2, 3])); // 1 2 3
// 이터러블이 아닌 일반 객체는 Spread 문법의 대상이 될 수 없다.
console.log(...{ a: 1, b: 2 });
// TypeError: Found non-callable @@iterator
// Case 2
function foo(x, y, z) {
console.log(x); // 1
console.log(y); // 2
console.log(z); // 3
}
// 배열을 foo 함수의 인자로 전달하려고 한다.
const arr = [1, 2, 3];
/* ...[1, 2, 3]는 [1, 2, 3]을 개별 요소로 분리한다(→ 1, 2, 3)
spread 문법에 의해 분리된 배열의 요소는 개별적인 인자로서 각각의 매개변수에 전달된다. */
foo(...arr);
// Case 3 array.concat
const arr = [1, 2, 3];
// ...arr은 [1, 2, 3]을 개별 요소로 분리한다
console.log([...arr, 4, 5, 6]); // [ 1, 2, 3, 4, 5, 6 ]
// Case 4 array.push
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// ...arr2는 [4, 5, 6]을 개별 요소로 분리한다
arr1.push(...arr2); // == arr1.push(4, 5, 6);
console.log(arr1); // [ 1, 2, 3, 4, 5, 6 ]
// Case 5 array.splice
const arr1 = [1, 2, 3, 6];
const arr2 = [4, 5];
// ...arr2는 [4, 5]을 개별 요소로 분리한다
arr1.splice(3, 0, ...arr2); // == arr1.splice(3, 0, 4, 5);
console.log(arr1); // [ 1, 2, 3, 4, 5, 6 ]
// Case 6 객체 병합 프로퍼티 변경/추가
// 객체의 병합
const merged = { ...{ x: 1, y: 2 }, ...{ y: 10, z: 3 } };
console.log(merged); // { x: 1, y: 10, z: 3 }
// 특정 프로퍼티 변경
const changed = { ...{ x: 1, y: 2 }, y: 100 };
// changed = { ...{ x: 1, y: 2 }, ...{ y: 100 } }
console.log(changed); // { x: 1, y: 100 }
// 프로퍼티 추가
const added = { ...{ x: 1, y: 2 }, z: 0 };
// added = { ...{ x: 1, y: 2 }, ...{ z: 0 } }
console.log(added); // { x: 1, y: 2, z: 0 }