[Javascript] 19. this

SebellKO·2022년 12월 1일
0

Javascript

목록 보기
21/23
post-thumbnail

내가 이해한대로 정리한 내용이니 정확하지 않을 수 있다. 😉

this

19.1 this 키워드

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

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

함수를 호출하면 arguments 객체와 this 가 암묵적으로 함수 내부에 전달된다. arguments 객체를 지역 변수처럼 사용할 수 있는 것처럼 this 도 지역 변수처럼 사용할 수 있는데, this 가 가르키는 값, 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.

const  circle = {
  radius: 5,
  getDiameter() {
    return 2 * this.radius;
  }
};

console.log(circle.getDiameter()); // 10

객체 리터럴의 메서드 내부에서의 this 는 메서드를 호출한 객체, 즉 circle 을 가르킨다.

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

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

const circle = new Circle(5);
console.log(circle.getDiameter()); // 10

생성자 함수 내부의 this 는 생성자 함수가 생성할 인스턴스를 가르킨다.

자바스크립트의 this 는 다른 언어와는 다르게 바인딩이 동적으로 결정된다.


// this는 어디서든지 참조 가능하다.
// 전역에서 this는 전역 객체 window를 가르킨다.
console.log(this); // window

function square(number) {
  // 일반 함수 내부에서 this는 전역객체 window를 가르킨다
  console.log(this); // window
  return number * number;
}
square(2);

const person = {
  name: 'Lee',
  getName() {
    // 객체 리터럴 내부의 메서드에서 this는 메서드를 호출한 객체를 가르킨다.
    console.log(this);
    return this.name;
  }
};

console.log(person.getName());

function Person(name) {
  // 생성자 함수 내부에서 this는 생성자 함수가 생성할 인스턴스를 가르킨다.
  this.name = name;
  console.log(this); // Person {name: "Lee"}
}

const me = new Person('Lee');

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

일반함수에서는 의미  ✖️, 일반함수에서는 전역객체 window가 바인딩 된다.


19.2 함수 호출 방식과 this 바인딩

this 바인딩은 함수 호출 방식, 즉 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다.

함수의 호출 방식으로는 4가지가 있다.

  • 일반 함수 호출
  • 메서드 호출
  • 생성자 함수 호출
  • Function.prototype.apply/call/bind 메서드에 의한 간접 호출

19.2-1 일반 함수 호출

기본적으로 this 에는 전역 객체가 바인딩된다.

function foo() {
  console.log("foo's this: ", this); // window
  function bar() {
    console.log("bar's this: ", this); // window
  }
  bar();
}
foo();

일반 함수로 호출하면 함수 내부의 this 에는 전역 객체가 바인딩된다. 다만 일반 함수에서 this 는 의미가 없다.

// 전역 변수 value 선언
// var 키워드로 선언한 전역 변수 value는 전역 객체의 프로퍼티이다.
var value = 1;
// const 키워드로 선언한 전역 변수 value는 전역 객체의 프로퍼티가 아니다.

const obj = {
  value: 100,
  foo() {
    console.log("foo's this: ", this); // {value: 100, foo: f}
    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
    }
    
    // 메서드 내에서 정의한 중첩 함수도 일반 함수로 호출되면 중첩 함수 내부의 this 에는 전역 객체가 바인딩 된다.
    
    bar();
  }
};

obj.foo();

콜백 함수가 일반 함수로 호출되어도 콜백 함수 내부의 this 에는 전역 객체가 바인딩된다.

var value = 1;

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

obj.foo();

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

그러나 메서드내의 중첩함수와 콜백함수는 외부함수를 도와주는 헬퍼 함수의 역할을 하는데 여기서 this 가 전역객체를 바인딩하는 것은 문제가 된다.

예제를 통해 바인딩을 일치시키는 방법을 알아보자.

var value = 1;

const obj = {
  value: 100,
  foo() {
    // this 바인딩을 변수 that에 할당한다.
    const that = this;
    
    setTimeout(function() {
      // 콜백 함수 내부에서 this 대신 that을 참조한다.
      console.log(that.value);
    }, 100);
  }
};

obj.foo();

이 방법 외에도 this 를 명시적으로 바인딩할 수 있는 Function.prototype.apply, Function.prototype.call, Function.prototype.bind 메서드가 있다.

var value = 1;

const obj = {
  value: 100,
  foo() {
    setTimeout(function() {
      console.log(this.value);
    }.bind(this), 100);
  }
};

obj.foo();

또는 화살표 함수를 이용하자

var value = 1;

const obj = {
  value: 100,
  foo() {
    // 화살표 함수 내부의 this는 상위 스코프의 this를 가르킨다.
    setTimeout(() => console.log(this.value), 100); // 100
  }
};
obj.foo();

19.2-2 메서드 호출

메서드 내부의 this 에는 메서드를 호출한 객체가 바인딩 되는데, 주의할 점은 메서드 내부의 this 는 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩된다는 것이다.

const person = {
  name: 'Lee',
  getName() {
    // 메서드 내부의 this는 메서드를 호출한 객체에 바인딩된다.
    return this.name;
  }
};

console.log(person.getName()); // Lee

person 객체의 getName 프로퍼티가 가르키는 함수 객체는 person 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체이다.

따라서 getName 메서드는 다른 객체의 프로퍼티에 할당하는 것으로 다른 객체의 메서드가 될 수도 있고 일반 변수에 할당하여 일반 함수로 호출될 수도 있다.

const anotherPerson = {
  name: 'Kim'
};
// getName 메서드를 anotherPerson 객체의 메서드로 할당
anotherPerson.getName = person.getName;

