📝

7. this

7-1. this란?

자바스크립트에서는 this키워드를 사용할 수 있다. this는 자바스크립트 엔진에 의해 생성되고 코드 어디에서나 참조가 가능하다. this값은 런타임에 결정되며 this는 함수를 호출하는 방식에 의해 값이 달라진다.
개발자 도구(F12) 실행하고 콘솔에서 this를 입력해보자.
 
this; // Window
notion imagenotion image
notion imagenotion image
💡
흔히 사용하는 console.log나 console.dir, console.table은 window 객체에 있다.
 
브라우저 환경의 전역 객체인 Window를 가리킨다. 전역 실행 컨텍스트에서 this는 항상 전역 객체를 참조한다. 여기서 전역 실행 컨텍스트란 자바스크립트 엔진이 코드를 실행할 때 처음으로 생성되는 컨텍스트이다. 전역 객체는 자바스크립트를 실행하는 환경에 따라 다르다. 만약 Node.js 환경에서 실행한다면 global 객체가 전역 객체가 된다.
 
chrome 환경에서 console.logchrome 환경에서 console.log
chrome 환경에서 console.log
node 환경에서 console.lognode 환경에서 console.log
node 환경에서 console.log
 
this는 기본적으로 자신이 속한 객체를 나타내는 것임을 알 수 있다.

7-2. 함수 호출 방식

앞서 this는 함수를 호출하는 방식에 따라 값이 달라진다고 설명했다. 함수를 호출하는 방식은 아래와 같다.
  • 일반 함수
  • 메서드
  • 생성자 함수
  • call(), apply(), bind()

7-2-1. 일반 함수

함수 선언문에서 함수를 호출하는 경우에는 this는 전역 객체인 Window가 바인딩된다.
 
function a() { console.log(this); } a(); // Window
 
그렇다면 자바스크립트 엄격 모드에서는 어떻게 바인딩 되는지 알아보자. use strict 지시문을 함수 최상단에 넣어 엄격 모드를 활성화할 수 있다.
💡
엄격 모드는 함수에 개별적으로 적용하기 보다는 script 전체에 적용한다. 유지보수를 할 때 혼란을 줄 수 있기 때문이다.
 
function a() { 'use strict'; console.log(this); } a(); // undefined
 
자바스크립트 엄격 모드에서 thisundefined이다.
a() 함수를 직접 호출하여 함수의 컨텍스트가 어디에 속하는지 알 수 없기 때문에 undefined가 바인딩 되는 것이다.
 
💡
자바스크립트 엄격 모드란? 자바스크립트 엄격 모드는 ES5에서 등장했으며 ES5에서 이전 문법을 호환하지 않는 몇몇 기능들을 추가하면서 생겼다. 호환성을 고려하지 않는다면 이전 스크립트가 문제가 생기기 때문에 엄격 모드에서 작동되게 한 것이다. 이는 자바스크립트 역사와도 관련이 있기 때문에 아래의 링크를 통해 좀 더 자세히 확인 할 수 있다.
 
중첩 함수를 일반 함수로 호출하는 경우에서도 마찬가지로 전역 객체가 바인딩된다.
 
