자바스크립트의 this

이희제·2024년 4월 8일
post-thumbnail

코어 자바스크립트 3장을 통해 자바스크립트의 this에 대해 알아보자.

일반적으로 다른 객체지향 언어에서의 this는 클래스로 생성한 인스턴스 객체를 의미한다.

하지만, 자바스크립트에서는 상황에 따하서 this가 바라보는 대상이 달라진다.


1. 상황에 따라 달라지는 this

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

즉, this는 함수를 호출할 때 결정되는데 함수를 어떻게 호출하는냐에 따라 this의 값이 달라진다.

전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킨다. 전역 객체는 자바스크립트 런타임 환경에 따라 다르다

  • 브라우저 환경의 전역 객체: window
  • Node.js 환경의 전역 객체: global

    • 정확히 전역에서 this는 module.export이고 함수 선언문 내에서 this는 global이다.

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

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

자바스크립트의 모든 변수를 특정 객체의 프로퍼티로서 동작한다.

여기서 특정 객체는 실행 컨텍스트의 LexicalEnvironment이다. 실행 컨텍스트는 변수를 수집해서 LexicalEnvironment의 프로터티로 저장한다.

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

함수 vs. 메서드

함수의 실행 방법은 다음과 같다.

  1. 함수로서 호출 - 그 자체로 독립적인 기능을 수행
  2. 메서드로서 호출 - 자신을 호출한 대상 객체에 관한 동작 수행

함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무소건 메서드가 아니라 객체의 메서드로서 호출할 경우만 메서드로 동작하고 아니면, 함수로 동작한다.

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

var obj = {
  method: func,
};
obj.method(2); // { method: f } 2, 메서드로서의 호출

메서드 내부에서의 this

함수를 메서드로서 호출하는 경우 호출 주체는 함수명 앞의 객체이다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 된다.

var obj = {
  methodA: function() {
    console.log(this); // this는 obj
  },
  inner: {
    methodB: function() {
      console.log(this); // this는 inner
    },
  },
};
obj.methodA(); // { methodA: f, inner: {...} }    ( === obj)
obj.inner.methodB(); // { methodB: f }            ( === obj.inner)

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

함수 내부에서의 this

함수를 함수로서 호출할 때 this는 전역 객체를 바라본다.

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

예시 코드를 통해 살펴보자

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

    var obj2 = {
      innerMethod: innerFunc,
    };
    obj2.innerMethod();
  },
};
obj1.outer();
  • (1)outer를 메서드로서 호출했기 때문에 thisobj1을 가리킨다.

  • (2)innerFunc를 함수로서 호출했기 때문에 this는 전역객체 Window를 바라본다.

  • (3)innerFunc를 메서드로서 호출했기 때문에 thisobj2를 바라본다

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

호출 주체가 없을 때 호출 당시 주변 환경의 this를 그대로 상속받을 수는 없을까?

ES5 기준으로 방법은 다음 예시 코드와 같이 변수를 활용하는 방법이다.

var obj = {
  outer: function() {
    console.log(this); // (1) { outer: f }
    var innerFunc1 = function() {
      console.log(this); // (2) Window { ... }
    };
    innerFunc1();

    var self = this; // 변수 self에 this를 할당한다. 
    var innerFunc2 = function() {
      console.log(self); // (3) { outer: f }
    };
    innerFunc2();
  },
};
obj.outer();

this를 바인당하지 않는 함수(Arrow Function)

ES6에서의 화살표 함수(arrow function)은 this를 바인딩 하지 앟는다.

화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정이 없다. 따라서 상위 스코프의 this를 활용할 수 있다.

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

화살표 함수인 innerFunc의 내에서 this는 상위 스코프인 obj를 가리킨다.

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

콜백 함수란?

콜백 함수는 전달인자로 다른 함수에 전달되는 함수 (출처: MDN)

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) html button element 출력
  console.log(this, e);
});

(3)addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의되어 있다. 따라서, 콜백 함수 내의 thisHTMLButtonElement를 바라본다.

