6일 5월 2020
이 글은 다음 언어로만 작성되어 있습니다. عربي, English, 日本語. 한국어 번역에 참여해주세요.

Reference Type

In-depth language feature

This article covers an advanced topic, to understand certain edge-cases better.

It’s not important. Many experienced developers live fine without knowing it. Read on if you’re want to know how things work under the hood.

A dynamically evaluated method call can lose this.

For instance:

let user = {
  name: "John",
  hi() { alert(this.name); },
  bye() { alert("Bye"); }
};

user.hi(); // works

// now let's call user.hi or user.bye depending on the name
(user.name == "John" ? user.hi : user.bye)(); // Error!

On the last line there is a conditional operator that chooses either user.hi or user.bye. In this case the result is user.hi.

Then the method is immediately called with parentheses (). But it doesn’t work correctly!

As you can see, the call results in an error, because the value of "this" inside the call becomes undefined.

This works (object dot method):

user.hi();

This doesn’t (evaluated method):

(user.name == "John" ? user.hi : user.bye)(); // Error!

Why? If we want to understand why it happens, let’s get under the hood of how obj.method() call works.

Reference type explained

Looking closely, we may notice two operations in obj.method() statement:

  1. First, the dot '.' retrieves the property obj.method.
  2. Then parentheses () execute it.

So, how does the information about this get passed from the first part to the second one?

If we put these operations on separate lines, then this will be lost for sure:

let user = {
  name: "John",
  hi() { alert(this.name); }
}

// split getting and calling the method in two lines
let hi = user.hi;
hi(); // Error, because this is undefined

Here hi = user.hi puts the function into the variable, and then on the last line it is completely standalone, and so there’s no this.

To make user.hi() calls work, JavaScript uses a trick – the dot '.' returns not a function, but a value of the special Reference Type.

The Reference Type is a “specification type”. We can’t explicitly use it, but it is used internally by the language.

The value of Reference Type is a three-value combination (base, name, strict), where:

  • base is the object.
  • name is the property name.
  • strict is true if use strict is in effect.

The result of a property access user.hi is not a function, but a value of Reference Type. For user.hi in strict mode it is:

// Reference Type value
(user, "hi", true)

When parentheses () are called on the Reference Type, they receive the full information about the object and its method, and can set the right this (=user in this case).

Reference type is a special “intermediary” internal type, with the purpose to pass information from dot . to calling parentheses ().

Any other operation like assignment hi = user.hi discards the reference type as a whole, takes the value of user.hi (a function) and passes it on. So any further operation “loses” this.

So, as the result, the value of this is only passed the right way if the function is called directly using a dot obj.method() or square brackets obj['method']() syntax (they do the same here). Later in this tutorial, we will learn various ways to solve this problem such as func.bind().

Summary

Reference Type is an internal type of the language.

Reading a property, such as with dot . in obj.method() returns not exactly the property value, but a special “reference type” value that stores both the property value and the object it was taken from.

That’s for the subsequent method call () to get the object and set this to it.

For all other operations, the reference type automatically becomes the property value (a function in our case).

The whole mechanics is hidden from our eyes. It only matters in subtle cases, such as when a method is obtained dynamically from the object, using an expression.

result of dot . isn’t actually a method, but a value of `` needs a way to pass the information about obj

과제

중요도: 2

아래 코드의 실행 결과를 예측해보세요.

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)()

주의: 함정을 파놓았습니다. (• ◡•)

에러가 발생합니다!

코드를 직접 실행해봅시다.

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)() // error!

브라우저에서 출력되는 에러 메시지만 봐서는 무엇이 잘못되었는지 파악하기 어려울 겁니다.

에러는 user = {...}뒤에 세미콜론이 없어서 발생했습니다.

자바스크립트는 괄호((us... ) 앞에 세미콜론을 자동으로 넣어주지 않습니다. 따라서 코드는 아래와 같아집니다.

let user = { go:... }(user.go)()

이렇게 두 표현식이 합쳐지면서 인수가 (user.go)인 객체 형태의 함수를 호출한 것처럼 되었습니다. 여기에 더하여 객체 user가 정의되지 않은 상태에서 같은 줄에 let user를 사용했기 때문에 에러가 발생합니다.

user = {...}뒤에 세미콜론을 붙여서 에러를 해결해봅시다.

let user = {
  name: "John",
  go: function() { alert(this.name) }
};

(user.go)() // John

참고로, (user.go)를 감싸는 괄호는 아무런 역할을 하지 않습니다. 괄호는 대개 연산자 우선순위를 바꾸는 데 사용되는데, (user.go)에선 점 . 연산자가 먼저 동작하기 때문에 의미가 없습니다. 문제 출제 의도는 세미콜론 여부였습니다.

중요도: 3

아래 코드에선 다양한 방법으로 user.go()를 4번 연속 호출합니다.

그런데 첫 번째((1))와 두 번째 호출((2)) 결과는 세 번째((3))와 네 번째((4)) 호출 결과와 다릅니다. 이유가 뭘까요?

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

이유는 다음과 같습니다.

  1. 우리가 알고 있는 일반적인 메서드 호출 방법입니다.

  2. 역시 일반적인 호출 방법에 속합니다. 괄호가 추가되었긴 하지만 연산 우선순위를 바꾸진 않으므로 점 연산자가 먼저 실행됩니다.

  3. 좀 더 복잡한 패턴의 호출((expression).method())이 등장했네요. 세 번째 호출은 아래와 같은 코드로 쪼갤 수 있습니다.

    f = obj.go; // 표현식 계산하기
    f();        // 저장된 것 호출하기

    위 코드에서 f()는 (메서드가 아닌) 함수로써 호출되었습니다. this에 대한 정보가 전혀 없는 상태에서 말이죠.

  4. (3)과 동일한 패턴의 호출입니다. expressionobj.go || obj.stop라는 차이점만 있습니다.

(3)(4)에서 어떤 일이 일어나는지 알려면 참조 타입을 다시 상기해야 합니다. 점이나 대괄호를 통해 프로퍼티에 접근하려는 경우 참조 타입 값((base, name, strict))이 반환됩니다.

메서드 호출을 제외하고, 참조 타입 값에 행해지는 모든 연산은 참조 타입 값을 일반 값으로 변환시킵니다. 이 과정에서 this에 정보가 누락됩니다.

튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.