[코어 자바스크립트] 3. this

홍예찬·2021년 1월 9일
0
post-thumbnail

What is 'this'?

다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미합니다.
그러나, 자바스크립트에서 this는 어디서든 사용 가능합니다.
자바스크립트에서 this의 또 다른 특징은 동적으로 변경된다는 것입니다. 상황에 따라 this가 바라보는 대상이 달라진다는 의미죠. 바로 이 부분에서 this에 대한 혼란스러움이 발생하게 됩니다.

3-1 상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됩니다.
즉, this는 함수를 호출할 때 결정된다고 할 수 있겠습니다.

①전역 공간에서의 this

전역 객체에서 this는 전역 객체를 가리킵니다. 전역 객체는 JS 런타임 환경에 따라브라우저 환경에서 전역객체는 window이고 Node.js 환경에서는global입니다.


//브라우저 환경에서의 this
console.log(this)	// {alert: f(), atob: f(), blur: f(), btoa: f(), ...}
console.log(window)	// {alert: f(), atob: f(), blur: f(), btoa: f(), ...}
console.log(this === window)	//true

//Node.js 환경에서의 this
console.log(this)	// { process: { title: 'node', version: 'v10.13.0', ...}}
console.log(global)	// { process: { title: 'node', version: 'v10.13.0', ...}}
console.log(this === global)	//true

자바스크립트의 모든 변수는 실제로 특정 객체의 프로퍼티로서 동작합니다.
따라서 전역 변수를 선언하게 되면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당하게 됩니다.
그렇다면 a를 직접 호출했을 경우 1이 나오게 되는 이유는 무엇일까요? 이는 앞서 실행 컨텍스트에서 배웠던 스코프 체인의 개념으로 이해하면 되겠습니다. 가장 마지막에 도달하는 전역 스코프의 LexicalEnvironment , 즉 전역 객체에서 해당 프로퍼티 a를 발견해서 반환하는 것이죠. 이를 window.가 생략된 것으로 여겨도 무방할 것 같습니다.
이를 코드로 확인해보면 다음과 같습니다.


var a = 1;
window.b = 2;
console.log(a, window.a, this.a)	//1 1 1
console.log(b, window.b, this.b)	//2 2 2

window.a = 3;
b = 4;
console.log(a, window.a, this.a)	//3 3 3
console.log(b, window.b, this.b)	//4 4 4

그러나 삭제의 경우 전역변수 선언과 프로퍼티 할당 사이에 다르게 작동합니다. 처음부터 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되는 반면에, 전역변수로 선언한 경우에는 삭제가 되지 않습니다.


var a = 1;
delete window.a;	//false
delete a;		//false
console.log(a, window.a, this.a)	//1 1 1

window.b = 2;
delete window.b;	//true
delete b;	//true
console.log(b, window.b, this.b)	//ReferenceError: b is not defined

② 메소드로 호출할 때 그 내부에서의 this

함수 vs 메소드

함수를 실행하는 가장 일반적인 방법으로는 함수로서 호출하거나 메소드로서 호출하는 경우입니다.
그러나 함수는 그 자체로 독립적인 기능 수행하는 반면에, 메소드는 자신을 호출한 객체에 관한 동작을 수행하게 됩니다.
메소드는 객체의 프로퍼티에 할당한다고 해서 그 자체로 메소드가 되는 것이 아니라, 객체의 메소드로서 호출할 경우에만 메소드로 동작하고, 그렇지 않을 경우 함수로 동작하게 됩니다.
이렇게 말로 설명해서는 잘 이해가 가지 않습니다. 밑의 예제 코드를 보면서 다시 설명해보겠습니다.


let func = function (x) {
	console.log(this, x);
};
func(1);	// Window { ... } 1

let obj = {
	method: func
};
obj.method(2);	// { method: func() } 2

이처럼 원래 익명함수는 그대로인데 이를 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것입니다.

메서드 내부에서의 this

this에는 호출한 주체에 대한 정보가 담기게 되는데 어떤 함수를 메소드로서 호출하게 되는 경우, 호출 주체는 함수명(프로퍼티명)앞의 객체입니다. 점 표기법의 경우에는 마지막 점 앞에 명시된 객체가 곧 this가 됩니다.


