본문 바로가기

TIL

JS. this

this에 대해 공부하다가 헷갈리는 부분들이 있어서 간단하게 정리하려고 한다. 자바스크립트 this는 다른 객체지향 언어들의 this와 좀 다르게 this에 바인딩되는 객체가 한 가지가 아니라 해당 함수 호출 방식에 따라 this에 바인딩되는 객체가 달라진다.  이게 왜 이런 식으로 설계되었는지는 저번에 프로토타입 글에서 공유한 '자바스크립트는 왜 프로토타입을 선택했을까? '글을 보면 자바스크립트의 철학을 통해 조금은 알 수 있다.

자바스크립트의 경우 함수 호출 방식에 의해 this에 바인딩할 어떤 객체가 동적으로 결정된다. 함수를 선언할 때 this에 바인딩할 객체가 정적으로 결정되는 것이 아니라 함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정된다. 여기서 저번에 말한 거처럼 자바스크립트라는 언어가 문맥을 중요하게 생각하는 걸 느낄 수 있는 거 같다. 또 선언이랑 호출 같은 얘기 하니까 렉시컬 스코프가 생각나는데, this는 동적으로 바인딩한다.

함수 호출

기본적으로 브라우저에서 전역으로 this는 window를 가리킨다. node에서는 global을 가리킨다고 한다. 아래 같은 경우는 어떨까?

var value = 1;

var obj = {
  value: 100,
  foo: function() {
    console.log("foo's this: ",  this);  // obj
    console.log("foo's this.value: ",  this.value); // 100
    function bar() {
      console.log("bar's this: ",  this); // window
      console.log("bar's this.value: ", this.value); // 1
    }
    bar();
  }
};

obj.foo();

메서드 내부 함수일 경우에 this는 전역 객체에 바인딩되는 결과를 보여준다. 콜백 함수도 같은 결과를 보여준다. 

이렇게 하기 싫으면 뒤에서 얘기하겠지만, apply, call, bind를 사용해서 함수에 객체를 바인딩해준다. 아니면 다른 변수를 선언해서 내부 함수에서 그걸 사용하면 된다.

메소드 호출

함수가 객체의 프로퍼티 값이면 메서드로서 호출된다. 메소드 내부의 this는 해당 메서드를 소유한 객체에 바인딩됨.

var obj1 = {
  name: 'Lee',
  sayName: function() {
    console.log(this.name);
  }
}

var obj2 = {
  name: 'Kim'
}

obj2.sayName = obj1.sayName;

obj1.sayName();
obj2.sayName();

obj1은 Lee를 출력하고 obj2는 Kim을 출력한다. 

생성자 함수 호출

다음은 생성자 함수 동작 방식이다. new 연산자와 함께 생성자 함수를 호출하면 다음과 같은 수순으로 동작한다.

1. 빈 객체 생성 및 this 바인딩
생성자 함수의 코드가 실행되기 전 빈 객체가 생성된다. 이 빈 객체가 생성자 함수가 새로 생성하는 객체이다. 이후 생성자 함수 내에서 사용되는 this는 이 빈 객체를 가리킨다. 그리고 생성된 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.

2. this를 통한 프로퍼티 생성
생성된 빈 객체에 this를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다. this는 새로 생성된 객체를 가리키므로 this를 통해 생성한 프로퍼티와 메소드는 새로 생성된 객체에 추가된다.

3. 생성된 객체 반환
반환문이 없는 경우, this에 바인딩된 새로 생성한 객체가 반환된다.
명시적으로 this를 반환하여도 결과는 같다.반환문이 this가 아닌 다른 객체를 명시적으로 반환하는 경우, this가 아닌 해당 객체가 반환된다. 이때 this를 반환하지 않은 함수는 생성자 함수로서의 역할을 수행하지 못한다. 따라서 생성자 함수는 반환문을 명시적으로 사용하지 않는다.
function Person(name) {
  // 생성자 함수 코드 실행 전 -------- 1
  this.name = name;  // --------- 2
  // 생성된 함수 반환 -------------- 3
}

var me = new Person('Lee');
console.log(me.name);

 

 

그럼 객체 리터럴과 생성자 함수의 차이는 뭘까?  객체 리터럴 방식과 생성자 함수의 차이는 프로토타입 객체에 있는데, 객체 리터럴 같은 경우 생성된 객체의 프로토타입 객체는 Object.prototype이고 생성자 함수 방식은 그 생성자 함수. prototype이다.

생성자 함수를 new 없이 호출한 경우 this는 전역을 가리키게 된다. 그리고 암묵적으로 반환하던 this도 반환하지 않으며, undefined를 반환한다. 그래서 이러한 위험성을 피하기 위해 사용되는 패턴, Scope-safe Constructor라고 하는데 다음과 같다. callee는 arguments객체의 프로퍼티로 함수 바디 내에서 현재 실행 중이 함수를 참조할 때 사용한다.

