🔥 학습목표
- 클로저에 대한 기초 개념을 학습한다.
- ES6 신규 문법 중 스프레드 문법과 rest 문법에 대해 공부하고, 앞으로 스프레드 문법으로 대체할 수 있는 건 최대한 대체하면서 사용에 익숙해진다.
- 자바스크립트 DeepDive 24장을 학습한다.
- 과제 중 헷갈렸던 부분을 다시 정리하고 복습한다.
원시 자료형을 변수에 할당하면 메모리 공간에 값 자체가 저장 된다.
원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달 된다. 이를 값에 의한 전달이라 한다.
원시 자료형은 변경 불가능한 값이다. 이때 변경 불가능하다는 것은 변수가 아니라 값에 대한 진술이다. (변수는 언제든지 재할당 할 수 있다.)
원시 값을 할당한 변수에 새로운 원시 값을 재할당 하면 새로운 메모리 공간을 확보하고 재할당한 원시 값을 저장한 후, 변수는 새롭게 재할당한 원시 값의 메모리 공간 주소를 가리킨다.
이는 변수에 할당 된 원시 값이 변경 불가능한 값이기 때문이다. 값을 직접 변경할 수 없다.
이러한 특성을 불변성이라 한다.
Ex. 유사 배열 객체인 문자열 let str = 'string'
이 존재할 때, str[0] = 'S'
와 같이 변경하지 못하는 이유도 문자열이 원시 값이기 때문이다.
참조 자료형을 변수에 할당하면 메모리 공간에 주솟값이 저장 된다. 생성 된 객체가 저장된 메모리 공간의 주소 그 자체다.
참조 값을 갖는 변수를 다른 변수에 할당하면 주솟값이 복사되어 전달 된다.
즉 여러 개의 식별자가 하나의 객체를 공유할 수 있다.
let copy = arr.slice();
: 새롭게 생성 된 배열은 원본 배열과 참조하는 주소가 다르다. 복사한 배열에 요소를 추가해도 원본 배열엔 추가되지 않는다.
let copy = [...arr];
새로운 배열 안에 원본 배열을 펼쳐서 전달한다. 마찬가지로 같은 요소를 가지고 있지만 각각 다르주소를 참조한다.
let copy = Object.assign({}, obj);
새로운 객체는 원본 객체와 다른 주소값을 가진다.
let copy = {...obj};
얕은 복사 까지만 이루어진다. 즉 한 단계까지만 복사하고, 객체에 중첩되어 있는 객체는 같은 주솟값을 참조하게 된다.
const _ = require('lodash');
const obj2 = _.cloneDeep(obj);
스코프란 식별자가 유효한 범위를 말한다. 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고도 할 수 있다.
지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다.
변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.
블록 레벨 스코프: 함수 뿐만 아니라 모든 코드블록(if, for, while, try/catch 등)을 포함하여 지역 스코프를 만든다.
함수 레벨 스코프: 함수의 코드 블록(함수 몸체)만을 지역 스코프로 인정.
🔴 화살표 함수는 블록 스코프로 취급한다.
동적 스코프(dynamic scope): 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정한다.
정적 스코프(static scope) 또는 렉시컬 스코프(lexical scope): 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정한다. 함수 정의가 평가되는 시점에 상위 스코프가 정적으로 결정. 자바스크립트를 비롯한 대부분의 프로그래밍 언어는 렉시컬 스코프다.
const x = 1;
function foo() {
const x = 10;
// 함수 호출 위치와 상위 스코프는 아무 관계 없다.
bar();
}
// 함수 bar의 상위 스코프는 전역 객체 window 메서드다.
function bar() {
console.log(x);
}
아래에서 중첩 함수 innerFunc는 자신의 상위 스코프인 외부함수 outerFunc의 x 변수에 접근할 수 있다.
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
console.log(x); // 10
}
innerFunc();
}
outerFunc();
🔵 함수는 자신의 내부 슬롯 [[Enviroment]]
에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
기존 함수 내부에서 새로운 함수를 리턴하면 클로저로서 활용할 수 있다.
아래 코드에서 outer 함수의 실행이 종료되면 outer 함수의 실행 컨텍스트는 스택에서 제거된다. outer 함수의 지역변수 x 또한 생명주기를 마감한다.
그러나 inner 함수 호출 시 x 변수에 계속해서 접근한다.
🔴 외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저(closure)라고 부른다.
const x = 1;
function outer(x) {
let y = 2
const inner = function (z) {
return console.log(x * y * z);
}
return inner;
}
const multi = outer(10);
multi(2); // 40
위 코드에서는 inner
가 클로저로서 outer의 매개변수와 지역변수인 x, y에 접근할 수 있다. 게다가 inner 함수 호출 시 계속 재사용 할 수 있다.
🔴 왜 그럴까?
outer 함수의 렉시컬 환경이[[Enviroment]]
에 계속해서 "보존"되기 때문이다.
outer 함수가 실행 컨텍스트 스택에서 제거되었더라도 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.
outer 함수의 렉시컬 환경은 inner 함수의 [[Enviroment]] 내부 슬롯에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다.
여러 전달인자를 가진 함수를 연속적으로 리턴하는 함수로 변경하는 행위.
전체 프로세스의 일정 부분까지만 실행하는 경우 유용하다.
예를들어, a + b + c 를 해야하는 경우
// 이렇게 한꺼번에 하는 게 아닌
function sum(a, b, c) {
return a+b+c;
}
// 단계를 나누어 계산한다. 어쩌다 중간까지만 호출해도 되도록.
function currySum(a) {
return function(b) {
return function(c) {
return a+b+c;
🔵 클로저의 장점
function makeCounter(aux) {
// 외부에서 counter를 바꾸지 못하게 은닉한다.
let counter = 0;
return function() {
counter = aux(counter);
return counter;
};
}
// 보조 함수
function increase(n) {
return ++n;
}
function decrease(n) {
return --n;
}
// 함수로 함수를 생성한다. 보조 함수를 인수로 전달 받아 함수를 반환한다.
const increaser = makeCounter(increase);
console.log(increaser()); // 1
const decreaser = makeCounter(decrease);
console.log(decreaser()); // 0
...arr
배열을 풀어서 인자로 전달하거나, 각각의 요소로 넣을 때 사용한다.
let arr1 = [0,1,2];
let arr2 = [3,4,5];
arr1 = [...arr1, ...arr2] // [0, 1, 2, 3, 4, 5]
let obj1 = {foo: 'bar', x: 42}
let obj2 = {foo: 'baz', y: 20}
let mergeObj = {...obj1, ...obj2};
console.log(mergeObj); // { foo: 'baz', x: 42, y:20}
파라미터를 배열의 형태로 받아서 사용할 수 있다. 파라미터 개수가 가변적일 때 유용하다.
생김새는 spread와 비슷하나 역할이 다르다!
rest는 객체, 배열, 함수의 파라미터에서 사용이 가능하다.
others 안에 name을 제외한 값이 들어있다...!!
const personInfo = {
name: '김다함',
age: 22,
major: 'Computer Engineering',
position: 'SEB_FE_44'
}
const {name, ...others} = personInfo;
console.log(name); // '김다함'
console.log(others); // {age: 22, major: 'Computer Engineering, position: 'SEB_FE_44'};
const {age, ...rest} = personInfo;
console.log(rest); // {name:'김다함', major: 'Computer Engineering, position: 'SEB_FE_44'};
const arr = [1, 2, 3, 4, 5];
const [one, ...rest] = numbers;
console.log(one); // 1
console.log(rest); // [2, 3, 4, 5]
만약 파라미터의 개수가 몇 개가 될지 모르는 상황이라면...
rest 파라미터를 사용하면 파라미터를 배열의 형태로 받아들이기 때문에 2개 수를 더하든 5개 수를 더하든 함수를 작동하는 데 문제가 생기지 않는다.
function sum(...rest) {
return rest.reduce((acc, current) => acc+current, 0);
}
const result = sum(1, 2, 3, 4, 5); // 21
spread 문법을 이용하여 값을 해체한 후, 개별 값을 변수에 새로 할당하는 과정을 말한다.
위에서 보여준 배열, 객체 분해를 즉 구조 분해 할당이라고 한다...
const array = [1, 2, 3, 4, 5];
const [first, second] = array;
console.log(first); // 1;
console.log(second); // 2;
const arr = [1, 2, 3, 4, 5];
const [one, ...rest] = numbers;
console.log(one); // 1
console.log(rest); // [2, 3, 4, 5]
🔴 화살표 함수를 이용해 클로저 표현
const adder = x => {
return y => {
return x + y
}
}
let result = adder(10)(20);
console.log(result); // 30
// 또는 아래와 같이 축약 가능
const adder = x => y => {
return x + y
}
spread와 rest 문법에 대해 잘 모르고 사용하지 않았었는데 이번에 똑바로 배우게 되었다. 다만 아직 개인적인 코딩 중에 제대로 활용한 적은 없어서... 그때그때 번뜩 잘 떠오를지 확신이 들진 않는다. 프로그래머스 문제 같은 걸 풀다보면 사용하게 되겠지?
클로저를 이해하고 나니 무척 재밌었다. 오늘 배운 것 중에 제일 개인적으로 활용해보고 싶은 부분이 클로저를 활용한 정보 은닉이다. 매번 ~이렇게 저렇게 하는 편이 더 좋다~ 라는 걸 보고서야 아차 수정하게 되는데, 앞으로는 내 스스로 더욱 안전하고 좋은 코드를 작성하는 법에 능숙해지면 좋겠다.
스코프 관련해서는... 멍하니 코드를 흐름에 따라 작성하면서 생각하면 스코프 에러는 잘 안 나는데, 이번 koans 과제처럼 곰곰히 과정을 추리하며 근거를 따지려들면 갑자기 머리가 복잡해진다. 이것도 내가 다 알고있다고 착각한 탓이겠지? 평소에도 꼼꼼하게 생각하면서 코딩하도록 계속 의식해야겠다.