React를 위한 Javascript 정리

 ES6?

우리가 흔히 이야기하는 '자바스크립트'는 사실 자바와 무관하다는 것을 알고 있을 것입니다. 이 스크립트 언어가 개발될 때 인기 있었던 Java언어의 인기에 편승하고자 'JavaScript'라는 이름을 지은것이지요. 그 당시 이름을 지은 결정권자는 마케팅 측면에서 결정한 것이겠지만 오늘날 많은 프로그래머에게 많은 혼란을 주는 이름이되었죠. 아무튼 자바와는 아무관계 없는 언어입니다. 아무튼 이 언어도 계속 발전을 거듭해 표준 기관에 의해 6번째 개정판이 나오게 되는데 이 때 이름은 ECMAScript 6, 줄여서 ES6라고 합니다. 물론 ES6도 그냥 '자바스크립트'라고 불러도 무방하겠지만 많은 부분이 새롭게 추가되고 있으므로 새로운 문법 규칙을 확인하지 않으면 기존의 자바스크립트와 매우 다른 스타일의 프로그래밍에 혼란을 겪을지도 모르겠습니다. 크게 변한 부분 몇 가지를 적어보면 다음과 같습니다.

  • 클래스 (class) - 단순히 기존의 프로토타입 개념을 보기좋게 만들어놓은 문법 양념
  • 화살표 함수 (Arrow Functions) - 여러 언어의 람다식(Lambda)과 거의 동일한 개념으로 익명의 함수 표현
  • 변수 선언 방법 (let, const, var) - let으로 변경 가능한 지역변수 선언, const는 변경 불가능한 지역변수 선언, var은 전체 범위를 갖는 오래된 선언 키워드

class

자바스크립트는 function 키워드로 클래스를 선언할 수 있다는 부분이 기존의 객체지향 언어를 다뤄온 사람들에게는 헷갈리는 부분입니다. ES6문법에서는 클래스 개념을 새롭게 넣을것이 아닌 단순히 보기 좋게 하기 위해 class키워드와 생성자를 위한 클래스 내부에 constructor() 메서드를 지원하고 있습니다.

ES6 이전에는, 생성자 함수(constructor)를 만들고 프로토타입을 확장해서 프로퍼티를 추가하여 클래스를 구현했습니다.

function Person(name, age, gender) {
    this.name   = name;
    this.age    = age;
    this.gender = gender;
}

Person.prototype.incrementAge = function () {
    return this.age += 1;
};

 ES6로 표현하면 다음과 같습니다.

class Person {
    constructor(name, age, gender) {
        this.name   = name;
        this.age    = age;
        this.gender = gender;
    }

    incrementAge() {
      this.age += 1;
    }
}

객체를 생성할 때는 new키워드를 사용합니다. 다음 예를 봅시다.

class Car {
  constructor(name) { // 생성자
    this.name = name;
  }
  
  printName() { // 메서드
    return 'Model: ' + this.name;
  }
}

mycar = new Car("Sonata");
console.log(mycar.printName());

클래스의 상속

기존의 클래스 상속의 개념을 위해서 다음과 같은 코드가 사용되는 것이 일반적이었습니다.

function Personal(name, age, gender, occupation, hobby) {
    Person.call(this, name, age, gender);
    this.occupation = occupation;
    this.hobby = hobby;
}

Personal.prototype = Object.create(Person.prototype);
Personal.prototype.constructor = Personal;
Personal.prototype.incrementAge = function () {
    Person.prototype.incrementAge.call(this);
    this.age += 20;
    console.log(this.age);
};

ES6에서는 클래스의 상속을 표현하기 위해 extends키워드를 사용해 표현할 수 있습니다. 상위 클래스의 메서드나 속성을 그대로 사용할 수 있습니다.

class Personal extends Person {
    constructor(name, age, gender, occupation, hobby) {
        super(name, age, gender);
        this.occupation = occupation;
        this.hobby = hobby;
    }

    incrementAge() {
        super.incrementAge();
        this.age += 20;
        console.log(this.age);
    }
}

또다른 사용예를 봅시다.

class Car {
  constructor(name) {
    this.name = name;
  }
  
  printName() {
    return 'Model: ' + this.name;
  }
}

class Truck extends Car {
  constructor(name, capacity) {
    super(name);
    this.capacity = capacity;
  }
  showDesc() {
    return this.printName() + ' Capacity: ' + this.capacity;
  }
}

mycar = new Truck("Porter", "1ton");
console.log(mycar.printName());
console.log(mycar.showDesc());

 super() 메서드는 부모 클래스의 생성자를 호출해 부모 클래스에 있는 name프로퍼티를 설정할 수 있습니다.

