일반적으로 자바스크립트는 렉시컬 스코프를 따릅니다.
요새 스터디하면서 많은 스터디원분들께서 렉시컬 스코프라는 것을 자주 헷갈려하시는 것 같아요.
우리, 막 외우지 말고 하나하나 차근히 살펴봅시다.
렉시컬하다라는 것은 무엇일까요?
일반적으로 컴파일에서 쪼개진 토큰들의 의미를 분석하는 단계를 렉싱한다라고 표현해요.
뭔가 코난처럼 뇌리에 스치지 않나요?
몰랐었다면 뭔가 자신의 언어로는 완전히 형성이 안 되었지만, 뭔가 몽글몽글한 몇 개의 단어들이 떠오를 것 같아요.
좀 더 자신만의 언어로 해석하기 위해 설명을 보태볼게요!
의미 분석은 렉싱... 그렇다면 렉시컬 스코프는 뭘까요?
어떤 선언된 변수들의 의미를 분석하는 데 도움을 주는 스코프... 이런 느낌을 준다는 거겠죠?!
우리, 스코프가 뭐였죠?
제가 별도로 만들었던 8-1장에서 바로 변수가 사는 지역의 범위를 지켜주는 울타리 같은 거라고 했어요.
결국 렉시컬 스코프는 이해하면 별 게 없어요.
일반적으로 자바스크립트의 변수는 단지 선언된 곳을 기준으로 자신의 유효 범위를 결정짓는 데, 이것이 자신이 속한 스코프의 영역에 영향을 받는다~해서 자바스크립트의 대부분의 변수는 렉시컬 스코프를 따른다고 하는 거에요. 🥰
이 얘기를 왜 하냐구요?
this
는 일반적인 변수가 아니기 때문입니다. 크흡.
핵심은 this
는 동적 스코프를 따른다는 겁니다.
이는 이전의 17장인 생성자 함수 글에서도 설명이 되었던 거군요!
const name = 'jaeyoung';
function getName() {
return this.name;
}
const person = {
name: 'sunyoung',
getName
}
console.log(person.getName()) // sunyoung
위의 내용이 이상하지 않나요? 그렇다면 정말 제대로 공부하신 게 맞아요! 😉
위의 결과가 어색한 이유는 다음과 같습니다.
this
const name = 'jaeyoung'; // 전역에서 선언했군!
function getName() { //오, 전역에서 함수를 선언했네!
return this.name; // 그러면 name도 선언한 위치가 전역이니, jaeyoung이겠구만!
}
const person = {
name: 'sunyoung',
getName
} // 객체를 선언했고, name은 sunyoung이고, 메서드는 getName의 참조 값을 할당했군!
console.log(person.getName())
// 응? 그러면 getName의 값은 jaeyoung인데, 왜 sunyoung이 나오지...?
이런 뇌의 흐름으로 인해, 어색한 게 정상이에요.
이러한 결과가 나온다는 것은 무엇일까요? 바로 this
는 렉시컬 스코프를 따르지 않는다는 거에요. 바로 호출한 시점에 동적으로 결정짓는다 해서 동적 스코프를 따른다고 합니다.
😖 잠깐, 우리 머리 아픈데... 이거 외워야 할까요...?
저는 최대한 막 외우기보다는, 덜 헷갈리게 이해하려 노력하는 편이에요.
우리 이참에 한 번 this
랑 친해져봐요 🙇🏻♂️
this
라는 용어는 흔히들 자기 참조 변수로 말하고는 해요.
저는 항상 말의 뉘앙스를 정말 중요시 여겨요.
따라서 이 용어를 처음 들었을 때, 이런 의구심으로 엄청 고민했어요.
🤤 도대체, 자기를 참조한다는 건, 무슨 뜻일까...?
(a.k.a. 참조한다는 건 뭔지 몰라도... 좋은 거야... 너가 참조되었다면 나도 좋아... 🙃)
음, 저는 이 모든 원흉(?!)이 생성자 함수로부터 비롯되었다고 생각했어요.
생성자 함수에서는 암묵적으로 빈 객체를 생성하고, 나중에 이를 인스턴스의 값으로 반환합니다.
그런데, 이를 설명할 게 없는 거에요. 그래서 너 값이 뭔데?!라고 묻는데, 뭔가 모르는 객체라고 말하기가 어렵잖아요?
그래서 constructor
한 생성자 함수로 인해 자기 자신의 값을 참조하고 나타내야 할 변수가 필요하게 된 거죠.
저는 그것을
this
라고 정의하기로 했습니다!
적어도 자신이 어떤 객체인지를 표현해야 하기 위해서 나타난 친구라고 말이죠!!
this
뭔가 그렇게 좀 이해를 하다 보니까, 이제는 짜증나기보다는, 너무 재밌더라구요.
뭔가 저는 이런 스터디가 마치 계단을 오르는 것 같아요.
정말 죽을듯이 짜증나는데, 막힌 게 뚫리면 엄청난 쾌감이... 🥰
자, 그러면 우리 결국 생성자 함수에서 this
라는 친구가 나왔다고 치자구요.
그러면 이로써 얻을 수 있는 게 무엇일까요?
생성자 함수의 가장 큰 장점은 바로 반복해서 재사용할 수 있다는 점이었죠?!
왜냐! this
라는 요 객체는 생성자 함수 내에서 암묵적으로 "빈 객체로 생성된 것"을 값으로 가진다고 했죠? (이것은 리터럴로 생성할 때도 추상연산으로 인해 빈 객체로 생성됩니다!)
따라서 인스턴스들의 값은 독립적이며, 겹치지 않게 되는 거에요.
참조가 아니라, 아예 새롭게 생성된 객체 값이니까요 😉
💡 잠깐! 그런데 값이 하나하나씩 나온다는 건 엄청난 손해 아니에요?
🎉 정답입니다! 그래서 있는 친구가 바로 프로토타입이죠. 마치 다같이 유전자처럼 같은 값을 공유하는 거에요! 점점 자바스크립트가 재밌지 않나요 😉
이것이 왜 this
가 좋은 친구라는 근거냐구요?
this
는 반복해서 재사용할 수 있는 데 최적화 되어있기 때문입니다.
그냥
this
가 우리에게 주는 뉘앙스는, 거, 잘 모르니까 고거 써!의 느낌이에요.
this
가 없다면 어떻게 될까요?가장 큰 문제. 생성자 함수의 인스턴스 값을 정확히 설명하지 못하겠죠. 😭
사실, 이것말고도 문제는 따지자면 많을 수 있어요.
저도 생각을 쭉 해봤는데요. 약간 억까(?)스러울 수 있지만 this
가 없다면, 우선 하나하나 호출한 곳을 명시해야 하므로 쓸데 없이 코드가 늘어날 수 있겠네요. 또한, 오히려 헷갈리는 요소들이 등장할 수 있겠어요.
아까 썼던 위의 함수를 예시로 들어볼까요?
function getName() {
console.log(this.name)
};
const person = { name: 'jaeyoung', getName }
이 친구는 this
로 인해 편하게 정의하고, 호출이 가능했어요.
그런데 만약 this
가 없다면 어떻게 될까요? 지금은 괜찮지만, 이를 참조할 객체가 많아진다면 번잡해집니다.
const person1 = { name: 'jaeyoung', getName: function () {
console.log(person1.name)
}}
const person2 = { name: 'jaeyoung2', getName: function () {
console.log(person2.name)
}}
const person2 = { name: 'jaeyoung3', getName: function () {
console.log(person3.name)
}}
console.log(person1.getName()) //
우와... 잠깐 예시로 쓰기만 해도 진짜 불편하네요... 😭
결과적으로 기존 메서드처럼 인수없이도 정상적으로 호출되지만, 호출한 객체를 function
에 하나하나 써야 한다는 점이 굉장히 충격이군요!
😮 잠깐! 이를 파라미터로 반복해서 쓰면 어떨까요?!
한 번 이를 적용해볼까요?
function getName(target) {
console.log(target.name)
};
const person1 = { name: 'jaeyoung', getName }
const person2 = { name: 'jaeyoung2', getName }
const person3 = { name: 'jaeyoung3', getName }
console.log(person1.getName(person1)) // jaeyoung - 자기가 자기를 호출한다니... 어색한 걸?
console.log(person2.getName(person3)) // jaeyoung3 - 잠깐... 너가 남의 이름을 호출하면 어떻게 해!
이런 웃픈(?) 상황이 벌어지게 됩니다.
결과적으로 그냥 this
는 재사용성을 위해 엄청난 특화된 능력을 가진 친구에요.
우리가 굳이 설명하지 않아도, 아, 고거 있잖아, 고거 불러봐!하는 식으로 써주는 거죠 🙆🏻
그리고 가장 중요한 거!
그 '고거'는 생성자 함수로부터 나온, 암묵적으로 할당된 빈 객체를 기준으로 한다는 거죠~ 🎉
this
는 전역 객체로 바인딩 된다.우리, 전역 객체는 기본적으로 전역 프로퍼티와 전역 함수들을 담는 공간을 하는 객체에요.
즉, 브라우저 환경에서 우리가 전역 스코프에서 변수와 함수를 선언한다는 것은,
Window
라는 생성자 함수의 객체 속에 프로퍼티와 메서드를 추가한다는 것과 같아요.
function sayHello() {
console.log('Hello!');
}
window.sayHello(); // Hello!
그러니까 결국 this
를 일반 함수에서 호출한다는 것은
Window
있잖아! 걔 불러~하는 거죠. 굉장히 this
, 알고 보면 구수한 친구죠? 🤣
this
바인딩의 문제점다만, 이러한 전역 객체로 바인딩된다는 것이 ES3
에 나온 new
연산자를 까먹기만 해도 바인딩이 잘못되어 큰 오류가 발생할 수 있는 문제가 발생했어요.
당시에는 클래스와 같은 문법도 존재하지 않았기에, 이는 정말 골치 아픈 문제였겠죠?!
(우리가 아는 생성자 함수 방어코드의 대명사인 new.target
도 ES6
부터 지원되었답니다!)
따라서 이러한 전역 객체로 mapping되는 this
가 골치 아파서, ECMAScript 5
부터는 use strict mode
로, 전역 객체의 this
바인딩이 곧 undefined
로 나올 수 있도록 했어요. 🥰
"use strict"
console.log(this) // undefined
apply/call/bind
로 this
핸들링하기그런데, 가끔은 이러한 this
가 헷갈릴 때도 있기도 하고, 무엇보다 원하지 않는 객체에 바인딩되어 있을 수도 있겠죠!
그런 경우에 대안으로 사용하는 것이 위의 세 메서드입니다 :)
세 메서드는 Function
의 프로토타입 객체에 있는 메서드라, 모든 함수가 다같이 쓸 수 있답니다 😉
Function.prototype.apply
apply
는 함수 호출에 목적을 두고 있어요.
따라서 다음과 같이 인자들을 배열로 묶어서 전달하여, 바인딩을 동적으로 시도합니다!
function getThis(age, address) {
this.age = age;
this.address = address;
return this;
}
const obj = { name: 'jaeyoung' };
console.log(getThis.apply(obj, [29, 'Seoul']));
// {name: 'jaeyoung', age: 29, address: 'Seoul'}
이 친구도 함수 호출에 목적을 두고 있는데, 차이점이라면 배열이 아닌 인자들을 열거해서 전달합니다.
function getThis(age, address) {
this.age = age;
this.address = address;
return this;
}
const obj = { name: 'jaeyoung' };
console.log(getThis.call(obj, 29, 'Seoul'));
// {name: 'jaeyoung', age: 29, address: 'Seoul'}
이 친구는 좀 다른데요!
apply
와 call
이 함수 호출에 목적을 두었다면 이 친구는 순수 this
바인딩에 목적을 둡니다.
따라서 함수를 호출하지 않기에, 명시적으로 호출을 해주어야 합니다!
function getThis(age, address) {
this.age = age;
this.address = address;
return this;
}
const obj = { name: 'jaeyoung' };
console.log(getThis.bind(obj)(29, 'Seoul'));
// {name: 'jaeyoung', age: 29, address: 'Seoul'}
차이를 이해하셨나요?
apply
와 call
이 다르고,this
바인딩에만 목적을 두었다면 bind
를 사용하면 된답니다 🥰후, 말은 하기 쉬운데... 글쓰기가 정말 힘들군요!
결국 정리하자면, 우리는 이 글을 통해 다음을 알 수 있겠군요 🙇🏻♂️
this
는 렉시컬 스코프가 아닌, 동적 스코프를 따른다. 풀어서 설명하면 동적으로 자신을 호출한 객체(자신을 불렀던 녀석)를 참조하는 자기 참조 변수이다!
2.this
는, 결국 재사용성을 위해 반드시 필요한 친구다. (a.k.a. 그거 있잖아 그거...!)- 전역에서 호출된
this
는 전역 객체에 바인딩되는데,strict mode
에서는undefined
로 나타난다.apply/call/bind
를 통해 동적으로 바인딩을 우리가 해줄 수도 있다!
생각보다 많은 것들을 짚어나갔네요.
어떻게 보면 제 추측이 90% 들어간 글이긴 합니다! 만약 틀린 부분이 있으면 말씀해주세요 😭
그래도 이 글이, 누군가에게는 이해하는 데 꼭 도움이 되었으면 좋겠어요. 이상! 🌈
모던 자바스크립트 Deep Dive - 17장
모던 자바스크립트 Deep Dive - 22장
TCP SCHOOL - 전역 객체
스택 오버 플로우 - 왜 strict mode에서는 전역 this가 undefined인지