Friday, June 10, 2016

ECMAScrtip2015 ( ES2015, ES6 )

아래의 내용은 codeschool의 ES2015를 요약한 것이다. 실제로 강의에서는 더 많은 예와 사용사례를 들지만 필요한 부분만 정리하였다.

let 변수
1. let 변수는 hoisting이 되지 않고 block 영역 내에서만 유효하게 된다.
function showUsers (users) {
  if (users.length >= 2) {
    var messagePlural = 'the plural';
  } else {
    var messageSingular = 'the singular';
  }
  console.log(messageSingular);}

var users = ['a', 'b', 'c'];
showUsers(users);

위 코드를 돌려보면 프로그램이 에러가 발생하지 않고 undefined가 콘솔에 찍히게 된다. 이는 javascript의 hoisting 이라는 특성때문인데 var 가 있는 변수를 프로그램이 시작하기 전에 가장 가까이에 있는 function의 최상단에 변수를 초기화 하기 때문이다. 곧 아래와 같이 작동하게 된다.

function showUsers (users) {
  var messagePlural, messageSingular;
  if (users.length >= 2) {
    messagePlural = 'the plural';
  } else {
    messageSingular = 'the singular';
  }
  console.log(messageSingular);}

var users = ['a', 'b', 'c'];
showUsers(users);

ES2015 에서는 let 이라는 키워드를 이용해서 선언된 변수는 function이 기준이 아닌 block ('{', '}' 으로 둘러쌓여 있는 코드 영역, 곧 if, else, for, while 등) 영역내에서 유의한 변수가 된다. 그래서 block 영역 이외에서 let 변수가 사용되어 지면 ReferenceError 가 발생하게 된다.

보통 for 문을 사용할 때 index에 var변수를 많이 사용하였는데 이는 아래와 같은 문제가 발생하게 된다. 
function changeProfile(profiles) {
  for (var i in profiles) {
    _update('users/' + profiles[i], function () {
      console.log(profiles[i]);
    });
  }}

changeProfile(['a', 'b', 'c'])
// c
// c
// c 가 출력됨

위 코드에서 var i 는 hoisting이 되어 for 문이 실행되기 전에 함수 상단에 선언되어 지고 for 문에 의해 _update의 인자로 주어진 각각의 콜백함수가 실행되기 전에 동일한 i를 갖게 끔 등록되어 진다. 이 후 실행이 되어지기 때문에 i가 갖는 최종값 2를 공유하게 된다.

이럴 경우 for 문에 var 대신 let을 사용하면 위와 같은 문제가 해결된다.

2. let 변수는 재선언을 해서는 안된다.
let 변수에 새로운 값을 할당하는 reassign은 가능하지만 재선언을 하면 TypeError 가 발생

let variable = 'good';
variable = 'best'; // reassigning 은 가능

let variable = 'good';
let variable = 'bad'; // redeclaring 은 불가능



Const 변수

const 변수는 읽기만을 위한 변수이다.
선언시 변수에 값을 할당(정의)을 해야 한다.
reassign이 안된다.
또한 let 변수와 마찬가지로 function 단위가 아닌 block 단위에서 유의한 변수이다.


functions
1. default parameter

javascript 는 parameter를 필요로 하는 함수에 paramter가 없이 호출을 해도 에러를 발생시키지 않는다. 그렇기 때문에 아래 countName1과 같은 방법으로 default paramter를 설정하였으나 ES2015 부터는 countName2 처럼 default paramter를 설정할 수 있다.

function countName1 (userNames) {
  let names = typeof userNames !== 'undefined' ? userNames : [];
  console.log(names.length);}

countName1();


function countName2 (userNames = []) {
  console.log(names.length);}

countName2();


2. named parameter

아래 코드에서 show1 함수의 두번째 인자 options은 object 이다. show1 코드에 options 에는 어떠한 property가 들어가야 하는지 직관적이지 않다. 이는 ES2015에서 show2 처럼 named parameter 를 사용하는 걸로 바꿀 수 있다. 

function show1 (name, options) {
  let color = options.color;
  let size = options.size;
  console.log(color);
  console.log(size);}

