CoreJS - this

SANGKU OH·2020년 11월 29일
1

CoreJavascript

목록 보기
6/10
post-thumbnail

This

상황에 따라 this가 바라보는 대상이 달라지는데, 어떤 이유로 그렇게 되는지를 파악하기 힘든 경우도 있고, 예상과 다르게 엉뚱한 대상을 바라보는 경우도 있다.

함수와 객체(메소드)의 구분이 느슨한 자바스크립트에서 this는 실질적으로 이 둘을 구분하는 거의 유일한 기능이다!

상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다.

실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다고 할 수 있다. 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라지는 것이다.

전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킨다.
개념상 전역 컨텍스트를 생성하는 주체가 바로 전역 객체이기 때문이다.

console.log(this); // { alert: f(), atob: f(), blur: f(), ... }
console.log(window); // { alert: f(), atob: f(), blur: f(), ... }
console.log(this === window); // true

전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다.
변수이면서 객체이기도 한 셈이다.

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

자바스크립트의 모든 변수는 실은 특정한 객체의 프로퍼티로써 동작한다.
사용자가 var 연산자를 이용해 변수를 선언하더라도 실제 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식하는 것이다.

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

함수 vs 메소드

어떤 함수를 실행하는 방법은 여러가지가 있다. 가장 일반적인 방법 두 가지는 함수로서 호출하는 경우와 메소드로 호출하는 경우다.
함수와 메소드는 미리 정의한 동작을 수행하는 코드 뭉치다.
이 둘을 구분하는 유일한 차이는 독립성이다.
함수는 그 자체로 독립적인 기능을 수행하는 반면, 메소드는 자신을 호출한 대상 객체에 관한 동작을 수행을 한다. 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했다.

어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메소드가 되는 것이 아니다.
객체의 메소드로서 호출할 경우에만 메소드로 동작하고, 그렇지 않으면 함수로 동작한다.

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

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

처음에 함수를 호출할 때는 this로 window가 출력되고, obj라는 객체에 func을 메소드로 담아주고 호출했더니, 이번에는 this가 obj라고 한다.

메소드 내부에서의 this

this에는 호출한 주체에 대한 정보가 담긴다!

어떤 함수를 메소드로서 호출하는 경우, 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체이다.
즉, 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 되는 것이다.

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

함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우 this가 지정되지 않는다.

this에는 호출한 주체에 대한 정보가 담긴다. 그런데 함수로서 호출하는 것은 호출 주체(객체지향언어에서의 객체)를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없는 것이다. 이전에 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역 객체를 바라본다고 알아봤다. 따라서 함수의 this는 전역 객체를 가리킨다.

메소드의 내부함수에서의 this

내부함수 역시 이를 함수로서 호출했는지 메소드로서 호출했는지만 파악하면 this의 값을 정확히 맞출 수 있다.

let obj = {
  outer: function() {
    console.log(this); // obj
    let innerFunc = function() {
      console.log(this); // 전역객체(window), obj2
    }
    innerFunc();
    
    let obj2 = {
      innerMethod: innerFunc
    };
    obj2.innerMethod();
  }
};
obj.outer();

7번째 줄에서는 outer 메소드 내부에 있는 함수(innerFunc)를 함수로서 호출했다. 반면 12번째 줄에서는 같은 함수(innerFunc)를 메소드로서 호출했다. 같은 함수임에도 7번째 줄에 의해 바인딩되는 this와 12번째 줄에 의해 바인딩되는 this의 대상이 서로 달라진 것!

