Ama_grammer 2024. 9. 10. 21:57

- 목표

  1. this는 무엇일까?
  2. this를 써야하는 이유는?
  3. 함수 호출 방식에 의해 동적으로 결정되는 this 바인딩

- this 는 무엇일까?

자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수다.
this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.

 

Java나 C++ 같은 클래스 기반 언어에서 this는 언제나 클래스가 생성하는 인스턴스를 가리키지만,

JavaScript의 this는 함수가 호출되는 방식에 따라 this에 바인딩될 값, 즉 this 바인딩이 동적으로 결정된다.

 

※ JavaScript는 strict mode(엄격 모드) 역시 this 바인딩에 영향을 준다.


- this 사용 이유

객체는 상태를 나타내는 property와 동작을 나타내는 method를 하나의 논리적인 단위로 묶은 복합적인 자료구조다.

method는 자신이 속한 객체의 property를 참조하고 변경할 수 있어야한다. 이때 method가 자신이 속한 객체의

property를 참조하려면 method가 속한 객체의 식별자를 참조할 수 있어야한다.

 

이때 객체 리터럴 방식으로 생성된 객체는 내부에서 method 자신이 속한 객체를 가리키는 식별자를 재귀적으로 참조할 수 있다.

const bmi = {
    weight : 65,
    height : 180,
    getBMI() {
        return bmi.weight / bmi.height;
    }
}

console.log(bmi.getBMI()); // 0.3611111111111111

 

위의 코드를 살펴보면 객체 리터럴 방식으로 생성된 bmi 객체에는 각각 weight, height 가 property로 존재하고, getBMI라는 method가 있다. method내부에서 property를 참조하기위해 bmi.height, bmi.weight와 같이 getBMI가 속한 객체의 식별자 bmi를 붙여서 사용하는 것을 확인할 수 있다. 이렇게 사용하는 것을 객체 내부에서 식별자를 재귀적으로 참조하여 property에 접근한다고 한다.

 

하지만 자기 자신이 속한 객체를 재귀적으로 참조하는 방식은 좋은방식은 아니다.

만약 생성자 함수 방식으로 인스턴스를 생성한다면 어떨지 생각해보자.

function BMI (weight, height) {
    // 이 시점에서는 생성자 함수가 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
    ?.weight = weight;
    ?.height = height;
}
BMI.prototype.getBMI = function() {
    // 이 시점에서는 생성자 함수가 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
    return ?.weight / ?.height;
}
// 생성자 함수로 인스턴스 생성시, 생성자 함수를 먼저 정의해야한다.
const bmi = new BMI(80,190);

 

즉, 생성자 함수로 인스턴스 생성시 생성자 함수를 먼저 정의해야 인스턴스를 참조할 수 있다.

하지만, 생성자 함수를 정의하는 시점에는 아직 인스턴스를 생성하기 이전이기 때문에 생성자 함수가 생성할 인스턴스를 가리키는 식별자를 알 수 없다. 이를 위해 JavaScript는 this라는 특수 식별자를 사용한다.


- 함수 호출 방식에 의해 동적으로 결정되는 this 바인딩 값

앞서 말했지만 JavaScript는 this가 항상 class가 생성한 인스턴스를 가리키는 Java나 C++ 같은 클래스 기반 언어와는 달리, 함수가 호출되는 방식에 따라 동적으로 this의 값이 바인딩 된다.

 

this바인딩 값의 동적할당 하는 함수호출 방식의 종류는 아래와 같다.

  1. 일반 함수 호출
  2. 메서드 호출
  3. 생성자 함수 호출
  4. Function.prototype.(apply/call/bind) 메서드에 의한 간접 호출

1. 일반 함수 호출

일반 함수 호출에서의 this는 환경에 따라 바인딩 값이 다른데,

웹 환경에서는 전역객체 window를, node.js 환경에서는 global을 나타낸다.

중첩함수, 메서드 내부에 정의한 중첩함수, 콜백 함수를 포함한 모든 함수가 일반 함수로 호출 되는 경우 그 함수 내부의 this에는 전역 객체(web : window, node.js : global)가 바인딩된다.

 

※ 중첩 함수와 콜백 함수는 외부 함수를 돕는 헬퍼 함수 역할을 하기에 외부 함수의 일부 로직을 대신하는 경우가 대부분이다. 그런 상황에서 메서드 내에 정의한 중첩 함수와 메서드에게 전달한 콜백 함수가 일반 함수로 호출 될 때 this가 전역 객체를 바인딩 하는 것은 외부 함수인 메서드와 this가 일치하지 않게 되어 헬퍼 함수로 동작하기 어렵게 만든다.

