본문 바로가기

Javascript/Javascript

[Javascript] this

728x90
반응형

 

 

 

this 정의

let group = {
  title: "1team",
  students: ["Kim", "Lee", "Park"],

  title2 : this.title,
  title3() { console.log(this.title) }
};

console.log(group.title2); //undefined
group.title3(); // 1team

 

this는 함수의 블록 스코프 내에서 선언되어야 작동한다.

 

 

브라우저의 콘솔창을 켜고(F12) this를 입력해 보자.

this; // Window {}

 

그리고 이번엔 변수와 함수 안에 넣어서 실행해 보자.

var ga = 'Global variable';
console.log(this.ga); // === window.ga

function a() { console.log(this); };
a(); // Window {}

 

역시 window이다(함수일 경우 strict 모드일 때는 undefined).

 

여기서 한 가지 사실을 알 수 있다. this는 기본적으로 window 객체다. 일반 함수 내에서 혼자 this를 선언하면 그 this는 window 객체를 가리킨다.

 

 

그렇다면 이번엔 일반 함수가 아닌 객체 메서드의 경우를 보자.

var obj = {
  a: function() { console.log(this); },
};
obj.a(); // obj

 

객체 내 함수 a 안의 this는 객체 obj를 가리킨다. 이는 객체의 메서드를 호출할 때 this를 내부적으로 바꿔주기 때문이다.

 

단, 위의 예시에서 다음과 같이 하면 결과가 달라진다.

var a2 = obj.a;
a2(); // window

 

a2obj.a를 꺼내온 것이기 때문에 더이상 obj의 메서드가 아니고 변수에 담긴 일반 함수이다. 결국 this를 호출할 때 호출하는 함수가 객체의 메서드인지 그냥 일반 함수인지가 중요하다.

 

 

Java의 경우 this는 인스턴스 자신(self)을 가리키는 참조 변수다. this가 객체 자신에 대한 참조 값을 가지고 있다는 뜻이다. 주로 매개변수와 객체 자신이 가지고 있는 멤버변수명이 같을 경우 이를 구분하기 위해 사용된다. 따라서 아래 Java 코드의 생성자 함수 내의 this.name은 멤버변수를 의미하며 name은 생서앚 함수가 전달 받은 매개변수를 의미한다.

// JAVA
public Class Person {

  private String name;

  public Person(String name) {
    this.name = name; // this.name과 그냥 name은 완전히 다른 놈이다.
  }
}

 

하지만 자바스크립트의 경우 Java와 같이 this에 바인딩되는 객체가 한 가지로 고정되는 게 아니라 해당 함수의 호출 방식에 따라 this에 바인딩되는 객체가 달라진다.

 

 


 

 

함수 호출 방식과 this 바인딩

자바스크립트의 경우 함수 호출 방식에 의해 this에 바인딩할 객체가 동적으로 결정된다. 다시 말해서 함수를 선언할 때 this에 바인딩할 객체가 정적으로 결정되는 것이 아니라 함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정된다는 것이다.

 

함수를 호출하는 방식은 아래와 같이 다양하다

  • 함수 호출
  • 메서드 호출
  • 생성자 함수 호출
  • 콜백 호출
  • apply/call/bind 호출

 

 

1. 함수 호출

 

기본적으로 this는 전역 객체(Global Object)에 바인딩된다. 일반 전역 함수는 물론이고 내부 함수의 경우에도 this는 외부 함수가 아닌 전역 객체에 바인딩된다.

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

 

 

또한 객체 메서드의 내부 함수일 경우에도 this는 전역 객체에 바인딩된다.

var value = 1;

var obj = {
  value: 100,
  foo: function() {
    console.log("foo's this: ",  this);  // obj
    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
    }
    bar();
  }
};

obj.foo();

 

 

콜백 함수의 경우에도 this는 전역 객체에 바인딩된다.

var value = 1;

var obj = {
  value: 100,
  foo: function() {
    setTimeout(function() {  /* 콜백함수 */
      console.log("callback's this: ",  this);  // window
      console.log("callback's this.value: ",  this.value); // 1
    }, 100);
  }
};

obj.foo();

 

내부 함수는 일반 함수, 메서드, 콜백 함수 어디에서 선언되었든 관계 없이 this는 전역 객체를 바인딩한다.

 

 