// getName 메서드를 호출한 객체는 anotherPerson이다.
console.log(anotherPerson.getName()); // Kim

// getName 메서드를 변수에 할당
const getName = person.getName;

// getName 메서드를 일반 함수로 호출
console.log(getName()); // ' '
// 일반 함수로 호출된 getName 함수 내부의 this.name은 브라우저 환경에서 window.name과 같다.

따라서 메서드 내부의 this 는 프로퍼티로 메서드를 가르키고 있는 개체와는 관계가 없고 메서드를 호출한 객체에 바인딩된다.

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

Person.prototype.getName = function() {
  return this.name;
};

const me = new Person('Lee');

// getName 메서드를 호출한 객체는 me다.
console.log(me.getName()); // (a) Lee

Person.prototype.name = 'Kim';

// getName 메서드를 호출한 객체는 Person.prototype 이다.
console.log(Person.prototype.getName()); // (b) 'Kim'

(a) 의 경우 getName 메서드를 호출한 객체는 me 이기에 thisme 를 가르키며 this.nameLee 이다.

(b) 의 경우 getName 메서드를 호출한 객체는 Person.prototype 이기에 thisPerson.prototype 을 가르키며 this.nameKim 이다.

19.2-3 생성자 함수 호출

생성자 함수 내부의 this 에는 생성자 함수가 생성할 인스턴스가 바인딩된다.

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

const circle1 = new Circle(5);

const circle2 = new Circle(10);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20

생성자 함수는 일반 함수와 동일한 방법으로 정의한다.
만약, 정의한 함수를 new 연산자와 함께 호출하지 않으면 일반 함수로 동작한다.

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

apply, call, bind 메서드는 Function.prototype 의 메서드이다. 이들 메서드는 모든 함수가 상속받아 사용할 수 있다.

apply, call 메서드는 this 로 사용할 객체와 인수 리스트를 인수로 전달받아 함수를 호출한다.

function getThisBinding() {
  return this;
}

const thisArg = {a : 1};

console.log(getThisBinding()); // window

// getThisBinding 함수를 호출하면서 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩한다.
console.log(getThisBinding.apply(thisArg)); // {a : 1}
console.log(getThisBinding.call(thisArg)); // {a : 1}

applycall 메서드의 본질적인 기능은 함수를 호출하는 것이다.

다음으로 호출할 함수에 바인딩과 인수를 같이 전달해보자.

function getThisBinding() {
  console.log(arguments);
  return this;
}
// this로 사용할 객체
const thisArg = {a : 1};

// getThisBinding 함수를 호출하면서 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩한다.
console.log(getThisBinding.apply(thisArg, [1, 2, 3]));
// Arguments(3) [1, 2, 3, callee: f, Symbol(Symbol.iterator): f] {a : 1}

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

apply 메서드는 호출할 함수의 인수를 배열로 묶어 전달한다. 반면에 call 메서드는 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달한다.

두 메서드의 동작은 차이가 없으며 대표적인 용도로는 arguments 객체는 배열이 아닌 유사 배열 이기 때문에 배열의 메서드를 사용할 수 없지만 applycall 메서드를 이용하면 가능하다.

function convertArgsToArray() {
  console.log(arguments);
  
  // arguments 객체를 배열로 변환
  // Array.prototype.slice를 인수 없이 호출하면 배열의 복사본을 생성한다.
  const arr = Array.prototype.slice.call(arguments);
  // const arr = Array.prototype.slice.apply(arguments);
  console.log(arr);
  
  return arr;
}

convertArgsToArray(1, 2, 3); // [1, 2, 3]

Function.prototype.bind 메서드는 applycall 메서드와 달리 함수를 호출하지 않고 this 로 사용할 객체만 전달한다.

function getThisBinding() {
  return this;
}

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

// bind 메서드는 함수에 this로 사용할 객체를 전달한다.
// bind 메서드는 함수를 호출하지는 않는다.
console.log(getThisBinding.bind(thisArg)); // getThisBinding

// bind 메서드는 함수를 호출하지는 않으므로 명시적으로 호출해야 한다.
console.log(getThisBinding.bind(thisArg)());

bind 메서드는 메서드의 this 와 메서드 내부의 중첩 함수 또는 콜백 함수의 this 가 불일치하는 문제를 해결하기 위해 사용되기도 한다.

const person = {
  name: 'Lee',
  foo(callback) {
    setTimeout(callback, 100);
  }
};

person.foo(function() {
           console.log(`Hi! my name is ${this.name}.`); // Hi! my name is .
  // 일반 함수로 호출된 콜백 함수 내부의 this.name은 브라우저 환경에서 window.name과 같다.
  // Node.js 환경에서 this.name 은 undefined 이다.
});

위 예제를 bind 메서드를 사용하여 this 를 바인딩 해보자.

const person = {
  name: 'Lee',
  foo(callback) {
    setTimeout(callback.bind(this), 100);
  }
};

person.foo(function() {
  console.log(`Hi! my name is ${this.name}.`);
}); // Hi! my name is Lee.

this 바인딩이 호출 방식에 따라 달라지는 것을 정리해 보자.

  • 일반 함수 호출 방식 ➡️ 전역 객체
  • 메서드 호출 ➡️ 메서드를 호출한 객체
  • 생성자 함수 호출 ➡️ 생성자 함수가 생성할 인스턴스
  • Function.prototype.apply/call/bind 메서드에 의한 간접 호출 ➡️ Function.prototype.apply/call/bind 메서드의 첫번째 인수로 전달한 객체

0개의 댓글