var obj = {
  methodA : function() {console.log(this);},
  inner: {
    methodB: function() {console.log(this);}
  }
}
obj.methodA();		// {methodA: func, inner: { ... }}  (===obj)
obj['methodA']();	// {methodA: func, inner: { ... }}  (===obj)

obj.inner.methodB();		// { methodB: func }	(===obj.inner)
obj.inner['methodB']();		// { methodB: func }	(===obj.inner)
obj['inner'].methodB();		// { methodB: func }	(===obj.inner)
obj['inner']['methodB']();	// { methodB: func }	(===obj.inner)

③ 함수로 호출할 때 그 내부에서의 this

함수 내부에서의 this

함수로 호출하는 것은 호출 주체(객체지향 언어에서의 객체)를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없습니다. 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우에는 전역 객체를 바라보기 때문에, 함수에서의 this는 전역 객체를 바라보게 됩니다.

메소드 내부함수에서의 this

내부함수의 경우 이를 함수로서 호출했는지, 메소드로 호출했는지 파악하면 this의 값을 정확히 맞출 수 있습니다.


var obj1 = {
  outer: function() {
    console.log(this);		//(1)
    var innerFunc = function () {
      console.log(this);	//(2) (3)
    }
    innerFunc();
    
    var obj2 = {
    innerMethod: innerFunc
	};
	obj2.innerMethod();
  }
}
obj1.outer();

(1),(2),(3)에는 각각 어떤 값이 출력될까요?
(1)의 경우에는 obj.outer() 메소드로 호출했기 때문에 obj1이,
(2)의 경우에는 innerFunc() 함수로 호출했기 때문에 전역객체가,
(3)의 경우에는 obj2.innerMethod() 메소드로 호출했기 때문에 obj2가 출력됩니다.
즉, 함수를 실행하는 당시의 주변환경(함수 내부인지, 메소드 내부인지)은 중요하지 않고, 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건인 것입니다.

그렇다면 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있는 방법은 없을까요? 변수의 스코프체인처럼 말이죠. 그 대표적인 방법은 바로 변수를 활용하는 것입니다.


var obj1 = {
  outer: function() {
    console.log(this);		//(1) { outer: func }
    var innerFunc1 = function () {
      console.log(this);	//(2) Window { ... }
    }
    innerFunc1();
    
	var self = this;
    var innerFunc2 = function() {
    	console.log(self);	//(3) { outer: func }
    }
    innerFunc2()
  }
}
obj1.outer();

④ 콜백함수로 호출할 때 그 내부에서의 this

콜백함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조하게 되지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.

⑤ 생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수입니다. 객체지향 언어에서는 생성자를 Class, 클래스를 통해 만들어진 객체를 Instance라고 합니다.

자바스크립트는 함수에 생성자로서의 역할을 함께 부여했습니다. new 명령어와 함께 함수를 호출하게 되면 해당 함수는 생성자 함수로서 동작하게 됩니다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우, 내부의 this는 곧 새로 만들 구체적인 인스턴스 자신이 됩니다.

var Cat = function (name,age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
}
var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco)
console.log(nabi)
/*
Cat {
  bark: '야옹',
  name: '초코',
  age: 7,
  __proto__: Cat { constructor: ƒ Cat() }
}
Cat {
  bark: '야옹',
  name: '나비',
  age: 5,
  __proto__: Cat { constructor: ƒ Cat() }
}
*/

3-2 명시적으로 this를 바인딩하는 방법

① call 메소드

call 메소드는 메소드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이빈다. 이 때, call 메소드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 합니다.


var func = function (a,b,c) {
  console.log(this, a, b, c);
}

func(1,2,3)				//window { ... } 1 2 3
func.call({x:1}, 4,5,6)			// { x:1 } 4 5 6

메소드에 대해서도 마찬가지로, 객체의 메소드를 호출하게 되면 this는 객체를 참조하지만, caal메소드를 이용하게 되면 임의의 객체를 this로 지정할 수 있습니다.


var obj = {
  a: 1,
  method: function(x,y) {
    console.log(this.a, x, y)
  }
}

