[JavaScript 정복기] this가 뭐야?? (코어 자바스크립트) - 1

예흠·2020년 11월 3일
2

JavaScript 정복기!

목록 보기
1/2

javascript를 공부해보며 뭔가 깊이 몰랐던 부분들 위주로 정리 해보고자 한다.

this ?

자바스크립트에서 가장 혼란스러운 개념중 하나로 꼽히는게 this가 아닐까 한다.
다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미한다.
자바스크립트에서는 this를 어디서든 사용할 수 있어서 상황에 따라 this가 바라보는 대상이 달라지는데, 어떤 이유로 그렇게 되는지를 파악하기 힘든 경우도 있고 예상과 다르게 엉뚱한 대상을 바라보는 경우도 있다.

* 상황에 따라 달라지는 this

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

1. 전역 공간에서의 this

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

console.log(this === window); //true (브라우저 환경)
console.log(this ====global); //true (Node.js 환경)

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

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

this.awindow.a의 값이 1인 것은 자바스크립트의 모든 변수는 실은 특정객체의 프로퍼티로서 동작하기 때문이다.
=> 정확히는 '전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다'

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

var로 선언하는 대신 window.a 이런식으로 직접 프로퍼티에 할당하더라도 var로 선언한 것과 똑같이 동작할 것이라고 예상 할 수 있다.

대부분은 맞지만 전역변수 선언과 전역객체의 프로퍼티 할당 사이에 전혀 다른 경우도 있다.
=> 바로 '삭제'명령

var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1 
...
window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // Uncaught ReferenceError: c is not defined

위와같이 처음부터 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되는 반면 전역변수로 선언한 경우에는 삭제가 되지 않는 것을 확인 할 수 있다.
=> 사용자가 의도치 않게 삭제하는 것을 방지하는 차원에서 마련한 나름의 방어 전략이라고 해석된다.

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

이렇게 함수로의 호출과 메서드로서의 호출은 this가 달라진다.
=> 함수 앞에 점(.)이 있는지 여부만으로 간단하게 구분할 수 있다.

메서드 내부에서의 this

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

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

obj.methodA(); // { methodA: f, inner: {...} } ( === obj ) 

obj.inner.methodB(); // { methodB: f } ( === obj.inner )

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

함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않는다.
=> 함수로서 호출하는 것은 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없는 것이다.

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

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

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

var obj1 = {
  outer: function () {
    console.log(this); // obj1
    var innerFunc = function () {
      console.log(this); 
    }
    innerFunc(); // 전역객체(window)
    
    var obj2 = {
      innerMethod: innerFunc
    };
    obj2.innerMethod(); // obj2
  }
};
obj1.outer();

위의 예제를 보면 확실히 감이 올 것이다.
함수로서의 호출인가 메서드로서의 호출인가로 전역객체인지 호출주체인지를 .을 통해 파악할 수 있기 때문이다.

7번째 줄과 12번째 줄처럼 같은 함수임에도 바인딩되는 this가 달라지는 것이다.

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

아쉽게도 ES5까지는 자체적으로 내부함수에 this를 상속할 방법이 없지만 다행히 이를 우회할 방법이 없지는 않다.

대표적인 방법은 바로 변수를 활용하는 것이다.

var obj1 = {
  outer: function () {
    console.log(this); // { outer: f }
    var innerFunc = function () {
      console.log(this); 
    }
    innerFunc(); // 전역객체(window)
    
    var self = this;
    var innerFunc2 = function () {
      console.log(self);
    };
    innerFunc2(); // { outer: f }
  }
};
obj1.outer();

이런식으로 self라는 변수에 this를 저장한 상태에서 호출한 innerFunc2의 경우 self에는 객체 obj가 출력된다. 별거아닌 방법이지만 충분히 부합하다.

this를 바인딩하지 않는 함수

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

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

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

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

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

이때 함수 A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부로직에서 정한 규칙에 따라 값이 결정된다.
=> 콜백함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.

setTimeout(function () { console.log(this); }, 300); // 0.3초 뒤 전역객체가 출력

