[JavaScript] this 알아보기

bbio3o·5일 전
0

JavaScript

목록 보기
9/11
post-thumbnail

출처1 출처2 출처3 출처4

위의 출처들을 통해 정리한 내용입니다.

📌 this란?

자바스크립트는 기본적으로 변수나 함수가 선언되었을 때 스코프가 결정되는 정적 스코프 방식(lexical scope) 을 따릅니다. 하지만 this는 전역 스크립트가 실행되거나 함수가 호출될 때 JS 내부 규칙에 따라서 동적으로 결정되기 때문에 헷갈리게 되는 것이죠.

this란 일반적으로 메서드를 호출한 객체가 저장되어 있는 속성 입니다.
이 this는 호출하는 방법에 따라, 호출하는 객체가 this가 됩니다.
'this' 값은 'this'를 사용하는 해당 함수를 "어떻게" 실행하느냐에 따라 this의 값이 바뀝니다.

  1. 할머니: 나는 허리가 아프다 (나 === 할머니)
  2. 아버지: 나는 다리가 아프다 (나 === 아버지)
  3. 어머니: 나는 머리가 아프다 (나 === 어머니)

자바스크립트에서 this란 위의 문장들에서 나타난 '나' 라는 단어와 비슷합니다. 어떤 문맥이냐에 따라 그 의미 (값)이 바뀝니다.

this가 만들어지는 경우는 여러가지 경우들이 있습니다.

**1. 일반 함수에서의 this

  1. 메서드에서의 this
  2. call(), apply(), bind() 메서드에서의 this
  3. new 키워드에서의 this
  4. 중첩함수의 this
  5. 이벤트리스너에서의 this**

순으로 알아보겠습니다.


📌 1. 일반 함수에서의 this (Regualar function call)

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

foo();

일반함수에서 this는 global object이며 이 자바스크립트를 브라우저에서 실행시키면 window 객체라 할 수 있습니다.


'use strict';

var name = 'ken';

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

foo();

특이케이스로는 ES5에서 추가된 엄격한 자바스크립트 실행환경을 제공하는 'use strict' 모드를 사용하는 경우입니다.
'use strict' 모드에서 this는 undefined가 됩니다.
위의 예제에서는 this.name은 오류를 야기하며 this가 아닌 window.name을 써 의도를 더 명확하게 보여주는 코드를 작성해야 한다 볼 수 있습니다.


일반함수의 다른 예제를 살펴보겠습니다.

var age = 100;

function foo() {
  var age = 99;
  bar(); // bar(); 일반 함수로 호출 되는 것에 주목
}

function bar() {
  console.log(this.age) // 100
}

foo();

📌 2. 메서드에서의 this (Dot Notation .)

예제1

var age = 100;

var ken = {
  age: 35,
  foo: function foo() {
    console.log(this.age) // 여기서 this.age는 ken.age로 볼 수 있다. 35가 출력
  }  
};

ken.foo();

예제2

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

var age = 100;

var ken = {
  age: 35,
  foo: foo
}

var wan = {
  age: 31,
  foo: foo
}

ken.foo(); // 35
wan.foo(); // 31

예제3

var age = 100;

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

var wan = {
  age: 31,
  foo: ken.foo
}

ken.foo(); // 35
wan.foo(); // 31
foo(); // what is the value of 'this' in this case?

예제3의 foo();의 결과값은 어떻게 될까요? 답은 foo()는 일반함수로서 호출 되었기 때문에 window.age로 볼 수 있어 100이 출력됩니다.
위의 예제를 통해 알 수 있듯이 어떤 오브젝트의 메소드로 함수가 실행되는 경우 this는 메서드를 호출한 객체가 됩니다.


📌 3. Function.prototype.call(), Function.prototype.apply(), Function.prototype.bind()

call(), apply(), bind() 메소드를 이용해서 함수를 호출하는 경우를 살펴보겠습니다.
세가지 함수는 모두 함수의 프로토타입이 가지고 있는 함수들 입니다. Function.prototype.call, Function.prototype.apply, Function.prototype.bind 에요. 말 그대로 자바 스크립트의 함수라면 모두 이 함수를 사용할 수 있다는 것이죠!
예제들을 살펴보겠습니다.

