Introduction

Built with sNyung

JavaScript란?

JavaScript(JS)가벼운 인터프리터형, JIT-컴파일형 프로그래밍 언어, first-class functions를 지원한다.

주로 웹 페이지를 위한 스크립팅 언어로 알려졌지만, Node.js, Apache CouchDB, Adobe Acrobat처럼 많은 비 브라우저 환경에서도 사용된다.

JavaScript프로토타입 기반의 다중 패러다임 스크립팅 언어로서, 역동적이고, 객체지향형, 명령형 및 선언형(가령 함수형 프로그래밍) 스타일을 지원한다.

이 문서는 JavaScript 언어 자체만 다루며 웹 페이지를 비롯한 다른 사용 환경에 대해서는 다루지 않는다. 웹 페이지의 특정 API에 대하여 알고 싶다면 웹 APIDOM을 참고하면 됩니다.

JavaScript의 표준은 ECMAScript입니다.

2012년 기준 최신 브라우저는 모두 ECMAScript 5.1을 전부 지원한다.

이전 브라우저의 경우는 최소한 ECMAScript 3까지 지원한다.

2015년 6월 17일 ECMA International에서는 ㄴ공식명 ECMAScript 2015로 불리는 ECMAScript의 6번째 주 버전을 발표했다(ECMAScript 6 혹은 ES6).

그 이후 ECMAScript 표준은 출시가 1년 주기이다.

JavaScriptJava 프로그래밍 언어와 혼동해서는 안된다.

"Java"와 "JavaScript" 두 가지 모두 Oracle이 미국 및 기타 국가에 등록한 상표이다. 하지만, 두 언어는 문법 체계와 사용방법이 전혀 다릅니다.


Reference

Call Stack

Intro

먼저 들어가기에 앞서 우리가 사용하려는 언어는 스크립트 언어로 이름은 자바스크립트이다.

우리가 사용할 언어가 어떻게 실행되는지 알아보기 위해서는 엔진에 대해서 알아야 한다.

V8 Engine

최근 많이 사용하는 브라우저는 Chrome이다. Chrome에서 사용되는 자바스크립트 엔진은 구글의 V8 Engine이다.

이 엔진에서의 2가지 Main Components가 있다.

  1. Memory Heap : 메모리의 할당이 일어나는 곳.
  2. Call Stack : Stack Frame이 실행되는 곳. 쉽게 말해서 우리가 작성한 코드가 실행되는 곳.

Call Stack

Call Stack은 LIFO (Last In, First Out) 원리를 사용하여 함수 호출을 임시 저장하고 관리하는 데이터 구조

LIFO : Last In, First Out의 데이터 구조 원칙에 따라 호출 스택이 작동한다. 스택으로 푸시 된 마지막 함수가 처음으로 튀어나옴을 의미한다.

자바스크립트에서 Call Stack은 주로 함수 호출에 된다. Call Stack이 하나이기 때문에 함수 실행은 위에서 아래로 한 번에 하나씩 수행된다.

기본적으로 자바스크립트는 싱글 쓰레드 프로그래밍 언어이다. 이 말은 싱글 Call Stack을 가지고 있다는 것을 의미한다. 다른 말로 하자면 한 번에 한 가지의 일만 한다는 것이다.

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

위와 같은 코드가 있다고 한다면 printSquare(5) ⇒ multiply(x, x) ⇒ console.log(s) ⇒ printSquare(5)순으로 쌓이고 실행이 된다는 것을 의미한다.

각각 한 줄을 Stack Frame이라고 한다.

function foo() {
    throw new Error('Session Stack will help you resolve crashes :)');
}

function bar() {
    foo();
}

function start() {
    bar();
}

start();

위와 같은 코드를 Chrome에서 실행한다면 당연하게 에러가 떨어지면서 Stack 형태를 자세히 볼 수 있다.

다른 경우로는 Call Stack의 사이즈를 넘어서서 쌓이는 경우도 발생한다.

function foo() {
    foo();
}

foo();

위와 같은 코드를 사용하면 안되겠지만 위와 같은 경우는 브라우저에서 계속 쌓아가다가 16000개가 넘어가는 순간에 Stack Size가 넘쳤다고 나올 것이다.

싱글 쓰레드는 멀티 쓰레드보다 다루기는 쉽다. (Deadlock(교착상태) 같은 일이 발생하지 않음으로)

그러나 역시 제한적이다.

예시로 내가 버튼을 눌러서 서버에서 사진을 가져오려고 할 때. 버튼을 누르고 사진을 가져올 때까지 브라우저는 멈춘 상태가 되어 버린다.

이에 대안으로 Asynchronous Callbacks 이다.

Web API

우리가 사용하는 자바스크립트에도 제공이 되지 않는 것들이 있다. 가령 우리가 비동기를 사용하기 위해서 사용하는 setTimeOut(), setInterval() 등등 브라우저에서 제공하는 API라고 생각하면 된다.

간략하게 지원하는 API로는

  1. DOM
  2. AJAX(==XMLHttpRequest)
  3. setTimeOut
  4. 등등

이 있다.

브라우저 웹 API : DOM 이벤트, XMLHttpRequest, setTimeout 등과 같은 비동기 이벤트를 처리하기 위해 C ++로 구현된 브라우저로 만든 쓰레드

Queue(Message Queue || CallBack Queue)

자바스크립트 런타임에는 처리할 메시지 목록과 콜백 함수인 메시지 Queue가 있다. 현재 Stack의 용량이 충분하다면 Queue에서 메시지를 가져와서 처리된 것의 CallBack function을 실행한다.

기본적으로 이러한 메시지는 콜백 기능이 제공되면 외부 비동기 이벤트에 대해 응답을 한다. 예를 들어 사용자가 버튼을 클릭하고 콜백 함수가 제공되지 않으면 아무런 메시지도 Queue에 추가되지 않게 되는 것이다.

Event Loop

네크워크는 느리다. 그래서 사진을 불러오는 것은 느리다. 이에 사용하는 것이 우리가 흔히 AJAX라고 부르는 비동기 함수이다. 만약 이러한 작업이 동기라면 위와 같이 멈추는 현상이 일어날 것이다.

가장 쉬운 해결책이 비동기 Callbacks이다. 위에서 언급한 Web API에서 제공하는 것에 비동기 Callbacks이다.

console.log()와 다르게 바로 실행이 되지 않는다. 그렇다면 이런 것들은 어떻게 되는 것인가?

응답에서 호출자를 분리하면 비동기 작업이 완료되고 콜백이 시작될때까지 기다리는 시간 동안 자바스크립트 런타임에서는 다른 작업을 할 수 있다.

Web API에서 요청한 작업을 완료한 후 Callbacks을 실행해야한다. 그러나 만약 작업이 완료되고 직접 Web API 쪽에서 Call Stack에 실행 코드를 넣을 수 있다면 마음대로 바로 나타날 것이다.

그래서 있는 것이 Queue이다. Web API에서 요청한 작업을 완료한 후에 Queue에 넣어 준다.

Event Loop는 이제 Call Stack이 비었을 때 Queue에서 들어온 Callbacks를 수행한다.

Execute Context

실행 컨텍스트는 자바스크립트 코드가 평가되고 실행되는 환경의 추상적인 개념이다. 자바스크립트에서 코드를 실행할 때마다 실행 컨텍스트 내에서 실행된다.

기본적으로 자바스크립트 내에는 3가지 타입의 실행컨텍스트가 있다고 한다.

  1. Global
  2. Functional
  3. ~~Eval Function~~

그러나 중요한 것은 1번과 2번이다.

Execution Stack

실행 스택은 우리가 위에서 보았다. Call Stack의 개념이다.

LIFO (Last In, First Out) 원리를 사용하여 함수 호출을 임시 저장하고 관리하는 데이터 구조이다.

실행컨텍스트를 만드는데 2개의 단계가 있다.

  1. Creation 단계
  2. Execute 단계

Creation 단계

한국말로는 만드는 단계이다. 먼저 2가지를 만든다.

  1. LexicalEnvironment
  2. VariableEnvironment

기본적인 형태는 아래와 같다.

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

Lexical Environment

Lexical Environment은 식별자, 변수 맵핑을 가지고 있는 구조이다.

여기서 식별자는 변수/함수의 이름을 가리키며 변수는 실제 객체 [함수 객체 및 배열 객체 포함] 대한 참조입니다.

var a = 20;
var b = 40;

function foo() {
  console.log('bar');
}

위와 같은 코드가 있다고 하면 Lexical Environment은

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}

위와 같이 만들어질 것이다.

여기에는 3가지의 정보를 가지고 있다.

  1. Environment Record : 변수 및 함수 선언이 Lexical Environment 내에 저장되는 장소
  2. Reference to the outer environment : 외부 환경에 대한 참조
  3. This binding : this가 결정되거나 설정된다.

Environment Record

기본적으로 2가지 정보를 담고 있다.

  • Declarative environment record(선언적 환경 정보) : 변수 및 함수 선언을 저장합니다.
  • Object environment record(객체 환경 정보) : 전역 코드의 Lexical Environment에는 객체 환경 레코드가 포함되어 있다. 변수 및 함수 선언 외에 객체 환경 레코드는 전역 바인딩 개체 (브라우저의 창 개체)도 저장합니다. 따라서 각 바인딩 객체의 속성에 대해 새 항목이 레코드에 만들어진다.

함수 코드의 경우 Environment Record에는 함수에 전달된 인덱스와 인수 사이의 매핑과 함수에 전달된 인수의 길이가 포함된 arguments 객체도 포함이 된다.

예를 들어, 아래 함수에 대한 인수 객체는 다음과 같습니다.

function foo(a, b) {
  var c = a + b;
}

foo(2, 3);

// argument object
Arguments: {0: 2, 1: 3, length: 2},

Reference to the Outer Environment

외부 환경에 대한 참조는 outer environment에 액세스 할 수 있음을 의미한다. 즉, 자바스크립트 엔진은 현재 lexical environment에서 찾을 수 없는 경우 outer environment에서 변수를 찾을 수 있다.

This Binding

여기에는 어렵고도 중요한 개념인 this가 결정되거나 설정된다.

전역 실행 컨텍스트에서이 값은 전역 개체를 참조합니다.

함수 실행 컨텍스트에서이 값은 함수가 호출되는 방식에 따라 다르게 this가 나온다. 객체 참조 때문에 호출되면 this 값은 해당 객체로 설정되고, 그렇지 않으면이 값은 전역 객체로 설정되거나 정의되지 않는다.

const person = {
  name: 'peter',
  birthYear: 1994,
  
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}

person.calcAge(); 
// 'this' refers to 'person', because 'calcAge' was called with //'person' object reference

const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given

Variable Environment

이것은 위에서 봐왔던 LexicalEnvironment와 같다.

ES6에서 LexicalEnvironment 구성 요소와 VariableEnvironment 구성 요소의 차이점 중 하나는 함수 선언과 변수 (letconst)바인딩을 저장하는데 사용되는 반면, 후자는 변수 (var)바인딩만 저장하는 데 사용된다.

Execute 단계

이 단계에서 모든 변수에 대한 할당이 완료되고 코드가 최종적으로 실행된다.

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
  var g = 20;
  return e * f * g;
}

c = multiply(20, 30);
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}
GlobalExectionContext = {
    
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
    
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier bindings go here
        Arguments: {0: 20, 1: 30, length: 2},
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>,
    },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

함수가 완료된 후 반환 값은 c 안에 저장된다. 그래서 글로벌 Variable Environment이 업데이트된다. 그 후, 전역 코드가 완료되고 프로그램이 완료된다.

letconst 정의 변수는 생성 단계에서 연관된 값이 없지만 var 정의 변수는 undefined로 설정된다.

이는 생성 단계에서 함수 선언이 환경에 전체적으로 저장되는 동안 변수 및 함수 선언에 대해 코드가 검색되고 변수가 초기에 undefined(var의 경우)로 설정되거나 초기화되지 않은 상태로 유지되기 때문이다. letconst의 경우).

이것이 var 정의 변수가 선언되기 전에 (정의되지는 않았지만) var 정의 변수에 액세스할 수 있지만 letconst 변수가 선언되기 전에 액세스 할 때 참조 오류가 발생하는 이유이다.

실행 단계에서 자바스크립트 엔진이 let 변수의 값을 소스 코드에서 선언된 실제 위치에서 찾지 못하면 정의되지 않은 값을 할당한다.

Type

Primitive Type(원시 타입)

한국말로 간단하게 말하면 원시 자료형이라고 하는 자바스크립트의 타입에 대해서 알아보자.

자바스크립트는 자바나 C언어와는 다르게 동적 타입언어라 불린다. 동적 타입 언어의 자료형은 컴파일시 자료형을 정하는 것이 아니고 실행시에 결정된다.

자바스크립트의 타입의 종류와 사용법에 대해서 알아보자.

6가지의 기본 Type

ES6 이전에는 기본적으로 5가지의 타입이었으나, ES6에서 Symbol 타입이 추가되었다.

  • String : 텍스트를 셋팅할 때 사용하는 타입.
  • Number : 숫자를 셋팅할 때 사용하는 타입. 기본적으로 소수점도 가능하다(infinity, -inifinity, NaN 표현이 가능하다.).
  • Null : null 타입은 정확히는 1개의 값은 가지고 있지만, 비어있다는 뜻이다.
  • Undefined : 값이 할당되지 않은 것을 나타내는 타입.
  • Boolean : true 또는 false 로 나타내는 타입.
  • Symbol : 새로 추가된 타입으로 unique하고 immutable한 원시값 으로 사용된다.

위에서 보이듯이 기본적으로 6가지의 형태를 가지고 있으며, 나머지는 Object형이라고 통칭한다.

  • Array : 배열, 리스트의 형태를 가지고 있다.
  • Function : Javascript에서 Function Object가 존재하지만 결국, Function도 Object이다.
  • Object : Map처럼 사용하는 즉, key : value의 형태로 사용하고 있는 Object.

위에서 보았던 6가지 기본타입을 생성하는 방법은 크게 2가지이다.

  1. Literal로 생성하기

Literal로 생성한다고 하면 우리가 가장 많이 사용하는 방법이다. 단순하게 변수를 초기화 후 할당하는 방법과 초기화를 하고 나중에 할당을 하는 방법으로 구분을 지을 수 있다.

// 초기화와 할당을 동시에 진행
var bol = true;
var str = "hello";
var num = 3.14;
var nullType = null;
var undef = undefined;

// 초기화 후 할당진행
var bol2;
var str2;
bo2 = false
str2 = "world"
  1. Wrapper Object를 사용해서 만들기

Wrapper Object를 사용해서 만든다고 하면 Constructor를 사용해서 만드는 것을 말한다. 쉽게 말하자면 new를 사용해서 만드는 것을 말한다.

new Boolean(false);
new String("world");
new Number(42);

Symbol("foo"); //Symbol 타입의 생성방법

Literal로 생성하는 방법과 Wrapper Object를 사용해서 만드는 방법은 차이점이 존재한다.

typeof true; //"boolean"
typeof Boolean(true); //"boolean"
typeof new Boolean(true); //"object"
typeof (new Boolean(true)).valueOf(); //"boolean"
     
typeof "abc"; //"string"
typeof String("abc"); //"string"
typeof new String("abc"); //"object"
typeof (new String("abc")).valueOf(); //"string"
     
typeof 123; //"number"
typeof Number(123); //"number"
typeof new Number(123); //"object"
typeof (new Number(123)).valueOf(); //"number"

중간중간 이상하게 new를 사용하지 않고 사용하는 것들은 window.Boolean, window.String 이런것으로 생각하면 되며, 이러한 Function은 해당 타입으로 변환하는 작업을 할 때 사용이 된다.

위에서 보게 되면 Literal로 생성한 것의 타입은 6가지 중 하나로 나오게 된다. 그런데 new를 사용하여 Wrapper Object로 만들게 되면 Object타입이 나오게 된다. 사용을 하려면 valueOf라는 Function을 사용해야만 입력한 값이 나오게 된다.

아래의 예제를 보자. 어떻게 결과가 어떻게 6이 나올 수 있는 것인가?

var str = 'string'

str.length // 6

위의 예제는 단순하게 str 변수안에 'string'이라는 String 타입의 값을 할당을 했다. 그런데 해당 변수는 Wrapper 객체가 아닌데 어떻게 갯수를 세는 Method를 사용하고 있는 것인가?

String.prototype.returnMe= function() {
    return this;
}
     
var a = "abc";
var b = a.returnMe();  
     
a; //"abc" 
typeof a; //"string" (still a primitive)

b; //"abc"
typeof b; //"object"

위의 예제를 보게 되면 답이 나오게 된다. 우리가 사용하는 var str를 사용하게 되면 Wrapper 객체로 임시변환이 이루어지고 returnMe() 와 같은 함수를 사용하면 다시 Wrapper 객체가 사라지게 된다.

심화내용

자바스크립트에서는 신기하게도 숫자타입을 하나로 사용하고 있다. 어떻게 하나의 타입으로 모든 것을 표현할 수 있는 것일까? Number타입은 국제 표준 부동 소수점 IEEE 754를 따르고 있다. 기본적으로 컴퓨터가 실수를 표현하는 방식은 2진법에 따라서

  • 13 = 8 + 4 + 1 이므로 해당 자리 숫자를 1로 표현하고 나머지는 0으로 표현하게 되고 1101이 된다.
  • 0.75 = 0.5 + 0.25 이므로 0.11 로 표현할 수 있다.

일반적으로 소수점을 표현하는 방법은 2가지 방법이 있다.

고정 소수점

  • 정수를 표현하는 비트수와 소수를 표현하는 비트수를 미리 정해놓고 해당 비트만큼 사용해서 숫자를 표현하는 방식.
  • 예) 실수 표현에 4byte(32bit)를 사용하고 그 중 부호1bit, 정수 16bit, 소수 15bit 를 사용하도록 약속해 놓은 시스템에 있다. 이렇게 약속된 시스템에서 263.3을 표현하면 (0)0000000100000111.010011001100110 이렇게 표현된다.
  • 정수를 표현하는 비트수를 늘리면 큰 숫자를 표현할 수 있지만 정밀한 숫자를 표현하긴 힘들다. 그래서 소수를 표현하는 bit를 늘릴 경우 정밀한 숫자를 표현할 수 있지만 큰 숫자를 표현하지 못한다.

이런 문제를 해결하기 위해서 소수점을 고정하지 않고 표현할 수 있는 부동 소수점(floating point) 을 사용하게 되었다.

부동소수점

부동 소수점을 표현하는 방식도 정하는 방식에 따라 다를 수 있지만 일반적으로 사용하고 있는 방식은 위에서 언급한 IEEE 754에서 표준으로 제안한 방식을 따른다.

우선 고정 소수점으로 나타낸 263.3을 2진수 부동 소수점 방식으로 변환을 해보면, 100000111.010011001100110... 으로 표현되던 것을 맨 앞에 있는 1 바로 뒤로 소수점을 옮겨서 표현하도록 변환하게 되면 1.00000111010011001100110... * 2^8(2의 8승) 으로 표현된다.

  • 2^8의 8을 지수라고 하고 지수 부분에 기록을 하고(IEEE 754 표현 방식에서는 127 + 지수를 기록한다. )
  • 소수점 이후 숫자열 전체를 가수라고 하고 가수 부분에 기록을 한다.

최종적인 모양을 아래와 같다.

  • 부호 비트(1 bit) : 0 (양수)
  • 지수 비트(8 bit) : 10000111 (127 + 8 = 135)
  • 가수 비트(23 bit) : 00000111010011001100110

이렇게 표현하게 된다.

하지만 여기서도 0.010011001100110은 정확히 0.3을 나타낼 수 없게 된다. 10진수로 나타내 보면 0.29998779296875로 나오게 된다. 그래서 자바스크립트에서 0.1 + 0.2를 하게 되면 0.30000000000000004가 나오는 이유이다.

블록체인에서는 부동소수점에서 8자리까지를 사용한다고 한다.

값타입과 참조타입

기본적으로 원시타입을 값타입이라고 한다면 Object를 참조타입이다.

원시타입은 값타입이다.

var a = 13         // assign `13` to `a`
var b = a          // copy the value of `a` to `b`

b = 37             // assign `37` to `b`

console.log(a)     // => 13

위에서 b에 a의 값을 복사를 했다. 그리고 b의 값을 변경을 했는데 a에는 영향이 가지 않았다. 이유는 당연하게 2개의 값이 저장된 공간이 다르기 때문이다.

var a = 5;
var b = a;

a = 10;

console.log(a); // 10
console.log(b); // 5
// string, boolean, null, undefined은 같은 결과가 나온다.

Object는 참조타입이다.

var a = { c: 13 }  // assign the reference of a new object to `a`
var b = a          // copy the reference of the object inside `a` to new variable `b`
b.c = 37           // modify the contents of the object `b` refers to
console.log(a)     // => { c: 37 }

원시타입과는 다르게 복사한 것을 변경을 했더니 a에도 영향이 간다. 이유는 당연하게 같은 값의 주소를 복사를 하여서 a에 들어있는 주소의 공간이 바뀌었으므로 a로 바뀐값을 불러오는 것이다.

var a = {};
var b = a;

a.a = 1;

console.log(a); // {a: 1}
console.log(b); // {a: 1}

array의 경우에 있어도 예외는 없다.

var a = [];
var b = a;

a.push(1);

console.log(a); // [1]
console.log(b); // [1]
console.log(a === b); // true
function changeAgeImpure(person) {
    person.age = 25;
    return person;
}
var alex = {
    name: 'Alex',
    age: 30
};
var changedAlex = changeAgeImpure(alex);

console.log(alex); // -> { name: 'Alex', age: 25 }
console.log(changedAlex); // -> { name: 'Alex', age: 25 }
function changeAgePure(person) {
    var newPersonObj = JSON.parse(JSON.stringify(person));
    newPersonObj.age = 25;
    return newPersonObj;
}
    
var alex = {
    name: 'Alex',
    age: 30
};
var alexChanged = changeAgePure(alex);

console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }

문제

function changeAgeAndReference(person) {
    person.age = 25;
    person = {
        name: 'John',
        age: 50
    };
        
    return person;
}
    
var personObj1 = {
    name: 'Alex',
    age: 30
};
    
var personObj2 = changeAgeAndReference(personObj1);

console.log(personObj1); // -> ?
console.log(personObj2); // -> ?

👉 답 확인하기

명시적 변환, 암시적 변환, 덕 타이핑

명시적 변환 vs 암묵적 변환

개발자가 Number(value) 와 같은 코드를 작성하여 변환할 의사를 명확하게 표현하는 것을 명시적 변환이라고 한다. JavaScript는 약타입 언어이므로 값을 자동으로 여러 유형간에 변환을 자동으로 한다. 이것을 암묵적 변환 이라고 한다.

