1ilsang

Developer who will be a Legend

자바스크립트의 this 를 알아보자.

2020-03-31 1ilsangJavaScript

JS 에서 this 는 기존의 OOP(e.x Java)와는 다르게 동작하기 때문에 처음 사용시 큰 혼란을 준다.

대표적인 경우로 자바에서 this 는 인스턴스 자신을 가리키는데 반해 자바스크립트는 this 가 호출 방식에 따라 달라진다.

오늘은 이 예시들을 나열하고 모호했던 개념들을 정리해 보려고 한다.

TL;DR

  1. 기본적으로 this 는 생성자 함수와 객체 메서드를 제외한 모든 함수에서 전역객체(window, global)를 가리킨다.
  2. 화살표 함수와 일반 함수에서 this 의 바인딩이 다르다.
  3. 생성자 함수 및 객체 메서드는 왜 this 가 다를까?
  4. 객체 메서드가 매개변수로 넘어갈 때 주의해야 한다.
  5. 엄격모드(use strict)일 경우 this 의 값이 null 혹은 undefined 여도 전역객체를 바라보지 않는다.

this 가 어떻게 된다는 건가요?

아래의 코드를 보면서 this 가 변하는 과정들을 살펴보자.

(각 화살표를 클릭해 주세요)

기본 문제
var devVar = `functionScope`;
let dev = `dev!`;

function ilsang() {
  console.log(this); // window
  console.log(this.devVar); // functionScope
  console.log(this.dev); // undefined
}

function ilsang2() {
  this.dev = `dev2`; // window.dev = `dev2`;
  console.log(this); // window
  console.log(this.dev); // dev2
}

const ilsang3 = {
  dev: `dev3`,
  getThis() {
    console.log(this); // ilsang3
    console.log(this.devVar); // undefined
    console.log(this.dev); // dev3
  },
  getThis2: () => {
    console.log(this); // window
    console.log(this.devVar); // functionScope
    console.log(this.dev); // dev2 (ilsang2 함수에서 업데이트 했으므로)
  }
};

ilsang();
ilsang2();
console.log(dev); // dev!
console.log(this.dev); // dev2;
console.log(devVar); // functionScope
console.log(this.devVar); // functionScope
console.log(ilsang3.getThis());
console.log(ilsang3.getThis2());
이벤트 리스너와 바인드
///////////
const button = document.createElement(`button`);
button.innerHTML = `WTF`;
button.addEventListener(`click`, function(event) {
  console.log(this); // button
  console.log(this.innerHTML); // WTF
  console.log(event); // mouseEvent
});

button.addEventListener(`click`, event => {
  console.log(this); // window
  console.log(this.innerHTML); // undefined
  console.log(event); // mouseEvent
});

button.click();
//////
function ilsang() {
  console.log(this.dev); // WTF
  return {
    dev: `hit`,
    log: function() {
      console.log(this.dev); // hit
      console.log(dev); // ReferenceError: b is not defined
    }
  };
}
ilsang.call({ dev: `WTF` }).log();

function ilsang() {
  console.log(this.dev); // WTF
  return {
    dev: `hit`,
    log: () => {
      console.log(this.dev); // WTF
      console.log(dev); // ReferenceError: b is not defined
    }
  };
}
ilsang.call({ dev: `WTF` }).log();
함수와 클래스
///////////
var a = 1;
let c = 3;

function test() {
  let a = 100;
  var b = 2;
  console.log(a, this.a); // 100 1
  console.log(b, this.b); // 2 undefined
  console.log(c, this.c); // 3 undefined

  function test2() {
    console.log(b, this.b); // undefined undefined
    var b = 20;
    console.log(a, this.a); // 100 1
    a += 10;
    console.log(a, this.a); // 110 1

    this.b = 999;
    console.log(b, this.b); // 20 999
    console.log(c, this.c); // 3 undefined
  }

  const test3 = () => {
    console.log(a, this.a); // 110 1
    a += 10;
    console.log(a, this.a); // 120 1
    console.log(b, this.b); // 2 999
    console.log(c, this.c); // 3 undefined
  };

  test2();
  test3();
}

test();
console.log(this.b); // 999

//////
function Test() {
  this.a = 1;
}
Test.prototype.getFnThis = function() {
  console.log(this);
  return this;
};
Test.prototype.getArrowThis = () => {
  console.log(this);
  return this;
};
Test.prototype.getFn = function() {
  return this.a;
};
Test.prototype.getArrow = () => {
  return this.a;
};
Test.prototype.consoleFn = function() {
  console.log(this.a);
};
Test.prototype.consoleArrow = () => {
  console.log(this.a);
};