function show2 (name, {color, size}) {
  console.log(color); // color is a local variable.
  console.log(size); //}

show2('Tom', {color: 'blue'}) // 'size' will be undefined.
show2('Tom') // this will be error.


function show3 (name, {color, size} = {}) {
  console.log(color); // color is a local variable.
  console.log(size);
}

show2 를 사용하는데 있어 options 인자를 아예 주지 않고 사용하면 TypeError가 발생하게 되는데 이를 방지하기 위해 show3 처럼 default parameter를 지정할 수 있다.


Rest params, Spread op, Arrow Func
1. rest parameters
variadic function 이란 parameter의 수가 유동적인 함수를 의미한다. 이는 보통 아래 코드의 showUser1 처럼 구현한다. 그러나 이는 직관적이지 않고 또한 showUser1에 추가적으로 parameter가 필요할 경우 문제가 생길수 있다. 이를 대체하기 위한 방법으로 ES2015에서는 rest paramter를 사용한다.

function showUsers1 () {
  for (let i in arguments) {
    console.log(arguments[i]);
  }}

showUsers1('user1', 'user2', 'user3');

function showUser2 (...users) {
  for (let i in users) { // users == ['user1', 'user2', 'user3']
    console.log(users[i]);
  }}

showUser2('user1', 'user2', 'user3');

showUser2에서 처럼 인자 앞에 점 3개를 붙이면 이는 rest parameter를 의미하게 되고 넘겨받은 인자들을 array로 받게 된다. rest paramter 는 항상 마지막 인자로 사용되어야 한다.

2. spead operator 
위의 rest parameter 에서는 함수 정의에 사용된 점 3개는 분리된 인자를 하나의 array로 받겠다는 의미가 되고 반대로 함수의 호출에서 넘겨주는 인자에 점 3개를 사용하면 array를 분리된 인자로 변형시키겠다는 의미가 된다. 

function showUser (...users) { // users is an array.
  for (let i in users) {
    console.log(users[i]);
  }}

var users = ['user1', 'user2', 'user3']
showUser(...users);

3. arrow function 
javascript에서 함수는 호출이 되는 scope에 bind 하게 되어 지는데 arrow function 를 이용하면 함수가 정의된 scope에 bind하게 된다.
arrow function 의 형태는 아래와 같다. ES5에서의 보통의 함수 정의에서 function 키워드를 제거하고 "=>"를 붙인 형태로 나타낸다. 아래 코드는 동일한 기능을 한다. parameter가 한개일 때는 괄호를 생략할 수 있다.

// function 1
function (param) {
  statements
}

// arrow function 1
(param) => {
  statements
}

// arrow function 2
param => {
  statements
}

아래의 코드는 동일한 기능을 하는 코드이다. arrow function의 특징 중 한가지는 함수의 내용이 한줄의 표현식 밖에 없을 경우에는 대괄호 "{}" 를 생략 가능하고 이 경우에는 자동적으로 표현식 앞에 암묵적으로 return이 삽입된다. 그렇기 때문에 아래 코드의 arrow function 2와 같이 return을 생략해도 되는 것이다.

// function 1
function (param) {
  return expression
}

// arrow function 1
(param) => {
  return expression
}

// arrow function 2
param => expression

arrow function은 보통의 함수와는 달리 this와 arguments 객체를 갖지 않는다. 그렇기 때문에 lexial this 를 부모 함수와 공유하게 된다.


Objects and strings

Object initializer shorthand
object 의 property 명과 값을 가지고 있는 변수의 식별자가 동일한 경우 이를 축약해서 표현 가능하다. 아래 코드에서 name1 함수와 name2함수의 동일한 형태의 object 를 return하게 된다.

function name1 (first, last) {
  let fullname = first + ' ' + last;
  return {first: first, last: last, fullname: fullname};
}

function name2 (first, last) {
  let fullname = first + ' ' + last;
  return {first, last, fullname};
}

let first = "Good";
let last ="Boy";
let fullname = name1(first, last);

someoneName1 = {first: first, last: last, fullname: fullname};
someoneName2 = {first, last, fullname};

마찬가지로 someoneName1과 someoneName2는 동일한 형태의 object 를 갖는다.

function을 property로 갖을 경우 아래 코드의 makeName2에서 function 키워드를 생략하여 표현할 수 있다.

function makeName1 (first, last) {
  let fullname = first + ' ' + last;
  return {
    first,
    last,
    fullname,
    showName: function () {
      console.log(fullname)
    };
  };
}

function makeName2 (first, last) {
  let fullname = first + ' ' + last;
  return {
    first,
    last,
    fullname,
    showName () {
      console.log(fullname)
    };
  };
}

object destructuring
아래 코드와 같이 object에서 property 값을 받을 경우 {} 괄호를 써서 object 의 property를 풀어서 받을 수 있다. 아래 코드의 맨 아래 표현식에서 보듯이 전체 property가 아닌 특정 property만을 받을 수도 있다. 

function makeName (first, last) {
  let fullname = first + ' ' + last;
  return {first, last, fullname};
}

let someone = makeName('good', 'boy');
let first = someone.first;
let last = someone.last;
let fullname = someone.fullname;

let {first, last, fullname} = makeName('good', 'boy');

let {first, fullname} = makeName('good', 'boy');

template strings
template string은 back-ticks ( `` )에 의해 표현된다. 아래 코드의 fullname은 template string 으로 표현한 문자열이다. 또한 template string 를 이용하면  multiline line text를 쉽게 표현할 수 있다.

let first = 'good';
let last = 'boy';
let fullname = `${first} ${last}`;

let sentence = `
Hi, ${first} ${last}.
Nice weather !
`;

Object.assign
Object.assign 함수를 이용하면 object를 merge할 수 있다. parameter 중 뒷부분의 object값이 앞 object 의 property를 덮어 쓰게 된다. 그리고 첫번째 parameter인 object에 값을 변경하여 return하게 된다. 

let option1 = {first: 'good', last: 'girl'};
let option2 = {last: 'boy'};

let finalOption = Object.assign({}, option1, option2);
// finalOption = {first:'good', last:'boy'}와 동일

이는 함수의 default값을 설정할 경우 유용하게 사용되어 질 수 있다.

Array
array destructuring
아래 코드와 같이 array의 값을 개별 변수에 할당하는 방법이 있다.

let names = ['name1', 'name2', 'name3'];

let name1 = names[0];
let name2 = names[1];
let name3 = names[2];

let [name1, name2, name3] = names;

let [name1, , name3] = names;

위 코드의 마지만 표현식에서와 같이 불필요한 값은 skip할 수 있다.

또한 아래의 코드와 같이 위에서 알아본 rest params를 이용하여 나머지 값들을 하나의 변수에 array로 받을 수 있다.

let names = ['name1', 'name2', 'name3'];

let [name1, ...rest] = names
// rest = ['name2', 'name3'] 와 동일


for …of (array loop)
for of 문을 이용하여 array 의 값에 직접 접근 가능하다.

let names = ['name1', 'name2', 'name3'];

// for1 
for (let index in names){
  console.log(names[index]);
}

//for2
for (let name of names){
  console.log(name);
}

for …of는 plain javascript object(일반 객체) 에서는 사용할 수 없다. for …of는 Symbol.iterator라는 속성에 특별한 함수가 있는 객체에서만 사용가능하다. 

find element
아래 코드와 같이 array의 find함수에 test 함수를 인자로 호출하면 test 함수에서 true 값을 return하는 첫번째 element를 찾을 수 있다.

let users = [
  {name: 'name1', admin: false},
  {name: 'name2', admin: true},
  {name: 'name3', admin: true},
];

let admin = users.find( (user) => user.admin );

// admin = {name: 'name2', admin: true}; 와 동일

Maps
map 은 key/value 쌍의 집합인 자료 구조이다. 물론 object를 map 처럼 사용할 수 있으나 key 값을 string으로 처리하기 때문에 아래와 같은 문제가 생긴다.

let user1 = {name: 'Sam'};
let user2 = {name: 'Bill'};

let userProfile = {};
userProfile[user1] = 'student';  // userProfile['object Object'] = 'student'
userProfile[user2] = 'teacher';  // userProfile['object Object'] = 'teacher'

console.log(userProfile[user1]);  // teacher
console.log(userProfile[user2]);  // teacher

위 문제는 map 자료구조를 통해 해결할 수 있다.

let user1 = {name: 'Sam'};let user2 = {name: 'Bill'};

let userProfile = new Map();
userProfile.set(user1, 'student');
userProfile.set(user2, 'teacher');

console.log(userProfile.get(user1));  // student
console.log(userProfile.get(user2));  // teacher

특히나 runtime시 key 값이 결정되는 자료구조의 경우 map을 사용하는 것이 좋다. 또한 key들의 자료형이 동일하고 마찬가지로 value들의 자료형이 동일할 경우 map을 사용하는 것이 일관성있어서 좋다.

map을 사용할 경우 아래와 같이 for ..of 구문을 사용할 수 있다.

let mapData = new Map();

mapData.set('key1', 'value1');
mapData.set('key2', 'value2');
mapData.set('key3', 'value3');

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

WeakMap
key로 object 만 허용된 map 으로 primitive data type (stirng, number, boolean) 은 key로 사용할 수 없다. weakmap은 for ..of 구문을 사용할 수 없다.
weakmap의 장점은 메모리 효율에 있다. weakmap은 key로 사용하는 object의 garbage collection을 막지 않는다. 곧 weakmap에서 key로 사용되는 object 가 다른 곳에서 사용이 다 되어서 garbage collection이 되어 사라진다 하더라도 weakmap은 여전히 사용가능하다.

Sets
array와는 달리 unqiue한 element만 저장한다. 중복된 element는 대표인 하나의 element만 저장하는 자료 구조이다.

for ..of 구문을 사용할 수 있고 array destructuring이 가능하다.

let setData = new Set();
setData.add('Sam');
setData.add('Mike');
setData.add('Bill');
setData.add('Bill');

console.log(setData.size); // 3

for (let name of setData) {
  console.log(name);}

WeakSet
object만 저장할수 있다. WeakMap과 마찬가지로 gabage collection을 막지 않으며 for ..of 문을 사용할 수 없다.

Class
class syntax는 constructor function을 이용한 prototype-based inheritance의 syntactical sugar로 새로운 형태의 객체가 아닌 기존의 객체를 다른 방식으로 생성하는 것 뿐이다.

아래 코드에서 constoructor function 을 통한 객체 생성과 class 문법을 사용한 객체는 동일하다.

// Constructor function
function Person1 (name, age) {
  this.name = name;
  this.age = age;}

Person.prototype.action = function (something) {
  console.log(something);}

person1 = new Person1('Tom', 12);


// Class syntax
class Person2 {
  constructor (name, age) {  // new 키워드로 객체 생성 시 항상 실행되는 함수
    this.name = name;
    this.age = age;
  }
  action (something) { // instance method
    console.log(something);
  }
}

person2 = new Person2('Tom', 12);

_ (underscore)로 시작하는 이름의 method 는 암묵적으로 객체 내부에서만 사용하는 method를 의미하기 때문에 객체 외부에서는 호출하지 않는것이 일반적이다. 

prototype-based inheritance 는 extends 라는 키워드로 아래와 같은 방식으로 상속받는다.

class Human {
  constructor (name, age) {
    this.name = name;
    this.age = age;
  }
  eat () {
    console.log('eat')
  }}

class Student extends Human {
  constructor (name, age, grade) {
    super(name, age);  // 상속을 받았으면 항상 super를 호출해야 함.
    this.grade = grade;
  }
  study () {
    console.log('study')
  }
  eat() {
    super.eat(); // 부모 클래스의 메소드 호출
    console.log('eat a lot');
  }
  gettingOld () {
    this.age += 1;
  }}

let student1 = new Student('Tom', 14, 2);
student1.eat()
student1.gettingOld();
student1.age  // 15

Module
이전에 ECMAScript 에서 modularization을 하는 방법은 global variable을 사용하는 것이였다(html 파일에 <script> 태그를 이용하여 링크시킴). ECMAScript2015에서도 html에 <script> 를 통해 링크를 시키는 것은 동일하지만 아래 코드의 print-message.js 와 같이 global namespace를 사용하지 않기 때문에 부작용이 일어나지 않는다.

// print-message.js
export default function (message) {  // default 로 export 했기 때문에 임의의 이름으로 import 가능
  console.log(message);
}

// root.js
import print from './print-message';  // .js 확장자 생략 가능
print('good');

export default 로 정의된 module은 그 밖의 함수들은 외부에서 접근 불가능 하다.

// print-message.js
export default function (message) {  // default 로 export 했기 때문에 임의의 이름으로 import 가능
  console.log(message);
}

function print2 (message) { // export default 때문에 이 함수는 외부에서 호출 할 수 없다.
  console.log(message);
}

이럴 경우 default 키워드를 사용하지 말고 외부에서 접근 가능하게 하고자 하는 함수 앞에 export 키워드를 붙인다. 모듈을 호출하는 파일에서는 import 시 함수이름을 모듈에서 정의한 함수 이름과 동일하게 해야 한다.

// print-message.js
export function print1 (message) {
  console.log(message);
;}

export function print2 (message) {
  console.log(message);
}

// root.js
import {print1, print2} from './print-message'; // 호출하는 함수의 이름이 함수 정의에 사용된 이름과 동일해야 함.
print1('good');
print2('best');

// anotherRoot.js
import * as print from './print-message';
print.print1('good');
print.print2('best');

아래와 같은 방법으로 module의 함수들을 export 할 수도 있다.

// print-message.jsfunction print1 (message) {
  console.log(message);}