이 불일치를 해결하기 위한 방법은 메서드 내부의 중첩 함수나 콜백 함수의 this 바인딩을 2depth에서 참조 할 수 있게 1depth에서 

const that = this;

 

위의 코드 형태로 this를 명시적으로 바인딩해주고 2depth(콜백함수, 중첩함수) 에서 this.(property) 가 아닌 that.(property)로 참조를 진행하면 불일치 문제를 해결할 수 있다.

+) Function.prototype.(apply/call/bind), arrow function 으로도 해결 가능.

const foo = function() {
    console.log(this);
}
foo(); // web : window, node : global

 

중첩함수 일경우

const foo = function () {
  console.log(this); // web : window, node : global
  function bar () {
    console.log(this); // web : window, node : global
  }
  bar();
}
foo();

 

중첩 함수를 일반 함수로 호출하면 함수 내부의 this에도 전역 객체가 바인딩된다.

하지만 this는 자기 참조 변수이므로 객체를 생성하지 않는 일반 함수에서의 this는 의미가 없다.

 

※ 일반 함수에 strict mode가 적용되면  this는 window/global이 아닌 undefined가 바인딩 된다.

const foo = function () {
  'use strict';
  console.log(this); // undefined
  function bar () {
    console.log(this); // undefined
  }
  bar();
}
foo();

 

2. 메서드 호출

 메서드 내부의 this에는 메서드를 호출한 객체가 바인딩 된다.

const product = {
    name: 'pencil',
    getProduct() {
       // method 내부 this는 method를 호출한 객체에 바인딩
       return this.name;
    }
}
// method getProduct를 호출한 객체는 product
console.log(product.getProduct()); // pencil

 

※ 메서드 호출에서 메서드 내부의 this는 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩 된다.

const product = {
    name: 'pencil',
    getProduct() {
        return this.name;
    }
}

const product2 = {
    name: 'eraser';
}
product2.getProduct = product.getProduct;

console.log(product2.getProduct()); // eraser

 

위의 코드와 같이 getProduct() 메서드를 소유한 객체는 product이지만, product2.getProduct = product.getProduct로 product2에서 getProduct 메서드가 동작하게 했을 때의 결과를 보면 메서드 호출에서 메서드 내부의 this가 메서드를 호출한 객체에 바인딩 되었음을 확인할 수 있다.

 

※ 프로토타입 메서드 내부에 사용된 this 또한 메서드를 호출한 객체에 바인딩된다.

function Product(name){
    this.name = name;
}
Product.prototype.getProduct = function() {
    return this.name;
}

const product1 = new Product('pencil');

// getProduct를 호출한 객체는 product1임
console.log(product1.getProduct()); // pencil

Product.prototype.name = 'eraser';
console.log(Product.prototype.getName()); // eraser

 

주의할 점은 'pencil'은 product1이라는 객체에 저장된것이고 'eraser'는 Product.prototype "객체"에 저장된 것이다.

3. 생성자 함수 호출

생성자 함수 내부 this에 생성자 함수가 생성할 인스턴스가 바인딩됨

※ 당연한 것이지만, new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수가 아닌, 일반 함수로 동작한다. 일반 함수로 동작시 this 또한 일반함수 호출 방식에 맞게 동적 할당

function Product(name) {
    this.name = name;
    this.getProduct = function() {
        return this.name;
    }
}
const product1 = new Product('pencil');
const product2 = new Product('eraser');

console.log(product1.getProduct()); // pencil
console.log(product2.getProduct()); // eraser

4. Function.prototype.(apply/call/bind) 메서드에 의한 간접 호출

위 제목을 보면 알겠지만, apply, call, bind는 Function.prototype 객체의 메소드이다.

19장 prototype에서 학습한 것에 따르면 특정 상황을 제외한 모든 객체에는 prototype이 상속되어 있다. 이것은 한 마디로 모든 함수가 이 메서드를 상속 받아 사용할 수 있다는 것이다.

 

※ arguments 객체는 배열이 아니기에 slice 같은 배열 관련 메서드를 사용할 수 없지만, apply와 call을 사용해 배열 관련 메서드를 사용 할 수 있게 된다.

- apply, call, bind 는 무엇인가?

함수 호출여부 메서드 동작
함수 호출 apply 호출할 함수의 인수를 배열로 묶어 전달
call 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달
함수 미호출 bind 첫 번째 인수로 전달한 값으로 this 바인딩이 교체된 함수를 새롭게 생성해 반환

 

- call

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

const thisArg1 = {a:1};
const thisArg2 = {b:2};
const thisArg3 = {c:3};

// {} 인수가 전달되진 않음
// Window {window: Window {...}, self: Window ...}
console.log(getThisBinding());

// {} 인수가 전달되진 않음
// call1 > (1) {a: 1}
console.log('call1',getThisBinding.call(thisArg1)); 