화살표 함수

다른 언어가 가지고 있는 것과 비슷하게 화살표함수는 =>으로 표현하며 한 줄에 표현할 경우 반환을 위한 return 키워드를 생략할 수 있습니다.

let message1 = function() { // (1)
  return 1 + 2;
}

let message2 = () => { // (2)
  return 1 + 2;
}

let message3 = () => 1 + 2; // (3)

(1)~(3)은 결국 모두 같은 표현입니다.  세번째 표현에서 블럭 ( { ... } )을 없애고 return 을 생략할 수 있음을 주목합니다.

매개변수의 전달

필요한 경우 매개변수를 다음과 같이 전달 할 수 있습니다.

let val1 = 1;
let val2 = 2;

let add = (a, b) => 1 + 2;

console.log(add(val1, val2));

매개변수가 하나일 경우에는 소괄호를 생략할 수 있습니다.

let message = msg => "Hello " + msg;

console.log(message("World"));

간단한 값의 반환

간단한 값을 반환하기 위해서 함수 표현식보다는 화살표 함수를 사용합니다. 예를 들면,

var squares = arr.map(function (x) { return x * x }); // 함수 표현식

다음과 같이 변환 합니다.

const arr = [1, 2, 3, 4, 5];
const squares = arr.map(x => x * x); // 화살표 함수

 

함수범위와 블럭 범위 제한

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 1000)
}

여기서 i는 var로 정의되어 있어 함수 범위(function-scoped)가지는데 여기에 함수는 존재하지 않으므로 i는 전역(Global) 영역에 정의되게 됩니다.

for (let i = 0; i < 5; i++) { // POINT: var을 let으로 교체
  setTimeout(() => console.log(i), 1000)
}

따라서 for문에 한정하여 i를 사용하려면 let을 사용해 정의되어야 합니다.

변수의 정의

ES6문법에서는 var은 최대한 사용하지 않고 불변값은 const, 가변값은 let을 사용해서 정의합니다.

변수의 범위(scope)

잠깐 언급했던 letconst를 적적히 사용한다면 변수의 범위에 대해 고민할 필요가 없습니다. 다만 기존에 사용되던 var을 사용하는 경우 함수의 블럭 범위에 따라서 미치는 영향이 달라지므로 알아둘 필요가 있습니다.

전역 변수와 지역 변수

함수를 벗어나 선언된 value는 전역 변수로 해당 스크립트의 전체 범위를 가집니다. func()함수 내부에 value는 또 다시 var로 선언되어 있으므로 함수내의 스코프를 가지는 새롭게 정의된 지역 변수가 됩니다.

 

 

함수

내의 valuevar 선언 키워드를 사용하지 않으면 전역에 사용된 value가 그대로 사용되 변경된 값이 남아있게 됩니다.

블록 스코프

var로 선언한 변수의 스코프는 함수 스코프이거나 전역 스코프입니다. 블록 기준으로 스코프가 생기지 않기 때문에 블록 밖에서 접근 가능합니다.

if (true) {
  var test = true; // var은 블록 스코프를 무시함
}

alert(test); // 따라서 여기서 true

블록 스코프를 따르는 let을 사용하면 블록 바깥에서 접근할 수 없죠.

if (true) {
  let test = true; // 블록 스코프의 test
}

alert(test); // Error: test is not defined

만일 함수 내에 블록에서 var을 사용하면 함수 범위의 변수가 됩니다.

function sayHi() {
  if (true) {
    var phrase = "Hello";
  }

  alert(phrase); // Hello
}

sayHi();
alert(phrase); // Error: phrase is not defined

 

호이스팅(Hoisting)

변수나 함수를 선언하게되면 할당문을 제외하고 선언부분만 상단으로 끌어 올려지는 현상을 호이스팅이라고 합니다.

console.log(value); // (1) undefined
normal(); // (2) normal

function normal() {
  console.log('normal');
}
var value = 'hello';

(1)번에서 var value라는 선언문이 할당된 'hello'는 제외되 상위로 끌어올려지며 undefined가 출력되고 (2)번은 함수가 아래에 있지만 상위로 끌어 올려져 실행됩니다. 다만 (1)번에서 값이 undefined 가 나오는 이유는 선언부만 호이스팅 되었기 때문입니다. var value = 'hello'는 다음과 같이 두 가지 일이 일어나죠.

  1. 변수 선언 -  var value;
  2. 변수에 값을 할당 - value = 'hello';

