[코어 자바스크립트]3. this

Donghun Seol·2022년 11월 23일
0

코어자바스크립트

목록 보기
3/7

this는 상황에 따라 달라진다.

일단은 this는 함수를 호출한 주체의 정보를 가진다고 알아두자.

전역공간에서의 this

브라우저환경에서 전역객체는 window, node에서 전역객체는global이다.
전역변수를 선언하면 전역객체의 프로퍼티로도 접근할 수 있다.
변수로 선언한 것과 전역객체의 프로퍼티로 선언한 것은 대부분의 경우 동일하게 작동한다. 다만 전역객체의 프로퍼티로 선언된 변수는 delete연산자를 사용할 수 있다. 변수를 굳이 전역객체의 프로퍼티로 선언해서 일을 혼란스럽게 만들 필요는 없어보인다. 선언된 변수에 null을 대입하는 방식이 낫지 않을까?

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

메서드로 호출할때의 this

일반적으로 메서드는 객체의 프로퍼티로 할당된 함수이다. 하지만 객체의 프로퍼티로 할당된 함수가 해당 객체의 컨텍스트에서 호출되어야 메서드로서 기능한다. 간단하게는 함수 앞에 dot(.)이 포함되면 메서드로 호출된 것이라 볼 수 있다.
(JS에서는 객체의 프로퍼티로 선언된 함수를 객체의 컨텍스트 밖에서도 호출 가능하다)

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

var obj = {
  method: func
};

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

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

var obj1 = {
  outer: function () {
    console.log(this); // (1)
    var innerFunc = function () {
      console.log(this); //(2) and (3)
    }
    innerFunc();
    var obj2 = {
      innerMethod: innerFunc,
    }
    obj2.innerMethod();
  },
}

obj1.outer();

위 함수의 실행순서는 다음과 같다. (한번에 이해가 안되므로 교재를 타이핑하면서 따라가보자)
해설을 보지않고 자바스크립트 엔진의 동작을 설명할 수 있으면 실행컨텍스트를 충분히 이해한 것이라 생각한다.
복습할때 실행컨텍스트를 추리해 보고 정답과 비교하는 연습을 해 보자.

실행순서

  1. 1행 : 객체를 생성한다. 객체 내부에는 익명함수가 할당된 outer라는 프로퍼티가 생성된다. 생성한 객체를 obj1에 할당한다.
  2. 15행 : obj1.outer를 호출한다.
  3. 2행 : obj.outer 함수의 실행 컨텍스트가 생성된다.
    • 호이스팅 (var innerFunc, var obj2)
    • 스코프 체인 정보를 수집
    • this를 바인딩 한다.
    • 호출 당시에 .이 있으므로 메서드로 호출되어 .앞의 객체인 obj1가 this에 바인딩된다.
  4. 3행 : obj1 객체 정보가 출력된다.
  5. 4행 : 2행에서 호이스팅된 변수 innerFunc는 outer 스코프 내에서만 접근할 수 있는 지역변수다. 이 지역변수에 익명함수가 할당된다.
  6. 7행 : innerFunc()를 호출한다.
  7. 4행 : innerFunc의 실행 컨텍스트가 생성되면서, 호이스팅, 스코프 체인, this바인딩이 수행된다. .이 없는 함수로서 실행되었으므로 this에는 글로벌 객체가 바인딩된다.
  8. 5행 : 글로벌 객체의 정보가 출력된다.
  9. 9행 : 이미 3번에서 호이스팅된 obj2 역시 지역변수인데, 여기에 객체를 할당한다. 내부적으로도 할당한다.
  10. 12행 : obj2.innerMethod를 호출한다.
  11. 9행 : 함수호출로 실행컨텍스트가 생성되면서 호이스팅, 스코프체인, this바인딩이 수행된다. innerMethod 앞에 .이 있으므로 this에는 .앞의 객체인 obj2가 바인딩된다.
  12. 10행: obj2 객체 정보가 출력된다.

함수의 출력 결과

(1), (2), (3)의 출력값을 생각해 보자.
(1), (3)은 메소드로서 호출되어 this값이 해당 객체로 출력되지만 (2)는 함수로 호출되어 전역객체를 this로 갖는다.

arrow function에서의 this

ES6에서는 함수 내부의 this가 전역객체를 바라보는 문제를 해결하고자 this를 바인딩하지 않는 화살표함수를 도입했다.
화살표함수를 호출시 실행 컨텍스트 생성과정은 호이스팅, 스코프체인 수집만 포함하고 this바인딩은 제외된다. 따라서 화살표함수 lexical environment에는 this가 없다. 따라서 스코프체인상 직전의 스코프의 lex에 있는 this를 참조한다. 위의 코드에서 innerFunc를 화살표함수로 선언한다면 (2)번의 출력값이 obj1이 된다.