// {} 인수가 전달되진 않음
// call2 > (3) [{...}, {...}, {...}]
console.log('call2',getThisBinding.call([thisArg1,thisArg2,thisArg3]));

// (1) {0: Array(3)}
// call3 > (1) {a: 1}
console.log('call3',getThisBinding.call(thisArg1,[1,2,3]));

// (3) {0: 1, 1: 2, 2: 3}
// call4 > (1) {a: 1}
console.log('call4',getThisBinding.call(thisArg1,1,2,3));

 

- apply

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

const thisArg1 = {a:1};
const thisArg2 = {b:2};
const thisArg3 = {c:3};

// {} 인수가 전달되진 않음
// Window {window: Window {...}, self: Window ...}
console.log(getThisBinding());

// {} 인수가 전달되진 않음
// apply1 > (1) {a: 1}
console.log('apply1',getThisBinding.apply(thisArg1));

// {} 인수가 전달되진 않음
// apply2 > (3) [{...}, {...}, {...}]
console.log('apply2',getThisBinding.apply([thisArg1,thisArg2,thisArg3]));

// 호출할 함수의 인수를 배열로 묶어 전달
// (3) {0: 1, 1: 2, 2: 3}
// apply3 > (1) {a: 1}
console.log('apply3',getThisBinding.apply(thisArg1,[1,2,3]));

// TypeError: CreateListFromArrayLike called on non-object
console.log('apply4',getThisBinding.apply(thisArg1,1,2,3));

 

- bind

bind 메서드는 apply와 call 과는 달리 함수를 호출하지 않는다. 그렇기 때문에 함수를 호출하려면 명시적으로 호출해야한다.

bind 메서드는 메서드의 this와 메서드 내부 중첩 함수 혹은 콜백 함수의 this가 불일치하는 문제를 해결하는데 유용하다.

const product = {
  name: 'pencil',
  foo(callback) {
    setTimeout(callback,100);
  }
}
product.foo(function(){
  console.log(`Product name is ${this.name}.`); // Product name is ./Product name is undefined. 
});

 

위의 코드를 보면 우선 product 객체는 name이라는 property를 갖고 foo 라는 콜백 method를 갖는다.

foo 메서드 내부의 this는 2번 메서드 호출 함수에 따라 product가 바인딩된다. 하지만, 아래 product.foo는 콜백 함수가 1번의 일반 함수로 호출된 함수에 따라 this에 window/global 바인딩된다. (window/global).name 은 각각 ' '과 undefined이기 때문에 출력했을 경우 해당 영역은 ' '빈칸 혹은 undefined가 출력된다.

 

이 문제를 해결하기 위해 bind적용해 보자.

const product = {
  name: 'pencil',
  foo(callback) {
    setTimeout(callback.bind(this),100);
  }
}
product.foo(function(){
  console.log(`Product name is ${this.name}.`); // Product name is pencil.
});

- 함수 호출 방식 정리

함수 호출 방식 this에 바인딩 되는 값
일반 함수 호출 전역 객체(web : window, node.js : global)
메서드 호출 메서드를 호출한 객체
생성자 함수 호출 생성자 함수가 생성할 인스턴스
Function.prototype.(apply/call/bind)에 의한 간접 호출 apply, call, bind 메서드에 첫번째 인수로 전달할 객체

apply : 호출할 함수의 인수를 배열로 묶어 전달
call : 호출할 함수의 인수를 쉼표(,)로 구분한 리스트 형태로 전달
bind : 첫 번째 인수로 전달한 값으로 this 바인딩이 교체된 함수를 새롭게 생성해 반환

🔥 배운점

Java와 달리 JavaScript에서 this는 함수의 호출 방식에 따라 동적으로 바인딩 되는 것을 처음 알았다. 이 학습을 진행하기 전에 this는 Java와 비슷하게 블록 범위에서 사용한 경우를 주로 보고, 블록영역을 포함하는 객체만을 의미하는 줄 알았다. 알고 있는 개념이다 생각하고 깊이 볼 필요성을 못 느꼈다가 큰코다칠뻔 했다. 그리고, arguments가 객체형태라 배열 관련 메서드를 사용못하는데 apply와 call을 사용하면 배열 관련 메서드를 사용할 수 있다는 것 또한 처음알았다.

가능하면 this를 사용하는 상황을 더 만들어 이해한 개념을 적용해보는 시간을 갖어야할 것 같다.

 

- 참고

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this

 

this - JavaScript | MDN

JavaScript에서 함수의 this 키워드는 다른 언어와 조금 다르게 동작합니다. 또한 엄격 모드와 비엄격 모드에서도 일부 차이가 있습니다.

developer.mozilla.org