New collections in JS: Map, Set, WeakMap, WeakSet

Piotr Kowalski @piecioshka

Speaker

Piotr Kowalski

"New collections in JS: Map, Set, WeakMap, WeakSet"

2016-09-14

@piecioshka

2nd       birthday🎂

Who am I❓

Piotr Kowalski

Trainer

Trainer

Kierownik Działu Aplikacji Webowych

Cyfrowy Polsat, Warsaw

JavaScript Ninja. Mac lover. Open source fan. Blogger.
Organizer WarsawJS. Author of several libs in @npm

"Kto chce szuka sposobu, kto nie chce szuka powodu."

Can you explain what happens here⁉️

            
                let MAP = {}
                let foo = { name: 'foo' }
                let bar = { name: 'bar' }

                MAP[foo] = "Hello"
                MAP[bar] = "World"

                console.log(MAP[foo]) // ???
            
        

My interview question.

I ❤ Map

            
                let MAP = new Map()
                let foo = { name: 'foo' }
                let bar = { name: 'bar' }

                MAP.set(foo, "Hello")
                MAP.set(bar, "World")

                console.log(MAP.get(foo)) // ???
            
        

What collections
do we have⁉️

Ignore this list of native collections

SIMD: Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array

DOM: DOMStringList, DOMStringMap, DOMTokenList, HTMLCollection, HTMLAllCollection, HTMLFormControlsCollection, HTMLOptionsCollection, NodeList, RadioNodeList, SVGAnimatedLengthList, SVGAnimatedNumberList, SVGAnimatedTransformList, SVGLengthList, SVGNumberList, SVGPointList, SVGStringList, SVGTransformList

Misc: ArrayBuffer AudioTrackList, CSSRuleList, ClientRectList, DataTransferItemList, FileList, MediaKeys, MediaList, MediaQueryList, MimeTypeArray, PluginArray, SourceBufferList, SpeechGrammarList, StylePropertyMap, StyleSheetList, TextTrackCueList, TextTrackList, TouchList, TrackDefaultList, VTTRegionList, VideoTrackList

Today's heros❗

Object
(well known)

Array
(well known)

Map

Set

WeakMap

WeakSet

Who of you uses Map or Set every day⁉️

Desktop browsers support

... Map Set WeakMap WeakSet
Chrome 38 38 36 2014-07 36
Firefox 13 13 6 34
IE 11 11 11
x
Opera 25 25 23 23
Safari 7.1 7.1 7.1 9

Which collection is the best⁉️

None.
Depends on your needs

Map

The Map object is a simple key/value map. Any value (both objects and primitive values) may be used as either a key or a value. MDN @ 2016

            
                // #1: API: Hashmap
                // --------------------------------------------------
                let map = new Map()

                map.set('foo', 'bar')
                // instead of: object[key] = value

                map.size // 1
                // instead of: array.length

                map.get('foo') // 'bar'
                 // instead of: object[key]
            
        
            
                // #2: API
                // --------------------------------------------------

                // Remove pair key/value (by reference or primitive)
                map.delete(key) // instead of: array.splice(0, 1)

                // Clear collection
                map.clear() // instead of: array.length = 0
            
        
            
                // #3: API
                // --------------------------------------------------

                // Check if key is used in collection
                map.has(key)

                // instead of:
                object.hasOwnProperty(key) // or: key in object
                array.indexOf(value) !== -1
                array.includes(value) // ES2016 (a.k.a. ES7) way
            
        
            
                // #4: API: Magic
                // --------------------------------------------------
                // Get pair key/value form collection {MapIterator}
                map.entries() // similar to: Object.entries(object)

                // Get keys from collection {MapIterator}
                map.keys() // similar to: Object.keys(object)

                // Get values from collection {MapIterator}
                map.values() // similar to: Object.values(object)
            
        
            
                // #5: API: Simple iteration
                // --------------------------------------------------

                // Iteration through collection
                map.forEach((value, key) => /* ... */)
                // the same in arrays

                // New loop `for..of` (ES2015)
                for (let [key, value] of map.entries()) {
                    map.delete(key) // true
                }
            
        
            
                // #6: API: WTF?
                // --------------------------------------------------
                let map = new Map()
                map.set() // Map { undefined => undefined }
                map.size // 1

                // In arrays
                let array = []
                array.push()
                array.length // 0
            
        
            
                // #7: API: "Holy" couple key/value
                // --------------------------------------------------
                let map = new Map()
                let rand = Math.random()

                map.set([1, 2, 3], Math.random())
                map.set([1, 2, 3], Math.random())
                map.set([1, 2, 3], rand)
                map.set([1, 2, 3], rand)

                map.size // 4
            
        
            
                // #8: API: .. but unique key!
                // --------------------------------------------------
                let map = new Map()
                let rand = Math.random()

                map.set('item', Math.random())
                map.set('item', rand) // overwrite previous

                map.size // 1

                map // Map {"item" => 0.199...}
            
        
            
                // #9: API: Adding without coercing
                // --------------------------------------------------

                let map = new Map()

                map.set(5, 'foo')
                map.set("5", 'bar')

                console.log(map.size) // 2
            
        
            
                // #1: Example: Array as key
                // --------------------------------------------------
                let map = new Map()
                map.set(['win8', 'moz'], 'a').set(['xp', 'chrome'], 'b')

                function check(collection, ...browsers) {
                    for (let [key, value] of collection)
                        // "xp,firefox" === "win8,moz"
                        if (String(browsers) === String(key))
                            return value
                }
                check(map, 'xp', 'chrome') // 'b'
            
        
            
                // #2: Example: Async iteration
                // --------------------------------------------------
                map.set({ foo: 1 }, 'a').set({ foo: 2 }, 'b')
                let iterator = map.values() // {MapIterator}
                let interval = setInterval(() => {
                    let item = iterator.next()
                    if (item.done) {
                        clearInterval(interval)
                        return
                    }
                    console.log(item.value) // 'a' => [ONE SECOND] => 'b'
                }, 1000)
            
        
            
                // #3: Example: Subclassing of Map
                // --------------------------------------------------
                class MultiMap extends Map {
                    set(...args) {
                        args.map(([key, value]) => super.set(key, value))
                        return this
                    }
                }
                let map = new MultiMap()
                map.set(['key', 'value'], ['key2', 'value2'])
                // Map {"key" => "value", "key2" => "value2"}
            
        

