본문 바로가기

NodeJS/javascript

자바스크립트 this

원본 : http://www.nextree.co.kr/p7522/







JavaScript의 this는 자바와 C++ 등을 주로 사용하던 개발자들에게 혼란을 주는 키워드입니다. 보통 자바와 C++ 등 여타언어에서의 this는 self(자기 자신)라는 의미로 사용됩니다. 하지만 JavaScript의 this는 기존 언어에서 사용하던 this와는 다릅니다. 비슷한 의미로 사용될 때도 있지만, JavaScript의 this는 여러 가지 함수가 호출되는 방식(호출 패턴)에 따라 참조(바인딩)하는 객체가 다르기 때문입니다.

먼저 자바에서의 this를 간단히 살펴보겠습니다. 자바에서의 this는 인스턴스 자신을 가리키는 참조변수입니다. this가 객체 자신에 대한 참조 값을 가지고 있다는 것입니다. 주로 매개변수와 객체 자신이 가지고 있는 변수의 이름이 같을 경우 이를 구분하기 위해서 사용됩니다.

 자바의 this 키워드

위 예제와 같이 함수의 매개변수와 클래스의 멤버변수가 같은 이름을 가지고 있을 때, this를 사용한 다음에 오는 변수명은 멤버 변수를 말하는 것입니다. 그리고 그냥 변수명을 사용한 경우는 매개변수를 말하는 것입니다. 이런 식으로 Java에서는 this가 ‘자기 자신’이라는 의미로 사용됩니다. 하지만 JavaScript에서는 이렇게 한가지 의미로 this 키워드가 사용되지 않고, 해당 함수 호출 패턴에 따라 사용하는 의미가 다릅니다.

JavaScript의 this가 해당 함수 호출 패턴에 따라 어떤 객체를 참조(바인딩)하는지에 대한 규칙을 요약하면 다음과 같습니다.

1. 기본적으로 this는 전역 객체를 참조한다.
2. 메소드 내부의 this는 해당 메소드를 호출한 부모 객체를 참조한다.
3. 생성자 함수 코드 내부의 this는 새로 생성된 객체를 참조한다.
4. call()과 apply() 메소드로 함수를 호출할 때, 함수의 this는 첫 번째 인자로 넘겨받은 객체를 참조한다.
5. 프로토타입 객체 메소드 내부의 this도 해당 메소드를 호출한 부모 객체를 참조한다.
6. JavaScript의 this 키워드는 접근제어자 public 역할을 한다.

위와 같은 규칙들을 하나씩 살펴보겠습니다.

 

1. this는 전역 객체를 참조합니다.

JavaScript의 모든 전역 변수는 실제로 전역 객체(브라우저에서는 window객체)의 프로퍼티들입니다. 모든 프로그램은 global code의 실행에 의해서 시작하고 this는 정해진 실행 문맥의 안에 고정되기 때문에 JavaScript의 this는 global code의 전역 객체입니다. (_this refers to the global object in all global code._ Since all programs start by executing global code, and this is fixed inside of a given execution context, we know that, by default, this is the global object.)

이제부터 소개할 3가지 규칙에서의 this를 제외하고 JavaScript의 this는 전역 객체를 참조한다고 생각하면 이해하기 쉬울 것입니다.

 

2. 메소드를 호출할 때, 메소드 내부의 this는 해당 메소드를 호출한 부모 객체를 참조합니다.

객체의 프로퍼티가 함수일 경우, 이 함수를 메소드라고 부릅니다. 이러한 메소드를 호출할 때, 메소드 내부의 this는 해당 메소드를 호출한 부모 객체를 참조합니다. 

[source1] 메소드 내부의 this

counter객체가 increment메소드를 부모 객체로서 호출하는 예제입니다. 예제에서 counter.increment()와 counter['increment']() 모두 JavaScript에서 부모 객체를 통해 메소드를 호출하는 같은 표현입니다. 앞서 설명했던 대로 increment 메소드 내부에서 사용된 this는 자신을 호출한 객체를 참조하여 val 값을 증가시킵니다.

 

 [source2] 전역 객체를 참조하는 this

