코어 자바스크립트 #3 this

신윤철·2022년 1월 21일
1

코어자바스크립트

목록 보기
3/8
post-thumbnail

들어가며 ..
처음 블로그 글을 작성하다 보니 가독성도 좋지 않고 그냥 책을 배껴쓴다는 생각이 들었습니다. 그래서 이번 파트부터는 저의 생각을 정리하며 조금 요약된 방식으로 적어보려 합니다.
뭐가 더 좋은 방식인지는 아직 모름..

👉this

this 란?

대부분의 객체지향 언어에 존재하는 개념으로 this는 클래스로 생성한 인스턴스를 말합니다.

하지만 실제클래스가 존재하지 않는 자바스크립트에선 this가 조금 다른 규칙이 적용됩니다.

자바스크립트에서 this는 어디에서든 사용할 수 있는데, 상황에 따라 this가 바라보는 대상이 달라집니다.

만약 자바스크립트의 작동방식을 잘 이해하지 못한다면 this가 바라보는 대상을 잘못 예측해 코드를 작성할 수 있고 이는 심각한 문제가 될 수 있으니 이번에 this의 동작과정에 대해 잘 알아봅시다!

🔍자바스크립트에서 this

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

즉, this는 함수를 호출할 때 결정된다고 볼 수 있습니다.
때문에 this는 자바스크립트에서 함수와 객체를 구분짓는 핵심 기능입니다.

전역 공간에서의 this

전역공간에서 this는 전역 객체를 가리킵니다.
앞서 'this는 실행 컨텍스트가 생성될 때 함께 결정된다'라고 한듯이 전역 컨텍스트를 생성&실행하는 객체가 전역 객체이기 때문입니다.

//전역 공간에서 this(브라우저 환경)
console.log(this) 		// window
console.log(window)		// window
console.log(this === window) 	// true	

브라우저 환경에서 전역 객체는 window입니다.
예를 보면 전역 공간에서 this와 window를 일치 연산자로 비교시 true가 출력되는 것을 확인할 수 있습니다.

  • 즉 전역 공간에서 this는 전역 객체(window) 입니다.
    (노드 환경에서는 노드의 전역 객체인 global === this)

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

메서드 내부에서의 this를 알아보기 전에 함수와 메서드의 차이를 알아보겠습니다.
저도 둘의 차이를 확실하게 알지 못했는데 책에서 이 둘을 명확히 구분해 주었습니다.

기본적으로 함수와 메서드는 같은 구조를 갖습니다.
=> ( function (매개변수) { 실행문 } )
함수와 메서드를 구분하는 유일한 특징은 '독립성'인데 여기서 독립성은 혼자 호출될 수 있느냐, 객체의 메서드로서 호출되느냐 입니다.

// 함수로서 호출, 메서드로서 호출
function func(x) {		// func 함수 선언
  console.log(this, x);
}
var obj = {			// obj 객체 선언
  method: func,			// obj 객체의 method 프로퍼티에 func함수 할당
};
func(10);			// window {...} 10
obj.method(10);		// {method: f} 10

예제를 보면 하나의 함수 func을 함수로 호출할때와 obj(객체)의 프로퍼티로서 호출할 때의 this 값이 달라졌습니다.

같은 함수 func을 할당받았는데 왜 다른 값이 나올까요?
이유는 같은 함수라도 함수로 호출됐느냐, 메서드로 호출됐느냐에서 차이가 발생한 것 입니다.

팁 : 함수로서 호출과 메서드로서 호출을 구분하는 방법

  1. 함수 앞에 점(.)이 있는지 확인 { ex) obj.method() }
  2. 함수를 대괄호표기법으로 호출 { ex) obj['method'] }

어떤 방식이든 메서드로 호출하기 위해선 앞에 객체가 명시되어있습니다!

🔍메서드에서 this

메서드 내부에서의 this