So, why Map is cool⁉️

Set

The Set object lets you store unique values of any type, whether primitive values or object references. MDN @ 2016

            
                // #2: API
                // --------------------------------------------------
                let set = new Set([1, 2, 3, 4])

                set.add(value) // instead of: array.push(value)
                // NOT: set.set() // ??? undefined

                // Removing is by value (or reference), not index.
                set.delete(3) // true
                set.clear() // ...and we have empty Set
                set.has(value) // Check by reference
                set.get(key) // TypeError: set.get is not a function
            
        
            
                // #3: API: Magic
                // --------------------------------------------------
                // Get values from collection {SetIterator}
                set.entries()
                set.keys()
                set.values()

                // hmmm...
                // set.values() and set.keys() returns same Iterator
            
        
            
                // #6: API: Simple iteration
                // --------------------------------------------------
                let set = new Set([1, 1, 2])

                set.forEach((item) => {
                    console.log(item) // 1, 2
                })
            
        
            
                // #1: Example: AssetsLoader: loadImage helper
                // --------------------------------------------------
                function loadImage(path /* string */) {
                    return new Promise((resolve, reject) => {
                        let image = new Image()
                        let on = image.addEventListener

                        on('load', () => resolve(image))
                        on('error', () => reject(image))
                        image.src = path
                    })
                }
            
        
            
                // #1: Example: AssetsLoader: test case
                // --------------------------------------------------
                let al = new AssetsLoader()
                al.addImage('../assets/images/plants.jpg')
                al.addImage('../assets/images/sky.jpg')
                al.loadImages()
                    .catch((error) => {
                        console.error(error)
                    })
                    .then((images) => {
                        console.info('Loaded successful', images)
                    })
            
        
            
                // #1: Example: AssetsLoader v1
                // --------------------------------------------------
                class AssetsLoader {
                    constructor() {
                        this._images = new Set()
                    }
                    addImage(path /* string */) {
                        return this._images.add(loadImage(path))
                    }
                    loadImages() {
                        return Promise.all(this._images)
                    }
                }
            
        
            
                // #2: Example: AssetsLoader v2: Subclassing Set
                // --------------------------------------------------
                class AssetsLoader {
                    constructor() {
                        this._images = new Images()
                    }
                    addImage(path /* string */) {
                        return this._images.add(path)
                    }
                    loadImages() {
                        return this._images.load()
                    }
                }
            
        
            
                // #2: Example: AssetsLoader v2: Subclassing Set
                // --------------------------------------------------
                class Images extends Set {
                    add(path) {
                        return super.add(loadImage(path))
                    }
                    load() {
                        return Promise.all(this.keys())
                    }
                }
            
        

Which solutions is better⁉️

            
                // #3: Example: Remove duplicated item (Array)
                // --------------------------------------------------
                let array = [1, 2, 3, 1, 3]

                // Constructor expects another collection
                [...new Set(array)] // [1, 2, 3]
            
        
            
                // #3: Example: Remove duplicated items (Map)
                // --------------------------------------------------
                let map = new Map()
                map.set({ foo: 1 }, 'a').set({ foo: 2 }, 'b')

                [...new Set(map.keys())]
                // [{ foo: 1 }, { foo: 2 }]

                [...new Set(map.values())]
                // ['a', 'b']
            
        

So, why Set is cool⁉️

WeakMap

