목차
- 재귀와 스택
- 나머지 매개변수와 전개 문법
- 변수의 유효범위와 클로저
- 오래된 'var'
- 전역 객체
- 객체로서의 함수와 기명 함수 표현식
- new Function 문법
- setTimeout과 setInterval을 이용한 호출 스케줄링
- call/aply와 데코레이터, 포워딩
- 함수 바인딩
- 화살표 함수 다시 살펴보기
재귀와 스택
문제 해결을 하다 보면 함수에서 다른 함수를 호출해야 할 때가 있다. 이때 함수가 자기 자신을 호출할 수도 있는데,
이를 재귀라고 부른다.
두 가지 사고방식
가장 처음 하는 호출을 포함한 중첩 호출의 최대 개수는 재귀 깊이(recursion depth)라고 한다.
pow(x, n)의 재귀 깊이는 n이다.
자바스크립트 엔진은 최대 재귀 깊이를 제한한다.
만개 정도까진 확실히 허용하고, 엔진에 따라 이보다 더 많은 깊이를 허용하는 경우도 있다.
실행 컨텍스트와 스택
실행 중인 함수의 실행 절차에 대한 정보는 해당 함수의 실행 컨텍스트에 저장된다.
함수 호출 1회당 정확히 하나의 실행 컨텍스트가 생성된다.
함수 내부에 중첩 호출이 있을 때는 아래와 같은 절차가 수행된다.
- 현재 함수의 실행이 일시 중지된다.
- 중지된 함수와 연관된 실행 컨텍스트는 실행 컨텍스트 스택(execution context stack)이라는 특별한 자료 구조에
저장된다. - 중첩 호출이 실행된다.
- 중첩 호출 실행이 끝난 이후 실행 컨텍스트 스택에서 일시 중단한 함수의 실행 컨텍스트를 꺼내오고, 중단한 함수의
실행을 다시 이어간다.
재귀 깊이는 스택에 들어가는 실행 컨텍스트의 수의 최댓값과 같다.
실행 컨텍스트는 메모리를 차지하므로 재귀를 사용할 땐 메모리 요구사항에 유의해야 한다.
pow(x, n)에서 n을 늘리면 n이 줄어들 때마다 만들어지는 n개의 실행 컨텍스트가 저장될 메모리 공간이 필요하다.
한편, 반복문을 사용하면 메모리가 절약된다.
재귀적 순회
let company = {
sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600}],
development: {
sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800}],
internals: [{name: 'Jack', salary: 1300}]
}
};
// 급여 합계를 구해주는 함수
function sumSalaries(department) {
if(Array.isArray(department)) { // 첫 번째 경우
// 배열의 요소를 합함
return department.reduce((prev, current) => prev + current.salary, 0);
}
else {
let sum = 0;
for(let subdep of Object.values(department)) {
sum += sumSalaries(subdep); // 재귀 호출로 각 하위 부서 임직원의 급여 총합을 구함
}
return sum;
}
}
alert(sumSalaries(company)); // 7700
- arr.reduce는 배열의 합을 계산해준다.
- Object.values는 프로퍼티의 값이 담긴 배열을 반환한다.
재귀적 구조
HTML과 XML도 재귀적 자료 구조 형태를 띈다.
HTML 문서에서 HTML 태그는 아래와 같은 항목으로 구성되기 때문이다.
- 일반 텍스트
- HTML - 주석
- 이 외의 HTML 태그(이 아래에 일반 텍스트, HTML-주석, 다른 HTML 태그가 올 수 있다.)
연결 리스트
배열은 요소 '삭제'와 '삽입'에 들어가는 비용이 많이 든다는 문제가 있다.
arr.unshift(obj) 연산을 수행하려면 새로운 obj를 위한 공간을 만들기 위해 모든 요소의 번호를 다시 매겨야 한다.
배열이 커지면 연산 수행시간이 더 걸리게 된다.
요소 전체의 번호를 다시 매기지 않아도 되는 조작은 배열 끝에 하는 연산인 pop/push 이다.
빠르게 삽입 혹은 삭제를 해야 할 때는 배열 대신 연결 리스트라 불리는 자료 구조를 사용할 수 있다.
연결 리스트의 요소는 객체와 아래 프로퍼티들을 조합해 정의할 수 있다.
- value
- next: 다음 연결리스트 요소를 참조하는 프로퍼티. 다음 요소가 없을 땐 null이 된다.
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
아래와 같은 코드이다.
let list = {value: 1};
list.next = {value: 2};
list.next.next = {value: 3};
list.next.next.next = {value: 4};
list.next.next.next.next = null;
나누고, 합치는 것도 쉽게 가능하다.
let secondList = list.next.next;
list.next.next = null;
합치기 :
list.next.next = secondList;
하지만 연결리스트가 항상 배열보다 좋은것은 아니다.
가장 큰 단점은 인덱스로 요소에 쉽게 접근하기가 어렵다는 것이다.
n번째 항목을 얻고싶다면 배열은 arr[n]으로 접근이 가능하지만, 연결리스트는 첫번째 항목부터 n번을 이동해야 한다.
나머지 매개변수와 전개 문법
나머지 매개변수 ...
함수 정의 방법과 상관없이 함수에 넘겨주는 인수의 개수엔 제약이 없다.
function sum(a, b) {
return a + b;
}
alert(sum(1,2,3,4,5));
에러는 발생하지 않지만, 앞의 두개의 매개변수 빼고는 다 무시된다.
이렇게 여분의 매개변수는 그 값들을 담을 배열 이름을 ... 뒤에 붙여주면 함수 선언부에 포함시킬 수 있다.
이떄 ... 는 "나머지 매개변수들을 한데 모아 배열에 집어넣어라."는 것을 의미한다.
function sumAll(...args) { // args는 배열의 이름이다.
let sum = 0;
for(let arg of args) sum += arg;
return sum;
}
alert(sumAll(1)); // 1
alert(sumAll(1,2)); // 3
alert(sumAll(1,2,3)); // 6
'arguments' 변수
arguments 라는 특별한 유사 배열 객체를 이용하면 인덱스를 사용해 모든 인수에 접근할 수 있다.
function showName() {
alert(arguments.length);
alert(arguments[0]);
alert(arguments[1]);
// arguments는 이터러블 객체이기 때문에
// for(let arg fo arguments) alert(arg); 를 사용해 인수를 나열할 수 있다.
}
// 2, Julius, Caesar가 출력됨
showName("Julius", "Caesar");
// 1, Minjun, undefined가 출력됨(두 번째 인수는 없음)
showName("Minjun");
나머지 매개변수는 비교적 최신의 문법이라 오래된 코드를 보다 보면 arguments를 만날 수 있다.
spread 문법
배열을 통째로 매개변수에 넘겨주는 것 같은 기능이 필요할 때도 있다.
let arr = [3, 5, 1];
let arr2 = [8, 9, 15];
let merged = [0, ...arr, 2, ...arr2];
alert(merged); // 0, 3, 5, 1, 2, 8, 9, 15 (0, arr, 2, arr2 순서로 합해진다.)
배열이 아니더라도 이터러블 객체이면 전개 문법을 사용할 수 있다.
let str = "Hello";
alert([...str]); // H,e,l,l,o
전개 문법은 for..of와 같은 방식으로 내부에서 iterator를 사용해 요소를 수집한다.
문자열에 for..of를 사용하면 문자열을 구성하는 문자가 반환된다.
메서드 Array.from은 문자열 같은 이터러블 객체를 배열로 바꿔주기 때문에 Array.from을 사용해도
동일한 작업을 할 수 있다.
let str = "Hello";
// Array.from은 이터러블을 배열로 바꿔준다.
alert(Array.from(str)); // H,e,l,l,o
array/object의 새로운 복사법
Object.assign()과 똑같은 것을 할 수 있다.
let arr = [1, 2, 3];
let arrCopy = [...arr]; // 배열을 전개해서 매개변수의 리스트로 들어간다
// 그리고 새로운 배열의 결과에 넣는다.
// 두 배열이 같은 요소를 가지고 있는지 비교
alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
// 두 배열이 같은지 비교
alert(arr === arrCopy); // false (참조가 같지 않다)
// 첫번째 배열을 바꿔도 두 번째 배열은 바뀌지 않음
arr.push(4);
alert(arr); // 1, 2, 3, 4
alert(arrCopy); // 1, 2, 3
객체 복사도 가능하다
let obj = {a: 1, b: 2, c: 3};
let objCopy = {...obj};
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
alert(obj === objCopy); // false
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1, "b":2, "c":3, "d":4}
alert(JSON.stringify(objCopy)); // {"a":1, "b":2, "c":3}
변수의 유효범위와 클로저
자바스크립트는 함수 지향 언어이다. 이런 특징은 함수를 동적으로 생성할 수 있고, 생성한 함수를 다른 함수에 인수로 넘길 수 있으며, 생성된 곳이 아닌 곳에서 함수를 호출할 수 있는 이점이있다.
그런데 함수가 생성된 이후에 함수가 접근했던 외부 변수가 변경되면 새로운 값을 가져올까 아니면 생성 시점 이전의
값을 가져올까?
중첩 함수
함수 내부에서 선언한 함수는 '중첩(nested)' 함수라고 부른다.
function sayHiBye(firstName, lastName) {
// 헬퍼(helper) 중첩 함수
function getFullName() {
return firstName + " " + lastName;
}
alert("Hello, " + getFullName());
alert("Bye, " + getFullName());
}
중첩 함수는 새로운 객체의 프로퍼티 형태나 중첩 함수 그 자체로 반환될 수 있다.
이렇게 반환된 중첩 함수는 어디서든 호출해 사용할 수 있다.
물론 이때도 외부 변수에 접근할 수 있다는 사실은 변함없다.
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert(counter()); // 0
alert(counter()); // 1
alert(counter()); // 2
렉시컬 환경
단계 1. 변수
자바스크립트에선 실행 중인 함수, 코드 블록 {...}, 스크립트 전체는 렉시컬 환경(Lexical Environment) 이라 불리는
내부 숨김 연관 객체(internal hidden associated object)를 갖는다.
렉시컬 환경 객체는 두 부분으로 구성된다.
- 환경 레코드(Environment Record) - 모든 지역 변수를 프로퍼티로 저장하고 있는 객체이다. this 값과 같은
기타 정보도 여기에 저장된다. - 외부 렉시컬 환경(Outer Lexical Environment)에 대한 참조 - 외부 코드와 연관됨
'변수'는 특수 내부 객체인 환경 레코드의 프로퍼티일 뿐이다. '변수를 가져오거나 변경'하는 것은 '환경 레코드의
프로퍼티를 가져오거나 변경'함을 의미한다.
let phrase = "Hello"; // Lexical Environment - phrase: Hello ->(outer) null
alert(phrase);
위 코드엔 렉시컬 환경이 하나만 존재한다.
phrase: Hello는 변수가 저장되는 환경 레코드에 존재한다.
이렇게 스크립트 전체와 관련된 렉시컬 환경은 전역 렉시컬 환경(global Lexical Environment)라고 한다.
execution start // phrase: <uninitialized> ->(outer) null
let phrase; // phrase: undefined
phrase = "Hello"; // phrase: "Hello"
phrase = "Bye"; // phrase: "Bye"
주석은 코드가 한줄씩 실행될 때 마다 전역 렉시컬 환경이 어떻게 변화하는지 보여준다.
- 스크립트가 시작되면 스크립트 내에서 선언한 변수 전체가 렉시컬 환경에 올라간다(pre-populated)
- 이때 변수의 상태는 특수 내부 상태(special iternal state)인 'uninitialized'가 된다.
자바스크립트 엔진은 uninitialized 상태의 변수를 인지하긴 하지만, let을 만나기 전까진
이 변수를 참조할 순 없다.
- 이때 변수의 상태는 특수 내부 상태(special iternal state)인 'uninitialized'가 된다.
- let phrase를 만나면 아직 값을 할당하기 전이기 때문에 프로퍼티 값은 undefined이다. phrase는 이 시점 이후부터 사용할 수 있다.
- phrase에 값이 할당되었다.
- phrase에 값이 변경되었다.
단계 2. 함수 선언문
함수는 변수와 마찬가지로 값이다.
다만 함수 선언문(function declaration)으로 선언한 함수는 일반 변수와는 달리
바로 초기화된다는 점에서 차이가 있다.
함수 선언문으로 선언한 함수는 렉시컬 환경이 만들어지는 즉시 사용할 수 있다. 변수는 let을 만나 선언이 될 때까지
사용할 수 없다.
execution start // phrase: <uninitialized> ->(outer) null
// say: function
let phrase = "Hello";
function say(name) {
alert(`${phrase}, ${name}`);
}
하지만 let say=function(name)... 같이 함수를 변수에 할당한 함수 표현식은 해당되지 않는다.
단계 3. 내부와 외부 렉시컬 환경
함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어진다. 이 렉시컬 환경엔 함수 호출 시 넘겨받은
매개변수와 함수의 지역 변수가 저장된다.
say("John")을 호출하면 아래와 같은 내부 변화가 일어난다.(현재 실행 흐름은 붉은색 화살표로 나타낸 줄에 멈춰있다)
함수가 호출 중인 동안엔 호출 중인 함수를 윟나 내부 렉시컬 환경과 내부 렉시컬 환경이 가리키는 외부 렉시컬 환경을 갖는다.
- 내부 렉시컬 환경은 함수 say에 상응한다.
내부 렉시컬 환경엔 함수의 인자인 name으로부터 유래한 프로퍼티 하나만 있다.
say("John")을 호출했기 때문에, name의 값은 "John"이 된다. - 예시의 외부 렉시컬 환경은 전역 렉시컬 환경이다. 전역 렉시컬 환경은 phrase와 say를 프로퍼티로 갖는다.
내부 렉시컬 환경은 외부 렉시컬 환경에 대한 참조를 갖는다.
코드에서 변수를 접근할 땐, 먼저 내부 렉시컬 환경을 검색 범위로 잡는다. 내부 렉시컬 환경에서 원하는 변수를
찾지 못하면 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장한다. 이 과정은 검색 범위가
전역 렉시컬 환경으로 확장될 때까지 반복한다.
단계 4. 함수를 반환하는 함수
function makeCounter() {
let count = 0;
return function() {
return count++;
}
}
let counter = makeCounter();
makeCounter()를 호출하면 호출할 때마다 새로운 렉시컬 환경 객체가 만들어지고 여기에 makeCounter를 실행하는데 필요한 변수들이 저장됨.
여기서 중요한 사실이 하나 있다. 모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다는 점이다.
함수는 [[Environment]]라 불리는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한
참조가 저장된다.
따라서 counter.[[Environment]]엔 {count: 0}이 있는 렉시컬 환경에 대한 참조가 저장된다.
[[Environment]]는 함수가 생성될 때 딱 한 번 값이 세팅되고 영원히 변하지 않는다.
counter()를 호출하면 각 호출마다 새로운 렉시컬 환경이 생성된다.
그리고 이 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 렉시컬 환경으로서 참조한다.
실행 흐름이 중첩 함수의 본문으로 넘어오면 count 변수가 필요한데, 먼저 자체 렉시컬 환경에서 변수를 찾는다.
익명 중첩 함수엔 지역변수가 없기 때문에 이 렉시컬 환경은 비어있는 상황이다.(<empty>).
이제 counter()의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count를 찾는다.
(외부 렉시컬 환경은 counter()의 [[Environment]]에 함수가 생성된 위치가 저장되있는데 이 렉시컬 환경을 외부 렉시컬 환경으로 참조한다. 따라서 makeCounter()안의 count를 찾는것이다.)
이제 count++가 실행되면서 count값이 1 증가해야하는데, 변숫값 갱신은 변수가 저장된 렉시컬 환경에서 이뤄진다.
<!> 클로저
클로저는 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미한다.
자바스크립트에선 모든 함수가 자연스럽게 클로저가 된다.(예외가 하나 있다)
자바스크립트의 함수는 숨김 프로퍼티인 [[Environment]]를 이용해 자신이 어디서 만들어졌는지를 기억한다.
함수 본문에선 [[Environment]]를 사용해 외부 변수에 접근한다.
가비지 컬렉션
함수 호출이 끝나면 함수에 대응하는 렉시컬 환경이 메모리에서 제거된다.
함수와 관련된 변수들은 이때 모두 사라진다. 함수 호출이 끝나면 관련 변수를 참조할 수 없는 이유가 바로 여기에 있다.
자바스크립트에서 모든 객체는 도달 가능한 상태일 때만 메모리에 유지된다.
그런데 호출이 끝난 후에도 여전히 도달 가능한 중첩 함수가 있을 수 있는데, 이때는 [[Environment]] 프로퍼티에
외부 함수 렉시컬 환경에 대한 정보가 저장된다. 도달 가능한 상태가 되는 것이다.
함수 호출이 끝났지만 렉시컬 환경이 메모리에 유지되는 이유가 바로 이 때문이다.
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // g.[[Environment]]에 f() 호출 시 만들어지는
// 렉시컬 환경 정보가 저장된다.
최적화 프로세스(V8 엔진 부작용)
오래된 'var'
var로 선언한 변수의 스코프는 함수 스코프이거나 전역 스코프이다. 블록 기준으로 스코프가 생기지 않기 때문에
블록 밖에서 접근이 가능하다.
var가 if, while, for문 등에서 선언이 되었다면, 전역 변수가 되고, 만약 그 제어문들이 함수 안에있다면
함수 레벨 변수가 된다.
따라서 제어문 밖에서도 var로 선언된 변수에 접근이 가능하다는 것이다.
아주 오래전의 자바스크립트에선 블록 수준 렉시컬 환경이 만들어 지지 않았기 때문에 이런 부작용이 있다.
그리고 var로 선언된 변수가 var로 똑같이 또 선언되도 에러를 일으키지 않고 그냥 무시된다.
var user = "Pete";
var user = "John"; // var는 아무것도 하지 않는다.
// 에러를 발생시키지 않는다.
alert(user); // John
선언하기 전 사용할 수 있는 'var'
var 선언은 함수가 시작될 때 처리된다. 전역에서 선언한 변수라면 스크립트가 시작될 때 처리된다.
함수 본문 내에서 var로 선언한 변수는 선언 위치와 상관없이 함수 본문이 시작되는 지점에서 정의된다.
(코드 블록이 무시되기 때문이다.)
function sayHi() {
phrase = "Hello";
alert(phrase);
var phrase;
}
sayHi();
function sayHi() {
var phrase;
phrase = "Hello";
alert(phrase);
}
sayHi();
위 두 코드는 똑같다.
이렇게 변수가 끌어올려지는 현상을 '호이스팅(hoisting)'이라고 부른다.
하지만 선언은 호이스팅 되지만 할당은 호이스팅 되지 않는다.
즉시 실행 함수 표현식(IIFE) (모던 자바스크립트에선 쓰이지 않음)
전역 객체
전역 객체를 사용하면 어디서나 사용 가능한 변수나 함수를 만들 수 있다.
브라우저 환경에선 전역 객체를 window, Node.js 환경에선 global 라고 부르는데, 각 호스트 환경마다 부르는 이름은
다르다.
전역 객체의 이름을 globalThis로 표준화하자는 내용이 최근에 자바스크립트 명세에 추가되었기 때문에 모든 호스트
환경이 이를 따라야 한다. Chrominum 기반이 아닌 몇몇 브라우저는 아직 globalThis를 지원하지 않지만, 이에 대한
폴리필(polyfill)을 쉽게 만들 수 있다.
var gVar = 5;
alert(window.gVar); // 5 (var로 선언한 변수는 전역 객체 window의 프로퍼티가 된다.)
하위 호환성 때문에 이런 방식으로 전역 객체를 사용해도 동작은 하지만, 이 방법은 쓰지 않는게 좋다.
중요한 변수라서 모든 곳에서 사용할 수 있게 하려면, 아래와 같이 전역 객체에 직접 프로퍼티를 추가해 주는게 좋다.
// 모든 스크립트에서 현재 사용자(current user)에 접근할 수 있게 이를 전역 객체에 추가함
window.currentUser = {
name: "John"
};
// 아래와 같은 방법으로 모든 스크립트에서 currentUser에 접근 가능
alert(currentUser.name); // John
// 지역 변수 'currentUser'가 있다면
// 지역 변수와 충돌 없이 전역 객체 window에서 이를 명시적으로 가져올 수 있음
alert(window.currentUser.name); // John
전역 변수는 되도록 사용하지 않는 것이 좋다. 함수를 만들 땐 외부 변수나 전역 변수를 사용하는 것보다
'인풋'변수를 받고 이를 이용해 '아웃풋'을 만들어내개 해야 테스트도 쉽고, 에러도 덜 만들어낸다.
폴리필 사용하기
전역 객체를 이용해 현재 사용중인 브라우저가 최신 자바스크립트 기능을 지원하는지 확인할 수 있다.
구식 브라우저는 Promise객체를 지원하지 않는다.
if(!window.Promise) {
alert("구식 브라우저");
}
따라서 구식 브라우저는 alert가 동작할 것이다.
객체로서의 함수와 기명 함수 표현식
자바스크립트에서 함수는 값으로 취급된다.
그렇다면 함수의 자료형은 무엇일까?
함수는 객체이다.
기명 함수 표현식
let sayHi = function func(who) {
alert(`Hello ${who}`);
};
"func"라는 이름은 어떤 경우에 붙일까?
func 같은 이름을 붙여주면 두 가지의 변화가 생긴다.
- 이름을 사용해 함수 표현식 내부에서 자기 자신을 참조할 수 있다.
- 기명 함수 표현식 외부에선 그 이름을 사용할 수 없다.
let sayHi = function func(who) {
if(who) {
alert(`Hello, ${who}`);
}
else {
func("Guest"); // func를 사용해 자신을 호출.
}
};
sayHi(); // Hello, Guest
// 하지만 아래와 같이 func를 호출하는 것은 불가.
func(); // Error, func is not defined (기명 함수 표현식 밖에서는 그 이름에 접근 불가.)
하지만 아래와 같이 작성하면 외부 코드에 의해 sayHi가 변경될 수 있다.
함수 표현식을 새로운 변수에 할당하고, 기존 변수에 null을 할당하면 에러가 발생한다.
let sayHi = function(who) {
if(who) {
alert(`Hello, ${who}`);
}
else {
sayHi("Guest"); // TypeError: sayHi is not a function
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // 중첩 sayHi 호출은 더 이상 불가
에러는 함수가 sayHi를 자신의 외부 렉시컬 환경에서 가져 오기 때문에 발생한다.
지역 렉시컬 환경엔 sayHi가 없기 때문에 외부 렉시컬 환경에서 sayHi를 찾는데, 함수 호출 시점에 외부 렉시컬 환경의
sayHi엔 null이 저장되어있기 때문에 에러가 발생한다.
함수 표현식에 이름을 붙여주면 바로 이런문제를 해결할 수 있다.
new Function 문법
함수 표현식과 함수 선언문 이외에 함수를 만들 수 있는 방법인데, 잘 사용하는 방법은 아니다.
문법
new func = new Function([arg1, arg2, ...argN], functionBody);
let sum = new Function('a', 'b', 'return a+b');
alert(sum(1,2)); // 3
기존에 사용하던 방법과 가장 큰 차이는 런타임에 받은 문자열을 사용해 함수를 만들 수 있다는 점이다.
함수 표현식과 함수 선언문은 개발자가 직접 스크립트를 작성해야만 함수를 만들 수 있었다.
그러나 new Function 이라는 문법을 사용하면 어떤 문자열도 함수로 바꿀 수 있다.
서버에서 전달받은 문자열을 이용해 새로운 함수를 만들고 이를 실행하는 것도 가능하다.
let str = ... 서버에서 동적으로 전달받은 문자열(코드 형태) ...
let func = new Function(str);
func();
클로저
new Function 을 이요해 함수를 만들면 함수의 [[Environment]] 프로퍼티가 현재 렉시컬 환경이 아닌
전역 렉시컬 환경을 참조하게 된다.
따라서 new Function을 이용해 만든 함수는 외부 변수에 접근할 수 없고, 오직 전역 변수에만 접근할 수 있다.
function getFunc() {
let value = "test";
let func = new Function('alert(value)');
return func;
}
getFunc()(); // ReferenceError: value is not defined
일반적인 함수와 비교해보자.
function getFunc() {
let value = "test";
let func = function() { alert(value); };
return func;
}
getFunc()(); // getFunc의 렉시컬 환경에 있는 값 "test"가 출력된다.
실무에선 이 기능이 아주 유용하게 쓰인다.
문자열을 사용해서 함수를 만들어야 한다고 가정하자. 스크립트를 작성하는 시점엔 어떻게 함수를 짤지 알 수 없어서
기존의 함수 선언 방법은 사용하지 못하는데, 스크립트 실행 시점 즈음엔 함수를 어떻게 짜야 할 지 아이디어가
떠오를 수 있을 것이다. 이때 서버를 비롯한 외부 출처를 통해 코드를 받아올 수 있다.
그런데 이렇게 만들어진 새 함수는 기존 스크립트와 문제없이 상호작용할 수 있어야 한다.
압축기는 스크립트에서 주석이나 여분의 공백 등을 없애 코드 크기를 줄여주는 특수한 프로그램인데 압축기가
지역 변수 이름을 짧게 바꾸면서 문제가 발생한다.
함수 내부에 let userName이라는 변수가 있으면 이 지역변수는 압축기에 의해 let a 등으로 대체되는데,
이때 userName이 모두 a로 교체된다.
압축기가 동작한 이후엔, new Function으로 만든 함수 내부에서 외부 렉시컬 환경에 접근하려고 할 때
문제가 발생할 수 있다.
setTimeout과 setInterval을 이용한 호출 스케줄링
'Web > Vanilla JS' 카테고리의 다른 글
Object Method (0) | 2023.03.30 |
---|---|
Array Method (0) | 2023.03.30 |
Modern Javascript / 코드 품질 - Chrome으로 디버깅 (0) | 2021.07.07 |
Modern Javascript / 자료구조와 자료형 (0) | 2021.07.01 |