내부함수의 this가 전역 객체를 참조하는 것을 회피하는 방법은 아래와 같다.

  • var that = this;와 같이 객체의 this를 변수에 저장해 사용.
  • call, bind, apply로 this 설정
  • 화살표 함수(arrow function) 사용
var value = 1;

var obj = {
  value: 100,
  foo: function() {
    var that = this;  // Workaround : this === obj

    console.log("foo's this: ",  this);  // obj
    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

      console.log("bar's that: ",  that); // obj
      console.log("bar's that.value: ", that.value); // 100
    }
    bar();
  }
};

obj.foo();

 

 


 

 

2. 메서드 호출

 

함수가 객체의 프로퍼티 값이면 메서드로서 호출된다. 이때 메서드 내부의 this는 해당 메서드를 소유한 객체, 즉 해당 메서드를 호출한 객체에 바인딩된다.

var obj1 = {
  name: 'Lee',
  sayName: function() {
    console.log(this.name);
  }
}

var obj2 = {
  name: 'Kim'
}

obj2.sayName = obj1.sayName;

obj1.sayName(); // Lee
obj2.sayName(); // Kim

 

 


 

 

3. 프로토타입

 

프로토타입 객체도 메서드를 가질 수 있다. 프로토타입 객체 메서드 내부에서 사용된 this도 일반 메서드 방식과 마찬가지로 해당 메서드를 호출한 프로토타입 오브젝트 객체에 바인딩된다. 

 

따라서 this의 프로퍼티를 찾을 때 우선 직접 바인딩되어 있는 프로토타입 오브젝트에서 찾고 없으면 체이닝에 의해 new 생성자로 생성된 객체에서 찾게 된다.

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

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

var me = new Person('Lee');
console.log(me.getName());  // Lee
// 우선 프로토타입에서 name프로퍼티를 찾는다. 없으니 체이닝에 의해 me 객체에서 찾아서 반환


Person.prototype.name = 'Kim';
console.log(Person.prototype.getName());  // Kim
// 우선 프로토타입에서 name 프로퍼티를 찾는다. 찾았으니 반환.

 

 

 


 

 

4. 생성자 함수 호출

 

자바스크립트의 생성자 함수는 말 그대로 객체를 생성하는 역할을 한다. 하지만 자바와 같은 객체지향 언어의 생성자 함수와는 다르게 그 형식이 정해져 있는 것이 아니라 기존 함수에 new 연산자를 붙여 호출하면 해당 함수는 생성자 함수로 동작한다.

 

이는 반대로 생각하면 생성자 함수가 아닌 일반 함수에 new 연산자를 붙여 호출하면 생성자 함수처럼 동작할 수 있다는 것이다. 따라서 일반적으로 생성자 함수명은 첫 문자를 대문자로 기술하여 혼란을 방지하려는 노력을 한다.

// 생성자 함수
function Person(name) {
  this.name = name;
}

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

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

 

 


 

 

5. 콜백 함수 호출

let userData = {
    signUp: '2020-10-06 15:00:00',
    id: 'minidoo',
    name: 'Not Set',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback) {
    callback(firstName, lastName);
}

getUserName('PARK', 'MINIDDO', userData.setName);

console.log('1: ', userData.name); // Not Set
console.log('2: ', window.name); // PARK MINIDDO

 

우리는 첫 번째 콘솔의 값이 PARK MINIDDO이기를 기대했지만 Not Set이 출력된다. setName() 함수가 실행되기 전의 name 값이 나오는 것인데 이는 getUserName()이 전역 함수이기 때문이다. userData.setName를 argument로 넘겨줄 때 CALL BY VALUE로 가는 걸 명심해야 한다.(JS는 무조건 call by value)

 

한 마디로 함수가 복사되어 callback 파라미터에 담기게 되니 당연히 setName()의 this는 전역객체 window를 가리키게 되는 것이다.

 

cf) 해결방안: call()과 apply()를 활용하여 this를 보호할 수 있다.

function getUserName(firstName, lastName, callback) {
    callback.call(userData, firstName, lastName);
}

getUserName('PARK', 'MINIDDO', userData.setName);

console.log('1: ', userData.name); // PARK MINIDDO
console.log('2: ', window.name); // 빈칸. 왜냐하면 변수가 없으니까

 

 


 

 

6.  apply / call / bind 호출

