[JS] Core Javascript (2)

RINM·2024년 3월 4일

Study

목록 보기
2/7

[Inflearn] 코어 자바스크립트

This

실행 컨택스트가 생성될 때, 즉 함수가 호출될 때 ThisBinding이 일어난다. 즉, 호출할 때 this가 정의된다. 함수가 어떻게 호출되는지에 따라 this는 다르게 바인딩된다. 일반 함수가 호출될 때, 메서드를 호출했을 때, 콜백 호출했을 때 등등 어떤 함수가 어떻게 호출되었는지에 따라 다르다.

  • 전역: 전역 객체 (Window, global)
  • 함수 호출: 전역 객체 (Window,global)
  • 메서드 호출: 호출한 객체
  • callback 호출: 기본적으로 함수 내부와 동일
  • 생성자 함수 호출: 생성한 인스턴스

예를 들어 a(), b() 이런식으로 호출되는 일반 함수는 어디서 호출되든 전역 객체를 this로 가진다. 웹 브라우저에서는 Window, 노드에서는 global이다. 객체 a의 매서드 b()를 호출하는 a.b()가 실행되면 이때 this는 객체 a이다. b를 메서드로서 호출했기 때문이다. a.b.c()를 호출하면 이때의 this는 객체 a.b가 된다. 물론 다른 표기법인 a['b'].c()도 a['b] 객체를 this로 가진다. 그런데 한 편으로는 객체 안에 정의된 함수가 다른 함수를 호출하면 그 객체가 아닌 전역 객체를 this로 가지게 된다. 즉 어디서 호출되든 객체 없이 단독으로 c()와 같이 실행되면 전역 객체를 this로 가지게 되는 것이다.

var a = 10
var obj = {
	a: 20,
  	b: function(){
    	console.log(this.a); // 20
    	function c () {
        	console.log(this.a); //10
        }
      	c()
    }
}

위와 같은 코드에서 첫번째 로그는 b의 this가 객체 obj이기 때문에 20으로 찍힌다. 그러나 이후 실행된 함수 c는 객체 안에서 정의되었지만 단독으로 호출되기 때문에 전역 객체를 this로 갖게 되고, 전역변수 중 a라는 이름을 가진 변수가 가진 값 10을 출력하게 된다. 그렇다면 c가 obj를 this로 가지게 하려면 어떻게 해야할까?

this는 자동으로 바인딩되므로 함부로 바꿀 수 있는 값이 아니다. (call/apply를 사용해서 this를 직접 제공할 수 있기는 하다.) 즉, c에서 obj를 참조하게 하고 싶다면 다른 변수를 사용해야한다. scope chain을 통해 c가 obj를 참조하도록 할 수 있다.

var a = 10
var obj = {
	a: 20,
  	b: function(){
      	var self = this;
    	console.log(this.a); // 20
    	function c () {
        	console.log(self.a); //20
        }
      	c()
    }
}

b는 obj 객체에 포함된 메서드이므로 obj를 this로 가진다. 이 값을 메서드 함수 내의 지역 변수 self에 저장해주고 이를 c에서 사용할 수 있게 한다. c함수가 호출되면 self라는 변수를 찾게 된다. c 함수 내부에 정의된 값이 없으니 (c 자신의 Lexical Environment에 self가 없으니) Outer Environment Referrence을 타고 밖으로 나가게 된다. (scope chain) 가장 가까이 정의된 self는 b 함수 안에 선언된 것이다. c는 이 self 변수를 사용하게 된다. 그럼 c에서 obj를 참조할 수 있게 되는 것이다.

ES6에서 추가된 arrow function은 this를 바인딩하지 않는다. c가 arrow function으로 정의된 함수라고 해보자. 아래와 같이 정의되었을 것이다.

var a = 10
var obj = {
	a: 20,
  	b: function(){
    	console.log(this.a); // 20
    	const c = () => {
        	console.log(this.a); //20
        }
      	c()
    }
}

c는 this가 바인딩되지 않았다. 즉, this라는 변수가 Lexical Environment에 존재하지 않는다. 그러니 scope chain을 타고 밖으로 나가 this를 찾는다. 가장 가까운 b의 내부에서 this를 찾게 되고, 바로 obj를 참조할 수 있게 된다.

명시적 this Binding

ES5에서는 call, apply, bind를 사용하여 this를 직접 지정해주었다.

function a(x,y,z){
	console.log(this,x,y,z)
}
var b = {
	bb: 'bbb'
}

a.call(b,1,2,3) // {bb:"bbb"} 1 2 3

a.apply(b,[1,2,3]) // {bb:"bbb"} 1 2 3

var c = a.bind(b); 
c(1,2,3) // {bb:"bbb"} 1 2 3

var d = a.bind(b,1,2); 
d(3); // {bb:"bbb"} 1 2 3

위 예제에서 모든 함수 호출 결과는 {bb:"bbb"} 1 2 3로 같다. 차례대로 살펴보자. a.call(b,1,2,3)에서 첫번째 인자 b는 this가 되었고 나머지는 매개변수가 되면서 a 함수가 호출되었다. a.apply(b,[1,2,3])도 유사하게 첫 인자인 b는 this가 되었고 그 뒤의 배열은 a함수의 매개변수로 맵핑되었다.

c는 a.bind(b)로 먼저 선언되었는데 이때 c라는 함수가 a의 함수에 this를 b로 가지는 것을 알 수 있다. 이후 c(1,2,3)으로 호출하면 모든 인자는 매개변수가 되고 this는 선언된 b가 된다.

d는 a.bind(b,1,2)로 선언되었는데 유사하게 a 함수의 원형에 this가 b로 지정되었고, 첫 번째, 두 번째 매개변수 x, y가 각각 1,2로 지정되었다. d(3)으로 함수를 호출하면 지정되지 않은 매개변수는 마지막 매개변수 z 뿐이고, 이 자리에 인수로 주어진 3이 들어가면서 실행된다.

Callback 함수가 호출될 때는 일반적으로 일반 함수가 호출될 때와 같지만 예외가 존재한다.

var callback = function() {
	console.dir(this) //window
}
var obj = {
	a: 1,
  	b: function(cb){
    	cb()
    }
}

obj.b(callback)

객체 obj 안에 정의된 매서드 함수 b는 매개변수 cb로 함수를 받고 이것을 실행한다. callback은 전역 공간에서 정의된 함수이다. 이것을 b의 인수로 넣어 실행하면 b는 callback을 호출한다. 이때 콘솔에 찍히는 this는 무엇일까? callback은 매서드 함수가 아니다. 일반적으로 호출이 되었기 때문에 당연히 전역 객체인 window나 global을 this로 가진다. 콘솔에는 window가 출력된다.

var callback = function() {
	console.dir(this) //obj
}
var obj = {
	a: 1,
  	b: function(cb){
    	cb.call(this)
    }
}

obj.b(callback)

그렇다면 위와 같이 callback을 실행할 때 call 함수로 this를 전달하면 어떻게 될까? b의 context에 있던 this, 즉 obj가 callback의 this로 지정된다. 콘솔에는 obj가 출력된다.
이와 같이 callback 함수의 this는 callback을 호출하는 함수가 callback을 호출할 때 어떻게 처리하는 지에 따라 달라진다. 기본적으로 this는 전역객체로 보아야 맞지만, 호출 방식에 따라 달라질 수 있음을 알아야한다. 이때 원하는 값을 this로 주기 위하여 앞서 살펴본 call, apply, bind가 사용된다.

var callback = function() {
	console.dir(this) //window
}
var obj = {
	a:1
}
setTimeout(callback,100);

위의 예제에서는 callback이 별도의 처리 없이 호출되니 당연히 전역 객체를 this로 갖게 된다. 여기서 객체 obj를 callback의 this로 만들어서 호출하고 싶다면 bind를 사용하면된다.

var callback = function() {
	console.dir(this) //obj
}
var obj = {
	a:1
}
setTimeout(callback.bind(obj),100);

이렇게 하면 callback의 this가 전역변수 obj로 지정된 후 실행되어 콘솔에 obj가 출력된다.
주의해야할 점은 내장 함수 중에 callback의 this를 별도로 지정해놓은 경우가 꽤 있다는 것이다.

document.getElementById('a').addEventListener(
	'click',
  	function() {
    	console.dir(this) // #a (해당 태그)
    }
)

예를 들어 위의 addEventListener의 경우 콜백 함수의 this는 이벤트를 추가하려는 해당 태그를 this로 갖는다. 따라서 id가 a인 이 태그를 클릭하면 전역 객체인 window가 아니라 해당 태그가 출력된다. 이렇게 내부적으로 처리된 this를 무시하고 직접 this를 지정해주고 싶다면 조금 전의 setTimeout 예제처럼 bind 함수를 사용하면 된다.

생성자 함수를 호출하는 경우 this는 생성한 바로 그 인스턴스가 된다.

function Person(n,a){
	this.name = n;
  	this.age = a;
}
var john = Person('철수',20)

위의 예제에서 Person은 생성자이긴 하지만 new 없이 일반 함수로서 호출되었다. 당연히 this는 전역객체가 되고 뜬금 없이 window.name과 window.age에 '철수'와 20이 담기게 된다. 또한 반환값이 따로 없기 때문에 john에는 아무것도 담기지 않는다.

function Person(n,a){
	this.name = n;
  	this.age = a;
}
var john = new Person('철수',20)

생성자로서 new와 함께 호출하면 이때의 this는 새로 생성한 인스턴스 그 자체가 된다. 따라서 새 인스턴스의 name과 age property에 '철수'와 20이 담기고, 그 인스턴스가 john에 담긴다.

Callback Function

callback이 무슨 뜻일까. 어떤 일을 처리한 다음에 다시(back) 알려주겠다(call) 이런 의미로써 쓰인다. 예를 들어 피자를 전화로 주문하면(call) 피자 가게가 이 주문을 접수하고, 피자가 완성되면 이 소식을 고객에게 알려준 다음(callback), 피자를 배달해준다.

즉, callback 함수에 제어권을 넘겨서 어떤 일을 대신 처리하게 하는 것이다. 제어권을 위임하면 이때, 실행 시점, 매개변수, this가 함께 전달된다. 앞서 살펴본 addEventListener에서 this가 호출과 상관 없이 태그로 지정된 것이 그 예이다.

실행시점을 넘겨주는 예제로는 setInterval이 있다. setInterval은 callback으로 넘겨받은 함수를 또다른 매개변수로 받은 특정 시간을 간격으로 실행한다. 즉, callback 함수의 실행시점이 setInterval에게 그 제어권이 넘어간 것이다.

매개변수가 위임되는 예로는 forEach가 있다. forEach는 해당 배열을 순회하면서 콜백 함수를 실행한다. 이때 배열의 원소마다 그 값은 callback함수의 첫 매개변수, 인덱스는 두 번째 매개변수에 각각 부여된다. forEach가 callback의 매개변수를 제어하며 실행하는 것이다.

var arr = [1,2,3,4,5]
var entries = []

arr.forEach(function(v,i){
	entries.push([i,v,this[i]])
}, [10,20,30,40,50])

console.log(entries)
// [[0,1,10],[1,2,20],[2,3,30],[3,4,40],[4,5,50]]

위와 같은 코드를 실행할 때, forEach는 v와 i에 각각 arr의 원소의 값과 인덱스를 차례대로 넘겨준다. 이때 forEach의 두번째 인자는 thisArg로 callback함수의 this를 지정해준다. 따라서 this[i]는 forEach의 두번째 매개변수인 배열 [10,20,30,40,50]의 각 원소가 된다. 즉, callback의 매개변수와 this 모두 forEach가 지정해서 호출되는 것이다.

자연스럽게 callback 함수는 제어권을 넘겨주는 함수가 지정하는 형태로 작성하게 된다. 따라서 callback 함수가 미리 지정된 것과 다른 this를 갖게 하고 싶거나 다른 작동을 하고 싶은 경우 bind를 사용하거나 thisArg로 지정해야한다.

[freeCodeCamp] JavaScript Callback Function –Explained in Plain English

0개의 댓글