다만 다음과 같이 할당된 함수 표현식은 할당문이 제외되므로 실행되지 못합니다.

console.log(value); // undefined
normal(); // ERROR

var normal = function() {
  console.log('normal');
}
var value = 'hello';

var과는 다르게 letconst 상태는 스포크 내 최상단으로 호이스팅되지 않습니다.

let과 var의 상태

var로 선언된 값을 사용할 때는 호이스팅됩니다.

var value = 'hello';

function getValue(condition) {
    if (condition) {
        var value = 'world';
        return value;
    }
    return value; // (1)
}

console.log(getValue(false)); // undefined

따라서 (1)번의 경우 value는 할당문을 제외한 선언부가 호이스팅되어 undefined를 반환합니다.

하지만 다음과 같이 let으로 바꾸게 되면 할당문이 유지되어 hello를 출력합니다.

let value = 'hello';

function getValue(condition) {
    if (condition) {
        let value = 'world';
        return value;
    }
    return value; // (1)
}

console.log(getValue(false)); // hello

var을 블록 단위 스코프를 가지도록 나오게된 IIFE

IIFE는 즉시 실행 함수 표현식(Immediately-Invoked Function Expressions)의 줄인말이죠. '이피'라고 발음합니다. var이 블록 단위 스코프를 가지고자 나온 일종의 꼼수입니다. let이 나온이상 앞으로 많이 사용해야하는 표현은 아니지만 오래된 레거시 코드가 많으므로 이해할 필요가 있습니다.

(function() {
  // 여기서 무언가 하기
})();

함수 선언(Declaration)과 함수 표현식(Expression)

함수 선언은 미리 자바 스크립트의 실행 컨텍스트에 로딩 되어 있으므로 어느 위치에 있던지 호이스팅에 의해 호출할 수 있지만, 함수 표현식은 런타임 시 인터프리터가 해당 라인에 도달 하였을때만 실행이 됩니다.

foo(); // success!
function foo() { // 함수 선언
    alert('foo');
}

위는 함수 선언 방식이며 호이스팅되어 위치에 상관없이 실행되죠. 함수 표현식은 다음과 같습니다.

foo(); // "foo" is not defined.
var foo = function() {  // 함수 표현식
    alert('foo');
};

함수 표현식에서는 함수 자체의 할당문이 호이스팅되지 않으니 정의되지 않았다고 나옵니다. 

alert(foo); // "foo" is not defined.
(function foo() {
  // do something
});
alert(foo); // "foo" is not defined.

마지막은 함수를 표현식으로 만들어 내기 위해 소괄호로 감싼 것입니다. 이것은 아직 실행되지 않으며 괄호를 벗어난 어디서도 접근할 수 없죠. 마지막에 함수처럼 다시한번 괄호를 넣어줍니다.

// 방법 1
(function () {
  alert("I am an IIFE!");
}());

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

두 가지 방법의 작성 스타일은 모두 IIFE를 나타냅니다. 이제 이 함수는 즉시 실행되죠. 단, 다시 재사용하기 위한 이름이 없습니다. 함수에 이름을 부여하고 이름을 이용해 나중에 호출할 수 있습니다.

(showName = function (name) {
  console.log(name || "No Name")
})(); // No Name
showName("Kildong"); // Kildong
showName(); // No Name

소괄호 안에 익명 함수가 있으며 소괄호의 평가(evaluate)는 바로 익명함수를 반환하는 것입니다. 그리고 즉시 실행됩니다. 이후의 두개의 showName() 형태는 익명 함수를 호출해 사용하는것과 같습니다. 이런 형태가 IIFE라고 합니다. 위의 코드는 총 3번 실행되게 되죠.

IIFE의 사용 이유

IIFE를 사용하는 주된 이유는 변수를 전역(global scope)으로 선언하는 것을 피하기 위해서 입니다. 특히 JQuery가 사용되는 부분에서 이런 스타일의 코드를 자주 찾아볼 수 있습니다.

 IIFE스타일의 코드  
 // IIFE형태로 감싸여진 코드
(function () {
    var name = "Kildong";
    function init () {
        doStuff(name);
        // 작업의 시작
    }
    function doStuff(name) {
        // 함수 처리 내용
    }
    function doMoreStuff() {
        // 추가 함수 내용
    }
    // 애플리케이션의 시작
    init ();
}) ();

IIFE를 스코프로 다루기

IIFE를 사용하는 진짜 이유는 바로 스코프라고 할 수 있습니다.

 

Tip:IIFE 내부에 정의된 어떤 변수라도 바깥 세상에서는 보이지 않는다.

 IIFE 내부의 변수나 함수는 외부에서 접근할 수 없는 private한 가시성을 가지는 것과 같습니다.

