📝 TL;DR
this
- 자신이 속한 객체 또는 자신이 생성할 객체(instance)를 가리키는 자기 참조 변수
- 함수를 어떤 방식으로 호출하느냐에 따라 this의 값이 달라짐
실행 문맥 Execution Context
- 전역 문맥(Global Context)
- 브라우저에서는 window 객체
- NodeJS에서는 global 객체 또는 module.exports에서 사용 가능한 해당 모듈
- 함수 문맥(Function Context)
- 일반 함수 호출 : window 객체
- 메서드 호출 : 호출한 객체 자체
- 생성자 함수 호출 : 생성자 함수가 (미래에) 생성할 객체(instance)
- apply, call, bind 메서드에 의한 간접 호출 : 각 메서드에 첫 번째 인자(argument)로 전달한 객체
- 화살표 함수 호출 : 자신이 정의된 블록 내에서 가장 바깥 범위 객체의 this 값
정적 범위 Lexical Scope
- 자신이 선언된 위치에 따라 전역 범위와 지역 범위로 나뉨
- 함수를 어디서 호출했는지, 어디서 정의했는지에 따라 동적 범위와 정적 범위로 나뉨
- 자바스크립트는 정적 범위(Lexical Scope)를 따르는 언어임
🎯 들어가기에 앞서
- Javascript에서 객체(Object)란 상태(State)를 나타내는 속성(Property)과 동작(Behavior)을 나타내는 메서드(Method)를 하나로 엮은 자료구조입니다.
- 여기서 메서드는 자신이 속한 객체의 속성을 참조하려면 먼저 자신이 속한 객체를 가리키는 식별자(Identifier)를 참조할 수 있어야 합니다.
🧐 this 넌 누구냐
- this는 자신이 속한 객체나 자신이 생성할(new) 객체(instance)를 가리키는 자기 참조 변수로서, this를 통해 자신이 속한 객체나 자신이 생성할 인스턴스의 속성과 메서드를 참조할 수 있게 됩니다.
- this는 자바스크립트 엔진에 의해 암묵적으로 생성되며, 코드 어디서든 참조할 수 있습니다.
🔄 this 그때 그때 달라요
- this는 아래 설명할 실행 문맥(Execution Context)이 생성될 때 함께 결정되며, 실행 문맥은 함수를 호출할 때 생성되므로 this는 함수를 호출할 때 동적으로 결정되는 값입니다.
- 즉 함수를 어떤 방식으로 호출하느냐에 따라 그때마다 this의 값이 달라지게 됩니다.
- 실행 문맥의 두 가지 종류인 전역 문맥(Global Context)과 함수 문맥(Function Context)를 현실 생활의 예시를 통해 들어보겠습니다.
전역 문맥(Global Context)에서의 this
- 당신은 글로벌 다국적기업 의 CEO라고 상상해봅시다. 운영, 마케팅, 재무, 유통, 인사 등 회사의 모든 것을 책임지고 있는 것은 당신입니다.
- 이와 같이 전역 문맥에서의 this는 CEO 본인과 같습니다.
- 브라우저 환경에서 this는 window 객체로 객체, 생성자, 함수, 이름공간 등 DOM에 속한 모든 것들에 접근합니다.
- node 환경에서의 this는 global 객체 또는 module.exports를 통해 사용 가능한 해당 모듈 객체로, 브라우저에서처럼 DOM이 존재하지 않고 NodeJS의 내장 모듈(fs, http, process 등) 및 사용자가 정의한 모듈을 전역적으로 사용할 수 있게 해 줍니다.
console.log(this);
this.ceoName = `Tim Cook`;
const printCeoName = () => {
console.log(window.ceoName);
}
printCeoName();
환경별 특징 및 차이점 정리
- 즉 window 객체, global 객체 모두 프로퍼티와 메서드를 통해 각각 브라우저와 NodeJS 환경에서 전역 문맥을 제공하는 역할을 합니다.
특징 | 브라우저(window) | Node.js(global) |
---|
환경 | 웹 브라우저 | 서버(nodejs 엔진) |
DOM | 존재함, 문서 조작 | 없음 |
핵심기능 | 브라우저 API, DOM 인터페이스, 기본 함수 | 모듈 시스템 연결, 서버 환경정보, 내장모듈 |
함수 문맥(Function Context)에서의 this
- CEO가 모든 업무를 직접 처리하는 것은 불가능하므로 마케팅, 인사 담당 등 각 부서에 책임자를 두게 됩니다.
- 이처럼 함수 문맥에서는 this각 각 부서의 책임자와 같습니다.
- 함수가 실행될 때 해당 함수의 범위(Scope) 내에서 this 키워드를 통해 자신이 속한 부서(즉 호출된 객체)를 나타냅니다.
const researchAndDevDept = {
budget: 10000000,
allocateFunds: function () {
console.log(this.budget);
}
};
researchAndDevDept.allocateFunds();
함수 호출 방식에 따라 달라지는 this
- 앞서 함수가 어떻게 호출되었는지에 따라 this가 동적으로 결정된다고 하였습니다.
- 이에 따라 여러 가지 함수 호출 방식에 따라 this가 어떻게 결정되는지 알아보겠습니다.
일반 함수 호출
- 일반 함수 호출에서 내부의 this는 아래와 같이 전역 객체 window를 가리키게 됩니다.
const func = function () {
console.log(this);
}
func();
메서드 호출
- 메서드(Method)란 객체(Object)의 속성(Property)으로 정의된 함수입니다.
- 객체의 메서드를 호출할 때 this는 호출한 객체 자체를 가리키게 됩니다. 즉, 아래 코드와 같이 메서드가 어떤 객체에 속해 있는지를 가리키게 됩니다.
const introduce = {
name: 'ChoboDev',
sayHello: function () {
console.log(`안녕하세요. 나는 ${this.name} 입니다.`);
}
};
introduce.sayHello();
- 마찬가지로 이벤트 핸들러를 등록하기 위해 사용하는 addEventListener 또한 메서드이기 때문에 this는 이벤트가 발생한(이벤트 핸들러가 속한) 객체를 가리키게 됩니다.
const boo = document.getElementById('boo');
boo.addEventListener('click', function() {
console.log(this);
});
생성자 함수 호출
- 생성자 함수는 이름 그대로 객체(instance)를 생성하는 함수입니다. new 연산자를 통해 함수를 호출하게 되면 해당 함수는 생성자 함수로 동작하게 됩니다.
- 여기서 instance는 실제 메모리 상에 할당된 객체를 의미합니다.
- 생성자 함수 내부에서의 this는 생성자 함수가 (미래에) 생성할 객체(instance)를 가리키게 됩니다.
const Circle = function (radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius * this.radius;
}
}
const circle5 = new Circle(5);
const circle8 = new Circle(8);
console.log(circle5.getArea());
console.log(circle8.getArea());
apply, call, bind 메서드에 의한 간접 호출
- Javascript는 아래 정적 범위(Lexical Scope)에서 설명할 변수나 함수가 선언될 때 범위가 결정되는 정적 범위(Lexical Scope)를 따르는 언어입니다.
- ES2015(ES6)가 등장하기 전, this가 가리키는 객체를 변경하고 싶을 때 apply, call, bind 등의 메서드를 통해 명시적으로 this가 가리키는 객체를 변경할 수 있는 유일한 방법이었습니다.
- 즉 ES2015 이전에는 일반 함수에서 this는 실행 문맥에 따라 달라지거나 apply, call, bind 등을 통해 명시적으로 this가 가리키는 값을 변경(명시적 바인딩)하는 방법이 있었습니다.
- apply 예시
- sum
함수는 인수들을 arguments
객체를 통해 처리하며,
- apply()
를 사용하여 두 번째 인자로 배열 numbers
를 전달함으로써 각 요소가 arguments
에 들어가 함수에 인수로 전달됩니다.
const sum = function () {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
const numbers = [1, 2, 3, 4];
const result = sum.apply(null, numbers);
- call 예시
- salute
함수의 this
는 일반적으로 호출하는 경우 window
객체가 되지만, call()
을 사용하여 officer
객체를 첫 번째 인자로 전달함으로써 함수 실행 중 this
를 officer
객체로 변경합니다.
- 즉, salute
안에서 this.rank
는 officer
객체의 rank
속성 (값은 "Captain") 를 참조하게 됩니다.
const salute = function (prefix, rank) {
console.log(`${prefix}! ${this.rank}!`);
}
const officer = { rank: `Captain` };
salute.call(officer, `Aye aye Sir`, `Peter`);
- bind 예시
- salute.bind(officer)
을 통해 새로운 함수 bindSalute
가 생성되고, 이 함수는 salute
함수의 원본과 동일한 기능을 가지지만, 항상 this
값이 officer
객체로 고정됩니다.
- 그래서 bindSalute('Yes sir', 'Peter')
호출 시에도 this
는 officer
객체를 가리키고 'Captain' 이 출력됩니다.
const salute = function (prefix, rank) {
console.log(`${prefix}! ${this.rank}!`);
}
const officer = { rank: `Captain` };
const bindSalute = salute.bind(officer);
bindSalute('Yes sir', 'Peter');
화살표 함수 호출
- ES2015(ES6)에서 화살표 함수(arrow function)의 등장으로 새로운 방식으로 this를 결정하게 되었습니다.
- 화살표 함수는 자신이 정의된 블록 내의 가장 바깥 범위(Scope)의 this 값을 가리킵니다(묵시적 바인딩).
- 화살표 함수는 그 자체로 이미 this의 값이(묵시적으로) 결정되어 있으므로 apply, call, bind 등의 메서드를 통해 this가 가리키는 값을 변경할 수 없습니다.
- 그리고 화살표 함수 내에서 this가 어떤 객체를 가리키는지는 함수가 호출되는 맥락에 따라 달라지는 데 일반적으로 호출된 위치(객체 메서드, 이벤트 핸들러 등)의 this 값이 상속됩니다.
- 화살표 함수는 함수 자체의 this 바인딩(this가 가리키는 값)을 갖지 않으므로, 화살표 함수 내부에서 this를 참조하면 상위 범위의 this를 그대로 참조합니다. 이를 lexical this라 합니다.
- 즉 this가 함수가 정의된 위치에 의해 결정되는 것입니다.
const officer = {
rank: `Captain`,
salute: () => {
console.log(`Aye aye Sir! ${this.rank}!`);
},
oldSchooledSalute: function() {
console.log(`Aye aye Sir! ${this.rank}!`);
}
};
officer.salute();
officer.oldSchooledSalute();
함수 호출별 총 정리
함수 호출 방식 | this가 가리키는 값 |
---|
일반 함수 호출 | 전역 객체(window) |
메서드 호출 | 메서드를 호출한 객체(이벤트 리스너의 경우 그 요소) |
생성자 함수 호출 | 생성자 함수가 (미래에) 생성할 객체(instance) |
apply, call, bind 등 간접 호출 | apply, call, bind 메서드에 첫 번째 인수(argument)로 전달한 객체 |
화살표 함수 | 자신이 정의된 블록 내에서 가장 바깥 범위의 this 값 |
💻 실행 문맥(Execution Context)에 관하여
실행 문맥에 대해
- 위에서 크게 전역 문맥과 함수 문맥으로 설명을 했는데 둘 다 실행 문맥의 한 형태입니다.
- 실행 문맥(Execution Context)이란 코드가 실행될 때, 어떤 객체와 변수를 사용할 수 있는 환경을 의미하며 그 공간 안에서 this, 함수, 변수 등을 활용하는 지역을 말합니다.
- 종류로는 위에서 이미 언급한 전역 문맥(Global Context)과 함수 문맥(Function Context) 그리고 Eval 실행 문맥(Eval execution Context), 모듈 문맥(Module Context)이 있습니다.
전역 실행 문맥(Global Execution Context)
- 전역 코드는 전역 변수를 관리하기 위해 최상위 범위(Scope)를 생성해야 합니다.
- 전역 실행 문맥은 브라우저 창이 열릴 때 처음 생성되며, this는 window 객체를 가리킵니다.
- 모든 전역 변수는 이곳에 저장되고 함수와 객체도 여기에 선언될 수 있습니다.
const globalVar = `I am inevitable`;
const globalFunc = function () {
}
함수 실행 문맥(Function Execution Context)
- 함수 코드는 지역 범위(Scope)를 생성하고 지역 변수, 매개변수, arguments 객체를 관리해야 합니다.
- 모든 함수가 호출될 때 함수 실행 문맥이 생성되며, 이전에 설명한 바와 같이 함수의 종류와 호출 방식에 따라 this가 가리키는 값이 달라집니다.
- 함수 내부에서 선언된 변수는 함수 범위(Function Scope) 내에 있으며, 호출 외부에서는 접근할 수 없습니다.
const myFunc = function() {
let localVar = `I am Iron Man`;
console.log(localVar);
}
myFunc();
console.log(localVar);
Eval 실행 문맥(Eval Execution Context)
- eval 코드는 strict mode(엄격 모드)에서 자신만의 독자적인 범위(Scope)를 생성합니다.
- eval() 함수를 사용할 때 생성되며, 이 함수는 문자열을 Javascript 코드로 해석하여 실행합니다.
- eval() 호출 시 새로운 실행 문맥이 만들어지며 이때 this 값은 호출한 위치의 this 값이 됩니다.
- eval()은 인자로 받은 코드를 호출한 함수의 권한으로 실행하는 매우 위험한 함수이므로 악의적인 사용자(해커 등)에 의해 실행되므로 사용해서는 안 됩니다.
- 관련하여 좀 더 상세한 이유를 알고 싶다면 MDN eval() - eval을 절대 사용하지 말 것! 을 확인하세요.
모듈 실행 문맥(Module Execution Context)
- 모듈 코드는 모듈별로 독립적인 모듈 범위(Scope)를 생성합니다.
- ECMAScript Modules(ES Modules) 기능이 활용될 때 해당 문맥이 사용(import, export 키워드) 됩니다.
- 각 모듈은 독립적인 실행 문맥을 가지며, 변수와 함수는 모듈 내에서만 접근할 수 있으며, 다른 모듈과의 상호작용을 하려면 import, export 키워드를 통해 제어합니다.
const message = "Hello from module!";
export { message };
import { message } from './myModule.js';
console.log(message);
📌 정적 범위(Lexical Scope)에 관하여
코드의 범위(Scope)
- 코드는 전역(Global)과 지역(Local)으로 구분할 수 있으며, 이 때 변수는 자신이 선언된 위치에 따라 자신이 유효한 범위(Scope)가 결정됩니다.
- 즉 전역에서 선언된 변수는 전역 범위(Global Scope)를 갖는 전역 변수이고,
- 지역에서 선언된 변수는 지역 범위(Local Scope)를 갖는 지역 변수입니다.
구분 | 설명 | 범위 | 변수 |
---|
전역 | 코드의 가장 바깥 영역 | 전역 범위 | 전역 변수 |
지역 | 함수 몸체 내부 | 지역 범위 | 지역 변수 |
범위(Scope)의 결정 방법
const outieFunc = () => {
let outieSay = `Mark(outie): 좋은 사람은 규칙을 따르지만 훌륭한 사람은 스스로를 따를 겁니다.`;
const innieFunc = () => {
let innieSay = `Mark(innie): 왜 우리는 여전히 어둠 속에서 일하고 있을까요?`;
console.log(outieSay);
console.log(innieSay);
}
innieFunc();
console.log(innieSay);
}
outieFunc();
- outieFunc는 외부 함수이며, 이 함수 내에서 outieSay라는 변수가 선언되고,
- innieFunc은 outieFunc 내에 정의된 내부 함수입니다.
- 내부 함수에서 외부 함수에 선언된 outieSay에 접근할 수 있지만
- 반대로 외부 함수에서 내부 함수의 변수인 innieSay에는 접근할 수 없습니다.
- 이는 정적 범위(Lexical Scope)에 의해 내부 함수는 바깥 범위(Outer Scope)에 접근할 수는 있으나 그 반대는 불가능하기 때문입니다.
- 위에서처럼 실행 결과는 함수의 상위 범위가 무엇인지에 따라 두 가지 형태로 결정될 것입니다.
- 함수를 어디서 호출했는지에 따라 상위 범위를 결정하기
- 함수를 어디서 정의했는지에 따라 상위 범위를 결정하기
동적 범위(Dynamic Scope)
- 첫 번째 방법을 동적 범위(Dynamic Scope)라 합니다.
- 이는 함수를 정의하는 시점에는 함수가 어디서 호출될 지 알 수 없으므로 함수가 호출되는 시점에 동적으로 결정해야 하기 때문에 동적 범위(Dynamic Scope)라 부릅니다.
정적 범위(Lexical Scope)
- 두 번째 방법을 정적 범위(Static Scope 또는 Lexical Scope)라 합니다.
- 위와 같이 상위 범위가 동적으로 변하지 않고 함수의 정의가 평가(evaluation)되는 시점에서 상위 범위가 정적으로 결정되기 때문에 정적 범위(Lexical Scope)라 부릅니다.
- 또한 Javascript를 포함한 많은 언어에서 Lexical Scope를 따릅니다.
📚 참고자료
도서
- Flanagan, D. (2022). 자바스크립트 완벽 가이드(7판). 인사이트.
- 이웅모. (2020). 모던 자바스크립트 Deep Dive : 자바스크립트의 기본 개념과 동작 원리. 위키북스.
- 정재남. (2019). 코어 자바스크립트 : 핵심 개념과 동작 원리로 이해하는 자바스크립트 프로그래밍. 위키북스.
웹사이트
와우 넘 잘 만드셨네요. 글의 깊이가 느껴집니다. 근데 젤 첨에 TL;DR <-- 이거 뭐에요??