예를 들어 일반적으로 연산자를 다양한 유형의 값에 적용하면 1 == null, 2 / '5', null + new Date () 또는 if (value) {...} 와 같이 문법에 의해 발생할 수 있다.

우리가 가장 많이 사용하는 암시적 타입 변환을 하지 않는 연산자는 === 이며, 완전 항등 연산자 라고 한다. 반면에 느슨한 항등 연산자 == 는 필요하다면 비교와 타입 강제 변환을 수행 합니다.

암시적 타입 변환으로 == 을 사용하게 되면, 가독성을 잃지 않으면서 적은 코드로 작성할 수있는 유용한 매커니즘이다. 그러나 ES6+를 사용한다면 느슨한 항등 연산자는 사용하지 않는 것을 추천한다. 완전하게 항등연산자를 이해하고 결과값을 예상할 수 있는 경우가 아니라면 내가 생각한 것과 다른 결과가 나올 확률이 매우 높다.

기본적으로 변환은 3가지 유형의 전환이 있다.

  • to string
  • to boolean
  • to number

String 변환

명시적으로 값을 문자열로 변환하려면 String() 함수를 사용하면 된다. 암시적 강제 변환은 binary 연산자가 아닌것에 + 연산자를 사용하면 변환이 이루어진다.

String(123) // 명시적

123 + ''    // 암시적

아래의 예제를 보면 예상대로 다 문자열로 변환이 잘 이루어지고 있다.

String(123)                   // '123'
String(-12.3)                 // '-12.3'
String(null)                  // 'null'
String(undefined)             // 'undefined'
String(true)                  // 'true'
String(false)                 // 'false'

Symbol에 대해서는 생각과는 다르게 나온다.

String(Symbol('my symbol'))   // 'Symbol(my symbol)'
'' + Symbol('my symbol')      // TypeError is thrown

Symbol 변환은 명시적으로만 변환될 수 있고, 암시적 변환은 되지 않는다.

Boolean 변환

명시적으로 값을 Boolean으로 변환하려면 Boolean() 함수을 사용하면 된다. 암시적 변환은 논리 Context에서 발생하거나 논리 연산자에 의해 작동을 하게 된다.( ||, &&, ! )

Boolean(2)          // 명시적
if (2) { ... }      // 논리적 문맥 때문에 암시적
!!2                 // 논리적 문맥 때문에 암시적
2 || 'hello'        // 논리적 문맥 때문에 암시적

논리 연산자(예 : ||&& )에 따른 Boolean 변환을 내부적으로 수행하지만 Boolean값이 아니더라도 원래 피연산자의 값을 실제로 반환한다. 아래를 보게 되면 Boolean 변환을 해서 검사는 하지만 실제로는 123도 반환되고 있다.

// true를 반환하는 것이 아닌 123를 반환하고 있다.
// 'hello' and 123 은 표현식을 계산하기 위해서 Boolean으로 강제 변환을 한다.
let x = 'hello' && 123;   // x === 123

Boolean 변환의 결과는 true, false 2개만 있다. false 값 목록은 쉽게 기억이 가능하다.

Boolean('')           // false
Boolean(0)            // false     
Boolean(-0)           // false
Boolean(NaN)          // false
Boolean(null)         // false
Boolean(undefined)    // false
Boolean(false)        // false

목록에 없는 값 object, function, Array, Date, 사용자 정의 유형등은 true 로 변환한다.

Boolean({})             // true
Boolean([])             // true
Boolean(Symbol())       // true
!!Symbol()              // true
Boolean(function() {})  // true

Numeric 변환

명시적 변환의 경우 Boolean()String() 에서와 마찬가지로 Number() 함수를 사용하면 된다. 암시적 변환은 많은 경우에서 작동이 되기 때문에 까다롭다.

  • 비교 연산자(><<=,>=)
  • 비트 연산자( | & ^ ~)
  • 산수 연산자 (- + * / % ).
    • 참고로, +는 피연산자가 문자열일 때 숫자 변환을 하지 않고 문자열 변환을 한다.
  • 단항 연산자(기호로 사용하는) +
  • 느슨한 비교 연산자 == (!=).
    • 두 피연산자가 모두 문자열 인 경우 == 는 숫자 변환을 하지 않는다.
Number('123')   // 명시적
+'123'          // 암시적
123 != '456'    // 암시적
4 > '5'         // 암시적
5 / null        // 암시적
true | 0        // 암시적
Number(null)                   // 0
Number(undefined)              // NaN
Number(true)                   // 1
Number(false)                  // 0
Number(" 12 ")                 // 12
Number("-12.34")               // -12.34
Number("\n")                   // 0
Number(" 12s ")                // NaN
Number(123)                    // 123

문자열을 숫자로 변환할 때 엔진은 먼저 앞뒤의 공백, \ n, \ t 문자를 제거하고, 문자열이 유효한 숫자를 나타내지 않으면 NaN 을 반환한다. string이 비어 있으면 0을 반환합니다.

nullundefined는 다르게 처리가 되는데 null은 0으로 undefinedNaN으로 된다.

Symbol은 명시적 또는 암시적으로 숫자로 변환될 수 없다. 또한 TypeErrorundefined로 발생하는 것처럼 NaN으로 자동 변환하는 대신 throw 된다.

Number(Symbol('my symbol'))    // TypeError is thrown
+Symbol('123')                 // TypeError is thrown

기억해야 할 두 가지 특별한 규칙이 있는데

  1. ==null 또는 undefined 에 적용하면 숫자 변환이 발생하지 않는다. nullnull, undefined과 동일하다.
null == 0               // false, null is not converted to 0
null == null            // true
undefined == undefined  // true
null == undefined       // true
  1. NaN은 그 자체가 동등하지 않다.
if (value !== value) { 
    console.log("we're dealing with NaN here") 
}

Object 강제 변환

참고: JavaScript type coercion explained


심화

덕 타이핑(Duck Typing)이란?

쉽게 정의를 하자면 사람이 오리처럼 행동하면 오리로 봐도 무방하다라는게 덕 타이핑(Duck Typing)이다.

이것은 타입을 미리 정하는게 아니라 실행이 되었을 때 해당 Method들을 확인하여 타입을 정한다는 것으로 타입의 변화가 느슨하다.

  • 장점
    • 타입에 대해 매우 자유롭다.
    • 런타임 데이터를 기반으로 한 기능과 자료형을 만들어 내는 것
  • 단점
    • 런타임 자료형 오류가 발생할 수 있다. 런타임에서, 값은 예상치 못한 유형이 있을 수 있고, 그 자료형에 대한 무의미한 작업이 적용된다.
    • 이런 오류가 프로그래밍 실수 구문에서 오랜 시간 후에 발생할 수 있다
    • 데이터의 잘못된 자료형의 장소로 전달되는 구문은 작성하지 않아야 한다. 이것은 버그를 찾기 어려울 수도 있다.

Reference

Function

Function에 대해 알아보기 전, 자바스크립트의 Scope를 배워보자. 자바스크립트에서의 Scope는 그게 나누면 2개로 나뉘게 된다.

함수 안에서 정의가 된 변수들은 기본적으로 Local Scope에서 선언되었다고 하며, 함수 외부에 정의된 변수는 Global Scope에서 선언되었다고 한다. 함수 외부라고 하면 단순하게 중첩된 함수에서의 외부가 아닌 함수 1개가 있다는 기준에서의 외부이다. 이렇게 완전한 외부를 **Window(Global)**라고 하자.

  • Global Scope(함수 외부에 정의)
  • Local Scope(함수 안에서 정의)

각각의 함수들은 실행이 되면 내부적으로 새로운 Scope를 생성하고 가지게 된다.

Global Scope

우리는 자바스크립트를 실행하게 되면 이미 Global Scope안에 있다. 그래서 Function 안쪽에서 선언하지 않는 것들은 Global Scope에서 선언하는 것이다.

// Global Scope
const name = 'snyung';
const age = '27';

Global로 선언한 변수는 다른 Scope 어디든 접근이 가능하다.

const name = 'snyung';

// 같은 Scope에서의 호출은 된다.
console.log(name);  // snyung
    
function logName() {
    console.log(name); // name이라는 변수는 어디서든 접근이 가능하다.
}
    
logName(); // snyung

Local Scope

함수 안에서 선언한 변수는 Local Scope안에 있는 것이다. 우리는 같은 변수의 이름을 다른 함수 안에서 각각 정의를 할 수 있는데, 해당 변수는 서로 다른 Scope에 바인딩 되어 각각의 함수에서는 접근이 불가능하다.

// Global Scope
function someFunction() {
    // Local Scope #1
    function someOtherFunction() {
        // Local Scope #2
    }
}
    
// Global Scope
function anotherFunction() {
    // Local Scope #3
}
// Global Scope

위와 같이 코드가 있다면, Local Scope#1에 선언한 변수는 Local Scope#2에서는 접근이 가능하지만 Local Scope#3에서는 접근이 불가하다. 마찬가지로 Local Scope#3에 선언된 변수는 Local Scope#1, Local Scope#2에서 접근이 불가하다. Local Scope#2 에서 Local Scope#1 접근이 가능한 이유는 뒤에서 다룰 것이다.

Functional Scope

자바스크립트는 위에서 본 것과 같이 함수 단위로 Scope를 구분한다. 즉, 같은 함수 안에서 선언된 변수들은 같은 Level의 Scope를 가지게 되는 것이다. 각각의 함수는 독립적인 Scope를 가지게 되어 다른 함수의 Scope에 접근을 할 수 없다.

// Global Scope
function someFunction() {
    if (true) {
        var name = 'snyung'; 
    }
}

위와 같이 Global Scope에 someFunction()을 선언하고 내부에 if문 괄호 안에 선언한 변수는 someFunction function Scope에 붙게 되는 것이다. 즉, block 단위가 아닌 function 단위의 scope가 정의되는 것을 볼 수 있다.

Block Scope

Block Statement는 우리가 많이 보는 if문, switch문, for, while문이다. 이러한 문장들은 괄호로 감싸진 부분이 존재하지만 새로운 Scope를 만들지는 않는다. Block Statement 안에서 정의한 변수는 가장 가까운 함수의 Scope에 붙게 된다.

if (true) {
    // this 'if' conditional block doesn't create a new scope
    var name = 'snyung'; // name is still in the global scope
}
    
console.log(name); // logs 'snyung'

ECMAScript6에서 let, const가 추가되었다. 이 2개는 var 대용으로 사용된다. 그러나 그보다 더 중요한 개념이 들어간다. 바로 Block Level Scope라는 것이다. 기존의 자바스크립트는 위에서 본 것처럼 Functional Scope 이다. 그러나 let, const 를 사용하게 되면 Block Level Scope 지원이 가능하다. 아래의 예제를 보자

if (true) {
    // this 'if' conditional block doesn't create a new scope
    var name = 'snyung'; // name is still in the global scope
    let likes = 'Coding';
    const skills = 'js';
}
    
console.log(name); // logs 'snyung'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defined

var와는 다르게 let, const는 Block Statement내에서 Local Scope 를 지원한다. 즉, 이제 Scope가 가장 가까운 function에 붙는 것이 아닌 해당 Block에 붙게되는 것이다.

Global Scope는 응용 프로그램이 살아있을 때까지 유효하며, Local Scope은 함수가 호출되고 실행되는 한 유지된다.

Lexical Scope

Lexical Scope는 중첩된 함수에서 내부 함수는 부모 Scope의 변수와 다른 자원에 접근이 가능하다. 즉, 하위 함수는 부모의 실행 컨텍스트에 바인딩된다. Lexical scopeStatic Scope라고도 불린다.

function grandfather() {
    var name = 'snyung';
        // likes is not accessible here
    function parent() {
            // name is accessible here
            // likes is not accessible here
        function child() {
            // Innermost level of the scope chain
            // name is also accessible here
            var likes = 'Coding';
        }
    }
}

Function

함수는 자바스크립트에서 중요한 컨셉이다. 자바스크립트에서 함수는 1급 객체이다.

  1. Function Declaration(함수 선언식)
  2. Function Expression(함수 표현식)
  3. Named Function Expression(이름이 있는 함수 표현식)

함수 선언식

function [name](param1, param2, ...param3) {
    // Function Body & Logic
}

[function name] 앞에 [function keyword]를 붙인다. 항상 앞에 function으로 시작하며 함수의 이름을 지어주어야 한다. 선언식의 주요한 개념은 **호이스팅(Hoisting)**이 된다는 것이다.

호이스팅으로 인해서 함수를 선언하기 전에 함수를 실행하는 코드를 넣어도 작동하는 것이다.

이러한 선언 방법은 일부 논리를 함수 본문으로 추상화를 하고 나중에 실제 구현이 완료될 때 유용하다.

var num1 = 10;
var num2 = 20;
var result = add(num1, num2); // ==> 30 [Executing before declaring]

function add(param1, param2) {
    return param1 + param2 ;
}

위와 같은 코드는 좋지 못하다. 항상 호이스팅을 이용하는 것이 아닌 함수를 먼저 선언을 하고 실행하는 습관을 들이는 것이 좋다.

함수 표현식

어떤 값을 다른 변수에 할당하는 명령문은 표현식으로 간주한다.

var a = 100;
var b = 'Hello World';

함수 표현식의 경우 이름이 없이 함수를 작성하며 변수에 할당한다.

var [name] = function(param1, param2, ...param3) {
    // Function Body & Logic
}

foo(1,3,4);

함수 선언식과 다르게 정의될 때까지 함수를 사용할 수 없다. 즉, 호이스팅이 일어나지 않는다는 것을 의미한다. 정확하게 보면 변수는 호이스팅이 일어나지만, 할당이 이루어지는 행위는 호이스팅이 안 된다고 보는 것이 좋을 듯 하다.

var num1 = 10;
var num2 = 20;
var result = add(num1, num2);  
// Uncaught TypeError: add is not a function
var add = function(param1, param2) {
    return param1 + param2 ;
}

위와 같은 코드는 작동하지 않는다. 아래와 같이 작성을 하여야한다.

var num1 = 10;
var num2 = 20;
var add = function(param1, param2) {
    return param1 + param2 ;
}
var result = add(num1, num2); // ==> 30

함수 표현식의 장점

선언식보다 표현식이 더 유용하게 사용되는 몇 가지 이유가 있다.

  • As closures
  • As arguments to other functions
  • As Immediately Invoked Function Expressions (IIFE)

이름이 있는 함수 표현식 - 두 가지 접근 방식의 결합

선언식과 표현식의 차이점을 보고 두가지를 섞으면 어떻게 되는지 살펴보자.

var num1 = 10;
var num2 = 20;
var addVariable = function addFunction(param1, param2) {
    return param1 + param2 ;
}

위에 코드를 보게 되면 표현식이 더이상 익명이 아니고 addFunction 이라는 이름을 가지고 있다. 또한 addVariable 이라는 변수명에 할당하였다.

우리가 함수의 이름으로 addFunction을 추가했다고 실행할 수 있는 것은 아니다.

var result = addFunction(num1, num2); 
// ==> Uncaught ReferenceError: addFunction is not defined

우리가 할당한 addVariable 변수로만 사용이 가능하다.

var result = addVariable(num1, num2); 
// ==> 30

고려해야 할 사항

  1. addFunctionaddVariable 보다 콜스택상 먼저 나오게 된다.
  2. 외부에서 addFunction 을 호출하게 되면 에러가 나오게 된다.
    1. 그러나 내부에서는 addFunction 을 사용할 수 있다.
var num1 = 10;
var num2 = 20;
var addVariable = function addFunction(param1, param2) {
    var res = param1 + param2;
    if (res === 30) {
        res = addFunction(res, 10);
    }
    return res;
}
var result = addVariable(num1, num2); // ==> 40

결과가 30이 아니라 내부적으로 addFunction 이 한번 더 호출되어 40이 나오게 된다.

  1. IE8이하에서는 이름이 있는 함수 표현식을 사용하게 되면 심각한 이슈가 발생하게 되는데, 바로 완전히 다른 두개의 함수객체를 생성한다는 것이다.(Double take).

IE8을 지원해야하는 일이 있으면 익명의 표현식을 사용하는 것을 추천한다.

Statements, Expressions

Expressions

Expressions는 단일값이 되는 자바스크립트 코드 Snippets이다. 표현식은 원하는만큼 길게 사용이 가능하지만 단일값이다.

2 + 2 * 3 / 2
    
(Math.random() * (100 - 20)) + 20
    
functionCall()
    
window.history ? useHistory() : noHistoryFallback()
    
1+1, 2+2, 3+3
    
declaredVariable
    
true && functionCall()
    
true && declaredVariable

위의 예제들은 모두 표현식이다. 흔히 값을 원할때 어디서든 사용하는 방법이다. 그래서 아래의 예제에서도 값이 단일값으로 나오게 된다.

console.log(true && 2 * 9) // 18

표현식은 상태를 변경하지 않는다.

var assignedVariable = 2; //this is a statement, assignedVariable is state

assignedVariable + 4 // expression
assignedVariable * 10 // expression
assignedVariable - 10 // expression

console.log(assignedVariable) // 2

위의 예제들은 표현식임에도 할당된 값은 마지막까지 2로 남는다. 함수 호출은 표현식이지만 함수 상태를 변경할 수 있는 문장은 필수적이다.

const foo = () => {
    assignedVariable = 14
}

foo()는 표현식이지만 undefined나 다른 값을 반환한다. 그러나 이렇게 사용하게 됨으로써 상태를 변화시킬 수 있다.

const foo = () => {
  return 14 //explicit return for readability
}

assignedVariable = foo()

더 좋은 방법은 아래와 같이 작성하는 것이다.

const foo = (n) => {
    return n//explicit return for readability
}

assignedVariable = foo(14)

이렇게 작성을 하면 코드가 읽기 쉽게 구성이 가능하며 표현식과 명령문을 명확하게 구분할 수 있다. 이런 것이 선언적 자바스크립트의 근본이다.

Statements

기본적으로 문장은 행동을 수행한다.

자바스크립트에서 값이 필요한 곳에서는 명령문을 사용할 수 없다. 그래서 함수의 인수, 할당의 오른쪽, 연산자, 피연산자, 반환값으로 사용할 수 없다.

foo(if () {return 2}) 

명령문의 종류

  1. if
  2. if-else
  3. while
  4. do-while
  5. for
  6. switch
  7. for-in
  8. with (deprecated)
  9. debugger
  10. variable declaration

브라우저 콘솔창에서 아래와 같이 입력을 치게 되면,

if (true) { 9+9 }

18을 반환한다. 그러나 원하는 곳에 사용할 수 없다. 명령문은 아무것도 반환하지 않기를 바란다. 우리가 그것을 사용할 수 없다면 반환된 값은 쓸모가 없어지기 때문이다.

IIFE(Immediately Invoked Function Expression)

우리가 흔히 즉각 실행함수라 부르는 패턴이다. 이것을 사용하면 함수는 새로운 Scope를 만들게 된다. IIFE 는 단순하게 함수 표현식이다. 인터프리터가 즉각적으로 실행한다.

익명함수의 표현식과 비슷하게 생겼다.

var foo = 'foo';
    
(function bar () {
  console.log('in function bar');
})()
    
console.log(foo);

위에서 간단하게 보면 foo가 출력되기 전에 bar()를 호출하지 않았는데 in function bar 가 출력이 되었다.

  • ( 괄호를 사용해서 함수를 감싸서 선언식이 아닌 표현식이 된다.
  • 마지막에 () 괄호를 다시 써서 표현식을 즉시 실행하는 구문이 된다.

ES6 이전에는 IIFE를 사용해서 외부에서 접근하지 못하도록 변수를 숨기고 제한하는데 사용이 되었다. 또한 비동기 작업을 실행하고 IIFE 범위에서 변수상태를 보존하려는 경우에도 매우 유용하다.

for (var i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log('index: ' + i);
    }, 1000);
}

위에 코드는 흔히 발생하는 잘못된 코드이다. 이와 같은 코드를 IIFE를 사용해서 해결할 수 있다.

for (var i = 0; i < 5; i++) {
    (function logIndex(index) {
        setTimeout(function () {
            console.log('index: ' + index);
        }, 1000);
    })(i)
}

그러나 ES6+를 사용한다면 Block Level Scope 를 지원하는 let 또는 const 를 사용하면 된다.

부록

Semi-colon vs Comma operator**

세미콜론을 사용함으로써 표현식을 표현식 문장으로 변환 시킬 수 있다. 2+2 자체는 표현식이지만 완전한 라인은 문장이다.

2+2 // on its own is an opposition
    
foo(2+2) //so you can use it anywhere a value is expected
    
true ? 2+2 : 1 + 1
    
function foo () {return 2+2}
    
2+2; //expression statement
foo(2+2;) //syntaxError

세미콜론을 사용하면 여러 줄을 한 줄로 표현이 가능하다.

const a; function foo () {}; const b = 2

쉼표 연산자를 사용하면 여러 표현식을 연결하여 마지막 표현식만 반환이 가능하다.

console.log( (1+2,3,4) ) //4
console.log( (2, 9/3, function () {}) ) // function (){}
console.log( (3, true ? 2+2 : 1+1) ) // 4

모든 표현식은 왼쪽에서 오른쪽으로 계산이 되고 마지막 표현식이 반환된다.


Reference

Module

  • Webpack, SystemJS 같은 도구는 무엇일까?
  • AMD, UMD, CommonJS는 무엇인가?
  • 이것들은 어떤 관련이 있는가?
  • 왜 그걸 왜 필요로 하게 되었을까?

모듈이란?

모듈은 구현사항을 캡슐화하고 기능에 따라 Public API로 노출하여 다른 곳에서 쉽게 불러서 사용하도록 하며, 재사용이 가능하도록 한 코드 뭉치이다.

왜 모듈이 필요하게 되었을까?

  1. 추상적인 코드 : 전문 라이브러리에 기능을 위임하여 실제 구현의 복잡성을 이해할 필요가 없도록 하기 위해서
  2. 코드 캡슐화 : 코드를 변경하지 못하도록 하기 위해 모듈 내부의 코드를 숨기기 위해서
  3. 재사용 코드 : 같은 코드를 계속해서 사용하기 위해서
  4. 의존성 관리 : 우리의 코드를 다시 작성하지 않고 쉽게 종속성을 변경하기 위해서

Module patterns in ES5

기존의 자바스크립트는 모듈을 염두해두고 설계가 된 언어가 아니다. 시간이 지나면서 사람들이 필요에 따라 다양한 패턴을 만들게 된 것이다.

우리가 간단하게 볼만한 패턴은 IIFE와 공개 모듈 패턴이 있다.

