자바스크립트의 this keyword (Javascript)

자바스크립트의 this keyword (Javascript)

오늘은 'this' 키워드에 대해 정리해 보고, 그동안 'this'에 대해 온전히 이해하지 못 한 부분을 해소하고자 합니다.

'this'의 값은 코드가 쓰여진 위치를 보고 판단할 수 있는 lexical scope와는 다르게, 기록된 코드만 보고는 그 값을 알 수 없습니다. 다시 말하면, 'this'의 값은 binding이 어떻게 되는지에 따라 달라집니다. 그래서, 'this'가 binding되는 다섯까지 방법에 대해 학습해 보도록 하겠습니다.

'this'를 학습하기 전 실행문맥(Execution context)에 대해 개념을 어느정도 인지하면 더 좋겠습니다.


Contents

  • 'this' keyword 란?
  • 'this'가 binding되는 다섯 가지 패턴 요약

    • Global 영역에서의 this
    • Regular function call - 일반 함수 실행
    • Method 호출 시
    • Construction mode
    • Call, Apply and Bind
  • 피해서 사용해야 하는 경우의 'this' keyword

this keyword 란?

  • 모든 함수 scope 내에서 자동으로 설정되는 특수한 식별자이며, 자바스크립트의 예약어 중 하나입니다.
  • 'this'는 실행문맥(Execution context)의 구성요소 중 하나로, 함수가 실행되는 동안 이용할 수 있습니다.
  • 'this''가 가리키는 대상은, 함수가 어떻게 호출되는지에 따라 다르다.(호출 맥락)

this가 binding되는 다섯 가지 패턴 요약

  1. Global 영역에서의 this : window 객체를 가리킴.
  2. Regular function call - 일반 함수 실행 : window 객체를 가리킴.
  3. Method 호출 시 : 부모 Object
  4. Construction mode (new 연산자로 생성된 function 영역의 this) : 새로 생성된 객체를 가리킴
  5. .call / .apply and bind 호출 : call, apply의 첫번째 인자로 명시 된 객체를 가리킴.

  1. Global 영역에서의 this

Global 영역에 this는 global object를 가리킵니다. 웹 브라우저 환경에서 global object는 window객체를 가리킵니다.

var name = 'global';
console.log(this.name); // => global
  1. 함수 호출 시 함수 안의 this

일반적인 함수 호출 시 함수 내부의 this는 전역 객체를 가리킵니다.

// 예시 1
var name = 'global';
function foo() {
  console.log(this.name); // global
}

foo();

// 예시 2
function outer1() {
  function inner() {
    console.log(this.name); // global
  }
  inner();
}

outer1();

// 예시 3
var age = 100;

function foo() {
  var age = 99;
  bar(); // --> 100이 출력.
}

function bar() {
  console.log(this.age);
}

foo();

// 예시 4
function checkThisKeyword() {
  if (window === this) {
    console.log('this === window');
  }
}
checkThisKeyword(); // this === window
window.checkThisKeyword(); // this === window

// 예시 5
function variablesInThis() {
  this.person = 'Julie';
  // 이 경우 this는 global object이고, 웹브라우저에서는 global object가 윈도우이기 때문에 일반적인 person이라는 전역변수를 생성함.
}
variablesInThis();

console.log(person); // 'Julie'

예시 5를 참고하면, this를 잘 못 사용하면 의도치 않게 전역 변수를 생성할 수 있습니다. Javascript의 stric mode에서 위의 코드를 실행하면, 객체 안의 this는 global object가 아닌 undefined가 됩니다. 이로 인해, stric mode를 사용하면 객체 안에서 개발자의 실수로 전역변수를 생성하는 일을 줄일 수 있습니다.

'use strict';

console.log(this);
function whatIsThis() {
  return this;
}

function variablesInThis() {
  this.person = 'Julie'; // this가 undefined이기에, typeError가 발생함.
}

variablesInThis(); // undefined
whatIsThis(); // undefined
  1. Method 호출 시

객체의 method를 호출했을 때, method 내부 'this'는 method를 호출한 parent object를 가리킵니다.