그럼 이제 메서드 내부에서 this가 어떤 객체를 가리키고 있는지 살펴보겠습니다.

var obj = {				// 객체obj
  methodA: function () {		// obj의 methodA 프로퍼티(함수)
    console.log('A', this);
  },
  inner: {				// obj의 inner 프로퍼티(객체)
    methodB: function () {		// inner의 methodB 프로퍼티(함수)
      console.log('B', this);
    },
  },
};
obj.methodA();				// A {inner: {…}, methodA: ƒ} ( => obj와 같은 의미 ) 
obj.inner.methodB();			// B {methodB: ƒ} ( => obj.inner와 같은 의미 )

this는 호출한 주체에 대한 정보가 담기는데, 메서드 내부에서는 this가 어떤 객체를 가리키고 있는지 알기 쉽습니다.

예제를 통해서도 알 수 있듯이 메서드 내부에서 this는 호출 함수명(프로퍼티명) 앞의 객체입니다.
(== 점 표기법 또는 대괄호 표기법의 앞에 나오는 객체)

🔍함수에서 this

이제 함수내부에서 this가 가리키는 객체를 살펴보겠습니다.
(함수 내부에서의 this는 약간 복잡합니다.)

함수 내부에서의 this

이전에 this에는 호출한 주체에 대한 정보가 담긴하고 했었습니다.
그런데 함수를 함수로서 호출한 경우에는 호출주체(객체지향언어에서의 객체)를 명시하지 않은 것입니다.

때문에 함수를 직접 호출한 경우에는 호출 주체를 알 수 없고 이처럼 this가 지정되지 않을 경우 this는 자동으로 전역 객체를 바라보게 됩니다.

즉, 코드내에서 함수를 직접 호출할 경우에 함수는 전역 객체를 가리킵니다.

매서드의 내부 함수에서의 this

메서드 내부에서 정의하고 실행한 함수에서의 this는(설명부터 복잡..) this의 동작과정을 제대로 이해하지 못하면 큰 혼란을 일으킵니다.

//메서드 내부에 정의된 함수에서의 this
var obj1 = {
  outer: function () {
    console.log(this);
    var innerFunc = function () {
      console.log(this);
    };
    innerFunc();			// (1) 

    var obj2 = {
      innerMethod: innerFunc,
    };
    obj2.innerMethod();			// (2)
  },
};
obj1.outer();				// (3)

객체 obj1의 프로퍼티 outer 함수 내부에서 함수 innerFunc과 객체 obj2를 정의하고 객체 obj2의 프로퍼티로 innerFunc 함수를 할당한 예제입니다.

과연 실행 (1),(2),(3) 의 결과는 어떻게 나올까요?
(1) : window, (2) : obj2, (3) : obj1 가 나옵니다.

실행 순서는 (3) -> (1) -> (2) 이므로 실행 순서대로 코드를 분석해보겠습니다.

  • (3)에서 객체obj1의 메서드로서 outer()을 호출합니다.

  • outer함수가 실행되면서 실행 컨텍스트가 생성되고 this는 실행 컨텍스트가 생성될때 함께 결정됩니다.(this 바인딩)

  • 그런데 이때 outer함수는 obj1의 메서드로서 호출되었으므로 이때 this는 객체 obj1을 가리키게 됩니다. ( (3) = obj1 )

  • 다음으로 outer 함수 내에서 innerFunc 익명함수가 생성되고 호출됩니다.(1)

  • 그런데 innerFunc()이 호출될 때 함수명 앞에는 .이 없었습니다. 이는 함수로서 호출 되었다는 의미이고 this가 지정되지 않았다는 것입니다.

  • 따라서 (1) = window, 즉 전역객체를 가리키게 됩니다.

  • obj2는 객체의 메서드의 내부에 정의된 객체이고 내부 프로퍼티로 innerMethod를 가지고 있으며 이는 innerFunc과 연결되어 있습니다.

  • (2)에서 obj2.innerMethod 함수가 호출되면서 실행 컨텍스트가 만들어지고 innerMethod의 앞에 .으로 obj2가 연결되어 있었으므로 this 바인딩에 obj2가 들어갑니다.

  • 따라서 (2) = obj2 객체정보가 출력됩니다.

