본문 바로가기

JavaScript/[책] 코어 자바스크립트 -ing

03. this in JavaScript

 

 

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

1) 전역 공간에서의 this

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

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

  - 함수 내부에서의 this

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

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

  - this를 바인딩하지 않는 함수

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

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

 

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

1) call 메서드

2) apply 메서드

3) call / apply 메서드의 활용

  - 유사배열객체(array-like object)에 배열 메서드를 적용

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

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

4) bind 메서드

  - name 프로퍼티

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

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

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

 


 

00. 알아두면 좋다

출처: https://medium.com/crocusenergy/js-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-2b8ab8da4f4

 

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

실행 컨텍스트는 함수를 호출할 때 생성되므로, this는 함수를 호출할 때 결정된다.

 

2) 프로그래밍 언어에서 함수와 메서드는 미리 정의한 동작을 수행하는 코드 뭉치를 의미한다.

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

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

 

3) 메서드 = 객체의 프로퍼티에 할당된 함수?(▵)

어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만(object.method() 뭐 이런 형식으로) 메서드로 동작하고, 그렇지 않으면 함수로 동작한다.

 

 

@flowforfrank

 

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

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

- 전역 공간에서의 this(브라우저 환경)는 window 객체(window 객체가 전역 객체)

- 전역 공간에서의 this(Node.js 환경)는 global 객체(global 객체가 전역 객체)

 

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

 

 

2) 메서드로 호출할 때 그 메서드 내부에서의 this호출한 주체(주로 함수명(프로퍼티명) 앞의 객체)를 가리킨다.

this를 좀 더 살펴 보기 전에...

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

 

- 함수 VS 메서드

함수 앞에 점(.)의 여부 또는 대괄효 표기법으로로 구분한다. 

점(.)이 있으면 메서드로서 호출, 없으면 함수로서의 호출이다.

 

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

var func = function (x) {
  console.log(this, x);
};
func(1); // Window { ... } 1 함수로서 호출, this는 전역객체를 가리킨다.

var obj = {
  method: func
};
obj.method(2); // { method: f } 2 메서드로서 호출, this는 method 메서드를 호출한 주체인 obj를 가리킨다.

 

 

3) 함수로서 호출할 때 그 함수 내부에서의 this전역객체를 가리킨다.

- 어떤 함수를 함수로서(메서드가 아니라) 호출할 경우에는 this가 지정되지 않는다.

실행 컨텍스트 활성화할 당시에 this가 지정되지 않은 경우 this는 전역 객체를 바라본다.

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

 

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

this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고,

오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건이다.

var obj = {
  outer: function() {
    console.log(this);
    var innerFunc = function () {
      console.log(this);
    }
    
    innerFunc(); // Window, 메서드의 내부함수인 innerFunc을 함수로서 호출했기 때문에 this 는 전역객체를 가리킨다.
    
    var obj2 = {
      innerMethod: innerFunc
    };
    obj2.innerMethod(); // obj2
  }
};

obj1.outer();

 

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

var obj = {
  outer: function () {
    console.log(this);
    var innerFunc1 = function () {
      console.log(this);
    };
    innerFunc1();
        
    var self = this; // obj.outer() 코드에서 outer를 호출할 때 메서드로서 호출 했기 때문에 this는 obj 객체를 가리키게 된다.
    var innerFunc2 = function () {
      console.log(self);
    };
    innerFunc2(); // { outer: f } 따라서 innerFunc2는 객체가 출력된다.
  }
};
obj.outer();

 

- this를 바인딩하지 않는 함수: 화살표 함수에서 this상위스코프의 this를 가리킨다.

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

var obj = {
  outer: function () {
    console.log(this); // obj { outer: f }
    var innerFunc = () => {
      console.log(this); // obj { outer: f } 
    };
        
    innerFunc(); // 원래는 함수로써 호출했기 때문에 this는 전역객체가 되어야 하지만 innerFunc 함수가 화살표 함수로 함수가 정의되었기 때문에 this를 바인딩하지 않고 상위 SCOPE의 this를 쓸 수 있게 된 것이다.
  }
}

obj.outer();

위 예제에서

outer 메서드 내부의 함수인 innerFunc을 함수로서 호출(7번 라인)했기 때문에 this는 전역객체가 되어야 하지만

innerFunc 함수가 화살표 함수로 함수가 정의되었기 때문에 innerFunc 실행 컨텍스트를 생성할 때

this를 바인딩하지 않고 상위 SCOPE의 this를 쓸 수 있게 된 것이다.!

 

 

4) 콜백 함수 호출 시 그 콜백 함수 내부에서의 this는 기본적으로 전역 객체이다.

콜백 함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조하지만,

제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조한다.

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

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

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

- setTimeout 함수와 forEach 메서드는 그 내부에서 콜백함수를 호출할 때 대상이 될 this를 지정하지 않는다. 

따라서 this는 전역 객체를 가리킨다.

 

- addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있다(이벤트를 등록한 DOM요소가 this가 된다).

 

 

5) 생성자 함수 내부에서의 this객체 자신을 가리킨다.

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

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

 

자바스크립트는 함수에 생성자로서의 역할을 함께 부여했다.

