logo
PostsInvestingHomebarArticlesAbout

Javascript 이해하기 / 2.This

2019.01.19 / 14min

Input

Variable Object에 이어서 세번째 주제인 this에 대해서 알아보자.
여러 사례에서 보듯이, this는 어려워서 종종 다른 실행 컨텍스트의 this 값을 처리할 때 이슈를 만든다.


TOC

Definition

this는 Excution Context의 Property.
Context 코드를 실행하는 특수한 객체다.

ActiveExecutionContext
activeExecutionContext = {
  VO: {...},
  this: thisValue // 다른 하나가 더 있다. + scope => 총 3개다.
};

VO : 변수 객체(Variable Object)를 의미한다.
this: Context 실행 코드 타입과 직접적인 연관이 있다.

this는 Context로 진입하는 과정에서 정해지며, Context안의 코드가 실행 중에는 변하지 않는다.


This value in the global code

전역 코드 안의 this(This value in the global code)는 항상 자신(global)이다.

즉, Brower에서는 window Node에서는 global이다.

// 명시적인 전역 객체 프로퍼티 정의
this.a = 10; // global.a = 10
alert(a); // 10

// 규정되지 않은 식별자 할당을 이용한 암묵적 정의
b = 20;
alert(this.b); // 20

// 전역 Context의 변수 객체(VO)는 전역 객체 자신이기 때문에
// 또한 변수 선언을 이용한 암묵적 정의도 가능하다.
var c = 30;
alert(this.c); // 30

This value in the function code

이 경우가 많은 이슈를 낳고있다.
함수 타입의 this는 함수에 정적으로 바인딩이 되지 않는다.

해당 this는 Context로 들어갈때 정해지며, 함수 코드의 this는 바뀔 수 있다.

이렇게 바껴버리니 개발자 입장에서는 매순간 긴장을 할 수밖에 없다.

그러나 runtime의 this는 변경이 이루어질 수 없다. 즉 this를 재할당하는 것이 불가능하다.

let foo = { x: 10 };
let bar = { 
  x: 20, 
  test: function () {   
    console.log(this === bar); // true
    console.log(this.x); // 20 

    this = foo; // Uncaught SyntaxError: Invalid left-hand side in assignment

    console.log(this.x); // 출력안됨
  }
};

// Context로 들어올 때 this가 가리키는 대상이 bar 객체로 결정된다.
// 왜 그러한지는 아래에서 자세하게 설명하겠다.
bar.test(); // true, 20
foo.test = bar.test;

// 그러나 여기의 this는 이제 foo를 참조할 것이다.  
// 심지어 같은 함수를 호출하는 데도 말이다.
foo.test(); // false, 10

그렇다면 어떻게 this가 바뀌는 것일까?
Context의 코드를 활성화시킨 호출자(caller)에 의해 결정된다.

정확하게 함수를 호출하는 방법이 Context의 this에 영향을 준다.

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

foo(); // global
console.log(foo === foo.prototype.constructor); // true
foo.prototype.constructor(); // foo.prototype

위의 의미가 처음에는 무슨 의미인지 몰랐다. 그러나 foo === foo.prototype.constructor 이 부분을 먼저 보면 전역함수인 foofoo.prototype.constructor가 같으므로 둘 다 전역이라고 생각할 수 있다.

foo.prototype.constructor를 실행하게 되면, thisglobal이 되는 것이 맞다.

그런데 실질적인 결과는 다르다! 이것과 유사하게 어떤 객체의 메서드로 정의된 함수를 호출하는 경우에 있어서도 this가 달라질 수 있다.

const foo = {
  bar: function () {   
    console.log(this);   
    console.log(this === foo);
  }
};

foo.bar(); // foo, true

const exampleFunc = foo.bar;
console.log(exampleFunc === foo.bar); // true

exampleFunc(); // global, false

위의 경우도 동일하다.
exampleFunc === foo.bar의 결과가 같다면 this는 같은 것이 나올거라 일반적으로 생각할 수 있다. 결과를 보면 놀랍게 global이 된다.

그렇다면 호출 표현식의 형태가 어떻게 this에 영향을 미칠까?

this가 갖는 값을 결정하는 과정을 완벽하게 이해하기 위해서, 내부 타입 중에 하나인 Reference type에 대해서 자세하게 알 필요가 있다.


Reference type

Reference type은 pseudo-code를 이용해서 base(프로퍼티가 속해 있는 객체)와 base안에 있는 propertyName이라는 2개의 프로퍼티를 갖고 있는 객체로 나타낼 수 있다.

var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
};

Reference type의 값은 오직 아래의 2가지 경우에만 있을 수 있다.
개인적으로 아래에 2가지 경우가 this를 이해하는데 핵심이다.

  1. 식별자(identifier)를 다룰 때
  2. 프로퍼티 접근자(property accessor)를 다룰 때.

