-
자바스크립트의 this 👉Web/자바스크립트 2021. 5. 3. 03:34반응형
예전에 자바스크립트의 this에 관해 기초적인 내용을 포스팅한 적이 있는데,
생각보다 this를 아직 잘 모르고 있다는 느낌이 들때가 종종 있었다.
오늘 하브루타 스터디의 주제가 'this'였는데, 진행하는 동안 배운 게 참 많기도 하고
마침 this에 관해 공부를 좀 해봐야겠다는 필요성을 느꼈던 터라,
관련된 내용을 꼭 기억해두고 싶어 이렇게 포스팅을 하게 됐다.
각 문맥에서 this를 출력해 본 결과를 바탕으로
중간중간 나와 스터디 크루들의 뇌피셜을 섞어 정리했으므로 주의할 것.
■ this는 왜 필요한가?
객체는 프로퍼티(객체의 상태)와 메서드(객체 자신의 상태를 조작)로 이루어진다.
메서드를 통해 자신의 상태, 즉 프로퍼티를 조작하기 위해서는 외부의 상태들과 구별되는,
자기 자신의 상태임을 표시해주는 일종의 '식별자'가 필요하다. 그것이 바로 this.
즉, this는 자신이 속한 객체를 가리키는 자기 참조 변수이다.
자바스크립트에서는 이 'this'를, '호출 시점에 자신을 (property로)소유하고 있는 객체'로 정의한 듯 하다.
내부 프로퍼티를 변경하거나 메소드를 사용하지 않는다면 이 this를 사용할 이유가 없으며
그렇기에 일반 함수가 아닌, 객체의 메소드나 생성자 함수에서만 this가 그 의미를 갖게 된다.
그래서 일반 함수 호출의 경우를 보면, 전부 this가 window객체를 가리킨다는 것을 알 수 있다.
(나중에 말하겠지만, strict mode에서는 일반 함수 호출 내부의 this가 window가 아니고 undefined가 된다.
일반 함수에서는 this를 사용할 필요가 없기 때문)
그렇다면 일반 함수 호출에서는 this가 왜 하필 window 객체를 가리키는 것일까?
이를 이해하기 위해서는 '실행 컨텍스트'에 대해 조금 알아볼 필요가 있다.
■ 실행 컨텍스트
함수의 실행 컨텍스트에 관해 아주 간단히 알아보자.
실행 컨텍스트는 간단히 말하면 '실행 가능한 코드가 실행되기 위해 필요한 환경'이라고 말할 수 있다.
여기서 말하는 '실행 가능한 코드'는 전역 코드와 함수 코드(+ eval 코드)가 있다.
이러한 코드들을 실행하기 위해, 자바스크립트 엔진은
'변수, 함수 선언, 변수의 유효 범위(scope), this' 등의 정보를 알고 있어야 한다.
그리고 이러한 실행에 필요한 정보들과 코드들의 덩어리,
즉 실행 컨텍스트를 관리하기 위해 이들을 물리적 객체의 형태로 보관한다.
실행 컨텍스트의 두 가지 중요한 특징은 '호이스팅이 일어나고, 함수가 실행될 때 열린다' 라는 것이다.
이 두 가지 특징 중 '함수가 실행될 때 실행 컨텍스트가 열린다' 라는 사실 때문에,
자바스크립트 함수의 this가 함수 실행 시 동적으로 바인딩되는 것이다.
실행 컨텍스트는 전역 컨텍스트부터 시작해서 콜스택에 쌓이고,
전역 컨텍스트를 쭉 훑으며 특정 함수의 호출부를 만나게 될 때마다
해당 함수의 함수 컨텍스트가 콜스택에 이어서 쌓이게 된다.
(함수가 return하면 해당 함수의 컨텍스트도 콜스택에서 사라진다)
콜스택이건 뭐건, 그 의미와 역할까지 상세히 알 필요는 없고
여기서 중요한 건 애플리케이션이 로딩될 때 전역 컨텍스트부터 시작된다는 것이다.
그 사실때문에 전역 컨텍스트에 위치하는 함수들의 this가 전부 전역 객체에 바인딩된다.
즉, 전역 컨텍스트가 생성되면서 this를 전부 window에 바인딩시키고 시작한다는 것.
결론적으로, this는 기본적으로 window를 가리키도록 설계가 되어있다는 뜻이다.
기본적으로 this에는 전역 객체가 바인딩되고, 그 이후에는 호출 방식에 따라 동적으로 this가 결정된다고 볼 수 있다.
■ this의 바인딩
이제 원론적인 얘기는 이쯤 하고, this의 바인딩에 관한 얘기를 해보도록 하자.
자바스크립트 함수의 this는 호출되는 시점에, 해당 함수를 호출하는 방식에 따라 동적으로 바인딩된다.
함수 호출 방식은 크게 4가지로 구분된다.
1. 일반적인 함수 호출(기본 바인딩)
일반적인 함수 호출의 경우, 기본적으로는 this가 전역 객체(window)에 바인딩된다고 보면 된다.
여기서 '일반적인 함수 호출'은, 다음과 같은 함수 호출을 의미한다.
func1();
즉, 특정한 호출 주체가 없는 일반적인 형태의 함수 호출에서 this는 window를 가리킨다.
물론 화살표 함수냐 아니냐에 따라 바인딩이 달라지고, strict mode이냐 아니냐에 따라서도 달라지지만
일단은 기본적으로 일반 함수 호출의 default this는 전역 객체라고 생각해도 무방하다.
전역 함수는 물론이고, 일반 함수와 메소드의 내부함수, 콜백함수 어디서든 this는 전역 객체를 바인딩한다.
참고로, 기본적으로 모두 화살표 함수가 아니라는 전제.
2. 메소드 호출(암시적 바인딩)
특정 함수가 객체의 프로퍼티 값이면, 해당 함수는 메소드로써 호출된다.
이 때 메소드 내부의 this는 해당 메소드를 소유한 객체, 즉 해당 메소드를 호출한 객체에 바인딩된다.
const test = { a: function() { console.log(this); } } test.a(); // test // test의 메소드로써 a를 호출했기 때문에.
const test = { a: function() { console.log(this); } } const method = test.a; method(); // window // 일반 함수 형태로 호출했기 때문에.
프로토타입 객체 또한 메소드를 가질 수 있는데, 프로토타입 객체 메소드 내부에서 사용된 this도
일반 메소드와 마찬가지로 해당 메소드를 호출한 객체에 바인딩된다.
function Test(param) { this.param = param; } Test.prototype.a = function() { console.log(this.param); } const test = new Test('hi'); test.a(); // 'hi'
3. 생성자 함수 호출(new 바인딩)
자바스크립트의 생성자 함수는 말 그대로 객체를 생성하는 역할을 하는 함수이다.
하지만 그 형태가 정해져있진 않고, 그냥 함수 호출 앞에다가 'new' 키워드를 붙이면 생성자 함수로 동작한다.
(따라서, 혼동하지 않기 위해 일반적으로 생성자 함수의 첫 문자는 대문자로 적는다)
new 연산자와 함께 생성자 함수를 호출하면 다음과 같은 순서로 동작한다.
a) 빈 객체 생성 및 this 바인딩
b) this를 통한 프로퍼티 생성
c) 생성된 객체 반환
단계 a)에서 생성자 함수가 빈 객체를 생성한 후, 생성자 함수 내부의 this가 해당 객체를 가리키도록 한다.
(그리고, 생성된 빈 객체는 생성자 함수의 prototype property가 가리키는 객체를 자신의 prototype 객체로 설정한다)
즉, 생성자 함수 내부의 this는 자신이 생성할 객체를 가리킨다.
※ 참고로, 생성자 함수에 반환문이 없는 경우 this에 바인딩된 '새로 생성한 객체'가 반환된다.
물론 명시적으로 this를 반환해도 결과는 동일하다.
반면, 반환문이 this가 아닌 다른 객체를 명시적으로 반환하는 경우, this가 아닌 해당 객체가 반환된다.
(하지만 this를 반환하지 않은 해당 함수는 생성자 함수로써의 역할을 수행하지 못하기 때문에
생성자 함수는 반환문을 명시적으로 사용하지 않는다)
4. apply/call/bind 호출(명시적 바인딩)
'명시적 바인딩'이라고도 불리는 방식인데, 말 그대로 함수를 호출할 때 명시적으로 this를 바인딩해주는 방식.
apply/call/bind 세 함수 모두 첫 번째 인자로 바인딩하고자 하는 this를 집어넣어줄 수 있다.
(apply, call은 this를 바인딩해서 함수를 호출하고, bind는 this를 바인딩한 함수를 return한다)
이 경우는 말 그대로, 첫 번째 인자로 넣어준 객체가 바로 함수 내부의 this가 된다.
function hello() { console.log("hello, " + this.name); } var obj = { name: "peter", }; setTimeout(hello.bind(obj), 1000); // 1초 뒤에 "hello, peter" name = "grooming";
■ 바인딩의 우선순위
1. new로 생성자 함수를 호출했다면, 해당 함수 호출의 실행 결과로 반환되는 객체가 바로 this이다.
2. call, apply, bind를 사용해 함수를 호출했다면, 함수 호출 시 인자로 넘겨준 객체가 바로 this이다.
3. 객체의 프로퍼티로 접근하여 함수를 호출했다면(메소드 호출), 해당 객체가 바로 this이다.
4. 그 이외의 경우에는 this는 전역 객체에 바인딩된다.
(기본 모드에서는 window, 엄격 모드에서는 undefined)
결론적으로, 바인딩은 다음과 같은 우선순위를 가진다.
new 바인딩 > 명시적 바인딩 > 암시적 바인딩 > 기본 바인딩
■ 화살표 함수의 this 바인딩
우선, 화살표 함수의 this는 선언부에서 정적으로 바인딩된다는 사실을 알고 시작하자.
(어떻게 호출되느냐에 따라 동적으로 바인딩되는 것이 아니고)
화살표 함수는 고유한 this를 가지지 않는다. 즉, 화살표 함수 자체에는 this가 없다.
다만, 화살표 함수 내부에서 this를 참조하면 화살표 함수가 아닌 '평범한' 외부 함수에서 this값을 가져온다.
즉, '실행 컨텍스트를 가진 함수'가 나올 때까지 부모로 쭉쭉 타고 올라간다는 뜻.
그러다가 함수를 만나면 해당 함수의 this를 가져오게 되고,
함수를 만나지 못하면 전역 컨텍스트까지 올라가서 전역 객체인 window를 this로 가져오게 된다.
참고로 화살표 함수의 부모에 화살표 함수가 존재해도, 화살표 함수는 this를 가지지 않기 때문에
this를 가진, 화살표 함수가 아닌 함수를 찾아서 계속 위로 쭉쭉 올라간다.
여기서 주의할 점은, 객체는 무시하고 함수만 찾아 올라간다는 점.
객체는 this를 가지지 않기 때문인데, 이는 객체가 실행 컨텍스트를 가지지 않기 때문이다.
this는 실행 컨텍스트에 존재하고, 실행 컨텍스트를 가지는 건 함수이기 때문에
오직 함수만이 고유한 this를 가질 수 있다.
객체가 this를 가진다고 생각했었지만, 객체가 this를 가지는 게 아니라 오직 함수만이 this를 가지는 것 같다.
아마, 특정 함수의 this가 그 함수를 둘러싼 객체를 가리키기 때문에 마치 객체가 자신만의 this를 가지고 있고,
그 this를 내부에 있는 함수가 참조하는 것처럼 보였던 게 아닐까 싶다.
즉, 객체가 별도의 this를 가지는 게 아니라, 객체 내부의 함수가 this를 가지고 있는 것이고
그냥 단순히 그 this가 둘러싼 객체를 가리키고 있었다. 라고 보는 게 맞는 듯.
■ 'use strict'에서의 this 바인딩
ES5에 와서 자바스크립트에 새로운 기능들이 추가되고,
기존의 기능들에 변화가 생기면서 하위 호환 문제가 발생했다.
그래서 변경사항 대부분은 ES5의 기본 모드에서는 활성화되지 않고,
엄격 모드(strict mode)를 활성화 했을 때만 ES5의 변경사항들이 온전히 활성화되게 설계를 했다(고 한다).
그리고 이 엄격 모드를 활성화하는 지시자가 바로 'use strict'이다.
'use strict' 지시자는 대개 스크립트 최상단에 위치해야하며,
그렇게 되면 스크립트 전체가 '모던한 방식', 즉 최신 기능들을 지원하는 방식으로 동작하게 된다.
참고로 'use strict'는 함수 본문 맨 앞에 위치할 수도 있는데, 그렇게 되면 해당 함수만 엄격 모드로 실행된다.
많은 경우에 strict mode로 코드를 실행하는 것이 더 편하고 효율적이지만,
class와 module(import, export)을 사용하면 자동으로 strict mode가 적용되므로 너무 신경쓸 필요는 없을 듯.
이 strict 모드에서는 일반 함수 호출에서의 this가 window가 아니고 undefined로 처리된다.
그 이유는, 생성자 함수나 메소드가 아닌 일반 함수 호출의 내부에서는 this가 사용될 필요가 전혀 없기 때문.
function func() { console.log(this); } func(); // window
'use strict'; function func() { console.log(this); } func(); // undefined
■ 퍼블릭 클래스 필드 문법
class LoggingButton extends React.Component { handleClick = () => { console.log('this is:', this); } render() { return ( <button onClick={this.handleClick}> Click me </button> ); } }
'클래스 필드'는, 클래스 기반 객체지향 언어에서 클래스가 생성하는 '인스턴스의 프로퍼티'를 의미하는 용어이다.
원래 자바스크립트에서는 객체 내부에서 화살표 함수를 사용하면 해당 함수 내부의 this는 window를 가리킨다.
하지만 클래스에서는 이 '퍼블릭 클래스 필드' 문법에 의해, 내부에 화살표 함수를 선언해줬을 때
해당 함수 내부의 this가 해당 class의 인스턴스를 가리키게끔 만들어준다.
아직까지는 stage3에 위치한 실험적인 문법이며, 주로 리액트에서 애용되는 편이다.
컴포넌트에서 퍼블릭 클래스 필드로 선언해주면, 자동으로 해당 class의 인스턴스가 바인딩되기 때문에
별도로 바인딩을 하느라 render 함수가 실행될 때마다 매번 함수를 생성해야하는 비효율을 없앨 수 있기 때문.
(render 함수 내부에서 .bind(this)를 통해 바인딩을 하거나, 콜백함수 형태로 함수를 새로 만들어서 넣어주면
매번 render 함수가 실행될 때마다 해당 함수들이 계속해서 새로 생성된다)
간단히 말하자면, 이 퍼블릭 클래스 필드 문법을 활용하면
해당 함수의 this가 생성자의 prototype에 엮이는 것이 아니라, 인스턴스의 프로퍼티로 들어가게 된다.
마치 생성자 함수 내부에 해당 함수를 선언해주는 것과 유사한 효과.
(생성자 내부의 this는 해당 생성자에 의해 생성될 객체, 즉 인스턴스를 가리킨다)
class LoggingButton extends React.Component { constructor() { this.handleClick = () => { console.log('this is:', this); } } ... }
이상, this에 관해 많은 가르침을 주신 크루, 일타강사 '콜린'에게 감사의 메시지를 전하면서 글을 마쳐본다.
참고
반응형'Web > 자바스크립트' 카테고리의 다른 글
default export를 피해야 하는 이유? (2) 2021.12.04 WebSocket 채팅 클라이언트 구현기 (4) 2021.08.20 명령형 vs 선언형 프로그래밍 (22) 2021.04.08 객체의 불변성(Immutability) (0) 2021.03.21 async/await을 이용한 비동기 자바스크립트 (0) 2021.02.21