new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다.(그럼 객체가 만들어지겠지?)

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

 

생성자 함수를 호출하면(new 명령어와 함께 함수를 호출)

생성자의 prototype 프로퍼티를 참조하는 __proto__ 라는 프로퍼티가 있는 객체(인스턴스)를 만들고

미리 준비된 속성 및 개성을 해당 객체(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 키워드를 써서 초기화하고 App()함수를 호출했더니

TypeError가 발생했다.

new 키워드를 이용하여 생성자 함수로 호출했더니 TypeError가 나지 않았다.

그냥 함수로 호출했을 때는 this가 전역객체를 가리키는데 전역 객체에는 name이라는 프로퍼티가 없으니 에러가 났던 것이다.

그러나 생성자 함수로 호출하니 this는 생성자 함수로 만든 객체를 가리키게 되고 (app { menu: [] }) 

그 객체안에는 menu 프로퍼티가 있으니 에러가 나지 않았던 것이다.

아 이제 이해했어!! ㅋㅋㅋㅋ

 

 


 

 

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

1) call 메서드

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/call

 

- call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령으로

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

(call 메서드로 이용하면 임의의 객체를 this로 지정할 수도 있다!)

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

func(1, 2, 3); // Window {...} 1 2 3
func.call({ x:1 }, 1, 2, 3); // {x: 1} 1 2 3

// 예제 2
var obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y)
  }
};

obj.method(2, 3); // 1, 2, 3
obj.method.call({ a: 4 }, 5, 6); // 4, 5, 6

 

 

2) apply 메서드

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply

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

call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면,

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

 

 

3) call / apply 메서드의 활용

- 유사배열객체(array-like object)에 배열 메서드를 적용할 수 있다.

유사 배열 객체란 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체,

즉 배열의 구조와 유사한 유사 배열 객체의 경우 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']

ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 -> 배열로 전환하는 Array.from 메서드를 새로 도입했다.

 

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

생성자 내부에서 다른 생성자와 공통된 내용이 있는 경우, call 또는 apply를 이용해 다른 생성자를 호출 할 수 있다.

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price); // 생성자 함수(Food) 내부에서 다른 생성자(Product)를 호출
  this.category = 'food';
}

console.log(new Food('cheese', 5).name);
// expected output: "cheese"

 

- 여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 apply 활용(ES6 에서는 펼치기 연산자(spread operator)를 이용하면 apply보다 더욱 간편하게 작성 가능)

var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min); // 45, 3

const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max, min); // 45, 3

 

 

4) bind 메서드 

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

  - name 프로퍼티: bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티 앞에 bound라는 접두어가 붙는다.

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를 내부함수나 콜백 함수에 전달하기

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

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

 

 

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

(ES6) 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다.

따라서 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 상위 scope의 가장 가까운 this에 접근 가능하다.

 

 

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

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

 

 

스터디 회고

이번 챕터 및 스터디를 통해서 함수 호출 방식에 따라 this가 가리키는게 다르다는 것을 처음 '인지'했다.  

퍼블리싱 처음 배울 때, 제이쿼리를 많이 썼는데 그 때 this를 아주 편하게 썼었다. '클릭한 걔'를 지칭하는데 this 만큼 편한게 없었다. 그런데 자바스크립트 안에서 this 를 쓰려니 제이쿼리에서만큼 간단하지 않았다. 내가 왜 자바스크립트 코드 해석하는게 종종 막혔는지 이제 이유를 알게 됐다. 책을 읽는 내내 this란 놈이 헷갈렸지만 그래도 여러번 읽고 계속 예제코드를 돌려 가면서 생각해보니 조금씩 this에 대한 가닥이 잡힌다.

 

그러다 문득, 꼭 this를 써야만 하는가? 그냥 this 자리에다가 구체적인 식별자를 넣으면 헷갈릴 일도 없는데 왜 굳이 this를 써야 하는지 의문이다. 구체적인 식별자를 넣어도 내가 원하는 값 잘 나오잖아. 왜 쓰는거지? 그 이유를 찾는 중이다.

 

그리고 . 나는 ES5, ES6 환경 차이를 직접적으로 느껴본 적이 없는지라 두 환경을 다 커버할 수 있는 방법을 알아야 하는게 머릿속에 잘 안들어왔다. 특히 call / apply 메서드는 명시적으로 별도의 this를 바인딩하면서 함수 또는 메서드를 실행하는 방법이지만 이로 인해 this를 예측하기 어렵게 만들어 코드 해석을 방해한다는 단점이 있다. 그럼에도 불구하고 ES5 이하의 환경에서는 마땅한 대안이 없기 때문에 실무에서 매우 광범위하게 활용되고 있다고하는데.... 그래 알아야만 하겠구나.. 하지만 어렵다.. 개념이 어렵다긴 보다는 이 메서드를 쓰는 그 환경에 대한 게 머리에 그려지지 않아서.. 실제로 ES5에서 어떤 오류를 뿌려줄지 몰라서 등등과 같은 이유로... 어렵다.

 

익숙해질때까지 계속 반복반복 해보자.!

 

 

추가로 공부하면 좋을 것들

- 다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미한다.

- mdn this https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this