전역 객체를 참조하는 this 예제입니다. 예상과 달리 결과가 0, 101로 출력됩니다. 전역 변수인 inc에 increment 메소드를 대입하고 inc함수를 호출하게 되면 더 이상 increment 메소드가 counter 객체의 자식으로서 호출되는 것이 아니라 전역 객체에 포함된 일반 함수 inc가 호출되는 것입니다. 그래서 this는 더 이상 counter 객체를 참조하지 않고 전역 객체를 참조하게 되어 전역변수 val의 값을 1 증가시켜 결과가 0, 101로 출력됩니다.

 

[source3] 내부 함수의 this

이번엔 내부 함수의 this의 예제입니다. 예상했던 결과인 2, 3, 4가 아닌 2, 101, 102로 결과가 출력됩니다. 이러한 결과 때문에 내부 함수에서 this를 사용할 때 주의해야 합니다. 실행결과가 예측했던 것과 다르게 출력된 이유는 JavaScript에서는 내부 함수 호출 패턴을 정의해 놓지 않기 때문입니다. 내부 함수도 결국 함수이므로 이를 호출할 때는 함수 호출로 취급되어 함수 호출 패턴 규칙에 따라 내부 함수의 this는 전역 객체를 참조하게 됩니다. 따라서 func1(메소드)에서는 메소드 내부의 val 값이 출력되고, func2(내부 함수), func3(내부 함수)에서는 전역 변수의 val 값이 출력됩니다.

 

[source4] 내부 함수의 this 한계 극복

내부 함수의 this의 문제를 해결한 예제입니다. 내부 함수가 this를 참조하는 JavaScript의 한계를 극복하려면 부모 함수의 this를 내부 함수가 접근 가능한 다른 변수에 저장하는 방법이 사용됩니다. 보통 관레상 this값을 저장하는 변수의 이름을 that이라고 짓습니다. 자신을 둘러싼 부모 함수의 변수에 접근 가능한 내부 함수의 특징을 가지고 내부 함수에서는 that 변수를 통해 부모 함수의 this가 가리키는 객체에 접근할 수 있습니다.  

 

3. 생성자 함수를 호출할 때, 생성자 함수 코드 내부의 this는 새로 생성된 객체를 참조합니다.

[source5] 생성자 함수 코드 내부의 this

여타 언어와 비슷한 패턴이라 이해하기 쉽습니다. 생성자 함수 F를 정의하고 new 연산자를 사용하여 객체를 생성하는 예제입니다. new 연산자로 JavaScript 함수를 생성자로 호출할 때 동작하는 방식을 살펴보면, 생성자 함수 코드가 실행되기 전 빈 객체가 생성되고 이 객체에 this가 바인딩 되는 순서로 동작합니다. 그래서 f객체 내부의 this는 f를 참조하게 되고 인자로 전달받은 constructor function 을 출력하게 됩니다. 

 

[source6] new 연산자를 붙이지 않은 예제

생성자 함수에 new 연산자를 붙이지 않은 예제입니다. 생성자 함수를 호출할 때, new 연산자를 붙이지 않으면 일반 함수를 호출하는 것과 같이 동작하여 this는 전역 객체를 참조합니다. 그래서 원래 의도와는 다르게 this가 바인딩 된 전역 객체(window 객체)에 동적으로 val 이 생성됩니다. 전역 객체에 val이 생성되었기 때문에 console.log(f.val) 는 Uncaught TypeError가 발생하게 되고 console.log(val)을 통해 전역 객체 val 의 값을 출력할 수 있습니다. 이러한 문제가 있기 때문에 생성자 함수를 사용할 때, new 연산자 사용을 주의해야 합니다.

 

4. apply()과 call() 메소드로 함수를 호출할 때, 함수의 this는 첫 번째 인자로 넘겨받은 객체를 참조합니다.

