Maps in ES6

본 포스트는 ponyfoo.comES6 Maps in Depth를 번역한 것입니다. ponyfoo.com의 주인장인 Nicolás의 승인하에 올리는 번역글입니다.

Maps

  • 자바스크립트 객체를 이용해서 hash map을 만드는 일반적인 패턴을 대체하기 위해 등장
  • key는 숫자, 문자 이외에도 DOM elements나 function 사용 가능
  • iterable protocol의 한 종류
  • new Map()을 이용해서 map을 생성
  • map.set(key,value), map.get(key)
  • map.has(key)를 사용해서 해당 key가 map에 있는지 확인
  • map.delete(key)를 사용해서 엔트리를 제거
  • for (let [key, value] of map)을 이용해서 map의 엔트리를 순회

    ES6 Maps in Depth

    Before ES6, There Were Hash-Maps

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var registry = {}
    function add (name, meta) {
    registry[name] = meta
    }
    function get (name) {
    return registry[name]
    }
    add('contra', { description: 'Asynchronous flow control' })
    add('dragula', { description: 'Drag and drop' })
    add('woofmark', { description: 'Markdown and WYSIWYG editor' })

자바스크립트는 map을 지원하지 않기 때문에, 위의 소스코드와 같이 문자열을 key로 사용하고, 임의의 값을 value로 사용하게 함으로써 hash map을 구현할 수 있다. 그러나 임시방편으로 생성한 hash map은 몇가지 문제점을 가지고 있다.

  • iterable protocol을 구현하는 것이 복잡하다.
  • Key가 문자열로 한정되어 있다.
  • Security issues where user-provided keys like __proto__, toString, or anything in Object.prototype break expectations and make interaction with these kinds of hash-map data structures more cumbersome 이 문장은 아래의 소스코드를 함께 보면 이해하기 쉬울 것 같다. 위의 소스코드에 이어서 작성한 코드이다.
    1
    2
    3
    4
    5
    6
    registry.toString();
    # "[object Object]" 가 출력된다.
    add('toString', {description:'toString blah blah'});
    registry.toString();
    # VM356:2 Uncaught TypeError: registry.toString is not a function(…) 가 출력된다.

뭔가 자바스크립트 객체가 가지고 있는 기본 기능들을 사용할 수 없게 되버린다. 앞선 문장의 Security issues는 이런 경우를 이야기 하는 것 같다.

ES6 Maps

1
2
3
4
var map = new Map()
map.set('contra', { description: 'Asynchronous flow control' })
map.set('dragula', { description: 'Drag and drop' })
map.set('woofmark', { description: 'Markdown and WYSIWYG editor' })

위와 같이 ES6에서 Mapkey/value 자료 구조로 이루어진다. 이는 hash map보다 훨씬 간단하게 사용 가능하다. 가장 중요한 차이점은 hash map이 문자열만 key로 사용 할 수 있었던 것과 달리, ES6의 Map은 함수, 객체, Date도 Key로 사용 가능하다.

1
2
3
4
var map = new Map()
map.set(new Date(), function today () {})
map.set(() => 'key', { pony: 'foo' })
map.set(Symbol('items'), [1, 2])

[['key', 'value'], ['key', 'value']] 형태의 collection이거나 iterable protocol은 Map으로 생성할 수 있다.

1
2
3
4
5
var map = new Map([
[new Date(), function today () {}],
[() => 'key', { pony: 'foo' }],
[Symbol('items'), [1, 2]]
])

위에서 언급한 [['key', 'value'], ['key', 'value']] 형태의 collection이나 iterable protocol은 Map의 생성자를 통해서 쉽게 map으로 변형할 수 있다. Map의 생성자는 내부적으로 아래와 같이 동작한다. forEach를 이용해서 key/value쌍을 하나씩 map에 할당해주고 있다. (2-dimensional item의 key와 value를 효율적으로 가져오기 위해서 destructuring을 사용했다.) Map constructor를 이용하면 쉽게 key/value를 쉽게 가져올 수 있다.