또는 명시적으로 call, apply등의 메서드를 활용해 this를 바인딩 해줄수 있다.

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

다음 장에서 콜백에 대해 자세히 다루겠지만, 일반적으로 콜백은 함수 cb의 제어권을 다른 함수 B에게 넘겨주는 경우, 함수 cb를 콜백함수라 한다. 이 때 함수 cb는 함수 B에 정의된 로직에 따라 수행되고, cb의 this도 B에 정의된 규칙에 따라 바인딩된다. 일반적인 원칙에 따라 cb가 함수형태로 호출될때 cb에서의 this도 전역객체를 가리키지만, B에서 별도의 규칙을 지정할 경우 this가 달라질 수 있다.

forEach의 내부에서는 this바인딩에 관한 규칙이 없어 this가 전역객체를 가리키지만 addEventListener에는 this바인딩이 정의되어 this가 바뀌었다.

[1,2,3].forEach(function (elem) {
  console.log(elem, this); // this is Global
});

document.body.querySelector('#click')
  .addEventListener('click', function(e) {
  	console.log(this, e); // this is target element 
  });            

생성자 함수 내부에서의 this

자바스크립트에서 new와 함께 함수를 호출하면 해당 함수가 생성자로 작동한다. 어떤 함수가 생성자로서 작동한 경우, 내부에서의 this는 새로만들 구체적인 인스턴스 자신이 된다. 생성자 함수의 호출과정은 아래와 같다고 한다. prototype에 대한 이해가 없어서 일단 따라 작성만하고 넘어가자.

  1. new 키워드와 함께 생성자 함수가 호출된다.
  2. 생성자의 prototype 프로퍼티를 참조하는 __proto__ 라는 프로퍼티가 있는 객체가 생성된다.
  3. 미리 준비된 공통 속성 및 개성을 해당 객체(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);

작성하고 보니 다분히 직관적으로 동작한다. 또한 화살표 함수로는 생성자 함수를 만들수 없다. 앞서 학습했듯이 화살표 함수는 this가 없으므로 당연히 생성자 함수가 될 수 없다.

명시적 this 바인딩

call

먼저 아래의 함수 시그니처를 읽는 법을 정리하고 지나가자

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

call은 Function 타입의 모든 인스턴스가 가지고 있는 메서드다.
prototype에 정의되어 있으므로 스태틱 메서드가 아닌 인스턴스의 메서드다
call 메서드는 필수 인자로 thisArg를 받는다.
선택적으로 0 ~ n개의 인자를 받을 수 있다. [ ] 은 optional parameter를 의미한다.

call은 this를 바인딩한 결과를 인자와 함께 즉시 수행하게 하는 메서드다.
아래의 예시를 확인하면 직관적으로 쉽게 이해 가능하다. 적절히 활용하는건 별개지만.

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}, 2, 3); // 4, 2, 3,

apply

함수 시그니쳐만 보고도 대략적인 함수의 작동을 파악할줄 알아야 한다. 그러려고 시그니처를 작성해 놓는거고.
Function.prototype.apply(thisArgs[, argsArray])

결국 apply는 call과 동일한 기능을 하지만 인자를 여러개가 아니라 배열로 받는 메서드다.

call / apply 메서드의 적절한 활용

유사배열객체

ES6에서는 유사배열객체를 배열로 변환시키는 Array.from(arrLikeObj)(스태틱) 메서드를 활용해서 유사배열객체를 배열로 변환시키고, 유용한 배열 메서드를 활용할 수 있지만, ES5에는 해당 메서드가 없다. 따라서 call과 apply를 활용해서 유사배열객체에 배열메서드를 활용한다.

유사배열객체란 키값이 양의정수고, length 프로퍼티가 있는 객체를 말한다. 해당 조건을 만족시키는 객체, 문자열, querySelectorAll의 반환형인 NodeList, 함수의 인자를 나타내는 예약어 arguments가 대표적인 유사배열객체이다.
단, 문자열의 경우 immutable하므로 원본을 변화시키는 push, pop, shift, unshift, splice의 메서드는 사용할 수 없다.

var obj = {0:'a',1:'b',length:2}; // 유사 배열객체
Array.prototype.push.call(obj, 'c'); // obj = {0:'a', 1:'b', 2:'c', length:3}

var arr = Array.prototype.slice.call(obj); // slice.call()을 활용해 객체를 배열로 변환
console.log(arr) // [a, b, c]

