this란? 🔍
this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수다. 가장 일반적으로는 객체의 메서드 안에서 사용되며 이를 통해 동일한 메서드를 서로 다른 객체에서 재사용할 수 있다.
일반 변수는 선언된 위치(렉시컬 스코프)에 의해 값이 결정되지만 this는 다르다. 함수가 어떤 방식으로 호출(바인딩)되었는지에 따라 가리키는 값이 동적으로 결정된다.
쉽게 비유하자면 "나"라는 단어와 같다. "나는 밥을 먹었다"에서 '나'가 누구인지는 그 말을 누가 했느냐에 따라 달라지는 것처럼 this도 누가(어떤 컨텍스트가) 함수를 호출했느냐에 따라 달라진다.
this가 결정되는 4가지 규칙 🚨
1. 기본 바인딩
아무런 컨텍스트 없이 함수를 단독 호출하면 this는 전역 객체를 가리킨다. 그리고 이 this는 브라우저에서 window 객체가 된다.
function greet() {
console.log(this); // window (브라우저)
}
greet();
2. 암시적 바인딩
객체의 메서드로 호출되면 this는 그 객체를 가리킨다.
const user = {
name: '철수',
greet() {
console.log(this.name); // '철수'
}
};
user.greet(); // this === user
3. 명시적 바인딩
함수가 호출될 때 apply, call 또는 bind가 사용되었다면 첫번째 인자로 전달하는 값에 this 를 바인딩 한다.
bind는 새로운 함수를 반환하며 this를 영구 고정한다는 점에서 call/apply와 다르다.
jsfunction greet() {
console.log(this.name);
}
const user = { name: '철수' };
greet.call(user); // '철수'
greet.apply(user); // '철수'
const bound = greet.bind(user);
bound(); // '철수'
4. new 바인딩
new 키워드로 생성자 함수를 호출하면, this는 새로 생성되는 인스턴스 객체를 가리킨다
function Person(name) {
this.name = name; // 새 인스턴스에 바인딩
}
const p = new Person('민준');
console.log(p.name); // '민준'
❗️우선순위: new > 명시적(bind) > 암시적(메서드) > 기본❗️
화살표 함수의 this — 완전히 다른 규칙❓
화살표 함수는 this를 아예 갖지 않는다. 대신 자신이 선언된 위치의 외부 스코프 this를 그대로 캡처한다.(클로저)
const person = {
name : 'Seo',
sayName : function() {
innerFun = function() {
return `안녕하세요 ${this.name}님`
}
console.log(innerFun()) /// 안녕하세요 ''님
}
}
person.sayName()
innerFun 함수 앞에 마침표(.) 을 붙여서 호출하지도 않았고, bind, call, apply 를 사용하지도 않았다. 일반 함수 호출 되었기 때문에 여기서 this 는 window가 된다.
const person = {
name : 'Seo',
sayName : function() {
innerFun =() => {
return `안녕하세요 ${this.name}님` /// 안녕하세요 Seo님
}
console.log(innerFun())
}
}
person.sayName()
여기서도 innerFun 은 일반함수로 호출 되었으나 innerFun 이 화살표 함수로 선언이 되어 있다. 화살표 함수에서의 this 는 자신의 상위 스코프를 따르기 때문에 여기서 this 는 person 객체 안에 선언된 name 이 된다.
클로저 - 기억하는 함수 📸
클로저의 정의
MDN은 클로저를 이렇게 정의한다.
"A closure is the combination of a function and the lexical environment within which that function was declared." 클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합이다.
풀어 말하면 외부 함수가 종료된 후에도 내부 함수가 외부 함수의 변수를 참조할 수 있는 현상이다.
이게 왜 가능하냐면 함수 객체는 생성될 때 [[Environment]]라는 내부 슬롯에 자신이 정의된 위치의 상위 스코프 참조를 저장한다.(함수가 살아있는 한 유지)
const x = 1;
function outer() {
const x = 10;
const inner = function() { console.log(x); }; // [[Environment]] = outer의 렉시컬 환경
return inner;
}
const innerFunc = outer(); // outer 실행 컨텍스트는 스택에서 제거
innerFunc(); // 10 — 그러나 x는 여전히 참조 가능
outer의 실행 컨텍스트가 콜 스택에서 제거되어도 inner의 [[Environment]]가 outer의 렉시컬 환경을 참조하고 있기 때문에 가비지 컬렉션의 대상이 되지 않는다.
클로저라고 부르는 기준
- 상위 스코프의 식별자를 참조한다
- 외부 함수보다 더 오래 유지된다 (외부로 반환된다)
function foo() {
const x = 1;
// 클로저 — 상위 스코프 참조 + 외부로 반환
function bar() {
console.log(x);
}
return bar;
}
클로저 왜 쓰지?
클로저의 핵심 활용: 상태 은닉
클로저를 실전에서 가장 많이 쓰는 이유는 외부에서 접근할 수 없는 private 상태를 만들기 위해서다.
// 전역 변수 방식 — 누구나 변경 가능한 취약한 구조
let num = 0;
const increase = function() { return ++num; };
// 클로저 방식 — num은 외부에서 직접 접근 불가
const increase = (function() {
let num = 0;
return function() {
return ++num;
};
}());
increase(); // 1
increase(); // 2
increase(); // 3
즉시 실행 함수(IIFE)로 num을 감싸면 외부에서 직접 접근이 불가능해진다. 반환된 함수만이 num을 변경할 수 있다.
증가와 감소 모두 필요하다면 메서드를 가진 객체를 반환하면 된다:
const counter = (function() {
let num = 0;
return {
increase() { return ++num; },
decrease() { return num > 0 ? --num : 0; }
};
}());
counter.increase(); // 1
counter.increase(); // 2
counter.decrease(); // 1
장점은
- 전역변수 사용의 최소화
- 데이터 보존 가능
- 모듈화를 통한 코드 재사용에 편리
- 정보의 접근 제한 (캡슐화)
| this | 클로저 | |
| 결정 시점 | 함수 호출 시점 | 함수 정의 시점 |
| 핵심 질문 | 누가 이 함수를 호출했나? | 어디서 이 함수가 정의됐나? |