(function() {
  // var이나 IIFE 밖에서 접근 불가 변수들
  var name; 
  var age; 
  
  init();
  
  // IIFE 밖에서는 접근할 수 없는 함수
  function init() {
    name = 'Kildong'; // IIFE 내에서는 접근 가능
    age = 30;
  }
}());

IIFE 값의 반환

변수에 할당문을 사용하면 즉시 함수가 반환되어 할당되므로 나중에 사용할 수 있습니다.

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

alert(result); // IIFE 사용

IIFE의 매개변수

매개 변수를 다음과 같이 즉시 넘길 수도 있습니다.

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

이제 JQuery에서 보이는 다음과 같은 형태를 보겠습니다.

(function($, global, document) {
  // jQuery를 위해 $를 사용하고, window를 위해 global을 사용
}(jQuery, window, document));

자바스크립트는 특정 식별자를 찾기 위해 현재 함수의 스코프부터 계속 더 높은 레벨의 스코프로 올라가며 찾기 때문에 document와 같은 식별자는 지역 범위를 넘어 IIFE를 빠져나와 참조하게 됩니다.

프로토타입 객체

ES6에서 '클래스처럼' 보이는 여러가지 키워드가 추가되었지만 사실은 자바스크립트는 프로토타입기반의 객체지향언어입니다. 따라서 앞서 언급했듯이 새롭게 추가된 객체지향을 위한 다양한 키워드는 복잡한 프로토타입 시스템을 숨겨주는 문법 양념이 불가하다고 할 수 있죠. 프로토타입 시스템은 다음과 같이 어떤 특정 기능을 확장해야 하는 경우가 생기는데 이럴 때 필요한 개념입니다.

자바스크립트 객체는 프로토타입이라는 숨긴 프로퍼티를 같습니다. 이 숨겨진 프로퍼티의 값은 다른 개체를 참조하는데 그때 참조 대상을 프로토타입 객체라고 부릅니다. 만일 참조할게없다면 프로토타입은 null이 됩니다. 특정객체 내에 프로퍼티가 존재하지 않으면 바로 프로토타입 참조를 따라가면서 프로퍼티가 존재하는지 확인합니다.  

객체의 생성

자바스크립트에서 객체를 만들어내는 방법은 2가지가 있습니다 객체리터럴을 이용하거나 new 키워드를 사용하는 생성자를 이용하는 것입니다.

var object1 = { }; // 리터럴 이용

var object2 = new Object(); // 생성자 이용

이 객체에는 아무것도 정의되어 있지 않지만 이것을 콘솔상에서 살펴보면 다음과 같이 숨겨진 프로퍼티 __proto__가 있는것을 알수 있습니다.

리터럴이나 생성자나 모두 객체의 내용이나 프로토타입의 구조 면에서 동일한 객체를 만들어냅니다. 둘 다 Object 타입을 갖는 객체로 Object 타입의 메서드인 hasOwnProperty 나 toString, valueOf 등을 사용할 수 있죠. 그리고 constructor가 부여되므로 new 키워드를 통해 객체를 생성할 수 있게 되는 것입니다.

Object 타입은 모든 객체의 최상위 타입입니다. 자바스크립트의 객체는 이렇게 참조된 __proto__라는 특수한 참조역할을 하는 프로퍼티를 통해서 Object의 내용들을 가져다 사용할 수 있는데 이것은 일종의 '상속'의 개념과 같이 필요한 내용을 상위에 찾아가서 실행할 수 있게 되는 것입니다. 객체지향에서 엄밀한 의미의 상속은 아니지만 참조를 통해 객체간의 내용을 서로 공유할 수 있게 되는 것입니다.

프로토타입 체인

두 객체의 멤버를 접근하기 위해 다음과 같이 테스트 해 보겠습니다.

var a = {
    attr1: 'a1'
}

var b = {
    attr2: 'a2'
}

b.__proto__ = a;

console.log(b.attr1); // a1

b객체는 a의 프로퍼티인 attr1을 멤버로 가지고 있는것처럼 연결되었습니다.

이것은 마치 체인처럼 연결되 attr1을 찾기 위해 연결된 객체의 내용을 하나씩 따라 올라가게 됩니다. 만일 모든 객체의 속성에 찾고자 하는 프로퍼티가 없다면 최종적으로 Object.prototype에서 찾고 그래도 없다면 undefined를 반환하게 됩니다. 단일 링크드리스트처럼 작동하기 때문에 반대로 a의 객체에서 b의 attr2는 접근할 수 없습니다.