func.apply(thisArg, [argsArray])
func.call(thisArg, argsArray)
func.bind(thisArg)(argsArray)

// thisArg: 함수 내부의 this에 바인딩할 객체
// argsArray: 함수에 전달할 argument의 배열

 

기억해야 할 것은 apply()를 호출하는 주체는 func 함수이며 .apply() 함수는 this를 특정 객체에 바인딩할 뿐 본질적인 기능은 함수 호출이라는 것이다.

 

var name = "window name";

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);  // window name

 

1의 시점에서 this는 Person 객체다. 그러나 2의 시점에서 this는 전역 객체 window를 가리킨다. 기본적으로 콜백함수 내부의 this는 window를 가리킨다.

 

 

따라서 콜백함수 내부의 this를 콜백함수를 호출하는 함수 내부의 this와 일치시켜 줘야 한다.

   // --------- 1
   callback.bind(this)();

 

 


 

 

실무에서 이벤트리스너나 JQuery 등을 썼을 경우를 가정해 보자.

document.body.onclick = function() {
  console.log(this); // <body>
}

 

이건 단순히 함수인데 this가 window가 아니라 <body>이다. 객체 메서드도 아니고 bind한 것도 아니고 new를 붙인 것도 아닌데 말이다. 어떻게 된 일일까?

 

바로 이벤트가 발생할 때 내부적으로 this가 바뀐 것이다. 내부적으로 바뀐 것이기 때문에 동작을 외울 수밖에 없다.

 

 

$('div').on('click', function() {
  console.log(this);
});

 

위와 같은 JQuery 소스를 본 적이 있을 것이다. 여기서 this는 클릭한 <div>가 된다. 이 또한 function을 호출할 때 내부적으로 this를 바꿔버린 것이다.

 

 

아래는 응용 사례다. 방금 전 클릭 이벤트에서 JQuery가 내부적으로 this를 바꿔버린다고 설명했는데 inner 함수 호출 시에는 this가 window이다(일반적으로 내부함수의 this는 window).

$('div').on('click', function() {
  console.log(this); // <div>
  function inner() {
    console.log('inner', this); // inner Window
  }
  inner();
});

 

이는 그저 click 이벤트리스너가 내부적으로 this를 바꿨음에도 명시적으로 알리지 않은 것이다.

 

위의 문제를 해결하기 위해서는

$('div').on('click', function() {
  console.log(this); // <div>
  var that = this; // <-------------------------------------------------------------
  function inner() {
    console.log('inner', that); // inner <div>
  }
  inner();
});

 

위와 같이 this를 변수(위 예에서는 that)에 저장하든지

 

$('div').on('click', function() {
  console.log(this); // <div>
  const inner = () => {
    console.log('inner', this); // inner <div>
  }
  inner();
});

 

ES6의 화살표 함수를 쓴다. 화살표 함수는 this로 window 대신 상위 함수의 this를 가져온다(위 예에서는 <div>).

 

 


 

 

체이닝

함수에서 자기 자신 this를 리턴하면 자기 객체를 가리키기 때문에 연속으로  . 을 사용할 수 있다.

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
  }
}

ladder.up().up().down().up().down().showStep(); // 1

 

 


 

정리

 

정리하자면 this는 기본적으로 window 객체이지만 객체 메서드, bind / call / apply, new일 때 this가 바뀐다. 그리고 이벤트리스너나 기타 document 라이브러리처럼 this를 내부적으로 바꿀 수도 있으니 항상 this를 확인해 볼 필요가 있다.

 

우리가 선언한 function의 this는 항상 window라는 것을 알아두자(strict 모드에서는 undefined).

 

this 값은 런타임에 결정된다. 또한 함수를 선언할 때 this를 사용할 수 있다. 다만, 함수가 호출되기 전까지는 this에 값이 할당되지 않는다.

 

함수를 복사해 객체 간 전달할 수 있다. 함수를 객체 프로퍼티에 저장해 object.method() 같이 메서드 형태로 호출하면 this는 object를 참조한다.

 

화살표 함수는 자신만의 this를 가지지 않는다는 점에서 독특하다. 화살표 함수 안에서 this를 사용하면 외부에서 this 값을 가져온다.

 

 

 

 

 

참고

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-this-%EC%B4%9D%EC%A0%95%EB%A6%AC

 

 

 

 

728x90
반응형