주저리 주저리 많이 적어놨는데 쉽게 생각하면 객체.함수로 호출될 경우 this는 객체를 가리키게 되고, 함수가 호출될 경우 함수의 this는 전역 객체를 의미하게 됩니다.

this 우회법

메서드의 내부에서 this를 우회하는 방법

그런데 예제의 (1) 같이 객체 내에 있음에도 호출 주체가 없다는 이유로 this가 속해있는 컨텍스트가 아닌 최상위 컨텍스트인 전역 객체를 가리키는 것은 많이 어색합니다.

변수를 검색하면 가장 가까운 스코프의 L.E를 찾고 없으면 상위 스코프를 탐색하듯 this도 현재 컨텍스트에 바인딩 된 대상이 없다면 직적 컨텍스트의 this와 연결되는 것이 자연스럽습니다.

이를 해결하는 방법은 ES5에서와 ES6이후에서 달라집니다.

  • ES5에선 자체적으로 내부에서 this를 상속할 방법이 없어 우회하여 사용합니다. (원하는 this를 임의의 변수(self)에 저장하여 사용)

  • ES6에선 this를 바인딩하지 않는 함수인 화살표 함수(arrow function)을 사용합니다. (화살표 함수는 실행 컨텍스트를 생성할때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있습니다.)

// this를 바인딩하지 않는 함수 (arrow function)
var obj = {
  outer: function(){
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    };
    innerFunc();			// (2) { outer: f }
  }
};
obj.outer();				// (1) { outer: f }

this를 바인딩하지 않기때문에 innerFunc()이 전역 객체가 아닌 상위 컨텍스트의 this를 그대로 사용합니다.

콜백 함수 호출 시 this

콜백 함수 호출 시 그 함수 내부에서의 this

❓콜백함수란

  • 함수 A의 제어권(실행)을 다른 함수(또는 메서드)B에게 넘겨주는 경우 함수 A를 콜백 함수라 합니다.

  • 콜백함수A는 제어권을 B에게 주었기 때문에 B의 내부로직에 따라 실행됩니다.

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

예제를 통해 콜백함수의 내부에서 this를 살펴보겠습니다.

setTimeout(function(){console.log(this);}, 300)		// (1) window

[1,2,3].forEach(function (x) {console.log(this, x);});	// (2) window,1 window,2 window,3

document.body.innerHTML += '<button id="a">클릭</button>';
const a = document.body.querySelector("#a");
a.addEventListener("click",function(e){console.log(this,e);});	// (3) '<button id="a">클릭</button>', PointerEvent{...}
  • (1)은 0.3초 이후 콜백함수 function을 실행합니다. 이때 제어권을 받은 setTimeout함수는 따로 this의 대상을 지정하지 않아 전역객체 window가 this의 대상이 됩니다.

  • (2)는 배열 [1,2,3]을 하나씩 콜백함수에서 출력하는 로직입니다. 이 또한 콜백함수의 대상을 지정하지 않아 전역객체가 this에 출력됩니다.

  • (3)은 addEventListener 함수가 제어권을 받아 콜백함수를 실행합니다. 이때 addEventListener앞에 .(점표현식)으로 대상이 지정된 것을 확인할 수 있습니다. 때문에 this의 대상이 지정되어 출력됩니다.

생성자 함수 내부에서의 this

❓생성자 함수란

  • 생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는데 사용하는 함수입니다. 네 맞습니다 흔히 생각하는 클래스입니다.
    (타 객체지향과 자바스크립트에서 클래스는 약간 다르지만 나중에 설명하겠습니다.)
  • 생성자는 구체젝인 인스턴스를 만들기 위한 일종의 틀이며 만들어진 각 인스턴스들은 개별적인 개성을 추가해 발전할 수 있습니다.