함수의 프로토타입

//constructor
function Parent(name) {
    this.name = name;
}

Parent.prototype.getName = function() {
    return this.name;
};

var p = new Parent('myName');

함수를 통해 생성하면 생성자와 함께 Prototype Object를 가리키며 구성됩니다. Prototype Object는 prototype이라는 특별한 프로퍼티를 통해 접근하게 되죠. 이것을 그림으로 표현하면 다음과 같습니다.

여기서 getName은 Parent의 멤버 메서드처럼 사용됩니다. 객체 p가 프로토타입 메서드에 접근할 수 있는 이유는 p객체의 __proto__ 속성이 Parent.prototype을 가리키고 있기 때문입니다. 이것은 [[Prototype]] 라는 참조 링크를 통해서 연결됩니다. 마지막의 참조 링크는 없으므로 null로 끝나게 됩니다. 그리고 new를 통해서 생성되는 이유는 constructor 생성자 메서드를 호출할 수 있기 때문입니다.

그렇다면 객체를 두개 생성하면 어떻게 될까요?

...
var p = new Parent('myName');
var n = new Parent('Dooly');

이것을 그림으로 표현하면 다음과 같습니다.

new에의해 생성될 때 name값은 객체마다 가지게 됩니다. 하지만 getName() 메서드는 공유하게 되죠. 이것은 메모리 관점에서 효율적으로 사용할 수 있는 방법이죠. 이 메서드에 구현되어 있는 this.name에서 .(점) 앞의 this는 실행 컨텍스트에 따라 달라지므로 각각의 객체를 가리키게 됩니다. 따라서 다음의 결과에서 각각 다른 이름을 출력할 수 있게 되는 것입니다.

console.log(p.getName()); // myName
console.log(n.getName()); // Dooly

여기에서 this를 표현하면 다음과 같습니다.

그러면 만일 getName()을 다음과 같이 Parent에 정의한다면 어떻게 될까요?

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

var p = new Parent('myName');
var n = new Parent('Dooly');

console.log(p.getName()); // myName
console.log(n.getName()); // Dooly

물론 결과는 동일하지만 구조상 getName()은 객체마다 가지게되는 함수가 되어 다음 그림과 같이 메모리 낭비를 부릅니다.

연결을 통한 상속

이제 앞서 만든 예제에서 __proto__를 직접 사용해서는 안됩니다. 대신 Object.crate()을 이용해 프로토타입 체인을 연결합니다.

var a = {
    attr1: 'a'
};

var b = Object.create(a);

b.attr2 = 'b';

만일 객체의 초기화가 필요한 경우에는 init함수를 실행하면서 필요한 값을 초기화 할 수 있습니다.

var person = {
  init: function(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  },
  fullname: function() {
    return this.firstname + ' ' + this.lastname;
  }
}

var kildong = Object.create(person); // new를 사용하지 않음
kildong.init('Kildong', 'Go'); // 초기화를 위해 init()호출

혹은 다음과 같이 create의 두번째 인자에 초기화 값을 지정한 후 생성할 수 있습니다.

...
var kildong = Object.create(person, {
    firstname: { value: 'Kildong' },
    lastname: { value: 'Go' }
}); 

이것을 상속과 같은 개념으로 사용하기 위해 다음과 같은 예제를 살펴봅시다.

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

Parent.prototype.getName = function() {
    return this.name;
};

function Child(name, age) {
    Parent.call(this, name);

    this.age = age;
}

Child.prototype = Object.create(Parent.prototype); // (1)
Child.prototype.constructor = Child;

Child.prototype.getAge = function() {
    return this.age;
};

var c = new Child('Kildong', 30); // (2)

(1) 에서 Object.create() 을 이용해 Childprototype 객체를 교체합니다. (1) 에서 만들어진 새 객체인 Child.prototype 은  __proto__ 속성이 Parent.prototype 을 가르키게 되고 (2) 에서 Child의 인스턴스 c__proto__Child.prototype 을 참조하게 됩니다. 이렇게 해서 cChild.prototypeParent.prototype 으로 연결되는 프로토타입이 만들어졌고 프로토타입 탐색에서 탐색 경로로 식별자를 찾게 됩니다.

이 그림에도 좀 더 나아가면 Child()나 Parent는 결국 함수로 만들어지므로 Function prototype객체를 참조하고 최종적으로 이것의 constructor를 호출하여 생성되는 것입니다.  Function prototype까지 포함하면 그림은 다음과 같죠.

