이미지 출처 : https://www.nextptr.com/question/a64969404/the-this-is-bound-to-enclosing-context-in-a-javascript-arrow-function
참고 및 소스 출처 : 모던 자바스크립트 Deep Dive: 자바스크립트의 기본개념과 동작원리(480~482),리액트를 다루는 기술(개정판) p70, 노트 ES6의 화살표 함수
화살표 함수의 this를 이해하는 논리
: 렉시컬 스코프(정적 스코프)
코드에서 화살표 함수의 this를 이해
: 코드를 통해서 화살표 함수 this가 무엇을 참조하는지 알아볼 수 있습니다.
: [코드1],[코드2],[코드3],[코드4],[코드5],
[코드6] : ‘일반함수는 자신이 종속된 객체를 this로 가리키며, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.’
Q: 어떻게 화살표 함수가 나왔을까?
A: 콜백함수의 this를 맞춰줄 목적으로 나오지 않았을까요.
화살표 함수는 일반함수, 즉 콜백함수가 호출될 때, 이 콜백함수의 this와 , 이 콜백함수를 호출하는 this의 불일치를 해결해 줍니다. bind(..)나 call/apply와 같이 특정 this를 주입해줘서 콜백함수의 this를 일치시킬 수도 있지만, 화살표 함수를 통해 그런 문제를 해결 할 수 있습니다.
다음의 코드를 확인해봅시다.
class Prefixer{
constructor(prefix){
this.prefix=prefix;
}
add(arr){
//여기의 this는 클래스 Prefixer로 만든 인스턴스를 참조합니다.
return arr.map(function(item){
//여기의 this는 무엇을 참조할까요??
return this.prefix + item
});
}
}
const prefixer = new Prefixer('-webkit-');
console.log(prefixer.add(['transition', 'user-select']))
클래스 Prefixer는 결론적으로 ‘-webkit-transition’ , ‘-webkit-user-select’ 텍스트를 만듭니다.
배열 ['transition', 'user-select’]을 add함수의 인자로 전달하면, 생성한 인스턴스(prefixer)가 배열 아이템 하나하나를 map(매핑)하여 각각 텍스트 ‘-webkit-transition’ , ‘-webkit-user-select’ 출력합니다.
위 코드를 실행하면 다음과 같은 에러가 발생합니다.
source.js:10 Uncaught TypeError: Cannot read properties of undefined (reading 'prefix')
at source.js:10
at Array.map anonymous
at Prefixer.add (source.js:8)
at source.js:17
function(item){….}에서
’prefix’ 가 this에 없다는 의미입니다.
function(item){….}안에서 하나씩 따져봅시다
this.prefix
여기서 this는 클래스 Prefixer가 만드는 인스턴스( const prefixer) 입니다. 왜냐하면 클래스의 메서드 안에 정의된 this는 해당 클래스(Prefixer)가 만든 인스턴스 이기 때문입니다.
const prefixer = new Prefixer('-webkit-');
constructor(prefix){….}의 인자로 ‘-webkit-‘ 를 전달했습니다. 전달된 이것은 construtor 매개변수 prefix 인자로 들어가 prefix라는 프로퍼티에 값이 바인딩되었습니다.
즉. 인자로 넣은 값은 prefix라는 프로퍼티에는 ‘-webkit-‘ 값이 바인딩되었습니다.
그래서 우리는 this.prefix를 통해서 ‘-webkit-‘ 라는 값을 구할 것이라는 생각을 갖습니다.
그러나!
map이 호출하는 콜백함수 function(){…}안에서는 this.prefix가 원하는 값을 출력하지 않았습니다.
왜 그럴까요?
그건, 콜백함수(일반함수)가 호출될때, 그 안에 정의된 this는 window를 참조하기 때문입니다.
다시 말해,
일반함수의 호출이 되어, 함수안의 this는 우리가 생각하는 클래스 Prefixer의 인스턴스 prefixer가 아니라 “window” 를 참조 하게 됩니다.
콜백함수는 콜백함수 자신을 호출해주는 또 다른 함수가 있습니다.
즉. 함수가 함수를 호출해주는 방식인거죠
여기서 호출해주는 함수는 map이고, map이 콜백함수인 function(item){…} 을 호출해 주는 겁니다.
각각의 함수는 서로 영향을 주지 않고, 그냥 자신의 역할을 충실히 이행합니다.
map은 function(item){…} 가 내부 어떻게 작동되는지 관심이 없습니다.
즉,
function(item){…}안의 this를 클래스(Prefixer)가 만든 인스턴스(prefixer)를 가리키도록 ‘어떤 조치’를 해야합니다.
여기서 조치한 서로 참조하는 this가 동일한 객체를 바라보도록 해야하는 그것입니다.
조치내용은 다음과 같습니다
1)함수에 bind(객체)로 새로운 함수 반환하기,
2)함수(add) 안에 const that = this; 를 정의하고, const that를 function(item){…}안에서 that 사용하기
3)map함수의 마지막 인자로 this 넘겨주기
이와같은 방법을 사용한다면
function(item){…} 안에서 this는 클래스의 인스턴스를 참조할 것입니다.
그런데, 이렇게 해야하는거 좀 까다롭지 복잡하지 않나요?
그래서 화살표 함수가 등장했습니다.
Q: 화살표 함수에서 this는 어떻게 파악해야 할까요?
A: 화살표 함수의 this, 그 this가 정의된 스코프를 어디인지를 확인해주세요!
앞의 코드에서 function일반 함수가 아닌 =>화살표함수로 형태를 바꿈으로 this가 참조값 문제를 해결할 수 있습니다.
그 이유는 무엇일까요?
그건,
화살표 함수 this 특징이 갖는 “스코프” 때문입니다.
여기서 스코프는 “정적 스코프(렉시컬 스코프)’ 를 말합니다.
갑자기 왜 화살표 함수와 스코프 일까요?
개인적인 추측이지만,
this를 맞추기 위한 일련의 조치들(const that = this를 해서 that 변수활용, 함수인자로 this 를 넘겨주는 것)은
‘다른 곳(영역)의’ this값을 ‘현재(영역)의’ this값으로 할당하는 것입니다.
즉, 스코프를 맞춰주는 의도가 깔려있어 보입니다.
그래서, 혹시 스코프가 this가 서로 연결된 개념일까 찾아봤는데, 서로 관련된 그것은 아니라 합니다.
다시 돌아와,
화살표 함수의 this는 스코프 라는 개념을 바탕으로 this문제를 해결하고, this를 사용합니다.
그 this는 this 자신이 정의된 위치를 알아야 하고, 그 상위 스코프의 this를 함수의 this로 사용합니다.
Q: 화살표 함수의 this는 무엇인가요?
A: 화살표 함수의 this는 상위 스코프의 this를 자신의 this로 사용합니다.
‘화살표 함수의 this는, 함수 자신이 ‘정의된 스코프의 그 상위 스코프”의 this를 ‘ 화살표 함수 자신의 this로 사용합니다’
함수가 어디에 정의 되어있느냐,
그리고
그 위치에서 상위는 어디이냐 를 파악해야 합니다.
즉,
1)내가 정의된 스코프가 어디인지 아는 것,
2)그리고 그 상위 스코프를 아는 것
3)그래서 그 상위 스코프의 this를 아는 것,
이 세가지를 이해하는 것이 화살표 함수의 this 값을 이해하는 방법입니다.
화살표 함수에서 사용하는 스코프는 정적 스코프(렉시컬 스코프) 입니다.
화살표 함수의 this코드
화살표 함수의 this의 값은, this가 정의된 스코프의 상위(스코프) this를 참조합니다.
코드를 보면서 차근차근 이해해봅시다
총 5개 코드를 다뤄봅니다
[#코드1]
(function(){
const foo = ()=> console.log(this)
foo()
}).call({a:1}) // {a:1}
상위 스코프가 어디인지 이해하기 위해, 화살표 함수 부분을 일반함수로 바꿔봅니다
(function(){
//<= 여기는 foo의 상위 스코프다
//즉, 화살표 함수의 this는 즉시실행함수 그것이다.
const foo = function(){
//여기부터
console.log(this)
//여기까지 정의된 this의 유효범위
//this의 유효범위(스코프)는 foo 안이다
}
foo()
}).call({a:1})
다시말해,
(function(){
//<= 화살표함수 foo의 this는 즉시실행함수의 this를 참조한다
const foo = ()=> console.log(this)//{a:1}
foo()
}).call({a:1})
[#코드2]
const foo = ()=> console.log(this)
foo();
다시말해,
//<= 여기는 foo의 상위 스코프다
//foo는 전역에 정의되어있다.
//foo의 상위 스코프는 전역이다
//전역의 상위 스코프는 전역이다
const foo = function(){
//this는 foo에 설정되어 있다.
console.log(this) // window
}
foo();
화살표 함수 foo는 전역 스코프의 선언되어 있는데, 그 상위 스코프는 사실 전역이라고 할 수 있다.
전역 스코프 위에는 스코프가 존재하지 않기 때문이다.
[#코드3]
const counter = {
num : 1,
increase:()=> ++this.sum
}
//화살표 함수 increase의 상위스코프는 전역이다
//counter 객체는 전역에 선언되어 있다
const counter = {
num : 1,
increase:function(){
//this는 어디에 포함 되었다기 보단
//this는 counter의 속성이다.
return ++this.name
}
}
//스코프를 위계구조로 표현할 때,
//화살표함수 increase는 counter함수와 동등한 위치다(속성이기 때문)
//즉, increase의 상위는 곧 counter의 상위라고 할 수 있다.
//counter 상위 스코프는 전역임으로 화살표 함수increase의 this는 window를 참조한다.
const counter = {
num : 1,
increase:()=> ++this.sum //this는 window
}
[#코드4]
Person.prototype.sayHi = ()=> console.log(this)
Person.prototype.sayHi = function(){
//sayHI는 곧 function(){...}이다.
//즉.스코프를 위계구조로 볼때 sayHi와 console.log(this)라는 동등하다
//아니, 하나의 함수의 형태다
//하나의 함수가 전역에 선언되어 있다.
return console.log(this)
}
//화살표함수 sayHi안의 this는 상위 스코프의 this를 참조한다
//상위 스코프는 전역이다
//화살표함수 sayHi안의 this는 window를 참조한다.
Person.prototype.sayHi = ()=> console.log(this)
[#코드5]
const a = function(){
const b = ()=> console.log(this)
}
const a = function(){
//화살표 함수 b의 상위 스코프
//화살표 함수 b의 this
const b = function(){
//this가 선언된 위치
//this의 유효범위
console.log(this)
}
}
const a = function(){
//화살표 함수 b의 상위 스코프
//화살표 함수 b의 this
const b = ()=> console.log(this)
}
[#코드6]
[!! 해당 코드들이 어떤 의도/의미에서 작동되는지 코드설명은 찾아볼 수 없었지만, 개인적인 추측을 통해 적어봅니다]
‘일반함수는 자신이 종속된 객체를 this로 가리키며, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.’
전자의 의미는 객체 안에서 일반함수가 호출되는 경우를,
후자는 화살표 함수가 스코프라는 기본개념에서 무엇을 참조하고 있는 나타나는 경우라 생각합니다.
‘일반함수는 자신이 종속된 객체를 this로 가리키며, …중략…’
‘…중략…, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.’
```javascript
function BlackDog(){
this.name = '흰둥이'
return{
** name:'검둥이',**
bark:function(){
console.log(this.name + ': 멍멍');
}
}
}
const blackDog = new BlackDog();
console.log(blackDog.bark()) //검둥이: 멍멍
function WhiteDog(){
** this.name='흰둥이'**
return{
name:'검둥이',
bark:()=>{
console.log(this.name + ': 멍멍')
}
}
}
const whiteDog = new WhiteDog()
console.log(whiteDog.bark()) //흰둥이: 멍멍
위 코드들을 통해 다음을 배울 수 있습니다.
1) 클래스에서 return
2) 일반함수와 화살표 함수의 this차이
1) 클래스에서 return
흥미로운 점은 각 함수(BlackDog, WhiteDog)가 new연산자로 만든 인스턴스가
일반함수냐 화살표함수냐 차이만 있을 뿐 , 출력되는 결과는 동일합니다.
console.log("blackdog에서 return ", blackDog)
// {name: '검둥이', bark: ƒ}
// bark: ƒ ()
// name: "검둥이"
// [[Prototype]]: Object
console.log("whiteDog에서 return ", whiteDog)
// {name: '검둥이', bark: ƒ}
// bark: ()=>{ console.log(this.name + ': 멍멍') }
// name: "검둥이"
// [[Prototype]]: Object
두 클래스가 갖는 값이 동일한 이유는 무엇 일까요?
그 이유는 return 키워드 떄문이라고 생각합니다.
클래스는 new 연산자를 통해 ‘암묵적으로’ 빈 인스턴스를 생성하고, 거기에 this.프로퍼티 형태로 비어있는 인스턴스에 값을 바인딩합니다.
그런데 return을 ‘의도적으로’ 넣을 경우, 클래스는 사용자가 정의한 return내용을 인스턴스로 반환합니다.
생성된 인스턴스인
blackDog과,
whiteDog는 해당 클래스에서 정의한 return{…} 부분을 출력합니다.
두 클래스 모두, return이하의 값이 동일합니다.
단, 함수가 일반함수냐 화살표 함수냐의 차이는 다릅니다.
‘일반함수는 자신이 종속된 객체를 this로 가리키며, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.’
function BlackDog(){
this.name = '흰둥이'
return{
name:'검둥이',
bark:function(){
console.log(this.name + ': 멍멍'); //검둥이: 멍멍
}
}
}
const blackDog = new BlackDog();
console.log(blackDog.bark()) //검둥이: 멍멍
blackDog는 {name:’검둥이’, bark:function(){…}} 입니다.
이때 blackDog에서 blackDog의 정의된 함수 bark를 호출합니다.
이때 blackDog는 객체입니다(객체의 형태죠)
객체가 함수(일반함수)를 호출할때, 그 함수안에 정의된 this는 함수 앞의 객체입니다.
즉. this.name에서 name은 {name:’검둥이’, bark:function(){…}} 의 name, ‘검둥이’를 참조합니다.
그래서
console.log(blackDog.bark()) //검둥이: 멍멍
입니다.
‘일반함수는 자신이 종속된 객체를 this로 가리키며, …중략…’ 은
이제, 왜 이렇게 말했을까요??
bark인 일반함수(function 형태이기 때문에 일반함수라고 하지 않았을까,)는
함수 자신이 속한 객체({name:..bark:function(){..}}안에 bark가 있으니…)가
자신을 호출했기 때문에, 자신 안에 정의된 this가 객체를 참조합니다.
즉. blackDog.bark()는 객체.함수()의 형태이기 때문에, 함수안의 this는 객체를 참조합니다.
다시 말하면,
bark인 일반함수는 함수 자신이 포함된(종속된) 객체(name:…, bark…)를 this로 가리킵니다
일반함수의 this 호출형태 중, 객체일 경우를 말하는 것 같습니다.
‘…중략…, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.’
function WhiteDog(){
this.name='흰둥이'
return{
name:'검둥이',
bark:()=>{
console.log(this.name + ': 멍멍')
}
}
}
const whiteDog = new WhiteDog()
console.log(whiteDog.bark()) // 흰둥이 : 멍멍
우선 유념해둘 사항이 있습니다.
1) whiteDog은 blackDog과 결괏값이 같다 // return{name:…, bark:…}
2) return{…}에서 return의 {..}은 스코프의 개념을 갖지 않는다.
3) 자바스크립트에서 function WhiteDog(){…}은 생성자로 클래스 역할을 한다. 즉, 인스턴스를 만드는 틀 역할을 한다는 것이다. ES5에서 function으로 한것이 ES6에서는 class로 변경됐다.
whiteDog과 blackDog, 둘다, 갖는 결괏값이 같은데
왜, 하나는 인스턴스를 다른 하나는 객체를 말하는 걸까요??
그것은
화살표 함수 this의 특징이기 때문입니다.
whiteDog로 화살표 함수 bark를 호출 할 때,
화살표 함수 bark의 this는 자신의 값을 찾습니다.
찾는 방법은 이겁니다.
‘자신이 정의된 스코프를 아는 것,’
그리고
‘그 스코프의 상위 this가 화살표 함수의 this’ 라는 것 입니다.
그럼,
화살표 함수 bark가 정의된 스코프는 어디일까요?
제 추측이지만,
코드
function WhiteDog(){
this.name='흰둥이'
return{
name:'검둥이',
bark:()=>{
console.log(this.name + ': 멍멍')
}
}
}
는 사실 이것이 아닐까 싶습니다.
function WhiteDog(){
this.name='흰둥이'
//return 부분이 삭제됨
name:'검둥이',
bark:()=>{
console.log(this.name + ': 멍멍')
}
}
그리고
정의된,
화살표 함수 bark의 렉시컬 스코프는 함수(클래스) WhiteDog() 안 입니다.
function WhiteDog(){
//화살표 함수 bark의 유효 범위(스코프)는
//여기부터
this.name='흰둥이'
name:'검둥이'
bark:()=>{
console.log(this.name + ': 멍멍')
}
//여기까지
}
렉시컬 스코프가 함수(클래스) WhiteDog안 이면,
이것의 상위 스코프는
함수(클래스) WhiteDog 클래스 외부를 가리킵니다.
그럼 전역, 화살표 함수 bark의 this는 window란 일까요?
아닙니다
모던 자바스크립트 Deep Dive: 자바스크립트의 기본개념과 동작원리,p485 [예제 26-45]를 통해 힌트를 얻었습니다.
‘클래스 필드에 할당한 화살표 함수 상위 스코프는 사실 클래스 외부다. 하지만 this는 클래스 외부의 this를 참조하지 않고 클래스가 생성할 인스턴스를 참조한다. 따라서 ..클래스 필드에 할당한 화살표 한수내부에서 참조한 this는 constructor 내부의 this 바인딩과 같다’
정리하면,
이제 화살표 함수 bark 코드는 클래스(함수) WhiteDog 바로 안에 위치합니다. (return{..}에서 {..}은 스코프 개념이 없기 떄문에, 스코프 관점에서 없어도 되는 것이라 삭제했습니다.) 즉 렉시컬 스코프가 WhiteDog 입니다.
화살표 bark 함수안의 this는 상위 스코프인 클래스(함수) WhiteDog() 외부를 참조 할 것 같지만
외부를 참조하지 않고, constructor(생성자 함수)안에 정의된 인스턴스를 this를 가리킨다는 겁니다.
결론적으로
화살표 함수 bark안에 this는 인스턴스를 참조한다는 것입니다.
this.name='흰둥이'
이때,
this.name='흰둥이' 코드가 있습니다.
(추측인데 혹은)
this.name=“흰둥이” 에서 this는 인스턴스를 의미합니다.
화살표 함수 bark안에 this로 name값을 찾고 있습니다(this.name)
이미 this.name = ‘흰둥이’ 로
인스턴스(this)에 name을 ‘흰둥이’로 값을 바인딩 했습니다.
그래서,
화살표 함수 bark는 ‘흰둥이’를 출력합니다.
‘…중략…, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.’
사실 화살표 함수’는’ 인스턴스만을 가리키는 것은 아니라 생각합니다. 상위 스코프를 참조한다는 기본 원칙에서 인스턴스를 참조하게 된, 어떤 경우인것이지 일반화 된 원칙은 아니라 생각합니다.