New in JavaScript 1.7

Mozilla의 New In JavaScript 1.7를 정리한 것이다. 이 글의 내용은 아직 논쟁중이고 보편적이지 않아서 당장은 별 도움되지 않을 듯...

Generators and Iterators

개인적으로 closure가 있는데 Generator나 Iterator가 유용한지 조금 회이적이다.

Iterators

Iterator는 Iterator() 함수로 만들고:

var lang = { name: 'JavaScript', birthYear: 1995 };
var it = Iterator(lang);

next()를 호출하면 해당 key, value가 반환된다:

var pair = it.next(); // Pair is ["name", "JavaScript"]
pair = it.next(); // Pair is ["birthYear", 1995]
pair = it.next(); // A StopIteration exception is thrown

next()대신 for..in으로도 사용할 수 있다. 여기에서도 StopIteration 예외가 발생하면 loop이 중지된다:

var it = Iterator(lang);
  for (var pair in it)
print(pair); // prints each [key, value] pair in turn

key, value pair가 아니라 단순히 key만 iteration하고 싶으면 Interator()의 두번째 인자에 true를 넘긴다:

var it = Iterator(lang, true);
for (var key in it)
  print(key); // prints each key in turn

Iterator()는 Array에도 OK:

var langs = ['JavaScript', 'Python', 'C++'];
var it = Iterator(langs);
for (var pair in it)
  print(pair); // prints each [index, language] pair in turn

Array에서 Iterator()의 두번째 인자로 true를 주면 index를 iteration한다.

var langs = ['JavaScript', 'Python', 'C++'];
var it = Iterator(langs, true);
for (var i in it)
  print(i); // prints 0, then 1, then 2

let 키워드로 block scoped 변수를 만들어 할당할 수 있다:

var langs = ['JavaScript', 'Python', 'C++'];
var it = Iterator(langs);
for (let [i, lang] in it)
  print(i + ': ' + lang); // prints "0: JavaScript" etc.

Defining custom iterators

특정 범위의 수를 Iteration하는 Interator를 만들어 보자.

먼저 Range 객체를 만들고:

function Range(low, high){
  this.low = low;
  this.high = high;
}

low, high사이의 값을 순서대로 반환하고 끝나면 StopInteration 예외를 던지는 RangeIterator를 만든다:

function RangeIterator(range){
  this.range = range;
  this.current = this.range.low;
}
RangeIterator.prototype.next = function(){
  if (this.current > this.range.high)
    throw StopIteration;
  else
    return this.current++;
};

Range에 RangeIterator를 반환하는 메소드를 추가한다. iterator는 이를 위한 특별한 프로퍼티다.

Range.prototype.__iterator__ = function(){
  return new RangeIterator(this);
};

만든 RangeIterator를 써보자:

var range = new Range(3, 5);
for (var i in range)
  print(i); // prints 3, then 4, then 5 in sequence

Generators: a better way to build Iterators

Custom Iterator와 같은 것을 보다 쉽고 간결하게 작성할 수 있다. Generator는 Iterator의 Factory라고 볼 수 있는데 함수에 새로 추가된 yield 키워드를 사용하면 그 함수가 Generator가 된다.

yeild 키워드를 사용하려면 Script Block을 다음과 같이 선언해야 한다:

<script type="application/JavaScript;version=1.7"/>

Generator()를 호출하면 그 함수가 바로 실행되는 것이 아니라 generator-iterator 객체를 반환한다. 그리고 반환된 generator-iterator 객체의 next() 메소드를 호출할 때마다 다음 yield까지 실행한다. 함수가 끝나거나 return되면 StopIteration 예외를 던진다.

간단한 예제를 보자:

function simpleGenerator(){
  yield "first";
  yield "second";
  yield "third";
  for (var i = 0; i < 3; i++)
    yield i;
}