1
2
3
4
5
6
7
var items = [
[new Date(), function today () {}],
[() => 'key', { pony: 'foo' }],
[Symbol('items'), [1, 2]]
]
var map = new Map()
items.forEach(([key, value]) => map.set(key, value))

spread operator도 사용할 수 있다.

1
2
3
4
5
6
7
var map = new Map()
map.set('p', 'o')
map.set('n', 'y')
map.set('f', 'o')
map.set('o', '!')
console.log([...map])
// <- [['p', 'o'], ['n', 'y'], ['f', 'o'], ['o', '!']]

for of 루프를 사용하여 map을 순회 할 수 있는데, destructuring과 결합하여 map을 간결하게 사용 할 수 있다.

1
2
3
4
5
6
7
8
var map = new Map()
map.set('p', 'o')
map.set('n', 'y')
map.set('f', 'o')
map.set('o', '!')
for (let [key, value] of map) {
console.log(key, value)
}

api를 추가하기 위해서는 사전에 정의된 api를 통해서 명시적으로 추가해야 하지만, unique한 key의 성질을 보존하기 위해서 key는 자동으로 관리된다. 동일한 key에 값을 넣으면 이전의 값을 덮어쓰게 된다.

1
2
3
4
5
6
var map = new Map()
map.set('a', 'a')
map.set('a', 'b')
map.set('a', 'c')
console.log([...map])
// <- [['a', 'c']]

ES6 Map에서 NaNcorner-case가 된다. key로 사용될 때는 동일하게 인식된다.

1
2
3
4
5
6
7
console.log(NaN === NaN)
// <- false
var map = new Map()
map.set(NaN, 'foo')
map.set(NaN, 'bar')
console.log([...map])
// <- [[NaN, 'bar']]

Collection Methods in Map

1
2
3
4
5
6
7
8
9
var map = new Map([[NaN, 1], [Symbol(), 2], ['foo', 'bar']])
console.log(map.has(NaN))
// <- true, NaN은 key로 사용 가능하다
console.log(map.has(Symbol()))
// <- false
console.log(map.has('foo'))
// <- true
console.log(map.has('bar'))
// <- false

Symbol 값은 항상 다르기 때문에 key로는 사용 할 수 없고, value로만 사용해야 한다.

1
2
3
4
5
6
7
8
var map = new Map([[1, 'a']])
console.log(map.has(1))
// <- true
console.log(map.has('1'))
// <- false
// example of key casting
var a = {}
a['1'] === a[1]

map은 key casting을 하지 않는다.

1
2
3
4
5
6
var map = new Map([[1, 2], [3, 4], [5, 6]])
map.clear()
console.log(map.has(1))
// <- false
console.log([...map])
// <- []

clear()는 map을 초기화 시키기 위해서 사용할 수 있다.

1
2
3
4
5
var map = new Map([[1, 2], [3, 4], [5, 6]])
console.log([...map.keys()])
// <- [1, 3, 5]
console.log([...map.values()])
// <- [2, 4, 6]

keys()values()는 각각 map의 모든 key와 value를 반환한다. 결과의 형태는 Iterator object 이다.

1
2
3
4
5
6
7
8
9
var map = new Map([[1, 2], [3, 4], [5, 6]])
console.log(map.size)
// <- 3
map.delete(3)
console.log(map.size)
// <- 2
map.clear()
console.log(map.size)
// <- 0

delete()를 통해서 map의 요소를 삭제할 수 있다.

1
2
3
4
5
var map = new Map([[NaN, 1], [Symbol(), 2], ['foo', 'bar']])
map.forEach((value, key) => console.log(key, value))
// <- NaN 1
// <- Symbol() 2
// <- 'foo' 'bar'

위 소스코드는 for of 와 동일한 동작을 한다.

References