obj.method(2,3)			// 1 2 3
obj.method.call({a:4}, 5,6)     // 4 5 6

② apply 메소드

apply메소드의 경우 call메소드와 기능적으로 완전히 동일하나, 두 번째 인자를 배열로 받아 그 배열의 요소들을 함수의 매개변수로 지정한다는 점에서만 차이가 있습니다.


var func = function (a,b,c) {
  console.log(this, a ,b ,c)
}
func.apply({x:1}, [4,5,6])	//{ x: 1 } 4 5 6


var obj = {
  a:1,
  method: function(x,y) {
    console.log(this.a, x, y)
  }
}
obj.method.apply({a:4}, [5,6])  //4 5 6

call, apply 메소드는 유사배열객체에서도 사용할 수 있습니다.
(유사배열객체: 키가 0또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티값이 0또는 양의 정수인 객체(배열의 구조와 유사한 객체))


var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
}
Array.prototype.push.call(obj, 'd');
console.log(obj);	// {0:'a', 1:'b', 2:'c' 3:'d', length: 4}

var arr = Array.prototype.slice.call(obj);
console.log(arr);	// ['a', 'b', 'c', 'd']
  • slice 메소드의 경우 아무런 매개변수를 넘기지 않으면 원본 배열의 얕은 복사본을 반환합니다.

③ bind 메소드

bind 메소드는 ES5에 추가된 기능으로, call과 비슷하지만 즉시 호출하지 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메소드입니다.
다시 새로운 함수를 호출할 때 인자를 넘기게 되면 그 인자들은 기존 bind 메소드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록됩니다.
즉, bind 메소드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닙니다.


var func = function(a,b,c,d) {
  console.log(this, a, b, c, d)
}
func(1,2,3,4);			// Window { ... } 1 2 3 4

var bindFunc1 = func.bind({ x:1 });
bindFunc1(5, 6, 7, 8)		// { x:1 } 5 6 7 8

var bindFunc2 = func.bind({ x:1 }, 4, 5);
bindFunc2(6,7);			// { x:1 } 4 5 6 7
bindFunc2(8,9);			// { x:1 } 4 5 8 9

④ 화살표 함수의 예외사항

ES6에서는 this가 전역객체를 바라보는 문제를 보완하고자 arrow function이라는 개념이 도입되었습니다. 이를 사용하게 되면 실행 컨텍스트에서 this 바인딩 과정 자체가 빠지게 되어 상위 스코프의 this를 그대로 활용할 수 있게 되는 것입니다.


var obj = {
  outer: function () {
    console.log(this);		//{ outer: outer() }
    var innerFunc = () => {
      console.log(this);	//{ outer: outer() }
    };
    innerFunc();
  }
};
obj.outer();

정리

명시적 this 바인딩이 없는 한 다음의 규칙이 성립됩니다.

  • 전역공간에서의 this는 전역 객체를 참조하게 됩니다.
  • 함수를 메소드로 호출한 경우 this는 메소드 호출 주체를 참조합니다.
  • 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다.(메소드의 내부 함수에서도 동일합니다)
  • 콜백 함수 내부의 this는 해당 콜백함수의 제어권을 넘겨받은 함수가 정의한 바에 따르고, 정의하지 않은 경우에는 전역객체를 참조합니다
  • 생성자 함수의 this는 생성될 인스턴스를 참조합니다.

명시적으로 this를 바인딩하는 방법입니다.

  • call, apply 메소드는 this를 명시적으로 지정하면서 함수 또는 메소드를 호출합니다.
  • bind 메소드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.
  • 요소를 순회하면서 콜백함수를 반복 호출하는 일부 메소드는 this를 두번 째 인자로 받기도 합니다(forEach, map, filter, some, every)

위의 글은 온전히 학습 목적을 위해 작성한 글입니다. 위 내용의 모든 지적 재산권,저작권은 코어 자바스크립트 저자에게 있으며, 무단 전재 및 재배포를 금합니다.

profile
내실 있는 프론트엔드 개발자가 되기 위해 오늘도 최선을 다하고 있습니다.

0개의 댓글