// 예시 1
var counter = {
  val: 0,
  increment: function() {
    this.val += 1;
  }
};

counter.inrement();
console.log(counter.val); // 1
counter.increment();
console.log(counter.val); // 2

// 예시 2
var obj1 = {
  fn: function(a, b) {
    return this;
  }
};

var obj2 = {
  method: obj1.fn
};
console.log(obj1.fn() === obj1); // true
console.log(obj2.method() === obj2); // true
  1. Construction mode (new 연산자로 생성된 function 영역의 this)

생성자 함수의 this는 생성자 함수로 새로 생성된 객체를 가리킵니다.

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

var John = new Person('John', 20);
console.log(John.name); // 'John'
console.log(John.age); // 20
  1. Call, Apply, and Bind

Function method인 call, apply, bind method를 사용하면, this keyword의 맥락(context)를 변경할 수 있습니다.

  • Call / Apply

call과 apply는 method의 this 값이 될 요소를 인수로 사용하여 실행되는 메서드입니다. call과 apply의 첫번째 인자로 전달되는 요소가 this의 값이 됩니다. call과 apply의 차이점은, this의 값이 될 첫 인자를 제외하고 call method는 인수를 쉼표로 구분한 값으로 받습니다. 하지만, apply 메서드는 배열로 인수를 받습니다.

// 예제 1
function say(greetings, honorifics) {
  console.log(greetings + ' ' + honorifics + this.name);
}
var tom = { name: 'Tom Sawyer' };
var becky = { name: 'Becky Thatcher' };
say.call(tom, 'Hello', 'Mr.');
say.call(becky, 'Hi!', 'Ms.');
say.apply(tom, ['Hello', 'Mr.']);
say.apply(becky, ['Hi!', 'Ms.']);

// 예제 2
var obj1 = {
  string: 'origin',
  foo: function() {
    console.log(this.string);
  }
};

var obj2 = {
  string: 'what?'
};

obj1.foo(); // origin
obj1.foo.call(obj2); // what?

// 예제 3
function makeParamsToArray() {
  return Array.prototype.slice.call(arguments);
}
console.log(makeParamsToArray('Hello', 'world!'));
// => ['Hello', 'world!'];

// 예제 4
function getMax() {
  var argArray = Array.prototype.slice.call(arguments);
  var maxValue = argArray.reduce(function(accumulator, currentValue) {
    return accumulator > currentValue ? accumulator : currentValue;
  });
  return maxValue;
}
getMax(1, 2, 3, 4, 5); // => 7

// 예제 5
function Product(name, price) {
  this.name = name;
  this.price = price;
  this.print = function() {
    console.log(this.constructor.name + ': ' + this.name + '\t\t' + this.price + ' USD');
  };
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 48);

cheese.print();
// 'Food: feta  5 USD
// 'Toy: robot  48 USD

// 예제 6
var min1 = Math.min(7, 35, 2, 8, 21);
console.log(min1); // 2

var arr = [7, 35, 2, 8, 21];
var min2 = Math.min.apply(null, arr);
console.log(min2); // 2
// noth: Math 객체는 기본적으로 instance를 가질 수 없기 때문에 this의 값이 불필요합니다.
  • Bind