var age = 100;

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

var ken = {
  age: 35
}

var wan = {
  age: 31
}

foo(); // window.age로 볼 수 있다.100

foo.call(wan); // wan.age로 볼 수 있다. 31
foo.apply(ken); // ken.age로 볼 수 있다. 35

call(), apply(), bind() 메서드를 통해 함수가 어떻게 호출되었는지 개의치 않고 this 값을 우리가 직접 설정할 수 있습니다.


call(), apply(), bind() 메서드를 좀 더 알아보기 위해 다른 예제들을 살펴보겠습니다.

var age = 100;

function foo(a,b,c,d,e) {
  console.log(this.age);
  console.log(arguments);
}

var ken = {
  age: 35
}

foo.call(ken, 1, 2, 3, 4, 5); // 35를 출력 , [Arguments Object] {0: 1, 1: 2, 2: 3, 3: 4, 4: 5} 유사배열 Arguments출력
foo.apply(ken, [ 1,2,3,4,5 ]); // 35를 출력 , [Arguments Object] {0: 1, 1: 2, 2: 3, 3: 4, 4: 5} 유사배열 Arguments출력

✨ func.call(thisArg[, arg1[, arg2[, …]]]) 알아보기

foo.call(ken, 1, 2, 3, 4, 5);을 먼저 살펴보면
this.age는 ken.age 즉 35이며
2번째 인자부터 마지막 인자 까지가 foo()함수의 인자 a,b,c,d,e라는 인자가 담겨있는 유사배열arguments를 출력합니다.

이런 call()을 어디에 사용할 수 있을까요?

call() 예제1 다른 객체의 메서드를 나의 메서드인 것 처럼 사용하기

let obj1 = {
    name : 'michelle',
    speakMyname() {
        console.log(`My name is ${this.name}!`);
    }
}

let obj2 = {
    name : 'frank'
}

obj1.speakMyname(); // My name is michelle!
obj2.speakMyname(); // obj2.speakMyname is not a function
obj1.speakMyname.call(obj2); // My name is frank!

obj2는 speakMyname()가 없습니다. 하지만 obj1에 있는 speakMyname()을 호출할 때, call()을 용하여 첫 번재 매개변수로 obj2를 넘겨주면 함수가 정상적으로 실행됩니다.
이처럼 call()을 사용하면 다른 객체의 메서드를 자신의 메서드인 것처럼 사용할 수 있게 된다는 것을 기억하시면 됩니다.

call() 예제2 생성자 내부에서 사용하여, 다른 생성자 함수의 멤버를 가져오기

