this에 대해서

김세현·2022년 2월 9일
1

JavaScript

목록 보기
1/5

자바스크립트 "this"에 대해서


자바스크립트의 함수는 호출될 때, 매개변수로 전달되는 인자값 이외에, arguments 객체와 this를 암묵적으로 전달 받는다.
함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this에 바인딩되는 '객체'가 동적으로 결정된다.
즉 어떤 문맥, 상황이냐에 따라 그 this가 가리키는 객체가 달라진다.

그리고 자바스크립트에서 함수를 실행하는 방법엔 크게 4가지가 있다. (+이벤트 핸들러)
(this를 이용하는)함수를 이 4가지중 어떤 방식으로 실행하느냐에 따라 this의 값은 바뀐다.
즉, this의 값을 판단하기 위해선 일단, this키워드를 사용하는 함수를 찾고, 그 함수를 호출하는 위치를 찾아간 다음, 어떠한 방식으로 호출하고 있는지 살펴 보아야 한다.


1.일반 함수 실행 방식 (Regular function call)


가장 기본적인 함수 실행 방식이다.

function foo () {
  console.log(this); // 'this' === global object (브라우저상에선 window 객체)
}

foo();

this를 갖고 있는 함수 foo, 그리고 이 함수에서 사용한 this가 무엇을 가리키느냐?
자바스크립트에서는 일반적으로 함수를 실행할 때, 그 함수의 this는 전역객체인 whindow 객체를 가리킨다.
전역객체는 모든 객체의 유일한 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window, Server-side(Node.js)에서는 global 객체를 의미한다.

// in browser console
this === window // true

//Node
this === global // true

1-1.strict mode가 적용된 경우

'use strict';

let name = 'park';

function foo () {
  console.log(this.name); // 'this' === undefined
}

foo(); //'strict' 모드에서 일반 함수 실행 방식의 this는 무조건 undefined이다.

this는 함수를 실행시키는 문맥에 따라 달라질 수 있다.
그렇기 때문에 window 객체를 가리킬 목적이라면, 그냥 window라는 키워드를 사용하는 것이 적합하다.(가독성)

즉 1번과 1-1번의 경우를 살펴보면 window를 사용하기 위해 키워드 this를 사용하는 것은 보통 좋지 않은 방법(=버그)이 될 수 있다.
따라서 1-1번의 경우, window 객체를 사용하기 위해 this 키워드를 사용하는 것은 버그같은 행위이기 때문에 strict 모드에서 this는 undefined이다.
만약, strict 모드가 아니라면, this.name은 window 객체의 name을 가리키게 되고 이는 undefined이다.
undefined는 엄밀하게 오류를 의미하는 것은 아니지만 자칫하면 이후에 오류가 발생할 수 있는 상황을 놓칠 수 있는 형태이다.
하지만 strict 모드를 적용하면 this는 undefined가 되며, undefined는 객체가 아니므로 dot notation을 사용하면 오류가 발생하게 된다.

VM42 script.js:4 Uncaught TypeError: Cannot read
properties of undefined (reading 'name')

(MDN 설명 : undefined는 전역 객체의 속성입니다. 즉, 전역 스코프에서의 변수입니다. undefined의 초기 값은 undefined 원시 값입니다.)

var age = 100;

function foo () {
  var age = 99;
  bar(); //일반 함수 실행 방식
}

function bar () {
  console.log(this.age); // this라는 키워드가 있는 bar라는 함수는 어디서 실행되느냐?? foo
}

foo();  // 일반 함수 실행 방식

즉, this.age는 무엇인가?를 따져봐야 할 때는,
this를 사용하는 함수 bar를 어디서 실행시키는지 찾아가 본다.
그리고 foo에서는 bar 함수를 일반 함수 실행방식으로 호출하고 있다.
그리고 strict 모드가 적용되지 않았으므로, 이 예제의 this는 window 객체를 가리킨다.
따라서 전역 변수인 age = 100이 출력이 된다.

let 키워드로 선언할 경우 undefined가 출력된다.

전역객체 간단한 설명
전역객체 mdn

1번과 1-1번 case에 대한 결론 :

