[Javascript] this

js this

Posted by dongjune on September 21, 2021

🍑 자바스크립트의 this

자바스크립트의 this는 다른 언어와는 달라 헷갈리는 개념입니다. java에서의 this는 인스턴스가 자기 자신을 가리키는 참조변수입니다.
하지만 자바스크립트의 경우 this에 바인딩 되는 객체는 함수의 호출 방식에 따라 달라지게 됩니다.

🍑 함수호출 방식에 따른 this 바인딩

자바스크립트의 this는 함수가 어떻게 호출되는지에 따라 바인딩 할 객체가 동적으로 결정됩니다. 아래는 자바스크립트의 함수 호출방식입니다.

  1. 함수 호출
    1
    2
    3
    4
    
    function foo(){
      console.log(this);
    }
    foo(); // window
    
  2. 메소드 호출
    1
    2
    
    const obj = {foo:foo};
    obj.foo(); // obj
    
  3. 생성자 함수 호출
    1
    
    const instance = new foo(); // instance;
    
  4. apply,call,bind 호출
    1
    2
    3
    4
    
    const bar = {hi:'hi'};
    foo.call(bar); // bar
    foo.apply(bar); // bar
    foo.bind(bar)(); // bar
    

이제 이 4가지 함수 호출 방식에 따라 this가 어떻게 바인딩 되는지 자세히 살펴보겠습니다.

🍒 1. 함수 호출

전역 객체는 모든 객체의 최상위 계층을 의미합니다. 브라우저에서는 window, node에서는 global이 전역 객체입니다.
만일 함수나 변수를 전역에 선언하게 되면 그것들은 전역 객체의 프로퍼티가 됩니다.

1
2
3
4
5
6
7
8
const hello = 'hello';

function foo(){
  console.log('hi');
}

console.log(window.hello) // hello
window.foo() // hi

✅ 기본적으로 전역 함수의 this는 이 전역객체에 바인딩이 됩니다. 그리고 내부함수의 this도 마찬가지로 전역객체에 바인딩 됩니다.

1
2
3
4
5
6
7
8
function foo(){
  console.log(this); // window
  function bar(){
    console.log(this); // window
  }
  bar();
}
foo();

✅ 내부함수는 일반 함수, 메소드, 콜백함수 어디에서 선언됐는지에 상관없이 항상 전역객체에 this가 바인딩 되게 됩니다.

🍒 2. 메소드 호출

함수가 객체의 프로퍼티가 되면 이것을 메소드라고 부릅니다. 이때 메소드 내부의 this는 해당 메소드를 소유한 객체, 즉 메소드를 호출한 객체와 바인딩 됩니다.

1
2
3
4
5
6
7
8
const obj = {
  name: 'dongjune',
  hi:function(){
    console.log('hi' + this.name);
  }
}

obj.hi(); // hi dongjune

프로토타입 객체도 메소드를 가질 수 있습니다. 프로토타입에서도 마찬가지로 this는 그 메소드를 호출한 객체에 바인딩 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
  this.name = name;
}

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

const me = new Person('Lee');
console.log(me.getName()); // Lee

Person.prototype.name = 'Kim';
console.log(Person.prototype.getName()); // Kim

🍒 3. 생성자 함수 호출

생성자 함수를 통해 객체를 생성할 수 있습니다. 다른 언어와는 다르게 기존 함수에 new 키워드를 붙이면 해당 함수가 생성자 함수로 동작하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
// 생성자 함수
function Person(name) {
  this.name = name;
}

const me = new Person('Lee');
console.log(me); // {name: "Lee"}

// new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수로 동작하지 않는다.
const you = Person('Kim');
console.log(you); // undefined

new 연산자로 생성자 함수를 호출하게 되면 앞서 살펴본 메소드나 함수 호출때와는 this 바인딩이 다르게 동작합니다.

🌱 3.1. 생성자 함수 동작 방식

