코어자바스크립트 스터디

3장 this

려낭 2024. 7. 8. 23:23
window d = 4;​
window.d = 4;​
1. 상황에 따라 달라지는 this

 

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다.

>> this는 함수를 호출할 때 결정된다.

 

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

 

3-1-1 전역 공간에서의 this

 

전역 공간에서 this는 전역객체를 가리킨다.

전역 컨텍스트를 생성하는 주체가 전역 객체이기 때문이다.

전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가지고 있습니다.

브라우저 환경에서 전역객체는 window이고 Node.js환경 에서는 global 이다.

 

 

 

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

사용자가 var 연산자를 이용해 변수를 선언하더라도 실제 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식하는 것이다.

특정 객체란? 바로 실행 컨텍스트의 LexicalEnvironment이다.

실행 컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장한다. 이후 어떤 변수를 호출하면 L.E를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환한다.

 

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

전역공간에서는 var로 변수를 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 결과적으로 var로 선언한 것과 똑같이 동작할 것이다.

 

전역변수 선언과 전역객체의 프로퍼티 할당 사이에 전혀 다른 경우도 있다.

바로 '삭제'명령에 대해 그렇다.

 

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

 

 

변수에 delete 연산자를 쓰는 것이 이상해보일 수도 있다, 

(window.)을 생략한 것으로 이해하면 된다. 전역변수가 곧 전역객체의 프로퍼티이기 때문이다.

 

예제를 보았을 때 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되는 반면 전역변수로 선언한 경우에는 삭제가 되지 않는 것을 확인할 수 있다.

이는 사용자가 의도치 않게 삭제하는 것을 방지하는 차원에서 마련한 방어 전략이라고 해석된다.

전역변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 flalse로 정의하는 것이다.

 

3-1-2 메서드로서 호출할 때 그 메서드 내부에서의 this

 

함수 vs 메서드

 

어떤 함수를 실행하는 방법은 여러가지가 있다. 

가장 일반적인 방법 두가지는 함수로서 호출하는 경우와 메서드로서 호출하는 경우이다. 

프로프래밍 언어에서 함수와 메서드를 구분하는 유일한 차이는 독립성에 있다.

함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.

 

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

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

 

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

함수 앞에 점(.)이 있는지 여부만으로 간단하게 구분할 수 있다.

4번째 줄은 앞에 점이 없으니 함수로서 호출한 것이고,

9번재 줄은 method 앞에 점이 있으니 메서드로서 호출한 것이다.

 

점 표기법이든 대괄호 표기법이든 어떤 함수를 호출할 때 그 함수 이름 앞에 객체가 명시돼 있는 경우에는 메서드로 호출한 것이고, 그렇지 않은 모든 경우에는 함수로 호출한 것이다.

 

 

 

메서드 내부에서의 this

 

this는 호출한 주체에 대한 정보가 담긴다.

어떤 함수를 메서드로서 호출하는 경우 호출 주체는 함수명 앞의 객체이다.

점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 된다.

 

 

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

 

함수 내부에서의 this

 

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않는다.

함수로서 호출하는 것은 호출 주체(객체지향 언어에서의 객체)를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에

호출 주체의 정보를 알 수 없다.

함수에서의 this는 전역 객체를 가리킨다.

 

메서드 내부함수에서의 this

 

우리는 이미 어떤 함수를 메서드로서 호출할 때와 함수로서 호출할 때 this가 무엇을 가리키는지를 알고 있다. 

내부함수 역시 이를 함수로서 호출했는지 메서드로서 호출했는지만 파악하면 this의 값을 정확히 맞출 수 있다. 

 

this를 바인딩하지 않는 함수

 

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수를 새로 도입했다.

 

화살표 함수는 실행 컨텍스트를 생성할 때, this 바인딩 과정 자체가 빠지게 되어 상위 스코프의 this를 그대로 활용할 수 있다.

 

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

 

 

3-1-4 콜백 함수 호출 시 그 함수 내부에서의 this

 

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

이때 함수 A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정된다.

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

 

 

3-1-5 생성자 함수 내부에서의 this

 

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다.

객체지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스 라고 한다.

 

생성자는 구체적인 인스턴스를 만들기 위한 일종의 틀이다. 이 틀에는 해당 클래스의 공통 속성들이 미리 준비돼 있고,

여기에 구체적인 인스턴스의 개성을 더해 개별 인스턴스를 만들 수 있다.

 