const Util = function() {
	this.getName = function() {
		return this.name;
	}

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

class Car {
	constructor(name, price) {
        Util.call(this);
        this.name = name;
        this.price = price;
	}
}

const mycar = new Car('ford', 999)

mycar.getName(); // ford
mycar.setName('BMW');
mycar.getName(); // BMW

Car 클래스의 생성자 함수를 살펴보세요. 생성자 내부에서 Util.call(this)로 전달했을 때, 이 this는 Car 클래스로 만들어진 인스턴스 입니다.
이렇게 this로 전달된 인스턴스는 Util 생성자 함수가 가지고 있는 멤버(변수와 함수)를 자신의 멤버로 가져올 수 있습니다.

call() 예제3 내부함수의 this가 상위 스코프를 가리키도록 변경하기

class Flower{
	constructor(varieties){
		this.varieties = varieties;
		this.height = 0;
	}

	water (ml) {

		function grow(){
			this.height += ml;
			console.log(`꽃에 ${ml}만큼 물을 주어, 꽃의 높이가 ${this.height} 되었습니다.`)
		}

        //grow(); -> Cannot read property 'height' of undefined
		grow.call(this);
	}
}

const rose = new Flower('ROSE');
rose.water(100);

water()는 rose 인스턴스에서 호출한 메서드이기 때문에 this는 rose에 바인딩 됩니다.
하지만 rose.water() 함수에서 선언된 grow()는 내부 함수이기 때문에 this는 Window 객체에 바인딩 되어, rose 인스턴스의 height에 접근할 수 없게 되는 것이죠.
grow.call(this)을 이용하여 this에 바인딩 되는 객체를 rose 인스턴스로 변경해 주어야 합니다.

지금은 내부 함수를 예로 들었지만, 콜백 함수로 넘겨진 함수의 this도 Window 객체에 바인딩 되는 대표적인 예시입니다. 추가적인 활용법을 꼭 살펴보세요!

call() 예시4 this와 함께 매개변수 넘겨주기

const public = {
    getIdentificationNumber(birthYear, birthMonthandDay){
        birthYear = birthYear.slice(2, birthYear.length)
        return birthYear.concat(birthMonthandDay)
    }
}

const person = {
    name : 'michelle'
}

public.getIdentificationNumber.call(person, '1990', '1225'); // '901225'

call을 사용하여 호출할 함수가 매개 변수를 받는다면, this를 변경할 객체와 함께 필요한 매개변수를 넘겨줘야 합니다. getIdentificationNumber()는 태어난 년도와 태어난 날짜가 필요한 함수이기 때문에 두 개의 인자를 넘겨주었습니다.

✨ func.apply(thisArg, [argsArray]) 알아보기

apply()는 앞서 살펴본 call()과 기능이 동일합니다.
하나의 차이점은 인자를 전달하는 방식 입니다. call()은 인자를 하나 전달하는 반면,
apply()는 각 인자들을 배열로 만들어서 두번쨰 인자로 전달합니다.
예시를 살펴보겠습니다.

const public = {
    getIdentificationNumber(inputData){
        const birthYear = inputData[0].slice(2, inputData[0].length);
		const birthMonthandDay = inputData[1];
        return birthYear.concat(birthMonthandDay)

    }
}

const person = {
    name : 'michelle'
}

public.getIdentificationNumber.call(person, ['1990', '1225']);

call()의 example4에서 실행한 코드를 apply() 버젼으로 변경하였습니다. 차이점을 확인해보세요.


✨ func.bind(thisArg[, arg1[, arg2[, …]]]) 알아보기

var age = 100;

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

var ken = {
  age: 34
}

var bar = foo.bind(ken);

bar(); // 34

bind() 또한 call()과 apply()처럼 함수 내부에서 this가 바인딩 되는 객체를 변경합니다.
하지만 call()과 apply()는 this가 바인딩 되는 객체를 변경한 후 함수를 실행하는 반면에, bind()는 this가 바인딩 되는 객체만 변경하고 함수를 실행하지 않습니다.
그 대신 변경된 새로운 함수를 반환합니다.

foo.bind(ken)을 호출 했을 때는 함수가 실행되지 않고, 반환되는 함수를 변수 안에 넣은 뒤에 호출해야 실행되는 것을 볼 수 있습니다.


📌 4. new 키워드

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

new foo();

위의 예제에서 foo() 앞에 new 키워드를 볼 수 있습니다. 결론부터 말하자면 위와 같은 예제처럼 new 키워드가 붙으면 this는 빈 객체가 생성 됩니다. (이해하기 쉽게 new라는 키워드가 붙으면 새로운 new 객체가 생성된다고 생각해 볼 수 있습니다.)

function foo() {
  // this = {};
  this.name = '세상아 안녕!';
  
  // this = { name: '세상아 안녕!'};
  // return this;
}

var helloWorld = new foo();

console.log(helloWorld); // foo {name: '세상아 안녕!'}

this={} 빈 객체 였던 this에 function foo() 내부에서 name이라는 속성이 추가되어
this는 name을 갖게됩니다.

지금 function foo()에서는 어떤한 값도 return 하고 있지 않습니다. 하지만 new키워드를 쓰게 되면, 맨 처음 this에 빈 객체가 할당되고 return 값을 직접 써주지 않아도 this를 return하게 됩니다.

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

var helloWorld = new foo('세상아 안녕!');

console.log(helloWorld); // foo {name: '세상아 안녕!'}

함수에 인자를 넣어도 똑같이 작동합니다.
다른 예제를 좀 더 살펴보겠습니다.

// A function used with 'new' keyword: Constructor function(생성자 함수)
// -Usually capitalized 
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// Called 'instances'
var john = new Person('john', 34) // 생성자함수
var smith = new Person('sminth', 30)

console.log(john); //  Person {name: 'john', age: 34}
console.log(smith); // Person {name: 'smith', age: 30}

보통 new를 써서 실행하게 되는 함수들을 생성자 함수(Constructor function) 라고 합니다.
일반적으로 생성자 함수들은 Person과 같이 첫글자를 대문자로 써줍니다.
Person 생성자 함수 내의 this.name = name 보면,
this에 빈 객체가 생성되고 이 안에 name을 할당합니다.
age도 마찬가지로 작동되며 변수 john에는 john이라는 name 속성과 34라는 age속성이 할당됩니다.
변수 smith도 같은 원리로 작동되어 콘솔에 Person {name: 'smith', age: 30}을 출력하게 됩니다.
이렇게 새로 만들어진 객체들을 인스턴스(instance) 라고 부르며
결국 new를 써서 함수를 실행시킬 때 마다 this에는 새로운 객체가 할당되어 실행되는 것 입니다.


📌 5. 중첩함수의 this

결론부터 말하자면 일반 함수의 중첨함수, 메서드 내부의 중첩 함수 모두 this는 window 입니다.

var obj = { 
  func1 : function(){ 
    console.log(this); // obj가 기록된다 
    // 중첩된 내부 함수정의 
    var func2 = function() { 
      console.log(this); // window 가 기록되며, 여기서부터는 this의 값이 계속 window이다. 
      // 중첩된 함수를 또 정의 
      var func3 = function() { 
        console.log(this); // window 가 기록 
      }(); 
    }(); 
  } 
}; 

obj.func1();

이렇게 중첩된 함수에서의 this는 함수를 포함한 객체, 호출된 컨텍스트에 따르는 것이 아니라 글로벌 객체를 참조하게 됩니다.

그렇다면 부모함수의 this에 대한 참조를 유지할 수는 없는 것일까요?

중첩된 함수 문제는 스코프 체인을 사용해 부모함수의 this에 대한 참조를 저장해 두면 this의 값이 사라지는 것을 막을 수가 있습니다.

다음 코드는 that 변수와 이 변수의 스코프를 사용해 함수 컨텍스트가 변하는 와중에도 this 값을 유지하고 있습니다.

var obj = { 
  prop : 'minji', 
  func1 : function(){ 
    var that = this; // this에 대한 참조인 obj를 func1 함수 스코프에 저장해 둔다. 
    // 중첩된 내부 함수정의
    var func2 = function() { 
      // that(=this)의 스코프 체인을 통해 'minji'를 기록한다. 
      console.log(that.prop); 
      
      // 중첩된 함수를 또 정의 
      var func3 = function() { 
        console.log(that); // obj 를 참조한다. 
        console.log(this); // window 가 기록   
      }(); 
    }(); 
  } 
}; 

obj.func1();

📌 6. 이벤트 리스너에서의 this

이벤트 리스너에서의 this는 조금 특이합니다.
예제를 살펴보겠습니다.

let element = document.querySelector('#hello');

element.addEventListener('click', function onClick (event) {
   console.log(this);
   console.log(event.target);
   console.log(event.currentTarget);
})

currentTarget : 이벤트 리스너가 달린 요소
target : 실제 이벤트가 발생하는 요소

event handler 내에서 this를 쓰는 경우는 흔치 않습니다.
문맥이나 함수를 어떻게 쓰느냐에 따라 바뀌는 this이기 때문에 element, target, currentTarget을 가르키키 위해 this를 쓰는 것은 많은 개발자들과 협업할 때 헷갈릴 수 있기 때문입니다.
따라서 더 직관적으로 element, target, currentTarget이라 명시해 주는 것이 좋아보입니다.

profile
그림도 그리는 개발자 🎨👩‍💻

0개의 댓글