본문으로 바로가기

3장 this

category 코어 자바스크립트 2020. 12. 21. 16:28

다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미한다, 클래스에서만 사용할 수 있기 때문에 혼란의 여지가 거의 없다, 하지만 자바스크립트에서의 this는 어디서든 사용되어 혼란스러워 진다

함수와 객체(메서드)의 구분이 느슨한 자바스크립트에서 this는 실질적으로 이 둘을 구분하는 거의 유일한 기능이다

상황별로 this가 어떻게 달라지는지, 왜 그렇게 되는지, 예상한 대상과 다를 경우 추적하는 방법 등을 살펴보자

 

상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다 실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다 라고 할 수 있다

 

전역공간에서의 this

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

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

//브라우저 환경에서 this
console.log(this === window) // true

//Node.js 환경에서 this
console.log(this === global) // true

 

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

var a = 1;
console.log(a); // 1  <<스코프 체인에서 a를 검색하다가 제일 마지막에 도달한 window.a를 찾음
console.log(window.a); // 1
console.log(this.a); // 1  << this === window

 

그래서 전역변수 선언과 전역객체의 프로퍼티 할당을 똑같이 사용해도 될 것 같지만

'삭제' 명령에는 약간 다르게 동작한다

var a = 1;
delete a; // false
console.log(a); // 1

window.b = 2;
delete b; // true
console.log(b) // error

분명히 앞에서 똑같다고 했는데 다르게 나왔다. 이유가 무엇일까?

 

이유는 자바스크립트에서 사용자가 의도치 않게 삭제하는 것을 방지하는 차원에서 마련한 나름의 방어 전략이라고 해석하면 된다 

더보기

더 자세한 이유는 객체 프로퍼티는 값과 함께 플래그(flag)라 불리는 특별한 속성 세 가지를 갖는데 그 중에 configurable 속성(변경 및 삭제 가능성)을 false로 정의 했기 때문이다

참고 [ko.javascript.info/property-descriptors]

이처럼 var로 선언한 전역변수와 전역객체의 프로퍼티는 호이스팅 여부 및 configurable 여부에서 차이를 보인다

 

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

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

이 둘을 구분하는 유일한 차이는 독립성에 있다

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

 

메서드는 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고 그렇지 않으면 함수로서 동작한다

 

함수로서 호출, 메서드로서 호출

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

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

이렇게 함수로서 호출과 메서드로서 호출은 this가 다르다

함수 앞에(.) 점이 있으면 그건 메서드로 호출 한 것이다 

물론 대괄호 표기법도 가능하다

 

대괄호 표기법

var obj = {
	method: function (x) { console.log(this, x); }
};

obj.method(1);      // { method: f }, 1
obj['method'](2);   // { method: f }, 2

앞에 객체가 명시돼 있는 경우에만 메서드로 호출한 것이고 그렇지 않은경우는 모두 함수로서 호출한 것이다

 

메서드 내부에서의 this

var obj = {
    A: function () { console.log(this); },
    inner: {
    	B: function () { console.log(this); }
    }
}

obj.A();  // this === obj

obj.inner.B();  // this === obj.inner

메서드로서 호출 할때 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
        };
        obj.innerMethod();
    }
};
obj1.outer();

this가 어떻게 출력될 것 같은지 예상해보자

답은 (1) obj1, (2) window, (3) obj.innerMethod 이다

 

함수 내부임에도 (2)번의 this는 window를 가리킨다

 

왜 설계상의 오류라고 지적했을까?

this 바인딩에 관해서는 함수를 실행하는 당시의 주변환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건이기 때문이다

더불어 스코프 체인과의 일관성을 지키려면 주변 환경의 this를 그대로 상속받아 사용하는게 마땅하긴 하다

하지만 우리는 이 언어를 사용해야하기 때문에 this를 우회해서 사용해보는 법을 알아보는게 현명하다

 

this를 우회하는 방법

var obj1 = {
    outer: function () {
        var self = this;
        var innerFunc = function () {
            console.log(self);  // self === obj1
            console.log(this);  // this === window
        }
        innerFunc();    
    }
};
obj1.outer();

