[코어 자바스크립트] 상황에 따라 달라지는 this ①

2021. 6. 23. 15:58개발공부/자바스크립트

자바스크립트에서의 this는 다른 대부분의 언어에서와 달리 어디서든 사용 가능하다.

상황에 따라 달라지는 this

- this는 실행 컨텍스트가 생성될 때 결정 된다. = this는 함수를 호출할 때 결정된다.

- 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라진다.

 

전역 공간에서의 this

- 전역객체를 가르킴

- global (window, global)

브라우저 환경 (Node.js환경에선 window 대신 global)

 

전역공간에서의 this는 전역객체를 의미하므로 window.a === this.a 이지만 그렇다고 그 값이 어떻게 1인걸까?

바로, 자바스크립트의 모든 변수는 특정 객체의 프로퍼티로 동작하기때문이다.

전역변수와 전역객체

  • var연산자를 이용해 변수를 선언하더라도 실제 js엔진은 어떤 특정 객체의 프로퍼티로 인식한다.
  • 특정객체 = 실행객체의 Lexical Environment
  • 실행 컨텍스트는 변수를 수집해서 LE의 프로퍼티로 저장한다.
  • var a = 1; -> a앞에 window.이 생략되었다고 생각하면 됨
  • 따라서, 전역 공간에서 var로 변수를 선언하는 대신 window의 프로퍼티에 직접 할당해도 같은 결과가 나온다.
    단, '삭제'명령일땐 예외이다.(의도치않은 삭제 방지 차원)
var a = 1;
delete window.a; // false
console.log(a, window.a, this.a) // 1 1 1

var b = 2;
delete b; // false
console.log(b, window.b, this.b) // 222

window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // Uncaught ReferenceError: c is not defined

window.d = 4;
delete d; // true
console.log(d, window.d, this.d) // Uncaught ReferenceError: d is not defined
  • 처음부터 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되는 반면 전역변수로 선언한 경우에는 삭제가 되지않는다.

 

메서드로서 호출할 때 그 메서드 내부의 this

함수 vs 메서드

  • 함수와 메서드 모두 미리 정의한 동작을 수행하는 코드 뭉치
  • 둘을 구분하는 유일한 차이 = 독립성
  • 함수는 그 자체로 독립적인 기능 수행
  • 메서드는 자신을 호출한 대상 객체에 관한 동작 수행

1번 예제는 변수에 익명함수를 할당하여 호출한 경우

-> this로 전역객체 window가 출력되었다.

2번 예제는 obj 객체의 프로퍼티에 할당해 호출한 경우

-> this = obj

 

✋🏻 '함수로서 호출'과 '메서드로서 호출'을 어떻게 구분할까?

var obj = {
	method: function (x) { console.log(this, x); }
};
obj.method(1);
obj['method'](2);
  • '함수로서 호출'(=func(1))과 '메서드로서 호출'(=obj.method(2))은 함수 앞에 점(.)의 여부로 구분 가능
  • 점 표기법이든 대괄호 표기법이든, 어떤 함수를 호출할 때 그 함수 이름(프로퍼티명) 앞에 객체가 명시돼 있는 경우에는 메서드로 호출한 것이고, 그렇지 않은 모든 경우에는 함수로 호출 한 것이다.

 

메서드 내부에서의 this

this에는 호출한 주체에 대한 정보가 담기는데, 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 프로퍼티명 앞의 객체이다. 즉, 마지막 점(.) 앞에 명시된 객체가 this에 담길 것이다.

var obj = {
    methodA: function() { console.log(this); },
    inner: {
    	methodB: function() { console.log(this); }
    }
};
obj.methodA(); // {inner: {…}, methodA: ƒ}
obj['methodA'](); // {inner: {…}, methodA: ƒ}

obj.inner.methodB(); // {methodB: ƒ}
obj.inner['methodB'](); // {methodB: ƒ}
obj['inner'].methodB(); // {methodB: ƒ}
obj['inner']['methodB'](); // {methodB: ƒ}

 

함수로서 호출할 때 그 함수 내부에서의 this

  • 함수에서의 this는 전역 객체를 가리킨다. (함수로서 호출할때 this가 지정되지않는데, 컨텍스트를 활성화할 당시 this가 지정되지 않은 경우 this는 전역 객체를 바라본다.)
    → 설계상의 오류라고 지적

메서드의 내부함수에서의 this

  • 내부함수를 함수로서 호출했는지 메서드로서 호출했는지만 파악하면 this의 값을 정확히 알 수 있다.
var obj1 = {
    outer: function () {
        console.log(this); // (1)
        var innerFunc = function () {
            console.log(this); // (2) (3)
        }
        innerFunc();
        
        var obj2 = {
            innerMethod: innerFunc
            // obj2에 할당된 객체의 innerMethod라는 프로퍼티
        };
        obj2.innerMethod();
    }
};
obj1.outer();

(1) 15번째 줄에서 메서드로서 호출되기때문에 outer함수의 this는 obj1를 가리킨다. 콘솔엔 obj1 객체 정보가 출력된다.

(2) 7번째 줄에서 innerFunc()함수가 함수로서 호출되고있다.(함수명앞에 점이 없음) 따라서, this가 지정되어있지 않고 자동으로 스코프 체인상의 최상위 개체인 전역객체(Window)각 바인딩된다. 콘솔엔 Window 객체 정보가 출력된다.

(3) innerMethod()함수는 메서드로서 호출된다. obj2가 바인딩되고 콘솔엔  obj2 객체 정보가 출력된다.

✨ 같은 innerFunc함수라도 2번째엔 함수로서 호출되면서 전역객체를 바인딩하고, 3번째는 메서드로서 호출되면서 obj2를 바인딩한다. 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건이다.

 

 

