Javascript는 일급 함수, 프로토타입 기반 언어입니다. 이 때문에 this
키워드는 함수 실행 상황에 따라 바뀌게 됩니다. 전역 문맥과 함수 문맥으로 크게 나뉘고, 함수, 객체 메서드, 화살표 함수, 엄격모드 모두 다르게 작동 되므로, 이 모든 내용은 알아 두셔야합니다.
전역 문맥의 this
는 실행환경 별로 다르게 작동합니다. 브라우저에서는 전역 객체인 window
를, Node.js와 Bun은 빈 객체 {}
를, Deno는 undefined
로 할당되어 있습니다.
각기 다른 글로벌 객체를 다루기 위해 ES2020에서는 globalThis
라는 키워드가 추가되어있습니다. 런타임의 글로벌 객체는 Deno
, global
등 다양하기 때문에 때문에 브라우저에서 전역 문맥의 this
가 globalThis
와 같다 할 수 있지만, 런타임 계열(Node.js, Bun)에서는 그렇지 않다는걸 유의하셔야 합니다.
// Browser console.assert(window === this); console.assert(globalThis === this); // ES2020 // Node.js / Bun _.isEqual(this, {}); // true // Deno console.assert(this === undefined);
전역 문맥의 this
기본 함수의 this
는 전역 문맥과 동일하게 동작합니다.
function fn() { return this; } console.assert(fn() === globalThis); // Node.js / Bun _.isEqual(fn(), {});
일반 함수 예제
문맥을 다른 문맥으로 넘기려면 Function.prototype.call
, Function.prototype.apply
메서드를 사용하시면 됩니다. 매개변수 전달 방식의 차이만 있을 뿐 두개의 사용법과 기능은 거의 동일합니다.
function fn(arg1, arg2) { return this.num + arg1 + arg2; } const obj1 = { num: 10 }; console.assert(fn.call(obj1, 20, 30) === 60); console.assert(fn.apply(obj1, [1, 2]) === 13);
call
, apply
예제
두 메서드는 실행할 때만 문맥을 바꿔 동작을 합니다. 하지만 함수를 일급 함수를 사용하다 보면, 함수의 문맥을 고정할 필요가 있습니다. 이를 위해 Function.prototype.bind
메서드가 ES5에 도입되었습니다.
function fn() { return this; } const bindedFn = fn.bind("Some"); console.assert(fn() === globalThis); console.assert(bindedFn() === "Some");
bind
예제
기본 상태의 JS는 대부분의 에러를 알려주지 않은채로 넘어가게 되면서 프로그래머가 이상한 버그들을 만들어 낼 수 있습니다. 이를 느슨한 모드(Sloppy mode)라 부르고, 이를 해제하기 위해 엄격한 모드(Strict mode)가 만들어졌습니다. 이 엄격한 모드는 파일 최상단 혹은 적용할 함수의 시작점에 "use strict";
를 작성하면 되고, 런타임 계열 환경이나 export default
구문을 사용할 수 있는 모듈 방식의 파일에선 기본적으로 엄격모드로 작동됩니다.
'use strict'; // 파일 전체에 대해 엄격 모드 사용 function fn() { 'use strict'; // `fn`함수에 대해 엄격 모드 사용 return; }
엄격 모드 사용법
일반 함수의 this
처럼 사용하게 되면, this
를 할당하는 비용문제가 발생할 뿐만 아니라, 전역 객체를 쉽게 노출하는 것이 보안상 위험한 방식입니다. 이러한 문제 때문에 엄격 모드에서는 기본적으로 아무것도 할당하지 않아 undefined
가 됩니다.
'use strict'; function fn() { return this; } console.assert(fn() === undefined); console.assert(fn.call('Some') === 'Some'); const obj = {}; console.assert(fn.bind(obj)() === obj);
엄격 모드의 함수에서 this
함수를 객체의 메서드로 사용하게 되면 this
는 해당 객체가 됩니다.
const obj = { num: 10, getNumber() { return this.num; } }; console.assert(obj.getNumber() === 10);
객체의 메서드 예제
또한 프로토타입 체인의 메서드 또한 만들어진 객체로 동작됩니다.
// ...위 예제 const obj2 = Object.create(obj); obj2.num = 20; console.assert(obj2.getNumber() === 20);
프로토타입 체인의 메서드
ES6에 추가된 화살표 함수는 함수가 만들어질때 문맥을 고정하게 되며 이후 바꾸는 것이 불가능합니다.
const obj = { name: 'Foo', getThis: (function () { return () => this; })() }; console.assert(obj.getThis() === obj); const obj2 = { getThis: obj.getThis }; console.assert(obj2.getThis() === obj);
화살표 함수 예제
이러한 특성 때문에 call
, apply
, bind
를 통한 문맥 변경은 무시한채 동작하게 됩니다.
const fn = () => this; console.assert(fn.call('Some') === window); console.assert(fn.apply(123) === window); const bindedFn = fn.bind('Bind'); console.assert(bindedFn() === window);
화살표 함수 문맥 변경 시도 예제
클래스의 메서드에서 사용하는 this
는 클래스의 객체를 할당 받게됩니다. 다른 객체지향 언어에서 사용하던 방식과 동일한 방식입니다.
class MyClass { getThis() { return this; } } const instance = new MyClass(); console.assert(instance.getThis() === instance);
클래스 예제
정적 메서드 안에서는 클래스 자체가 할당됩니다. 참고로 객체의 constructor
속성에는 원본 클래스가 할당되어있어, 객체를 통해 정적 메서드를 호출 할 수 있습니다.
class MyClass { static getThis() { return this; } constructor() { console.assert(MyClass.getThis() === this.constructor.getThis()); } } console.assert(MyClass.getThis() === MyClass); // Call constructor - no-error new MyClass()
클래스 정적 메서드 예제
브라우저 API 중 하나인 addEventListener
메서드에 할당되는 메서드는 콜백 함수로서 다른 문맥을 지니기 때문에 this
를 이벤트를 발생한 요소로 지정합니다. 물론 화살표 함수로 할당하면 this
는 만들어질 때의 문맥 기준의 this
입니다.
const element = document.createElement('button'); element.addEventListener('click', function (e) { console.assert(this === element); console.assert(this === e.currentTarget); }); element.addEventListener('click', () => { console.assert(this === globalThis); });
이벤트 리스너 예제
간혹 React 클래스 컴포넌트 예제를 보면 생성자에서 메서드를 바인딩하거나 화살표 함수로 작성하는 모습을 보실 수 있습니다. 이는 React의 JSX를 이용해서 클래스 컴포넌트를 렌더링할 때 나타나는 문제인데, 클래스 컴포넌트의 생성자 및 라이프 사이클 메서드를 제외한 메서드가 this
를 undefined
로 나타나는 문제가 존재합니다. 바인딩 코드를 제외하고 일반 new
키워드를 통해 생성해보시면 메서드의 this
가 정상적으로 나타나는 것을 확인 하실 수 있습니다.
class ButtonComponent extends React.Component { constructor(props) { super(props); // Binding this.onClick = this.onClick.bind(this); } // Binding을 하지 않을 경우 undefined일 수 있다. onClick() { console.log(this); } render() { // 라이프 사이클 메서드는 `this`가 존재한다. console.assert(this !== undefined); return <button onClick={this.onClick}>Button</button> } }
리액트 클래스 컴포넌트 예제
this
키워드의 상황별 결과값에 대해 알아봤습니다. 위 특성들은 모두 실제 프로덕트에서 종종 사용되기도 하는 내용 들이고, npm에 등록된 패키지들에서 이러한 내용을 활용한 코드나 API를 확인하실 수 있습니다. JS 개발자라면 이 특성들을 숙지하시는 것이 보다 효율적이고 유지보수가 용이한 코드를 작성하실 수 있습니다.