[JS/this/호출스택] JS의 This 정리 (호출스택,This)
이제 JavaScript에 대해서 조금 깊게 다루어볼까 합니다. JS를 다룬지 벌써 꽤 되었습니다. 하지만, 여전히 익숙하지 않은 부분이 많고, "JS"만의 특징들이 몇개 있습니다. 그 점을 다루어보려고 합니다! JavaScript에서는 함수의 매개변수로 함수를 넘길 수 있다는 점입니다. 심지어, "함수"그 자체가 변수가 되기도 하였습니다.
JS에서 함수(Function)은 1급 객체이다.
- 자바스크립트에서 함수는 1급 객체입니다. 함수가 "값"처럼 취급이 됩니다. 대표적인 형태가 callback 함수입니다.
- "값"처럼 취급 된다는 이야기는, JavaScript에서는 함수를 변수로 담을 수 있고, 함수를 매개변수로 전달도 할 수 있다는 이야기입니다.
const calc = (callback,a,b) => callback(a,b) //함수 자체를 변수로 담을 수 있다.
//함수 자체를 매개변수로 사용할 수 있습니다.
const adder = calc((a,b)=>a+b,3,4)
const minus = calc((a,b)=>a-b,3,4)
const multi = calc((a,b) =>a*b,3,4)
console.log(adder)
console.log(minus)
console.log(multi)
JS에서 함수의 "정의"와 "호출"은 엄격하게 분리된다.
- JS에서 함수가 "값"으로 쓰일 수 있기 때문에, 함수의 정의와 함수를 호출하는 개념을 철저하게 분리되어 있습니다.
함수의 정의
- function a(){} , arrow Function등의 방법을 사용해서 , 함수를 "정의"하는 행위입니다. 단순히 로직을 정의할 뿐입니다.
함수의 호출
- 함수를 eval (평가), 실행하겠다는 의미입니다. 즉, return 값이 발생하게 되고, 함수 내부의 로직이 실행되게 됩니다.
아래의 코드를 보면서, 정의부분과 호출부분을 구분해봅시다.
//a의 정의
function a(){
console.log("a")
//b의 정의
function b(){
console.log("b")
}
//function b call
b()
}
// c의 정의
function c(){
console.log("c")
a()
}
c() // function c call
함수의 호출과 정의가 중요한 이유
사실, 함수를 호출하는 과정과 함수를 정의하는 과정을 나누는 것은 너무 당연한 이야기라서, 신경을 쓰지 않는 부분입니다. (숨을 의식적으로 쉬거나, 눈을 의식적으로 깜박이지는 않습니다)
하지만, 이 부분이 JS에서 중요한 이유는 callback의 개념과 "this"가 함수가 호출될 때 결정되기 때문입니다.
callback 함수
stackover flow에 아주 직관적인 질문이 있어서 모셔왔습니다. 질문도 심플합니다. what is a callback function?
https://stackoverflow.com/questions/824234/what-is-a-callback-function
747개의 up을 받은 답변은 아래와 같이 설명합니다.
- accessible by another function and,
- in invoked after the first function if that first function completes
한국어로 하자면, 다른 함수에서 접근이 가능한 함수 그리고, 첫번째 함수가 실행된 뒤에 실행되는 함수로 정리가능합니다. 코드로 보죠! 진짜 일까요?
// ....
const sayHi = () =>{
console.log("Hi!")
}
rootEl.addEventListener("click",sayHi)
//
sayHi는 addEventListener가 접근할 수 있는 함수입니다. O
sayHi는 addEventListener가 끝나고 나서, 그 후에 어떤 행위를 할지 나타내는 함수입니다. O
이 callback 함수에는 함수의 "호출"이 아니라, 함수의 "정의"가 들어가야 합니다.
This의 결정
- this는 기본적으로 "window" , strict를 썻다면 "undefined입니다.
- This는 함수가 "호출"되어질 때, 정해집니다. 그리고, this를 결정하는건 normal Function일 떄는 3가지 경우가 있습니다.
This를 결정하는 3가지
- obj.method 형태로 호출하면, this는 해당 객체를 가리키게 됩니다.
- 일반적으로 , 객체안의 this는 자기자신을 가리킬 때, 씁니다. 하지만 객체안의 this가 무조건적으로 자기 자신의 객체를 가리키진 않습니다.
cons obj = {
msg:"hello",
sayHi(){
console.log(this.msg)
}
}
obj.sayHi();
이게 무슨 말이냐면, this는 결국 "호출"시점에 따라 결정이 되므로 , 아래와 같이 코드가 작성되면, 객체안의 this는 자신을 가리킨다는 말이 무산됩니다.
const obj = {
msg:"hello",
sayHi(){
console.log(this.msg)
}
}
const saying = obj.sayHi
saying() //undefiend!
//saying() 함수를 호출할 때, this에 대한 처리를 하지 않았다.
new 연산자
- new 연산자로 할당하면, this는 해당 객체에 bind되게 됩니다.
function Obj(msg){
this.msg =msg;
this.sayHi = function(){
console.log(this.msg);
}
}
let saying = new Obj("hello");
saying.sayHi()
bind,call,apply
- bind,call,apply는 모두 "this를 내가 지정한걸로 바꾸겠다"는 메서드 입니다. obj.method.bind(obj)
- 아래와 같은 상황에서 bind를 사용할 수 있습니다.
function askPassword(ok, fail) {
let password = prompt("비밀번호를 입력해주세요.", '');
if (password == "rockstar") ok();
else fail();
}
let user = {
name: 'John',
loginOk() {
alert(`${this.name}님이 로그인하였습니다.`);
},
loginFail() {
alert(`${this.name}님이 로그인에 실패하였습니다.`);
},
};
askPassword(user.loginOk, user.loginFail);
// user.loginOk는 this가 undefined므로, this를 정해주어야한다.
// askPassword(user.loginOk.bind(user),user.loginFail.bind(user));
Arrow Function일 때, This
ArrowFunction은 this가 부모의 this를 받아오게 됩니다. 아래와 같은 상황에서 유용하게 쓸 수 있습니다.
const obj = {
msg:"hello",
outer(){
console.log(this.msg)
function inner(){
console.log(this.msg)
}
obj.inner()
}
}
obj.outer()
- outer의 this는 obj.outer()이므로, obj를 가리키고 있습니다.
- inner의 this는 inner()이므로, 현재는 lossing this, this가 정해져있지 않습니다.
위와 같이, 부모의 this를 하위메서드에서 직접적으로 쓰고 싶다면, 여러가지 방법들이 있을 수 있습니다.
- this를 담아서, inner에 직접적으로 넣어주기 ( this,that,self 등 다양한 변수로 이용)
- 단점) 매번 this를 선언해주고 넣어줘야함. 가독성이 떨어짐.
const obj = {
msg:"hello",
outer(){
//this,that
const _this = this
console.log(this.msg)
function inner(){
console.log(_this.msg)
}
inner()
}
}
obj.outer()
- binding 해주기
- 단점 ) 호출시점에서 매번 bind를 해줘야함.
const obj = {
msg:"hello",
outer(){
console.log("this",this)
console.log(this.msg)
function inner(){
console.log(this.msg)
}
inner.bind(this)()
}
}
obj.outer()
등 여러가지 방법이 있지만, Arrow Function을 이용하면, 더욱 깔끔하게 할 수 있습니다.
const obj = {
msg:"hello",
outer(){
console.log("this",this)
console.log(this.msg)
const inner = ()=>{
console.log(this.msg)
}
inner()
}
}
obj.outer()
이벤트 Listener에서의 This
이벤트 리스너에 this의 형태로 callback을 줄 때, callback 함수 안에 this가 있을 수 있습니다.
this는 함수 "호출"시 정해집니다. 즉 , addEventListener() <- 은 함수 호출과정이므로, 앞의 innerEl가 this가 됩니다.
따라서, binding을 해줄 필요성이 생기게 됩니다.
// =====================
class Obj{
// private
#rootEl,
#innerEl,
constructor(){
this.msg = "hello";
this.assginElement();
},
assignElement(){
this.#rootEl= document.getElementById("root");
this.#innerEl = this.#rootEl.querySelector("#inner");
},
addEvent(){
this.#innerEl.addEventLister("click",this.sayhi.bind(this))
},
sayhi(){
console.log(this.msg)
},
}
new Obj()
다른 예제로 , 아래의 this는 어떻게 될까요?
const test ={
say:"hello",
sayhi:()=>{
console.log(this.say)
}
}
test.sayhi()
// undefined
sayhi는 arrow function이기 때문에, 부모의 this를 받아옵니다.
test.sayhi()로 sayhi가 호출됬고, 호출 됬을때, sayhi의 부모는 annoymous 즉 전역객체이기 때문에, window.say는 undefined가 나오게 됩니다
refer