var g = simpleGenerator();
print(g.next()); // prints "first"
print(g.next()); // prints "second"
print(g.next()); // prints "third"
print(g.next()); // prints 0
print(g.next()); // prints 1
print(g.next()); // prints 2
print(g.next()); // StopIteration is thrown

위에서 살펴봤던 RangeIterator의 구현을 Range의 prototype의 iterator 프로퍼트를 바로 구현하여 더 간단하게 구현할 수 있다:

function Range(low, high){
  this.low = low;
  this.high = high;
}
Range.prototype.__iterator__ = function(){
  for (var i = this.low; i <= this.high; i++)
    yield i;
};
var range = new Range(3, 5);
for (var i in range)
  print(i); // prints 3, then 4, then 5 in sequence

이전 두 수의 합을 구하는 피보나치 수열을 Generator로 구현해보자:

function fibonacci(){
  var fn1 = 1;
  var fn2 = 1;
  while (1){
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    yield current;
  }
}

var sequence = fibonacci();
print(sequence.next()); // 1
print(sequence.next()); // 1
print(sequence.next()); // 2
print(sequence.next()); // 3
print(sequence.next()); // 5
print(sequence.next()); // 8
print(sequence.next()); // 13

무한 수열을 구하는 것을 만들었지만 수열의 끝나도록 끝을 명시할 수 있다. Generator는 인자를 받을 수 있기 때문에 fibonacci()에 limit 인자를 추가한다:

function fibonacci(limit){
  var fn1 = 1;
  var fn2 = 1;
  while (1){
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    if (limit && current > limit)
      return;
    yield current;
  }

위에서도 설명했지만 return하면 결국 StopIteration 예외를 던지는 것이다.

Advanced generators

next()함수를 적어도 한번 호출해서 Iteration을 시작하면 send()를 이용해서 특정 시점으로 강제로 지정할 수 있습니다. send(whatever)라고 호출하는 것은 이 전의 yield한 값으로 whatever로 사용하는 next()와 같습니다.

피보나치 Generator에 send()를 사용해보자:

function fibonacci(){
  var fn1 = 1;
  var fn2 = 1;
  while (1){
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    var reset = yield current;
    if (reset){
        fn1 = 1;
        fn2 = 1;
    }
  }
}

var sequence = fibonacci();
print(sequence.next());     // 1
print(sequence.next());     // 1
print(sequence.next());     // 2
print(sequence.next());     // 3
print(sequence.next());     // 5
print(sequence.next());     // 8
print(sequence.next());     // 13
print(sequence.send(true)); // 1
print(sequence.next());     // 1
print(sequence.next());     // 2
print(sequence.next());     // 3

send(undefined)는 next()와 완전히 동일하고 지금 막 만든 Generator에서는(아직 next()를 한번도 호출하지 않은) 엄한 값이 사용되면 TypeError 예외를 던진다.

Generator를 종료할 때에는 close()를 사용한다:

var gen = generator();

function driveGenerator() {
  if (gen.next()) {
    window.setTimeout(driveGenerator, 0);
  } else {
    gen.close();
  }
}

Array comprehensions

Array Comprehension은 다른 객체를 애용해 새로운 Array를 만들 때 사용할 수 있는 새로운 문법이다. 보통 map()과 filter()를 사용하는 코드대신 사용된다.

다음과 같은 코드를:

var numbers = [1, 2, 3, 4];
var doubled = numbers.map(function(i) { return i * 2; });
alert(doubled); // Alerts 2,4,6,8

Array Comprehension으로 바꿀 수 있다:

var numbers = [1, 2, 3, 4];
var doubled = [i * 2 for each (i in numbers)];
alert(doubled); // Alerts 2,4,6,8

filter()의 경우에도:

var numbers = [1, 2, 3, 21, 22, 30];
var evens = numbers.filter(function(i) { return i % 2 == 0; });
alert(evens); // Alerts 2,22,30

Array Comprehension을 사용할 수 있다:

var numbers = [1, 2, 3, 21, 22, 30];
var evens = [i for each (i in numbers) if (i % 2 == 0)];
alert(evens); // Alerts 2,22,30

map()과 filter()가 결합된 코드도 작성할 수 있다:

var numbers = [1, 2, 3, 21, 22, 30];
var doubledEvens = [i * 2 for each (i in numbers) if (i % 2 == 0)];
alert(doubledEvens); // Alerts 4,44,60

Array comprehension을 위한 [] 블럭은 scoping 블럭이다. let 키워드를 사용하여 변수를 정의한 것 처럼 [] 블럭 밖에서는 사용할 수 없다.

Array comprehension을 위해 사용하는 입력 데이터에 Iterator나 Generator도 사용할 수 있다.

String도 Array처럼 취급할 수 있기 때문에 사용할 수 있다:

var str = 'abcdef';
var consonantsOnlyStr = [c for each (c in str) if (!(/[aeiouAEIOU]/).test(c))  ].join(''); // 'bcdf'
var interpolatedZeros = [c+'0' for each (c in str) ].join(''); // 'a0b0c0d0e0f0'

Array comprehension의 결과는 Array이기 때문에 join()으로 다시 합쳤다.

Block scope with let

let으로 선언한 변수의 lifecycle은 해당 블록이다. var는 Global 변수를 선언하거나 함수의 Local 변수를 선언하는 것이다.

예제를 보자:

var a = 5;
var b = 10;
if (a === 5) {
let a = 4; // The scope is inside the if-block
  var b = 1; // The scope is inside the function

  console.log(a);  // 4
  console.log(b);  // 1
}
console.log(a); // 5
console.log(b); // 1

Destructuring assignment

Destructuring assignment는 Array 같은 Collection을 통째로 할당하는 것을 말한다.

Destructuring assignment를 사용하면 임시 변수는 만들지 않고도 swap할 수 있다:

var a = 1;
var b = 3;

[a, b] = [b, a];

다음과 같이 변수를 할당하는 것도 가능하다:

var i = 0, j = 1;
[i, j] = [j, i + j];

리턴값을 여러개 넘길 수 있다:

function f() {
  return [1, 2];
}
var a, b;
[a, b] = f();
document.write ("A is " + a + " B is " + b + "<br>\n");

리턴값중에서 몇개는 무시할 수도 있다.

function f() {
  return [1, 2, 3];
}

var [a, , b] = f();
document.write ("A is " + a + " B is " + b + "<br>\n");

결과를 Array로도 받을 수 있다.

var a = f();
document.write ("A is " + a);

for loop에서도 사용할 수 있다:

let obj = { width: 3, length: 1.5, color: "orange" };

for (let [name, value] in Iterator(obj)) {
  document.write ("Name: " + name + ", Value: " + value + "<br>\n");
}

Array안에 있는 객체의 프로퍼티에 직접 접근할 수도 있다.

var people = [
  {
    name: "Mike Smith",
    family: {
      mother: "Jane Smith",
      father: "Harry Smith",
      sister: "Samantha Smith"
    },
    age: 35
  },
  {
    name: "Tom Jones",
    family: {
      mother: "Norah Jones",
      father: "Richard Jones",
      brother: "Howard Jones"
    },
    age: 25
  }
];

for each (let {name: n, family: { father: f } } in people) {
  document.write ("Name: " + n + ", Father: " + f + "<br>\n");
}

함수 인자에서도 사용할 수 있다:

function userId({id}) {
  return id;
}

function whois({displayName: displayName, fullName: {firstName: name}})
  console.log(displayName + " is " + name);
}

var user = {id: 42, displayName: "jdoe", fullName: {firstName: "John", lastName: "Doe"}};

console.log("userId: " + userId(user));
whois(user);

정규표현식에도 사용할 수 있다:

var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
if (!parsedURL)
  return null;
var [, protocol, fullhost, fullpath] = parsedURL;

JavaScript 1.7 compatibility

브라우저 호환 테이블은 여기