// Scope-Safe Constructor Pattern
function A(arg) {
  // 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈객체를 생성하고 this에 바인딩한다.

  /*
  this가 호출된 함수(arguments.callee, 본 예제의 경우 A)의 인스턴스가 아니면 new 연산자를 사용하지 않은 것이므로 이 경우 new와 함께 생성자 함수를 호출하여 인스턴스를 반환한다.
  arguments.callee는 호출된 함수의 이름을 나타낸다. 이 예제의 경우 A로 표기하여도 문제없이 동작하지만 특정함수의 이름과 의존성을 없애기 위해서 arguments.callee를 사용하는 것이 좋다.
  */
  if (!(this instanceof arguments.callee)) {
    return new arguments.callee(arg);
  }

  // 프로퍼티 생성과 값의 할당
  this.value = arg ? arg : 0;
}

var a = new A(100);
var b = A(10);

console.log(a.value);
console.log(b.value);

마지막으로 간단하게  apply, call, bind 호출을 살펴보고 끝내겠다. 원래 this에 바인딩될 객체는 함수 호출 패턴에 의해 결정되는데 이는 자바스크립트 엔진이 수행한다. 이러한 자바스크립트 엔진의 암묵적 this바인딩 이외에 this를 특정 객체에 명시하는 방법이 바로 위의 메소드들이다. 이 메소드는 Function.prototype 객체의 메소드다. 예제 코드를 보자.

var Person = function (name) {
  this.name = name;
};

var foo = {};

// apply 메소드는 생성자함수 Person을 호출한다. 이때 this에 객체 foo를 바인딩한다.
Person.apply(foo, ['name']);

console.log(foo); // { name: 'name' }

첫 번째 인자는 바인딩할 객체를 넣고 apply는 두 번째 인자에 arg 를 배열로 넣는다. call은 arg를 그냥 값으로 하나씩 넘긴다. 이 함수들은 콜백 함수의 this를 위해서 사용된다.

function Person(name) {
  this.name = name;
}

Person.prototype.doSomething = function (callback) {
  if (typeof callback == 'function') {
    callback.call(this);
  }
};

function foo() {
  console.log(this.name);
}

var p = new Person('Lee');
p.doSomething(foo);  // 'Lee'

여기서 callback.call을 안 했다면 undefined가 나왔을 것이다. 왜냐면 callback을 호출하기 전 doSomething에서 는 Person객체지만 callback에서는 window를 가리키기 때문이다. 다음은 apply함수의 다른 사용 예시다.

function convertArgsToArray() {
  console.log(arguments);

  // arguments 객체를 배열로 변환
  // slice: 배열의 특정 부분에 대한 복사본을 생성한다.
  var arr = Array.prototype.slice.apply(arguments); // arguments.slice
  // var arr = [].slice.apply(arguments);

  console.log(arr);
  return arr;
}

convertArgsToArray(1, 2, 3);

여기서 arguments를 apply 해줬으니까 this는 arguments 객체로 바인딩하라는 말이다. 즉 위의 메소드를 arguments 객체의 메소드인 것처럼 arguments.slice() 와 같은 형태로 호출하라는 것이다.

bind는 위의 둘과 다르게 바인딩된 함수를 리턴한다. 

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

 

개념은 전체적으로 봤는데 실제로 코드를 작성하면서 익혀야겠다. 헷갈리지 않으려면 위에서 말한 것처럼 코드의 문맥 그리고 주체가 누구인지를 항상 생각하면서 코드를 봐야 할 것 같다.

 

참고:

https://poiemaweb.com/js-this

 

this | PoiemaWeb

자바스크립트의 this keyword는 Java와 같은 익숙한 언어의 개념과 달라 개발자에게 혼란을 준다. Java에서의 this는 인스턴스 자신(self)을 가리키는 참조변수이다. this가 객체 자신에 대한 참조 값을

poiemaweb.com

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

 

Function.prototype.bind() - JavaScript | MDN

bind() 메소드가 호출되면 새로운 함수를 생성합니다. 받게되는 첫 인자의 value로는 this 키워드를 설정하고, 이어지는 인자들은 바인드된 함수의 인수에 제공됩니다.

developer.mozilla.org

 

'TIL' 카테고리의 다른 글

TS. 타입스크립트 시작전에  (0) 2022.01.19
DOM. 관련 개념 정리  (0) 2022.01.19
JS. Closure  (0) 2022.01.17
Redux, Redux-toolkit은?  (0) 2022.01.16
JS. 자바스크립트 번들러 그전에 모듈 시스템  (0) 2022.01.15