여기서는 생략했으나 getAge()getName()도 함수이므로 모두 Function prototype객체를 참조하죠. 그리고 코드에서 사용되었던 this.name이나 this.agethis는 결국 new로 생성된 c를 가리킵니다. 따라서 초기화된 값인 "Kildong"과 30은 오직 생성된 객체인 c에만 가지게 되는 것이죠.

프로토타입 상속이라는 것은 결국 부모 프로토타입 객체의 내용을 하위에서 사용할 수 있게 되는 것입니다. 최종적인 객체 c는 Parent의 name과 Child의 age를 포함해 getAge()와 getName()메서드를 사용할 수 있게됩니다.

추가로 Child 생성자에서 Parent 생성자를 빌려서 객체를 확장한 것은 생성자 빌려 쓰기라는 오래된 기법으로 자바스크립트 상속에서 부모의 생성자를 실행하는 방법으로 부모타입의 생성자의 내용도 상속 받도록 합니다. 단 자바스크립트는 단일상속만 가능하다는 것을 기억하시기 바랍니다.

Object.create()를 사용하지 않는 환경에서는 다음과 같은 방법으로 연결해야 합니다.

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

Parent.prototype.getName = function() {
    return this.name;
};

function Child(name) {
    Parent.call(this, name);

    this.age = 0;
}

function Ghost() {};
Ghost.prototype = Parent.prototype;
Child.prototype = new Ghost();

Child.prototype.constructor = Child;

Child.prototype.getAge = function() {
    return this.age;
};

var c = new Child();

위의 3줄은 Object.create()의 역할과 동일합니다. 생성되는 인스턴스들에서 독립적으로 name에 대한 내용을 사용하기 위해 중간에 Ghost()라는 임시 생성자를 통해 연결하며 이것을 new를 통해 Child의 prototype 객체를 생성하게 됩니다. Ghost는 단순히 프로토타입 체인만 연결된 빈 객체를 얻기 위함입니다.

모든것은 Object를 상속받는다

자바스크립트에서 사용하는 Array, Date, Function과 같은 내장 객체들도 프로토타입에 메서드를 저장해 놓습니다. 이러한 프로토타입은 네이티브 프로토타입이죠. 배열 [1, 2, 3]을 만들면 기본 new Array() 생성자가 내부에서 사용되기 때문에 Array.prototype이 배열 [1, 2, 3]의 프로토타입이 되죠. Array.prototype은 배열 메서드도 제공합니다. 결국 모든 프로토타입 객체는 최상위에 Object로부터 상속된다고 할 수 있습니다.  객체만 강조해서 그림을 그리면 다음과 같이 되겠죠.

그런데 그림처럼 toString()함수의 경우 하위에도 존재합니다. 이 경우에는 체인에서 가장 가까운 메서드가 사용되므로 '오버로딩'을 한 효과처럼 작동합니다. 부모의 구현을 다시 쓴것과 같으니까요.

숫자(Number)나 문자열(String), 불린(Boolean)의 경우에는 new로 생성되지 않으면 사실 객체가 아닌 '원시값'을 가지며 이것은 객체로부터 무언가 상속하지 않는다는 것입니다. 다만 이런 원시값의 프로퍼티에 접근하려고하면 내장 생성자 String,Number, Boolean을 사용하는 임시 래퍼 객체가 생성됩니다. 임시 래퍼 객체는 이런 메서드를 제공하고 사라집니다.

프로토타입에서 빌려오기

종종 네이티브 프로토타입에 있는 메서드를 가져다 쓰고 싶을때가 있을겁니다. 다음의 코드를 봅시다.

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join; // (1)

alert( obj.join(',') ); // Hello,world!

배열 객체를 만들고 여기에 (1)번과 같이 Array의 join()을 복사해 가져오고 있습니다.  내장 메서드 join의 내부 알고리즘은 제대로 된 인덱스가 있는지와 length 프로퍼티가 있는지만 확인하기 때문입니다. 호출 대상이 진짜 배열인지는 확인하지 않죠. 다수의 내장 메서드가 이런 식으로 동작합니다.

객체의 생성 패턴

 

ES6문법

앞서 장황한 코드는 ES6 문법에서 추가된 class와 extends를 이용하여 표현할 수 있습니다.

class Parent {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
}

class Child extends Parent {
    constructor(name) {
        super(name); // 생성자 빌려쓰기 대신 super 함수를 이용
        this.age = 0;
    }

    getAge() {
        return this.age;
    }

}

코드의 모양이 좀 간결해 졌지만 객체의 프로토타입 체인 연결 구조는 기존과 동일하고 식별자를 찾아가는 방식도 내부적으로 동일합니다.