즉시 실행 함수 표현(Immediately-invoked Function Expression)

IIFE는 ES5기준으로 가장 많이 사용되던 패턴 중 하나이다. Scope Block을 만드는 유일한 방법은 함수이기 때문이다. 따라서 아래와 같은 예제는 ReferenceError가 나온다.

Function 다시보기

(function() {
    var scoped = 42;
}());
    
console.log(scoped); // ReferenceError

IIFE는 오픈소스라이브러리에서 Block scope를 만드는 데 사용되었다. 이렇게 하게 되면 우리가 만들면서 공개하는 것과 아닌 것을 구분할 수 있다.

var myModule = (function() {
    // private variable, accessible only inside the IIFE
    var counter = 0;
    
    function increment() {
        counter++;
    }
    
    // publicly exposed logic
    return {    
        increment: increment
    }
}());

ES6에서는 modules라는 스펙이 추가되어 modules를 사용할 수 있다. 현재 이 모듈은 자신의 범위를 선언하고 모듈 내부에서 생성된 변수는 전역객체에서 부를 수 없도록 한다.

!function() {
    alert("Hello from IIFE!");
}();

위의 예제는 놀랍게도 IIFE이다. 우리가 흔히 알고 있는 모양새와 다르다고 생각해서 아니라고 할 수 있다. 그러나 우리가 사용하는 IIFE에서의 괄호는 표현식으로 나타내는 것에 불가하다. 그래서 표현식으로 나타낼 수 있는 어떤 것이든 생성 후 바로 실행이 되도록 하는 문장이 될 수 있는 것이다.

void function() {
    alert("Hello from IIFE!");
}();

또한 void 역시 기본적으로 함수가 표현식으로 취급되도록 한다.

전통적인 IIFE

처음에 예제에서 보았듯이 IIFE 패턴의 핵심은 함수를 표현식으로 바꾸고 즉시 실행하는 것이다.

(function() {
    alert("I am not an IIFE yet!");
});

위의 코드는 단순하게 함수를 괄호로 감싸고 있다. 그러나 이 함수식은 실행이 바로 되지 않으므로 즉시 실행 함수가 아니다. 이걸 IIFE로 바꾸기 위해서는 2가지의 스타일을 변환이 필요하다.

// Variation 1
(function() {
    alert("I am an IIFE!");
}());
    
// Variation 2
(function() {
    alert("I am an IIFE, too!");
})();

위의 예제에는 익숙한 문법이 있기도 하고 문법이 이상하다고 생각되는 것도 있다. 간단하게 살펴보면

  1. Variation 1의 4행에서 호출을 위한 괄호를 안쪽에 넣었다. 다시 바깥 괄호는 함수 밖의 함수 표현식을 만드는 데 필요하게 된다.
  2. Variation 2에서 마지막줄의 괄호는 함수 표현식을 호출하기 위한 괄호가 밖에 위치하고 있다.

두방법은 널리 사용되고 있다. 핵심부분으로 들어가게되면 2가지의 작동이 다르다. 그러나 상관없이 자신이 원하는 방법을 사용하면 된다.

작동하는 예제와 작동하지 않는 두가지 예제를 보자 익명의 함수를 사용하는 것은 좋은 방법이 아니므로 지금부터는 IIFE의 이름을 적어주자

// Valid IIFE
(function initGameIIFE() {
    // All your magical code to initalize the game!
}());
    
// Following two are invalid IIFE examples
function nonWorkingIIFE() {
    // Now you know why you need those parentheses around me!
    // Without those parentheses, I am a function definition, not an expression.
    // You will get a syntax error!
}();
    
function () {
    // You will get a syntax error here as well!
}();

위의 예제를 실행하게 되면서 우리가 왜 괄호가 필요한지 알게될 것이다. IIFE를 만들려고 하면 우리는 표현식이 필요하다. 함수 선언, 함수 문장은 필요가 없다.

IIFEs and private variables

(function IIFE_initGame() {
    // Private variables that no one has access to outside this IIFE
    var lives;
    var weapons;
        
    init();
    
    // Private function that no one has access to outside this IIFE
    function init() {
        lives = 5;
        weapons = 10;
    }
}());

IIFEs with a return value

var result = (function() {
    return "From IIFE";
}());
    
alert(result); // alerts "From IIFE"

IIFEs with parameters

(function IIFE(msg, times) {
    for (var i = 1; i <= times; i++) {
        console.log(msg);
    }
}("Hello!", 5));

Classical JavaScript module pattern

우리는 전체 시퀀스를 손상시키지않도록 작동하는 싱글톤 객체를 구현해보려고 한다.

 var Sequence = (function sequenceIIFE() {
        
    // Private variable to store current counter value.
    var current = 0;
        
    // Object that's returned from the IIFE.
    return {
    };
        
}());
    
alert(typeof Sequence); // alerts "object"

위의 예제를 간단하게 설명하면 sequenceIIFE라는 이름을 가진 함수를 표현식으로 나타내어 IIFE 패턴을 적용하였으며 private 변수로 current 를 선언해서 0을 담았으며 return으로 Object를 반환해서 Sequence라는 변수에 담고 있다. 그러므로 당연하게 그것의 타입은 object가 나오게 된다.

var Sequence = (function sequenceIIFE() {
    // Private variable to store current counter value.
    var current = 0;
        
    // Object that's returned from the IIFE.
    return {
        getCurrentValue: function() {
            return current;
        },
            
        getNextValue: function() {
            current = current + 1;
            return current;
        }
    };
}());
    
console.log(Sequence.getNextValue()); // 1
console.log(Sequence.getNextValue()); // 2
console.log(Sequence.getCurrentValue()); // 2

좀 더 그럴듯하게 꾸며보자. 위의 예제에서 return ObjectgetCurrentValue, getNextValue 2가지의 함수를 만들어서 반환하고 있다. 그리고 후에 반환된 Object 의 함수를 실행시켜서 IIFE 안쪽에 current 값을 바꾸거나 가져오고 있다.

위의 current 라는 변수는 IIFE의 전용이므로, 클로저를 통해 접근할 수 있는 함수 외에는 변수를 수정하거나 직접 접근은 불가능하다.

이렇게 IIFE와 클로져를 같이 사용해서 구현해보았다. 이것은 모듈 패턴에 대한 매우 기본적인 변형이다. 더 많은 패턴이 존재하지만 거의 모든 패턴이 IIFE를 사용하여 폐쇄범위를 만든다.

When you can omit parentheses

var result = function() {
    return "From IIFE!";
}();

함수 표현식 주위의 괄호는 기본적으로 함수가 명령문이 아닌 표현식이 되도록 한다. 위의 예에서 function 키워드는 명령문의 첫 단어가 아니라서 자바스크립트가 이걸 선언문이나 정의로 취급하지 않는다. 마찬가지로 표현식이라는 것을 알 경우 괄호를 생략할 수 있다.

그러나 다른 개발자들분들도 그렇게 생각할지 모르지만, 항상 괄호를 붙이는 것이 가독성에 좋다.

싱글톤 대신 모듈로 구현하여 생성자 함수를 노출할 수도 있다.

// Expose module as global variable
var Module = function(){
    
    // Inner logic
    function sayHello(){
        console.log('Hello');
    }
    
    // Expose API
    return {
        sayHello: sayHello
    }
}

var module = new Module();

module.sayHello();  
// => Hello

Module formats

기존의 ES6이전에는 자바스크립트에 모듈을 정의하는 공식문법이 존재하지 않았다. 그래서 다양한 모듈을 정의해왔었는데

  • Asynchronous Module Definition (AMD)
  • CommonJS
  • Universal Module Definition (UMD)
  • System.register
  • ES6 module format

Asynchronous Module Definition (AMD)

AMD 형식은 브라우저에서 사용되며 정의함수를 사용하여 모듈을 정의한다.

//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {
    
    //Define the module value by returning a value.
    return function () {};
});

CommonJS format

CommonJS 형식은 Node.js에서 사용되며 requiremodule.exports를 사용하여 종속성, 모듈을 정의한다.

var dep1 = require('./dep1');  
var dep2 = require('./dep2');
    
module.exports = function(){  
  // ...
}

Universal Module Definition (UMD)

UMD는 브라우저와 Node에서 모두 사용이 가능하다.

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['b'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('b'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.b);
    }
}(this, function (b) {
    //use b in some fashion.
    
    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));

System.register

System.register형식은 ES5에서 ES6 모듈 구문을 지원하도록 설계되었다.

import { p as q } from './dep';
    
var s = 'local';
    
export function func() {  
    return q;
}
    
export class C {  
}

ES6 module format

    // lib.js
    
    // Export the function
    export function sayHello(){  
      console.log('Hello');
    }
    
    // Do not export the function
    function somePrivateFunction(){  
      // ...
    }

사용하려면 import로 불러서 사용한다.

import { sayHello } from './lib';
    
sayHello();  
// => Hell

아직 브라우저는 ES6문법을 지원하지 않는다. 이미 ES6 모듈 포맷을 사용할 수 있지만, 브라우저에서 코드를 실제로 실행하기 전에 코드를 AMDCommonJS와 같은 ES5 모듈 형식으로 바꾸기 위해 Babel과 같은 변환기가 필요하게 된다.

Module loaders

모듈 로더는 특정 모듈 형식으로 작성된 모듈을 해석하고 load한다.

런타임 시점에 loader를 실행한다.

  • 브라우저에서 모듈 로더를 로드한다.
  • 모듈 로더에게 어느 파일을 로드할 것인지 알려준다.
  • 모듈 로더가 주파일을 다운로드해서 해석한다.
  • 필요한 경우에는 모듈 로더가 파일을 다운로드한다.

널리 사용되는 모듈 로더는 아래와 같이 있다.

  • RequireJS : AMD 형식의 모듈용 로더
  • SystemJS : AMD, CommonJS, UMD 또는 System.register 형식의 모듈용 로더

Module bundlers

모듈 번들러는 모듈 로더를 대체한다. 그러나 로더와 차이점은 모듈 번들은 빌드시에 실행된다.

  • 빌드할 때 모듈 번들을 실행하여 번들파일을 생성한다.
  • 브라우저에서 번들을 로드한다.

널리 사용되는 모듈 번들러는 아래와 같이 있다.

  • Browserify : CommonJS 모듈용 bundler
  • Webpack : AMD, CommonJS, ES6 모듈용 bundler

Reference

Event Loop

자바스크립트는 싱글 쓰레드이다. 그래서 비동기를 처리하기 위해서는 다른 누군가의 도움이 필요하다.

우리가 자바스크립트를 기본적으로 브라우저에서 사용을 한다. 그렇다면 당연하게 자바스크립트의 한계를 보완해주는 역할은 브라우저가 해준다는 것이다. 브라우저가 해주는 많은 역할 중 하나는 비동기처리를 도와주는 것이다.

오늘은 비동기를 처리하는데 있어서 큰 역할을 하고 있는 것을 알아보도록 하자.

목차

  • [x] Heap
  • [x] Stack
  • [x] Browser or Web APIs
  • [x] Event Table
  • [x] Event Loop

자바스크립트는 스크립트가 실행이 되는 엔진이 있다. 크롬을 기준으로 본다면 엔진은 V8이 된다. 이 엔진을 구성하는 요소는 크게보면 Heap과 Stack으로 구성이 되어있다.

Heap

객체는 Heap에 할당이 된다. Heap은 메모리에서 대부분 구조화되지 않은 영역을 나타낸다.

Stack

자바스크립트 코드 실행을 위해 제공된 단일 쓰레드이다. 함수를 호출하게 되면 하나의 Stack Frame이 형성이 된다.

더이상의 자세한 내용은 CallStack에 대해서 작성한 글을 참고 해주세요.

CallStack 알아보기


Browser or Web APIs

흔히 WebAPI라 불리는 API들은 웹 브라우저에 내장되어 있으며 브라우저 및 이외 컴퓨터 환경에서 데이터를 노출 할 수 있으며 복잡한 환경에서 유용하게 사용할 수 있다.(ex. 위치정보등등)

이것은 자바스크립트에 포함되는 것이 아니며 자바스크립트 언어를 사용하는데 있어 강력한 성능을 제공한다.

예제로 살펴보기

function main(){
    console.log('A');
    setTimeout(
    function display(){ console.log('B'); }
    ,0);
    console.log('C');
}
main();
//	Output
//	A
//	C
//  B

event_loop

  1. main함수가 실행이 되어서 처음에 들어가게 된다. main함수 안에 있는 console.log 가 스택에 쌓이게 된다. 함수가 실행이 되면 알파벳이 콘솔창에 출력이 된다.
  2. 다음으로 setTimeout 이 들어오면서 실행이 된다. setTimeoutbrowserAPI 의 콜백을 지연하는 함수를 사용한다.
  3. 브라우저에서 타이머가 이루어지는 동안 console.log 가 실행이 되고 C가 출력이 된다. 여기서 초가 0이더라도 콜백은 메시지 큐에 담기게 되어 브라우저는 그것을 받는다.
  4. main함수가 모두 다 끝나게 되면 스택이 비게 된다. 그러면 브라우저가 큐에 쌓았던 콜백을 실행하게 된다.

스택이 비었을때 자바스크립트 엔진은 메시지 큐가 비었는지 확인을 한다. 만약에 비었다면 엔진은 첫번째 메시지를 지우고 함수의 안을 실행한다. 이때 새로운 스택프레임(inital frame)이 생성이 되고 스택에 쌓인다. 함수가 끝났다면 initial 프레임이 스택에서 제거가 된다. 이러한 과정은 메시지 큐가 없을 때까지 이루어진다.

The Event Loop

위에서 언급을 했던 끊임없이 비었는지 검사하는 것이 바로 이벤트 루프이다. 이벤트 큐에 대기중인 항목이 있으면 호출 스택으로 이동한다. 그렇지 않으면 아무일도 일어나지 않는다.

setTimeout(() => console.log('first'), 0)

console.log('second')

위의 경우는 second가 찍히고 first가 찍히게 된다.

멀티쓰레드라면 하나의 일을 하고 있을 때 다른 쓰레드를 사용해서 일을 처리할 수 있지만 자바스크립트는 싱글쓰레드이기 때문에 불가능하다. 그래서 비동기를 하는데 있어서 이벤트 루프는 필수적이다.

while (await messageQueue.nextMessage()) {
  let message = messageQueue.shift();
  message.run();
}

이벤트 루프는 메시지 큐에 메시지가 더 있는지 확인하는 루프이다.

메시지 큐에 메시지가 있으면 메시지 큐에서 다음 메시지를 제거하고 그 메시지와 연관된 기능을 실행한다. 그렇지 않으면 새 메시지가 메시지 대기열에 추가 될때까지 대기를 한다. 이벤트루프가 자바스크립트에게 비동기를 허용하는 기본 모델이다.


Reference

Async

자바스크립트는 기본적으로 싱글 쓰레드이다. 이 말을 쉽게 하면 한번에 1가지의 일을 할 수 밖에 없다. 간단한 예제를 들자면 우리가 요리를 한다고 하면, 야채를 썰면서 물을 끓이는 행위를 동시에 할 수 없다는 것을 뜻한다. 이러한 불편한 점을 알았는지 벤더들은 자바스크립트의 싱글 쓰레드를 확장 시켜줄 API를 만들어 주었다.

  • [x] setInterval
  • [x] setTimeout
  • [x] requestAnimationFrame
  • [ ] requestIdleCallback


setInterval

자바스크립트는 브라우저 내에서 작동하며 기본 동작은 2번째 인자로 받은 ms 마다 1번째 인자로 받은 CallBack Function 을 실행하는 것을 전제로 한다.

setInterval 이 실행이 되면 WEB API에서 시간을 기다리고 있다가 특정시간 마다 큐에 넣게 된다. 그러나 이것은 CallStack 이 비어있어야 하며 다른 작업을 계속해서 하고 있다면 한 없이 기다리게 될 수도 있다.

이렇게 setInterval 은 지연이 발생할 수 있으며 시간에 따라 증가하게 된다.

이러한 이유는 3가지로 정리 할 수 있다.

  • 앱을 실행하는 기기의 하드웨어 제한사항
  • 브라우저의 비활성탭에서 실행되도록 앱남기기(멈추지 않고 계속해서 실행이 된다.)
  • 최적화되지 않은 전체 코드베이스

Async_1

위의 사진을 간단하게 보면 dummyMethod1() 이 오래 걸리면 자바스크립트의 이벤트 루프는 본연의 특징으로 인해서 스택에 걸려버렸다. 이런 상황이 되면 실행하기 위해서 기다리는 방법밖에 없다.

이렇게 우리가 조작을 할 수 없는 3번의 순간에 보낸다. 타이머라는게 이상적인 상황일 때는 우리가 생각하는 그 시간에 갈 수 있지만 브라우저와 자바스크립트는 그렇게 이상적이지 않다.


setTimeout

setTimeoutsetInterval 을 한 번 실행하는 것과 동일하다.

Async_2

위에서 했던 내용을 이번에는 setTimeout 의 재귀적 호출로 해보자 그렇게 된다면 결국 setTimeoutCallBack FunctionsetTimeout 이 다시 불리는 구조가 될 것이다. 이렇게 만들어서 실행을 한다면 우리가 생각했던 것과 더욱이 달라질 것이다. setInterval 은 내가 정한 시간에 맞춰서 CallBack을 실행 하려고 큐에 담았을 것이다. 그러나 setTimeoutcallback function이 불려야 다음 setTimeout 이 실행이 될 수 있는 조건이 되어 interval 보다 지연이 더 심해 질 수 있다.

지연예시

var counter = 0;
    
var fakeTimeIntensiveOperation = function() {
    
    for(var i =0; i< 50000000; i++) {
        document.getElementById('random');
    }
    
    let insideTimeTakingFunction  = new Date().toLocaleTimeString();
    console.log('insideTimeTakingFunction', insideTimeTakingFunction);
}
    
    
var timer = setInterval(function(){ 
    
    let insideSetInterval = new Date().toLocaleTimeString();
    
    console.log('insideSetInterval', insideSetInterval);
    
    counter++;
    if(counter == 1){
        fakeTimeIntensiveOperation();
    }
    
    if (counter >= 5) {
        clearInterval(timer);
    }
}, 1000);
    
//insideSetInterval 13:50:53
//insideTimeTakingFunction 13:50:55
//insideSetInterval 13:50:55 <---- not called after 1s
//insideSetInterval 13:50:56
//insideSetInterval 13:50:57
//insideSetInterval 13:50:58 


requestAnimationFrame

기본적으로 브라우저는 60FPS 이다 그래서 1초에 60번을 실행하게 되면 애니메이션이 깔끔하게 보인다. 그렇다면 위에서 알게 된 setInterval 을 사용해서 표현을 하면

setInterval(function() {
    // animiate something
}, 1000/60);

이런식으로 표현이 가능하다. 그러나 위에서 언급을 했지만 문제가 있다. 이에 2017년 requestAnimationFrame 이라는 기능이 크롬의 Paul Irish에 의해서 추가가 되었다.

Paul의 설명에 의하면

  • 브라우저가 애니메이션을 최적화 할 수 있으므로 애니메이션이 부드럽게 처리될 수 있다.
  • 비활성 탭의 애니메이션이 중지되어 CPU가 시원해진다.
  • 더욱이 배터리 친화적이다.

가장 간단한 예제를 보면

function repeatOften() {
    // Do whatever
    requestAnimationFrame(repeatOften);
}

requestAnimationFrame(repeatOften);

한번 실행하면 재귀적으로 호출한다.

requestAnimationFrame 역시 취소하기 위햇 setTimeout setInterval과 마찬가지로 ID를 반환한다.

globalID = requestAnimationFrame(repeatOften);

cancelAnimationFrame(globalID);

그러나 아래의 링크를 보면 알게되지만 모든 브라우저가 지원하는 것은 아니다.