자바스크립트는 함수에 생성자로서의 역할을 함께 부여했습니다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로 동작하게 됩니다.

앞서 함수를 실행할 때 this가 만들어진다고 설명한 것처럼 생성자 함수도 호출될 때 this를 만들고 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 됩니다.

생성자 함수를 호출하면 우선 생성자의 prototype 프로퍼티를 참조하는 __protp__라는 프로퍼티가 있는 객체(인스턴스)를 만들고 미리 준비가 된 공통 속성을 해당 객체(this)에 부여합니다.(내부에서 this는 인스턴스 자신이므로)

예제로 확인해보겠습니다.

var Korean = function(name, age){
  this.nation = "대한민국";
  this.name = name;
  this.age = age;

  var kimho = new Korean("김호", 22);
  var namsu = new Korean("남수", 24);
  console.log(kimho, namsu);
/*결과
Korean {nation: "대한민국", name: '김호', age: 22}
Korean {nation: "대한민국", name: '남수', age: 24}
*/

함수 표현식으로 생성자를 만들고 생성자 내부에서는 this에 접근하여 nation, name, age 프로퍼티에 각각 값을 대입했습니다.

그리고 new 명령어와 Korean함수를 호출하여 새로운 변수 kimho, namsu에 각각 할당하고 출력을 살펴보았습니다.

그 결과 생성자의 this는 각각의 클래스에서 만들어진 인스턴스 객체를 가리키는 것을 확인할 수 있습니다.

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

앞에선 상황별로 this에 어떤 값이 바인딩 되는지를 살펴봤습니다.

이번엔 이 규칙을 깨고 this에 별도의 대상을 바인딩하는 방법을 알아보겠습니다.

call 메서드
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

call 메서드란

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

예제를 살펴보겠습니다.

// call 메서드(1) (함수 호출)
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는 전역 객체를 호출하지만 call 메서드로 {x:1}라는 임의의 객체를 첫 번째 인자로 할당하면 그 객체를 this에 바인딩합니다.

// call 메서드(2) (메서드 호출)
var obj = {
  num: 10,
  method: function (x, y) {
    console.log(this.num, x, y);
  },
};

obj.method(2, 3);			// 10, 2, 3
obj.method.call({ num: 20 }, 5, 6);	// 20, 5, 6

call을 사용하여 객체 obj를 this로 지정했기 때문에 this.num의 값을 20으로 변경할 수 있었습니다.

이처럼 call 메서드를 사용하면 임의의 객체를 this로 지정할 수 있습니다.

apply 메서드
Function.prototype.apply(thisArg[, argsArray])

apply 메서드는 call 메서드와 기능이 완전히 동일합니다.

하지만 호출 방식에 약간의 차이가 있는데, 인자를 하나씩 받는것이 아니라 배열로 받는다는 차이가 있습니다.

func({x:1},1,2,3)	// call 메서드 
func({x:1},[1,2,3]) 	// apply 메서드

call메서드와 apply메서드의 활용

1. 유사배열객체에 배열 메서드를 적용 가능

객체에는 배열 메서드(push, pop, slice..)를 직접 적용할 수 없습니다.

그러나 key가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체(==> 유사배열객체)의 경우 call과 apply를 통해 배열 메서드를 차용할 수 있습니다.

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]

객체인 obj가 유사배열객체라는 특징을 활용해 배열메서드인 push, slice를 적용시킨 모습입니다.

눈여겨볼 점은 slice메서드를 통해 객체의 요소만을 반환받아 하나의 배열을 만든 것입니다.

이처럼 call, apply를 잘 활용하면 자바스크립트를 더 다채롭게 사용할 수 있습니다.

2. 생성자 내부에서 다른 생성자를 호출