자바스크립트에는 함수에 생성자로서의 역할을 함께 부여했다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다.

어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

 

생성자 함수를 호출하면 우선 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체를 만들고 ,

미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여한다. 

 

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

 

3-2-1 call 메서드

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

 

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

이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다.

 

 

var func = function (a,b,c) {
	console.log(this, a,b,c);
};

func(1,2,3);        //window{ ... } 1 2 3
func.call({ x:1 }, 4, 5, 6);  //{ x:1} 4 5 6

 

객체의 메서드를 그냥 호출하면 this는 객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.

 

 

3-2-2 apply 메서드

 

 

Function.prototype.apply(thisArg[, argsArray])

 

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

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

 

var func = function(a,b,c) {
	console.log(this, a, b, c);
};
func.apply({x:1}, [4,5,6]);    // { x:1 } 4 5 6

var obj = {
	a:1,
    method: function (x,y) {
    	console.log(this.a, x, y);
      }
   };
   obj.method.apply({ a:4 },[5,6]);  //4 5 6

 

 

3-2-3 call / apply 메서드의 활용

 

유사배열객체에 배열 메서드를 적용

 

var obj = {
	0: 'a',
    1: 'b',
    2: 'c',
    length: 3
 };
 Array.prototype.push.call(obj, 'd');
 console.log(obj);        // { 0: 'a', 1:'b', 2: 'c', 3: 'd', length:4 }
 
 var arr = Array.prototype.slice.call(obj);
 console.log(arr);      // [ 'a', 'b', 'c', 'd' ]

 

객체에는 배열 메서드를 직접 적용할 수 없다. 

그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체,

즉 배열의 구조와 유사한 객체의 경우(유사배열객체) call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.

 

생성자 내부에서 다른 생성자를 호출

 

생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call또는 apply를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있다.

 

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 -apply 활용

 

여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용하면 좋다.

 

 

3-2-4 bind 메서드

 

Function.prototype.bind(thisArg[,arg1[,arg2[, ...]]])

 

bind 메서드는 call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다.

다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다.

즉 bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.

 

var func = function ( a,b,c,d) {
	console.log(this, a, b, c, d);
 };
 func(1,2,3,4);       //window{ ... } 1 2 3 4
 
 var bindFunc1 = func.bind({ x:1 }); 
 bindFunc(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

 

6번째 줄에서 bindFunc1 변수에는 func에 this를 { x:1 }로 지정한 새로운 함수가 담긴다.

이제 7번째 줄에서 bindFunc1을 호출하면 원하는 결과를 얻을 수 있다.

9번째 줄의 bindFunc2 변수에는 func에 this { x:1 } 로 지정하고, 앞에서부터 두 개의 인수를 각각 4,5로 지정한 새로운 함수를 담았다.

이후 10번째 줄에서 매개변수로 6,7을 넘기면 this 값이 바뀐 것을 제외하고는 최초 func함수에 4,5,6,7을 넘긴 것과 같은 동작을 한다.

 

 

name 프로퍼티

 

bind 메서드를 적용해서 새로 만든 함수는 한 가지 독특한 성질이 있다.

바로 name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙는다.

어떤 함수의 name 프로퍼티가 'bound xxx'라면 이는 곧 함수명이 xxx인 원본 함수에 bind메서드를 적용한 새로운 함수라는 의미가 되므로 기존의 call이나 apply보다 코드를 추적하기 더 수월해졌다.

 

var func = function (a, b, c, d) {
	console.log(this, a,b,c,d);
 };
 var bindFunc = func.bind({ x:1 }, 4, 5};
 console.log(func.name);       //func
 console.log(bindFunc.name);   //bound func

 

 

상위 컨텍스트의 this를 내부 함수나 콜백 함수에 전달하기

 

메서드의 내부 함수에서 메서드의 this를 그대로 바라보게 하기 위한 방법으로 self 등의 변수를 활용한 우회법을 소개했었는데, call apply 또는 bind 메서드를 이용하면 더 깔끔하게 처리할 수 있다.

 

 

3-2-5 화살표 함수의 예외사항

 

새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다.

즉 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.

 

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

 

이렇게 하면 별도로 변수로 this를 우회하거나 call/apply/bind를 적용할 필요가 없어 더욱 간결하고 편리하다.

 

 

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

 

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this 로 지정할 객체 (thisArg)를 인자로 지정할 수 있는 경우가 있다.

이러한 메서드의 thisArg값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있다. 

이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진되어 있다.