브라우저 지원여부 확인하기(https://caniuse.com/#feat=requestanimationframe)

예제

https://codepen.io/seonhyungjo/pen/MRVPxL

이외의 Async

requestIdleCallback

Observer

  • mutation
  • resize
  • intersection
  • performance

Reference

JavaScript Engine

목차

  • [x] 자바스크립트 엔진 파이프라인
  • [x] V8
    • [x] 이그니션
    • [x] 터보팬
    • [x] 이전의 엔진
  • [x] 히든 클래스
  • [x] 인라인 캐싱
  • [x] 최적화

JavaScript Engine이란?

JS코드를 실행하는 프로그램(가령 브라우저) 또는 인터프리터 를 말한다.

V8

제일 유명하고 사람들이 많이 사용하는 크롬에 들어가있는 V8 이 있다. 현재 Electron, Nodejs 에서도 사용이 되고 있고 CEF의 안에도 들어있다.

SpiderMonkey

최초의 자바스크립트 엔진 으로, JS의 창시자인 브랜던 아이크가 넷스케이프 브라우저를 위해 개발이 되었다. 지금은 Mozilla 재단에서 관리하며, FireFox 에서 사용되는 엔진이다.

Chakra

마이크로소프트가 개발한 엔진 이며, Edge 브라우저에 사용되고 있고 앞으로는 V8로 바꾼다는 말이 있다.

Chakra 엔진의 중요 부분은 Chakra Core 라는 오픈 소스로 구성되어있다.

Javascript Core

애플에서 개발한 JavaScriptCore는 처음에 WebKit 프레임워크 를 위해 개발. 최근에는 Safari와 React Native App에서 사용된다고 한다.

자바스크립트 엔진 파이프라인

소스코드를 기계어로 만드는 과정에 대해서 알아보려고 한다.

  1. 자바스크립트 소스를 파싱해서 AST로 만든다.
  2. AST를 토대로 인터프리터는 바이트코드로 만들어준다.

코드를 더 빨리 실행하기 위해서, 바이트코드는 프로파일링 된 데이터와 함께 optimizing compiler 로 보내지고 여기서는 프로파일된 데이터를 기반으로 하여 최적의 기계어를 생성 하게 된다.

바이트 코드 + 프로파일된 데이터 => 최적의 기계어

그러나 정확하지 않은 결과가 나왔다면 다시 deoptimizes하여 바이트코드로 되돌린다.

B_Engine_1.png

위의 파이프라인 작동하는 방식은 Chrome 및 Node.js에서 사용되는 JavaScript 엔진이 작동하는 방식과 거의 동일하다.

B_Engine_2.png

V8의 인터프린터 Ignition 이라고 불리며, bytecode 를 생성하고 실행하는 역할을 한다. Bytecode 를 실행하는 동안, 실행 속도를 높이기 위해서 profiling data 를 수집한다. 예를 들어, 종종 실행되는 기능에 부하가 걸리면, 생성된 bytecode 와 profiling data 는 TurboFan(최적화된 컴파일러)으로 전달되어 profiling data 를 기반으로 최적화 머신 코드( optimized code )를 생성합니다.

V8 살펴보기

V8.5.9 이전

내용이 많아 자세히 설명해주신 블로그 링크를 넣었습니다.

https://engineering.huiseoul.com/자바스크립트는-어떻게-작동하는가-v8-엔진의-내부-최적화된-코드를-작성을-위한-다섯-가지-팁-6c6f9832c1d9


JavaScript Engine이 객체 모델을 구현하는 방법

객체는 JavaScript 명세에 따르면 String으로 된 키와 이것으로 접근할 수 있는 값들을 가지고 있는 딕셔너리(Key-Value)이다. 키는 단순히 [[value]] 에 맵핑되는 것 뿐만 아니라 속성 값(property attributes) 이라고 하는 스펙에도 매핑이된다.

B_Engine_3.png

객체는 기본적인 속성 값으로 [[Writable]], [[Enumerable]], [[Configurable]] 상태가 있다.

  • Writable : 할당연산자를 통해 값을 바꿀 수 있는지
  • Enumerable : 해당 객체의 키가 열거 가능한지
  • Configurable : 이 속성 기술자는 해당 객체로부터 그 속성을 제거 할 수 있는지

어떤 객체나 속성이든 Object.getOwnPropertyDescriptor API를 이용해 이 값들에 접근할 수 있습니다.

const object = { foo: 42 };
    Object.getOwnPropertyDescriptor(object, 'foo');
    // => {value: 42, writable: true, enumerable: true, configurable: true}


JavaScript 배열

배열은 조금 다르게 처리하는 특별한 객체라고 생각하면 됩니다.객체와 다른 배열만의 특징은 다음과 같습니다.

  1. 인덱스(index)가 존재한다.

인덱스는 제한된 범위가 있는 정수로. JavaScript 명세에 따르면, 배열은 2³²−1 개 까지의 아이템을 가질 수 있다. 따라서 배열 인덱스는 0 부터 2³²−2 까지의 범위에서만 인덱스로 유요한 정수라는 것이다.

  1. 길이(length) 정보를 가집니다.

length property 는 배열에 추가하면 length property 는 저절로 늘어난다. 사실 엔진에서 자동으로 해주는 것이다.

const array = ['a', 'b'];
    array.length; // 2
    array[2] = 'c';
    array.length; // 3


JavaScript 엔진에서 배열을 다루기

객체와 비슷하다. 배열은 인덱스를 포함하여 모두 string 키를 가진다. 아래 그림을 보면 인덱스인 0 은 a 라는 값을 가지며, 값을 바꿀 수 있고(Writable), 열거 가능하고(Enumerable), 삭제 가능(Configurable) 하다. 또 다른 프로퍼티인 length 의 값은 1이며, 값을 바꿀 수 있지만 열거와 삭제가 불가능 하다.

B_Engine_4.png

배열에 Item을 추가하게 되면, JavaScript 엔진은 length의 속성 값 중 [[value]]를 자동으로 증가시킨다.

B_Engine_5.png


Hidden Class(Shape)

function logX(obj){
        console.log(obj.x);
}

const obj1 = { x:1, y:2 };
const obj2 = { x:3, y:4 };

logX(obj1);
logX(obj2);

동일한 프로퍼티 x와 ystring 키로 가지는 두 객체가 있다면. 이 두 객체의 모양(shapes)은 똑같다.

함수 logX를 통해 두 객체 각각에서 같은 프로퍼티 x 에 접근한다. JavaScript 엔진은 프로퍼티 접근 시에 모양이 같은 점을 이용하여 최적화를 한다.

B_Engine_6.png

객체의 키 xy는 각각의 속성 값(property attributes)을 가리킨다. 예를 들어 x 프로퍼티에 접근하게 되면 엔진은 Object 에서 x 키를 찾은 다음, 해당하는 속성 값을 불러오고 [[Value]] 값을 반환한다.

여기서 5와 6 같은 데이터는 어디에 저장되나?

모든 객체 별로 정보를 저장하게 되면 낭비가 된다. 비슷한 모양의 객체가 더 많이 생긴다면, 그만큼의 중복할 발생할 것이고 필요없는 메모리 사용이 늘어나게 되는 것이다.

그래서 엔진은 직접 값을 저장하는 방법 아래와 같은 방법을 사용하게 된다.

B_Engine_7.png

우선 엔진은 따로 Shape 라는 곳에 프로퍼티 이름과 속성 값들을 저장한다. 여기에 [[value]] 값 대신 JSObject 의 어디에 값이 저장되어 있는지에 대한 정보인 OffsetProperty information으로 가지고 있는다.

B_Engine_8.png

즉, 같은 모양을 가진 모든 JSObject는 동일한 Shape 인스턴스를 가리키게 되고, 각 객체에는 고유한 값만 저장되므로, 더 이상 중복되지 않는 것이다. 같은 모양으로 생긴 더 많은 오브젝트가 있다 하더라도 오로지 하나의 Shape 만 존재하게 된다.


Shape에 새로운 객체를 추가하기 (Transition chains)

이런 Shape가 있다고 합시다.

const o = {};
o.x = 5;
o.y = 6;

새로운 프로퍼티를 추가할 때, 엔진은 어떻게 새로운 Shape를 찾을수 있는 것일까? 엔진은 내부에 transition chains라고 하는 Shape를 만든다.

먼저, 비어있는 객체인 o가 있으며, 이는 비어있는 Shape를 가리킨다.

B_Engine_9.png

여기에 5라는 값을 가진 x 라는 프로퍼티를 추가하게 되면, 비어있던 Shape에서 x 를 프로퍼티로 가지고 있는 새로운 Shape로 이동(transition)하게 된다. 다음과 같이 JSObject 의 값이 추가되고, offset 은 0이다.

B_Engine_10.png

새로운 프로퍼티 y를 추가해도 똑같이 작동하게 된다. Shape(x,y) 로 이동한 다음 값을 추가한다.

B_Engine_11.png

하지만 이런 방법을 모든 테이블에 항상 적용했다가는 많은 메모리 낭비를 일으키겠지요. 그래서 실제로 엔진은 이렇게 동작하지 않습니다.


실제로 엔진에서 동작하는 방법

엔진은 추가되는 새로운 프로퍼티 정보를 저장하고, 이전 Shape로 가는 링크를 제공한다. 만약 o.x를 찾을 때 값이 Shape(x,y) 에 없다면 이전의 Shape(x)에 가서 찾는 것이다.

B_Engine_12.png


두 객체에서 동일한 Shape를 사용하는 경우 (Transition Tree)

만약에 두 객체에서 동일한 Shape를 사용한다면 어떻게 될까? 먼저 하나의 객체 ax = 5 라는 값을 가진 프로퍼티가 있다고 하면

B_Engine_13.png

이번엔 객체 b 에서 y 라는 프로퍼티를 추가할 경우 Shape(empty)에서 가지를 뻗어 새로운 Shape(y)를 만든다. 결국 2개의 체인에 3개의 Shape를 가진 트리 체인이 생성되는 것이다.

B_Engine_14.png

java의 Object처럼 모든 객체의 트리를 거슬러 올라가면 무조건 Shape(empty)에 도달하게 되는 것은 아니다.

const obj1 = {};
obj1.x = 6;

const ob2 = {x: 6};

ojb2 와 같이, JS에서는 Object Literal 을 사용하여 시작부터 프로퍼티를 가지고 생성하도록 할 수 있기 때문이다. 따라서 Shape(empty)가 아닌, 서로 다른 Root Shape가 생성된다.

B_Engine_15.png

이 방법은 transition chain 을 짧게 하고, 객체를 리터럴로부터 생성하여 더욱 효율적이다. point는 x,y,z 를 3차원 공간의 좌표로 가지는 객체이다.

const point = {};

point.x = 4;
point.y = 5;
point.z = 6;

앞서 배운 것에 따르면, 총 3개의 Shape가 메모리에 생성 될 것입니다. (empty Shape 제외)

B_Engine_16.png

만약 이걸 사용하는 프로그램에서 x 프로퍼티에 접근한다고 하면, 엔진은 가장 마지막에 생성된 Shape(x,y,z) 부터 링크드 리스트를 따라올라가 맨 위에 있을 x 를 찾는다.

객체의 프로퍼티가 더 많을수록, 그리고 이 과정을 자주 반복한다면 프로그램은 상당히 느려질 것이다.

그래서 엔진은 탐색 속도를 높이기 위해 내부적으로 ShapeTable 이라는 자료구조를 추가한다. 이는 딕셔너리 형태로, 각각의 Shape를 가리키는 프로퍼티 키를 저장하고 있다.

B_Engine_17.png

그렇다면 기껏 Shape가 나온 이유가 없어지는 것인가? 사실 엔진은 최적화를 위해 또 다른 방법인 Inline Cache(IC) 라는 것을 Shape에 적용한다. 

Chrome dev_tool Memory Tab에서 예제를 확인해보자!!!

:point_right: 예제

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

var foo = new Person("yonehara");
var bar = new Person("suzuki");

:point_right: 예제2

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

var foo = new Person("yonehara");
var bar = new Person("suzuki");
foo.job = "frontend";


Inline Cache(ICs)

Shape를 사용하는 주된 이유는 Inline Caches(ICs) 때문이다. ICs 는 JavaScript를 신속하게 실행할 수 있게하는 핵심 요소이다. JavaScript 엔진은 ICs를 사용하여 object에서 property를 찾을 수 있는 위치에 대한 정보를 암기하여, 높은 cost를 가지는 조회 횟수를 줄인다.

function getX(o) {	
    return o.x;
}

위의 함수를 JSC 에서 실행한다면, 아래의 그림과 같은 bytecode 를 생성할 것이다.

B_Engine_18.png

첫 번째 명령문 get_by_id는 첫 번째 argument (arg1) 에서 property 'x' 를 로드하여 결과를 loc0 에 저장한다.

두 번째 명령문은 loc0 에 저장한 것을 반환한다.

또한 JSC 는 get_by_id 명령문에 초기화되지 않은 두 개의 슬롯으로 구성된 Inline Cache 를 포함한다.

B_Engine_19.png

이제 위의 그림과 같이 {x: 'a'} object 가 getX 함수에서 실행될 때를 보게 되면, objectproperty 'x' 가 있는 shape를 가지며, 이 Shape는 property x 에 대한 offsetattribute 들을 가집니다. 이 함수를 처음 실행하면, get_by_id 함수property 'x' 를 검색하고 value는 offset 0 에 저장되어 있다는 것도 찾게된다.

B_Engine_20.png

위의 그림에서 처럼 get_by_id 명령문에 포함된 IC는 property 가 발견된 shape와 offset을 기억하게 된다.

B_Engine_21.png

위의 그림을 보게되면, 다음 명령문을 실행할 때, IC는 shape만 비교하면 되며, 이전과 같다면 저장되어있는 offset을 보고 value를 가져오면 된다. 구체적으로 말하면, 엔진이 IC가 이전에 기록한 shape의 object를 볼 경우, 더 이상 property 정보에 접근할 필요가 없다. 그리고 비용이 많이 들어가는 property 정보 조회를 완전히 생략하게 된다. 이 방법은 매번 property를 조회하는 것 보다 훨씬 더 빠르다.


어떻게 최적화된 자바스크립트 코드를 작성할 것인가

  1. 객체 속성의 순서 : 객체 속성을 항상 같은 순서로 초기화해서 히든클래스 및 이후에 생성되는 최적화 코드가 공유될 수 있도록 한다.
  2. 동적 속성: 객체 생성 이후에 속성을 추가하는 것은 히든 클래스가 변하도록 강제하고 이전의 히든클래스를 대상으로 최적화되었던 모든 메소드를 느리게 만든다. 대신에 모든 객체의 속성을 생성자에서 할당한다.
  3. 메소드 : 동일한 메소드를 반복적으로 수행하는 코드가 서로 다른 메소드를 한 번씩만 수행하는 코드 보다 더 빠르게 동작합니다(인라인 캐싱 때문)
  4. 배열 : 값이 띄엄띄엄 있어서 키가 계속해서 증가하는 숫자가 되지 않는 배열은 피하는게 좋다. 모든 요소를 가지지는 않는 배열은 해시테이블이다. 이와 같은 배열의 요소들은 접근하기에 많은 비용이 든다. 또한 커다란 배열을 미리 할당하지 않도록 하는 것이 좋다. 사용하면서 크기가 커지도록 하는 게 좋다. 마지막으로 배열의 요소를 삭제하지 말아야한다. 그 배열의 키가 띄엄띄엄 배치된다.
  5. 태깅된 값 : V8은 객체와 숫자를 32비트로 표현한다. 어떤 값이 오브젝트(flag = 1)인지 혹은 정수(flag = 0)인지는 SMI(Small Integer) 라는 하나의 비트에 저장하고 이 때문에 31비트가 남는다. 따라서 어떤 숫자가 31비트 보다 크면 V8은 이 숫자를 분리해서 더블 타입으로 전환한 다음 이 숫자를 넣을 새로운 객체를 생성한다. 이러한 동작은 비용이 높으므로 가능한한 31비트의 숫자를 사용하자.

Reference

Bitwise Operator

우리가 흔히 AND를 표현할 때 && 를 사용하며 OR을 표현할때 || 를 사용한다. 왜 두 개씩 사용해서 표현을 하는 것일까? 하나로는 표현이 안되는 것인가?

이유는 한 개를 사용하는 &, |는 비트연산자에서 사용이 되고 있기 때문이다.

Bits(비트)란?

비트는 숫자나 문자 또는 문자열과 함께 작동하지 않으며 이진 숫자만 사용한다. 쉽게 말하면 모든 것이 이진 형식으로 저장된다. 저장된 이진형식은 컴퓨터에서 UTF-8과 같은 인코딩을 사용하여 저장된 비트 조합을 문자, 숫자 또는 다른 기호에 Mapping한다.

당연히 가지고 있는 비트가 많을수록 더 많은 순열과 더 많은 것을 표현할 수 있다.

자바스크립트에서 우리가 비트를 얻는 방법은 간단하다. 가령 숫자 하나가 있다면 (Number).toString(2) 를 하게 되면 얻어오게 된다. 자바스크립트에서 바이너리를 직접 입력 할 수 있는 방법이 없다 이진수를 10진수로 변환하려면 parseInt(111,2) 를 해야한다.

parseInt - MDN


Bitwise Operator(비트연산자)란?

비트 수준에서 변수와 상호 작용하는 방법이다. 비트는 부동소수점, 정수로 변환되므로 정보를 쉽게 소화가능하다. 속도와 효율성을 중요시한다면 비트로 직접 처리, 변환하는 것이 유용하다.

비트 연산자(AND, OR, XOR)는 일반 논리연산자와 비슷하게 동작한다. 비트 수준에서는 일반적으로 논리를 해석하는 방식이 아니라 비트 수준에서 평가를 한다.

비트 연산자를 사용하면 어떤 이점이 있을까? 비트 수준에서의 평가는 일반적인 논리연산자보다 빠르기 때문에 큰 샘플에 대한 평가 또는 반복은 비트 연산이 더욱 효율적이다.

& (AND)

&& 논리 연산자와 매우 비슷하다. 비교되는 비트가 둘 다 1인 경우 1을 반환한다. 가령 12와 15가 있다면 각각 11001111 이다. 그것을 &연산자를 사용한다면 1100을 얻는다. 결과는 12가 나오게 된다.

하나의 Trick(트릭)으로 숫자가 짝수인지 홀수인지를 알아낼 수 있다. 숫자가 홀수인 경우 첫번째 비트는 항상 1이다. 따라서 &를 사용하여 1과 비교할 수 있다. 그러나 실제로는 사용하지 않는 것을 추천한다.

| (OR)

|| 논리 연산자와 매우 비슷하다. 비교되는 비트가 모두 0이거나 0과 1이 하나씩 있을 경우 1을 반환한다. 비트별로 이진수를 비교하는데 사용이 된다. 예제에 |연산자를 사용하면 15가 반환된다.

1100 | 1111는 비교를 하면 각각에 대해 1를 반환하여 15이 된다.

~ (NOT)

모든 비트를 1은 0으로 0은 1로 바꾼다. 즉 무엇이든 반대로 되돌린다는 것이다. 그런데 ~15 를 하게 되면 이상하게 결과가 -16이 나오게 된다. 이유는 2의 보수연산에서 숫자의 음수 표현을 얻으려면 먼저 비트를 뒤집어서 1을 더해야 하기 때문이다.

^ (XOR)

이 연산자는 XOR 연산자 또는 배타적 OR연산자라고 불린다. &| 같은 연산자는 양측의 숫자를 가져와서 비교하는 방식으로 이 연산자와는 차별화된다. 해당 비트를 비교하여 하나의 1이 있을 때만 1을 반환한다. 즉, 1 ^ 0 은 1을 반환하지만 1 ^ 1은 0을 반환한다.

Shifting operators

Shifting 비트(<<, >>)를 다루는 두 개의 연산자가 있다. 추측할 수 있듯이 차이점은 숫자의 비트를 이동시킨다는 것이다.

  • << : 숫자의 모든 비트를 왼쪽으로 n번 이동한다. 이동을 할 때 발생하는 빈 공간은 0으로 채워진다.
  • >> : 숫자의 모든 비트를 오른쪽으로 n번 이동한다. 이 연산자는 양수 비트를 0으로 채우고 음수 비트를 1로 채운다.

보통 숫자의 첫 번째 비트가 기호를 나타내는데 사용이 된다. 1이면 음수 0이면 양수이다. 따라서 오른쪽 이동에서 위에 있는 추론은 우리가 이동하는 숫자의 부호를 유지하려고 저런식으로 작동하는 것이다.

Bit manipulation

이제 연산자가 하는 일을 알았으니 비트를 조작하기 위해 연산자를 활용하는 방법을 살펴보자.

마스킹(Masking)

마스킹이라는 것은 단순하게 간단한 문자열을 보내고 다른 숫자를 할당하여 플래그를 나타내는 방법이다. 일련의 예 또는 아니오 질문을 신속하게 요청하는 방법이다.

우리가 웹사이트를 가지고 있고 4개의 Flag가 있다고 가정을 해보자

  1. A : 유저권한인가?
  2. B : 올바른 지역에 있나?
  3. C : 아이스크림을 먹을 수 있나?
  4. D : 로봇인가?

이렇게 4개의 Flag가 있다고 생각하면 4자리의 이진문자열로 표현해서 보낼 수 있다.

0000 = DCBA

어떤 위치에 1을 넣으면 플래그를 변경할 수 있다.

  • 1000 (binary) = Flag D = 8 (integer)
  • 0100 (binary) = Flag C = 4 (integer)
  • 0010 (binary) = Flag B = 2 (integer)
  • 0001 (binary) = Flag A = 1 (integer)

이렇게 되면 한 번에 2가지 이상의 Flag의 변경이 가능하다.

  • 1010 (binary) = Flag D and Flag B = 10 (integer)
  • 0111 (binary) = Flag C, B and A = 7 (integer)
function changeFlag(binary){
    const flagD = 8
    const flagC = 4
    const flagB = 2
    const flagA = 1
    let flags = []
    
    if(binary & flagD){
        flags.push("Change flagD")
    }
    if(binary & flagC){
        flags.push("Change flagC")
    }
    if(binary & flagB){
        flags.push("Change flagB")
    }
    if(binary & flagA){
        flags.push("Change flagA")
    }
    return flags
}

// 최대는 15
changeFlag(8)
changeFlag(11)
changeFlag(1)
changeFlag(15)


Cheat Sheet

이미지로 확인하기

Operator Usage Description
Bitwise AND a & b 왼쪽 피연산자와 오른쪽 피연산자의 비트가 모두 1 인 경우 1을 반환한다.
Bitwise OR a | b 왼쪽 또는 오른쪽 피연산자의 비트가 하나인 경우 각 비트에서 하나를 반환한다.
Bitwise XOR a ^ b 왼쪽 피연산자와 오른쪽 피연산자 둘 다 아닌 경우 비트 위치의 1을 반환한다.
Bitwise NOT ~a 피연산자의 비트를 뒤집는다.
Left shift a << b a를 이진수 표현 b 비트를 왼쪽으로 shift하고 오른쪽에 0을 shift한다.
Sign-propagating right shift a >> b a를 이진수로 b 비트를 오른쪽으로 shift하고, 오른쪽으로 나온 비트를 제거한다.
Zero-fill right shift a >>> b a를 이진수로 b 비트를 오른쪽으로 shift하고, shift off 한 비트를 버리고, 왼쪽에서 0을 shift한다.


Reference

DOM

들어가기에 앞서 먼저 BOM에 대해서 알아보자.

당연하게 웹은 브라우저에서 돌아가기 때문에 웹 개발을 하다 보면 브라우저와 밀접한 관계를 가지게 된다. 브라우저와 관련된 객체들의 집합을 브라우저 객체 모델(BOM : Browser Object Model)이라고 부른다. BOM을 사용하면 창을 이동하고 상태 표시줄의 텍스트를 변경하는 페이지 내용과 직접 관련이 없는 브라우저와 관련된 기능을 사용할 수 있다. DOM은 이 BOM 중 하나이다.

예시) history, location, navigator, screen

BOM의 최상위 객체는 window라는 객체이고, DOM은 window 객체의 하위 객체이다.

GIF_window document

DOM이란?

문서 객체 모델(The Document Object Model(DOM)) 은 HTML, XML 문서의 프로그래밍 interface 이다. - MDN

HTML에는 <html>, <head>, <body>와 같은 많은 태그가 있는데 이를 JavaScript로 사용할 수 있도록 객체로 만들면 그것을 Document Object하고 한다.

DOM은 문서의 구조화된 표현을 제공하며 프로그래밍 언어(JavaScript 등)가 DOM 구조에 접근할 수 있는 방법을 제공하여 문서 구조, 스타일, 내용 등을 변경할 수 있도록 해준다.

DOM은 어떻게 생겼나?

DOM의 모양을 이해하는데 선행되는 자료구조는 Tree 구조이다. DOM이 바로 Tree 형식의 자료구조를 가지고 있기 때문이다.

이름 그대로 Tree 구조는 나무가 땅에서 솟아 위로 뻗어 나가면서 가지를 치면서 나가는 모양으로, DOM은 거꾸로 있는 모양이다.

DOM_Tree

> The HTML DOM Tree of Object(by w3school)

DOM은...

  • 문서의 구조화된 표현을 제공하며 프로그래밍 언어가 DOM 구조에 접근할 수 있는 방법을 제공한다.
  • 문서 구조, 스타일, 내용 등을 변경할 수 있게 돕는다.