생성자 내부에서 다른 생성자와 공통된 내용이 있을 경우 call, apply를 이용하여 다른 생성자를 호출하면 반복을 줄일 수 있습니다.

function Person(name, age){
  this.name = name;
  this.age = age;
}
function Student(name, age, school){
  Person.call(this, name, age);			//Person 생성자의 공통부분 호출, 생성자에서 this는 만들어지는 인스턴스를 가리킴
  this.school = school;
}
var makedStudent = new Student("민기", 20, "서울대");	//Student {name: '민기', age: 20, school: '서울대'}

3. 여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 -> apply 활용

여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶다면 apply 메서드를 사용하시면 됩니다.

배열을 입력받아 최대, 최소값을 찾는 코드를 비교하며 설명하겠습니다.

// 최대/최솟값을 구하는 코드(no apply)
var numbers = [23, 15, 26, 38, 52];
var max = min = numbers[0]        // 배열의 첫번째 값을 max와 min의 초기값으로 할당

numbers.forEach(function(number){ // numbers 배열의 값을 하나씩 function에 대입
  if (number > max){              
    max = number;
  }
  if (number < min){              
    min = number;
  }
});

console.log(max,min);

이렇게 10줄이 넘는 코드를 apply를 사용하면 어떻게 변하는지 살펴보겠습니다.

// 최대/최솟값을 구하는 코드(apply 사용)
var numbers = [203, 15, 26, 380, 52];
var max = Math.max.apply(window, numbers);
var min = Math.min.apply(window, numbers);
console.log(max, min);

(위 코드에선 배열을 입력 받을때 객체의 역할이 없으므로 null, window, this 모두 사용 가능합니다.)

이처럼 apply를 통해 여러 인자를 받음으로서 훨씬 간결하고 가독성이 좋은 코드가 만들어졌습니다.

bind 메서드
Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

bind 메서드는 call과 유사하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드입니다.

// 함수 실행, call로 실행, bind로 실행의 차이
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
var bd = func.bind({ y: 2 });
bd(7, 8, 9);        //{y:2} 7 8 9

예제를 보면 bind와 call의 차이를 조금 더 명확하게 이해하실 수 있습니다.

name 프로퍼티

bind 메서드를 적용해서 새로 만든 함수는 한 가지 독특한 성질이 있습니다.

바로 name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙는다는 점 입니다.

// bind 메서드의 name 프로퍼티
var func = function (a, b, c) {
  console.log(this, a, b, c);
};
var bd = func.bind({ y: 2 }, 7, 8, 9);
console.log(func.name); 			// func
console.log(bd.name);				// bound func

화살표 함수의 예외사항

ES6부터 도입된 화살표 함수는 실행 컨텍스트 생성시 this를 바인딩 하는 과정이 제외됐습니다.

즉 함수내에 this가 없으며, this에 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 됩니다. (우리가 원하던것!!)

var obj = {
  outer: function(){
    console.log(this);
    var innerFunc = () => {
      console.log(this);	// innerFunc의 내부에 this가 없어 outer의 this를 참조한다.
    };
    innerFunc();		// { outer: f }
  }
};
obj.outer();			// { outer: f }

⚙정리

명시적 this 바인딩이 없을 때 성립

  • 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)을 참조합니다.

  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조합니다. (점표기법, 대괄호표기법)

  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다.

  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조합니다.

  • 생성자 함수에서의 this는 생성될 인스턴스를 참조합니다.

명시적 this 바인딩을 할 때 (위의 규칙이 부합하지 않으면 명시적 this 바인딩을 한 것임)

  • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출합니다.

  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.

  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 합니다.

profile
기본을 탄탄하게🌳

2개의 댓글

comment-user-thumbnail
2022년 1월 21일

저는 코어 자바스크립트 책 2장 요약 정리하다가 뇌절와서 포기했는데... 정리한 것만으로도 대단하세요!

1개의 답글