생성자 함수 내부에서의 this

자바스크립드는 함수에 생성자로사 역할을 함께 부여했다. 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, nabi);

/* 결과
Cat { bark: '야옹', name: '초코', age: 7 }
Cat { bark: '야옹', name: '나비', age: 5 }
*/

위 예시를 보면 실행한 생성자 함수 내부에서 this는 각각 choco, nabi 인스턴스를 가리킨다.


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

call 메서드

func.call(thisArg[, arg1[, arg2[, ...]]])

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행한다.

call 메서드를 이용하면 임의의 객체를 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 메서드

func.apply(thisArg, [argsArray]);

apply 메서드는 기능적으로 call과 동일하다.

apply 메서드는 두 번짜 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다.

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

call/apply 메서드의 활용

유사배열객체(array-like object)에 배열 메서드 적용

객체에는 배열 메서드를 직접 적용할 수 없지만, 키가 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' ]

참고

Array.prototype.slice() 메서드는 어떤 배열의 begin 부터 end 까지(end 미포함)에 대한 얕은 복사본을 새로운 배열 객체로 반환

ES6에서는 유사배열객체 또는 순회 가능한 모든 종류으 데이터 타입을 배열로 전환하는 Array.from 메서드를 제공한다.

얕게 복사하여 새로운 Array 인스터스를 생성한다.(MDN)

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
};
var 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;
}
var by = new Student('보영', 'female', '단국대');
var jn = new Employee('재난', 'male', '구골');

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

ES5 이하 환경에서 여러 개의 인수를 받는 메서드에게 하나의 배열로 인수를 전달하고 싶을 때 apply 메서드를 활용할 수 있다.

var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min); // 45 3

ES6 환경에서는 Spread Operator를 통해 더 간단히 코드 작성이 가능하다.

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

현재 실무에서는 ES6 이상의 환경을 기반으로 코드를 작성하기 때문에 call/apply 메서드는 많이 사용되지 않을 것으로 보인다.

bind 메서드

func.bind(thisArg[, arg1[, arg2[, ...]]])

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

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 }); // this만 미리 적용한 함수
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

name 프로퍼티

bind 메서드를 적용해 새로 만든 함수는 name 프로퍼티에 'bound' 접두어가 붙는다.

기존의 call, apply보다 코드를 추적하기 수월하다.

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

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

call, apply, bind를 통해 내부함수에 this를 전달할 수 있다.

var obj = {
  outer: function() {
    console.log(this);
    var innerFunc = function() {
      console.log(this);
    };
    innerFunc.call(this); // obj를 바라보는 this 전달
  },
};
obj.outer();

/////////////////////////////////////////////////////

var obj = {
  outer: function() {
    console.log(this);
    var innerFunc = function() {
      console.log(this);
    }.bind(this); // obj를 바라보는 this 전달
    innerFunc();
  },
};
obj.outer();

콜백 함수 내에서의 this에 관여하는 함수 또는 메서드에 대해서는 bind 메서드를 통해 변경할 수 있다.

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

화살표 함수의 예외사항

ES6에 동입된 화살표 함수의 내부에서의 this는 스코프체인상 가장 가까운 this에 접근하게 된다.

var obj = {
  outer: function() {
    console.log(this);
    var innerFunc = () => {
      console.log(this);  // this는 obj를 가리킨다.
    };
    innerFunc();
  },
};
obj.outer();

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

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있다.

이런 형태는 배열 메서드에 많이 포진돼 있으며, ES6에서 새로 등장한 Set, Map 등의 메서드에도 존재한다.

대표적으로 forEach 배열 메서드를 기반으로 예시 코드를 살펴보자.

var report = {
  sum: 0,
  count: 0,
  add: function() {
    var args = Array.prototype.slice.call(arguments);
    args.forEach(function(entry) {
      this.sum += entry;
      ++this.count;
    }, this); // add 메서드의 this를 전달했기 때문에 report를 가리킨다
  },
  average: function() {
    return this.sum / this.count;
  },
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80
profile
그냥 하자

0개의 댓글