변수를 활용하여 this가 현재 컨텍스트를 바라보게 하면 된다

변수 명은 _this, that, _등이 쓰이는데 self라는 변수 명이 가장 많이 쓰이니 이런상황에서는 self를 쓰도록 하자 

 

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

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

ES6에서는 this를 바인딩 하지 않는 함수 화살표 함수(arrow function)를 새로 도입했다

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

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

 

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

//대표적인 콜백 함수 3개
setTimeout(function () { console.log(this); }, 300);  // (1)번

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

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

(1)번과 (2)번은 콜백함수를 호출할 때 대상이 될 this를 지정하지 않아 전역객체를 참조한다

(3)번은 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의되어 있다

 

그래서 보통 콜백함수에서 화살표 함수를 사용한다

 

 

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

명시적으로 this를 바인딩 하는 메서드는 call,apply,bind가 있다

 

call 메서드

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

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

func(1, 2, 3);  // window, 1, 2, 3   (this === window)
func.call({x: 1}, 1, 2, 3);  // {x: 1}, 1, 2, 3   this === {x: 1}

 

apply 메서드

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

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

func(1, 2, 3);  // window, 1, 2, 3   (this === window)
func.apply({x: 1}, [1, 2, 3]);  // {x: 1}, 1, 2, 3   (this === {x: 1})

 

call과 apply의 차이점

call 첫번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정
apply 두번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정

즉 한개 이상의 인자가 있다면 call을,

인자가 배열이라면 apply를 사용하면 된다

 

call / apply 메서드의 활용 (유사배열객체에 배열 메서드를 적용)

//arguments에 배열 메서드를 적용
function a () {
    var argv = Array.prototype.slice.call(arguments);
    argv.forEach(function (arg) {
        console.log(arg);
    });
}
a(1, 2, 3); 
// NodeList에 배열 메서드를 적용
document.body.innerHTML = '<div>a</div><div>b</div><div>c</div>';
var nodeList = document.querySelectorAll('div');
var nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function (node) {
    console.log(node);
});

이런 식으로 유사배열 객체에 배열의 메서드를 사용가능하다

하지만 이런식으로 사용하게 되면 어떤 의도인지 파악하기 쉽지 않다

ES6에서 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입 했으니 그것을 사용하는게 더 가독성이 좋을 것 같다

 

bind 메서드

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

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

var bindFunc1 = func.bind({x: 1});
bindFunc1(1, 2, 3, 4); // {x: 1}, 1, 2, 3, 4

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

 

bind를 활용한 부분적용 함수

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

var bindFunc1 = func.bind({x: 1}, 1, 2);
bindFunc1(3, 4); // {x: 1}, 1, 2, 3, 4


console.log(func.name);  // func
console.log(bindFunc1.name);  // bound func

 

모든 함수는 name이라는 프로퍼티가 있는데 bind 메서드를 적용해서 새로 만든 함수는 'bound'라는 접두어가 붙는다

 

화살표 함수를 사용하면 this를 우회하거나 call/apply/bind 를 적용할 필요가 없어 더욱 간결하고 편리하다

 

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

var report = {
    sum: 0,
    add: function () {
        var args = Array.prototype.slice.call(arguments);
        args.forEach(function (entry) {
            this.sum += entry;
        }, this);
    }
};

report.add(100,200);
console.log(report.sum);  // 300

이런식으로 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체(thisArg)를 인자로 지정할 수 있는 경우가 있다, 이런 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있다

이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진돼 있고 ES6에 등장한 Set, Map 등의 메서드에도 일부 존재한다

 

 

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

Array.prototype Set.prototype Map.prototype
forEach, map, filter, some, every, find, findIndex, faltMap, from forEach forEach

 

 

'코어 자바스크립트' 카테고리의 다른 글

7장 클래스  (0) 2020.12.28
6장 프로토타입  (2) 2020.12.24
5장 클로저  (0) 2020.12.23
4장 콜백함수  (0) 2020.12.22
1장 메모리와 데이터 타입  (0) 2020.12.21
2장 실행 컨텍스트  (0) 2020.12.21