즉, this 바인딩에 관해서는 함수를 실행하는 당시의 주변환경(메소드 내부인지, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건인 것이다.

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

이렇게 하면 this에 대한 구분은 명확히 할 수 있지만, 그 결과 this라는 단어가 주는 인상과는 사뭇 달라진다. 호출 주체가 없을 때는 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있다면 좋겠다.

그게 훨씬 자연스러울뿐더러 자바스크립트 설계상 이렇게 동작하는 편이 스코프 체인과의 일관성을 지키는 방법이다. 변수를 검색하면 우선 가장 가까운 스코프의 L.E를 찾고 없으면 상위 스코프를 탐색하듯이, this 역시 현재 컨텍스트에 바인딩된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 말이다.

내부함수에서의 this를 우회하는 방법

let obj = {
  outer: function() {
    console.log(this) // { outer: f }
    let innerFunc1 = function() {
      console.log(this) // window { ... }
    };
    innerFunc1();
    
    let self = this;
    let innerFunc2 = function() {
      console.log(self); // { outer: f }
    };
    innerFunc2();
  }
};
obj.outer();
  • innerFunc1 내부에서 this는 전역객체를 가리킨다.
  • outer 스코프에서 self라는 변수에 this를 상태에서 호출한 innerFunc2의 경우 self에는 객체 obj가 출력된다.
    그저 상위 스코프의 this를 저장해서 내부함수에서 활용하는 것이다.

this를 바인딩하지 않는 함수

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수를 새로 도입했다!

화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다.

this 바인딩하지 않는 함수(화살표 함수)

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

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

함수 A의 제어권을 다른 함수(메소드) B에게 넘겨주는 경우 A함수를 콜백함수라고 한다.
이때 함수A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부로직에서 정한 규칙에 따라 값이 결정된다.
콜백 함수도 함수이기 때문에 기본적으로 this가 전역 객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.

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

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

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
	.addEventListener('click', function(e) { // (3)
	  console.log(this, e);
});

(1): setTimeout 함수는 300ms 만큼 시간 지연을 한 뒤 콜백 함수를 실행하라는 명령이다. 0.3초 뒤 전역객체가 출력된다.
(2): forEach 메소드는 배열의 각 요소를 앞에서 부터 차례로 하나씩 꺼내어 그 값을 콜백 함수의 첫번째 인자로 삼아 함수를 실행하라는 명령이다. 전역객체와 배열의 각 요소가 총 5회 출력된다.
(3): addEventListener는 지정한 HTML 엘리먼트에 'click' 이벤트가 발생할 때마다 그 이벤트 정보를 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력된다.

(1)의 setTimeout 함수와 (2)의 forEach 메소드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않는다. 따라서 콜백 함수 내부에서의 this는 전역객체를 참조한다.
한편 (3)의 addEventListener 메소드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의되어 있다. 즉, 메소드명의 점(.)의 앞 부분이 곧 this가 되는 것이다.

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

앞에서 상황별로 this에 어떤 값이 바인딩되는지를 살펴봤다.
이러한 규칙을 깨고 this에 별도의 대상을 바인딩하는 방법도 있다.

call 메소드

function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

call 메소드는 메소드의 호출 주체인 함수를 즉시 실행하도록 하는 명령어다.
이 때 call 메소드의 첫 번재 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다. 함수를 그냥 실행하면 this는 전역객체를 참조하지만 call 메소드를 이용하면 임의이 객체를 this로 지정할 수 있다.

call 메소드(1)

let 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 메소드를 이용하면 임의의 객체를 this로 지정할 수 있다.

call 메소드(2)

let 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 메소드

Function.prototype.apply(thisArg[, argsArray])

apply 메소드는 call 메소드와 기능적으로 완전히 동일하다!
call 메소드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면, apply 메소드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있다.

apply 메소드

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

let 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 메소드의 활용

유사배열객체에 배열 메소드 적용

let 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 }

let arr = Array.prototype.slice.call(obj);
console.log(arr); // [ 'a', 'b', 'c', 'd' ]

객체에는 배열 메소드를 직접 적용할 수 없다.
그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고, length 프로퍼티의 값이 0 또는 양의 정수인 객체, 즉 배열 구조와 유사한 객체의 경우(유사배열객체) call 또는 apply 메소드를 이용한 배열 메소드를 차용할 수 있다.
slice 메소드는 원래 시작 인덱스값과 마지막 인덱스값을 받아 시작값부터 마지막값의 앞 부분까지 배열 요소를 추출하는 메소드인데, 매개변수를 아무것도 넘기지 않을 경우에는 그냥 원본 배열의 얕은 복사본을 반환한다. 즉, call 메소드를 이용해 원본인 유사배열객체의 얕은 복사를 수행한 것인데, slice가 배열 메소드이기에 복사본은 배열로 반환하게 된 것이다.

사실 call / apply를 이용해 형변환하는 것은 'this를 원하는 값으로 지정해서 호출한다'라는 본래의 메소드 의도와는 다소 동떨어진 활용법이다. slice 메소드는 오직 배열 형태로 '복사'하기 위해 차용됐을 뿐이니..ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메소드를 새로 도입했다!

Array.from 메소드

let obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
let arr =Array.from(obj);
console.log(arr) // [ 'a', 'b', 'c' ]

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

생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있다.
*** 생성자 내부에서 다른 생성자를 호출

function Person(name, gender){
  this.name = name;
  this.gender = gender;
}
function Student(name, gender, school){
  Person.call(this, name, gender);
  this.school = school;
}
function Employee(name, gender, company){
  Person.apply(this, [name, gender]);
  this.company = company;
}
let by = new Student('보영', 'female', '단국대');
let jn = new Employee('재난', 'male', '구골');

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

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

let numbers = [10, 20, 3, 4, 5];
let max = Math.max.apply(null, numbers);
let min = Math.min.apply(null, numbers);
console.log(min, max); // 3 20

참고로 펼치기 연산자를 이용하면 apply를 적용하는 것 보다 더욱 간편한 코드 작성이 가능하다!

const numbers = [10, 20, 3, 4, 5];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(min, max) // 3 20

call/apply 메소드는 명시적으로 별도의 this를 바인딩하면서 함수 또는 메소드를 실행하는 좋은 방법이지만, 이로 인해 this를 예측하기 어렵게 만들어 코드 해석을 방해한다는 단점이 있다.

bind 메소드

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

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

this 지정과 부분 적용 함수 구현

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