우리가 위와같이 조작을 할 수 있는 이유는 DOM API를 제공하기 때문이다. 아래의 사진은 Node 하위 구조를 보여주고 있다.

DOM 계층

출처: http://www.stanford.edu/class/cs98si/slides/the-document-object-model.html

프로토타입 기반으로 본다면 아래와 같은 구조를 가진다.

Object < EventTarget < Node < DocumentType < <!DOCTYPE html>(ElementNode)

Object < EventTarget < Node < Element < HtmlElement < HtmlhtmlElement < html(ElementNode)
  • 아래 코드와 같이 HTML Element Node는 상속(Object, EventTarget, Node, Element, HtmlElement, HTMLhtmlElement) 받은 모든 객체의 속성 및 메서드들을 사용할 수 있게 된다.
const html = document.querySelector('html');

console.log(html); // html
console.log(html.__proto__); // HTMLhtmlElement
console.log(html.__proto__.__proto__); // HtmlElement
console.log(html.__proto__.__proto__.__proto__); // Elemenet
console.log(html.__proto__.__proto__.__proto__.__proto__); // Node
console.log(html.__proto__.__proto__.__proto__.__proto__.__proto__); // EventTarget
console.log(html.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__); // Object


DOM과 HTML 코드의 차이점

우리가 웹페이지를 만들 때 HTML을 작성한다. 그렇다면 우리가 작성하는 이 소스가 DOM과 똑같을까?

우리가 작성한 소스는 브라우저가 읽어서 DOM Tree를 만든다.

참고) Im-D/Dev-Docs 브라우저의 작동 원리

HTML 코드는 DOM과 똑같은 것으로 예상되지만 브라우저에서 생성한 DOM과는 엄연히 다르다. 예시로 우리가 작성한 코드 중 중대한 오류가 아닌 이상 브라우저가 자동으로 소스 코드의 오류를 수정한다. (Ex. tbody)

code1

code1_devtool

위의 사진을 비교하게 되면 왼쪽은 실제 코드를 작성한 것이고 오른쪽은 실제 DOM으로 만들어진 모양이다. 실제로 tbody 태그를 작성하지 않았지만 만들어주는 것을 보여준다.

이외

  • HTML 파일에 단어 하나라도 존재하더라도 브라우저는 이를 html과 body 으로 감싸고 head를 필수적으로 추가한다.

code2

code2_devtool

  • DOM을 생성하는 과정에서 여는 태그는 작성을 하고 닫는 태그를 작성하지 않는 경우 자동 생성하여 맞춰서 오류가 발생하지 않는다.

code3

code3_devtool

그렇다면 우리가 작성한 코드가 실제 DOM으로 만들어진 것을 어디서 볼 수 있나?

DevTools

우리가 브라우저로 소스를 열어서 F12버튼 또는 Ctrl + Shift + i를 누르게 되면 브라우저 DevTools이 나오게 된다.

dev_tools

Element Tab

Element가 실제로 그려진 DOM Tree를 볼 수 있는 곳으로 Element의 스타일 이벤트 등을 볼 수 있으며 실제로 조작을 하면서 변화를 확인해 볼 수 있다.

Console Tab

JavaScript를 사용해서 DOM을 조작할 수 있다. 실제로 JavaScript 엔진을 사용해서 테스트를 해보고 싶을 때 많이 사용하는 공간으로 IntelliSense를 보는 공간으로도 사용이 가능하다.

브라우저에서 테스트할 때 원하는 Element를 JavaScript로 찾기 힘들 때 Element Tab에서 해당 Element를 클릭 후 Console 창에서 $0으로 호출해서 바로 사용이 가능하다.

기본적으로 브라우저에서 클릭 된 history를 보관하고 있어 이전에 선택한 Element를 다시 가져올 수 있다.

devtools2


Reference

Class

ES6에 Class 문법이 추가되었다. Class는 특별하게 새로 만들어진 것이 아니다. 기존에 존재하고 있던 상속과 생성자 함수를 기본으로 한 Proptotypesyntactic sugar이다.

Class 문법이 나오기 전 사람들은 JavaScript를 OOP답게 사용하고 싶어 했다. 그래서 다양한 방법을 사용해서 구현했다.


Constructor Function

Class 문법이 생기기 전에는 JavaScript에서는 모든 것을 Function으로 만들어야 했다. 그래서 사람들이 Class처럼 사용하기 위해서 Constructor function을 사용해서 비슷하게 사용하였다.

function Vehicle(make, model, color) {
    this.make = make,
    this.model = model,
    this.color = color,
    this.getName = function () {
        return this.make + " " + this.model;
    }
}

위와 같이 함수를 선언함으로써 Java에서 사용하는 Class와는 모양새는 다르지만, Class에 좀 더 가까워졌다. 이제 우리는 Vehicle이라는 것을 선언했으니 인스턴스를 만들어보자

const car = new Vehicle("Toyota", "Corolla", "Black");
const car2 = new Vehicle("Honda", "Civic", "White");

위와 같이 new 키워드를 사용해서 인스턴스를 만들 수 있다.

그런데 문제가 있다.

새로운 Vehicle()을 만들 때 JavaScript 엔진은 두 객체의 각각에 대한 Vehicle Constructor 함수 사본을 만든다. 즉 각각의 인스턴스의 공간이 만들어지는 것이다. 또한 속성과 메소드는 Vehicle()의 모든 인스턴스에 복사가 된다.

이게 왜 문제가 되는가? 라고 생각할 수 있다. 제일 문제는 Constructor 함수의 멤버 함수(메서드)가 모든 객체에서 반복된다. 계속 똑같은 멤버 함수를 만드는 것이다. 이것은 매우 불필요하게 낭비를 하기 때문이다.

다른 문제는 기존의 만든 객체에 새로운 속성이나 메서드를 추가를 하지 못한다는 것이다.

car2.year = "2019"

위와 같이 추가를 하더라도 다른 인스턴스에는 새로운 속성이나 메서드는 따로따로 넣어주어야 한다.

function Vehicle(make, model, color, year) {
    this.make = make,
    this.model = model,
    this.color = color,
    this.year = year, // 위와 같이 추가를 해야한다.
    this.getName = function () {
        return this.make + " " + this.model;
    }
}


Prototype

JavaScript는 내부적으로 새로운 함수가 만들어지면 엔진은 prototype이라는 기본 속성을 추가한다.

image

prototype__proto__ 속성은 dunder proto 라고 불리고, Constructor 함수의 prototype 속성을 가리킨다.

생성자 함수의 새로운 인스턴스를 만들어질 때마다 dunder proto는 다른 속성, 메서드와 함께 인스턴스에 복사가 된다.

image

prototype 객체는 아래와 같이 Constructor 함수의 속성, 메서드를 추가 가능하며 Constructor 함수의 모든 인스턴스에서 사용할 수 있다.

car.prototype.year = "2019"

image

이 접근법에는 몇 가지 주의 사항이 있는데 prototype 속성과 메서드는 Constructor 함수의 모든 인스턴스와 공유를 한다. 만약 인스턴스의 하나가 기본 속성에서 변경했다면 모든 인스턴스가 아닌 해당 인스턴스에서만 변경이 된다.

다른 한 가지는 참조 유형 속성이 모든 인스턴스 간에 항상 공유된다. 하나의 인스턴스에서 수정하면 모든 인스턴스에 대해 수정이 된다.

image


Class

이제 Constructor 함수와 prototype에 대해 알아봤다. 이제 Class에 대해 알아보자. 위의 2개를 살펴봄으로써 좀 더 쉽게 이해할 수 있다. 이전의 것들과 크게 차이가 나는 것이 없기 때문이다.

JavaScript 클래스는 prototype 기능을 활용하여 Constructor 함수를 작성하는 새로운 방법일 뿐이다.(syntactic sugar)

class Vehicle {
    constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
    }

    getName() {
        return this.make + " " + this.model;
    }
}

생성 방법은 위처럼 만들면 된다. 위와 동일하게 인스턴스를 생성해보면 동일하게 new 키워드를 사용하면 된다.

const car = new Vehicle("Toyota", "Corolla", "Black");

Class로 만든 것을 기존 방법으로 다시 작성한다면 아래와 같을 것이다.

function Vehicle(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
}

Vehicle.prototype.getName = function () {
    return this.make + " " + this.model;
}
    
const car = new Vehicle("Toyota", "Corolla", "Black");

이것은 Class가 Constructor 함수를 수행하는 새로운 방법이라는 것을 증명한다. 더욱 실제 Class와 비슷하게 만들기 위해서 도입된 몇 가지 규칙이 있다.

생성자가 작동하려면 new 키워드가 필요하다.

const car = new Vehicle("Toyota", "Corolla", "Black");

new를 사용하지 않고 만들 경우 아래와 같은 에러가 발생한다.

Class의 메서드는 non-enumerable 하다.

JavaScript에서 객체의 각 속성에는 해당 속성에 대해 enumerable flag 가 있다. Class는 prototype에 정의된 모든 메서드에 대해 이 flagfalse로 설정한다.

Class에 생성자를 추가하지 않으면 기본 빈 constructor()가 자동으로 추가된다.

constructor(){ }

Class 안의 코드는 항상 strict 모드 이다.

이것은 오류가 없는 코드를 작성하거나, 잘못된 입력 또는 코드 작성 중 구문 오류 또는 다른 곳에서 참조된 실수로 일부 코드를 제거하여 코드를 작성하는 데 도움을 준다.

Class는 호이스팅이 되지 않는다.

image

Class는 Constructor 함수나 객체 리터럴과 같은 속성 값 할당을 허용하지 않는다.

함수 또는 getter/setter 만 가질 수 있다. property:value 할당은 불가능하다.


Class Features

constructor

constructor는 Class 자체를 나타내는 함수 자체를 정의하는 Class 선언의 특수 함수이다. 인스턴스를 새로 만들면 constructor()가 자동으로 호출된다.

const car = new Vehicle("Honda", "Accord", "Purple"); // call constructor

constructorsuper 키워드를 사용하여 확장된 Class constructor를 호출할 수 있다.

하나 이상의 생성자 함수를 가질 수 없다.

Static Methods

Static Methods는 prototype 에 정의된 Calss의 다른 메서드와는 다르게 prototype이 아닌 Class 자체의 함수이다.

Static Methods는 static 키워드를 사용하여 선언하며 주로 유틸리티 함수를 만드는 데 사용된다. Class의 인스턴스를 만들지 않고 호출 가능하다.

class Vehicle {
    constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
    }

    getName() {
        return this.make + " " + this.model;
    }

    static getColor(v) {
        return v.color;
    }
}

let car = new Vehicle("Honda", "Accord", "Purple");

Vehicle.getColor(car); // "purple"

정적 메서드는 Class 인스턴스에서 호출할 수 없다.

Getter / Setter

getter/setter를 사용하여 속성값을 가져오거나 속성값을 설정할 수 있다.

class Vehicle {
    constructor(model) {
        this.model = model;
    }
    
    get model() {
        return this._model;
    }

    set model(value) {
        this._model = value;
    }
}

getter/setter는 Class prototype에 정의된다.

image

Subclassing

Subclassing은 Javascript Class에서 상속을 구현할 수 있는 방법으로 extends 키워드는 Class의 하위 클래스를 만드는 방법이다.

class Vehicle {
    constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
    }

    getName() {
        return this.make + " " + this.model;
    }
}

class Car extends Vehicle{
    getName(){
        return this.make + " " + this.model +" in child class.";
    }
}

const car = new Car("Honda", "Accord", "Purple");

car.getName(); // "Honda Accord in child class."

getName()를 호출하면 자식클래스의 함수가 호출된다.

가끔 부모의 함수를 사용해야 할 때가 있다. 그럴 때는 super 키워드를 사용하면 된다.

class Car extends Vehicle{
    getName(){
        return super.getName() +"  - called base class function from child class.";
    }
}
  • 옛날 방식인 prototype을 사용해서 상속 구현해보기
function Vehicle(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
}

Vehicle.prototype.getName = function() {
    return this.make + " " + this.model;
};

function Car(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
}
Object.setPrototypeOf(Car, Vehicle);
Object.setPrototypeOf(Car.prototype, Vehicle.prototype);
Car.prototype.getName = function() {    
    return this.make + " " + this.model +" in child class.";
};

const vehicle = new Vehicle("Honda", "Accord", "Purple");
const car = new Car("Honda", "Accord", "Purple");

console.log(vehicle.getName()); // Honda Accord
console.log(car.getName()); // Honda Accord in child class.


팩토리 디자인 패턴 간단히 살펴보기

위의 코드를 아래와 같이 변경을 함으로써 new 키워드를 사용하지 않고 매번 새로운 객체를 만들어서 전달받는다.

const Vehicle = function(make, model, color){
    const newVehicle = {};
    newVehicle.make = make;
    newVehicle.model = model;
    newVehicle.color = color;
    
    newVehicle.getName = function(){
        return this.make + " " + this.model;
    }
    return newVehicle;
};

const vehicle = Vehicle("Honda", "Accord", "Purple");

console.log(vehicle.getName()) //Honda Accord

Reference

this call apply bind

Function vs Method

한가지 예제를 살펴보고 console.log()가 어떻게 출력이 되는지 확인해보자.

// 함수
const greeting = () => {
  console.log(this);
}

const module = {
  greeting() {
    console.log(this);
  }
}

greeting(); // window object

module.greeting(); // module object

흔히 사람들이 실수하는 코드 중 하나이다.

여기서 제일 유심히 보아야 하는 것은 this가 다르다는 것이다. Java나 다른 언어에서의 this와는 다르게 작동하고 있다는 것은 확실하다.

this에 대한 더 많은 내용은 아래의 참고 글을 읽어보길 바란다.

참고
scope this
JavaScript의 this

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

Module.prototype.getName = function() {
  var changeName = function() {
    console.log(this); // window
    return this.name + '입니다.';
  }
    return changeName();
}

const module = new Module('sNyung');

console.log(module.getName());

위와 같이 메서드 내부에서 함수를 정의하고 this를 사용하게 되면 Module이라고 생각하지만 window를 바라보고 있다. 그리고 이걸 해결하는 방법은 흔히 self 또는 that을 사용해서 해결한다.

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

Module.prototype.getName = function() {
  const self = this;
  // const that = this;
  
  const changeName = function() {
    console.log(self); // Module
    return self.name + '입니다.';
  }
    return changeName();
}

const module = new Module('sNyung');

console.log(module.getName());

또는 ES6에서 추가가 된 화살표 함수(Arrow Function)를 사용해서 해결 가능하다.

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

Module.prototype.getName = function() {
  const changeName = () => {
    console.log(this); // Module
    return this.name + '입니다.';  
  }
    return changeName();
}

const module = new Module('sNyung');

console.log(module.getName());


call()

자바스크립트에서 object를 효율적으로 사용하면서 재사용 패턴까지 구현할 수 있는 유용한 방법으로 Function.prototype.call() 함수가 있다.

call은 ES6에서 화살표 함수가 나오기 전 self 또는 that를 사용하지 않고 this까지 해결할 수 있는 방법이다.

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

Module.prototype.getName = function() {
  const changeName = function() {
    console.log(this);
    return this.name + '입니다.';
  }
  // return changeName.call(this, 1,2,3,4);
  return changeName.call(this);
}

const module = new Module('sNyung');

console.log(module.getName());

call은 호출하는 즉시 Function을 실행시킨다.

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


apply()

이번에는 call과 매우 유사한 apply를 보자.

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

Module.prototype.getName = function() {
  const changeName = function() {
    console.log(this);
    return this.name + '입니다.';
  }
  
    // return changeName.apply(this, [1,2,3,4]);
    return changeName.apply(this);
}

const module = new Module('sNyung');

console.log(module.getName());

apply도 호출하는 즉시 Function을 실행시킨다.

fun.apply(thisArg, [argsArray])

call과 apply는 첫 번째 인자의 this를 내부 함수에서 사용할 this로 설정한다. apply는 call과 같이 첫 번째 인자로 this를 받는 건 똑같지만 뒤에 넘겨줄 값을 [1,2,3,4,5]처럼 배열 형태로 넘겨준다. (call과 같은 경우에는 1,2,3,4,5처럼 배열이 아닌 ,로 전달한다)


bind()

call과 apply는 내부 함수에서 사용할 this를 설정하고 함수 바로 실행까지 해주지만, bind는 this만 설정해주고 함수 실행은 하지 않고 함수를 반환한다.

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

Module.prototype.getName = function() {
  const changeName = function() {
    console.log(this);
    return this.name + '입니다.';
  }
  let bindChangeName = changeName.bind(this);
    return bindChangeName();
}

const module = new Module('sNyung');

console.log(module.getName());

Reference

Prototype

Main

Javascript는 Proptotype기반의 언어라고 한다.
그렇다면 js를 공부한다면 알아야한다.

Javascript의 기본을 익힐때 Object, Function 이렇게 2가지를 먼저 생각하고 알고 있으면 된다.



새로운 function 생성

먼저 새로운 function을 생성한다고 가정하자.

prototype


// 기본 function 생성
function Animal(){
    this.name = "동물";
}

프로토타입에 있어서 크게 2가지로 나누게 된다면

  1. function(생성자)
  2. prototype Object

prototype object는 말그대로 object이면서 인자로 가지고 있는 constructor는 object function을 가르킨다. 여기서 가르키는 functions이 1번의 function이다. function의 prototype은 prototype object를 가르키고 있다. 즉 서로를 바라보고 있다.



Function(생성자)

위에서 생성된 function은 생성자의 역할을 한다. 우리가 만약

prototype


// cat 생성
var cat = new Animal();

위와 같이 고양이를 생성하게 된다면 function Animal인 생성자를 실행하는 것이다.
또한 function Animal안을 보게 되면 prototype이라는 인자가 있다. 이것은 Animal.prototype하고 연결이 되있는 것이다.

Prototype Object

그렇다면 이녀석?은 무엇일까?
기본적인 Animal의 prototype을 추가하게 되면 들어가는 객체이다.
추가적으로 constructor 역시 이 곳으로 들어가게 되고 function Animal하고 연결이 되어있어 생성을 한다면 여기의 생성자를 타게 된다.


Animal.prototype.constuctor / Animal(){this.name = "동물";}

이렇게 function Animal과 Animal.prototype(Object)를 서로 연결이 되어있다.

Prototype Link

[[Prototype]]], __proto__ 2개는 의미가 똑같다 그거 표기법이 다른 것이다.
그렇다면 위의 2개는 무엇일까:question:
먼저 위에서 생성한 cat을 살펴보면

prototype


cat
//result
//animal {name: "test"}
//  name: "test"
//  __proto__: Object

cat의 내역에는 당연하게 생성자로 생성된 name이 있다.
그런데 그 아래는?? 들어보지 못한 것이 하나 더 생겼다.
이것이 바로 Javascript에서 중요한 역할을 하게 된다.


cat.hasOwnProperty("name") //true

위의 예제를 보게 되면 나는 hasOwnPropert이런 함수를 생성한 적이 없다. 이 함수는 어디서 오게된 것일까:question:

바로 최상의 Object인 Object.prototype에서 온 것이다.

어떻게 해서 그것까지 갈 수가 있던걸까:question: 라고 생각할 수 있다.
바로 그역할을 하는 것이 __proto__이다.

__proto__안을 보게 되면 constructor : Animal()이 있는 것을 볼 수 있다. 즉, cat을 생성한 functionprototype__proto__에 연결이 되어 있다는 것을 알게 되는 것이다.

prototype

또한 Animal.prototype.__proto__에는 Object.prototype에 연결이 되어있다. 그리고 그 안에는 우리가 사용한 hasOwnPropert이 있어서 사용을 하는 것이다.

그렇다면 Object.prototype.__proto__는 무엇일까:question: 아무것도 없다. 그냥 null이 나온다. 모든 마지막은 Object.prototype에서 끝이 나는 것이다.


Object.prototype.__proto__ // null



참고



간단한 심화

심화적으로 최종적으로 Object로 모든것이 연결이 되는 것도 있지만 Object function.__proto__가 f(){}에 연결이 되어있다. f(){}의 constructor는 Function function이다.

prototype

또한

Function.prototype Object의 __proto__는 Object.prototype Object에 연결이 되어있다.

즉,
이런걸로 봐서는 Function.prototype.__proto__는 당연히 객체임으로 최상위 Object.prototype에 연결이 되는 것이고, 추가적으론 Object function의 __proro__는 함수라고 생각을 하여 Function.prototype에 연결이 되는 것이다.

Object.create() & Object.assign()

Object.create()

Object.create(prototype_object, propertiesObject)

Object.create()는 기준이 되는 Object를 prototype으로 만들고 새로운 객체를 생성한다.

Object.create()는 주로 객체를 상속하기 위해 사용하는 메서드다. 첫 번째 인자를 상속하며, 두 번째 인자의 속성들을 추가로 구성한다.


두번째 인자 - 속성의 구성요소

공통의 구성요소는

  1. configurable 속성의 값을 변경할 수 있고, 삭제할 수도 있다면 true로 설정한다. 기본값은 false.
  2. enumerable 속성이 대상 객체의 속성 열거 시 노출이 되게 하려면 true로 설정한다. 기본값은 false.

데이터 서술을 하는 데 사용되는 키는

  1. value 속성값. 아무 JavaScript 값(숫자, 객체, 함수 등)이나 가능하다. 기본값은 undefined
  2. writable 할당 연산자로 속성의 값을 바꿀 수 있다면 true로 설정한다. 기본값은 false.

접근자 서술을 하는 데 사용되는 키는

  1. get 속성 접근자로 사용할 함수, 접근자가 없다면 undefined.
    • 속성에 접근하면 이 함수를 매개변수 없이 호출하고, 반환 값이 속성의 값이 된다. 이때 this 값은 이 속성을 가진 객체=이다. 기본값은 undefined.
  2. set 속성 설정자로 사용할 함수, 설정자가 없다면 undefined.
    • 속성에 값을 할당하면 이 함수를 하나의 매개변수로 호출한다. 이때 this 값은 이 속성을 가진 객체이다. 기본값은 undefined.

Object.create()의 2번째 인자는 Object.defineProperties()를 따른다.



위의 구성요소는 이전에 사용하던 Object.defineProperties()에서 나오는 개념이다. 그 전에 Object.defineProperties()의 단일 설정 함수인 Object.defineProperty()를 살펴보자


Object.defineProperty()

Object.defineProperty()는 객체에 직접 새로운 속성을 정의하거나 이미 존재하는 속성을 수정한 후, 그 객체를 반환한다.