const test = new Test();
const fnThis = test.getFnThis(); // Test
const arrowThis = test.getArrowThis(); // window
console.log(fnThis.a, arrowThis.a); // 1 undefined

const test2 = {
  a: 2
};
test2.consoleFn = test.consoleFn;
test2.consoleArrow = test.consoleArrow;

console.log(test.consoleFn(), test.consoleArrow()); // 1 undefined
console.log(test2.consoleFn(), test2.consoleArrow()); // 2 undefined

//////
class Test {
  constructor() {
    let a = 10;
    this.a++;
    this.b = 20;
  }

  console() {
    let c = 10;
    console.log(this.a); // NaN
    console.log(this.c, c); // undefined 10
    console.log(this.b); // 20
    console.log(b); // ReferenceError: b is not defined
  }
}

const test = new Test();
test.console();
객체와 메소드
/////
const ilsang2 = {
  dev: `dev!`,
  devArray() {
    const dev2 = 22222;

    const a = () => console.log(this.dev, this.dev2); // dev! undefined
    const b = function() {
      console.log(this.dev, this.dev2); // undefined undefined
    };
  },
  devArray2: () => {
    const dev2 = 2222;
    const a = () => console.log(this.dev, this.dev2); // undefined undefined
    const b = function() {
      console.log(this.dev, this.dev2); // undefined undefined
    };
  }
};

//////
function Ilsang(dev) {
  this.dev = dev;
}

Ilsang.prototype.devArray = arr => {
  console.log(this.dev); // undefined
  return arr.map(e => this.dev + e); // undefined + e
};

Ilsang.prototype.devArray2 = function(arr) {
  console.log(this.dev); // 'dev!'
  return arr.map(e => this.dev + e); // dev! + e
};

const ilsang = new Ilsang(`dev!`);
console.log(ilsang.devArray([`1ilsang`, `dev`])); // ["undefined1ilsang", "undefineddev"]
console.log(ilsang.devArray2([`1ilsang`, `dev`])); // ["dev!1ilsang", "dev!dev"]
엄격 모드(use strict)
////////
`use strict`;

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

ilsang(); // undefined -> window 가 아니다!
ilsang.call(2); // 2
ilsang.apply(null); // null -> window 가 아니다!
ilsang.call(undefined); // undefined -> window 가 아니다!
ilsang.bind(true)(); // true

////////////
function Ilsang() {
  this.dev = `log`;
  this.console = function() {
    console.log(this.dev);
  };
}

const ilsang = new Ilsang();
const ilsangConsole = ilsang.console;

ilsang.console(); // log
ilsangConsole(); // undefined

표현 방법이 조금씩 다를 뿐이지 자세히 살펴보면 몇가지 원칙만 이해하고 있으면 모두 똑같다는 것을 알 수 있다.

위의 내용들을 정확하게 이해하고 있다면 아래의 글은 보지 않아도 된다.

this 는 동적이다.

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

ilsang(); // window
new ilsang(); // ilsang

위의 함수에서 new 의 유무에 따라 this 가 달라지는 것을 알 수 있다.

기본적으로 this 는 생성자 함수와 객체 메서드를 제외한 모든 함수에서 전역객체(window, global)를 가리킨다.

그러므로 일반 함수 실행의 경우인 ilsang()window를 가리키고 생성자 함수 실행인 new ilsang() 에서는 자기자신을 가리키게 된다.

이게 핵심이므로 꼭 외워두자.

  • 기본적으로 this 는 생성자 함수와 객체 메서드를 제외한 모든 함수에서 전역객체(window, global)를 가리킨다.

화살표 함수와 일반 함수에서의 this

Arrow function 은 thisarguments를 바인딩하지 않는다. 화살표 함수는 기존의 동적인 일반 함수와 달리 정적으로 this 를 선언과 동시에 바인딩 해버린다.

조금 어렵게, 정확하게 이야기 하자면 현재 화살표 함수를 둘러싼 Lexical scope 를 this 에 바인딩 한다.

한번더 강조하면 화살표 함수는 선언과 동시에 this 가 정해진다(Lexical this).

그러므로 화살표 함수는 bind 함수를 통해 this 를 변경할 수 없다!!!

// 일반 함수
function Ilsang(dev) {
  this.dev = dev;
}

Ilsang.prototype.devArray = function(arr) {
  console.log(this.dev); // 'dev!'
  return arr.map(function(e) {
    return this.dev + e; // undefined + e
  });
};

const ilsang = new Ilsang(`dev!`);
console.log(ilsang.devArray([`1ilsang`, `dev`])); // ["undefined1ilsang", "undefineddev"]