function print2 (message) {
  console.log(message)}

export {print1, print2};

const 변수와 class 도 위의 function과 같은 방법으로 export 할 수 있다.

Promise
Javascript 에서 서버와의 통신등으로 인해 발생할 수 있는 "main thread 의 block" 상황을 막기 위해서 callback 함수를 인자로 함수를 호출하는 식의 continuation passing style로 구현을 하는 경우가 많다. 이럴 경우 callback 함수에서 또 다른 callback을 던지는 경우 굉장히 복잡한 nested code가 된다. 또한 던져진 callback 함수에서 매번 error을 체크하는 코드를 넣어야 하는 불편함이 있다.

아래와 같이 return을 promise로 받으면 then 메소드를 이용하여 코드를 단순화 할 수 있다

// Define promise function getDataFromServer (name) {
  return new Promise(resolve, reject) {
    let url = `user/${name}`;
    let request = new XMLHttpRequest();
    request.open('GET', url, true);

    request.onload = function () {
      if (request.status >= 200 && request.status <400) {
        resolve(JSON.parse(request.response)); 
      } else { 
        reject(new Error(request.status)); // error 를 던지면 promise의 catch 메소드로 직행 
      }
    };
    request.onerror = function () {
      reject(new Error('error'));
    };
  }
}

// use promise 
getDataFromServer('Sam') 
  .then(someFilter) // promise의 then은 또 다른 promise를 return. 
  .then(doSomething) 
  .catch(function (err) { console.log(err); }); // promise가 error를 던지면 catch 메소드로 직행

