[JavaScript] this

YEN·2022년 7월 21일
0

JavaScript

목록 보기
7/7
post-thumbnail

1. 상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다.
-> 실행 컨텍스트는 함수를 호출할 때 생성되므로

즉, this는 함수를 호출할 때 결정된다고 할 수 있다.

1) 전역공간에서의 this

전역공간에서의 this는 전역 객체를 가리킨다.
-> 전역 컨텍스트를 생성하는 주체가 바로 전역객체이기 때문이다.

전역객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가지고 있다.
ex) 브라우저 환경에서의 전역객체: window / Node.js 환경에서의 전역 객체: global

var a = 1;
console.log(a);  //1
console.log(window.a);  //1
console.log(this.a);  //1

전역공간에서 변수 a에 1을 할당하였는데 window.a와 this.a의 결과는 모두 1이 출력된다.
-> 자바스크립트의 모든 변수는 실제로는 특정 객체의 프로퍼티로서 동작하기 때문이다.
(사용자가 var 연산자로 변수를 선언하더라도 실제 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식하는 것이다.)

전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다.

추가적으로 var로 변수를 선언하는 대신 window 프로퍼티에 직접 할당하더라도 결과는 var과 선언한 것과 같은 결과를 출력한다.

var a = 1;
window.b = 2;
console.log(a, window.a, this.a);  //1 1 1
console.log(b, window.b, this.b);  //2 2 2

but, 전역변수 선언과 전역객체의 프로퍼티 할당 사이에 다른 경우가 있는데 'delete' 명령일 경우가 바로 그것이다.

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);  //2 2 2

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

변수에 window. 을 생략한 것이므로 delete 연산자를 사용 가능하다.

변수 a와 b를 보면 전역변수로 선언한 경우인데, delete 연산자를 사용해도 삭제가 되지 않았다.
변수 c와 d는 처음부터 전역객체의 프로퍼티를 할당한 경우로, delete 연산자를 사용하자 삭제가 되었다.
-> 즉, 전역변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 '변경 및 삭제 가능성'을 false로 정의하는 것이다.

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

어떤 함수를 실행하는 일반적인 방법 두 가지는 함수로서 호출하는 경우와 메서드로서 호출하는 경우이다.
이 둘을 구분하는 차이는 독립성에 있다.
-> 함수는 그 자체로 독립적인 기능을 수행하지만, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.

ex) 함수로서의 호출과 메서드로서의 호출

var func = function (x) {
    console.log(this, x);
};
func(1);  //Window { ... } 1

var b = {
    method: func
};
b.method(2);  //{ method: f } 2

-> 원래의 익명함수는 그대로인데 이를 변수에 담아 호출한 경우와 b 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라진다.

함수로서의 호출과 메서드로서의 호출은 함수 앞에 . 이 있는지의 여부로 구분할 수 있다.
예제의 4번째 줄은 앞에 . 이 없으므로 함수로서 호출한 것이고, 9번째 줄은 앞에 . 이 있으므로 메서드로서 호출한 것이 된다.
(대괄호 표기법에 따른 경우도 메서드로서의 호출이다.)

this에는 호출한 주체에 대한 정보가 담긴다.
어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명 앞의 객체이다.
점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 된다.

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

ex) 내부함수에서의 this

var a = {
    outer: function() {
        console.log(this);  //(1)
        var innerFunc = function() {
            console.log(this);  //(2) (3)
        }
        innerFunc();

        var b = {
            innerMethod: innerFunc
        };
        b.innerMethod();
    }
};
a.outer();

① 1번째 줄: 생성한 객체 내부에는 outer 프로퍼티가 있으며 여기에는 익명함수가 연결되고, 이렇게 생성한 객체를 변수 a에 할당한다.

② 15번째 줄: a.outer를 호출한다.

③ 2번째 줄: a.outer 함수의 실행 컨텍스트가 생성되면서 호이스팅하고, 스코프 체인 정보를 수집하고, this를 바인딩한다. 이 함수는 호출할 때 함수명 outer 앞에 . 이 있었으므로 메서드로서 호출한 것이다. 따라서 this에는 마지막 점 앞의 객체인 a가 바인딩된다.

④ 3번째 줄: a의 객체 정보가 출력된다.

⑤ 4번째 줄: 호이스팅된 변수 innerFunc는 outer 스코프 내에서만 접근할 수 있는 지역변수로 이 지역변수에 익명 함수를 할당한다.

⑥ 7번째 줄: innerFunc를 호출한다.

⑦ 4번째 줄: innerFunc 함수를 호출할 때 앞에 . 이 없었으므로 함수로서 호출한 것이므로 this가 지정되지 않았고 자동으로
⑧ 스코프 체인상 최상위 객체인 전역객체 Window가 바인딩된다.

⑨ 5번째 줄: Window 객체 정보가 출력된다.

⑩ 9번째 줄: 호이스팅된 변수 b 역시 outer 스코프 내에서만 접근할 수 있는 변수로 여기에 다시 객체를 할당하는데 그 객체에는 innerMethod라는 프로퍼티가 있다. 이 곳이는 앞서 정의된 변수 innerFunc와 연결된 익명 함수가 연결된다.

⑪ 12번째 줄: b.innerMethod를 호출한다.