식별자는 알고리즘이 항상 Reference type 값(this와 관련해서 중요하다)을 결과로 돌려준다는 것만 명심하자.

식별자는 변수이름, 함수이름, 함수 전달인자의 이름 그리고 전역 객체의 비정규화 프로퍼티의 이름을 뜻한다.

var foo = 10; // 변수이름
function bar() {} // 함수이름

중간결과는 아래와 같이 된다.

var fooReference = {
  base: global,
  propertyName: 'foo'
};

var barReference = {
  base: global,
  propertyName: 'bar'
};

Reference type 값으로부터 객체가 갖고 있는 실제 값을 얻기 위해 쓰이는 GetValue 메서드가 있는데 이 메서드를 pseudo-code로 아래와 같이 나타낼 수 있다.

function GetValue(value) {
  if (Type(value) != Reference) {   
    return value; // 레퍼런스 타입이 아니면 값을 그대로 돌려준다.
  }
  
  var base = GetBase(value);
  
  if (base === null) {   
    throw new ReferenceError;
  }
  
  return base.[[Get]](GetPropertyName(value))
}

위에서 내부 [[Get]] 메서드는 프로토타입 체인으로부터 상속된 프로퍼티까지 분석해서 객체 프로퍼티의 실제값을 돌려준다.

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

(중요) 프로퍼티 접근자는 점 표기법(프로퍼티 이름이 정확한 식별자이고 미리 알 수 있을 때)이나 대괄호 표기법의 2가지 방법으로 표기할 수 있다.

foo.bar();
foo['bar']();

이번에도 중간 계산의 결과로 Reference type의 값을 갖게 된다.

var fooBarReference = { 
  base: foo, 
  propertyName: 'bar'
};

GetValue(fooBarReference); // function object "bar"

Reference type의 값과 함수 컨텍스트의 this 값은 어떤 관계일까? 이 부분이 가장 중요하며, 이 글의 메인이다.

함수 컨텍스트의 this값을 결정하는 일반적인 규칙은 다음과 같다.

함수 컨텍스트의 this값은 호출자가 제공하며 호출 표현식의 현재 형태에 의해서 값이 결정된다(함수 호출이 문법적으로 어떻게 이뤄졌는지에 따라서).

호출 괄호(…)의 왼편에 Reference type의 값이 존재하면, this는 Reference type의 this값인 base 객체를 값으로 갖는다.

다른 모든 경우에는(Reference type이 없는 다른 모든 값의 경우), this값은 항상 null로 설정된다. 그러나 nullthis의 값으로 의미가 없기 때문에 암묵적으로 전역 객체(global)로 변환된다.

예제
function foo() {
 return this;
}

foo(); // global

호출 괄호 왼쪽에 Reference type 값이 있다.(foo는 함수 이름으로 식별자이다)

var fooReference = {
 base: global,
 propertyName: 'foo'
};

따라서, this 값은 Reference type 값의 base 객체인 전역 객체로 설정된다.

var foo = {
  bar: function () {
    return this;
  }
};

foo.bar(); // foo

여기에서 다시 basefoo 객체인 Reference type의 값을 갖게 되고, 이것은 bar 함수 활성화시 this 값으로 이용된다.

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

그러나, 또 다른 형태의 호출 표현식으로 함수를 활성화시키면 this 값은 달라진다.

var test = foo.bar;
test(); // global

test가 식별자가 되면서 다른 Reference type 값을 만들기 때문에, 이 Reference type의 base값인 전역 객체가 this 값으로 사용된다.

var testReference = {
  base: global,
  propertyName: 'test'
};

이제는 다른 형태의 호출 표현식으로 활성화된 같은 함수가, 또한 다른 this값을 갖는지를 정확하게 이야기할 수 있다.=> Reference type의 중간값이 달라서 일어나는 현상

function foo() {
  alert(this);
}

foo(); // 전역이기 때문에

var fooReference = {
  base: global,
  propertyName: 'foo'
};

alert(foo === foo.prototype.constructor); // true

// 호출 표현식의 또 다른 형태
foo.prototype.constructor(); // foo.prototype이기 때문에

var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

호출 표현식 형태에 따라 this값이 동적으로 결정되는 다른 예제를 보자.

function foo() {
  alert(this.bar);
}

var x = {bar: 10};
var y = {bar: 20};

x.test = foo;
y.test = foo;

x.test(); // 10
y.test(); // 20

Function call and non-Reference type (함수 호출과 비-레퍼런스 타입)

호출 괄호의 왼편에 Reference type이 아닌 다른 값이 오는 경우 this값은 자동으로 null 값을 가지게 된다. Engine은 자동으로 전역객체(global)로 출력한다.

(function () {
  alert(this); // null => global
})();