Iterators
array, map, set 같은 경우 for ..of 구문을 사용하면 iterator object를 반환하게 된다. 아래 코드에서 보듯이 for ..of 문은 실제로주석에 표시되어 있는 것처럼 각 자료형의 Symbol.iterator 속성의 호출로 iterator 객체가 생겨서 일어나는 것이다.

let names = ['Jack', 'Bill', 'Tom'] ;

for (let name of names) { // let iterator = names[Symbol.iterator]()
  console.log(name);        // let firstRun = iterator.next()  //   firstRun == {value: 'Jack', done: false}
}                                     // let name = firstRun.value
                                      // let secondRun = iterator.next() ...

iterator의 next 메소드를 호출하면 done과 value 속성을 갖는 객체가 return 되고 done이 false 이면 for 문이 계속 돌게 되는 것이다.

plain javascript object의 경우 Symbol.iterator 속성이 없기 때문에 iterator object를 생성할 수가 없다. 그러나 Symbol.iterator를 구현해 주면 iterator 객체를 return 할수 있게 되고 for ..of 구문과 array destructuring에서 사용가능해 진다.

Generators
함수의 이름 앞에 * 기호를 사용하면 generator function이 된다. generator function은 return 대신 yield를 사용할 수 있다 (* 기호는 함수 이름앞에 붙여도 되고 띄어도 상관없다).

function * getNames () {
  yield "Jack";  // {done: false, value: 'Jack'};
  yield "Bill";  // {done: false, value: 'Bill'};
  yield "Tom";  // {done: false, value: 'Tom'};}

for (let name of getNames()) {
  console.log(name);}

let names = [...getNames()];

let [name1, name2] = getNames();

generator 함수는 iterator object와 같은 next 메소드를 갖는 객체를 return 하기 때문에 generator function에 의해 return 된 object는 for ..of, spead operator, destructuring 에서 사용가능하다.

이 genrator를 이용하면 plain javascript object 의 Symbol.iterator 속성의 구현을 쉽게 할 수 있다.




















이 글은 Evernote에서 작성되었습니다. Evernote는 하나의 업무 공간입니다. Evernote를 다운로드하세요.