this의 동적 바인딩과 예외사항 정리

개발 log·2021년 9월 16일
0

JS 지식

목록 보기
16/36
post-thumbnail

this 바인딩

this

자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수

this 바인딩

함수 호출 방식에 의해 동적으로 결정된다.

함수 호출 방식this 바인딩
일반함수로서 호출전역객체
메서드로서 호출메서드를 호출한 객체(마침표 앞의 객체)
생성자함수로서 호출생성자 함수가 생성할 인스턴스
Function.prototype 메서드에 의한 간접호출call / apply / bind 메서드의 첫번째 인수)

화살표 함수는 this가 다르게 동작하기 때문에 언급하기 전까지는 함수 선언문 또는 표현식으로 예제를 작성할 것이다.




함수 호출 방식 예제

const foo = function() {
  console.dir(this);
};

// 1. 일반 함수 호출
foo(); // window

// 2. 메서드 호출
const obj = { foo };
obj.foo(); // obj

// 3. 생성자 함수 호출
new foo(); // foo{}

// 4. Function.prototype.call/apply/bind 메서드에 의한 간접호출
const bar = { name: 'bar' };

foo.call(bar); // bar
foo.apply(bar); // bar
foo.bind(bar)(); // bar



일반함수로써 호출

일반함수로 호출 된 모든 함수(중첩함수, 콜백함수 포함) 내부의 this에는 전역 객체 바인딩

function foo() {
  console.log(this); // window
  
  function bar() {
    console.log(this); // window
  }
  
  // 일반 함수로써 호출
  bar();
}

// 일반 함수로써 호출
foo();

콜백함수로써 호출

var value = 1;

const obj = {
  value: 100,
  foo() {
    console.log(this); // {value: 100, foo: ⨍}
    
    // 콜백함수의 내부의 this에는 전역 객체가 바인딩된다.
    setTimeout(function() {
      console.log(this); // window
      console.log(this.value); // 1
    }, 100);
  }
};

// 메서드로써 호출
obj.foo();



메서드로써 호출

const circle = {
  radius: 5,
  getDiameter() {
    // 메서드로써 호출되었기 때문에 this는 메서드를 호출한 객체 circle을 가리킨다.
    return 2 * this.radius;
  }
};

// 메서드로써 호출
console.log(circle.getDiameter()); // 10



생성자함수로써 호출

// 생성자 함수
function Circle(radius) {  
  
  // this는 생성자 함수가 생성할 인스턴스를 가리킨다.
  this.radius = radius;
}

Circle.prototype.getDiameter = function() {
  
  // this는 생성자 함수가 생성할 인스턴스를 가리킨다.
  return 2 * this.radius;
};

// 인스턴스 생성
// 생성자 함수로써 호출
const circle = new Circle(5);
console.log(circle.getDiameter()); // 10



Function.prototype 메서드에 의한 간접호출

call / apply / bind

function getThisBinding() {
  console.log(arguments);
  return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };


// getThisBinding 함수를 호출하며 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩

// call 메서드는 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달
console.log(getThisBinding.call(thisArg, 1, 2, 3));
// Arguments(3) [1, 2, 3, callee: ⨍, Symbol(Symbol.iterator): ⨍]
// {a: 1}

// apply 메서드는 호출할 함수의 인수를 배열로 묶어서 전달
console.log(getThisBinding.apply(thisArg, [1, 2, 3])); 
// Arguments(3) [1, 2, 3, callee: ⨍, Symbol(Symbol.iterator): ⨍]
// {a: 1}

// bind 메서드는 함수를 호출하지는 않으므로 명시적으로 호출해야 한다.
console.log(getThisBinding.bind(thisArg)(1,2,3);   
// Arguments(3) [1, 2, 3, callee: ⨍, Symbol(Symbol.iterator): ⨍]
// {a: 1}



예외 사항

strict mode

this는 객체의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수이므로
일반적으로 객체의 메서드 내부 또는 생성자 함수 내부에서만 의미가 있다.

때문에 strict mode가 적용된 일반 함수 내부의 this에는 undefined가 바인딩 된다.

function foo() {
  'use strict';
  
  console.log(this); // undefined
  
  function bar() {
    console.log(this); // undefined
  }
  
  // 일반 함수로써 호출
  bar();
}

// 일반 함수로써 호출
foo();

화살표 함수

화살표 함수의 예외 사항을 알기전에 왜 화살표 함수는 다르게 설계되었는지 알아보도록 하자

콜백함수 내부의 this문제

콜백함수 내부의 this가 외부 함수의 this와 다르기 때문에 발생하는 문제를 해결하기 위해 의도적으로 this를 다르게 설계했다.

class Prefixer {
  constructor(prefix) {
    this.prefix = prefix;
  }
  
  add(arr) {
    return arr.map(function(item) {
      return this.prefix + item;
      // TypeError: Cannot read property 'prefix' of undefined
    });
  }
}

const prefixer = new Prefixer('-webkit-');
console.log(prefixer.add(['transition', 'user-select']));

위의 예제를 보면 Array.prototype.map 메서드가 콜백 함수를 일반함수로써 호출하기 때문에 class 내부는 use strict 모드가 적용되어 일반함수의 this에는 undefined가 바인딩 되므로 객체가 아니라는 TypeError가 발생하게 된다.


ES6이전 해결방안

콜백함수 내부의 this문제를 ES6이전에는 아래와 같은 방법들로 해결했다.


1. this를 회피시키고 콜백 함수 내부에서 사용하는 방법

add(arr) {
  
  // this 회피
  const that = this;
  return arr.map(function(item) {
    
    // this 대신 that을 참조
    return that.prefix + ' ' + item;
  });
}

2. Array.prototype.map의 두번째 인수로 add 메서드를 호출한 prefixer 객체를 가리키는 this를 전달하는 것

ES5에서 도입된 Array.prototype.map은 콜백함수 내부의 this문제를 해결하기 위해 두번째 인수로 콜백함수 내부에서 this로 사용할 객체를 전달할 수 있다.

add(arr) {
  return arr.map(function(item) {
    return this.prefix + ' ' + item;
  }, this); // this에 바인딩된 값이 콜백함수 내부의 this에 바인딩된다.
}

3. Function.prototype.bind 메서드를 사용하여 add 메서드를 호출한 prefixer 객체를 가리키는 this를 바인딩 한다.

add(arr) {
  return arr.map(function(item) {
    return this.prefix + ' ' + item;
  }.bind(this)); // this에 바인딩된 값이 콜백함수 내부의 this에 바인딩된다.
}

화살표함수로 해결

화살표 함수는 함수 자체의 this 바인딩을 갖지 않으므로 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조한다.

class Prefixer {
  constructor(prefix) {
    this.prefix = prefix;
  }
  
  add(arr) {
    return arr.map(item => this.prefix + item);
  }
}

const prefixer = new Prefixer('-webkit-');
console.log(prefixer.add(['transition', 'user-select']));

Lexical this

렉시컬 스코프와 같이 화살표 함수의 this가 함수가 정의된 위치에 의해 결정된다는 것




주의 사항

렉시컬 스코프와 this 바인딩

렉시컬 스코프와 this 바인딩은 결정 시기가 다르다

  • 렉시컬 스코프: 함수 정의가 평가되어 함수 객체가 생성되는 시점에 상위 스코프 결정

  • this 바인딩: 함수 호출 시점에 결정

profile
프론트엔드 개발자

0개의 댓글