⑫ 9번째 줄: b.innerMethod 함수의 실행 컨텍스트가 생성되고 앞에 . 이 있었으므로 메서드로서 호출한 것이다. 따라서 this에는 마지막 . 앞의 객체인 b가 바인딩된다.

⑬ 10번째 줄: b 객체 정보가 출력된다.

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자 this를 바인딩하지 않는 화살표 함수를 새로 도입하였다.
화살표함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어 상위 스코프의 this를 그대로 활용할 수 있다.

ex) this를 바인딩하지 않는 함수 (화살표 함수)

var a = {
    outer: function() {
        console.log(this);  //(1) {outer: f}
        var innerFunc = () => {
            console.log(this);  //(2) {outer: f}
        };
        innerFunc();
    }
};
a.outer();

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

  • 콜백함수: 함수 A의 제어권을 다른 함수 또는 메서드 B에게 넘겨주는 경우 함수 A를 콜백함수라고 한다.
    -> 함수 A는 함수 B의 내부 로직에 따라 실행되며 this 역시 함수 b 내부 로직에서 정한 규칙에 따라 값이 결정된다.

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

ex) 콜백함수 내부에서의 this

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

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

document.body.innerHTML += '<button id = "a"> 클릭 </button>';
document.body.querySelector('#a')
    .addEventListener('click', function(e) {  //(3)
        console.log(this. e);
    });

(1): setTimeout 함수는 300ms 만큼 시간 지연을 한 뒤 콜백함수를 실행하라는 명령으로 0.3초 뒤 전역객체가 출력된다.

(2): forEach 메서드는 배열의 각요소를 앞에서부터 차례로 하나씩 꺼내어 그 값을 콜백함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령으로 전역객체와 배열의 각 요소가 총 5회 출력된다.

(3): addEventListener 메서드는 지정한 HTML 엘리먼트에 'click' 이벤트가 발생할 때마다 그 이벤트 정보를 콜백함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령으로 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력된다.

(1)의 setTimeout 함수와 (2)의 forEach 메서드는 그 내부에서 콜백함수를 호출할 때 대상이 될 this를 지정하지 않는다.
-> 따라서 콜백함수 내부에서의 this는 전역객체를 참조한다.

(3)의 addEventListener 메서드는 콜백함수를 호출할 때 자신의 this를 상속하도록 정의돼 있으므로 메서드명의 . 앞부분이 this가 된다.

5) 생성자 함수 내부에서의 this

프로그래밍적으로 생성자는 구체적인 인스턴스를 만들기 위한 일종의 틀이다.

자바스크립트는 함수에 생성자로서의 역할을 함께 부여하였다.
new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

[ 생성자 함수를 호출하면? ]
① 생성자의 prototype 프로퍼티를 참조하는 proto 라는 프로퍼티가 있는 객체(인스턴스)를 만든다.
② 미리 준비된 공통 속성 및 개성을 해당 객체 this에 부여한다.
③ 구체적인 인스턴스가 만들어진다.


2. 명시적으로 this를 바인딩하는 방법

1) call 메서드

call 메서드: 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다.

2) apply 메서드

apply 메서드는 call 메서드와 기능적으로 완전히 동일하다.

call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하지만,
apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서 차이가 있다.

즉, 여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용한다.
예를 들어, 배열에서 최댓값과 최솟값을 구해야할 경우가 있다.

var numbers - [10, 40, 30, 20, 50];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);  //50,10

4) bind 메서드

bind 메서드는 ES5에서 추가된가능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다.
다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다.
-> bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.

var func = function(a, b, c, d) {
    console.log(a, b, c, d);
};
func(1, 2, 3, 4);  //Window{...} 1 2 3 4

var bindFunc1 = func.bind({x: 1});
bindFunc1(5, 6, 7, 8);  //{x:1} 5 6 7 8

var bindFunc2 = func.bind({x: 1}, 4, 5);
bindFunc2(6, 7);  //{x:1} 4 5 6 7
bindFunc2(8, 9);  //{x:1} 4 5 8 9

bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 동사 bind의 수동태 'bound'라는 접두어가 붙는다는 독특한 성질이 있다.

5) 화살표 함수의 예외사항

ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외되었다.
-> 즉, 이 함수 내부에는 this가 아예 없으며 접근하고자 하면 스코프 체인 상 가장 가까운 this에 접근하게 된다.

ex) 화살표 함수 내부에서의 this

var a = {
    outer: function() {
        console.log(this);
        var innerFunc = () => {
            console.log(this);
        };
        innerFunc();
    }
};
a.outer();

별도의 변수로 this를 우회하거나 call / apply / bind 메서드를 적용할 필요가 없다.

6) 별도의 인자로 this를 받는 경우 (콜백함수 내에서의 this)

콜백함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체 (thisArg)를 인자로 지정할 수 있는 경우가 있다.
이러한 메서드의 thisArg 값을 지정하면 콜백함수 내부에서 this 값을 원하는 대로 변경할 수 있다.
이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진되어 있다.
또한, 같은 이유로 ES6에서 새로 등장한 Set, Map 등의 메서드에서도 일부 존재한다. (대표적으로 forEach 메서드)

ex) 콜백함수와 함께 thisArg를 인자로 받는 메서드

Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
Array.prototype.findIndex(callback[, thisArg])
Array.prototype.flatMap(callback[, thisArg])
Array.prototype.from(arrayLike[callback[, thisArg]])
Set.prototype.forEach(callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])

0개의 댓글