this의 정리

다른 언어와 달리 자바스크립트의 this는 런타임에 결정됩니다. 메서드가 어디에 정의되어 있는지 상관없이 점 앞의 객체가 무엇인가에 따라 컨텍스트는 달라집니다. 메서드를 호출한 객체가 있는 경우 해당 객체가 this가되며 일반적인 경우에는 this는 전역 객체인 window가 됩니다.

function MyClass(name) { 
  this.myProperty = name; 
} 

MyClass.prototype.myMethod = function() { 
  console.log(this); 
  console.log(this.myProperty); 
  console.log(window === this); // false
}

var myClass = new MyClass('Kildong');
console.log(myClass.myProperty);
myClass.myMethod();
console.log(window === this); // true

Result  
"Kildong"
[object Object] {
  myMethod: function() {
  window.runnerWindow.proxyConsole.log(this);
  window.runnerWindow.proxyConsole.log(this.myProperty);
},
  myProperty: "Kildong"
}
"Kildong"
false
true

new 연산자에 의해 객체가 생성되었으므로 myMethod1()으로부터 호출되는 this는 myClass 객체가 됩니다. 단순히 바깥 전역의 this는 여전히 window를 나타냅니다. 만일 new 연산자를 사용하지 않으면 myProperty는 전역의 window에 저장됩니다.

function MyClass(name) {
  this.myProperty = name;
}

MyClass.prototype.myMethod = function() {
  console.log(this);
  console.log(this.myProperty);
  console.log(window === this); // false
}

var myClass = MyClass('Kildong');  // (1)
console.log(myClass) // undefined
// console.log(myClass.myProperty); // ERROR
// myClass.myMethod(); // ERROR
console.log(myProperty); // "Kildong"
console.log(this.myProperty); // "Kildong"
console.log(window === this); // true

MyClass가 함수로 읽게 되면 이제 이 함수에는 return이 없으므로 (1)번과 같이 할당문에 들어갈 내용이 없는 myClass는 undefined가 됩니다. myProperty만이 전역으로 존재합니다.

자동 객체 변환

call()이나 apply()함수를 사용하면 this로 넘겨지는 값이 객체가 아닐 때 자동으로 객체로 변환합니다.

var person = {}; // object
MyClass.apply(person, ['Dooly', ]);

console.log(person.myProperty); // Dooly

함수 내부의 함수에서 this 사용

$('div').on('click', function() {
  console.log(this); // <div>
  function inner() {
    console.log('inner', this); // inner Window
  }
  inner();
});

첫 블록 내에 this는 <div>를 가리키게 됩니다. 하지만 내부의 함수에서 this는 기본 호출 대상인 window가 지정됩니다. 이것을 계속해서 <div>로 지정하고자 하는 경우 다음과 같이 사용하게 됩니다.

$('div').on('click', function() {
  console.log(this); // <div>
  var that = this;
  function inner() {
    console.log('inner', that); // inner <div>
  }
  inner();
});

 화살표 함수를 사용하면 that을 사용하지 않아도 곧 호출자의 객체를 그대로 사용할 수 있습니다.

화살표 함수를 사용한 경우  
$('div').on('click', function() {
  console.log(this); // <div>
  const inner = () => {
    console.log('inner', this); // inner <div>
  }
  inner();
});

이렇게해서 중첩된 함수 안에서 this의 문맥을 보존할 수 있게 되었습니다.

문자열

.includes() - 문자열의 포함 여부

문자열 포함 여부를 구현하기 위해서 리턴 값이 -1보다 큰 지 체크하는 것 대신, 간단하게 불린 값을 리턴하는 .includes() 메소드를 사용할 수 있습니다.

var string = 'food';
var substring = 'foo';

console.log(string.indexOf(substring) > -1);

이것을 다음과 같이 사용합니다.

console.log(string.includes(substring)); // true

.repeat() - 문자열 반복

function repeat(string, count) {
    var strings = [];
    while(strings.length < count) {
        strings.push(string);
    }
    return strings.join('');
}

이런 코드를 다음과 같이 사용할 수 있습니다.

'hello'.repeat(3); // 'hellohellohello'

템플릿 리터럴

문자열 연결을 위해 다음과 같이 사용할 수 있습니다.

var name = '길동';
var age = 30;

console.log('제 이름은 ' + name + '이고, 나이는 ' + age + '살 입니다.');