//////////////////////////////////////////
// 화살표 함수
function Ilsang(dev) {
  this.dev = dev;
}

Ilsang.prototype.devArray = arr => {
  console.log(this.dev); // undefined
  return arr.map(e => this.dev + e); // undefined + e
};

Ilsang.prototype.devArray2 = function(arr) {
  console.log(this.dev); // 'dev!'
  return arr.map(e => this.dev + e); // dev! + e
};

const ilsang = new Ilsang(`dev!`);
console.log(ilsang.devArray([`1ilsang`, `dev`])); // ["undefined1ilsang", "undefineddev"]
console.log(ilsang.devArray2([`1ilsang`, `dev`])); // ["dev!1ilsang", "dev!dev"]

위의 예에서 보이는 대로 메서드에 화살표 함수를 사용하게 될 경우 this 로 접근하게 되면 undefined 가 떨어지는 것을 알 수 있다.

조금 더 쉽게 이해할 수 있도록 인스턴스 내에 달아보자.(prototype 과 객체 내에 달아주는 것의 차이는 참조로 인한 메모리의 차이가 있다.)

// this === window
const Ilsang = {
  getThis: this, // window
  dev: `dev!`,
  devArray2: arr => {
    this.dev; // undefined
    return arr.map(e => this.dev + e); // undefined + e
  }
};

객체의 this 가 바인딩 되는 것이 아닌 window 글로벌 객체가 바인딩 된다.

맨 처음에 이야기 했던 기본적으로 this 는 생성자 함수와 객체 메서드를 제외한 모든 함수에서 전역객체(window, global)를 가리킨다. 를 떠올려보자.

위에 따라 Ilsang 객체 내부의 this 는 window 를 가리킨다.(생성자 함수와 객체의 차이는 아래에서 풀겠다.) 따라서 this 의 키값에 접근하면 undefined 가 리턴 된다.

그러므로 메서드로 화살표 함수를 사용하게 될 경우 아래와 같이 해야한다.

const Ilsang = {
  getThis: this, // window
  dev: `dev!`,
  devArray2(arr) {
    // devArray2: function(arr) { } 과 동일
    this; // Ilsang
    this.dev; // dev!
    return arr.map(e => this.dev + e); // dev! + e
  }
};

객체 메서드로 작성하게 될 경우 this 가 현재 객체에 묶이게 되므로 정상적으로 객체의 프로퍼티 값을 가져올 수 있게 된다.

devArray2 는 객체 메서드이므로 thisIlsang 객체를 바라보게 된다. 그러므로 this.dev 로 접근할 수 있게 된 상태다.

이 상태에서 arr.map(e => this.dev + e) 를 하게 되면 화살표 함수는 자신을 둘러싼 외부의 scope 를 정적으로 가리키게 되므로 이미 정의된 this 인 Ilsang 의 값을 그대로 사용할 수 있게 되는 것이다.

반대적 예를 보자.

const Ilsang = {
  dev: `dev!`,
  devArray2(arr) {
    this.dev;
    return arr.map(function(e) {
      console.log(this); // window
      return this.dev + e; // undefined + e
    });
  }
};

map 함수에 기본 함수로 접근하게 될 경우 내부 함수이므로

[this 는 생성자 함수와 객체 메서드를 제외한 모든 함수에서 전역객체(window, global)를 가리킨다.] 의 조건에 의해 글로벌 window 를 가리키게 되고 undefined 가 떨어지게 된다.


EventListener 과 bind

eventListener에 화살표 함수를 사용하면 this 가 달라진다.

Arrow function 은 thisarguments를 바인딩하지 않는다. 화살표 함수는 기존의 동적인 함수와 달리 정적으로 this 를 선언과 동시에 바인딩 해버린다.

정적으로 this 를 선언한다고 했으니 이후의 bind 는 불가능하다.

const button = document.createElement(`button`);
button.addEventListener('click', () => {
  console.log(this === window); // => true
  console.log(this === button); // => false
});

///////////
const button = document.getElementById(`button`);
button.addEventListener('click', function() {
  console.log(this === window); // => false
  console.log(this === button); // => true
});

eventListener 은 내부적으로 this 를 해당 엘리먼트에 bind 하게 된다.

따라서 화살표 함수로 콜백을 던지면 this 가 선언될 때의 상태인 전역 스코프를 바라보게 되는 반면 일반 함수는 바인드된 값을 바라보기 때문에 정상적으로 button 을 가리키게 된다.

직접 바인드를 해보자.

const ilsang = () => console.log(this, this.dev);
const ilsang2 = function() {
  console.log(this, this.dev);
};

const log = ilsang.bind({ dev: `log` });
const log2 = ilsang2.bind({ dev: `log` });

