이번에 다뤄볼 주제는 악명 높은 This다.
실제로도 자바스크립트 관련 면접 질문에서 This와 관련해 문제 형식으로 종종 나왔던 것을 볼 수 있는데,
이게 대충 생각하면 딱 틀리기 좋은 주제라서 한번 정리해 보고자 한다.
초면은 아닌 거 같고, 누구시더라?
This는 사실 내 글에서 처음 등장한 개념은 아니다.
실행 컨텍스트에서 실행 컨텍스트의 구조를 설명할 때 잠깐 등장했다. (실행 컨텍스트 읽고 오기)
위의 사진은 내 글에서 발췌한 내용인데, 여기 실행컨텍스트의 구조 중
3번째에 있는 ThisBinding이 this 식별자가 바라보고 있는 대상 객체를 의미한다고 적혀있다.
여기서 바인딩은 식별자와 값을 연결하는 과정을 뜻하는데,
ThisBinding을 좀 더 쉽게 설명하면,
this라는 친구한테 '너가 가리켜야 하는 대상은 저 객체야'라고 알려주는 과정이라고 이해하면 되겠다.
This란?
This는 영어로 '이것'이라는 의미로 어떤 것을 지칭할 때 사용하는 단어다.
자바스크립트에서 this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수라고 한다.
자바스크립트뿐만 아니라 다른 언어에서도 사용되는 개념인데,
Java의 경우 인스턴스가 생성되면 this는 항상 해당 객체를 가리킨다고 하는데
우리 금쪽이 자바스크립트의 this는 상황에 따라 가리키는 대상이 자꾸 바뀌는 특징을 가지고 있다.
그래서 상황별로 this 금쪽이가 어떤 대상을 지칭하는지 정리해 보자.
This 기출문제
우선 일반적으로 this가 어떻게 쓰이는지부터 알아보자.
console.log(this) // window
브라우저 환경에서 자바스크립트가 실행될 때,
전역 환경에서 사용하는 변수가 함수는 window 객체의 속성으로 포함된다.
그래서 전역에서 출력한 this는 window 객체를 가리키게 된다.
function test () {
console.log(this);
};
test(); // window
전역에서 일반 함수를 생성하고 그 함수 내부에서 this를 출력하도록 한 뒤,
해당 함수를 호출해도 여전히 this는 window 객체를 가리키게 된다.
function test () {
function test2 () {
console.log(this);
};
test2();
}
test(); // window
함수 안에서 함수를 선언하고 호출하는 중첩 함수의 구조에서 this를 출력하도록 했다.
중첩 함수여도 여전히 this는 window 객체에 바인딩된다.
const person = {
name: "um",
age: 20,
whatAmI: this
};
console.log(person.whatAmI); // window
이번에는 객체 내에서 this를 프로퍼티 값으로 설정하고, 해당 프로퍼티를 출력하도록 해봤다.
그런데 이번에도 window 객체를 출력하는 것을 볼 수 있다.
자 여기서 헷갈릴 수 있는 부분이 있는데 this의 정의에서 this는 자신이 속한 객체라고 했다.
그런데 출력 값은 왜 person 객체가 아닌 window 객체일까?
이 질문에 대한 답을 삼단논법을 통해 풀어보자
전제 1 : this가 바인딩되는 시점은 실행 컨텍스트와 연관되어 있다.
전제 2 : 실행 컨텍스트는 함수가 호출될 때 생성된다.
결론 : this가 바인딩되는 시점은 함수가 호출될 때 이뤄진다.
이 결론에 따라 위의 코드를 보면 person 객체에서는 함수의 호출이 이뤄지지 않는다.
person이라는 객체가 생성될 때, this는 전역 객체를 이미 바인딩하고 있기에 이러한 결과가 나온 것이다.
그렇다면 내가 생성한 객체를 this에 바인딩하려면 어떻게 해야 할까?
const person = {
name: "um",
age: 20,
whatAmI: function () {
console.log(this);
}
};
person.whatAmI(); // { name: 'um', age: 20, whatAmI: ƒ whatAmI() }
내가 생성한 객체를 this가 가리킬 수 있도록 하는 방법은 객체의 메서드를 이용하는 방법이다.
객체의 메서드인 whatAmI에서 this를 출력하도록 설정하고 객체 밖에서 해당 메서드를 호출했더니
의도한 대로 person 객체를 출력하는 것을 확인할 수 있다.
* 기출문제 요약정리 *
this는 기본적으로 window 객체를 바인딩한다.
특정 객체를 this가 바인딩하도록 하는 방법은 객체의 메서드를 활용하는 방법이다.
This 변형 문제
어떤 시험이건 기출문제만 나오면 암기력 좋은 사람이 가장 높은 점수를 받을 텐데 세상은 호락호락하지 않다.
변별력이라는 구실로 다양한 기출변형문제들이 나오는데,
this는 변별력을 시험하는 것도 아니면서 변형 상황들이 등장한다.
중첩 함수 유형
const person = {
name: "um",
age: 20,
whatAmI: function () {
function inner () {
console.log(this);
};
inner();
}
};
person.whatAmI(); // window
첫 번째 알아볼 유형은 메서드 안에 일반 함수를 생성하고 호출하는 중첩 함수 유형이다.
함수만 하나 추가해 줬을 뿐인데 this는 window 객체를 출력해 버린다.
진짜 돌아버리겠다.
여기서 중요한 this의 특징은 호출의 주체가 어디인지에 따라서 결정된다는 점이다.
person.whatAmI는 person이라는 객체가 호출의 주체라는 것이 명확한데,
inner 함수는 호출의 주체가 명시되어 있지 않아서 window 객체를 가리키게 된다.
변수 할당 유형
const person = {
name: "um",
age: 20,
whatAmI: function () {
console.log(this);
}
};
const amI = person.whatAmI;
amI(); // window
다음으로 알아볼 유형은 외부 변수에 객체 메서드를 할당한 유형이다.
amI라는 변수에 객체 메서드인 whatAmI를 할당하고, amI를 호출했더니 window 객체를 출력한다.
첫 번째 유형과 마찬가지로 이것도 호출의 주체 문제가 되겠는데,
amI에 할당한 것은 함수 자체를 할당했다. 호출한 값을 할당한 것이 아닌 함수를 할당했다.
그리고 그 함수를 전역에서 호출했다. 그래서 window 객체가 출력된 것이다.
const numbers = {
one: function () {
console.log(this)
},
two: function () {
this.one();
},
three : function () {
const check = this.one;
check();
}
}
numbers.one(); // 1번
numbers.two(); // 2번
numbers.three(); // 3번
조금 더 꼬아서 이러한 형태의 코드가 있다고 가정해 보자.
이 상황에서 다른 값을 출력하는 건 몇 번일까? 직접 생각해 보고 답을 고민해 보자.
화살표 함수 유형
const person = {
name: "um",
age: 20,
whatAmI: () => {
console.log(this);
}
};
person.whatAmI(); // window
이번 유형은 객체에 메서드에 화살표 함수를 적용한 유형이다.
일반 유형이랑 크게 다를 게 없어 보이는데 window 객체를 출력하고 있는 것을 볼 수 있다.
하......
객체의 메서드를 이용했고, 호출의 주체도 명확하게 했는데 왜 또 이럴까?
이건 화살표 함수의 특성 때문에 발생한 결과다.
화살표 함수는 항상 상위 스코프의 this를 그대로 상속받도록 설계되었다.
현재 코드에서 화살표 함수의 상위 스코프의 this는 window 객체기에 window가 출력되는 것이다.
const person = {
name: "um",
age: 20,
whatAmI: function () {
(() => {
console.log(this);
})()
}
};
person.whatAmI(); // { name: 'um', age: 20, whatAmI: ƒ whatAmI() }
이번에는 중첩함수 형태로 this를 출력했다.
아까 첫 번째 유형과 조금 다른 점은 메서드 안에 일반 함수를 사용하는지 아니면 화살표 함수를 사용하는지의 차이다.
중첩 함수를 사용하면 호출의 주체가 달라져서 window 객체를 출력한다고 봤는데
이번에는 person 객체를 출력하고 있는 것을 볼 수 있다.
정신이 나갈 거 같은데 다시 정신 차리고
화살표 함수가 상위 스코프의 this를 상속받는다는 특징을 기억해 보자.
이 코드를 보면 person 객체에 whatAmI 메서드가 있는데
그 안에서 화살표 함수를 사용한 뒤, 즉시 실행을 통해 this를 출력하도록 했다.
whatAmI 메서드는 person.whatAmI()로 호출되었기 때문에 whatAmI의 this는 person 객체를 가리킨다.
그 안에서 화살표 함수는 호출 주체와 상관없이 상위 스코프의 this를 상속받기에
whatAmI가 가리키고 있는 person 객체를 상속받아 출력하게 된 것이다.
const person = {
name: "um",
sayName: function() {
// 여기서 this는 person 객체
setTimeout(function() {
console.log(this); // 전역 객체(window) 또는 undefined (strict mode)
}, 1000);
}
};
person.sayName();
화살표 함수는 왜 이 모양일까 생각하다가 화살표 함수가 이렇게 설계된 이유를 보니 고개를 끄덕일 수밖에 없었다.
기존의 일반 함수에서는 함수 호출 방식에 따라 this가 동적으로 바뀌었다.
특히, 콜백 함수나 이벤트 핸들러, 비동기 작업 등에서 this가 예상과 다르게 바인딩되는 경우가 많았다.
위의 코드를 보더라도 setTimeout의 콜백 함수로 사용된 함수에서 this는 예상과 다르게
전역 객체를 출력하고 있는 것을 볼 수 있다.
하지만 화살표 함수는 호출 주체에 상관없이 상위 스코프의 this를 상속받기에
이러한 문제를 해결할 수 있게 되었다.
this에게 객체 정해주기
상황마다 this가 지 맘대로 바인딩되는 것을 강제로 지정할 수 있는 방법도 있다.
바로 call, apply, bind 메서드를 사용하는 방법인데, 어떻게 사용하는지 알아보자.
call
call 메서드는 함수가 호출될 때 this의 값을 명시적으로 지정할 수 있는데,
인자를 쉼표로 구분해 전달하고 첫 번째 인자에는 this로 사용할 객체가 들어가게 된다.
두 번째 인자에는 함수에서 사용하는 값을 넘겨줄 수 있다.
const person = {
name: "um",
age: 20
};
function introduce (city, country) {
console.log(`my name is ${this.name}, i live in ${city}, ${country}`);
}
introduce.call(person, "seoul", "korea"); // `my name is um, i live in seoul, korea`
apply
apply는 call과 비슷하지만 인자를 배열로 전달하는 특징이 있다.
첫 번째 인자에는 this로 사용할 객체가 들어가고, 두 번째는 인자 배열이 들어가게 된다.
const person = {
name: "um",
age: 20
};
function introduce (city, country) {
console.log(`my name is ${this.name}, i live in ${city}, ${country}`);
}
introduce.apply(person, ["seoul", "korea"]); // `my name is um, i live in seoul, korea`
bind
bind는 함수를 호출하는 방식이 아닌 새로운 함수를 반환한다.
반환된 함수는 지정된 this를 가지고 있게 된다.
const person = {
name: "um",
age: 20
};
function introduce (city, country) {
console.log(`my name is ${this.name}, i live in ${city}, ${country}`);
}
const setIntroduce = introduce.bind(person);
setIntroduce("seoul", "korea"); // `my name is um, i live in seoul, korea`
call, apply, bind 차이점
call과 apply, bind는 객체의 메서드로 만들어진 함수가 아니더라도
특정 객체를 this로 바인딩할 수 있게 하는 것에는 공통점이 있다.
그러나 call과 apply는 함수를 호출하는 시점에 this를 바인딩하지만,
bind는 함수를 생성하는 시점에 this를 바인딩한다.
이러한 차이를 분명히 알고 상황에 맞게 해당 메서드들을 사용하면 되겠다.
함께 읽으면 좋은 글 리스트
실행 컨텍스트 : 자 이게 너가 사용할 수 있는 친구들이야
포스팅 작성에 참고한 감사한 글들
슥짱 님의 블로그 : this : 이름 값 못하는 자바스크립트의 this
F-Lab 블로그 : 자바스크립트의 this 키워드 완전 정복
'Stack > JS' 카테고리의 다른 글
데이터 타입 | null과 undefined : 코어 자바스크립트 씹뜯맛즐 하기 (1) | 2024.11.21 |
---|---|
데이터 타입 | 기본형과 참조형 : 코어 자바스크립트 씹뜯맛즐 하기 (2) | 2024.11.20 |
클로저 (Closure) : 자바스크립트의 네크로멘서 (3) | 2024.11.11 |
호이스팅 (Hoisting) : 변수, 함수 끌어~ 올려! (9) | 2024.11.09 |
실행 컨텍스트 : 자 이게 너가 사용할 수 있는 친구들이야 (6) | 2024.11.08 |