일반 함수 실행 방식으로 호출되는 함수안의 'this'는 전역객체 window를 가리킨다.
하지만, 전역객체 window를 가리키기 위해 this라는 키워드를 사용하는 것은 가독성이 좋지 않고 일종의 버그같은 행위라고 볼 수 있기 때문에, use strict 모드를 적용해 혹시라도 windows객체를 this를 통해 가리키고자 한다면 에러를 발생시키도록 한다.


2.Dot Notation을 통한 함수 실행(객체의 메소드 호출)시 this


함수가 객체의 프로퍼티 값이면 메소드로서 호출된다.
이때 메소드 내부의 this는 해당 메소드를 소유한 객체, 즉 해당 메소드를 호출한 객체에 바인딩된다.

첫 번째 예제

let age = 200;

let kim = {
  age: 30,
  foo: function() {
    console.log(this.age);
  }
};

kim.foo(); // ?

함수 표현식을 통해 객체의 foo라는 메소드를 정의하고 있다.
이때, this는 foo라는 메소드에서 사용하고 있고, 이 foo라는 메소드를 호출하는 위치를 찾아가 보니 kim이라는 객체를 통해 Dot Notation 접근 방식으로 foo를 호출하고 있다.
이러한 방식으로 접근하는 경우 = 객체의 메소드에 접근할 때, 해당 메소드 안의 this는 Dot Notation을 이용하는 객체인 kim을 가리키게 된다.
따라서 이 예제의 결과는 kim 객체의 age인 30을 출력하게 된다.

(단, 화살표 함수를 이용해 클로저 모듈 패턴을 따라 작성하게 되면 undefined가 된다.)

let age = 200;

let kim = {
  age: 30,
  foo: () => {
    console.log(this.age);
  }
};

kim.foo(); // undefined

두 번째 예제

function foo () {
  console.log(this.age);
}

let age = 100;

let ken = {
  age: 35,
  bar: foo
};

let wan = {
  age: 31,
  baz: foo
};

ken.bar(); // 35
wan.baz(); // 31

Dot Notation을 통해 객체의 메소드에 접근하고 있다.
그리고 해당 메소드들은 foo라는 함수를 가리키고 있고 foo라는 함수는 this를 사용하고 있다.
따라서 foo안의 this가 가리키는 대상은 해당 메소드를 가진 객체이다.(ken , wan)

세 번째 예제


let age = 100;

let ken = {
  age: 35,
  foo: function () {
    console.log(this.age);
  }
};

let wan = {
  age: 31,
  bar: ken.foo
};

let baz = ken.foo;

ken.foo(); // ?
wan.bar(); // ?
baz(); // ?

ken.foo()는 Dot Notation을 이용해 foo라는 메서드에 접근하고 있다.
그리고 foo는 this를 사용하고있으므로, 이 this는 ken을 가리키게 된다.

wan.bar()는 Dot Notation을 이용해 bar라는 메서드에 접근하고 있다.
그리고 bar라는 메서드에는 ken.foo라는 값이 저장되어 있고, 이 값은 함수이다.
따라서 bar = foo 와 같다.
즉 wan.foo와 같으며, foo의 this는 wan이라는 객체를 가리킨다.

baz = foo와 같다.
그리고 일반 함수 실행 방식으로 baz를 호출하고 있다.
그래서 Windows객체를 가리킬 줄 알았고 전역 변수인 age를 가리켜 100이라는 값이 호출될 줄 알았지만, undefined가 출력된다... 뭐지??


3.Explicit Binding (this가 가리키는 대상을 명시)


this에 바인딩될 객체는 함수 호출 방식에 의해 결정된다.
이는 자바스크립트 엔진이 수행하는 것이다.
이러한 자바스크립트 엔진의 암묵적 this 바인딩 이외에 this를 특정 객체에 명시적으로 바인딩하는 방법도 제공된다.
이것을 가능하게 하는 것이 Function.prototype.apply, Function.prototype.call 메소드이다.
이 메소드들은 모든 함수 객체의 프로토타입 객체인 Function.prototype 객체의 메소드이다.

모든 함수들이 기본적으로 가지고 있는 메소드들이다.

var age = 100;  //let으로 선언될 경우 foo()의 결과는 undefined;

function foo () {
  console.log(this.age);
}

var ken = {
  age: 35
};

var wan = {
  age: 31
};

