7-1. this란?7-2. 함수 호출 방식7-2-1. 일반 함수7-2-2. 메서드 호출7-2-3. 생성자 함수 호출7-2-3-1. 생성자 함수 내 this의 부재7-2-3-2. 생성자 함수 코드 내 this7-3. call(), apply(), bind()7-3-1. call()7-3-2. apply()7-3-3. bind()7-4. 화살표 함수의 this7-4-1. 화살표 함수에서의 this7-4-2. 화살표 함수 내 this 사용 시 주의사항7-4-3. this의 활용 7-5. 이벤트 핸들러 함수의 this7-5-1. addEventListener 방식7-5-2. event handler property 방식7-5-3. event handler attribute 방식7-5-4. 차이가 발생하는 이유Reference
7-1. this란?
자바스크립트에서는
this
키워드를 사용할 수 있다. this
는 자바스크립트 엔진에 의해 생성되고 코드 어디에서나 참조가 가능하다. this
값은 런타임에 결정되며 this
는 함수를 호출하는 방식에 의해 값이 달라진다. 개발자 도구(F12) 실행하고 콘솔에서
this
를 입력해보자.this; // Window
흔히 사용하는 console.log나 console.dir, console.table은 window 객체에 있다.
브라우저 환경의 전역 객체인
Window
를 가리킨다. 전역 실행 컨텍스트에서 this
는 항상 전역 객체를 참조한다. 여기서 전역 실행 컨텍스트란 자바스크립트 엔진이 코드를 실행할 때 처음으로 생성되는 컨텍스트이다. 전역 객체는 자바스크립트를 실행하는 환경에 따라 다르다. 만약 Node.js 환경에서 실행한다면 global
객체가 전역 객체가 된다.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
자바스크립트 엄격 모드에서
this
는 undefined
이다.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
를 가리킨다고 할 수는 없다. 함수 중에는 map
과 forEach
와 같이 인자로 명시적으로 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();
이 실행되는 동안 this
는 fruit
이라는 객체를 나타낸다. 대부분의 메서드가 객체 프로퍼티의 값을 활용한다. 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]
생성자함수.prototype
을 상속하는 빈 객체가 생성된다. 이때 빈 객체는생성자함수.prototype
을 상속하므로 빈 객체에 생성자 함수의 메서드들이 프로토타입 체인을 통해 연결된다.
- 생성자 함수 코드를 실행한다. 보통 이 단계를 통해서 생성된 객체에 필요한 프로퍼티를 연결시켜준다. (경우에 따라서 메서드도 추가가 가능하다.)
- 함수 실행이 끝나면 프로퍼티와 메서드가 연결된 완성된 객체(인스턴스)를
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);
코드의 출력 결과를 확인해 보면 생성된 인스턴스는 빈 객체로 아무런 프로퍼티를 가지고 있지 않다. 정상적인 생성자 함수의 코드라면
nomalArg1
, nomalArg2
프로퍼티와 normalMethod
를 메서드로 가지는 인스턴스가 생성되어야 했을 것이다. 이러한 문제가 발생하는 이유는 JavaScript에서 인스턴스 생성을 위한 빈 객체({}
)를 만들어주어도 이를 나타내는 변수가 없기 때문에 발생한다. 빈 객체({}
)에 프로퍼티나 메서드를 추가하려면 {}.normalArg1 = arg1
의 형식으로 프로퍼티와 메서드를 추가해야 하는데 {}
가 생성되어도 이를 나타내는 변수가 없기 때문에 추가하고자 하는 프로퍼티와 메서드가 추가되지 않고 빈 인스턴스가 생성되는 것이다.결국 생성자 함수 내부에는 생성된 빈 객체(인스턴스)를 가리킬 무언가가 있어야 우리가 원하는 방식의 동작(프로퍼티와 메서드가 채워진 인스턴스를 반환받는 것)이 가능해진다. 이 무언가의 역할을 Javascript에선
this
가 수행하고 있다. 따라서 생성자 함수로 호출된 함수 내의 this
는 생성자 함수가 생성할 인스턴스를 가리킨다고 할 수 있다. 이러한
this 바인딩
작업(this의 참조 값으로 인스턴스가 할당되는 작업)은 new
연산자를 통해 함수가 실행되어 빈 객체가 생성될 때 Javascript가 생성자 함수 내부의 this
와 생성된 빈 객체를 연결해 준다. 그렇기 때문에 위 예제처럼 this
를 명시적으로 써주지 않아도 실행 결과로 다음과 같이 빈 객체와 NormalFunction
의 this
가 바인딩 되어 new
연산의 결과로 빈 객체 인스턴스를 반환받을 수 있다.Tip: this 확인하기
- 코드를 수행하는 동안
console.dir
명령어로NormalFunction
의 구조 안에서는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);
new
키워드와 함께 함수를 호출하면 생성자 함수로 함수를 호출하여 인스턴스로 쓸 빈 객체를 생성한다. 이때 생성자 함수 내의this
에 빈 객체를 바인딩 한다.
// 전체 코드 function NormalFunction(arg1, arg2) { // 2. this는 아직 프로퍼티와 메서드가 추가되지 않은 빈 객체 {}를 가리킨다. console.log(this); ... } // 1. 빈 객체 {}가 생성되고 {}를 NoramlFunction의 this가 가리키게 된다. const normal = new NormalFunction(10, 20); ...
- 함수 내 코드를 수행하며 빈 객체에 프로퍼티와 메서드를 추가시켜준다.
// 전체 코드 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);
콘솔 창에 출력된 결과는 다음과 같다.
- 그리고 마지막으로 생성자 함수의 코드 수행이 완료되었다면
this
가리키고 있는 채워진 객체를 반환한다.
// 전체 코드 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(){...}
)를 추가했을 경우 인스턴스는 프로토타입 체인 상에 존재하는 메서드를 사용할 수 있게 되어 인스턴스(객체)의 메모리가 아닌 프로토타입 체인 상에 존재하는 한 개의 메서드를 공유해서 사용할 수 있다.참고로 함수는 일반 함수로 호출할 수도 있고 생성자 함수로도 호출할 수 있기 때문에 일반 함수로 호출되는 경우에는
this
가 widnow
를 가리키고 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
은 생성자 함수로 작성했지만 일반 함수로 호출되어 함수 내 this
값이 전역 객체 window
를 가리키게 된다. 그러므로 함수 호출을 통해서 window
에 normalArg1
과 normalArg 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, "첫 인수", "둘째 인수");
실행 결과를 확인해 보면
printThis.call(setThisValue,"첫 인수", "둘째 인수")
의 실행 결과로 printThis
함수가 실행되며 실행 시 함수 내 this
는 call()
메서드의 첫 번째 인수로 넘겨준 setThisValue
를 가리키게 된다. 또한 setThisValue
뒤의 “첫 인수”
, “둘째 인수”
는 printThis
함수가 실행될 때 함수의 인수로 사용되게 된다.{} 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, ["첫 인수", "둘째 인수"]);
실행 결과는
call()
메서드의 경우와 매우 유사하다. 메서드 이름이 apply
라는 점과 함수에 사용될 인수를 배열로 넘겨주는 것만 다를 뿐, printThis()
함수를 지정한 this
값으로 실행시킨다. 실행한 결과는 아래와 같다.{} 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()
메서드를 통해서 newPrintThis()
함수가 생성되며 이 함수가 실행될 때 함수 내 this 값은 setThisValue
를 가리키게 된다. 초기 인수로는 “첫 인수”
를 지정해 주어 newPrintThis(”첫 인수”, arg1, arg2, ...)
형식으로 함수를 호출할 경우 넘겨 주는 인수들 앞에 초기 인수가 삽입되어 함수가 호출되게 된다. bind()
함수는 새로운 함수를 생성해 주기 때문에 기존의 함수는 원래대로 동작한다.{} bind을 통해 넘겨 받은 인자들: 첫 인자, undefined {} bind을 통해 넘겨 받은 인자들: 첫 인자, 둘째 인자 window bind을 통해 넘겨 받은 인자들: undefined, undefined
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
위의 예제를 통해서
nestedFunc()
에 의해 내부 함수인 normalFunc()
와 arrowFunc()
, nestedArrowFunc()
가 각각 실행된다. 각 실행된 this
가 가리키는 참조 값은 다음과 같다.ㅤ | 호출된 함수 형태 | this 참조 값 |
global | - | window |
nestedFunc | 일반 함수 | obj |
normalFunc | 일반 함수 | window |
arrowFunc | 화살표 함수 | obj |
nestedArrowFunc | 화살표 함수 | obj |
arrowFunc2 | 화살표 함수 | obs |
nestedFunc
는 call
메서드로 인해 this
값으로 obj
를 가리키고 일반 함수로 호출된 함수는 window
를 가리킨다. arrowFunc
와 nestedArrowFunc
는 상위 스코프 즉 자신을 감싸고 있는 함수인 nestedFunc
의 this
값을 가리키므로 obj
를 가리키고 arrowFunc2
는 상위 스코프가 nestedArrowFunc
즉 화살표 함수이므로 한 단계 더 상위로 올라가 nestedFunc
의 this
값인 obj
를 this
값으로 가리키게 된다.이에 따라 화살표 함수 내
this
는 상위 스코프의 this
를 사용하며 상위 스코프의 함수가 화살표 함수일 경우 상위 스코프가 화살표 함수가 아닌 함수가 아닐 때까지 찾아 그 함수의 this
값을 사용하는 것을 확인할 수 있다.7-4-2. 화살표 함수 내 this 사용 시 주의사항
아래의 코드를 보면 위의 코드와 같은 코드로 보이나 결과를 보면 모든
fruitName
이 apple
로 출력된 것을 확인할 수 있다. // 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
자바 스크립트에서는 함수가 실행된 위치(
nestedFunc
내부)에서 상위 스코프를 찾는 것이 아니라 함수가 선언된 위치에서 상위 스코프를 찾기 때문에 이를 주의하며 코드를 작성해야 한다. 7-4-3. this의 활용
일반적으로
this
는 어떻게 함수가 호출되는지에 따라 자신의 this
값을 정의하는데 이는 객체 지향 스타일로 코딩할 때 좋지 못하다. 객체 단위로 this
를 사용하고 싶지만 콜백 함수나 일반 함수로 사용되는 경우가 메서드 내에 있다면 이는 의도치 않은 window
객체를 가리킬 수 있다. 이러한 문제는 화살표 함수를 이용하면 간단히 해결이 가능하다.[3]다음 예제의 메서드들은 인스턴스의
fruitName
프로퍼티에 해당하는 fruit chip을 만들고자 한다. 메서드로는 makeFruitChip1
과 makeFruitChip2
가 사용되며 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
이 실행되기 이전에 메서드들은 this.fruitName
의 값으로 인스턴스 fruit
의 fruitName
프로퍼티(banana)를 가리킨다. 하지만 실행 결과를 확인해 보면
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를 사용하는 방식이다. 출력 결과로
this
는 window
를 가리키는 것을 확인할 수 있다. 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와 event handler protperty 방식은
listener
프로퍼티로 handleBanana()
와 handleGraph()
가지며 이 함수들은 이벤트가 발생했을 때 이벤트 핸들러로써 호출되어 내부 this
값이 이벤트가 발생한 DOM 요소를 가리킨다. 하지만 event handler attribute 방식으로 등록하면 그림과 같이
onclick
함수로 이벤트 핸들러로 명시해 준 함수(handleApple()
)를 감싸주는 형태가 이벤트 핸들러로 등록된다. 따라서 이 경우 이벤트 핸들러 함수로 등록된 onclick
함수 내부의 this
값은 이벤트가 발생한 DOM 요소를 가리키게 되고 handleApple()
은 onclick
함수 내에서 일반 함수로 호출됨으로 handleApple()
내의 this
값이 전역 객체인 window
를 가리키게 된다. 이와 같은 이유로 7-5-3. event handler attribute 방식에서 보았던 것처럼
handleApple
의 인수로 this
를 넘겨주거나 call
메서드로 this
값을 지정해 주는 방법을 이용해야 handleApple
의 this
값이 onclick
함수의 this
가 참조하는 값인 이벤트가 발생한 DOM 요소를 가리키게 되는 것이다.이벤트 핸들러 확인하기
크롬 개발자 도구에서는 자바스크립트 이벤트 모니터링을 위해서 몇몇 콘솔 유틸리티 API를 제공하는데 그중에서
getEventListener() API
를 사용하면 DOM 요소에 부착된 이벤트 핸들러 함수 목록을 확인할 수 있다. 사용 방법은 크롬의 개발자 도구의 콘솔 창에서 getEventLis tener(확인할 DOM 요소)
를 입력해주면 해당 DOM 요소에 부착된 이벤트 핸들러 함수 목록을 확인할 수 있다.