[1, 2, 3, 4, 5].forEach(function (x) {
  console.log(this, x); // 전역객체와 배열의 각 요소가 5회 출력
});

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
  .addEventlistener('click', function (e) {
    console.log(this, e); // 버튼 클릭시 앞서 지정한 엘리먼트와 클릭 이벤트 정보 출력
  });
  • setTimeout 함수는 300ms 만큼 시간 지연을 한 뒤 콜백 함수를 실행하라는 명령이다.
  • forEach 메서드는 배열의 각 요소를 앞에서부터 차례로 하나씩 꺼내어 그 값을 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다.
  • addEventListener는 지정한 HTML 엘리먼트에 'click' 이벤트가 발생할 때마다 그 이벤트 정보를 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다.

setTimeout 함수와 forEach 메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않는다. 따라서 콜백 함수 내부에서의 this는 전역객체를 참조한다.
addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있다.

=> 콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라본다.

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

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

객체지향 언어에서는 생성자를 클래스class, 클래스를 통해 만든 객체를 인스턴스instance라고 한다. 프로그래밍적으로 '생성자'는 구체적인 인스턴스를 만들기 위한 일종의 틀이다.
=> 이 틀에는 해당 클래스의 공통 속성들이 미리 준비돼 있고, 여기에 구체적인 인스턴스의 개성을 더해 개별 인스턴스를 만들 수 있다.

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

  • new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작한다.
  • 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

생성자 함수를 호출하면
=> 생성자의 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에 접근해서 bark, name, age 프로퍼티에 각각 값을 대입한다.
6번째 줄의 내부에서의 this는 choco 인스턴스, 7번째 줄의 내부에서의 this는 nabi 인스턴스를 가리킨다.

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

1. call 메서드

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

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

이때 call메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로한다.
=> 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

메서드에 대해서도 마찬가지이다.

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 메서드

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. 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' ]
  • push를 객체 obj에 적용해 3에 'd'를 추가했다.
  • slice 메서드를 적용해 객체를 배열로 전환했다.
    => _slice 메서드는 원래 시작 인덱스값과 마지막 인덱스값을 받아 시작값부터 마지막값의 앞부분까지의 배열 요소를 추출하는 메서드지만, 매개변수를 아무것도 넘기지 않을 경우에는 그냥 원본 배열의 얕은 복사본을 반환한다.

slice가 배열 메서드이기 때문에 복사본은 배열로 반환한다.

function a () {
  var argv = Array.prototype.slice.call(arguments);
  argv.forEach(function (arg) {
    console.log(arg);
  });
}
a(1,2,3) // 1 2 3

함수 내부에서 접근할 수 있는arguments 객체도 유사배열객체이므로 이렇게 배열로 전환해서 활용할 수 있다.

(위의 사진은 arguments를 console.log로 확인해 본 것이다.) 몰랐기 때문에

ES6에서 등장한 Array.form

slice 메서드는 오직 배열 형태로'복사'하기 위해 차용됐을 뿐이니, 코드만 봐서는 어떤 의도인지 파악하기 쉽지 않다.
=> ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.form 메서드를 새로 도입했다.

var obj = {
  0: 'a',
  1: 'b', 
  2: 'c',
  length: 3
};
var arr = Array.form(obj);
console.log(arr); // ['a', 'b', '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 Employee(name, gender, company) {
  Person.apply(this, [name, gender]);
  this.company = company;
}

4. bind 메서드

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

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 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

name 프로퍼티

bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 동사 bind의 수동태인 '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

※ 화살표 함수의 예외사항

화살표 함수(arrow function)는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다.
=> 이 함수 내부에는 this가 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.

정리📄

명시적 this바인딩이 없는 경우

  • 적역공간에서의 this는 전역객체를 참조한다.
  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체를 참조한다.
  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조한다.(메서드 내부함수도 같다.)
  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조한다.
  • 생성자 함수에서의 this는 생성될 인스턴스를 참조한다.

명시적 this바인딩

  • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출한다.
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만든다.
  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 한다.
profile
노래하는 개발자입니다.

0개의 댓글