foo();  // 100 
foo.call(wan); // 31
foo.apply(ken); // 35

let bar = foo.bind(ken)
bar();//35

call()과 apply() 메소드를 사용하여 함수를 실행할 때, this의 값을 명시적으로 설정하는 방법이다.
apply() 메소드를 호출하는 주체는 함수이며 apply() 메소드는 this를 특정 객체에 바인딩할 뿐 본질적인 기능은 함수 호출이라는 것이다.

call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출한다.
따라서 foo.call(wan)의 결과는 31이다.

func.call(thisArg[, arg1[, arg2[, ...]]])

apply() 메소드는 주어진 this 값과 배열 (또는 유사 배열 객체) 로 제공되는 arguments 로 함수를 호출한다. 따라서 foo.apply(ken)의 결과는 35가 된다.

func.apply(thisArg, [argsArray])

apply() 메소드의 대표적인 용도는 arguments 객체와 같은 유사 배열 객체에 배열 메소드를 사용하는 경우이다. arguments 객체는 배열이 아니기 때문에 slice() 같은 배열의 메소드를 사용할 수 없으나 apply() 메소드를 이용하면 가능하다.
Array.prototype.slice.apply(arguments)는 “Array.prototype.slice() 메소드를 호출하라. 단 this는 arguments 객체로 바인딩하라”는 의미가 된다. 결국 Array.prototype.slice() 메소드를 arguments 객체 자신의 메소드인 것처럼 arguments.slice()와 같은 형태로 호출하라는 것이다.

function convertArgsToArray() {
  console.log(arguments);

  // arguments 객체를 배열로 변환
  // slice: 배열의 특정 부분에 대한 복사본을 생성한다.
  let arr = Array.prototype.slice.apply(arguments); // arguments.slice
  // var arr = [].slice.apply(arguments);

  console.log(arr);
  return arr;
}

convertArgsToArray(1, 2, 3);

//output
//[Arguments] { '0': 1, '1': 2, '2': 3 }
//[ 1, 2, 3 ]

call()과 apply()의 차이점은 call() 은 함수에 전달될 인수들의 목록을 받는데 비해, apply() 는 인수들이 담긴 배열을 받는다는 점이다.

ex)
foo.call(wan,1,2,3,4,5)
foo.apply(ken,[1,2,3,4,5])

bind() 메소드도 전달된 첫번째 인자로 this 값과 인수들의 목록을 받는다.
apply()와 call()과의 차이점은 bind() 함수는 새로 바인딩한 함수를 생성한다.
따라서 바로 실행되는 것이 아니다.
즉, Function.prototype.bind는 Function.prototype.apply, Function.prototype.call 메소드와 같이 함수를 실행하지 않기 때문에 명시적으로 함수를 호출할 필요가 있다.
이후에 일반 함수처럼 실행되어도 이미 this값이 설정된 새로운 함수가 생성된 것이기 때문에 window객체를 가리키는 것이 아니다.
즉 호출 방법과 관계없이 특정 this 값으로 호출되는 함수를 새로 만드는 것이다.

MDN bind에 대한 설명)

bind()의 가장 간단한 사용법은 호출 방법과 관계없이 특정 this 값으로 호출되는 함수를 만드는 겁니다. 초보 JavaScript 프로그래머로서 흔한 실수는 객체로부터 메소드를 추출한 뒤 그 함수를 호출할때, 원본 객체가 그 함수의 this로 사용될 것이라 기대하는 겁니다(예시 : 콜백 기반 코드에서 해당 메소드 사용). 그러나 특별한 조치가 없으면, 대부분의 경우 원본 객체는 손실됩니다. 원본 객체가 바인딩 되는 함수를 생성하면, 이러한 문제를 깔끔하게 해결할 수 있습니다.

let age = 100;

function foo () {
  console.log(this.age);
  console.log(arguments);
}

let ken = {
  age: 34
};

let bar = foo.bind(ken);

bar(1, 2, 3, 4, 5);

4.new 키워드를 사용한 함수 실행


