FrontEnd/Deep Dive

[JS] DeepDive(22) this

728x90

메서드가 자신이 속한 객체의 프로퍼티를 참조하려면 먼저 자신이 속한 객체를 가리키는 식별자를 참조할 수 있어야 한다.

 

 

 

객체리터럴 방식으로 생성한 객체의 경우에는 재귀적 방식으로 자신이 속한 객체를 가리키는 식별자를 참조할 수 있다.

// 객체 리터럴
const circle = {
  radius: 5,
  getDiameter() {
    // this는 메서드를 호출한 객체를 가리킨다.
    return 2 * this.radius;
  }
};

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

 

위 예제의 객체 리터럴은 circle 변수에 할당되기 직전에 평가되어 getDiameter메서드가 호출될때 circle 식별자에 생성된 객체가 할당된 이후로 참조할 수 있다.

 

허나 이 방식은 일반적이지 않고 바람직하지 않다.

 

 

 

// 생성자 함수
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

 

생성자 함수로 인스턴스를 생성하려면 위처럼 먼저 생성자 함수를 만들어야 한다.

 

 

위 방법또한 권장되는 방법은 아니다.

 

JS는 이러한 기능을 위해서 this라는 특수한 식별자를 제공한다.

 

 

 

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

 

 

this는  JS 엔진에 의해 암묵적으로 생성되며 코드 어디서든 참조할 수 있다.

 

 

자바나 C++과 같은 클래스기반 언어에서 this는 언제나 클래스가 생성하는 인스턴스를 가리키지만 JS의 this는 함수가 호출되는 방식에 따라 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); // {name: "Lee", getName: ƒ}
    return this.name;
  }
};
console.log(person.getName()); // Lee

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

const me = new Person('Lee');

 

일반함수에서는 this를 사용할 필요가 없으므로 strict mode가 적용된다면 일반함수 내부의 this에는 undefined가 할당되게 된다.

 

 

 

함수 호출 방식과 this 바인딩

 

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

 

1. 일반함수호출

2. 메서드호출

3. 생성자 함수 호출

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

 

 

 

// this 바인딩은 함수 호출 방식에 따라 동적으로 결정된다.
const foo = function () {
  console.dir(this);
}

// 동일한 함수도 다양한 방식으로 호출할 수 있다.

// 1. 일반 함수로 호출
// foo 함수를 일반적인 방식으로 호출
// foo 함수 내부의 this는 전역 객체 window를 가리킨다.
foo(); // window

// 2. 메서드 호출
// foo 함수를 프로퍼티 값으로 할당하여 호출
// foo 함수 내부의 this는 메서드를 호출한 객체 obj를 가리킨다
obj.foo(); // obj

// 3. 생성자 함수로 호출
// foo 함수를 new 연산자와 함께 생성자 함수로 호출
// foo 함수 내부의 this는 생성자 함수가 생성한 인스턴스를 가리킨다.
new foo(); // foo {}

// 4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출
// foo 함수 내부의 this는 인수에 의해 결정된다.
const bar = { name: 'bar' };

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

 

 

 

1. 일반 함수 호출

 

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

 

 

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

 

 

 

메서드 내에 정의한 중첩 함수도 일반 함수라면 중첩함수의 내부 this에는 전역 객체가 바인딩된다.

 

 

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