지금까지는 JavaScript에서 함수 호출이 발생할 때 각각의 상황에 따라 this가 정해진 객체를 자동으로 참조하는 것을 살펴보았습니다. JavaScript에는 이러한 내부적인 this 바인딩 이외에도 this를 특정 객체에 명시적으로 바인딩시키는 apply()와 call()메소드를 제공합니다. apply()메소드와 call() 메소드의 기능은 같습니다. apply()과 call() 메소드로 함수를 호출할 때, 함수는 첫 번째 인자로 넘겨받은 객체를 this로 바인딩하고, 두 번째 인자로 넘긴 값들은 함수의 인자로 전달됩니다. 두 메소드의 차이점은 두 번째 인자에서 apply() 메소드는 배열 형태로 인자를 넘기고, call()메소드는 배열 형태로 넘긴 것을 각각 하나의 인자로 넘기는 것입니다. 

[source7] apply(), call() 메소드

apply(), call() 메소드 예제입니다. 출력결과를 보시면 apply() 메소드와 call() 메소드의 호출 방법만 다르고 결과 값은 같습니다. apply() 메소드와 call() 메소드의 첫 번째 인자로 obj객체를 넘겨주고 add 함수는 넘겨받은 obj객체의 val값에 두 번째 인자로 넘겨받은 값들을 더하여 출력합니다.

 

5. 프로토타입 객체 메소드에서도 메소드 내부의 this는 해당 메소드를 호출한 부모 객체를 참조합니다.

프로토타입 객체는 메소드를 가질 수 있습니다. 이 프로토타입 메소드 내부에서의 this도 똑같이 메소드 내부의 this는 해당 메소드를 호출한 부모 객체를 참조합니다.

[source8] 프로토타입 객체 메소드 내부의 this

프로토타입 객체 메소드 예제입니다. foo 객체에서 getName() 메소드를 호출하면, getName() 메소드는 foo 객체에서 찾을 수 없으므로 프로토타입 체이닝이 발생합니다. foo 객체의 프로토타입 객체인 person.prototytpe에서 getName() 메소드가 있으므로, 이 메소드가 호출됩니다. 이때 getName() 메소드를 호출한 객체는 foo이므로, this는 foo 객체에 바인딩 됩니다. 따라서 foo.getName()의 결과로 foo가 출력됩니다. Person.prototype.getName() 메소드와 같이 바로 Person.prototype 객체에 접근해서 getName() 메소드를 호출하면 getName() 메소드를 호출한 객체가 Person.prototype이므로 this도 여기에 바인딩 됩니다. 그리고 Person.prototype 객체에 name 프로퍼티를 동적으로 추가하고 person을 저장했으므로 this.name은 person이 출력됩니다.

 

6. JavaScript의 this 키워드는 접근제어자 public 역할을 합니다.

JavaScript에서는 public, private 키워드 자체를 지원하지 않지만 public 역할을 하는 this 키워드와 private 역할을 하는 var 키워드가 있습니다. JavaScript에서는 이 두 키워드를 사용하여 캡슐화를 구현할 수 있습니다.

[source9] 접근제어자 this

JavaScript의 캡슐화 예제입니다. 결과를 보면 this키워드로 선언된 Age에는 접근할 수 있지만 var 키워드로 선언된 Name에는 접근할 수 없습니다.(var 키워드로 선언된 변수는 외부에서 확인할 수 없으므로 undefined로 출력됩니다.) 이렇게 감추고 싶은 멤버는 var키워드로 감추고 this키워드로 선언된 함수를 외부로 노출시켜 값을 얻어오거나 변경하는 등 캡슐화를 간단하게 구현할 수 있습니다.

 

맺음말

지금까지 JavaScript의 this가 해당 함수 호출 패턴에 따라 어떤 객체를 참조(바인딩)하는지에 대한 규칙을 살펴보았습니다. 처음 예제 소스를 만들며 실수를 하여 원하는 결과가 나오지 않아 혼란을 겪기도 했습니다. 하지만 이해되지 않는 부분에서 소스를 응용해 보며 공부한 게 효과가 있어 저와 같이 JavaScript를 처음 공부하시는 분들에게 도움이 될 것 같아 예제 소스를 첨부하였습니다. 혹여, 틀리거나 부족한 부분이 있다면 코멘트 부탁합니다. 감사합니다.