참고로 Array.from(obj)는 다음과 같이 동작한다.
length property가 존재하고 키값이 정수인 유사배열객체만 올바르게 배열로 변환시켜준다.

Array.from({a:'a',b:'b',c:'c'}); // [] 
Array.from({0:'a', 1:'b', 5:'c', length:6}) // ['a', 'b', undefined, undefined, undefined, 'c']

생성자 내부에서 생성자 호출

생성자 내부에서 다른 생성자와 공통적인 내용이 있을 경우 call 또는 apply를 활용해 다른 생성자를 호출하면 반복을 줄일 수 있다.
객체지향언어에서 부모클래스의 생성자를 호출하는 것과 유사한 효과를 낼 수 있다.

function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}

function Student(name, gender, school) {
  Person.call(this, name, gender);
  this.school = school;
}

function worker(name, gender, company) {
  Person.call(this, name, gender);
  this.company = company;
}

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

ES5에서는 다른 대안이 없으므로 가독성 좋고 짧은 코드를 작성하기 위해서 call/apply를 자주 활용한다.
ES6에는 동일한 역할을 수행하는 Spread operator... 가 있지만 항상 ES6로 작성된 코드로만 개발하는 것은 아닐 수 있으니 익혀 둘 필요가 있다.

var numbers = [10, 20, 30, 40]
var max = Math.max.apply(null, numbers);

// or use spread operator
var max = Math.max(...numbers);

bind

bind 메서드의 시그니쳐는 다음과 같다.
Function.prototype.bind(thisArs[, arg1[, arg2[, ...]]])
bind 메서드는 특정함수를 인자로 전달된 this와 args로 바인딩된 버전의 함수를 반환한다.
bind적인 활용법은 아래와 같다.

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

var boundFunc1 = func.bind({x:1});
boundFunc(1,2,3,4) // {x:1} 1 2 3 4 this가 {x:1}로 바뀜을 확인할 수 있다.

var boundFunc2 = func.bind(null, 11, 12); // thisArg로 null 전달시 this는 그대로
boundFunc2(3, 4); // global{...} 11, 12, 3, 4

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

객체의 프로퍼티로 선언된 함수를 항상 객체의 맥락에서만 실행되도록 강제하는 멋진 활용법이다.
객체의 프로퍼티로 선언된 함수를 작성한 개발자는 일반적으로 해당 함수가 객체가 아닌 다른 this에 binding되어 실행되는 것을 원치 않을 것이다. 하지만 js의 특성? 버그? 설계상의 실수?로 이러한 동작은 자연스럽게 강제되지는 않으므로, bind를 적절히 활용해주어서 해당 제약을 설정할 수 있다.

ES6에서는 arrow function을 활용하면 bind 없이도 해당 기능을 간단히 구현 가능하다.
(잊지 않기 위해 또 작성하자면, arrow function이 호출될때 실행컨텍스트의 형성 과정에서는 this binding이 생략된다. 따라서 arrow function은 자신의 lexical env에 this를 갖지 않으므로, 스코프체인을 뒤져서 상위 스코프에 위치한 this를 참조한다. 이러한 동작원리가 arrow function은 상위 스코의 this를 상속받는 것처럼 보이게 한다.)

알쏭달쏭 출력값 맞추기 퀴즈

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

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

var doubleArrowObj = {
  outer : () => {
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    }
    innerFunc(); 
  }
};
arrowObj.outer(); // window window

별도의 인자로 this를 받는 경우

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있다. 이를 통해서 콜백함수의 this를 인자로 받은 thisArg로 바인딩해서 실행한다. 일반적으로 배열메서드에 많이 포함되어 있다.

예제가 어려우니 받아적으면서 이해해보자.

var report = {
  sum: 0,
  count: 0,
  add: function () {
    var args = Array.prototype.slice.call(arguments);
    args.forEach(function (entry) {
      console.log(this);
      this.sum += entry;
      ++this.count;
    }, this); 
    // 윗줄에서 thisArg를 지정하지 않으면 콜백함수의 this는 전역객체이므로 의도한대로 동작하지 않는다.
  },
  average : function () {
    return this.sum / this.count;
  }
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240, 3, 80

만약 add를 arrow function으로 만들면 this를 바인딩해주지 않아도 의도한 대로 작동한다.
역시 ES6 문법이 편하고, 직관적이구만.

현재 학습중인 코어 자바스크립트 재밌고, 짧고, 유익하다.
자바스크립트의 내부 동작원리를 부담스럽지 않게 학습하고 싶다면 추천드린다.

profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글