log(); // window undefined
log2(); // {dev: `log`} log

화살표 함수의 경우 bind 하여도 값이 변하지 않는 것을 알 수 있다.

그러므로 기본적으로 bind 를 내장하고 있는 함수에서 화살표 함수를 사용하게 되면 this 가 먹히지 않는다.


생성자 함수와 객체 메서드에서는 왜 this 가 변하는 걸까?

function Ilsang() {
  const a = `dev`;
  this.b = `dev`;
}

Ilsang.prototype.console = function() {
  console.log(this.a, this.b);
};

const ilsang = new Ilsang();

위와 같은 코드에서 객체를 생성하게 될 경우

  1. 빈 객체를 만든다.
{
}
  1. 프로퍼티 값들을 넣어준다.
{
  b: undefined,
  console: undefined
}

이때 this 는 생성된 자신. 객체 인스턴스를 가리키게 된다.

a 가 사라진 것을 볼 수 있다! a 는 this 값이 아니므로 객체 생성에서 사라진다.

  1. 프로퍼티 값을 할당한다.
{
  b: `dev`;
  console: function Reference // prototype 으로 주었기 때문에 주소 복사
}
  1. 변수에 객체를 할당한다.
const ilsang = { ... }

물론 훨씬 더 많은 세부적인 과정이 있지만(Prototype, defineProperty 등) 그건 생성자 함수를 따로 다룰 때 더 깊게 정리해보겠다.

어쨌든 생성자 함수에서 빈 객체를 새로 생성하고 this 를 넣어주기 때문에 this 가 해당 객체를 가리키게 된다.

그렇다면 객체 메소드에서는 왜 this 가 바인딩 되는 것일까?

  • 일반적인 함수 실행과 메소드 실행은 다르다! 이 둘은 서로 다른 타입이다.

둘의 가장 큰 차이점은 속성 접근자의 유무이다.

  • 메소드 실행의 경우 속성 접근자를 사용해 functionProperty 를 호출한다.
  • 함수 실행의 경우 속성 접근자를 사용하지 않고 바로 호출한다.

아래의 코드를 보자.

function Ilsang() {
  this.dev = `log`;
  this.console = function() {
    console.log(this.dev);
  };
}

const ilsang = new Ilsang();
const ilsangConsole = ilsang.console;

ilsang.console(); // log
ilsangConsole(); // undefined

Function property

메소드 실행인 ilsang.console()은 속성 접근자를 사용하므로 ilsang 인스턴스 객체의 this 를 참조하게 된다.

하지만 ilsangConsole() 과 같은 일반 함수 호출의 경우 글로벌 환경을 바라보게 되므로 undefined가 나오게 되는 것이다.

위의 예시를 조금 더 꼬아보면 또 주의해야 할 점이 나온다.

객체 메서드가 매개변수로 넘어갈 때 주의해야 한다.

콜백 등의 내부 함수 요청을 할때 객체 메서드가 전역 this 를 바라보는 현상이다.

function Ilsang() {
  this.dev = `log`;
  this.console = function() {
    console.log(this.dev);
  };
}

const ilsang = new Ilsang();
const ilsangConsole = ilsang.console;

ilsang.console(); // log
ilsangConsole(); // undefined

setTimeout(ilsang.console, 1000); // undefined
setTimeout(() => ilsang.console(), 1000); // log

사실 위 이슈는 계속 언급한 기본적으로 this 는 생성자 함수와 객체 메서드를 제외한 모든 함수에서 전역객체(window, global)를 가리킨다. 를 생각해보면 당연한 예제다.

setTimeout의 콜백인자로 ilsang.console을 넘겼는데, 이건 위의 코드에서 const 로 선언해준 ilsangConsole을 넘기는 것과 동일한 행위다.

그러므로 당연히 undefined 가 떨어지게 되는 것이다.


use strict 엄격모드 일 경우 전역 객체로 값을 할당해 주지 않는다.

`use strict`;

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

ilsang(); // undefined -> window 가 아니다!
ilsang.call(2); // 2
ilsang.apply(null); // null -> window 가 아니다!
ilsang.call(undefined); // undefined -> window 가 아니다!
ilsang.bind(true)(); // true

엄격 모드는 자바스크립트가 자동으로 해버리는 모호한 점들을 막는다.

엄격 모드의 여러 기능들 중 하나가 this 스코프를 자동으로 지정하게 하지 않는 것이다.

따라서 만약 undefined 혹은 null 일 경우 window 와 같은 글로벌 스코프로 지정하는 것이 아닌 그 값 그대로 놔둬버린다.

그럼 이만!

참고