The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. The keys must be objects and the values can be arbitrary values. MDN @ 2016

            
                // #1: API: Simple API
                // --------------------------------------------------
                let weakMap = new WeakMap()
                let key = { foo: 1 }

                weakMap.set([1], 5) // WeakMap { [1] => 5 }
                weakMap.set(key, 6) // WeakMap { [1] => 5, {foo: 1} => 6 }

                weakMap.get(key) // 6
                weakMap.has(key) // true
                weakMap.delete(key) // true
            
        
            
                // #2: API: "Holy" couple key/value
                // --------------------------------------------------
                let weakMap = new WeakMap()

                weakMap.set([1, 2], Math.random())
                weakMap.set([1, 2], Math.random())
                weakMap.set([1, 2], Math.random())

                weakMap.size // ??? undefined

                weakMap // WeakMap {[1, 2] => 0.609..., [1, 2] => 0.268..., [1, 2] => 0.183...}
            
        

Why in WeakMap keys can ONLY be objects⁉️

            
                // #3: API: How it works?
                // --------------------------------------------------
                let users = [{ name: 'Peter' }, { name: 'Kate' }]
                let weakMap = new WeakMap()

                weakMap.set(users[0], 'some text 1')
                weakMap.set(users[1], 'some text 2')

                console.log(weakMap.get(users[0])) // 'some text 1'
                users.splice(0, 1)
                console.log(weakMap.get(users[0])) // 'some text 2'
            
        

☟︎ 2016-09-12 11:38:43.753 ☟︎

☝︎ 2016-09-12 11:38:59.656 ☝︎

More examples of using WeakMap

So, why WeakMap is cool⁉️

WeakSet

The WeakSet object lets you store weakly held objects in a collection. MDN @ 2016

            
                // #1: API: Simple API
                // --------------------------------------------------
                let weakSet = new WeakSet()

                // Adding ONLY objects
                weakSet.add([1])
                weakSet.add(1)
                // TypeError: Invalid value used in weak set

                // ... by reference (not value)
                weakSet.has([1]) // false
                weakSet.delete([1]) // false
            
        
            
                // #2: API: How it works?
                // --------------------------------------------------
                let users = [{ name: 'Peter' }, { name: 'Kate' }]
                let weakSet = new WeakSet()

                weakSet.add(users[0])
                weakSet.add(users[1])

                console.log(weakSet) // WeakSet {Object {name: "Kate"}, Object {name: "Peter"}}
                users.splice(0, 1)
                console.log(weakSet) // WeakSet {Object {name: "Kate"}, Object {name: "Peter"}}
            
        

☟︎ 2016-09-12 15:13:09.329 ☟︎

☝︎ 2016-09-12 15:13:17.837 ☝︎

More examples of using WeakSet

So, why WeakSet is cool⁉️

Who noticed similarities⁉️

Similarities

Differences

            
                // Symbol(Symbol.toStringTag)
                String(new Map) === "[object Map]"
                String(new Set) === "[object Set]"
                String(new WeakMap) === "[object WeakMap]"
                String(new WeakSet) === "[object WeakSet]"
            
        

Benchmark: Object is fastest

Type Operations / seconds
Object 2,590,044 ops/sec ±6.32% (72 runs sampled)
Array 675,882 ops/sec ±13.49% (65 runs sampled)
Map 152,399 ops/sec ±9.67% (63 runs sampled)
Set 184,469 ops/sec ±6.27% (78 runs sampled)
WeakMap 199,211 ops/sec ±1.93% (82 runs sampled)
WeakSet 202,026 ops/sec ±1.91% (82 runs sampled)

Memory allocation: Array is lightweight

Type usedJSHeapSize
Object 7.977 KB
Array 5.281 KB
Map 28.625 KB
Set 13.219 KB
WeakMap 15.844 KB
WeakSet 9.484 KB

Sources

Sources (my projects)

Thanks 👏 Next meetup: 12-10-2016

Tips & Tricks

            
                window.name = { foo: 1 }
                console.log(window.name) // [object Object]
                // what happen here?
                // [immutable type]

                const object = {}
                object.a = 1
                console.log(object.a) // 1
                // why we can do this?
                // [immutable reference]
            
        
            
                typeof null === 'object' // true
                // why?
                // [empty reference to object]

                Number.isFinite('1.2') // false | ES6
                isFinite('1.2') // true | ES3
                // OMG
                // [more robust]

                typeof class {} === "function" // true
                // why?
                // [syntactic sugar]
            
        

Angular2 fans?

            
                Object.observe(object, (changes) => {
                    console.log(changes)
                })

                // but today...
                Object.observe // undefined

                // what can I do? use Proxy!
            
        
            
                let object = {}
                object = new Proxy(object, {
                    set: (o, prop, value) => {
                        console.warn(`${prop} is set to ${value}`)
                        o[prop] = value
                    },
                    get: (o, prop) => {
                        console.warn(`${prop} is read`)
                        return o[prop]
                    }
                })
            
        
Example of Proxy

Thanks 👏 Do you remember when will be next meetup⁉️