This

this란 keyword는 c++에서의 this와 다르다.

  • 일반적인 프로그래밍 언어에서의 this는 self(자기자신)을 가리키는 참조변수다.
  • javascript에서 this는 함수호출 방식에 따라 this가 결정된다.

javascript에서 함수가 호출될 때, 기존 매개변수로 전달되는 인자 값에 대해서 arguments 객체와 this 인자가 함수 내부로 암묵적으로 전달된다.

함수 호출 방식과 this

  • 자바스크립트는 함수 호출시 어떻게 호출 되는가에 따라서 동적으로 this가 결정된다.
    • 함수 호출시 this : window
    • 메소드 호출시 this : 메소드 객체
    • 내부함수 호출시 this : window
    • 엄격모드에서의 this : undefined
    • 이벤트 리스너 호출시 this : 이벤트 객체
    • 생성자 함수 호출시 this : 생성된 새 객체
  • Arrow 함수는 일반적인 this 바인딩과 다르게 Lexical this를 가진다.
    • Arrow 함수 호출시 this : 함수 선언시의 상위 스코프의 this

1. 함수 호출시 this

자바스크립트에서 함수를 호출하면 해당 함수 내부 코드에서 사용된 this는 전역객체(window)에 바인딩된다.

즉, 함수는 전역객체의 메소드이다. 따라서 메소드 호출시와 동일하게 메소드 객체(winodw)를 this로 갖는다.

2. 메소드 호출시 this

객체의 메소드를 호출할 때는 해당 메소드를 호출한 객체로 바인딩된다.

프로토타입 객체도 메소드를 가질 수 있다. 프로토타입 객체도 일반적인 메소드의 this 바인딩 규칙을 따른다.

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

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

var song = new Person("song");
song.getName(); //"song"

Person.prototype.name = "kim";
Person.prototype.getName(); // "kim"

Alt this

  • 생성된 객체 song.getName() 호출시의 this는 메소드를 호출한 객체인 song객체이다. 따라서 song.name인 “song”이 리턴된다.
  • prototype객체의 getName()을 호출시의 this는 마찬가지로 메소드를 호출한 객체인 Person.prototype객체이다. 따라서 “kim”이 리턴된다.

3. 내부함수 호출시 this

내부함수도 함수 호출시 this 바인딩 규약을 따른다. 즉 this는 전역객체에 바인딩된다.

  • 일반함수, 메소드, 콜백함수에 관계 없이 해당 함수 내에 내부함수는 전역객체에 바인딩된다.
  • apply, call, bind 메소드를 통해서 this를 바인딩해서 사용할 수 있다.

4. 엄격모드에서의 this

엄격 모드는 코드 안정성과 오류 검증을 위해서 나온 것으로 ECMA 5.1 버전에서 나왔다.

  • 엄격 모드에서 함수 실행시 this는 undefined가 된다.
  • 내부 함수 호출시 this 또한 undefined가 된다.
  • 의도치 않게 전역객체에 바인딩된 this를 사용하는 것을 막을 수 있다.
    • null참조로 인해 죽게되기 때문에 의도치 않은 동작을 하는 것보다 쉽게 오류 검증을 할 수 있다.

5. 이벤트 리스너 호출시 this

이벤트 리스너에서의 this는 이벤트를 발생시킨 객체가 된다.

#("btn").click(function(){
    console.log(this); // this는 #btn
})

이벤트 리스너나 setTimeout등에 객체의 메소드를 콜백함수로 등록할 때 어떤 규약을 따를까?

  • 객체로부터 메소드가 분리되었을 때 this는 메소드가 정의된 객체가 아니다.
function Person(name) {
  this.name = name;
}

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

var song = new Person("song");
song.getName(); //"song"

setTimeout(song.getName, 1000); //""
//"song"
//""
  • setTimeout에 객체의 메소드를 전달하면 매개변수로 전달되었기 때문에 메소드는 객체로 부터 분리되어 있고, 함수로써 실행이 된다.
    • 따라서 this는 전역객체(window)이다.
      • 엄격모드에서는 undefined가 된다.
  • 객체의 메서드를 이벤트리스너로 등록했을 때에 this는 이벤트 객체이다.
    • setTimeout, 이벤트리스너등 모든 경우에 함수 호출시 this 바인딩 규약을 따른다.
    • 메서드가 정의된 객체를 this로 바인딩하기 위해서는 apply, call, bind를 사용해야한다.
      • lexical this