let bindFunc1 = func.bind({x: 1});
bindfunc1(1, 2, 3, 4) // { x: 1 } 1 2 3 4

let 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

name 프로퍼티

bind 메소드를 적용해서 새로 만든 함수는 독특한 성질이 있다.
바로 name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙는다는 점이다.
어떤 함수의 name 프로퍼티가 'bound xxx'라면 이는 곧 함수명이 xxx인 원본 함수에 bind 메소드를 적용한 새로운 함수라는 의미가 되므로 기존의 call 이나 apply보다 코드를 추적하기에 더 수월해진 면이 있다.

let func = function(a, b, c, d){
  console.log(this, a, b, c, d);
};
let bindFunc = func.bind({x: 1}, 4, 5);
console.log(func.name); // func
console.log(bind.bindFunc) // bound func

상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

위에서 메소드의 내부함수에서 메소드의 this를 그대로 바라보게 하기 위한 방법으로 self 등의 변수를 활용한 우회법을 소개했는데, call, apply 또는 bind 메소드를 이용하면 더 깔끔하게 처리가 가능하다.

내부함수에 this 전달 call / bind

let obj = {
  outer function() {
    console.log(this);
    let innerFunc = function() {
      console.log(this);
    };
    innerFunc.call(this);
  }
};
obj.outer();

let obj = {
  outer: function () {
    console.log(this);
    let innerFunc = function() {
      console.log(this);
    }.bind(this);
    innerfunc();
  }
};
obj.outer();

또한 콜백 함수를 인자로 받는 함수나 메소드 중에서 기본적으로 콜백 함수내에서의 this에 관여하는 함수 또는 메소드에 대해서도 bind 메소드를 이용하면 this 값을 사용자의 입맛에 맞게 바꿀 수 있다!

let obj = {
  logThis: function() {
    console.log(this);
  },
  logThisLater1: function() {
    setTimeout(this.logThis, 500);
  },
  logThisLater2: function() {
    setTimeout(this.logThis.bind(this), 1000);
  }
};
obj.logThisLater1(); // window{ ... }
obj.logThis.later2(); // obj { logThis: f, ... }  

화살표 함수의 예외사항

화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외된다.
즉, 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.

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

내부함수를 화살표 함수로 바꾸면 별도의 변수로 this를 우회하거나 call/apply/bind를 적용할 필요가 없어 더욱 간결하고 편리한 코드 작성이 가능하다.

별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

콜백 함수를 인자로 받는 메소드 중 일부는 추가로 this를 지정할 객체를 인자로 지정할 수 있는 경우가 있다.
이러한 메소드의 인자값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있다. 이런 형태는 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메소드에 많이 포진되어 있으며, set, map 등의 메소드에도 일부 존재한다.

let report = {
  sum: 0,
  count: 0,
  add: function() {
    let args = Array.prototype.slice.call(arguments);
    args.forEach(function(entry){
      this.sum += entry;
      ++this.count;
    }, this);
  },
  average: function() {
    return this.sum / this.count;
  }
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80
  • report 객체에는 sum, count 프로퍼티가 있고, add, average 메소드가 있다.

  • 5번째 줄에서 add 메소드는 arguments를 배열로 변환해서 args 변수에 담고, 6번째 줄에는 이 배열을 순회하면서 콜백 함수를 실행하는데, 이때 콜백 함수 내부에서의 this는 forEach 함수의 두 번째 인자로 전달해준 this(9번째 줄)가 바인딩된다.

  • 11번째 줄의 average는 sum 프로퍼티를 count 프로퍼티로 나눈 결과를 반환하는 메소드이다.

  • 15번째 줄에서 60, 85, 95를 인자로 삼아 add 메소드를 호출하면 이 세 인자를 배열로 만들어 forEach 메소드가 실행된다.

  • 콜백 함수 내부에서 this는 add 메소드에서의 this가 전달된 상태이므로 add 메소드의 this(report)를 그대로 가리키고 있다.

  • 배열을 세 요소를 순회하면서 report.sum 값 및 report.count 값이 차례로 바뀌고, 순회를 마친 결과 report.sum 에는 240, report.count에는 3이 담기게 된다.

정리

다음 규칙은 명시적 this 바인딩 없는 한 늘 성립한다.

  • 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조한다.
  • 어떤 함수를 메소드로 호출한 경우 this는 메소드 호출 주체(메소드명 앞의 객체)를 참조한다.
  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조한다. 메소드의 내부함수에서도 같다.
  • 콜백 함수 내부에서의 this는 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우 전역 객체를 참조한다.
  • 생성자 함수에서의 this는 생성될 인스턴스를 참조한다.

다음은 명시적 this 바인딩이다.

  • call, apply 메소드는 this를 명시적으로 지정하면서 함수 또는 메소드를 호출한다.
  • bind 메소드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만든다.
  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메소드는 별도의 인자로 this를 받기도 한다.
profile
Prof.Google을 통해 필요한 정보를 이 곳에 insert 🐸

0개의 댓글