Object.defineProperty(obj, prop, descriptor)
  • obj : 속성을 정의할 객체.
  • prop : 새로 정의하거나 수정하려는 속성의 이름 또는 Symbol.
  • descriptor : 새로 정의하거나 수정하려는 속성을 기술하는 객체.

아래의 예제는 자세히 보아야 한다. 여러 개념이 들어가 있는 예제이다.

const obj = {
  age: 27, 
  country : 'seoul'
}
let oldCountry = 'Yongin'

console.log('init str', obj) // init str {age: 27, country: "seoul"}

const descriptor = {
  enumerable: true,
  configurable: true,
  get: function(){
    return '???'
  },
  set: function(value){
    oldCountry = value
  }
}

const newObj = Object.defineProperty(obj, 'country', descriptor) // 기존의 속성에서 새로운 속성을 정의
console.log('obj values => ',obj, obj.age, obj.country) // obj values =>  {age: 27} 27 ???
console.log('newObj values => ',newObj, newObj.age, newObj.country) // newObj values =>  {age: 27} 27 ???
console.log('oldCountry => ', oldCountry) // oldCountry =>  Yongin

newObj.age = 28
newObj.name = 'sNyung'
newObj.country = 'Re Seoul'
console.log('After newObj && obj values are => ', obj, obj.age, obj.country) // After newObj && obj values are =>  {age: 28, name: "sNyung"} 28 ???
console.log('oldCountry => ', oldCountry) // oldCountry =>  Re Seoul


Object.defineProperties()

Object.defineProperties(obj, props)

Object.defineProperty()의 복수 버전이다.

Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});

Object.create() - Sample 1

const prototypeObject = {
  fullName: function(){
    return this.firstName + " " + this.lastName		
  }
}

const person = Object.create(prototypeObject, {
  'firstName': {
    value: "sNyung", 
    writable: true, 
    enumerable: true
  },
  'lastName': {
    value: "jo",
    writable: true,
    enumerable: true
  }
})
    
console.log(person) // {firstName: "sNyung", lastName: "jo"}

image


New VS Object.create()

Object.create()new Constructor()는 꽤 비슷하지만, 다른 점이 있다. 다음은 이 둘의 차이를 보여주는 예제이다.

function Dog(){
  this.pupper = 'Pupper';
};
Dog.prototype.pupperino = 'Pups.';

const maddie = new Dog();
const buddy = Object.create(Dog.prototype);

// Object.create()
console.log(buddy.pupper); // undefined
console.log(buddy.pupperino); // Pups.

// New
console.log(maddie.pupper); // Pupper
console.log(maddie.pupperino); // Pups.

성능상으로 new 생성자를 사용하는 것이 좋다고 한다. 확실히 성능 테스트를 해보면 알 수 있다.

성능 테스트

https://jsperf.com/snyung-new-vs-object-create

function Obj() {
  this.p = 1;
}

Obj.prototype.chageP = function(){
  this.p = 2
};

console.time('Object.create()');
var obj;
for (let i = 0; i < 10000; i++) {
  obj = Object.create(propObj);
}
console.timeEnd('Object.create()');
// Object.create(): 약 12.994140625ms

console.time('constructor function');
var obj2;
for (let i = 0; i < 10000; i++) {
  obj2 = new Obj();
}
console.timeEnd('constructor function');
// new: 약 2.379150390625ms


Polyfill

Object.create = (function () {
  function obj() {};

  return function (prototype, propertiesObject) {
    // 첫 번째 인자를 상속하게 한다.
    obj.prototype = prototype || {};

    if (propertiesObject) {
      // 두 번째 인자를 통해 속성을 정의한다.
      Object.defineProperties(obj.prototype, propertiesObject);
    }

    return new obj();
  };
})();

//--------------TEST---------------

const Vehicle = function(){
  this.wheel = 4
}

Vehicle.prototype.getWheel = function() {
  return this.wheel
}

const Motorcycle = Object.create(Vehicle.prototype, {'wheel': {value: 3, writable: true}});
console.log(Motorcycle)


Compat Table

image


Object.assign()

Object.assign() 메서드는 열거할 수 있는 하나 이상의 출처 객체로부터 대상 객체로 속성을 복사할 때 사용한다. 대상 객체를 반환한다.

  • target : 대상 객체.
  • sources : 하나 이상의 출처 객체.
const obj = { a: 1 };
const copy = Object.assign({}, obj);

console.log(copy); // { a: 1 }

Object.assign()은 첫 번째 인자로 들어오는 target을 대상으로 두 번째 이후로 들어오는 인자를 병합할 때 사용한다.

예시

같은 properties가 들어올 경우 마지막에 들어온 값이 덮어쓰게 된다.

// app.js

let o1 = { a: 21, b: 22, c: 24 };
let o2 = { b: 23, c: 25 };
let o3 = { c: 26 };

let finalObj = Object.assign({}, o1, o2, o3);
console.log(finalObj); // {a: 21, b: 23, c: 26}

그러나 안타깝게도 Object.assign()는 ES6 문법의 Spread Operator가 나오면서 관심이 많이 줄어들게 되었다. 같은 기능을 포함하고 있는 ES6 문법의 Spread는 숙련도에 따라 다양하게 사용될 수 있다.

가장 많이 사용되는 기능 3가지를 포함하고 있는 예시를 보게 되면,

// Create Function with REST
const add = function(...arg){
  console.log('arg', arg)
  arg.map(e => console.log(e))
}

// Create Object
const obj = { a : 1, b : 2, c: 3, d: 4}
// Create Arr + Spread
const arr = ['a', ...Object.values(obj), 'b', 'c']
console.log(...arr)
add(...arr)

const {a,b, ...rest} = obj
console.log(a, b, rest)

위와 같이 ...은 사용 방법에 따라 다양하게 사용될 수 있다. 더 많은 기능을 찾아보기를 바란다.

Polyfill