const obj = {
  value: 100,
  foo() {
    console.log("foo's this: ", this);  // {value: 100, foo: ƒ}
    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에는 전역 객체가 바인딩되게 된다. 중첩함수나 콜백함수의 this가 전역 객체를 바인딩하는것은 문제가 있다. 

 

중첩함수나 콜백함수가 헬퍼함수로 동작하기 힘들게 하기 때문이다.

 

var value = 1;

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

obj.foo();

 

위 예제를 통해 this 바인딩을 일치시켜 보자.

 

var value = 1;

const obj = {
  value: 100,
  foo() {
    // this 바인딩(obj)을 변수 that에 할당한다.
    const that = this;

    // 콜백 함수 내부에서 this 대신 that을 참조한다.
    setTimeout(function () {
      console.log(that.value); // 100
    }, 100);
  }
};

obj.foo();
var value = 1;

const obj = {
  value: 100,
  foo() {
    // 콜백 함수에 명시적으로 this를 바인딩한다.
    setTimeout(function () {
      console.log(this.value); // 100
    }.bind(this), 100);
  }
};

obj.foo();

 

혹은 화살표함수를 사용하면 this 바인딩이 일치된다.

 

var value = 1;

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

obj.foo();

 

 

 

2. 메서드 호출

 

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

// 메서드 getName을 호출한 객체는 person이다.
console.log(person.getName()); // Lee

위 코드에서 getName메서드는 person 객체의 메서드로 정의되었다. 메서드는 프로퍼티에 바인딩된 함수로 person객체의 getName 프로퍼티가 가리키는 함수 객체는 person 객체에 포함된 것이 아닌 독립적으로 존재한다.

 

따라서 getName 프로퍼티가 가리키는 함수 객체 즉 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과 같다.
// 브라우저 환경에서 window.name은 브라우저 창의 이름을 나타내는 빌트인 프로퍼티이며 기본값은 ''이다.
// Node.js 환경에서 this.name은 undefined다.

 

프로토 타입 메서드 내부에서 사용된 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()); // ① Lee

Person.prototype.name = 'Kim';

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

 

 

3. 생성자 함수 호출

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

 

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

// 반지름이 5인 Circle 객체를 생성
const circle1 = new Circle(5);
// 반지름이 10인 Circle 객체를 생성
const circle2 = new Circle(10);

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

 

new 연산자 없이 생성자 함수를 호출하지 않으면 일반함수로 동작한다는 것에 주의하자.

 

 

 

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

 

apply , call , bind 메서드는 Function.prototype의 메서드이다. 즉 이들 메서드는 모든 함수가 상속받아 사용할 수 있다. apply,call 메서드는 this로 사용할 객체와 인수 리스트를 인수로 전달받아 함수를 호출한다.

 

 

function getThisBinding() {
  return this;
}

// 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}

 

 

 

apply와 call 메서드의 본질적인 기능은 함수를 호출하는 것이다. apply와 call 메서드로 함수를 호출하면 첫번째 인수로 전달한 특정 객체를 호출한 함수의 this에 바인딩한다.

 

두 함수는 인수를 전달하는 방식만 다를 뿐 동일하게 동작한다.

 

 

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

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

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

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

 

 

apply와 call 메서드를 활용하면 arguments객체와 같은 유사 배열 객체에 배열 메서드를 사용하게 하는것이 가능하다.

 

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]

 

 

 

bind메서드는 apply와 call메서드와 달리 함수를 호출하지는 않으며 this 바인딩이 교체된 함수를 새롭게 생성해 반환한다.

 

function getThisBinding() {
  return this;
}

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

// bind 메서드는 첫 번째 인수로 전달한 thisArg로 this 바인딩이 교체된
// getThisBinding 함수를 새롭게 생성해 반환한다.
console.log(getThisBinding.bind(thisArg)); // getThisBinding
// bind 메서드는 함수를 호출하지는 않으므로 명시적으로 호출해야 한다.
console.log(getThisBinding.bind(thisArg)()); // {a: 1}

 

 

bind메서드를 활용하면 중첩함수 또는 콜백함수의 this와 메서드의 this가 일치하지 않는 문제를 해결할 수 있다.

 

 

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

person.foo(function () {
  console.log(`Hi! my name is ${this.name}.`); // ② Hi! my name is undefined.
  // 일반 함수로 호출된 콜백 함수 내부의 this.name은 브라우저 환경에서 window.name과 같다.
  // 브라우저 환경에서 window.name은 브라우저 창의 이름을 나타내는 빌트인 프로퍼티이며 기본값은 ''이다.
  // Node.js 환경에서 this.name은 undefined다.
});

 

위 예제를 bind함수를 써서 this를 일치시켜보자.

 

const person = {
  name: 'Lee',
  foo(callback) {
    // bind 메서드로 callback 함수 내부의 this 바인딩을 전달
    setTimeout(callback.bind(this), 100);
  }
};

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

 

 

 

728x90

'FrontEnd > Deep Dive' 카테고리의 다른 글

[JS] DeepDive(24) 클로저  (0) 2023.08.23
[JS] DeepDive(23) 실행 컨텍스트  (0) 2023.08.21
[JS] DeepDive(20) 빌트인 객체  (0) 2023.08.17
[JS] DeepDive(20) strict mode  (0) 2023.08.16
[JS] DeepDive(19) 프로토타입 -2  (0) 2023.08.13