new 연산자로 생성자 함수를 호출하면 다음의 과정을 거치게 됩니다.

  1. 빈 객체 생성 및 this 바인딩
    • 생성자 함수의 코드 실행 전에 빈 객체가 생성된다.
    • 생성자 함수 내에서 사용하는 this는 이 빈 객체를 가리킴
    • 그렇게 생성 된 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.
  2. this를 통한 프로퍼티 생성
    • 생성된 빈 객체에 this를 통해 동적으로 프로퍼티나 메소드를 할당한다.
  3. 생성된 객체 반환
    • 반환문이 없는 경우 this에 바인딩 된 새로 생성한 객체를 반환한다. 명시적으로 this를 반환하여도 같은 결과이다.
1
2
3
4
5
6
7
8
function Person(name) {
  // 생성자 함수 코드 실행 전 -------- 1
  this.name = name;  // --------- 2
  // 생성된 함수 반환 -------------- 3
}

const me = new Person('Lee');
console.log(me.name);

🌱 3.2. 생성자 함수에 new 연산자를 붙이지 않는 경우

생성자 함수를 new 없이 호출한 경우 함수 내부의 this는 전역 객체에 바인딩 됩니다. 또한 암묵적으로 생성한 빅 객체를 반환해주지 않기 때문에 아래의 예제에서 meundefined가 됩니다.

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
  // new없이 호출하는 경우, 전역객체에 name 프로퍼티를 추가
  this.name = name;
};

// 일반 함수로서 호출되었기 때문에 객체를 암묵적으로 생성하여 반환하지 않는다.
// 일반 함수의 this는 전역객체를 가리킨다.
var me = Person('Kim');

console.log(me); // undefined
console.log(window.name); // Kim

위와 같이 생성자 함수에 new를 붙이지 않아 발생하는 오류들이 있습니다. 보통 생성자 함수는 첫문자를 대문자로 하여 일반 함수와 구분하고 있지만 그럼에도 실수를 방지하기는 어렵습니다.
그래서 이러한 위험성을 방지하기 위한 Scope-Safe Constructor 패턴이 있습니다. 이 패턴은 대부분의 라이브러리에서 광범위하게 사용된다고 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Scope-Safe Constructor Pattern
function A(arg) {
  // 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈객체를 생성하고 this에 바인딩한다.

  /*
  this가 호출된 함수(arguments.callee, 본 예제의 경우 A)의 인스턴스가 아니면
  new 연산자를 사용하지 않은 것이므로 이 경우 new와 함께 생성자 함수를 호출하여 
  인스턴스를 반환한다.
  arguments.callee는 호출된 함수의 이름을 나타낸다. 이 예제의 경우 A로 표기하여도 
  문제없이 동작하지만 특정함수의 이름과 의존성을 없애기 위해서 arguments.callee를 
  사용하는 것이 좋다.
  */
  if (!(this instanceof arguments.callee)) {
    return new arguments.callee(arg);
  }

  // 프로퍼티 생성과 값의 할당
  this.value = arg ? arg : 0;
}

var a = new A(100);
var b = A(10);

console.log(a.value); // 100
console.log(b.value); // 10

🍒 4. apply,call,bind 호출

this에 바인딩 될 객체는 자바스크립트 엔진이 암묵적으로 수행해줍니다.
하지만 apply, call, bind 메소드를 사용하면 명시적으로 this를 바인딩 할 수 있는습니다.

apply, call, bind는 Function.prototype의 메소드이다.

1
2
3
4
5
Person.apply(foo, [1, 2, 3]);

Person.call(foo, 1, 2, 3);

Person.bind(foo)(1,2,3);

이 메소드들은 콜백 함수의 this를 위해서도 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
  this.name = name;
}

Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    // --------- 1
    callback();
  }
};

function foo() {
  console.log(this.name); // --------- 2
}

var p = new Person('Lee');
p.doSomething(foo);  // undefined

1 시점에서 thisPerson 객체입니다. 그러나 2 시점에서 this는 전역 객체입니다. 다음과 같이 call, apply, bind 메소드를 사용하여 this를 일치시킬 수 있습니다.

call

1
2
3
4
5
6
Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    // --------- 1
    callback.call(this);
  }
};

apply

1
2
3
4
5
6
Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    // --------- 1
    callback.apply(this);
  }
};

bind

bindthis가 바인딩 된 새로운 함수를 리턴합니다.

1
2
3
4
5
6
Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    // --------- 1
    callback.bind(this)();
  }
};