위의 경우가 좋은 예시로 Reference type(식별자, 프로퍼티 접근자)이 아니라 Reference type이 존재하지 않는다. 즉, null이라서 전역객체(global)를 출력한다.

var foo = {
  bar: function () {
    alert(this);
  }
};

foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo

(foo.bar = foo.bar)(); // global
(false || foo.bar)(); // global
(foo.bar, foo.bar)(); // global

이 경우가 정말 어렵다. 왜 아래의 3개의 경우에는 왜 전역객체가 나오는 것일까? 아래의 3개는 Reference type 값이 null이라는 것이다.

두번째 경우에는 그룹핑 연산자가 Reference type의 값으로부터 객체의 실제 값을 얻기 위한 메서드인 GetValue에 적용되지 않는다. 그래서 그룹핑 연산자가 평가 결과를 반환할 때도 여전히 Reference type의 값이 존재하게 되는데, 이것이 this값이 다시 base객체로 설정되는 이유다.

세번째의 경우는, 그룹핑 연산자와 다르게 할당 연산자는 GetValue 메서드를 호출한다. 반환의 결과로 thisnull로 설정되었음을 의미하는 함수 객체가 반환되기 때문에, 결국 전역 객체가 된다.

네번째와 다섯번째의 경우도 유사하다. 콤마 연산자와 논리적 OR 표현식GetValue메서드를 호출하고, 따라서 Reference type의 값을 잃어버리고 함수 타입의 값을 갖게 되어 this의 값은 전역 객체로 설정된다.

function foo() {
  function bar() {
    alert(this); // global
  }
  bar(); // AO.bar()와 같다.
}

위의 경우에서 활성화 객체(VO)는 항상 this 값으로 null을 반환한다.


예외의 with 함수

with 객체가 함수 이름 프로퍼티를 갖는 경우, with문의 블럭 안에서 함수를 호출할 때는 예외일 수 있다. with문은 자신의 Scope Chain의 가장 앞, 즉 활성화 객체(VO) 앞에 그 객체를 추가한다.

따라서 Reference type 값을 얻으려 할 때(식별자나 프로퍼티 접근자를 이용해서) 활성화 객체(VO)가 아닌 with 문의 객체를 base 객체로 갖게 된다.

그런데, 이는 with 객체가 Scope Chain에서 더 상위에 있는(전역 객체 또는 활성화 객체) 객체까지 가려버리기 때문에 중첩함수 뿐만 아니라 전역 함수와도 관련이 있다.

var x = 10;

with ({
  foo: function () { alert(this.x); },
  x: 20
}) {
  foo(); // 20
}
  
// because
var  fooReference = {
  base: __withObject,
  propertyName: 'foo'
};

catch절의 실제 파라미터인 함수를 호출할 것도 이와 유사하다.

항상 스코프 체인의 가장 앞, 즉 활성화 객체나 전역 객체 앞에 catch 객체가 추가된다.

아래 동작은 ECMA-262-3의 버그로 인정되어 새로운 버전인 ECMA-262-5에서는 수정되었다. ECMA-262-5는 이러한 경우 this 값이 catch 객체가 아닌 전역 객체로 설정된다.

try {
  throw function () { alert(this); };
} catch (e) {
  e(); // ES3:__catchObject | ES5에서는 수정:global
}

var eReference = {
  base: __catchObject,
  propertyName: 'e'
};

// 그러나, 이것은 버그이기 때문에
// this는 강제로 전역 객체를 참조하게 된다.
// null => global
var eReference = {
  base : global,
  propertyName: 'e'
}

This value in function called as the constructor (생성자로 호출된 함수 안의 this)

function A() {
  alert(this); // 새롭게 만들어진 객체, 아래에서 a 객체
  this.x = 10;
}

var a = new A();
alert(a.x); // 10

이 경우는, new 연산자가 A함수의 내부 [[Construct]] 메서드를 호출하고 차례로, 객체가 만들어진 후에 A와 모두 같은 함수인 내부의 [[Call]] 메서드를 호출하여 this값으로 새롭게 만들어진 객체를 갖게 된다.


Manual setting of this value for a function call (함수 호출시 this를 수동으로 지정하기)

함수 호출 시에 this 값을 수동적으로 지정할 수 있게 해주는 두 가지 방법이 Function.prototype 에 정의되어 있다(prototype에 정의되어 있으므로 모든 함수가 이용 가능). 바로 applycall메서드다.

var b = 10;

function a(c) {
  alert(this.b);
  alert(c);
}

a(20); // this === global, this.b == 10, c == 20

a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40

+ 추가(bind)

let b = 10;

function a(c) {
  alert(this.b);
  alert(c);
}

let c = a.bind({b: 20});
c(30) // this === {b: 20}, this.b == 20, c == 30

Reference


avatar
snyungSoftware Engineer(from. 2018)
social-mailsocial-githubsocial-facebooksocial-book