메서드의 내부 함수에서의 this를 우회하는 방법

호출 주체가 없을땐 자동으로 전역객체를 바인딩하지않고 호출 당시 주변 환경의 this를 그대로 상속받을 수 있다면 훨씬 좋을텐데?

그럴땐 상위 스코프의 this를 저장해 내부함수에서 활용하면 된다.

var obj = {
    outer: function() {
        console.log(this); // {outer: ƒ} // innerFunc1 내부에서 this는 전역객체를 가리킴
        var innerFunc1 = function() {
            console.log(this); // Window { … }
        };
        innerFunc1();
        
        var self = this;
        var innerFunc2 = function() {
            console.log(self); // {outer: ƒ}
        };
        innerFunc2();
    }
};
obj.outer();
  • outer스코프에서 self라는 변수에 this를 저장한 상태에서 호출한 innerFunc2의 경우 self에는 전역객체가아닌 객체 obj가 출력된다.
  • 내부함수에서 선언해준다.(self가 아닌 다른 변수명 가능)
  • 생각보다 단순하다. 그냥 상위 스코프의 this를 저장해서 내부함수에서 활용하는것이다.

 

this를 바인딩하지 않는 함수

  • 화살표 함수
    ES6에선 this가 전역객체를 바인딩하는 문제를 해결하고자, this를 바인딩하지 않는 화살표 함수를 도입하였다.
    화살표 함수는 실행컨텍스트를 생성할 때 this바인딩 과정 자체가 빠지게되어, 상위 스코프의 this를 그대로 활용할 수 있다.
var obj = {
    outer: function() {
        console.log(this); // {outer: ƒ}
        var innerFunc = () => {
            console.log(this); // {outer: ƒ}
        };
        innerFunc();
    }
};
obj.outer();
  • 그 외에도 call, apply등의 메서드를 활용해 호출할 때 명시적으로 this를 지정하는 방법이있다.

콜백 함수 호출 시 그 함수 내부에서의 this

콜백 함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.

*콜백함수: 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다.

// setTimeout 함수
setTimeout(function () { console.log(this); }, 300);

// forEach 메서드
[1,2,3,4,5].forEach(function (x) {
	console.log(this, x);
});

// addEventListener 메서드
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
	.addEventListener('click', function (e) {
    	console.log(this, e);
    });
  • setTimeout함수와 forEach메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않는다. 따라서 콜백 함수 내부에서의 this는 전역객체를 참조한다. 
  • addEventListener메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있다. 즉, 메서드명의 점(.) 앞부분이 this가 되는 것.
  • 콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라본다.

 

생성자 함수 내부에서의 this

  • 생성자 함수 = 어떤 공통된 성질을 지니는 객체들을 생성하는데 사용하는 함수
  • 생성자 = 클래스 = 구체적인 인스턴스를 만들기 위한 틀
  • 클래스를 통해 만든 객체 = 인스턴스
  • new명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작
  • 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신
var Cat = function (name, age) {
	this.bark = '야옹';
    	this.name = name;
    	this.age = age;
};

var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);

>> Cat { bark: '야옹', name: '초코', age: 7 }
>> Cat { bark: '야옹', name: '나비', age: 5 }

출력 결과를 확인하니 생성자 함수 내부에서의 this는 각각의 인스턴스를(choco와 nabi) 가르킴을 알 수 있다.

 

 

++

일반 중첩 함수에서의 this

var data=10;
function outer(){
    // 중첩함수
    function inner(){
        this.data=20;
        data=30;
        
        console.log("1. data1 = "+data); // 1. data1 = 30
        console.log("2. this.data = "+this.data); // 2. this.data = 30
        console.log("3. window.data = "+window.data); // 3. window.data = 30
    }
    inner();
}
outer();

중첩함수에서의 this는 window객체를 가르키므로 data, this.data, window.data 모두 전역변수 data로 동일한 값이다.

 

이벤트 리스너에서의  this

var data=10;
$(document).ready(function(){
    // 이벤트 리스너 등록
    $("#myButton").click(function(){
        this.data=20;
        data=30;
        
        console.log("1. data1 = "+data); //1
        console.log("2. this.data ="+this.data); //2
        console.log("3. window.data ="+window.data); //3
    });
});

1. data = data에 값을 할당하고있는 부분을 보면 중첩함수인것을 알수있다. 따라서 전역변수 data와 동일하다. 그 값에 30을 넣어주는 것이다. = 30

2. this.data = 이벤트리스너에서 this는 이벤트를 발생시킨 객체가 되기때문에 여기서 this는 #myButton. this.data는 #myButton 객체에 data라는 프로퍼티를 동적으로 추가하는 구문이다. = 20

3. window.data = 전역변수 data = 30

 

메서드 내부의 중첩 함수에서의 this

var data = 10;
function MyClass() {
    this.data=0;
}
MyClass.prototype.method1=function(){
    function inner(){
        this.data=20;
        data=30;
        
        console.log("1. data1 = " +data);
        console.log("2. this.data = " +this.data);
        console.log("3. window.data = "+window.data);
    }
    inner();
}
// 인스턴스 생성
var my1 = new MyClass();
// 메서드 호출
my1.method1();

메서드 내부의 중첩함수에서 this는 window 객체이다.

this는 전역변수를 가르키므로 this.data, data, window.data 모두 전역변수 data로 동일하다.

 

일반 함수에서 this: window 객체
중첩 함수에서 this: window 객체
이벤트에서 this: 이벤트를 발생시킨 객체
메서드에서 this: 메서드를 호출한 객체
메서드 내부의 중첩 함수에서 this: window 객체

 

 

 

출처: 정재남, 코어 자바스크립트, 위키북스(2019), 3장 this