MDN : new 연산자는 사용자 정의 객체 타입 또는 내장 객체 타입의 인스턴스를 생성한다.
mdn 설명
ES6 문법에서 우리는 class라는 키워드를 통해 객체를 정의하고 생성자 함수를 작성해주지만,
이전에는 함수를 통해 객체를 정의했고 이를 생성자 함수라고 했다.
그리고 new 생성자 함수()를 호출하여 객체 인스턴스를 생성 했다.
자바스크립트의 생성자 함수는 말 그대로 객체를 생성하는 역할을 한다.

4-1 생성자 함수 동작 방식

1. 빈 객체 생성 및 this 바인딩

생성자 함수의 코드가 실행되기 전 빈 객체가 생성된다. 이 빈 객체가 생성자 함수가 새로 생성하는 객체이다. 이후 생성자 함수 내에서 사용되는 this는 이 빈 객체를 가리킨다. 그리고 생성된 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.
2. this를 통한 프로퍼티 생성
생성된 빈 객체에 this를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다. this는 새로 생성된 객체를 가리키므로 this를 통해 생성한 프로퍼티와 메소드는 새로 생성된 객체에 추가된다.

3. 생성된 객체 반환

반환문이 없는 경우, this에 바인딩된 새로 생성한 객체가 반환된다. 명시적으로 this를 반환하여도 결과는 같다.

객체 리터럴 방식과 생성자 함수 방식의 차이는 프로토타입 객체([[Prototype]])에 있다.
객체 리터럴 방식의 경우, 생성된 객체의 프로토타입 객체는 Object.prototype이다.
생성자 함수 방식의 경우, 생성된 객체의 프로토타입 객체는 Person.prototype이다.

function foo () {
  console.log(this);
}

new foo();

이 예제에서 this값이 가리키는 것이 무엇인지 찾아봐야 할 때 함수가 호출되는 부분을 찾아가야 한다.
그리고 해당 foo함수는 new라는 키워드와 함께 실행되고 있다.
예제는 new 키워드를 통한 함수 실행시 빈 객체가 새로 생성되고, this는 생성된 빈 객체를 가리키게 된다.
(그런데 어떻게 this = {}이 가능한 것인가? => 프로토타입, 프로토타입 체인을 공부하기)

즉, new 키워드를 통해 함수를 실행하면 새로운 객체가 this값으로 할당되어 함수가 실행된다.

function foo () {
  this.name = 'kim';
}

let bar = new foo();

console.log(bar); // ?

new를 통해 foo()라는 함수를 실행하면 this의 값은 새로운 객체를 가리키고
foo라는 함수 내부에서는 this는 새로운 객체를 나타내고, 이 새로운 객체의 name이라는 속성을 만들고 값을 설정해 주는 것이다.

즉 이후 bar가 가리키는 객체는 name 속성을 가진 객체이다.

{
  name : 'kim'
}

new라는 키워드 사용과 함께 this가 가리킬 새로 생성될 객체에 인자를 전달하는 방법이다.

function foo (name) {
  this.name = name;
}

let bar = new foo('김세현');

console.log(bar);

Event Handler의 this


DOM을 통해 엘리먼트들을 조작할 때 해당 이벤트가 발생한 element를 가리킨다.
하지만 이러한 방식으로 event가 발생한 target을 가리키기 위해 this를 사용하는 것은 가독성에 좋지 못할 수 있다.
따라서 상황에 따라 event.target 또는 event.currentTarget를 통해 명시적으로 작성하는 것이 좋다.


질문) this가 무엇인가? 왜 사용하는가?

질문) call(), apply(), bind()는 무엇인가?

함수를 호출할 때 this값을 명시적으로 설정하기 위한 메소드

질문) 각각의 차이는 무엇인가?

call()과 apply()는 첫 번째 인자로 this값을 명시적으로 전달하는 점은 같지만, call()의 두 번째 인자는 값 들의 목록이지만 apply()의 두 번째 인자는 값들이 담긴 배열이다라는 차이가 있다.
bind()는 this 값이 명시적으로 지정된 새로운 함수를 생성하여 반환한다. (두 번째 인자는 값들의 목록) 그리고 이 함수의 실행 방식과 상관없이 지정된 this를 가리키고 있다.
call()과 apply()는 함수를 바로 호출하지만, bind()는 새로 생성된 함수가 반환되기 때문에 바로 호출되는 것이 아니다.

profile
under the hood

0개의 댓글