함수 심화 스코프, 클로저, this 바인딩
스코프 (함수 스코프, 블록 스코프, 스코프 체인)
스코프는 변수에 접근할 수 있는 범위를 뜻한다. JS에는 크게 두 종류의 스코프가 있다.
함수 스코프
var로 선언한 변수가 갖는 스코프다. 함수 단위로만 범위가 구분되고, if나 for 같은 블록은 스코프 경계가 되지 않는다.
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 (블록을 벗어나도 접근 가능)
}
example();블록 스코프
let, const로 선언한 변수가 갖는 스코프다. {}로 감싸인 블록 단위로 범위가 구분된다.
function example() {
if (true) {
let y = 10;
}
console.log(y); // ReferenceError: y is not defined
}이 차이 때문에 let, const가 var보다 더 예측 가능한 코드를 만들 수 있어서 요즘은 기본적으로 let, const를 사용한다.
스코프 체인
함수는 자신이 선언된 위치를 기준으로 상위 스코프를 참조할 수 있는 연결고리를 가진다. 이를 스코프 체인이라고 한다. 변수를 찾을 때 현재 스코프에 없으면 바깥쪽 스코프로 계속 거슬러 올라가며 찾는다.
const outer = "바깥";
function first() {
const inner = "안쪽";
function second() {
console.log(inner); // "안쪽" — first의 스코프에서 찾음
console.log(outer); // "바깥" — 전역 스코프에서 찾음
}
second();
}
first();중요한 건 스코프 체인이 함수가 호출된 위치가 아니라 선언된 위치를 기준으로 결정된다는 점이다. 이를 **렉시컬 스코프(Lexical Scope)**라고 부른다.
const value = "전역";
function outer() {
const value = "outer 내부";
inner();
}
function inner() {
console.log(value); // "전역" (outer 안에서 호출됐지만, inner가 선언된 위치 기준으로 스코프가 결정됨)
}
outer();클로저 (Closure)와 활용 사례
클로저는 함수가 자신이 선언될 당시의 스코프를 기억해서, 함수가 스코프 밖에서 실행되더라도 그 스코프의 변수에 계속 접근할 수 있는 현상을 말한다.
기본 예시
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3outer 함수는 이미 실행이 끝났는데도, 반환된 inner 함수는 여전히 count 변수에 접근하고 값을 변경할 수 있다. 이게 가능한 이유는 inner 함수가 자신이 선언된 스코프(outer의 스코프)를 기억하고 있기 때문이다.
활용 사례 1 — 상태 은닉 (캡슐화)
외부에서 직접 접근할 수 없는 비공개 변수를 만들 때 사용한다.
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.count); // undefined (외부에서 직접 접근 불가)count는 createCounter 함수 스코프 안에만 존재해서, 외부에서는 오직 반환된 메서드를 통해서만 값을 다룰 수 있다.
활용 사례 2 — 함수 팩토리
특정 값을 고정해두고, 그 값만 다르게 적용한 여러 함수를 만들 때 사용한다.
function multiplyBy(x) {
return function (y) {
return x * y;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30활용 사례 3 — 이벤트 핸들러에서 상태 유지
function setupButton() {
let clickCount = 0;
document.querySelector("button").addEventListener("click", function () {
clickCount++;
console.log(`${clickCount}번 클릭됨`);
});
}주의할 점 — 반복문과 클로저
var로 반복문을 돌리면서 클로저를 만들면 의도치 않은 결과가 나올 수 있다.
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// 3 3 3 (모두 같은 i를 참조하기 때문)var는 함수 스코프라서 반복문이 다 끝난 뒤의 i 값(3)을 모든 콜백이 공유해서 참조한다. let으로 바꾸면 매 반복마다 새로운 블록 스코프가 생겨서 의도한 대로 동작한다.
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// 0 1 2this 바인딩
this는 함수가 호출되는 방식에 따라 가리키는 대상이 달라진다. 함수가 선언된 위치가 아니라 호출 시점에 결정된다는 점이 스코프와 가장 큰 차이다.
전역에서의 this
console.log(this); // 브라우저 환경에서는 window 객체객체 메서드에서의 this
메서드를 호출한 객체를 가리킨다.
const user = {
name: "고리",
greet() {
console.log(this.name);
}
};
user.greet(); // "고리" (user가 this)단, 메서드를 변수에 따로 담아서 호출하면 this가 유지되지 않는다.
const greet = user.greet;
greet(); // undefined (this가 user를 가리키지 않음)일반 함수에서의 this
일반 함수로 호출하면 this는 전역 객체를 가리킨다. (strict mode에서는 undefined)
function showThis() {
console.log(this);
}
showThis(); // window (또는 strict mode에서는 undefined)화살표 함수에서의 this
화살표 함수는 자신만의 this를 가지지 않는다. 대신 자신이 선언된 위치의 상위 스코프 this를 그대로 물려받는다. 즉 this 바인딩도 스코프처럼 렉시컬(선언 위치 기준)하게 동작한다.
const user = {
name: "고리",
greet: () => {
console.log(this.name); // 화살표 함수는 user의 메서드로 호출돼도 자기만의 this가 없음
}
};
user.greet(); // undefined (전역의 this를 그대로 사용)이 특성은 오히려 콜백 함수 안에서 유용하게 쓰인다.
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // 화살표 함수라서 start를 호출한 timer의 this를 그대로 사용
console.log(this.seconds);
}, 1000);
}
};
timer.start(); // 1 2 3 ...만약 일반 함수로 썼다면 setInterval의 콜백은 일반 함수 호출 방식으로 실행되기 때문에 this가 timer를 가리키지 못했을 것이다.
explicit binding — call / apply / bind
this를 개발자가 명시적으로 지정하고 싶을 때 사용하는 메서드들이다.
const user1 = { name: "고리" };
const user2 = { name: "영희" };
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
// call: 인자를 하나씩 나열
greet.call(user1, "안녕"); // 안녕, 고리
// apply: 인자를 배열로 전달
greet.apply(user2, ["안녕"]); // 안녕, 영희
// bind: this가 고정된 새로운 함수를 반환 (즉시 실행하지 않음)
const greetUser1 = greet.bind(user1);
greetUser1("반가워"); // 반가워, 고리| 메서드 | 실행 시점 | 인자 전달 방식 |
|---|---|---|
| call | 즉시 실행 | 하나씩 나열 |
| apply | 즉시 실행 | 배열로 전달 |
| bind | 실행하지 않고 새 함수 반환 | 하나씩 나열 |
this 정리 — 바인딩 규칙 우선순위
new키워드로 호출 (생성자 함수) → 새로 생성된 객체call/apply/bind로 명시적 바인딩 → 지정한 객체- 객체의 메서드로 호출 → 호출한 객체
- 화살표 함수 → 선언 위치의 상위 스코프 this
- 그 외 일반 함수 호출 → 전역 객체 (strict mode에서는 undefined)