6. 생성자 함수 호출시 this

  • 자바스크립트 생성자 함수는 말그대로 객체를 생성한다.
  • 다른 객체지향 언어의 생성자 함수와 달리 형식은 정해저있지 않고 기존 함수에 new를 붙여서 호출하면 생성자 함수로 동작한다.
  • 다만 특정 함수가 생성자 함수로 정의되어 있음을 알리려고 첫 글자를 대문자로 하는 것을 권하고 있다.

자바스크립트 객체 생성 과정 new 연산자로 생성자 함수를 호출하면 다음과 같이 동작한다.

1. 빈 객체 생성 및 this 바인딩
  • 먼저 빈 객체가 생성된다.
    • 빈 객체란? 자신을 생성한 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체이다.
  • 생성된 객체는 this로 바인딩된다.
  • 따라서 이후 생성자 함수의 코드 내부에서 사용된 this는 이 빈 객체를 가리킨다.
  • 이 객체가 생성자 함수가 새로 생성하는 객체이다.
2. this를 통한 프로퍼티 설정
  • 생성된 빈 객체에 this를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다.
3. 생성된 객체 리턴
  • 특별하게 정의된 리턴문이 없는 경우 this로 바인딩된 새로 생성한 객체가 리턴된다.
  • 만약 명시적으로 다른 객체를 리턴하도록 했다면 다른 객체가 리턴된다.
  • 객체가 아닌 값을 리턴하는 경우 this로 바인딩된 새 객체가 리턴된다.
var Person = function(name) {
  // 1. 빈 객체 생성 및 this 바인딩
  this.name = name; //2. this를 통한 프로퍼티 설정
  // 3. 생성된 객체 리턴
};
var song = new Person("song");
console.log(song.name);

객체 리터럴 방식과 생성자 함수 방식의 차이

  • 이렇게 생성자를 이용해 객체를 생성하는 방식과 리터럴을 이용해 생성하는 방식의 차이는 프로토타입 객체(proto)에 있다.
  • 위에 예제에서의 song객체의 프로토타입 객체는 Person이고 리터럴로 동일한 song 객체를 만들면 그 song객체의 프로토타입 객체는 Object이다.
  • 이는 자바스크립트 객체 생성 규칙 때문이다. 자바스크립트 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.
    • 객체 리터럴 방식에서는 객체 생성자 함수가 Object이고, Object.prototype
    • 생성자 함수 방식의 경우는 생성자 함수 자체가 프로토타입 객체이다. Person.prototype

Scope-Safe Constructor Pattern

  • new를 사용하지 않고 생성자 함수를 호출했을 때, 전역객체에 프로퍼티가 할당되는 등 문제가 생길 수 있다.
  • 때문에 new를 쓰지 않고 객체를 생성하려할 때 new를 통해 생성하도록 강제해준다.
var Person = function(name) {
  if (!(this instanceof arguments.callee)) {
    return new arguments.callee(arg);
  }
  this.name = name;
};

7. Arrow 함수 호출시 this

일반 함수와 Arrow 함수의 가장 큰 차이점은 this이다.

  • 일반 함수의 this는 함수 호출시에 동적으로 바인딩된다.
  • Arrow 함수는 함수를 선언할 때 this에 바인딩될 객체가 정적으로 결정된다.
  • Arrow 함수의 this는 언제나 상위 스코프의 this를 가리킨다.
    • 즉, Lexical this를 가진다.
    • Lexical scope와 비슷한 개념이다.(함수의 상위 스코프를 결정하는 방식)

Arrow 함수를 사용하면 안되는 경우

1. 객체의 메소드를 Arrow 함수로 정의하는 경우
  • 객체의 메소드를 Arrow 함수로 정의하는 경우 메소드를 소유한(호출한) 객체가 this가 되는 것이 아니라 상위 스코프의 this인 전역객체가 this가 된다.
const song = {
  name: "song",
  sayHi: () => console.log(`Hi ${this.name}`)
};

song.sayHi(); // Hi undefined
  • 이러한 경우 ES6의 축약 메소드 표현을 사용해야 한다.