call과 apply method가 바로 실행된 것과 달리, bind는 인자로 넘겨준 객체와 연결(bind)된 새로운 함수를 반환합니다. callback 함수를 특정 객체와 연결하고 싶을 때 사용할 수 있습니다.

  • Syntax: fn.bind(thisArg, [, arg1[, arg2[, ..]])
// 예제 1
function foo() {
  return this;
}
var boundFoo = foo.bind({ a: 1 });
foo(); // window
boundFoo(); // {a:1};

// 예제 2
function say(greetings, honorifics) {
  console.log(greetings + ' ' + honorifics + this.name);
}
var tom = { name: 'Tom Sawyer' };
var sayToTom = say.bind(tom);
sayToTom('Hello', 'Mr.'); // -> Hello! Mr.Tom Sayer

// 예제 3
function Box(w, h) {
  this.width = w;
  this.height = h;

  this.getArea = function() {
    return this.width * this.height;
  };

  this.printArea = function() {
    console.log(this.getArea());
  };

  this.printArea();
  // 5000

  setTimeout(this.printArea, 2000);
  // setTimeout은 window 객체의 method이며 this가 window를 가리키기 때문에 위 코드의 경우 TypeError가 발생합니다.

  setTimeout(this.printArea.bind(this), 2000);
  // bind를 사용해 this의 값을 명시적으로 묶어주었기 때문에, 이 경우 의도한 대로 코드가 작동합니다.

  var that = this;
  setTimeout(this.printArea.bind(that), 5000);
  // this값을 that이라는 변수에 묶어 좀 더 명시적으로 코딩을 할 수도 있습니다.
}

var b = new Box(100, 50);

- [번외] bind를 이용한 커링 커링이란, 여러 개의 인자를 받는 함수가 있을 때, 각각의 인자를 별도로 받는 여러 함수를 지정하는 함수형 프로그래밍 기법 중 한 가지입니다. 커링이라는 이름은 영국의 수학자겸 논리학자 해스켈 커리의 이름에서 따왔다고 합니다.

function template(name, age) {
  return '<h1>' + name + '</h1><span>' + age + '</span>';
}

var tmplKim = template.bind(null, 'Kim');
tmpKim(20); // -> "<h1>Kim</h1><span>20</span>"

 


피해서 사용해야 하는 경우의 'this' keyword

위의 내용이 아직 명확하게 이해되지 않은 상태에서, 아래의 내용을 보면 조금 헷갈릴 수 가 있습니다. 아래 내용을 포스팅에서 삭제를 할까 하다가, 그래도 제가 학습했던 내용이기에 남겨 놓았습니다.

this가 어디에 묶이는지 명확하지 않은 코드는 피하는 것이 최선이라고 합니다. Javascript의 스트릭모드인지 아닌지에 따라 다르고, 함수 호출을 어디에서 했는지에 따라 this가 달라지기 때문입니다.

동일한 이유로 중첩된 함수에서의 this 사용도 피하는 것이 최선이라고 합니다. 아래와 같이 중첩된 함수 안에서 this를 사용해 보았습니다. 철자가 거꾸로 써진 'Julia' 이름을 기대하지만 제대로 작동하지 않습니다.

메서드 greetBackwards 안의 gerReverseName 함수 내부의 this는 윈도우를 가리키기 때문입니다. 만약에 'strict' 모드라면 undefined가 되어 'this.name.length'에서 에러가 발생합니다.

var o = {
  name: 'Julia',
  greetBackwards: function() {
    function getReverseName() {
      let nameBackwards = '';
      for (let i = this.name.length - 1; i >= 0; i--) {
        nameBackwards += this.name[i];
      }
      return nameBackwards;
    }
    return `${getReverseName()} si eman ym ,olleH`;
  }
};
o.greetBackwards();

이와 같은 문제를 해결하기 위해, 아래와 같이 this를 변수에 할당하는 방법이 있습니다.

var o = {
  name: 'Julia',
  greetBackwards: function() {
    var self = this;

    function getReverseName() {
      var nameBackwards = '';

      for (let i = self.name.length - 1; i >= 0; i--) {
        nameBackwards += self.name[i];
      }

      return nameBackwards;
    }

    return `${getReverseName()} si eman ym ,olleH`;
  }
};
o.greetBackwards();

또는 ES6 문법의 화살표 함수를 사용하는 방법도 있습니다. 화살표 함수와 일반 함수와 차이점 중 한 가지는 화살표 함수 내부에는 'this'가 없다는 것입니다. 따라서, 외부 lexical scope의 this를 참조합니다.

const o = {
  name: 'Julie',
  greetBackwards: function() {
    const getReverseName = () => {
      let nameBackwards = '';

      for (let i = this.name.length - 1; i >= 0; i--) {
        nameBackwards += this.name[i];
      }

      return nameBackwards;
    };

    console.log(this);

    return `${getReverseName()} si eman ym ,olleH`;
  }
};
o.greetBackwards();