function a() { console.log(this); // window function b() { console.log(this); // window } b(); } a(); function a() { 'use strict'; console.log(this); // undefined function b() { console.log(this); // undefined } b(); } a();
 
this는 자신이 속한 객체를 나타내는데 객체를 생성하지 않는 일반 함수에서 this는 의미가 없다. 중첩 함수도 일반 함수로 호출되면 중첩 함수 내부의 this에는 전역 객체가 바인딩 된다. 어떤 함수라도 일반 함수로 호출되면 전역 객체가 바인딩 되는 것을 기억하자.
 
💡
일반 함수 호출은 모두 전역 객체를 가리킬까?
모두 전역 객체 window를 가리킨다고 할 수는 없다. 함수 중에는 mapforEach와 같이 인자로 명시적으로 this 값을 지정하는 함수(메서드)도 있기 때문이다. 따라서 무조건 일반 함수로써 호출되었다고 해서 window로 단정 짓기 전에 어디서 어떻게 함수가 호출되었는지 확인해 볼 필요가 있다.
const arr = ["apple", "banana", "graph"]; // this가 window를 가리키지 않음 arr.map(function() { console.log(this); }, { a : 1 });
{ a: 1 } { a: 1 } { a: 1 } (3) [undefined, undefined, undefined]
 

7-2-2. 메서드 호출

this는 자신이 속한 객체를 나타내는 자기 참조 변수이다. this를 통해 자신이 속한 객체 참조가 가능하다. 그러므로 메서드 내부에서 this 키워드를 사용한다면 객체에 접근할 수 있다.
쉽게 이해할 수 있도록 예제를 통해 살펴보자.
 
let fruit = { name: 'Apple', price: 500, }; // 메서드 : 상태 데이터를 참조하고 조작하는 동작 fruit.fruitName = function() { alert("사과입니다."); }; fruit.fruitName(); // 사과입니다.
 
함수를 만들고 객체 프로퍼티 fruit.fruitName에 함수를 할당했다. 예시에서 메서드는 fruit에 할당된 fruitName이다.
메서드는 이미 정의된 함수를 이용해서도 만들 수 있다.
 
let fruit = { //... }; // 함수 선언 function fruitName() { alert("사과입니다."); }; // 선언된 함수를 메서드로 등록 fruit.fruitName = fruitName; fruit.fruitName(); // 사과입니다.
 
이제 this를 사용해보자.
 
let fruit = { // 프로퍼티 : 객체 고유의 상태 데이터 name: "Apple", price: 500, // 'this'는 '현재 객체'를 가리킵니다. fruitName: function() { alert( this.name ); } }; fruit.fruitName(); // Apple
 
단축 문법을 사용한다면 메서드를 선언할 때 function을 생략해도 메서드를 정의할 수 있다.
 
let fruit = { name: "Apple", price: 500, fruitName() { alert( this.name ); } }; fruit.fruitName(); // Apple
 
fruitName 메서드는 fruit 객체의 메서드로 정의되었다. .앞에 this는 객체를 나타낸다. fruit.fruitName(); 이 실행되는 동안 thisfruit 이라는 객체를 나타낸다. 대부분의 메서드가 객체 프로퍼티의 값을 활용한다. fruit 객체의 fruitName 프로퍼티가 가리키는 함수 객체는 fruit 객체에 포함된 것이 아닌 별도의 객체이다. this 키워드를 사용해 함수 객체를 가리키는 것 뿐이다. 그렇다면 fruitName 메서드는 다른 객체의 프로퍼티에 할당하는 것으로 다른 객체의 메서드가 될 수도 있고 일반 변수에 할당해 일반 함수로 호출할 수 있다.
 
let anotherFruit = { name: "Strawberry" }; // fruitName 메서드를 anotherFruit 객체의 메서드로 할당 anotherFruit.fruitName = fruit.fruitName; // fruitName 메서드를 호출한 객체는 anotherFruit이다. console.log(anotherFruit.fruitName()); // Strawberry // fruitName 메서드를 변수에 할당 let fruitName = fruit.fruitName; // fruitName 메서드를 일반 함수로 호출 console.log(fruitName()); // '' // this.name = window.name // window.name은 빌트인 프로퍼티로 브라우저 창의 이름을 나타내며 기본값은 ''이다.
 
결과적으로 this는 프로퍼티로 메서드를 가리키고 있는 객체와는 관계가 없고 메서드를 어떻게 호출했느냐에 따라 this 바인딩이 달라진다.
마지막으로 메서드를 사용할 때 주의할 점이 있다. 예제를 통해 알아보도록 하자.
 
let obj = { word: 'world', sayhello() { return `hello ${this.word}`; } } // 선언된 함수를 메서드로 등록 let sayhello = obj.sayhello; console.log(sayhello()); // hello undefined
 
예제처럼 메서드를 호출하면 this는 일반 함수를 호출했을 때와 동일하게 전역 객체를 참조하거나 엄격 모드인 경우에는 undefined가 바인딩 된다.
 
let obj = { word: 'world', sayhello() { console.log(this); return `hello ${this.word}`; } } // 선언된 함수를 메서드로 등록 let sayhello = obj.sayhello; console.log(obj.sayhello()); // hello world
 
그러므로 메서드를 제대로 사용하기 위해서는 반드시 해당 객체의 컨텍스트로 명확하게 호출해야 정확한 결과를 얻을 수 있다.
 

7-2-3. 생성자 함수 호출

생성자 함수는 생성자 함수의 코드를 기반으로 자신의 인스턴스를 만드는 함수를 뜻한다. new 연산자와 함께 사용되며 생성자 함수는 인스턴스를 만들기 위해 다음 3가지 단계를 수행한다.
 
인스턴스 생성 시 수행되는 3가지 단계.[1]
  1. 생성자함수.prototype을 상속하는 빈 객체가 생성된다. 이때 빈 객체는 생성자함수.prototype을 상속하므로 빈 객체에 생성자 함수의 메서드들이 프로토타입 체인을 통해 연결된다.
  1. 생성자 함수 코드를 실행한다. 보통 이 단계를 통해서 생성된 객체에 필요한 프로퍼티를 연결시켜준다. (경우에 따라서 메서드도 추가가 가능하다.)
  1. 함수 실행이 끝나면 프로퍼티와 메서드가 연결된 완성된 객체(인스턴스)를 new 연산의 결괏값으로 반환한다.
 
💡
생성자함수.prototype? prototype은 단순히 생성자 함수의 프로퍼티 중 하나이다. 생성자 함수 코드 내부에서 생성자함수.prototype.메서드이름 을 통해서 prototype 프로퍼티에 메서드를 등록해주면 new 연산자로 인해 생성된 인스턴스가 프로토타입 체인에 의해서 생성자함수.prototype을 가리키게 되고 인스턴스.메서드이름() 으로 생성자 함수의 메서드를 이용할 수 있다. 자세한 내용은 MDN 문서의 Objects prototypes를 참고하면 된다.[2]
// 전체 코드 function NormalFunction(arg1, arg2) { ... // 생성자함수.prototype.메서드이름 NormalFunction.prototype.normalMethod = function () { console.log("노말 메서드 예시입니다."); }; } const normal = new NormalFunction(10, 20); // 인스턴스.메서드이름() normal.normalMethod();
노말 메서드 예시입니다.
 

7-2-3-1. 생성자 함수 내 this의 부재

아래의 코드는 생성자 함수 코드를 예시로 작성한 코드인데 어딘가 동작에 문제가 있는 코드이다. 아래의 코드의 실행 결과를 확인해 보면서 어떠한 문제가 있는지 살펴보자.
 
// 전체 코드 function NormalFunction(arg1, arg2) { // 빈 객체 {}가 생성되고 함수 함수 코드를 실행하여도 {}를 가리키는 것이 없다면 {}에 프로퍼티와 메서드를 추가할 수 있는 방법도 {}를 반환할 방법도 없다. const normalArg1 = arg1; const normalArg2 = arg2; const normalMethod = function () { console.log("노말 메서드 예시입니다."); }; } const normal = new NormalFunction(10, 20); console.log(normal);
this가 없는 생성자 함수의 인스턴스 생성
this가 없는 생성자 함수의 인스턴스 출력 결과this가 없는 생성자 함수의 인스턴스 출력 결과
this가 없는 생성자 함수의 인스턴스 출력 결과
 
코드의 출력 결과를 확인해 보면 생성된 인스턴스는 빈 객체로 아무런 프로퍼티를 가지고 있지 않다. 정상적인 생성자 함수의 코드라면 nomalArg1, nomalArg2 프로퍼티와 normalMethod를 메서드로 가지는 인스턴스가 생성되어야 했을 것이다. 이러한 문제가 발생하는 이유는 JavaScript에서 인스턴스 생성을 위한 빈 객체({})를 만들어주어도 이를 나타내는 변수가 없기 때문에 발생한다. 빈 객체({})에 프로퍼티나 메서드를 추가하려면 {}.normalArg1 = arg1의 형식으로 프로퍼티와 메서드를 추가해야 하는데 {}가 생성되어도 이를 나타내는 변수가 없기 때문에 추가하고자 하는 프로퍼티와 메서드가 추가되지 않고 빈 인스턴스가 생성되는 것이다.
결국 생성자 함수 내부에는 생성된 빈 객체(인스턴스)를 가리킬 무언가가 있어야 우리가 원하는 방식의 동작(프로퍼티와 메서드가 채워진 인스턴스를 반환받는 것)이 가능해진다. 이 무언가의 역할을 Javascript에선 this가 수행하고 있다. 따라서 생성자 함수로 호출된 함수 내의 this는 생성자 함수가 생성할 인스턴스를 가리킨다고 할 수 있다.
이러한 this 바인딩 작업(this의 참조 값으로 인스턴스가 할당되는 작업)은 new 연산자를 통해 함수가 실행되어 빈 객체가 생성될 때 Javascript가 생성자 함수 내부의 this와 생성된 빈 객체를 연결해 준다. 그렇기 때문에 위 예제처럼 this를 명시적으로 써주지 않아도 실행 결과로 다음과 같이 빈 객체와 NormalFunctionthis가 바인딩 되어 new 연산의 결과로 빈 객체 인스턴스를 반환받을 수 있다.
 
💡
Tip: this 확인하기
  • 코드를 수행하는 동안 console.dir 명령어로 NormalFunction의 구조 안에서는 this를 찾아볼 수 없다.
  • this의 존재는 개발자 도구 > 소스에서 디버깅을 통해 로컬 스코프에 있는 this를 확인할 수 있다.
 
this의 존재는 개발자 도구 > 소스에서 디버깅을 통해 로컬 스코프에 있는 this를 확인할 수 있다.this의 존재는 개발자 도구 > 소스에서 디버깅을 통해 로컬 스코프에 있는 this를 확인할 수 있다.
this의 존재는 개발자 도구 > 소스에서 디버깅을 통해 로컬 스코프에 있는 this를 확인할 수 있다.
 

7-2-3-2. 생성자 함수 코드 내 this

this는 생성자 함수로써 함수가 호출되었을 때 생성되는 빈 객체를 가리켜 프로퍼티와 경우에 따라서는 메서드도 추가할 수 있게 해준다.(일반적으로 인스턴스는 this를 통해 프로퍼티를 추가하고 생성자 함수의 prototype 프로퍼티를 이용해 메서드를 이용한다.) 생성자 함수에서 this 는 추가해 줄 프로퍼티와 메서드가 속할 객체가 인스턴스 생성을 위해 생성된 빈 객체라는 것을 알려준다고 할 수 있다.
다음 코드는 위의 예제를 this를 사용하여 수정해 준 코드이다. 아래 코드를 3 단계로 나눠서 설명하자면 다음과 같다.
 
// 전체 코드 function NormalFunction(arg1, arg2) { console.log(this); // this가 가리키는 것이 무엇인지 확인 this.normalArg1 = arg1; this.normalArg2 = arg2; this.normalMethod = function () { console.log("노말 메서드 예시입니다."); }; console.log(this); // 함수 수행을 통해 this가 어떻게 변화하였는지 확인 } const normal = new NormalFunction(10, 20); console.dir(normal);
this를 사용한 생성자 함수 NormalFunction()
 
  1. new 키워드와 함께 함수를 호출하면 생성자 함수로 함수를 호출하여 인스턴스로 쓸 빈 객체를 생성한다. 이때 생성자 함수 내의 this에 빈 객체를 바인딩 한다.
    1. // 전체 코드 function NormalFunction(arg1, arg2) { // 2. this는 아직 프로퍼티와 메서드가 추가되지 않은 빈 객체 {}를 가리킨다. console.log(this); ... } // 1. 빈 객체 {}가 생성되고 {}를 NoramlFunction의 this가 가리키게 된다. const normal = new NormalFunction(10, 20); ...
       
  1. 함수 내 코드를 수행하며 빈 객체에 프로퍼티와 메서드를 추가시켜준다.
    1. // 전체 코드 function NormalFunction(arg1, arg2) { console.log(this); this.normalArg1 = arg1; // {}.normalArg1 = arg1;처럼 동작 this.normalArg2 = arg2; // {}.normalArg2 = arg2;처럼 동작 // {}.normalMethod = function(){...} 처럼 동작 this.normalMethod = function () { console.log("노말 메서드 예시입니다."); }; // 함수 수행을 통해 this가 어떻게 변화하였는지 확인 console.log(this); } const normal = new NormalFunction(10, 20);
       
      콘솔 창에 출력된 결과는 다음과 같다.
       
위 :  NormalFunction의 this가 가리키고 있는 인스턴스 생성을 위해 생성된 빈 객체
아래 :  NormalFunction의 코드를 수행하면서 this를 통해서 프로퍼티와 메서드가 추가된 객체위 :  NormalFunction의 this가 가리키고 있는 인스턴스 생성을 위해 생성된 빈 객체
아래 :  NormalFunction의 코드를 수행하면서 this를 통해서 프로퍼티와 메서드가 추가된 객체
위 : NormalFunction의 this가 가리키고 있는 인스턴스 생성을 위해 생성된 빈 객체 아래 : NormalFunction의 코드를 수행하면서 this를 통해서 프로퍼티와 메서드가 추가된 객체
 
  1. 그리고 마지막으로 생성자 함수의 코드 수행이 완료되었다면 this 가리키고 있는 채워진 객체를 반환한다.
    1. // 전체 코드 function NormalFunction(arg1, arg2) { ... } const normal = new NormalFunction(10, 20); console.dir(normal);
 
반환된 인스턴스 출력반환된 인스턴스 출력
반환된 인스턴스 출력
 
이로써 생성자 함수로 호출되었을 때 this가 필요한 이유와 this를 이용해 인스턴스가 생성되는 과정을 알아보았다.
 
💡
Tip: 생성자 함수 return 생성자 함수의 목적은 인스턴스를 생성하기 위함이 목적이므로 생성자 함수 내에서는 return 문을 쓰지 않는 것이 좋다. return 문을 쓸 경우 생성된 인스턴스가 아니라 return에 명시해 준 값이 new 연산자의 결과로 반환된다.
💡
Tip: Prototype 활용하기 위 예제에서는 생성자 함수에 집중하기 위해 인스턴스에 메서드를 추가하는 데 this를 사용하여 작성해 주었다.(this.normalMethod(){...}) 이렇게 작성하면 생성되는 객체마다 normalMethod()가 메모리 영역에 생성되기 때문에 비효율적이다. 따라서 인스턴스에 메서드를 추가하고 사용하기 위해서는 NormalFunction.prototype을 통해서 메서드를 추가하여 사용하는 것이 일반적이다. NormalFunction.prototype에 메서드(NormalFunction.prototype.normalMethod(){...})를 추가했을 경우 인스턴스는 프로토타입 체인 상에 존재하는 메서드를 사용할 수 있게 되어 인스턴스(객체)의 메모리가 아닌 프로토타입 체인 상에 존재하는 한 개의 메서드를 공유해서 사용할 수 있다.
 
참고로 함수는 일반 함수로 호출할 수도 있고 생성자 함수로도 호출할 수 있기 때문에 일반 함수로 호출되는 경우에는 thiswidnow를 가리키고 new와 함께 생성자 함수로 호출되었을 때 this는 인스턴스를 가리킨다는 것을 기억하자.
 
// 전체 코드 function NormalFunction(arg1, arg2) { console.log(this); // this가 가리키는 것이 무엇인지 확인 this.normalArg1 = arg1; this.normalArg2 = arg2; this.normalMethod = function () { console.log("노말 메서드 예시입니다."); }; console.log(this); // 함수 수행을 통해 this가 어떻게 변화하였는지 확인 } console.log(window.normalArg1, window.normalArg2); // undefined, undefiend NormalFunction(10, 20); console.log(window.normalArg1, window.normalArg2); // 10, 20
일반 함수로써 호출된 NormalFunction()
 
NormalFunction은 생성자 함수로 작성했지만 일반 함수로 호출되어 함수 내 this 값이 전역 객체 window를 가리키게 된다. 그러므로 함수 호출을 통해서 windownormalArg1normalArg 2의 프로퍼티가 생성되었음을 확인할 수 있다.

7-3. call(), apply(), bind()

this 값은 함수 호출 방식마다 변한다고 했다. 매번 변하는 this 값을 생각하면서 코드를 작성하는 것은 코드가 늘어나고 복잡해질수록 어려운 작업이 될 수밖에 없다. 이러한 문제를 해결하기 위해서 Javascript에서는 call()apply(), bind() 메서드를 제공하고 있다.
이 메서드들은 함수 호출 방식마다 변하는 this를 원하는 고정 값으로 고정해 줄 수 있다. 이 메서드들은 함수 객체의 메서드이기 때문에 함수 객체에 붙어서 func.call(func 함수 내 this가 가리킬 값, ...) 형식으로 사용되며 첫 번째 인수에 this 가 참조할 값을 넘긴다는 공통된 특징을 가지고 있다.
다음 예제들을 통해 각각이 어떻게 사용되는지 살펴보고 이들 간에는 어떤 차이가 있는지 알아보자.
 

7-3-1. call()

call 메서드는 함수 객체의 메서드이다. func.call(thisValue, arg1, arg2, ...) 형태로 사용되며 call()의 첫 번째 인수로 this가 가리킬 것을 명시적으로 선언해 주고, 뒤에는 실행시킬(호출할) 함수 객체의 인수를 넘겨준다.
 
// mdn에서 정의하는 call 함수 구문 // func.call(thisArg[, arg1[, arg2[, ...]]]) function printThis(arg1, arg2) { console.log(this); // 일반 함수로 호출되면 this는 window를 가리켜야 한다. console.log(`call을 통해 넘겨 받은 인자들: ${arg1}, ${arg2}`); } const setThisValue = {}; printThis.call(setThisValue, "첫 인수", "둘째 인수");
call() 함수를 통해 함수 실행 및 this 지정해주기
 
실행 결과를 확인해 보면 printThis.call(setThisValue,"첫 인수", "둘째 인수") 의 실행 결과로 printThis 함수가 실행되며 실행 시 함수 내 thiscall() 메서드의 첫 번째 인수로 넘겨준 setThisValue를 가리키게 된다. 또한 setThisValue 뒤의 “첫 인수”, “둘째 인수”printThis 함수가 실행될 때 함수의 인수로 사용되게 된다.
 
{} call을 통해 넘겨 받은 인자들: 첫 인수, 둘째 인수
call 예시 코드 출력 결과
 

7-3-2. apply()

apply() 메서드도 call() 메서드와 마찬가지로 명시적으로 this가 참조할 값을 고정시켜 줄 수 있다. func.apply(thisValue, [arg1, arg2, ...]) 형태로 사용되며 apply()의 첫 번째 인수로 this가 가리킬 것을 명시적으로 선언해 주고, 두 번째 인수로 실행시킬(호출할) 함수 객체의 인수를 배열로 묶어서 넘겨준다. (넘겨주는 배열은 유사 배열 객체도 가능하다.)
 
// mdn에서 정의하는 apply 함수 구문 // func.apply(thisArg, [argsArray]) function printThis(arg1, arg2) { console.log(this); // 일반 함수로 호출되면 this는 window를 가리켜야 한다. console.log(`apply을 통해 넘겨 받은 인자들: ${arg1}, ${arg2}`); } const setThisValue = {}; printThis.apply(setThisValue, ["첫 인수", "둘째 인수"]);
apply() 함수를 통해 함수 실행 및 this 지정해주기
 
실행 결과는 call() 메서드의 경우와 매우 유사하다. 메서드 이름이 apply라는 점과 함수에 사용될 인수를 배열로 넘겨주는 것만 다를 뿐, printThis() 함수를 지정한 this 값으로 실행시킨다. 실행한 결과는 아래와 같다.
 
{} apply를 통해 넘겨 받은 인자들: 첫 인수, 둘째 인수
apply 예시 코드 출력 결과
 

7-3-3. bind()

bind() 메서드도 this가 참조할 값을 고정해 주는 함수 객체의 메서드이지만 앞서 본 call()apply() 메서드와는 조금 차이가 있다. 함수의 호출 여부와 메서드에 사용되는 인수의 용도에 있어 차이가 있다.
bind() 메서드는 func.bind(thisValue, arg1, arg2, ...) 형식으로 사용되며 bind() 메서드를 사용하는 함수 객체는 실행되지 않고 함수 객체의 내용과 동일한 함수를 생성하게 된다. 이 함수의 this 값이 bind() 메서드에서 넘겨주는 첫 번째 인수 값(thisValue)으로 고정된다. 또한 첫 번째 이후 인수들은 생성된 함수의 초기 인수로 들어가게 된다.
 
💡
초기 인수? 함수 호출에 필요한 인수에 우선적으로 들어가는 값으로 초기 인수가 있다면 함수 호출 시 초기 인수로 지정된 값들이 함수의 인수 앞에 삽입되어 함수가 실행된다.
 
// mdn에서 정의하는 bind 함수 구문 // func.bind(thisArg[, arg1[, arg2[, ...]]]) function printThis(arg1, arg2) { console.log(this); // 일반 함수로 호출되면 this는 window를 가리켜야 한다. console.log(`call을 통해 넘겨 받은 인자들: ${arg1}, ${arg2}`); } const setThisValue = {}; // 함수를 호출(실행)하지 않고 새로운 함수를 생성해준다. // 초기 인수로 "첫 인자"를 지정해준다. const newPrintThis = printThis.bind(setThisValue, "첫 인수"); // 초기 인수로 "첫 인수"를 넘겨주어 함수 호출 시 ("첫 인수", 함수 호출 시 넘겨준 인수들)를 인수로 함수를 호출시켜 준다. newPrintThis(); // newPrintThis("첫 인자");로 함수를 호출 newPrintThis("둘째 인자"); // newPrintThis("첫 인자", "둘째 인자");로 함수를 호출 // bind를 통해 새 함수를 생성했기 때문에 기존의 함수는 원래대로 동작한다. this 값과 초기인수가 다름 printThis();
bind() 함수를 통해 함수 실행 및 this 지정해주기
 
실행 결과를 확인해 보면 bind() 메서드를 통해서 newPrintThis() 함수가 생성되며 이 함수가 실행될 때 함수 내 this 값은 setThisValue를 가리키게 된다. 초기 인수로는 “첫 인수”를 지정해 주어 newPrintThis(”첫 인수”, arg1, arg2, ...) 형식으로 함수를 호출할 경우 넘겨 주는 인수들 앞에 초기 인수가 삽입되어 함수가 호출되게 된다. bind() 함수는 새로운 함수를 생성해 주기 때문에 기존의 함수는 원래대로 동작한다.
 
{} bind을 통해 넘겨 받은 인자들: 첫 인자, undefined {} bind을 통해 넘겨 받은 인자들: 첫 인자, 둘째 인자 window bind을 통해 넘겨 받은 인자들: undefined, undefined
bind 예시 코드 출력 결과

7-4. 화살표 함수의 this

화살표 함수는 간편한 사용법과 this 값을 바인딩 하지 않는다는 특징으로 유용하게 사용될 수 있다. 화살표 함수 내에서 this는 어떤 것을 가리키는지 알아보자.
 
function () { console.log("hi"); } // 위의 함수와 동일하게 동작 () => { console.log("hi") };
화살표 함수 사용하기

7-4-1. 화살표 함수에서의 this

함수가 화살표 함수로 호출되었을 때는 this 값을 따로 바인딩 하지 않는다. 화살표 함수 내에서 this가 쓰였을 경우에는 this 변수에 대해서 일반 함수 규칙을 적용하여 자신에게 없는 this를 상위 스코프에 찾아 상위 스코프의 this 참조 값을 자신의 참조 값으로 사용하게 된다.[3] 따라서 일반 함수로 호출되었든 메서드로 호출되었든 호출 방식과는 상관없이 ‘화살표 함수를 감싸고 있는 것이 무엇인가’ 가 this가 참조할 값에 중요한 영향을 미친다.
 
mdn에서는 아래와 같이 말한다.
“ 화살표 함수에서 this는 자신을 감싼 정적 범위입니다. 전역 코드에서는 전역 객체를 가리킵니다. ” 출처: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this#화살표_함수
 
💡
스코프와 상위 스코프[4] 스코프란 식별자가 참조될 수 있는 범위를 뜻한다.[5] Javascrip에선 함수 단위로 스코프를 생성한다. 따라서 화살표 함수의 상위 스코프를 찾으려면 화살표 함수가 선언된 위치에서 화살표 함수를 감싸고 있는 함수를 찾으면 된다. 화살표 함수의 this는 감싸고 있는 함수의 this 값을 그대로 이용한다. 화살표 함수를 함수가 감싸고 있지 않을 경우에는 상위 스코프가 최상위 스코프인 전역 스코프(global scope)로 설정되어 함수 내 this 값이 window를 가리키게 된다.
 
다음 예제를 통해서 메서드로 호출된 함수의 this와 메서드로 호출된 화살표 함수의 this가 어떻게 차이가 발생하는지 알아보자.
 
// var를 통해 변수를 선언하면 전역객체 window의 프로퍼티로 저장된다. var fruitName = "apple"; const obj = { fruitName: "banana" }; function nestedFunc() { function normalFunc() { console.log(this.fruitName); } const arrowFunc = () => { console.log(this.fruitName); }; const nestedArrowFunc = () => { const arrowFunc2 = () => { console.log(this.fruitName); }; arrowFunc2(); }; normalFunc(); arrowFunc(); nestedArrowFunc(); } // call 메서드를 통해서 nestedFunc의 this 값을 obj로 명시해준다. nestedFunc.call(obj);
apple banana banana
상위 스코프를 가리키는 화살표 함수의 this (1)
 
위의 예제를 통해서 nestedFunc()에 의해 내부 함수인 normalFunc()arrowFunc(), nestedArrowFunc()가 각각 실행된다. 각 실행된 this가 가리키는 참조 값은 다음과 같다.
 
호출된 함수 형태
this 참조 값
global
-
window
nestedFunc
일반 함수
obj
normalFunc
일반 함수
window
arrowFunc
화살표 함수
obj
nestedArrowFunc
화살표 함수
obj
arrowFunc2
화살표 함수
obs
 
nestedFunccall 메서드로 인해 this 값으로 obj를 가리키고 일반 함수로 호출된 함수는 window를 가리킨다. arrowFuncnestedArrowFunc는 상위 스코프 즉 자신을 감싸고 있는 함수인 nestedFuncthis 값을 가리키므로 obj를 가리키고 arrowFunc2는 상위 스코프가 nestedArrowFunc 즉 화살표 함수이므로 한 단계 더 상위로 올라가 nestedFuncthis 값인 objthis 값으로 가리키게 된다.
이에 따라 화살표 함수 내 this는 상위 스코프의 this를 사용하며 상위 스코프의 함수가 화살표 함수일 경우 상위 스코프가 화살표 함수가 아닌 함수가 아닐 때까지 찾아 그 함수의 this 값을 사용하는 것을 확인할 수 있다.
 

7-4-2. 화살표 함수 내 this 사용 시 주의사항

아래의 코드를 보면 위의 코드와 같은 코드로 보이나 결과를 보면 모든 fruitNameapple로 출력된 것을 확인할 수 있다.
 
// var를 통해 변수를 선언하면 전역객체 window의 프로퍼티로 저장된다. var fruitName = "apple"; const obj = { fruitName: "banana" }; function nestedFunc() { normalFunc(); arrowFunc(); nestedArrowFunc(); } function normalFunc() { console.log(this.fruitName); } const arrowFunc = () => { console.log(this.fruitName); }; const nestedArrowFunc = () => { const arrowFunc2 = () => { console.log(this.fruitName); }; arrowFunc2(); }; // call 메서드를 통해서 nestedFunc의 this 값을 obj로 명시해준다. nestedFunc.call(obj);
apple apple apple
상위 스코프를 가리키는 화살표 함수의 this (2)
 
자바 스크립트에서는 함수가 실행된 위치(nestedFunc 내부)에서 상위 스코프를 찾는 것이 아니라 함수가 선언된 위치에서 상위 스코프를 찾기 때문에 이를 주의하며 코드를 작성해야 한다.
 

7-4-3. this의 활용

일반적으로 this는 어떻게 함수가 호출되는지에 따라 자신의 this 값을 정의하는데 이는 객체 지향 스타일로 코딩할 때 좋지 못하다. 객체 단위로 this를 사용하고 싶지만 콜백 함수나 일반 함수로 사용되는 경우가 메서드 내에 있다면 이는 의도치 않은 window 객체를 가리킬 수 있다. 이러한 문제는 화살표 함수를 이용하면 간단히 해결이 가능하다.[3]
다음 예제의 메서드들은 인스턴스의 fruitName 프로퍼티에 해당하는 fruit chip을 만들고자 한다. 메서드로는 makeFruitChip1makeFruitChip2가 사용되며 makeFruitChip1은 일반 함수를 인수로 받고 makeFruitChip2는 화살표 함수를 인수로 받는다. 메서드가 실행되면 1초 뒤에 fruit chip을 만들었다는 문구가 출력된다. 예제를 통해 출력 결과를 통해 화살표 함수의 유용성을 확인해 보자.
 
// 전역 객체에 fruitName 프로퍼티 추가한다. var fruitName = "apple"; function Fruit() { this.fruitName = "banana"; // setTimeout의 인수로 "일반 함수"를 사용 // 생성된 인스턴스에서 makeFruitChip1을 사용할 수 있도록 prototype에 함수 등록한다. Fruit.prototype.makeFruitChip1 = function() { console.log(`I will make ${this.fruitName} chips by makeFruitChip1`); // this는 함수를 호출한 인스턴스를 가리킨다. console.log("making fruit chips.."); setTimeout(function(){ // this 값은 전역 객체 window를 가리킨다. console.log(this.fruitName + "chips is Done! by makeFruitChip1"); // apple chip }, 1000); } // setTimeout의 인수로 "화살표 함수"를 사용 // 생성된 인스턴스에서 makeFruitChip2를 사용할 수 있도록 prototype에 함수 등록한다. Fruit.prototype.makeFruitChip2 = function() { console.log(`I will make ${this.fruitName} chips by makeFruitChip2`); // this는 함수를 호출한 인스턴스를 가리킨다. console.log("making fruit chips.."); setTimeout(() => { // this 값은 전역 객체 window를 가리킨다. console.log(this.fruitName + "chips is Done! by makeFruitChip2"); // banana chip }, 1000); } } const fruit = new Fruit(); fruit.makeFruitChip1(); fruit.makeFruitChip2();
 
출력 결과는 다음과 같다.
 
setTimeout이 실행되기 이전 출력setTimeout이 실행되기 이전 출력
setTimeout이 실행되기 이전 출력
 
각 메서드가 실행되었을 때 setTimeout이 실행되기 이전에 메서드들은 this.fruitName의 값으로 인스턴스 fruitfruitName 프로퍼티(banana)를 가리킨다.
 
setTimeout 실행 후 출력setTimeout 실행 후 출력
setTimeout 실행 후 출력
 
하지만 실행 결과를 확인해 보면 makeFruitChip1 메서드를 통해 출력된 결과는 bananachip을 만들려 했으나 applechip을 만들었다는 결과가 출력된다. this값이 fruit객체에서 전역 객체 window로 바뀐 것이다. 본 메서드의 목적은 해당 객체의 프로퍼티를 그대로 이용하고자 했지만 메서드 내에 setTimeout의 인자로 일반 함수가 들어가면서 내부에서 this가 가리키는 것이 fruit 인스턴스에서 window로 변하게 된다.
이렇듯 객체 지향 스타일로 코딩할 경우 this가 일반 함수나 콜백 함수가 내에서 사용될 때 this가 의도하지 않은 값을 가리킬 수 있다. 이 경우 화살표 함수를 이용해서 상위 스코프에 있는 this를 이용하여 이러한 문제를 해결할 수 있다.

7-5. 이벤트 핸들러 함수의 this

이벤트 핸들러 함수로써 함수가 호출되는 경우에 this는 일반적으로 이벤트가 처음 발생한 타깃을 가리키며 이벤트 핸들러를 등록하는 방식으로 어떤 방식을 사용했느냐에 따라 this의 참조 값은 달라질 수 있다. 각 등록 방식에 따라 함수 내의 this 값이 코드 상에서 무엇을 가리키고 있는지, 또 등록 방식마다 어떤 차이가 있는지를 확인해 보고 크롬에서 제공하는 getEventListener API를 이용하여 왜 등록 방식에 따라 이런 차이가 발생하는지 원인에 대하여 알아보자.[6]

7-5-1. addEventListener 방식

addEventListener 방식에서는 이벤트 핸들러 함수 내에 this는 이벤트가 처음 발생한 타깃을 가리킨다. 가장 많이 쓰이는 방식이며 선호되는 방식이다.
 
<button class="banana-btn">🍌</button>
const bananaBtn = document.querySelector(".banana-btn"); function handleBanana() { console.log("바나나입니다. 🍌"); console.log(this); } // 이벤트 핸들러 등록: addEventListener bananaBtn.addEventListener("click", handleBanana);
바나나 버튼 누를 시 출력 : 바나나입니다. 🍌 <button class="banana-btn">🍌</button>

7-5-2. event handler property 방식

addEventListener와 동일하게 이벤트 핸들러 함수 내에서 this는 이벤트가 처음 발생한 타깃을 가리킨다. 하지만 일부 브라우저에서는 이 규칙을 따르지 않을 수 있기 때문에 addEventListener 방식을 사용하길 권장한다.[7]
 
<button class="graph-btn">🍇</button>
const graphBtn = document.querySelector(".graph-btn"); function handleGraph(event) { console.log("포도 버튼을 눌렀습니다. 🍇"); console.log(this); } // 이벤트 핸들러 등록: onclick 프로퍼티 graphBtn.onclick = handleGraph;
포도 버튼 누를 시 출력 : 포도입니다. 🍇 <button class="graph-btn">🍇</button>

7-5-3. event handler attribute 방식

위의 두 방식과는 차이가 존재한다. event handler attribute 방식은 이벤트 핸들러 함수 내에서 this가 등록 방식에 따라 다른 값을 가지게 되는 이유의 가장 큰 원인이며 이벤트 핸들러 함수를 어떻게 선언해 주냐에 따라서 사용되는 this 값이 달라진다. 다음을 살펴보자.
 
<button class="apple-btn" onclick="handleApple()">🍎</button>
// 이벤트 핸들러 함수 function handleApple() { console.log("사과 버튼을 눌렀습니다. 🍎"); console.log(this); }
사과 버튼 누를 시 출력 : 사과입니다. 🍎 Window {window: Window, self: Window, document: document, name: '', location: Location, …}
 
이 경우는 일반적으로 event handler attribute를 사용하는 방식이다. 출력 결과로 thiswindow를 가리키는 것을 확인할 수 있다. this의 값은 위 두 경우와 다르게 이벤트가 발생한 DOM 요소를 가리키고 있지 않다. event handler attribute 방식에서 this가 이벤트가 발생한 DOM 요소를 가리키게 하고 싶다면 아래와 같이 쓸 수 있다. 이와 같은 방법이 가능한 이유는 7-5-4. 차이가 발생하는 이유에서 알아보겠다.
방법 1. 인자로 this 넘겨주기
<button class="apple-btn" onclick="handleApple(this)">🍎</button>
// this 값을 인자로 받아오는 것 function handleApple(thisValue) { console.log("사과 버튼을 눌렀습니다. 🍎"); console.log(thisValue); }
사과 버튼 누를 시 출력 : 사과입니다. 🍎 <button class="apple-btn" onclick="handleApple(this)">🍎</button>
 
방법 2. call() 메서드 사용하기
<button class="apple-btn" onclick="handleApple.call(this)">🍎</button>
// 이벤트 핸들러 함수는 기존의 핸들러 함수와 동일하다. function handleApple() { console.log("사과 버튼을 눌렀습니다. 🍎"); console.log(this); }
사과 버튼 누를 시 출력 : 사과입니다. 🍎 <button class="apple-btn" onclick="handleApple.call(this)">🍎</button>
 

7-5-4. 차이가 발생하는 이유

왜 addEventListener 방식과 event handler property 방식에서는 this가 이벤트가 발생한 DOM 요소를 가리키고 event handler attribute 방식에서는 window를 가리키는 것일까? 이는 각 DOM 요소에서 이벤트 핸들러 함수가 어떤 모습으로 참조되고 있는지를 확인하면 이유를 알 수 있다. 이러한 정보는 크롬에서 제공하는 getEventListener() API를 통해서 확인할 수 있다.
 
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>this 예제</title> </head> <body> <!-- 이벤트 핸들러 등록: HTML--> <button class="apple-btn" onclick="handleApple">🍎</button> <button class="banana-btn">🍌</button> <button class="graph-btn">🍇</button> <script> </script> </body> </html>
const appleBtn = document.querySelector(".apple-btn"); const bananaBtn = document.querySelector(".banana-btn"); const graphBtn = document.querySelector(".graph-btn"); // 이벤트 핸들러 함수 function handleApple() { console.log("사과 버튼을 눌렀습니다. 🍎"); console.log(this); } function handleBanana(event) { console.log("바나나 버튼을 눌렀습니다. 🍌"); console.log(this); } function handleGraph(event) { console.log("포도 버튼을 눌렀습니다. 🍇"); console.log(this); } // 이벤트 핸들러 등록: addEventListener bananaBtn.addEventListener("click", handleBanana); // 이벤트 핸들러 등록: onclick 프로퍼티 graphBtn.onclick = handleGraph;
사과 버튼 누를 시 출력 : 사과입니다. 🍎 window 바나나 버튼 누를 시 출력 : 바나나입니다. 🍌 바나나 버튼 포도 버튼 누를 시 출력 : 포도입니다. 🍇 포토 버튼
 
위의 코드를 실행하고 있는 브라우저에서 getEventListener를 통해 각 버튼의 이벤트 핸들러를 확인해 보면 다음과 같다.
 
addEventListener 확인 결과.addEventListener 확인 결과.
addEventListener 확인 결과.
 
event handler property 확인 결과.event handler property 확인 결과.
event handler property 확인 결과.
 
addEventListener와 event handler protperty 방식은 listener 프로퍼티로 handleBanana()handleGraph()가지며 이 함수들은 이벤트가 발생했을 때 이벤트 핸들러로써 호출되어 내부 this 값이 이벤트가 발생한 DOM 요소를 가리킨다.
 
event handler attribute 확인 결과.event handler attribute 확인 결과.
event handler attribute 확인 결과.
 
하지만 event handler attribute 방식으로 등록하면 그림과 같이 onclick 함수로 이벤트 핸들러로 명시해 준 함수(handleApple())를 감싸주는 형태가 이벤트 핸들러로 등록된다. 따라서 이 경우 이벤트 핸들러 함수로 등록된 onclick 함수 내부의 this 값은 이벤트가 발생한 DOM 요소를 가리키게 되고 handleApple()onclick 함수 내에서 일반 함수로 호출됨으로 handleApple() 내의 this 값이 전역 객체인 window를 가리키게 된다.
이와 같은 이유로 7-5-3. event handler attribute 방식에서 보았던 것처럼 handleApple의 인수로 this를 넘겨주거나 call 메서드로 this 값을 지정해 주는 방법을 이용해야 handleApplethis값이 onclick 함수의 this가 참조하는 값인 이벤트가 발생한 DOM 요소를 가리키게 되는 것이다.
 
💡
이벤트 핸들러 확인하기 크롬 개발자 도구에서는 자바스크립트 이벤트 모니터링을 위해서 몇몇 콘솔 유틸리티 API를 제공하는데 그중에서 getEventListener() API를 사용하면 DOM 요소에 부착된 이벤트 핸들러 함수 목록을 확인할 수 있다. 사용 방법은 크롬의 개발자 도구의 콘솔 창에서 getEventLis tener(확인할 DOM 요소)를 입력해주면 해당 DOM 요소에 부착된 이벤트 핸들러 함수 목록을 확인할 수 있다.
 
notion imagenotion image
 

Reference


  1. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/new
  1. https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object_prototypes
  1. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/Arrow_functions#바인딩_되지_않은_this
  1. https://developer.mozilla.org/ko/docs/Glossary/Scope
  1. https://developer.mozilla.org/ko/docs/Glossary/Identifier
  1. https://developer.chrome.com/docs/devtools/console/utilities/#getEventListeners-function
  1. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this#dom_이벤트_처리기로서