Object.defineProperty(Object, "assign", {
  value: function assign(target, varArgs) { // .length of function is 2
    'use strict';
    if (target == null) { // TypeError if undefined or null
      throw new TypeError('Cannot convert undefined or null to object');
    }

    var to = Object(target);

    for (var index = 1; index < arguments.length; index++) {
      var nextSource = arguments[index];

      if (nextSource != null) { // Skip over if undefined or null
        for (var nextKey in nextSource) {
          // Avoid bugs when hasOwnProperty is shadowed
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    return to;
  },
  writable: true,
  configurable: true
});


Compat Table

image


Reference

Map Filter Reduce

함수형 프로그래밍의 주요 기능 중 하나는 리스트와 리스트 연산자의 사용이다.

Javascript에는 초기 리스트에서 제공하는 모든 함수를 map, filter, reduce를 사용하여 원본 리스트는 그대로 유지하면서 다른 것으로 변환가능하다.

Map

구문

arr.map(callback(currentValue[, index[, array]])[, thisArg])

map() 메서드는 콜백함수를 적용하고 나오는 결과물을 가지고 새로운 배열로 반환한다.

객체 리스트에서 우리가 원하는 속성을 추가, 수정하는데 사용한다.

map은 callback을 인자로 받는다. 이 calllback은 iteration의 현재 값을 받으며 인덱스와 원본 배열도 같이 불린다.

optional하게 두 번째 인자로는 callback 내부에서 사용될 this를 바인딩한다.

V8 엔진 내부적으로 두번째 인자를 가지고 fn.call(secondArg, ...)를 사용하여 바인딩하고 있다.

Example

const numbers = [2, 4, 8, 10];
const halves = numbers.map(x => x / 2);
// halves is [1, 2, 4, 5]

pollyfill 만들어보기

Array.prototype.map = function (fn) {
  var args
  if(this == null){
    return new TypeError("this is null or not defined")
  }
  var o = Object(this)
  var len = o.length >>> 0;
  if(typeof fn !== 'function'){
    return new TypeError(fn, " is not a functions")
  }
  var arr = new Array(len)
  /* 추가적으로 들어오는 argument 처리 */
  if (arguments.length > 1) {
    args = arguments[1];
  }
  var n = 0;
  while (n < len){
    if (n in o) {
      arr[n] = fn.call(args, o[n], n, o)
      n++
    }
  }
  return arr
}

Map

Filter

arr.filter(callback(element[, index[, array]])[, thisArg])

주어진 함수에 대해서 조건을 통과한 요소들을 가지고 새로운 배열을 만들어서 반환하는 메서드.

filter는 map과 같은 arguments를 받는다. filter에서는 return 값으로 truefalse를 반환하면 된다. true를 반환하면 배열에 추가하며, false를 반환하면 추가하지 않는다.

결국, 새로운 배열이 나오되 true로 반환된 요소만 담겨서 나오게 된다.

const words = ["spray", "limit", "elite", "exuberant", "destruction", "present"];
const longWords = words.filter(word => word.length > 6);
// longWords is ["exuberant", "destruction", "present"]

pollyfill 만들어보기

Array.prototype.filter = function (fn) {
  var args
  if(this == null){
    return new TypeError("this is null or not defined")
  }
  var o = Object(this)
  var len = o.length >>> 0;
  if(typeof fn !== 'function'){
    return new TypeError(fn, " is not a functions")
  }
  var arr = new Array(len)
  if (arguments.length > 1) {
    args = arguments[1];
  }
  var n = 0;
  var i = -1;
  while (++i !== len){
    if (n in o) {
      if(fn.call(args, o[i], i, o)){
        arr[n++] = o[i]
      }
    }
  }
  arr.length = n
  return arr
}

Filter

Reduce

arr.reduce(callback[, initialValue])

reduce() 메서드는 accumulator와 배열의 각 요소(왼쪽에서 오른쪽으로)에 대해 함수를 적용하여 단일 값으로 반환한다.

map, filter와 비슷하지만, callback argument가 다르다. callback은 accumulator를 받는다. 이 값은 이전에 누적된 반환값으로 현재 값, 현재 인덱스, 원본 배열을 같이 받는다.

const total = [0, 1, 2, 3].reduce((sum, value) => sum + value, 1);
// total is 7

pollyfill 만들어보기

Array.prototype.reduce = function (fn) {
  var args;
  if(this == null){
    return new TypeError("this is null or not defined");
  }
  var o = Object(this);
  var len = o.length >>> 0; 
  if(typeof fn !== 'function'){
    return new TypeError(fn, " is not a functions");
  }
  var n = 0;
  var value;
  /* 추가적으로 들어오는 argument 처리 */
  if (arguments.length > 1) {
    value = arguments[1];
  }else{
    while (n < len && !(n in o)) {
      n++; 
    }
    if (n >= len) {
      throw new TypeError( 'Reduce of empty array with no initial value' );
    }
    value = o[n++];
  }
  
  while (n < len){
    if (n in o) {
      value = fn(value, o[n], n, o);
    }
    n++;    
  }
  return value
}

Reduce


Reference

JavaScript란?

JavaScript(JS)가벼운 인터프리터형, JIT-컴파일형 프로그래밍 언어, first-class functions를 지원한다.

주로 웹 페이지를 위한 스크립팅 언어로 알려졌지만, Node.js, Apache CouchDB, Adobe Acrobat처럼 많은 비 브라우저 환경에서도 사용된다.

JavaScript프로토타입 기반의 다중 패러다임 스크립팅 언어로서, 역동적이고, 객체지향형, 명령형 및 선언형(가령 함수형 프로그래밍) 스타일을 지원한다.

이 문서는 JavaScript 언어 자체만 다루며 웹 페이지를 비롯한 다른 사용 환경에 대해서는 다루지 않는다. 웹 페이지의 특정 API에 대하여 알고 싶다면 웹 APIDOM을 참고하면 됩니다.

JavaScript의 표준은 ECMAScript입니다.

2012년 기준 최신 브라우저는 모두 ECMAScript 5.1을 전부 지원한다.

이전 브라우저의 경우는 최소한 ECMAScript 3까지 지원한다.

2015년 6월 17일 ECMA International에서는 ㄴ공식명 ECMAScript 2015로 불리는 ECMAScript의 6번째 주 버전을 발표했다(ECMAScript 6 혹은 ES6).

그 이후 ECMAScript 표준은 출시가 1년 주기이다.

JavaScriptJava 프로그래밍 언어와 혼동해서는 안된다.

"Java"와 "JavaScript" 두 가지 모두 Oracle이 미국 및 기타 국가에 등록한 상표이다. 하지만, 두 언어는 문법 체계와 사용방법이 전혀 다릅니다.


Reference

ES3

ECMA-262-3 Execution Contexts

도입

오늘은 ECMAScript의 실행 컨텍스트와 이와 관련된 코드 유형에 대해 알아보자.

정의

Control이 ECMAScript 실행 코드로 이동될 때마다, Control은 실행 컨텍스트으로 들어가게 된다.

실행 컨텍스트(EC)는 ECMA-262 specification(사양)에서 실행 코드의 유형화와 차별화를 위해 사용하는 추상적인 개념이다.

이러한 표준은 기술적인 구현 관점에서 정확한 EC의 구조와 종류를 정의하지 않았다. 이는 결국 표준을 규현하는 ECMAScript 엔진의 달렸다.

논리적으로, 활성 실행 컨텍스트 집합은 stack으로 형성된다. 이 stack의 맨 아래에는 항상 global context가 있고, 가장 위에는 현재(활성) 실행 컨텍스트가 있다. 다양한 종류의 EC가 드나드는동안 stack이 수정된다.(pulled/poped)

실행 코드의 종류

실행 컨텍스트의 추상 개념으로 실행 코드 유형의 개념과 관련있다. 코드 유형에 대해 말하면 특정 순간의 실행 컨텍스트를 의미할 수 있다.

예를 들어, 실행 컨텍스트 stack을 배열로 정의해보자.

ECStack = [];

함수가 재귀적 또는 생성자로 호출되더라도 함수안으로 들어갈 때마다 stack에 push한다. 내장 eval 함수 작업에서도 그렇다.

Global code

이 유형의 코드는 Program 수준에서 처리된다. 즉, 로드된 외부 .js 파일 또는 로컬 인라인 코드(<script></script> 태그 내부)이다. Global code는 함수 본문에 있는 코드 부분은 포함되어 있지 않다.

초기화(프로그램 시작)시 ECStack은 다음과 같다.

ECStack = [
  globalContext
];

Function code

Function code (모든 종류의 함수)안으로 들어가게 되면, ECStack에 새로운 요소가 추가된다. concrete Function의 Code에는 내부 Function Code가 포함되어 있지 않다.

예를 들어, 한 번 재귀적으로 호출하는 함수를 보자.

(function foo(flag) {
  if (flag) {
    return;
  }
  foo(true);
})(false);

그러면 다음 ECStack이 다음과 같이 수정된다.

// first activation of foo
ECStack = [
  <foo> functionContext
  globalContext
];
  
// recursive activation of foo
ECStack = [
  <foo> functionContext – recursively 
  <foo> functionContext
  globalContext
];

함수에서 모든 리턴은 현재 실행 컨텍스트를 종료하고 이에 따라 ECStack은 stack의 자연스러운 구현에 따라 연속적으로 거꾸로 꺼내진다. 해당 코드의 작업이 완료되면, ECStack은 프로그램이 종료될 때까지 globalContext만 가지고 있는다.

Throw 되었지만 catch 되지 않은 예외는 하나 이상의 실행 컨텍스트를 종료할 수 있다.

(function foo() {
  (function bar() {
    throw 'Exit from bar and foo contexts';
  })();
})();

Eval code

eval 코드는 매우 흥미롭다. 이 경우 calling context의 개념, 즉 eval 함수가 호출된 컨텍스트가 있다.

변수 또는 함수 정의와 같이 eval에 의해 수핸된 작업은 calling context에 영향을 준다.

// influence global context
eval('var x = 10');
 
(function foo() {
  // and here, variable "y" is
  // created in the local context
  // of "foo" function
  eval('var y = 20');
})();
  
alert(x); // 10
alert(y); // "y" is not defined

Note: ES5의 strict-mode에서 eval은 이미 호출 컨텍스트에 영향을 미치지 않지만 대신 로컬 sandbox의 코드를 평가한다.

위의 예에서는 다음과 같은 ECStack 수정 사항이 있다.

ECStack = [
  globalContext
];
  
// eval('var x = 10');
ECStack.push({
  context: evalContext,
  callingContext: globalContext
});
 
// eval exited context
ECStack.pop();
 
// foo funciton call
ECStack.push(<foo> functionContext);
 
// eval('var y = 20');
ECStack.push({
  context: evalContext,
  callingContext: <foo> functionContext
});
 
// return from eval 
ECStack.pop();
 
// return from foo
ECStack.pop();

즉 아주 캐주얼하고 논리적인 call-stack이다.

Note: 예전 SpiderMonkey(Firefox)에서 버전 1.7까지는 eval 함수의 두 번째 인수로 전달할 수 있었다. 따라서 컨텍스트가 여전히 존재하면 개인 변수에 영향을 줄 수 있었다.

function foo() {
  var x = 1;
  return function () { alert(x); };
};
 
var bar = foo();
 
bar(); // 1
 
eval('x = 2', bar); // pass context, influence internal var "x"

하지만 최신 엔진에서는 보안상의 이유로 수정되어 더 이상 중요하지 않다.


Reference

ECMA-262-3 This

실행콘텍스트가 선행되어야 합니다.

이번 주제는 this 키워드다.

사례에서 보듯이, 이 주제는 상당히 어려워서 종종 다른 실행 콘텍스트의 this 값을 처리할 때 이슈를 만들곤 한다.

정의

this실행 콘텍스트의 프로퍼티다.

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

여기의 VO변수 객체(Variable object)를 의미 한다.

this 는 콘텍스트의 실행 코드 타입과 직접적인 관련이 있다. 이 값은 콘텍스트로 진입하는 과정에서 정해지며, 콘텍스트 안의 코드가 실행 중에는 변하지 않는다.

좀 더 자세하게 들여다보자.

전역 코드 안의 this(This value in the global code) 이건 정말 단순하다. 전역이니까 당연히 this 는 전역객체 자신 이 될 것이다.

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

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

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

함수 코드 안의 this(This value in the function code) 이제부터가 진짜라고 할 수 있다. 함수타입의 코드의 this 는 함수에 정적으로 바인딩이 되지 않는다는 것이다.

콘텍스트로 들어갈때 정해지며, 함수코드의 this 는 매번 바뀔 수 있다.

그러나 코드나 코드가 실행이 되고 this 는 변경이 이루어질수 없다(이 얘기는 아래에서 또 나올 것이다.)

this 를 재할당하는 것이 불가능 하다는 것이다.

var foo = {x: 10};
var bar = {x: 20,test: function () {   
  alert(this === bar); 
  // true   
  alert(this.x); // 20   
  this = foo; // 에러, this 값을 변경할 수 없다.   
  alert(this.x); // 
  // 만약 위에서 에러가 나지 않았다면 20이 아닌 10이 출력될 것이다.
  }
};
// 콘텍스트로 들어올 때 this가 가리키는 대상이 bar 객체로 결정된다.
// 왜 그러한지는 아래에서 자세하게 설명하겠다.
bar.test(); // true, 20
foo.test = bar.test;
// 그러나 여기의 this는 이제 foo를 참조할 것이다.  
// 심지어 같은 함수를 호출하는 데도 말이다.
foo.test(); // false, 10

그렇다면 어떻게 해야 this가 바뀌는 것일까??

콘텍스트의 코드를 활성화시킨 호출자(caller)에 의해 제공된다.

즉 함수를 호출한 부모 콘텍스트가 존재한다는 것이다. 어떤 콘텍스트의 this가 참조하는 값을 어려움없이 알아내기를 원한다면 중요한 이 부분을 이해하고 기억해야 한다.

정확하게 호출 표현식의 형태, 즉 다른 무엇이 아닌 함수를 호출한 방법이 호출된 콘텍스트this 값에 영향을 준다.

this 값은 함수가 어떻게 정의되었는가에 따라 정해진다. 전역 함수라면 this 는 전역 객체를 값으로 갖게 되고, 객체의 메서드라면 this 는 항상 이 객체를 값으로 갖는다.

이건 정말 나도 많이보고 이렇게 생각했다. 그러나 아니다...(많이 당했다 크게 당했다...심지어 보통의 전역 함수도 다른 형태의 호출 표현식으로 활성화되면 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 가 달라질 수 있다.

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

foo.bar(); // foo, true

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

exampleFunc(); // global, false

이 경우도 똑같다고 생각하면 된다. exampleFunc === foo.bar 의 결과가 같다면 this 는 같은 것이 나올거라 일반적으로 생각을 할 것이다.

그런데!! 결과를 보게 되면 놀랍다. global 이 되는 것이다.

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

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

레퍼런스 타입

레퍼런스 타입은 수도 코드를 이용해서 base (프로퍼티가 속해 있는 객체)와 이 base 안에 있는 propertyName s이라는 2개의 프로퍼티를 갖고 있는 객체로 나타낼 수 있다.

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

레퍼런스 타입의 값은 오직 아래의 2가지 경우에만 있을 수 있다.내가 볼 때 아래에 2가지 경우가 this 를 이해하는데 핵심이다.

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

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

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

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

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

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

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

레퍼런스 타입 값으로부터 객체가 갖고 있는 실제 값을 얻기 위해 쓰이는 GetValue 메소드가 있는데 이 메소드를 수도 코드로 아래와 같이 나타낼 수 있다.

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']();

이번에도 중간 계산의 결과로 레퍼런스 타입의 값을 갖게 된다.

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

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

그렇다면, 레퍼런스 타입의 값과 함수 콘텍스트의 this 값은 어떤 관계일까? 이 부분이 가장 중요하며, 이 글의 메인이다.

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

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

호출 괄호(…)의 왼편에 레퍼런스 타입의 값이 존재하면, this 는 레퍼런스 타입의 this 값인 base 객체를 값으로 갖는다.

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

예제

function foo() {
 return this;
}
foo(); // global

호출괄호 왼쪽에 레퍼런스 타입 값이 있다.(foo 는 함수이름으로 식별자이다)

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

따라서, this 값은 레퍼런스 타입 값의 base 객체인 전역 객체로 설정된다.

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

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

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

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

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

test 가 식별자가 되면서 다른 레퍼런스 타입 값을 만들기 때문에, 이 레퍼런스 타입의 base (전역 객체)가 this 값으로 사용된다.

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

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

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)

호출 괄호의 왼편에 레퍼런스 타입이 아닌 다른 값이 오는 경우 this값은 자동으로 null 값을 가지게 된다. 그리고 이것을 엔진은 자동으로 전역객체로 출력한다.

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

딱 위의 경우가 좋은 에시이다. 이것은 레퍼런스타입(식별자, 프로퍼티 접근자)가 아니다. 따라서 레퍼런스타입이 존재하지 않는다. 즉 null 이다 그럼으로 전역객체를 출력한다.

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개는 레퍼런스 타입 값이 null이라는 것이다.

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

세번째의 경우는, 그룹핑 연산자와 다르게 할당 연산자는 GetValue 메소드를 호출한다. 반환의 결과로 thisnull로 설정되었음을 의미하는 함수 객체(레퍼런스 타입 값은 아닌)가 반환되기 때문에, 이는 결국 전역 객체가 된다.

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

레퍼런스 타입과 값이 nullthis(Reference type and null this value) 위에도 말은 했지만 레퍼런스타입의 값이 null이 되면 this는 전역객체가 된다고 했다. 이것은 레퍼런스 타입 값의 base 객체가 활성화 객체인 경우와 관련이 있다.

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

활성화 객체는 항상 this 값으로 null을 반환한다.

예외의 with함수

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

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

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

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(); // __catchObject - ES3, global - ES5에서는 수정
}
// on idea
var eReference = {
 base: __catchObject,
 propertyName: 'e'
};
// 그러나, 이것은 버그이기 때문에
// this는 강제로 전역 객체를 참조하게 된다.
// null => global
var eReference = {
   base : global,
   propertyName: 'e'
}


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

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

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

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

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

함수 호출 시에 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


Reference

ECMA-262-3 ScopeChain

소개

  • 실행 컨텍스트의 데이터(변수, 함수 선언 그리고 함수의 매개변수)는 변수 객체의 프로퍼티( VO )로 저장된다.
    • 깨알 유용한 정보 : Context = VO|AO + this + SC
  • 컨텍스트로 진입 할 때 매번 초기값을 갖는 변수 객체를 생성하며(선언 + 초기화)(==호이스팅), 코드 실행 할 때 값을 갱신(할당)한다.

이번에는 스코프 체인에 대해서 정리를 해보자.

정의

스코프체인은 대게 중첩 함수와 관련이 있다.

중첩함수 란 함수안에 함수가 있는 것 <= 당연한 말인 듯하다.

심지어 부모 함수가 이러한 중첩 함수를 결과 값으로 반환이 가능하다.

var x = 10;

function foo() {
 var y = 20;

 function bar() {
   alert(x + y);
 }
 return bar;
}

foo()(); // 30

소개에서도 나오고 This 편에서도 나왔지만 모든 컨텍스트는 자신의 고유 변수 객체를 가진다. 전역 컨텍스트는 자기 자신을 변수 객체(VO_global)로 가지며, 함수 컨텍스트는 활성화 객체(AO)를 가진다.

  • 전역 컨텍스트 = VO + SC + this
  • 함수 컨텍스트 = AO + SC + this

스코프 체인은 내부 컨텍스트가 이용하는 모든(부모) 변수 객체의 리스트다. 변수를 검색할 때 이 체인을 이용한다.

위의 경우에서는 bar 컨텍스트의 스코프 체인은 AO(bar), AO(foo), VO(global)를 갖는다. 순서 또한 위와 같이 가진다. 즉 처음에 위치한 것은 자기자신이라는 것이다.

SC 는 내부 컨텍스트가 이용하는 모든 변수 객체의 리스트

스코프체인은 실행 컨텍스트와 관련 있으며, 식별자 해석시 변수 검색에 이용하는 변수 객체의 체인이다.

  • 스코프 체인은 함수를 호출할 때 생성되고
  • 활성화 객체와 함수의 내부 [[scope]] 프로퍼티를 가진다.


내부의 모습

activeExecutionContext = {
   VO: {...}, // or AO
   this: thisValue,
   Scope: [ // 스코프 체인(scope chain)
     // 식별자 검색에 이용할 모든 변수 객체의 리스트
   ]
};

스코프의 정의

Scope = AO + [[scope]]

예를 들기 위해서 스코프와 [[Scope]]ECMAScript 의 일반 배열로 나타낼 수 있다.

var Scope = [VO1, VO2, ..., VOn]; // 스코프 체인
var VO1 = {__parent__: null, ... other data};
var VO2 = {__parent__: VO1, ... other data};
...

함수 라이프 사이클(Function life cycle)

함수의 라이프 사이클은 생성 단계, 활성화 단계(call) 의 2가지로 나뉜다.

함수 생성

모두가 아시다시피 컨텍스트 단계로 들어갈 때 변수/활성화 객체(VO/AO)가 함수 선언으로 들어간다.

var x = 10;

function foo() {
  var y = 20;
  alert(x + y);
}

foo(); // 30

함수가 활성화가 되면 함수는 30 을 출력한다.

여기에서 변수 y 는 함수 foo 에서 정의되어있지만, 변수 xfoo 의 컨텍스트에 정의되어 있지않다. 그러므로 fooAO 에 추가가 되지 않는다. 그렇다면 xfoo 에 존재하지 않는 것인가?

fooContext.AO = {
  y: undefined // undefined – 컨텍스트 접근시, 20 – 활성화시
};

foo 컨텍스트의 활성화 객체는 y 프로퍼티 만을 가진다. 그렇다면 어떻게 해서 함수 foo 가 변수 x 에 접근할 수 있을까?

[[Scope]] 는 현재 함수 컨텍스트의 상위에 있는 모든 부모 변수 객체의 계층 체인 이다. 이 체인은 함수가 생성될 때 함수에 저장된다.

함수를 생성할 때 [[Scope]] 프로퍼티가 함수에 저장되는데, 일단 한 번 저장되고 나면 함수가 사라질 때까지 정적으로 변하지 않는다 는 사실을 주목하자. 함수를 결코 호출할 수 없어도, 함수 객체는 이미 [[Scope]] 프로퍼티를 가지고 있다.

foo.[[Scope]] = [
  globalContext.VO // === Global
];

함수 활성화

컨텍스트로 진입하고 AO/VO 가 만들어진 후에, 컨텍스트의 scope 프로퍼티는 다음과 같이 정의된다.

Scope = AO|VO + [[Scope]]

여기서 중요한 것은 활성화 객체가 Scope 배열의 첫번째 원소로 제일 앞으로 온다는 것이다.

Scope = [AO].concat([[Scope]]);

식별자 해석은 변수(또는 함수 선언)가 스코프 체인의 어떤 변수 객체에 속하는지를 결정하는 과정이다.

식별자 해석 과정은 변수의 이름에 해당하는 프로퍼티를 검색하는 과정을 포함하며, 스코프 체인 가장 깊은 곳에 있는 컨텍스트의 변수 객체부터 시작해서 가장 위에 있는 변수 객체까지 연속적으로 검사하는 과정이다.

그 결과 현재 컨텍스트의 지역 변수는 부모 컨텍스트에 있는 변수보다 검색 우선 순위를 가지며, 이름이 같지만 서로 다른 컨텍스트에 존재하는 두 변수의 경우, 더 깊은 컨텍스트에 있는 변수가 우선한다. 즉 가까운 곳에 위치한 변수가 우선순위가 높다는 것이다.

var x = 10;

function foo() {
 var y = 20;

 function bar() {
   var z = 30;
   alert(x +  y + z);
 }

 bar();
}

foo(); // 60

전역 컨텍스트의 변수 객체 :

globalContext.VO === Global = {
 x: 10
 foo: <reference to function>
};

foo 생성 시점에 foo[[Scope]] 프로퍼티 :

foo.[[Scope]] = [
 globalContext.VO
];

foo 함수의 활성화 시점(컨텍스트로 진입하는 단계)에 foo 컨텍스트의 활성화 객체 :

fooContext.AO = {
 y: 20,
 bar: <reference to function>
};

foo 컨텍스트의 스코프 체인 :

fooContext.Scope = fooContext.AO + foo.[[Scope]] 

fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

중첩된 bar 함수가 생성되는 시점에 bar 함수의 [[Scope]] :

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];

bar 활성화 시점에 bar 컨텍스트의 활성화 객체 :

barContext.AO = {
  z: 30
};

bar 컨텍스트의 스코프 체인 :

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:

barContext.Scope = [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

스코프의 특징(Scope features)

클로저

ECMAScript 의 클로저는 [[Scope]] 프로퍼티와 직접적으로 관련이 있다. [[Scope]] 는 함수를 생성할 때 함수에 저장되어서, 함수 객체가 사라질 때까지 존재한다. 실제로, 클로저는 정확하게 함수 코드와 [[Scope]] 프로퍼티의 조합이다

var x = 10;

function foo() {
    alert(x);
}

(function () {
    var x = 20;
    foo(); // 10, but not 20
})();

변수 x는 foo 함수의 [[Scope]] 에 있는 것을 알 수 있다. 변수를 검색할 때, 함수 호출 시점의 동적인 체인(이 경우 변수 x의 값은 20이 될 것이다)이 아닌, 함수 생성 순간에 정의된 어휘적인 체인을 이용하였다.

function foo() {
  var x = 10;
  var y = 20;

  return function () {
    alert([x, y]);
  };
}

var x = 30;
var bar = foo(); // 익명 함수를 반환한다.

bar(); // [10, 20]

위이 예제에서도 역시 식별자 해석에 함수 생성 시점에 정의된 어휘적 스코프 체인을 이용하였다. 변수 x를 30이 아닌 10으로 해석했다. 게다가, 이 예제는 함수의 [[Scope]] (함수 foo가 반환한 익명 함수의 경우에)가 심지어 생성된 함수의 컨텍스트가 이미 종료되고 난 이후에도 존재하고 있음을 명확하게 보여준다.

Function 생성자로 생성한 함수의 [[Scope]]

위의 예제에서 함수 생성시에 [[Scope]] 프로퍼티를 가져오고 이 프로퍼티를 통해서 모든 부모 컨텍스트의 변수에 접근한다는 것을 보았다. 그러나, 이 규칙에는 한가지 중요한 예외가 있는데, Function 생성자를 이용해서 함수를 생성하는 경우는 다르다

var x = 10;

function foo() {
  var y = 20;

  function barFD() { // FunctionDeclaration
    alert(x);
    alert(y);
  }

  var barFE = function () { // FunctionExpression
    alert(x);
    alert(y);
  };

  var barFn = Function('alert(x); alert(y);');
  barFD(); // 10, 20
  barFE(); // 10, 20
  barFn(); // 10, "y" is not defined
}

foo();

위에 보이듯이 생성자로 만든 함수는 Scope가 다르다. 그러나 y 에는 접근을 한다 이것은 [[Scope]]global은 가진다는 것이다.

2차원 스코프 체인 검색

스코프 체인 검색의 중요한 포인트는 ECMAScript 의 프로토타입적인 성격 때문에 변수 객체의 프토토타입 또한 고려해야 한다는 점이다. 객체 내에서 직접적으로 프로퍼티를 찾지 못한다면, 프로토타입 체인까지 검색을 수행 한다. 즉, 일종의 2차원 체인 검색인 셈이다. (1) 스코프 체인 연결, (2) 그리고 깊은 프로토타입 체인 연결에 있는 모든 스코프 체인 연결을 검색 한다.

function foo() {
  alert(x);
}

Object.prototype.x = 10;
foo(); // 10

쉽게 말하면 역시 scope 를 검색했는데 없다 그러면 protptype chain 까지 검색을 한다는 것이다. 그래서 2차원이다.

전역 컨텍스트와 eval 컨텍스트의 스코프 체인

전역 컨텍스트의 스코프 체인은 오직 전역 객체만을 갖는다. 그리고 eval 코드의 컨텍스트는 호출 컨텍스트와 같은 스코프 체인을 갖는다.

무조건 글로벌이라고 생각하면 된다.

globalContext.Scope = [
    Global
];
evalContext.Scope === callingContext.Scope;


코드 실행 중 스코프 체인에 영향을 미치기

ECMAScript 에는 코드 실행 런타임에 스코프 체인을 변경할 수 있는 두 가지 구문이 있다.

with문과 catch절이다.

둘 다 이들 구문 내에 나타나는 식별자를 찾기 위한 객체를 스코프 체인의 가장 앞에 추가한다. 이 중에 하나를 코드에 적용하면, 스코프 체인은 아래와 같이 변경된다.

Scope = withObject|catchObject + AO|VO + [[Scope]]

with 문의 경우에는 파라미터로 넘겨 받은 객체를 추가한다(그 결과, 이 객체의 프로퍼티에 접두사를 붙이지 않고 접근할 수 있다)

var foo = {x: 10, y: 20};

with (foo) {
  alert(x); // 10
  alert(y); // 20
}
Scope = foo + AO|VO + [[Scope]]
var x = 10, y = 10;

with ({x: 20}) {
  var x = 30, y = 30;

  alert(x); // 30
  alert(y); // 30
}

alert(x); // 10
alert(y); // 30

중요

  1. x = 10, y = 10
  2. 객체 { x : 20 }을 스코프 체인의 앞에 추가한다.
  3. 컨텍스트 진입 단계에서 모든 변수를 해석하고 추가했기 때문에 with 내에서 var 구문을 만났을 때 아무 것도 만들지 않는다.
  4. 오직 x 의 값을 수정하는데, 정확하게는 두번째 단계에서 스코프 체인의 앞에 추가된 객체 내에서 해석되는 x를 말한다. 20이었던 x 의 값이 10이 된다.
  5. 또한 위의 변수 객체 내에서 해석되는 y도 변경한다. 결과적으로 10이었던 y의 값이 30이 된다.
  6. 다음으로 with 문이 종료된 후에, 스페셜 객체는 스코프 체인에서 제거된다( x 의 값이 변경되고, 30 또한 객체에서 제거된다). 즉, 스코프 체인 구조가 with 문에 의해서 확장되기 이전 상태로 돌아온다.
  7. 마지막에 있는 두 번의 alert 호출을 통해서 알 수 있듯이, 현재 변수 객체 내에 있는 x의 값은 같은 상태로 남아있고, y의 값은 with 문 내에서 변경한 상태 그대로 30이다.

catch 절 또한 exception 파라미터에 접근하기 위해서 exception 파라미터의 이름을 유일한 프로퍼티로 갖는 중간 스코프 객체를 만들며, 이 객체를 스코프 체인의 앞에 추가한다. 개략적으로 아래와 같이 나타낼 수 있다.

try {
  ...
} catch (ex) {
  alert(ex);
}
var catchObject = {
  ex: <exception object>
}
Scope = catchObject + AO|VO + [[Scope]]

catch절 내의 작업이 종료된 후에, 스코프 체인은 이전 상태로 돌아온다.


Reference

ECMA-262-3 Function

도입

함수가 컨텍스트의 변수 객체(VO)에 어떠한 영향을 미치며, 각 함수의 스코프 체인에는 무엇이 들어가는지도 알아보자.

var foo = function () {...};

function foo() {...}

(function () {...})();

위와 같이 함수의 경우는 3가지가 있다.

선언식, 표현식, 즉시실행

이 3가지의 차이점과 특징은 무엇인가에 대해서 자세히 알아보자.

함수의 종류

ECMAScript에는 세가지 종류의 함수가 있고, 각각의 고유한 특징을 갖는다.

함수 선언식

함수 선언식(FD)은 아래와 같은 특징을 가진다.

  • 반드시 이름을 가진다.
  • 소스 코드 위치에 자리한다. 프로그램 레벨이나 다른 함수의 몸체안에 직접 위치한다.
  • 컨텍스트 진입 시점에 생성 한다.
  • 변수 객체에 영향을 준다.
function exampleFunc() {...}

가장 중요한 특징은 변수 객체에 영향을 미친다는 것이다. 이 함수는 컨텍스트의 변수 객체에 들어간다.

코드 실행 단계에서 이미 사용가능하다(FD가 컨텍스트 단계 시작시 VO에 저장되므로 실행이 시작되기 전).

foo(); // 작동함

function foo() {
    alert('foo');
} 

위의 소스는 그렇다면 GlobalVO에 들어가 있을 것이다.(흔히 호이스팅이라 불리는 것)

소스 코드 내에 함수를 정의하는 위치는 중요하다.

// 함수를 다음 2가지 방법으로 선언할 수 있다.
// 1) 전역 컨텍스트에 직접.
function globalFD() {

    // 2) 또는 다른 함수의 몸체 내에서 선언.
    function innerFD() {}
}

함수를 선언할 수 있는 위치는 결국 두 군데가 있는 것이다.

함수를 선언하는 다른 방법이 있다.

함수 표현식

함수 표현식(FE)은 아래와 같은 특징을 가진다.

  • 표현식 위치에만 정의할 수 있다.
  • 선택적으로 이름을 가질 수 있다.(없을 수도 있다.)
  • 함수 표현은 변수 객체에 영향을 주지 않는다.
  • 코드 실행 시점에 생성 한다.

이 함수 타입의 주요 특징은 항상 표현식 위치에 있다는 것이다.

var foo = function () {...};

위의 경우는 익명함수 표현식을 foo변수에 할당하는 것이다. 할당이 끝나면 foo 를 호출할 수 있다.

선택적으로 이름을 줄 수 있다.

var foo = function _foo() {...};

여기에서 주목해야 할 것은 함수 내부에서 _foo 라는 이름을 사용할 수 있다.(외부는 사용불가)

FE를 식별자에 할당하면 FD와 구분하기 어려워진다. 하지만 FE항상 표현식에 위치 한다는 사실을 알고 있다면, 둘을 쉽게 구분할 수 있다.

다음 예제에는 다양한 ECMAScript 표현식이 나와있는데, 모든 함수는 함수 표현식이다.

// 괄호(그룹화 연산자) 안에서는 표현식이 된다.
(function foo() {});

// 배열 리터럴 안에 있을 경우에도 표현식이다.
[function bar() {}];

// 콤마 또한 표현식으로 처리한다.
1, function baz() {};

위의 경우의 표현식들은, 표현식 위치에서 함수를 사용하고 변수 객체를 오염시키지 않으려면 필요하다.

function foo(callback) {
    callback();
}

foo(function bar() {alert('foo.bar');});
foo(function baz() {alert('foo.baz');});

FE를 변수에 할당하면, 함수는 메모리에 계속 존재한다. 따라서 나중에 변수명으로 접근할 수 있다(알고 있듯이 변수가 변수 객체(VO)에 영향을 주기 때문). Global VO에 존재하기 때문에 접근이 가능하다.

var foo = function () {
    alert('foo');
};

foo();

보조적인 역할을 하는 도우미 데이터를 외부 컨텍스트에 감추기 위해서 유효범위를 캡슐화하는 예제가 있다(FE를 생성 직후 호출).

var foo = {};

(function initialize() {
    var x = 10;
    foo.bar = function () {   
        alert(x);
    };
})();

foo.bar(); // 10;

alert(x); // "x" is not defined

함수 foo.bar ( foo[[Scope]] 프로퍼티에 있는)는 initialize 함수의 내부에 있는 변수 x에 접근할 수 있다. 그러나 외부에서 x를 직접 접근할 수 없다.

많은 라이브러리가 private 데이터를 만들어서 보조 개체를 감추는데 이용한다.

초기화하는 FE 의 이름을 종종 생략하기도 한다.

(function () {
    // 초기화 스코프
})();

런타임에 조건에 따라 FE를 생성함으로써 VO를 오염시키지 않는 예제도 있다.

var foo = 10;
var bar = (
    foo % 2 == 0 ? 
        function () { alert(0); }
        : 
        function () { alert(1); }
);

bar(); // 0

감싸는 괄호에 대한 질문

왜 괄호로 함수를 감싸야 선언과 동시에 호출할 수 있지?

그 답은 바로 표현식 구문이 가지는 제약 때문이었다.

표준에 따라서, 표현식 구문은 여는 중괄호, { 로 시작할 수 없다. 블럭과 구분할 수 없기 때문이다. 그리고 함수 선언과 구분하기 힘들기 때문에 함수 키워드로 시작해서도 안 된다.

다시 말해서, 즉시 실행 함수(function 키워드로 시작하는)를 만들기 위해서 아래와 같이 함수 선언식을 작성했다면,

function () {...}();// 또는 아래와 같이 이름이 있는.
function foo() {...}();

두 경우 모두 파서가 해석 에러를 보고할 것이다.

이 에러의 원인은 다양하겠지만, 전역 코드에 이렇게 선언을 하면(즉, 프로그램 레벨에), function 키워드로 시작하기 때문에 파서는 코드를 함수 선언식으로 이해한다.

첫번째 경우는 함수의 이름이 없기 때문에 SyntaxError 를 보고한다.

두 번째의 경우는 함수에 이름(foo)이 존재하기 때문에 파서가 정상적인 함수 선언으로 처리한다. 하지만 내부에 표현식이 없는 그룹화 연산자 를 사용하고 있음을 알리는 문법 에러가 발생한다. 이 경우에 함수 선언 뒤에 오는 것은 함수 호출을 위한 괄호가 아니라 그룹화 연산자일 뿐이다. 만약 코드를 다음과 같이 작성했다면,

// "foo"는 함수 선언이다
// 그리고 실행 컨텍스트 진입 시점에 생성한다.
alert(foo); 

// function
function foo(x) {
    alert(x);
}(1); // 이것은 호출이 아니라, 그룹화 연산자다.

foo(10); 
// 10

함수 선언과 표현식 (1)을 가지고 있는 그룹화 연산자가 있기 때문에 두 구문 모두 아무런 문제가 없다. 위의 예제는 아래의 예제와 같다.

// 함수 선언
function foo(x) {
    alert(x);
}
// 표현식이 있는 그룹화 연산자
(1);
// 다른 (function) 표현식을 갖는 또 다른 그룹화 연산자
(function () {});

// 내부에 있는 표현식
("foo");

ECMA 스펙상으로 볼 때, 위의 코드는 잘못된 구문이다(표현식 구문은 function 키워드로 시작할 수 없다). 하지만 아래에 나와있는 것처럼, 문법 에러를 제공하는 ECMAScript 구현체는 하나도 없으며 모두 이를 각자 나름의 방식으로 처리한다.

지금까지 설명한 내용을 가지고, 어떻게 파서에게 함수를 생성과 동시에 실행하고 싶다고 할 수 있을까?

함수 선언식이 아닌 함수 표현식을 사용하면 된다.

표현식을 만드는 가장 간단한 방법은 위에서 이야기 했듯이 그룹화 연산자를 사용한다. 그룹화 연산자 안에 표현식을 두면, 파서는 함수 표현식(FE)인 코드를 구분할 수 있으며 이에 따라 모호함도 사라진다. 이러한 함수는 코드 실행 단계 동안에 만들어지고, 함수 실행이 끝난 후에는 사라진다(함수를 참조하고 있는 곳이 없다면).

(function foo(x) {
    alert(x);
})(1); // 이건 그룹화 연산자가 아닌 함수 호출이다.

예제의 마지막에 있는 괄호는 FD의 경우처럼 그룹화 연산자가 아니라 함수 호출 괄호다.

다음 예제에 나오는 즉시 호출 함수는 괄호로 감쌀 필요가 없다는 것에 주목하자. 이유는 함수가 표현식의 위치에 있어서 파서가 이를 코드 실행 시점에 생성하는 FE로 처리해야 한다는 것을 이미 알고 있기 떄문이다.

var foo = {
    bar: function (x) {   
        return x % 2 != 0 ? 'yes' : 'no';
    }(1)
};

alert(foo.bar); // 'yes'

얼핏보면 foo.bar는 함수가 아니라 문자열처럼 보인다. 여기에 있는 함수는 프로퍼티를 초기화할 때만 사용하는데, 조건 매개변수 값에 따라서 값을 돌려주는 함수를 만들고 바로 실행한다. 따라서, 괄호 를 묻는 질문에 완벽한 대답은 다음과 같다.

:star: 제일 중요한 부분 :star:

그룹화 괄호는 함수가 표현식의 위치에 있지 않을 때 필요하고, 함수를 생성 후 즉시 실행하고 싶은 경우에는 직접 함수를 FE로 변환한다.

괄호를 감싸는 방법 외에 함수를 FE 타입으로 변경할 수 있는 다른 방법이 있다. 예를 들어,

1, function () { 
    alert('익명함수를 호출합니다.');
}();// 또는 이렇게,

!function () { 
    alert('ECMAScript');
}();// 그리고 수동적으로 변경하는 다른 방법들...

올바른 표현식

(function () {})();
(function () {}());

구현의 확장 : Function문

다음에 나오는 예제 코드는 어떤 ECMAScript 구현체도 명세를 따르지 않았음을 보여준다.

if (true) { 
    function foo() {   
        alert(0); 
    }
} else { 
    function foo() {   
        alert(1); 
    }
}
    
foo(); // 1 또는 0? 다른 ECMAScript 엔진에서 테스트 해보자.

표준에 비춰볼 때 이 구조는 문제가 있다. 코드 블럭 안에 함수 선언식(FD)을 둘 수 없기 때문이다(지금은 ifelseFD를 가지고 있음). 위에서 이야기 했듯이, FD는 프로그램 레벨이나 다른 함수의 몸체 안에 직접 위치해야 한다.

코드 블럭은 오직 구문만 가질 수 있기 때문에 위의 예제는 잘못되었다. 블럭 내에 함수는 표현식의 위치에만 나올 수 있으며, 함수를 정의할 때는 여는 중괄호(코드 블럭과 구분할 수 없음)나 함수 키워드로 시작할 수 없다(FD와 구분할 수 없음).

하지만 표준 문서의 error processing 섹션은 ECMAScript 구현체가 프로그램 구문을 확장할 수 있도록 허용하고 있다. 그리고 블럭 안에 등장하는 함수 처리가 이러한 확장 중에 하나다. 오늘날 존재하는 모든 구현체는 이 경우에 예외를 던지지 않고 각자 고유의 방식으로 처리한다.

위 예제의 if-else 분기문은 두 함수 중 어떤 것을 정의할지 선택할 수 있다고 가정한다. 이 결정은 런타임에 이루어지기 때문에, 함수 표현식(FE)을 사용해야 한다. 하지만 대부분의 구현체는 단순하게 컨텍스트 진입 시점에 두 개의 함수 선언식(FD)을 모두 생성한다. 두 함수 모두 같은 이름을 사용하기 때문에, 마지막에 선언한 함수만 호출할 수 있다. 이런 이유로 이 예제를 실행하면 else 로 코드 제어가 이동할 수 없음에도 불구하고 foo 함수는 1을 출력한다.

기명함수 표현식의 특징(Named Function Expression, NFE)

이름을 갖는 FE(기명 함수 표현식, 줄여서 NFE)는 중요한 특징 하나를 가지고 있다.

함수 표현식을 정의할 때 이야기 했던 것처럼

  • 함수 표현식은 컨텍스트의 변수 객체에 영향을 주지 않는다
  • 하지만 FE는 이름으로 자기 자신을 재귀 호출할 수 있다.
(function foo(bar) {
 if (bar) {
   return;
 }
 foo(true); // "foo" 이름을 이용할 수 있다.
})();
// 하지만 외부에서는 "foo"를 이용할 수 없다.  
foo(); // "foo" is not defined

foo 를 어디에 보관하는 걸까? foo의 활성화 객체 안도 아니다. foo 함수 내부에서 foo라는 이름을 정의한 적이 없다. 그렇다면 foo 를 생성하는 컨텍스트의 변수객체 안도 역시 아니다. FEVO에 영향을 주지 않는다는 사실을 외부에서 foo 를 호출하면서 확인했다. 그렇다면 어디일까?

코드 실행 시점에 인터프리터가 기명 함수 표현식(NFE)을 만나면. 함수 표현식을 만들기 전에 보조 특수 객체(auxiliary specilal object) 를 만들고 스코프 체인의 가장 앞에 이 특수 객체를 추가 한다. 그런 다음에 함수 표현식을 만드는데, 이 때 함수에 [[Scope]] 프로퍼티(Scope chain에서 배웠듯이)가 생긴다. 여기에는 함수를 생성하는 컨텍스트의 스코프 체인이 들어있다(즉, [[Scope]] 안에 특수 객체가 위치한다). 다음으로, 기명 함수 표현식을 특수 객체에 고유 프로퍼티로 추가한다. 이 프로퍼티의 값은 함수 표현식을 참조한다. 그리고 마지막으로 부모의 스코프 체인에서 특수 객체를 제거한다.

specialObject = {};

Scope = specialObject + Scope;

foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}

delete Scope[0]; // 스코프 체인의 가장 앞에 있는 specialObject를 삭제한다.

따라서, 외부에서는 이 함수의 이름을 사용할 수 없다. 함수의 [[Scope]] 안에 특수 객체가 저장되어 있기 때문에, 내부에서는 이 함수의 이름을 사용할 수 있다.

마무리

생각보다 내용이 많아졌다. 나도 선언식, 표현식을 비교하면서 보여주는 블로그들은 많이 봤지만 정확히 왜 그렇게 되고 즉시실행은 왜 저렇게 될 수 밖에 없는 것인가에 대해서 정리를 해보았다. 자꾸자꾸 보면서 쉬운말로 고쳐봐야겠다.


Reference

[ES3]ECMA-262-3 Closure

프로그래밍 패터다임중에 하나로 함수형 프로그래밍이 있다.

흔히 함수형 언어에서 함수는 데이터다.

초기 js가 만들어질 때 함수형의 영향을 어느정도 받았다고 한다. 그래서 함수형과 비슷한점이 존재한다.

즉 함수를 변수에 할당할 수 있고, 다른 함수에 인자로 전달할 수 있으며, 함수의 결과로 반환할 수 있다.

정의

함수 전달인자(Funarg)는 값이 함수인 전달인자다.

function exampleFunc(funArg) {
    funArg();
}

// 인자로 함수를 넘기고 있음
exampleFunc(function () {
    alert('funArg');
});

전달인자로 다른 함수를 받는 함수를 고차함수(HOF, High Order Function)라고 한다.

리액트를 하다보면 HOF로 구현을 하는 방법론이 나온다. 그 이야기의 시작은 여기라고 생각한다.

고차함수(HOF, High Order Function)는 기능적, 수학적으로 연산자로 볼 수 있다.

위 예제의 exampleFunc 함수는 고차 함수 이다. 함수를 전달인자로 넘길 수 있을 뿐만 아니라, 다른 함수의 결과 값으로 함수를 반환할 수도 있다.

(function functionValued() {
  return function () {
    alert('returned function is called');
  };
})()();// 함수로 반환한 것을 실행시키고 있다.

함수를 일반적인 데이터로 취급을 하고 있다.

함수를 전달하고 함수를 전달자로 받을수 있으며, 함수 값을 반환할 수 있는 함수를 일급객체라 한다.

일급객체(First Class)

ECMAScript 의 모든 함수는 일급 객체다. 자기 자신을 전달인자로 받는 함수를 자기 응용 함수라고 부른다..

(function selfApplicative(funArg) {

  if (funArg && funArg === selfApplicative) {
    alert('self-applicative');
    return;
  }

  // 자기 자신을 반환한다.
  // 전에도 말했지만 자신안에서 자신을 부를 수 있다.
  selfApplicative(selfApplicative);
})();

자기 자신을 반환하는 함수를 자기 복제 함수라고 한다. 어느곳에서는 자가 증식이라고 말하기도 한다고 한다.

(function selfReplicative() {
    return selfReplicative;
})();

자기 복제 함수를 호출할 때는 인자로 콜렉션 전체가 아닌 각각의 원소를 하나씩 전달한다.

// 콜렉션 자체를 매개변수로 받아서 처리하는 함수
function registerModes(modes) {
    // 인자로 전달받은 배열 modes를 순회하면서 각각의 모드를 등록한다.
    modes.forEach(registerMode, modes);
}

// 사용법
registerModes(['roster', 'accounts', 'groups']);

// 자기 복제 함수
function modes(mode) {
  registerMode(mode); // 하나의 모드를 등록한다.
  return modes; // 함수 자신을 반환한다.
}
// 사용법: 인자로 콜렉션 전체가 아닌 각각의 원소를 하나씩 전달한다.
modes
  ('roster')
  ('accounts')
  ('groups')

이렇게 함수를 호출할 수 있지만, 콜렉션 전체를 전달하는 방식이 더 효율적이고 직관적일 수 있다.

물론 함수 실행 시점에 전달인자로 넘기는 함수의 지역 변수에 접근할 수 있다. 콘텍스트에 진입할 때마다 콘텍스트 내부의 데이터 보관용 변수 객체를 만들기 때문이다.

function testFn(funArg) {
    // funarg 실행 시점에,
    // 지역 변수 "localVar"를 이용할 수 있다.
    funArg(10); // 20
    funArg(20); // 30
}

testFn(function (arg) {
    var localVar = 10;

    alert(arg + localVar);
});

하지만 Chapter 4. Scope Chain. 에서 봤듯이, ECMAScript 의 함수는 부모 함수에 속해 부모 콘텍스트의 변수를 사용할 수 있다.

이유는 당연히 SC에 의해서지 !!!

이러한 특징으로 인해서 함수 전달인자 문제(funarg problem) 가 발생하게 된다.

함수 전달인자 문제(Funarg problem)

Stack 지향 프로그래밍 언어는 함수를 호출할 때마다 함수의 지역 변수와 전달인자를 Stack에 넣는다. 그리고 함수를 종료할 때 Stack에서 변수를 제거한다.

이 모델을 적용하면 함수를 값(예를 들어, 부모 함수가 반환하는 값으로서의 함수)으로 사용하기 어렵다. – Stack에서 제거되면 사라지기 때문 -. 대게 함수가 자유 변수를 사용할 때 이런 문제가 발생한다.

자유 변수는 함수가 사용하는 변수 중, 파라미터와 함수의 지역 변수를 제외한 변수를 말한다.

function testFn() {
 var localVar = 10;

 function innerFn(innerParam) {
   alert(innerParam + localVar);
 }
 return innerFn;
}
var someFn = testFn();
someFn(20); // 30

이 예제의 localVarinnerFn 함수가 사용하는 자유 변수다. Stack 지향 모델이라고 가정해보면

testFn 함수를 종료하면서 모든 지역 변수를 스택에서 제거할 것이고, 때문에 외부에서 innerFn 함수를 실행하려고 할 때(즉 someFn을 사용하려고 하면 localvar는 이미 사라졌으므로) 에러가 발생할 것이다.

또한 위의 예처럼 innerFn함수를 반환하는 것이 아예 불가능하다. innerFn 함수가 testFn의 지역에 있기 때문에 testFn 함수가 종료되면서 innerFn 함수도 사라진다는 것이다.

동적 스코프를 이용하는 시스템에서 함수를 전달인자로 넘길 때 함수 객체가 갖고 있는 다른 문제가 일어난다.

var z = 10;

function foo() {
 alert(z);
}

foo(); // 10 - 정적 스코프와 동적 스코프를 둘다 사용

(function () {
 var z = 20;
 foo(); // 10 – 정적 스코프, 20 – 동적 스코프
})();

// 전달인자로 foo를 넘기는 것과 같다.
(function (funArg) {
 var z = 30;
 funArg(); // 10 – 정적 스코프, 30 – 동적 스코프
})(foo);

동적 스코프인 경우에는 동적(활동적) 스택을 이용해 변수를 처리한다. 결국 함수를 생성할 때 함수에 저장한 정적(어휘적) 스코프 체인이 아닌, 현재 실행중인 함수의 동적 스코프 체인에서 자유 변수를 찾는다. 이는 모호한 상황을 만든다.

예를 들어 지역 변수를 스택에서 제거하는 이전 예제와는 달리 z가 계속해서 살아있는 경우, 콘텍스트의 z를 사용해야할지 아니면 스코프의 z를 사용해야할지 알 수 없다.

지금까지 함수가 함수를 값으로 반환하거나(upward funarg), 함수를 다른 함수에 전달인자로 전달할 때(downward funarg) 생기는 2가지 유형의 함수 전달인자 문제(funarg problem)를 알아봤다.

클로저는 이러한 문제(및 서브타입)를 해결하기 위해 나온 개념이다.

클로저(Closure)

클로저는 코드 블럭과 이 코드 블럭을 생성한 콘텍스트가 갖고 있는 데이터의 조합이다.

var x = 20;

function foo() {
 alert(x); // 자유 변수 "x" == 20
}
// foo의 클로저
fooClosure = {
 call: foo // 함수를 참조
 lexicalEnvironment: {x: 20} // 자유 변수 검색을 위한 콘텍스트
};

위의 예제의 fooClosure는 물론 의사 코드다. ECMAScript 코드라면 foo 함수는 자신을 생성한 콘텍스트의 스코프 체인을 내부 속성으로 가질 것이다.

종종 어휘적이라는 단어를 생략하기도 하지만, 위 예제의 경우 클로저가 자기 부모의 변수를 소스 코드 내의 어휘적 위치에서 저장한다는 사실에 관심을 집중하자.

다음에 함수를 실행하면 저장한 콘텍스트 내에서 자유 변수를 검색한다. 위의 예제를 통해 ECMAScript에서는 변수 z가 항상 10인 것을 알 수 있다.

위에서 클로저를 정의할 때 코드 블록이라는 일반화 한 개념을 사용했지만, 보통 함수라는 용어를 사용한다.

하지만 오로지 함수만 클로저와 관련있는 것은 아니다.

구현에 대해서 이야기를 해보자면, 콘텍스트가 종료된 후에도 지역 변수를 보존하고 싶다면 스택 기반의 아키텍처는 더이상 적합하지 않다. 따라서 이 경우에는 부모 콘텍스트의 데이터를 가비지 콜렉터(GC)와 참조 카운팅을 이용하는 동적 메모리 할당 방식으로 저장해야 한다(힙 기반 구현). 이 방식은 스택 기반 보다 느리다. 하지만 함수 안에서 자유 변수를 사용할지 판단하고 이 결정에 따라 스택이나 힙에 데이터를 배치하는 과정을 스크립트 엔진이 해석 시점에 최적화 할 수 있다.

ECMAScript의 클로저 구현(ECMAScript closures implementation)

앞에서 이론적인 이야기를 하면서 마지막에 ECMAScript의 클로저를 언급했다. ECMAScript는 오직 정적(어휘적) 스코프만 사용한다는 사실을 명심하자(Perl 같은 일부 언어는 변수를 정적, 동적 스코프 두 가지 방식으로 선언할 수 있음).

var x = 10;

function foo() {
 alert(x);
}

(function (funArg) {
 var x = 20;
 // funArg는 변수 x를 자신이 선언된 어휘적 콘텍스트에
 // 정적으로 저장한다.
 // 따라서,
 funArg(); // 10, but not 20
})(foo);

기술적으로, 부모 콘텍스트의 변수는 함수 내부의 [[Scope]] 프로퍼티에 저장된다. Chapter 4에서 이야기했던 [[Scope]]와 스코프 체인을 완벽하게 이해했다면 ECMAScript의 클로저를 쉽게 이해할 수 있다.

함수 생성 알고리즘에 나와있듯이 ECMAScript의 함수는 부모 콘텍스트의 스코프 체인을 가지고 있기 때문에 모든 함수는 클로저다. 함수의 이후 실행 여부와는 상관없이 함수 생성 시점에 부모의 스코프를 함수의 내부 속성에 저장한다.

var x = 10;

function foo() {
 alert(x);
}

// foo는 클로저다.
foo: <FunctionObject> = {
 [[Call]]: <code block of foo>,
 [[Scope]]: [
   global: {
     x: 10
   }
 ],
 ... // 다른 프로퍼티들
};

앞에서 언급했듯이, 함수가 자유 변수를 사용하지 않는 경우에는 성능 최적화를 위해 JavaScript 엔진이 부모 스코프 체인을 함수 내부에 저장하지 않을 수도 있다.

그러나 ECMA-262-3 스펙은 이에 대해서 언급하고 있지 않다. 따라서 공식적으로(그리고 기술적 알고리즘에 따라) 모든 함수는 생성 시점에 [[Scope]] 프로퍼티에 스코프 체인을 저장한다.

일부 엔진은 사용자가 클로저 스코프에 직접 접근하는 것을 허용한다.

[[Scope]] 공유(One [[Scope]] value for “them all”)

ECMAScript에서 같은 부모 콘텍스트에서 만들어진 여러 중첩 함수는 같은 클로저 [[Scope]] 객체를 사용한다. 이는 어떤 클로저가 클로저 변수를 수정하면, 변경한 내용을 다른 클로저가 읽을 수 있다는 의미다.

즉, 모든 중첩 함수는 같은 부모의 스코프를 공유한다.

var firstClosure;
var secondClosure;

function foo() {
 var x = 1;
 firstClosure = function () { return ++x; };
 secondClosure = function () { return --x; };
 x = 2; // 두 클로저의 [[Scope]] 안에 있는 AO["x"]에 영향을 준다.
 alert(firstClosure()); // 3. firstClosure.[[Scope]]
}

foo();

alert(firstClosure()); // 4
alert(secondClosure()); // 3

이와 관련해서 많은 사람들이 자주하는 실수가 있다. 모든 함수가 고유의 루프 카운터 값을 갖게 만들기 위해 루프 안에서 함수를 생성할 때, 의도하지 않은 결과를 얻는 경우가 종종 있다.

var data = [];

for (var k = 0; k < 3; k++) {
 data[k] = function () {
   alert(k);
 };
}

data[0](); // 3, 0이 아니다.
data[1](); // 3, 1이 아니다.
data[2](); // 3, 2가 아니다.

이전 예제에서 이 동작을 설명했다. 세 함수 모두 같은 콘텍스트의 스코프를 갖는다. 이 세 함수는 모두 [[Scope]] 프로퍼티를 통해 변수를 참조하여 부모 스코프에 존재하는 변수 k를 쉽게 변경할 수 있다.

activeContext.Scope = [
 ... // 상위의 변수 객체
 {data: [...], k: 3} // 활성화 객체
];
data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;

따라서 세 함수는 실행 시점에 변수 k에 마지막으로 할당한 값인 3을 사용한다.

클로저의 실제적 사용(Practical usage of closures)

실제로 클로저를 이용하면 다양한 계산을 사용자가 함수 전달인자로 정의할 수 있게 하는 우아한 설계를 할 수 있다. 예를 들어 정렬 조건를 함수 전달인자로 받는 배열 정렬 메소드가 있다.

[1, 2, 3].sort(function (a, b) {
 ... // 정렬 조건
});

그리고 인자로 전달받은 함수를 배열의 각 원소에 적용한 결과를 갖는 새로운 배열을 만들어 돌려주는 map 메소드와 같은, 맵핑 고차함수(mapping functionals)가 있다.

[1, 2, 3].map(function (element) {
 return element * 2;
}); // [2, 4, 6]

검색 함수를 만들 때 함수를 전달인자로 받아 거의 무제한적인 검색 조건을 정의할 수 있게 구현해 놓으면 편리하다.

someCollection.find(function (element) {
 return element.someProperty == 'searchCondition';
})

또한, 배열을 순회하면서 각각의 원소에 함수를 적용하는 forEach 메소드와 같은 함수 적용 고차함수(applying functional)도 있다.

[1, 2, 3].forEach(function (element) {
 if (element % 2 != 0) {
   alert(element);
 }
}); // 1, 3

함수 객체의 apply, call 메소드는 함수형 프로그래밍의 함수 적용 고차함수(applying functional)에서 유래했다. 이미 이 메소드에 대해서는 Chapter 3. this 에서 이야기 했으므로, 이번에는 함수를 매개변수에 전달하는 방식을 살펴본다(apply 는 전달인자 목록을 받고, call 은 전달인자를 차례로 나열한다).

(function () {
 alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);

프로그래밍에서 apply는 데이터에 임의의 함수를 적용한다는 의미를 가진다. 만약 함수의 이름과 기능이 확정되어 있고, 그것을 알 수 있다면 함수의 이름과 파라미터 형식에 맞춰서 함수를 호출할 수 있다. 그런데 함수형 언어의 함수는 일급 객체로서 데이터로 취급할 수 있기 때문에 함수를 변수에 저장할 수 있고, 다른 함수의 인자로 전달할 수 있으며, 결과 값으로 반환할 수 있다.

간단히 말해서 함수를 다른 곳으로 넘길 수 있다는 이야기다. 이렇게 함수를 다른 곳으로 넘겼을 때 함수를 받은 쪽에서 받은 함수를 호출할 수 있는 장치가 필요한데, 이 장치를 사용하는 것을 apply라고 한다. JavaScript 역시 함수를 일급 객체로 취급하기 때문에 함수를 apply 하기 위한 call, apply 메소드를 가지고 있다.

그리고 함수들의 집합을 정의역으로 갖는 함수를 수학 용어로는 범함수(functional)라고 한다.

지연 호출은 클로저의 또 다른 중요한 응용 사례다.

var a = 10;
setTimeout(function () {
 alert(a); // 10, 1초 후 실행
}, 1000);
var x = 10;
// 예제용
xmlHttpRequestObject.onreadystatechange = function () {
 // 데이터가 준비 되었을 때, 지연 호출하는 콜백.
 // 콜백을 생성한 콘텍스트가 이미 사라졌는지와는 상관없이
 // 여기에서 변수 x에 접근할 수 있다.
 alert(x); // 10
};

또는 보조 객체를 감추기 위해 캡슐화 한 스코프를 만들 수 있다.

var foo = {};
// 초기화
(function (object) {
 var x = 10;
 object.getX = function _getX() {
   return x;
 };
})(foo);
alert(foo.getX()); // 클로저 변수 "x"의 값은 10

Reference

ES3