const song = {
  name: "song",
  sayHi() {
    // === sayHi: function() {
    console.log(`Hi ${this.name}`);
  }
};
song.sayHi(); // Hi song
  • 클래스나 생성자 함수에서는 생성된 객체로 바인딩되므로 사용해도 된다.
    • 객체 생성시 this가 이미 생성된 객체로 바인딩 되어있기 때문에 Arrow 함수의 this는 생성된 객체로 바인딩된다.
  • 리터럴을 통해 객체를 생성하는 경우 주의해야 한다.
var Person = function(name) {
  this.name = name;
  this.sayHi = () => console.log(`Hi ${this.name}`);
};
var song = new Person("song");
song.sayHi(); // Hi song
2. Prototype
  • Arrow 함수로 정의된 메소드를 prototype에 할당하는 경우, 객체의 메소드를 Arrow함수로 정의하는 경우와 같은 문제가 발생한다.
    • prototype 또한 객체이기 때문이다.
    • 클래스, 생성자 함수로 객체를 생성할 때 생성자 함수 내부에서 this로 할당하는 경우에는 문제가 없지만 아래와 같이 prototype 메서드로 정의하는 경우는 객체 생성시에도 제대로 this를 바인딩하지 못한다.
      • 전역객체가 바인딩된다.
function Person(name) {
  this.name = name;
}
Person.prototype.name = "kim";
Person.prototype.getName = () => console.log(this.name);

Person.prototype.getName(); // ""
function Person(name) {
  this.name = name;
}
Person.prototype.name = "kim";
Person.prototype.getName = function() {
  return this.name;
};
Person.prototype.getName(); // "kim"
3. 생성자 함수
  • Arrow 함수는 생성자 함수로 사용할 수 없다.
  • 생성자 함수는 prototype 프로퍼티를 가지며 객체 생성시 프로토타입 링크를 형성한다.
    • prototype 프로퍼티는 prototype 객체를 가리키고, prototype 객체의 constructor는 생성자 함수를 가리킨다.
  • 하지만 Arrow 함수는 prototype 프로퍼티를 가지고 있지 않다.
4. addEventListener 함수의 콜백함수
  • 이벤트 객체를 this로 갖도록 하려면 일반함수를 사용해야한다.
  • 일반 함수는 이벤트 객체(current Target)을 this로 갖는다.
  • Arrow 함수는 전역객체를 this로 갖는다.(생성될 때의 상위 스코프)
var button = document.getElementById("myButton");

button.addEventListener("click", () => {
  console.log(this === window); // => true
});
var button = document.getElementById("myButton");

button.addEventListener("click", function() {
  console.log(this === button); // => true
});
  • react에서 메소드를 전달할 때 Arrow 함수로 된 메소드를 전달해서 this를 유지시킨다.
    • this를 고정시키고 싶을 때는 Arrow 함수를 사용하면 된다.

call, apply, bind를 이용한 this 바인딩

Javascript의 복잡하고 이상한 this를 이해하고, 프로그래머의 의도대로 this를 사용하기 위해서 여러가지 메서드를 사용한다.

1. call

fun.call(thisArg[, arg1[, arg2[, …]]])

  • thisArg : 지정할 this
  • arg1 … : 객체를 위한 인수
function greet() {
  var reply = [this.animal, "typically sleep between", this.sleepDuration].join(
    " "
  );
  console.log(reply);
}

var obj = {
  animal: "cats",
  sleepDuration: "12 and 16 hours"
};

greet.call(obj); // cats typically sleep between 12 and 16 hours

2. apply

function.apply(thisArg, [argsArray])

  • thisArg : 지정할 this
  • argsArray : 유사배열 객체(arguments)

call과 apply의 기능은 같지만 apply가 두 번째 인자를 배열로 넘긴다는 차이가 있다.

3. bind

  • bind를 사용하면 this 값을 영구히 바꿀 수 있다.
  • 함수의 동작을 영구적으로 바꾸므로 버그의 원인이 될 수 있으니 주의해야 한다.
  • bind를 활용해서 this뿐 아니라 다른 인자도 고정시킬 수 있다.(currying 패턴)
function greet() {
  console.log(`Hello, I'm ${this.name}`);
}
greet(); //Hello, I'm
const songGreeting = greet.bind({ name: "song" });
songGreeting(); // Hello, I'm song;

참고

  • Learning Javascript
  • Inside Javascript
  • MDN-this
  • MDN-call
  • https://poiemaweb.com/js-this
  • https://poiemaweb.com/es6-arrow-function