이것은 다음과 같이 백틱(`)을 사용해 표현합니다.

console.log(`제 이름은 ${name}이고, 나이는 ${age}살 입니다.`);

 

 구조 분해 (Destructuring)

디스트럭처링은 배열이나 객체의 값들을 추출하는데 유용하게 사용됩니다.

var arr = [1, 2, 3, 4];
var a = arr[0];
var b = arr[1];
var c = arr[2];
var d = arr[3];

이것을 이렇게,

let [a, b, c, d] = [1, 2, 3, 4];

console.log(a); // 1
console.log(b); // 2

객체를 디스트러처링 할때는 다음과 같이 중괄호를 사용합니다.

let luke = { occupation: 'jedi', father: 'anakin' };
let {occupation, father} = luke;

console.log(occupation); // 'jedi'
console.log(father); // 'anakin'

 

매개변수 다루기

매개변수의 기본값

매개변수에 기본값을 지정하기 위해 사용하던 방법은 ES6에서 간단하게 설정할 수 있습니다.

function addTwoNumbers(x, y) {
    x = x || 0;
    y = y || 0;
    return x + y;
}

이런 방법을 다음과 같이 변경합니다.

function addTwoNumbers(x=0, y=0) {
    return x + y;
}

addTwoNumbers(2, 4); // 6
addTwoNumbers(2); // 2
addTwoNumbers(); // 0

레스트 매개변수 (Rest Parameter)

가변 인자를 처리하기 위해서

function logArguments() {
    for (var i=0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}

이와 같은 방법을 다음과 같이 레스트 연산자를 사용해 가변적인 인수를 넘깁니다.

function logArguments(...args) {
    for (let arg of args) {
        console.log(arg);
    }
}

네임드 파라미터(Named Parameter)

ES5의 네임드 파라미터를 처리하는 방법 중 하나는 jQuery에서 차용된 options object 패턴을 사용하는 것입니다.

function initializeCanvas(options) {
    var height = options.height || 600;
    var width  = options.width  || 400;
    var lineStroke = options.lineStroke || 'black';
}

파라미터에 destructuring을 사용하면 같은 기능을 구현할 수 있습니다.

function initializeCanvas(
    { height=600, width=400, lineStroke='black'}) {
        // 여기에서 height, width, lineStroke 변수를 사용합니다.
    }

만약 모든 파라미터를 선택적으로 넘기고 싶다면, 다음과 같이 빈 객체로 destructuring 하면 됩니다.

function initializeCanvas(
    { height=600, width=400, lineStroke='black'} = {}) {
        // ...
    }

전개 연산자(Spread Operator)

ES5에서는 배열 내 숫자들의 최대 값을 찾기 위해서 Math.maxapply 메소드를 사용했습니다.

Math.max.apply(null, [-1, 100, 9001, -32]); // 9001

ES6에서는 이제 전개 연산자를 이용해서 함수에 파라미터로 배열을 넘길 수 있습니다.

Math.max(...[-1, 100, 9001, -32]); // 9001

다음과 같이 직관적인 문법을 통해 쉽게 배열 리터럴을 합칠 수도 있습니다.

let cities = ['서울', '부산'];
let places = ['여수', ...cities, '제주']; // ['여수', '서울', '부산', '제주']

 

맵(Map)의 사용

맵은 값을 위해 get, set 그리고 search 등의 메소드를 제공합니다.

let map = new Map();
> map.set('name', 'kildong');
> map.get('name'); // kildong
> map.has('name'); // true

키 값으로 어떤 타입을 전달해도 문자열로 형변환되지 않습니다.

let map = new Map([
    ['name', 'kildong'],
    [true, 'false'],
    [1, '하나'],
    [{}, '객체'],
    [function () {}, '함수']
]);

for (let key of map.keys()) {
    console.log(typeof key);
    // > string, boolean, number, object, function
}

또한 .entries()를 사용하면 맵을 순회할 수 있습니다.

for (let [key, value] of map.entries()) {
    console.log(key, value);
}

 Promise

Promise는 다음과 같이 수평적인 코드(콜백 지옥)의 형태를 바꿀 수 있습니다.

func1(function (value1) {
    func2(value1, function (value2) {
        func3(value2, function (value3) {
            func4(value3, function (value4) {
                func5(value4, function (value5) {
                    // Do something with value 5
                });
            });
        });
    });
});

이것을 다음과 같이,

func1(value1)
    .then(func2)
    .then(func3)
    .then(func4)
    .then(func5, value5 => {
        // Do something with value 5
    });

수직적으로 바꿀 수 있습니다.

참고

 

 

youngdeok의 이미지

Language

Get in touch with us

"어떤 것을 완전히 알려거든 그것을 다른 이에게 가르쳐라."
- Tryon Edwards -