Web Front-end/JavaScript

[JS/this/호출스택] JS의 This 정리 (호출스택,This)

Mapin 2022. 10. 17. 16:39

이제 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

 

What is a callback function?

What is a callback function?

stackoverflow.com

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

제로초님의 this강의