Objects

Objects are the most important concept in JavaScript. But wait, didn't we already say that functions are the most important concept in JavaScript?! Yes we did. But functions are actually objects in JavaScript (and so are arrays)! In fact, almost everything in JavaScript is an object.

Like arrays, objects can store a wide variety of data. We can imagine that an array is like a locker room where each locker has a number. When you look into the locker, there could be anything in there. But with objects, each locker can have a string label. Wouldn't it be better if your locker simply had your name instead of a number you had to memorize? The label is called a key and the locker content is called the value.

To create an object you use {}.

const student1 = {
first: 'Harry',
last: 'Potter',
age: 25
}
const student2 = {
first: 'Ron',
last: 'Weasley',
age: 24
}
const total = student1.age + student2.age // total is 49

In the example above, we created 2 student objects:

  • The first object has 3 keys, first, last, and age, and 3 corresponding values, "Harry", "Potter", and 25.
  • The second object has the same keys as student1 and corresponding values "Ron", "Weasley", and 24.

Then, we used total to show you how to access a value associated with a key. This notation might look familiar; it turns out that by this point, you have used many objects already.

console.log('hello')
/*
console is an object created by an engineer for you to use.
console has a key called log, and the value at that key is a function.
console.log("hello") runs the function that
is stored in the console object under the key "log".
*/

Example implementation - using console2 as the object

const console2 = {
log: data => {
alert(data)
}
}
// Now, you can run the function by calling console2.log
console2.log('hello')
console2.log('World')

Examples of other objects

  • document.querySelector
    • document is an object created by an engineer for you to use.
    • document has a key called querySelector, and the value of that key is a function.
    • document.querySelector('.submit') runs the function that is stored in the document object under the key querySelector.
  • localStorage.getItem
    • localStorage is an object created by an engineer for you to use.
    • localStorage has a key called getItem, and the value of that key is a function.
    • localStorage.getItem('mydata') runs the function that is stored in the localStorage object under the key getItem.

Exercise

  1. Write an object with 3 keys (make up keys and values).
Answer
const obj = {
name: 'joe',
age: 940,
isStudent: false
}
  1. Friend Age Notebook (part 2): Modify your friend age notebook to use an array of objects instead of a 2D array.

    If you haven’t cleared your localStorage yet, you’ll need to do it now or use a different key to store your data for part 2. Any data already in there won’t be compatible with the new object model and will show up as ‘undefined.’

Hint
buttonElement.onclick = () => {
friends.unshift({
name: nameInput.value,
age: ageInput.value
})
}

Variables

You can also access the values of an object using the [] notation that you're familiar with from arrays.

  • document.querySelector('.button')
    • can be written as document['querySelector']('.button')
  • console.log('hello')
    • can be written as console['log']('hello')

To get a value in an object, the code is very similar to getting a value in an array. We pass in a string (key) instead of an index number.

const student = {
first: 'Harry',
last: 'Potter',
age: 25
}
const firstName = student['first'] // firstName has the value 'Harry'
const student2 = student
student2['name'] = 'last'
// what is student2?
// what is student?
// The benefit of using [] for getting values is that you can use variables as keys.
student2[student2.name] = 'Weasley'
// what is student2?
// what is student?

The benefit of using [] for getting values is that you can use variables as keys.

Answer
const student = {
first: 'Harry',
last: 'Potter',
age: 25
}
const firstName = student['first'] // firstName has the value 'Harry'
const student2 = student
student2['name'] = 'last'
/*
student2 is the same as student, which is:
{
name: 'last',
first: 'Harry',
last: 'Potter',
age: 25
}
*/
student2[student2.name] = 'Weasley'
/*
Notice how student2.name is a variable that contains the string 'last'
Therefore, it evaluates to
student2.last = 'Weasley'
student2 is the same as student, which is:
{
name: 'last',
first: 'Harry',
last: 'Weasley',
age: 25
}
*/

Non-Primitive

Like arrays, objects are non-primitive.

const star = { name: 'Tarzan' }
const star2 = star
star2['friend'] = star
star2['friend']['lover'] = 'Jane'
// what is star2?
// what is star?
star['name'] = 'Sarah'
// what is star2?
// what is star?
const allStars = [star, star2]
allStars[0]['lover'] = allStars[1]['name']
// what is star2?
// what is star?
// what is allStars?
Answer
// Section 1:
/*
star is the same as star2, which is:
{
name: 'Tarzan',
friend: Object,
lover: "Jane"
}
*/
// Section 2:
/*
star is the same as star2, which is:
{
name: 'Sarah',
friend: Object,
lover: "Jane"
}
*/
// section 3
/*
star is the same as star2, which is:
{
name: 'Sarah',
friend: Object,
lover: "Sarah"
}
*/
/*
allStars is:
[
{ name: 'Sarah', friend: [Circular], lover: 'Sarah'},
{ name: 'Sarah', friend: [Circular], lover: 'Sarah'}
]
*/

Values can be anything, including a function!

const snacks = {
nutella: () => {
return 200
},
pixyStix: () => {
return 9
},
lays: () => {
return 135
}
}
let calories = snacks['nutella']() // what is calories?
calories = snacks['pixyStix']() // what is calories?
calories = snacks['lays']() // what is calories?
Answer
// calories -> 200
// calories -> 9
// calories -> 135

Here are some more examples with functions for you to work through. In the first one, we introduce the building blocks of a concept called promises that we'll get to later in this chapter. We'll learn a function called fetch() that returns a promise object. The promise object has a then() property, which returns a promise object so you can keep chaining more then() properties together until what you want to accomplish is completed: fetch().then().then().then() etc. Here we introduce this kind of behavior where an object's function returns the object itself.

// Problem 1:
const magician = {
perform: () => {
return magician
}
}
const houdini = magician.perform().perform() // what is houdini?
const same = magician === houdini // what is same?
/* Problem 2: Create a prepareStage object with a then property so the code
below will not cause an error.
*/
prepareStage.then().then().then()
/* Problem 3: Create the prepareStage object with a then property that
console.logs each input: Should log Squirtle, Wartortle, Blastoise
*/
prepareStage.then('Squirtle').then('Wartortle').then('Blastoise')
/* Problem 4: Create the prepareStage object with a then property that
executes its function argument: Should log Abracadabra! 3 times
*/
const performMagic = () => {
console.log('Abracadabra!')
}
prepareStage.then(performMagic).then(performMagic).then(performMagic)
Answer
/* Problem 1: houdini is:
{
perform: () => {
return magician
}
}
same is true
*/
/* Problem 2:
const prepareStage = {
then: () => {
return prepareStage
}
}
*/
/* Problem 3:
const prepareStage = {
then: (input) => {
console.log(input)
return prepareStage
}
}
*/
/* Problem 4:
const prepareStage = {
then: (input) => {
input()
return prepareStage
}
}
*/

Exercises

  1. Write a function called addKV that takes in an object, 2 strings (key and value), and adds a new key and value to an object.
Answer
  1. Tests

    describe('addKV function', () => {
    it('should add a key and value to an object', () => {
    const marvel = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    fn.addKV(marvel, 'antman', 'funny')
    expect(marvel.antman).toEqual('funny')
    })
    it('should add a key and value to an object', () => {
    const marvel = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong',
    antman: 'funny'
    }
    fn.addKV(marvel, 'wonderwoman', 'smart')
    expect(marvel.wonderwoman).toEqual('smart')
    })
    it('should add a key and value to an object', () => {
    const marvel = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong',
    antman: 'funny',
    wonderwoman: 'smart'
    }
    const b = ['leader', 'honest']
    fn.addKV(marvel, 'captainamerica', ['leader', 'honest'])
    expect(marvel.captainamerica).toEqual(b)
    })
    })
  2. Shape

    const addKV = (obj, key, val) => {
    obj[key] = val
    }
  3. Explanation

    • You are given an object obj, a string key and a string val.
    • Assign val to obj at a given key.
  4. Code

    const addKV = (obj, key, val) => {
    obj[key] = val
    }
Debrief

Did you try a.b = c? Remember that using a . accesses exactly the key that comes after the .. To look inside a variable and access that key, you need [].

  1. Write a function called filterNonKeys that filters an array to only include strings that are also keys in a given object.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const avengers = ['ironman', 'strange', 'thor', 'spiderman', 'hulk']
    const result = filterNonKeys(avengers, info)
    // result is ["ironman", "spiderman", "hulk"]
Hint

To check if a key exists in an object, just try to access it—if it doesn’t exist you’ll get undefined, which is falsey.

Answer
  1. Tests

    describe('filterNonKeys function', () => {
    const avengers = ['ironman', 'strange', 'thor', 'spiderman', 'hulk']
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    it('should return an empty array when filtering on an empty object', () => {
    const result = fn.filterNonKeys(avengers, {})
    expect(result).toEqual([])
    })
    it('should return an empty array when starting with an empty array', () => {
    const result = fn.filterNonKeys([], info)
    expect(result).toEqual([])
    })
    it('should return an empty array if no matches are found', () => {
    const b = ['batman', 'superman', 'flash']
    const result = fn.filterNonKeys(b, info)
    expect(result).toEqual([])
    })
    })
  2. Shape

    const filterNonKeys = (arr, obj) => {}
  3. Explanation

    • You are given an array arr and an object obj.
    • Use array helper method filter to filter out the matching keys from the obj.
  4. Code

    const filterNonKeys = (arr, obj) => {
    return arr.filter(e => {
    return obj[e]
    })
    }
  1. Write a function called addDescriptions that adds a description key to each object in an array. The description should go with the name that matches the key in the input object.

    const characters = [
    { name: 'ironman' },
    { name: 'spiderman' },
    { name: 'hulk' }
    ]
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    addDescriptions(characters, info)
    /* characters is changed to:
    [
    {name: "ironman", description: "arrogant"},
    {name: "spiderman", description: "naive"},
    {name:"hulk", description: "strong"}
    ]
    */
Answer
  1. Tests

    describe('addDescriptions function', () => {
    it('should add 3 descriptions to corresponding names', () => {
    const characters = [
    { name: 'ironman' },
    { name: 'spiderman' },
    { name: 'hulk' }
    ]
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    fn.addDescriptions(characters, info)
    expect(characters).toEqual([
    { name: 'ironman', description: 'arrogant' },
    { name: 'spiderman', description: 'naive' },
    { name: 'hulk', description: 'strong' }
    ])
    })
    it('should not add descriptions to objects without names', () => {
    const characters = [
    { tonyStark: 'ironman' },
    { peterParker: 'spiderman' },
    { name: 'hulk' }
    ]
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    fn.addDescriptions(characters, info)
    expect(characters).toEqual([
    { tonyStark: 'ironman' },
    { peterParker: 'spiderman' },
    { name: 'hulk', description: 'strong' }
    ])
    })
    it('should ignore unmatched keys', () => {
    const characters = [
    { name: 'ironman' },
    { name: 'rocket' },
    { name: 'drax' }
    ]
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    fn.addDescriptions(characters, info)
    expect(characters).toEqual([
    { name: 'ironman', description: 'arrogant' },
    { name: 'rocket' },
    { name: 'drax' }
    ])
    })
    })
  2. Shape

    const addDescription = (chars, obj) => {}
  3. Explanation

    • You are given an array of objects chars and an object obj.
    • Go through each element of chars and assign "description" key.
    • The value of description is the value of the obj at a given name.
  4. Code

    const addDescriptions = (chars, obj) => {
    chars.forEach(e => {
    e.description = obj[e.name]
    })
    return chars
    }
  1. Write a function called countOccurrences that returns an object that counts how many times each item occurs in an array.

    const abc = ['abc', 'a', 'abc', 'b', 'abc', 'a', 'b', 'c', 'abc']
    const result = countOccurrences(abc)
    // should return: {abc: 4, a: 2, b: 2, c: 1}
Answer
  1. Tests

    describe('countOccurrences function', () => {
    it('should count occurrences of strings', () => {
    const abc = ['abc', 'a', 'abc', 'b', 'abc', 'a', 'b', 'c', 'abc']
    const result = fn.countOccurrences(abc)
    expect(result).toEqual({ abc: 4, a: 2, b: 2, c: 1 })
    })
    it('should count occurrences of numbers', () => {
    const nums = [0, 3, 3, 1, 0, 0, 3, 0, 0, 2]
    const result = fn.countOccurrences(nums)
    expect(result).toEqual({ 0: 5, 3: 3, 1: 1, 2: 1 })
    })
    it('should return an empty object for an empty array', () => {
    const result = fn.countOccurrences([])
    expect(result).toEqual({})
    })
    })
  2. Shape

    const count0occurences = arr => {}
  3. Explanation

    • You are given an array arr.
    • Use array helper method reduce
  4. Code

    const countOccurrences = arr => {
    return arr.reduce((acc, e) => {
    acc[e] = (acc[e] || 0) + 1
    return acc
    }, {})
    }
Debrief

We could have also done this using forEach: initialize an empty object outside, then call arr.forEach and pass in a function that adds to the object, then return it after the forEach call. But reduce is an extremely versatile function, and can simplify this task down to one function call.

Note that we added 1 to acc[e] || 0 in case acc[e] isn't defined yet--adding 1 to undefined results in NaN.

Object Helpers

The Object type comes with several "helper" functions that let you find and modify objects' keys and values:

Object.keys

This function takes in an object and returns an array of keys in the object.

const info = {
ironman: 'arrogant',
spiderman: 'naive',
hulk: 'strong'
}
const result = Object.keys(info)
// result is ["ironman", "spiderman", "hulk"]

Why isn't it info.keys()?

Most of the helper functions for Object are static functions rather than the prototype functions we learned about for arrays. One reason is that objects provide more opportunities for key collisions—for example your object could have a key called keys or values.

Object.values

This function takes in an object and returns an array of values in the object.

const info = {
ironman: 'arrogant',
spiderman: 'naive',
hulk: 'strong'
}
const result = Object.values(info)
// result is ["arrogant", "naive", "strong"]

Object.entries

This function takes in an object and returns an array. Each element of the array is an array with 2 elements, a key and its corresponding value in the object.

const info = {
ironman: 'arrogant',
spiderman: 'naive',
hulk: 'strong'
}
const result = Object.entries(info)
// result is [["ironman", "arrogant"], ["spiderman", "naive"], ["hulk", "strong"]]

Object.prototype.hasOwnProperty

Notice how this method is attached to the prototype of Object. Really, all we need to understand about this is that every Object instance (which is basically everything in JavaScript, since everything is an Object) has this method available to them.

The hasOwnProperty method allows us to check if a particular property exists ONLY on the object in context and not down the prototype chain of the object. Go here to read more on prototypes.

Let's look at an example to see why we may use this method.

const myObj = {
name: 'test',
age: 100
}
myObj.hasOwnProperty('name') // true
!!myObj['name'] // true
myObj.hasOwnProperty('age') // true
!!myObj['age'] // true
myObj.hasOwnProperty('height') // false
!!myObj['height'] // false
myObj.hasOwnProperty('weight') // false
!!myObj['weight'] // false
myObj.hasOwnProperty('toString') // FALSE
!!myObj['toString'] // TRUE

In the example above we are comparing hasOwnProperty vs just trying to use the [] notation to find a key inside of an Object. Putting !! in front just allows us to convert the value into a boolean to compare the returns values.

Now, the question is why does myObj.hasOwnProperty('toString') return false but !!myObj['toString'] returns true? When you use [] notation, you are looking for any property that exists on the Object OR down that Object's prototype chain! Using .hasOwnProperty() ONLY looks at the Object's properties.

When you start working with libraries and using Objects that have been defined somewhere else for you, you have no idea what is defined on the prototype chains. You may be able to get by with just checking properties using [] notation, but it's always better to be safe than sorry.

It is best practice to use .hasOwnProperty() in cases where you ONLY want to see if a property exists on the Object itself and not down the prototype chain.

delete

This function deletes the key (and its value) from an object. Example:

const info = {
ironman: 'arrogant',
spiderman: 'naive',
hulk: 'strong',
thanos: 'powerful'
}
delete info.thanos
// after delete, info becomes:
// {ironman: "arrogant", spiderman: "naive", hulk: "strong"}

Shouldn't it be Object.delete(info)?

Just when you were getting used to everything being a static function of the Object type, we switched it up by introducing delete! delete is actually an operator, and it can work on array elements and some variables, too—but in practice, you'll mostly see it used on objects.

Exercises:

  1. Write a function called longestString that finds the longest value string in an object.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = longestString(info) // result should be "arrogant"
Answer
  1. Tests

    describe('longestString function', () => {
    it('should find the longest string from the beginning of an object', () => {
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    expect(fn.longestString(info)).toEqual('arrogant')
    })
    it('should find the longest string from the end of an object', () => {
    const leaders = {
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga',
    saffron: 'Sabrina'
    }
    expect(fn.longestString(leaders)).toEqual('Sabrina')
    })
    it('should return the empty string for an empty object', () => {
    expect(fn.longestString({})).toEqual('')
    })
    })
  2. Shape

    const longestString = obj => {}
  3. Explanation

    • You are given an object obj.
    • Use array helper method reduce to go through each string value and find the longest string in the obj.
    • Starts with an empty string.
  4. Code

    const longestString = obj => {
    return Object.values(obj).reduce((str, e) => {
    if (e.length > str.length) return e
    return str
    }, '')
    }
Debrief

This was easy because we were just working with values, and could "throw away" the object itself. The next one will require you to make use of the keys array while still referring back to the entire object, and there are a couple of different ways to do it. See if you can figure one out!

  1. Write a function called keyOfLongestString that finds the longest value string but returns its key.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = keyOfLongestString(info) // result should be "ironman"
    // because "arrogant" is longer than "naive" and "strong"
    const info2 = {
    a: 'xxxxxx',
    bc: 'xx',
    abc: 'xxx'
    }
    const result2 = keyOfLongestString(info2) // result2 should be "a"
    // "xxxxxx" is longer than "xx" and "xxx"
Answer
  1. Tests

    describe('keyOfLongestString function', () => {
    it('should find key of longest string in the beginning of an object', () => {
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    expect(fn.keyOfLongestString(info)).toEqual('ironman')
    })
    it('should find key of longest string at the end of an object', () => {
    const leaders = {
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga',
    saffron: 'Sabrina'
    }
    expect(fn.keyOfLongestString(leaders)).toEqual('saffron')
    })
    it('should return undefined (no key) for an empty object', () => {
    expect(fn.keyOfLongestString({})).toEqual(undefined)
    })
    })
  2. Shape

    const keyOfLongestString = obj => {}
  3. Explanation

    • You are given an object obj.
    • We need a local variable(let's call it allKeys) and it will store all the keys from the obj.
    • When the length of allKeys reaches 0, return undefined.
    • We need another local variable(let's call it longestKey. It finds the longest key by using array method reduce to compare the length of the values and return the key with the longest value.
      • It starts with the first element of allKeys.
  4. Code

    const keyOfLongestString = obj => {
    const allKeys = Object.keys(obj)
    if (allKeys.length === 0) return undefined
    const longestKey = allKeys.reduce((lKey, key) => {
    if (obj[key].length > obj[lKey].length) {
    return key
    }
    return lKey
    }, allKeys[0])
    return longestKey
    }
Debrief

By now whenever you see an array (or something that should be turned into an array) and need to look at all elements and boil them down to one answer—whether it’s a length; a sum; another array; or an answer like the longest, greatest, or shortest, or smallest—you should be thinking of reduce. It’s quite a useful function!

Like we said in the last debrief, there are several other ways you could have written this function, such as reducing as Object.entries(obj).

  1. Use your keyOfLongestString function to write a function called removeLongestString.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    removeLongestString(info)
    // info is changed to:
    // {spiderman: "naive", hulk: "strong"}
Answer
  1. Tests

    describe('removeLongestString function', () => {
    it('should remove the longest string in the beginning of an object', () => {
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    fn.removeLongestString(info)
    expect(info).toEqual({ spiderman: 'naive', hulk: 'strong' })
    })
    it('should remove the longest string at the end of an object', () => {
    const leaders = {
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga',
    saffron: 'Sabrina'
    }
    fn.removeLongestString(leaders)
    expect(leaders).toEqual({
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga'
    })
    })
    it('should work on an empty object', () => {
    const imEmpty = {}
    fn.removeLongestString(imEmpty)
    expect(imEmpty).toEqual({})
    })
    })
  2. Shape

    const removeLongestString = obj => {}
  3. Explanation

    • You are given an object obj.
    • We need an additional variable(let's call it longestKey). It stores the result of passing in the obj as an argument.
    • delete the object value when the obj is at key longestKey.
  4. Code

    const removeLongestString = obj => {
    const longestKey = keyOfLongestString(obj)
    delete obj[longestKey]
    }
  1. Write a function called commas that returns a string of all of an object's values separated by commas.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = commas(info)
    // result is 'arrogant, native, strong'
Answer
  1. Tests

    describe('commas function', () => {
    it('should separate three items', () => {
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    expect(fn.commas(info)).toEqual('arrogant, naive, strong')
    })
    it('should put no commas if only one element', () => {
    expect(fn.commas(['funny'])).toEqual('funny')
    })
    it('should return an empty string if no elements', () => {
    expect(fn.commas([])).toEqual('')
    })
    })
  2. Shape

    const commas = obj => {}
  3. Explanation

    • You are given an object obj.
    • You need to get all the values of the obj. Attach reduce method to it.
    • Inside the reduce method,
      • create a local variable(let's say firstComma) which starts at empty string.
      • When index i reaches 0, firstComma equals empty string.
      • return acc + firstComma + e (In this case accumulator is result)
      • starts with an empty string
  4. Code

    const commas = obj => {
    return Object.values(obj).reduce((result = '', e, i) => {
    let firstComma = ', '
    if (i === 0) {
    firstComma = ''
    }
    return result + firstComma + e
    }, '')
    }

Feeling good about manipulating objects? In the last exercise you'll start building some skills that you’ll need to make use of objects in an HTML environment.

  1. Write a function called headers that joins all of an object's keys inside <h1> tags.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = headers(info)
    // result is '<h1>ironman</h1><h1>spiderman</h1><h1>hulk</h1>'
Answer
  1. Tests

    describe('headers function (part 1)', () => {
    it('should create h1s for 3 items', () => {
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    expect(fn.headers(info)).toEqual(
    '<h1>ironman</h1><h1>spiderman</h1><h1>hulk</h1>'
    )
    })
    it('should create headers for 4 elements', () => {
    const leaders = {
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga',
    saffron: 'Sabrina'
    }
    expect(fn.headers(leaders)).toEqual(
    '<h1>vermilion</h1><h1>cinnabar</h1><h1>fuchsia</h1><h1>saffron</h1>'
    )
    })
    it('should return an empty string if no elements', () => {
    expect(fn.headers([])).toEqual('')
    })
    })
  2. Shape

    const headers = () => {}
  3. Explanation

    • You are given an object obj.
    • Get all the keys from the obj and attach reduce method to it.
    • Inside the reduce function,
      • return accumulator + h1 tag wrapped around each element.
      • starts with an empty string
  4. Code

    const headers = obj => {
    return Object.keys(obj).reduce((result, e, i) => {
    return result + '<h1>' + e + '</h1>'
    }, '')
    }
  • Part 2

    Modify your function so that it returns both keys and values inside the <h1> tags.

    const result2 = headers(info)
    // result2 is '<h1>ironman: arrogant</h1><h1>spiderman: naive</h1><h1>hulk: strong</h1>'
Answer
  1. Tests

    describe('headers function (part 2)', () => {
    it('should create h1s for 3 items', () => {
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    // The line breaks are just for ease of reading;
    // they won't count as part of the expected
    // solution since they're escaped with \
    const exp =
    '<h1>ironman: arrogant</h1>\
    <h1>spiderman: naive</h1><h1>hulk: strong</h1>'
    expect(fn.headers(info)).toEqual(exp)
    })
    it('should create headers for 4 elements', () => {
    const leaders = {
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga',
    saffron: 'Sabrina'
    }
    const exp =
    '<h1>vermilion: Surge</h1>\
    <h1>cinnabar: Blaine</h1><h1>fuchsia: Koga</h1>\
    <h1>saffron: Sabrina</h1>'
    expect(fn.headers(leaders)).toEqual(exp)
    })
    it('should return an empty string if no elements', () => {
    expect(fn.headers([])).toEqual('')
    })
    })
  2. Shape

    const headers = obj => {}
  3. Explanation

    • You are given an object obj.
    • Get keys and values for obj using object helper method entries. Attach reduce method to it.
    • Inside the reduce function,
      • return the accumulator inside a string and wrap key and value with h1 tag.
  4. Code

    const headers = obj => {
    return Object.entries(obj).reduce((result, e, i) => {
    return `${result}<h1>${e[0]}: ${e[1]}</h1>`
    }, '')
    }
  • Part 3

    Finally, modify your function so that it returns a string of joined keys and values, separated into <div>s and with keys in <h1>s and values in <h2>s.

    const result3 = headers(info)
    // result3 is '<div><h1>ironman</h1><h2>arrogant</h2></div><div><h1>spiderman</h1><h2>naive</h2></div><div><h1>hulk</h1><h2>strong</h2></div>'
Answer
  1. Tests

    describe('headers function (part 3)', () => {
    it('should create h1s for 3 items', () => {
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const exp =
    '<div><h1>ironman</h1><h2>arrogant</h2></div>\
    <div><h1>spiderman</h1><h2>naive</h2></div>\
    <div><h1>hulk</h1><h2>strong</h2></div>'
    expect(fn.headers(info)).toEqual(exp)
    })
    it('should create headers for 4 elements', () => {
    const leaders = {
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga',
    saffron: 'Sabrina'
    }
    const exp =
    '<div><h1>vermilion</h1><h2>Surge</h2></div>\
    <div><h1>cinnabar</h1><h2>Blaine</h2></div>\
    <div><h1>fuchsia</h1><h2>Koga</h2></div>\
    <div><h1>saffron</h1><h2>Sabrina</h2></div>'
    expect(fn.headers(leaders)).toEqual(exp)
    })
    it('should return an empty string if no elements', () => {
    expect(fn.headers([])).toEqual('')
    })
    })
  2. Shape

    const headers = obj => {}
  3. Explanation

    • You are given an obj.
    • Get all the keys from the obj. Attach reduce method to it.
    • Inside the reduce function,
      • return the accumulator inside a string and wrap the element with h1 tag and wrap the obj at that key with h2 tags.
      • wrap both h1 and h2 tags with div tag.
      • starts with an empty string
  4. Code

    const headers = obj => {
    return Object.keys(obj).reduce((result, e, i) => {
    return `${result}<div><h1>${e}</h1><h2>${obj[e]}</h2></div>`
    }, '')
    }

Prototype Inheritance

Prototype functions are great for memory efficiency when you plan on constructing many Objects.

Want to add functions into every object that you create? Just assign them to Object.prototype. To make this work, you must:

  1. Define your function using function( ... params ... ) { ... code ...}

    It is important NOT to use an arrow-function () => {} here because of how it treats this - You can read more on arrow-functions here MDN Arrow Functions

  2. Assign your function to Object.prototype

  3. Access object properties using the this keyword

Object.prototype.forEach = function (
fun,
i = 0,
entries = Object.entries(this)
) {
if (i === entries.length) return
fun(entries[i][1], entries[i][0])
return this.forEach(fun, i + 1)
}

You can also add new properties to this using prototype functions.

Object.prototype.eat = function (value) {
const num = this.data || 0
if (value < num) {
return
}
this.data = value
}
const a = { name: 'iron' }
a.eat(5)
// a is: { name: "iron", data: 5 }
a.eat(3)
// nothing happens because 3 is smaller than 5
// a is: { name: "iron", data: 5 }
a.eat(30)
// 30 is bigger than 5, so update data property
// a is: { name: "iron", data: 30 }

The "new" Keyword

When you construct a new Object with the new keyword, there are a few things that are happening in the background for you. This involves how the this is defined and used.

Let's look at an example to see what's happening.

function Person(name, age) {
this.name = name
this.age = age
}
const me = new Person('Joe', 24)

The Person function is nothing special, it is just like any other function we use in JavaScript. What makes it special is HOW we use it, and the new keyword.

When I do something like new Person() JavaScript will handle constructing a new Object for me and set up the Prototype Inheritance for me.

This is how the Person function is being used when we call it with new.

function Person(name, age) {
// this part is done for us implicitly "behind the scenes"
const this = Object.create(Person.prototype)
this.name = name
this.age = age
// this part is done for us
return this
}

With the above code, I could use the Person function the same way as before WITHOUT the new keyword and construct a new Object just by calling Person(). Of course, why do the extra work when we can let JavaScript handle it for us by using the new keyword?

The main takeaway from this is what's happening behind-the-scenes and also, what Object.create() does for us. Object.create is how inheritance is set up in JavaScript, it allows you to create the "prototype chain" for a new Object. You can read more about it here MDN Object.create.

This is a view of the JavaScript console on the Chrome browser. The > is where code was input into the console and the <- is the output from the code that preceded.

  1. Set up the Person function to use as an Object constructor.
  2. Define a myName function on the prototype property of Person. (prototype is a property that exists on functions already, see Note below)
  3. Construct a new instance of Person and assign it to the j variable.
  4. Check to see what j contains. (from the code output you can see it is a Person object)
  5. The next line is to show that objects DO NOT have the prototype property on them. It is only the constructor functions.
  6. We then check the **proto** property on the object j which will show us the prototype chain (**proto** is deprecated and Object.getPrototypeOf(obj) should be used instead, see Note below)

Below is a note from the MDN page which talks all about prototypes in JavaScript. MDN JS Prototypes:

Note: It's important to understand that there is a distinction between an object's prototype (available via Object.getPrototypeOf(obj), or via the deprecated proto property) and the prototype property on constructor functions.

The constructor function Foobar() has its own prototype, which can be found by calling Object.getPrototypeOf(Foobar). However this differs from its prototype property, Foobar.prototype, which is the blueprint for instances of this constructor function.

If we were to create a new instance — let fooInstance = new Foobar() — fooInstance would take its prototype from its constructor function's prototype property. Thus Object.getPrototypeOf(fooInstance) === Foobar.prototype.

Exercises

Time to be creative and write some prototype functions!

  1. Write a forEach function for objects.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = info.forEach((key, value, i) => {
    console.log(key, value, i)
    })
    // Will print out the following:
    /*
    ironman arrogant 0
    spiderman naive 1
    hulk strong 2
    */
    // No need to set module.exports because there is nothing to export
Answer
  1. Tests

    describe('forEach function', () => {
    it('should run a function 3 times on 3 elements', () => {
    const fun = jest.fn()
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    info.forEach(fun)
    expect(fun).toHaveBeenCalledTimes(3)
    })
    it('should run a function 0 times on an empty object', () => {
    const fun = jest.fn()
    const imEmpty = {}
    imEmpty.forEach(fun)
    expect(fun).not.toHaveBeenCalled()
    })
    it('should let functions access object values & positions', () => {
    const vals = []
    const fun = (_k, v, i) => {
    vals.push(i + v)
    }
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    info.forEach(fun)
    expect(vals).toEqual(['0arrogant', '1naive', '2strong'])
    })
    })
  2. Shape

    Object.prototype.forEach = function (cb) {}
  3. Explanation

    • You are given a callback function cb.
    • We need an additional variable(let's call it i) to keep track of index which starts at 0.
    • We need another additional variable(let's call it keys) to get all the keys of the object given this.
    • When i reaches the length of keys, return.
    • Call the callback function.
    • Continue
  4. Code

    Object.prototype.forEach = function (cb, i = 0, keys = Object.keys(this)) {
    if (i === keys.length) return
    cb(keys[i], this[keys[i]], i, this)
    return this.forEach(cb, i + 1, keys)
    }
Debrief
We solved this function using recursion. But you could also have done it by calling the array `forEach` function on `Object.entries(this)`. After all, `forEach` is already implemented for arrays, and the `entries` array could help us extend this functionality to objects.
  1. Write a filter function for objects.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = info.filter((key, value) => {
    return key[0] === 'i' || value[0] === 'n'
    })
    console.log(result) // should print out:
    /*
    {
    ironman: "arrogant",
    spiderman: "naive"
    }
    */
Answer
  1. Tests

    describe('filter function', () => {
    const leaders = {
    vermilion: 'Surge',
    cinnabar: 'Blaine',
    fuchsia: 'Koga',
    saffron: 'Sabrina'
    }
    it('should filter based on keys', () => {
    const seven = k => {
    return k.length === 7
    }
    const result = leaders.filter(seven)
    expect(result).toEqual({ fuchsia: 'Koga', saffron: 'Sabrina' })
    })
    it('should filter based on keys', () => {
    const six = (_k, v) => {
    return v.length < 6
    }
    const result = leaders.filter(six)
    expect(result).toEqual({ vermilion: 'Surge', fuchsia: 'Koga' })
    })
    it('should return an empty object if no matches', () => {
    const celadon = k => {
    return k === 'Celadon'
    }
    const result = leaders.filter(celadon)
    expect(result).toEqual({})
    })
    })
  2. Shape

    Object, (prototype.filter = function (cb) {})
  3. Explanation

    • We are given a callback function cb.
    • We need an additional variable(let's call it result) and it starts with an empty object.
    • We need an additional variable(let's call it i) to track index and it starts at 0.
    • We need another additional variable(let's call it keys) to get all the keys of the object given this.
    • When i reaches the length of keys, return result.
    • We need a local variable(let's call it currentKey) and it starts with first index of keys.
    • We need a local variable(let's call it element) and it stores the result of calling the callback function.
    • When element is true, result at key currentKEy is equal to this at key currentKey.
    • Continue
  4. Code

    Object.prototype.filter = function (
    cb,
    result = {},
    i = 0,
    keys = Object.keys(this)
    ) {
    if (i === keys.length) return result
    const currentKey = keys[i]
    const element = cb(currentKey, this[currentKey])
    if (element == true) result[currentKey] = this[currentKey]
    return this.filter(cb, result, i + 1, keys)
    }
  1. Write a reduce function for objects.

    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = info.reduce((acc, key, value, i) => {
    return acc + `${key}-${i}-${value},`
    }, '')
    console.log(result) // should print out:
    /*
    ironman-0-arrogant,spiderman-1-naive,hulk-2-strong,
    */
Answer
  1. Tests

    describe('reduce function', () => {
    it('should let functions access keys, values, & positions', () => {
    const fun = (acc, key, value, i) => {
    return acc + `${key}-${i}-${value},`
    }
    const info = {
    ironman: 'arrogant',
    spiderman: 'naive',
    hulk: 'strong'
    }
    const result = info.reduce(fun, '')
    const exp = 'ironman-0-arrogant,spiderman-1-naive,hulk-2-strong,'
    expect(result).toEqual(exp)
    })
    it('should return the starting value if the object is empty', () => {
    const imEmpty = {}
    const result = imEmpty.reduce(() => {}, 'I am Groot')
    expect(result).toEqual('I am Groot')
    })
    })
  2. Shape

    Object.prototype.reduce = function (cb) {}
  3. Explanation

    • We are given a callback function cb.
    • We are given an accumulator acc.
    • We need an additional variable(let's call it i) to track index and it starts at 0.
    • We need another additional variable(let's call it keys) to get all the keys of the object given this.
    • When i equals the length of keys, return acc.
    • We need a local variable(let's call it next) and it stores the result of calling the callback function.
    • Continue
  4. Code

    Object.prototype.reduce = function (
    cb,
    acc,
    i = 0,
    keys = Object.keys(this)
    ) {
    if (i === keys.length) return acc
    const next = cb(acc, keys[i], this[keys[i]], i)
    return this.reduce(cb, next, i + 1, keys)
    }
Debrief

You’ve now gotten some insight into how three of the array helper functions work by implementing them for objects. They’re pretty simple once you get the hang of them! All our solutions used recursion, and some sort of result or accumulator variable to keep track of what the function is doing. The main decision was how to iterate through the object; keys don’t work like indices where we could say obj[0], obj[1], etc.

For all our solutions we chose to pull out the keys into an array, then iterate through the keys, using the keys to pull their corresponding values from the object (key = keys[i], value = this[keys[i]]). We could have also pulled the values separately using something like values = Object.values(this) and values[i] to get each value. We could have even used Object.entries(this) and then entries[i][0] for the keys and entries[i][1] for the values.

  1. Write a getCharCount prototype function for arrays of strings that returns an object of character counts.

    ;['Charmander', 'Charmeleon', 'Charizard'].getCharCount()
    /*
    * Returns
    {
    C: 3,
    h: 3,
    a: 5,
    r: 5,
    m: 2,
    n: 2,
    d: 2,
    e: 3,
    l: 1,
    o: 1,
    i: 1,
    z: 1
    }
    */
Answer
  1. Tests

    describe('getCharCount function', () => {
    it('should count letters in an array of 3 strings', () => {
    const result = ['Charmander', 'Charmeleon', 'Charizard'].getCharCount()
    expect(result).toEqual({
    C: 3,
    h: 3,
    a: 5,
    r: 5,
    m: 2,
    n: 2,
    d: 2,
    e: 3,
    l: 1,
    o: 1,
    i: 1,
    z: 1
    })
    })
    it('should handle an empty array', () => {
    const result = [].getCharCount()
    expect(result).toEqual({})
    })
    it('should count characters in empty strings', () => {
    const result = ['Pallet', '', 'Pewter', '', 'Saffron'].getCharCount()
    expect(result).toEqual({
    P: 2,
    a: 2,
    l: 2,
    e: 3,
    t: 2,
    w: 1,
    r: 2,
    S: 1,
    f: 2,
    o: 1,
    n: 1
    })
    })
    })
  2. Shape

    Array.prototype.getCharCount = function () {}
  3. Explanation

    • We need a helper function(let's call it addChar)
    • addChar takes in three parameters: map which starts with an empty object, string str, and an index i which starts at 0.
      • When i reaches the length of str, return map.
      • We need a local variable(let's call it letter) which stores the str at index i.
      • The value of map is equal to the value of map or 0.
      • The value of map is equal to the value of map plus 1.
      • Continue
      • Attach reduce method to this.
        • return by calling the addChar function.
        • It starts with an empty object.
  4. Code

    Array.prototype.getCharCount = function () {
    const addChar = (map = {}, str, i = 0) => {
    if (i === str.length) {
    return map
    }
    const letter = str[i]
    map[letter] = map[letter] || 0
    map[letter] = map[letter] + 1
    return addChar(map, str, i + 1)
    }
    return this.reduce((acc, e) => {
    return addChar(acc, e)
    }, {})
    }
Debrief

You might have seen an array and noticed an opportunity to use reduce. Great! Another simple way to solve this problem would have been to use two levels of recursion, like in zeroSquare—for example an i counter for the strings and a j counter for the letters. You also probably noticed that whether you used recursion or reduce, you would need an accumulator, starting as an empty object {}, to store the results.

  1. Write a getMostCommon prototype function for arrays that returns the most common element.
;[9, 8, 7, 8, 7, 7, 7].getMostCommon()
// returns 7 because it is the most common element
;['Batman', 8, 7, 'Batman', 'Robin'].getMostCommon()
// returns "Batman" because it is the most common element
Hint

Start by counting the frequency of each unique element in the array (you can make a new array to hold these frequencies). Then use reduce on the frequencies to find out which is the highest. Because object keys are always strings, you won't be able to use the elements themselves as keys; doing so would give "7" as the most common element of the first example above instead of 7. But you can make a little object to store an element along with its frequency, then store these little objects in an array.

Answer
  1. Tests

    describe('getMostCommon function', () => {
    it('should return a number as the most common', () => {
    const result = [9, 8, 7, 8, 7, 7, 7].getMostCommon()
    expect(result).toEqual(7)
    })
    it('should return a string as the most common', () => {
    const arr = ['Batman', 8, 7, 'Batman', 'Robin']
    const result = arr.getMostCommon()
    expect(result).toEqual('Batman')
    })
    it('should return first element if all equally common', () => {
    const types = ['grass', 'poison', 'fire', 'flying', 'water', 'bug']
    const result = types.getMostCommon()
    expect(result).toEqual('grass')
    })
    it('should return null on an empty array', () => {
    const result = [].getMostCommon()
    expect(result).toEqual(null)
    })
    })
  2. Shape

    Array.prototype.getMostCommon = function () {}
  3. Explanation

    Steps for Solution 1:

    • We need a local variable(let's call it mapOfElements) which stores the reduce method attached to this.
    • Inside the reduce method, build the object with the given array.
    • We need another local variable(let's call it mostCommon) which stores:
      • Attach Object.entries method to mapOfElements.
      • Attach reduce method to the the result of the step above.
    • Inside the reduce method.
      • We need a local variable(let's call it element) which equals first element of the array.
      • We need another local variable(let's call it count) which equals the second element of the array.
      • When count is less than the accumulator at index 1, return the element.
      • return the accumulator
      • It starts with an array [undefined, 0].
    • return mostCommon at index 0.

    Steps for Solution 2:

    • We need a local variable (let's call it arrMapCommon) which stores the result of attaching reduce method to the given array.
    • Inside the reduce method,
      • We need a local variable(let's call it map) and it is equal to the first element of the starting array.
      • Build the object in terms of how many times each elements exist.
      • We need a local variable(let's call it count) and it is equal to the third element of the starting array.
        • When the value of map at given element is greater than count,
          • set the last element of starting array to the value of map at that key element.
          • set the second element of starting array to the element you pass in the reduce method.
          • return the accumulator
      • return the element's value of arrMapCommon at second element.
  4. Code

    Array.prototype.getMostCommon = function () {
    const mapOfElements = this.reduce((acc, e) => {
    acc[e] = acc[e] || 0
    acc[e] = acc[e] + 1
    return acc
    }, {})
    const mostCommon = Object.entries(mapOfElements).reduce(
    (acc, e) => {
    const element = e[0]
    const count = e[1]
    if (count > acc[1]) {
    if (parseInt(element)) {
    return [parseInt(element), count]
    }
    return e
    }
    return acc
    },
    [null, 0]
    )
    return mostCommon[0]
    }
    // SOLUTION 2 -> Customize starting value of reduce
    Array.prototype.getMostCommon = function () {
    const arrMapCommon = this.reduce(
    (acc, e) => {
    const map = acc[0]
    map[e] = map[e] || 0
    map[e] = map[e] + 1
    const count = acc[2]
    if (map[e] > count) {
    acc[2] = map[e]
    acc[1] = e
    }
    return acc
    },
    [{}, null, 0]
    )
    return arrMapCommon[1]
    }
Debrief

This function has two parts: First we have to count the frequency of each item in the array. Then we have to find the item with the highest frequency. To count the frequency, we can use a very similar reduce call to the one in problem 4. Then we have to go back through the returned object and find which key occurs the most. For that, we can use the function that we wrote for Object Helpers, exercise 1 (longestString) with some silght modifications.

You’ll notice that an alternative solution is to let the one reduce call do all the work for us. In the first solution, we sort of tricked reduce into passing along 2 accumulator arguments, and in this one we're making it pass along all 3 things: all the frequencies as well the current most common element and its frequency. Even though reduce technically only takes one accumulator, we can get away with this by wrapping the 3 pieces of information we need in an array!

  1. Write a removeDupes prototype function for arrays that removes duplicate elements from the array.
const a = [9, 8, 7, 8, 7, 7, 7]
a.removeDupes() // a becomes [9]
Answer
  1. Tests

    describe('removeDupes function', () => {
    it('should remove 2 sets of duplicate numbers', () => {
    const data = [9, 8, 7, 8, 7, 7, 7]
    data.removeDupes()
    expect(data).toEqual([9])
    })
    it('should remove 1 set of duplicate strings', () => {
    const data = ['ice', 'electric', 'psychic', 'ice', 'ground', 'ice']
    data.removeDupes()
    expect(data).toEqual(['electric', 'psychic', 'ground'])
    })
    it('should remove duplicate boolean values', () => {
    const data = ['grass', false, 'poison', 'electric', false]
    data.removeDupes()
    expect(data).toEqual(['grass', 'poison', 'electric'])
    })
    it("shouldn't remove anything from an array with no dups", () => {
    const data = ['Pewter', 'Saffron', 'Vermilion', 'Veridian']
    data.removeDupes()
    expect(data).toEqual(['Pewter', 'Saffron', 'Vermilion', 'Veridian'])
    })
    it('should leave an empty array unchanged', () => {
    const data = []
    data.removeDupes()
    expect(data).toEqual([])
    })
    })
  2. Shape

    //Main Function
    Array.prototype.removeDupes = function () {}
  3. Explanation

    • First we need to get an object of how many times each element shows up in the array.
    • We need a function to remove all the count that does not equal 1:
      • We need an additional variable(let's call it i) to keep track of index and it starts at 0.
      • When i is at the the length of given array, we are done so return
      • If the element shows up exactly 1 time, then we continue to next i
      • Otherwise, remove the element at index i, and run the function again without increasing i
  4. Code

    //Main Function
    Array.prototype.removeDupes = function () {
    const map = this.reduce((acc, e) => {
    acc[e] = (acc[e] || 0) + 1
    return acc
    }, {})
    const remove = (i = 0) => {
    if (i === this.length) {
    return
    }
    if (map[this[i]] === 1) {
    return remove(i + 1)
    }
    this.splice(i, 1)
    return remove(i)
    }
    remove()
    }
Debrief

Examples: We've seen a couple examples of how this function should work. Note that it removes not just extra elements, but all copies of an element that occurs more than once.

Function shape: This is a prototype function, so the array we’re working on will be this. Since it modifies the array in place, it has no return value and takes no arguments.

Array.prototype.removeDupes = function () {}

Think: We'll need some code that counts how many times each element occurs in an array, then some code to remove all occurences of a given value from an array. We haven't done this exactly, but you might remember removeEvens from JS 2; we can use code similar to this. That function was recursive, but we probably shouldn't make this entire function recursive—we don't need to count the occurences multiple times!

So let's write a helper function inside Array.prototype.removeDupes to recursively remove the duplicate values once we've counted them. We can call it removeDupes without a name collision because it won't be attached to the array prototype. This will return nothing since it also modifies the array in place, and it will need only one parameter, an iterator or counter.

const removeDupes = (i = 0) => {}

Notice how we made this a Fat Arrow function—that way we can still use this to access the array we're working on! (Our little helper function won't get its own this that would override it.)

Code: The rest is quick because we've already written everything we need in other exercises—now we just have to put it together. There are several other ways we could have done this, such as using reduce to make an array of only values that occur more than once, then using forEach to remove each one from the original array.

Test: Testing your code should cover as many types of input as possible. Let's start with the examples and see if we can think of some more:

[9,8,7,8,7,7,7] // items occur 1, 2, and 3 times
["Batman", 8,7, "Batman", "Robin"] // one item occurs twice
["Spearow", "Fearow", "Ekans", "Arbok"] // no duplicates
["I am groot", "I am groot", "I am groot"] // all the same; should become []
[] // it's always good to test empty arrays

Arrays as Objects

In reality, an array is actually an object, with special status (you can use numbers as keys, etc). You can add keys and values to arrays! The following examples are for teaching purposes only—treating arrays as objects is not a good idea, as it will confuse other engineers on your team.

const a = [9, 8, 7, 5]
a.name = 'Tony Stark' // You assign a key and value to an array like an object
console.log(a) // will print out [ 9, 8, 7, 5, name: 'Tony Stark' ]

Here's a countForEach function that keeps a counter of how many times it's called:

// This creates a new array function, countForEach
// This function calls the normal forEach function and returns
// how many times the function has been called on the array
Array.prototype.countForEach = function (cb) {
// The first time this function is called, this.forEachCount will be undefined
// When that happens, we use 0
this.forEachCount = (this.forEachCount || 0) + 1
// Calls the regular forEach function in the array
this.forEach(cb)
return this.forEachCount
}
const villains = ['Joker', 'Catwoman', 'Penguin', 'Riddler']
villains.countForEach(() => {}) // returns 1
villains.countForEach(() => {}) // returns 2
villains.countForEach(() => {}) // returns 3
const moreVillains = ['Two-Face', 'Bane']
moreVillains.countForEach(() => {}) + villains.countForEach(() => {})
// Returns 1 + 4, which is 5

Notice how we gave the array a property, forEachCount, as if were an object.

Exercises

  1. Write a getNext prototype function for arrays that returns the next element of the array.

    const a = ["Edna", "Optimus", "Minion"]
    a.getNext() // returns "Edna"
    a.getNext() // returns "Optimus"
    a.getNext() // returns "Minion"
    a.getNext() // returns "Edna"
    a.getNext() // returns "Optimus"
    a.getNext() // returns "Minion"
    a.getNext() // returns "Edna"
    ...
Answer
  1. Tests

    describe('getNext function', () => {
    it('should iterate through 3 elements', () => {
    const arr = ['Edna', 'Optimus', 'Minion']
    let result = arr.getNext()
    expect(result).toEqual('Edna')
    expect(arr.getNext()).toEqual('Optimus')
    expect(arr.getNext()).toEqual('Minion')
    })
    it('should return to beginning once done', () => {
    const arr = [9, 80, 12, 2]
    expect(arr.getNext()).toEqual(9)
    expect(arr.getNext()).toEqual(80)
    expect(arr.getNext()).toEqual(12)
    expect(arr.getNext()).toEqual(2)
    expect(arr.getNext()).toEqual(9)
    expect(arr.getNext()).toEqual(80)
    })
    it('should return undefined for an empty array', () => {
    const arr = []
    expect(arr.getNext()).toEqual(undefined)
    })
    it('should iterate through one element', () => {
    const arr = ['Ironman']
    expect(arr.getNext()).toEqual('Ironman')
    expect(arr.getNext()).toEqual('Ironman')
    })
    it(`shouldn't iterate`, () => {
    const arr = []
    expect(arr.getNext()).toEqual()
    expect(arr.getNext()).toEqual()
    expect(arr.getNext()).toEqual()
    expect(arr.getNext()).toEqual()
    })
    })
  2. Shape

    Array.prototype.getNext = function () {}
  3. Explanation

    • Create a constant index variable that is assigned to this.indexCounter or 0.
    • Assign this.indexCounter to the currentIndex + 1 and modulo that result by this.length
    • Return the current index of this array.
  4. Code

    Array.prototype.getNext = function () {
    const index = this.indexCounter || 0
    this.indexCounter = (index + 1) % this.length
    return this[index]
    }
Debrief

Look familiar? This is very similar to Array.prototype.getIterator from JS 2. How is it different? Here, we’re not returning a function, so we need our iterator/counter variable to be defined outside of this function. Array.prototype.getNext(i=0) is no good because every time we run it, i will go back to 0 and we'll just have a lot of "Edna". But now that we know how to put named keys in arrays, we can define a counter outside the function just by setting this.indexCounter.

Because it's part of the Array prototype, it's not part of the array itself, and won't show up if you console.log the array! But just like every array can access prototype functions like map and pop, your array will now be able to access its indexCounter.

  1. Write an Array setMaxSize prototype function that gives arrays a max length beyond which new elements can no longer be pushed.

    const a = ['Edna', 'Optimus', 'Minion']
    a.setMaxSize(4)
    a.push('Groot') // push returns 4.
    // Array is ["Edna", "Optimus", "Minion", "Groot"]
    a.push('hello') // Nothing happens. push returns 4, array stays the same.
Hint
Did you know that you can override existing prototype functions? It's as easy as assigning a new value to a key inside of the setMaxSize object. Just make sure to save the old one into a variable first so you can keep using its functionality.
Answer
  1. Tests

    describe('setMaxSize prototype', () => {
    it('maxSize should stay four', () => {
    const arr = ['Michelangelo', 'Leonardo', 'Raphael']
    arr.setMaxSize(4)
    arr.push('Donatello')
    arr.setMaxSize(3)
    arr.push('Shredder')
    arr.setMaxSize(1)
    arr.push('Splinter')
    expect(arr.length).toEqual(4)
    })
    it('maxSize should increase', () => {
    const arr = ['Michelangelo']
    arr.setMaxSize(2)
    arr.push('Donatello')
    expect(arr.length).toEqual(2)
    })
    it('maxSize keeps array empty', () => {
    const arr = []
    arr.setMaxSize(0)
    arr.push('M', 'L', 'R')
    expect(arr.length).toEqual(0)
    })
    })
  2. Shape

    Array.prototype.setMaxSize = function (max) {}
  3. Explanation

    • Create Array.prototype.setMaxSize that passes in a max variable.
    • Inside setMaxSize prototype save this.push in a variable called this.oldPush.
    • Inside setMaxSize prototype assign this.push to an arrow function that passes in a new element.
    • if your max parameter is greather than this.length return this.oldPush envoked with your new element parameter.
    • else return this.length
  4. Code

    Array.prototype.setMaxSize = function (size) {
    this.oldPush = this.push
    this.push = newElement => {
    if (this.length < size) {
    return this.oldPush(newElement)
    }
    return this.length
    }
    }
Debrief

Why doesn't Array.prototype.push work?:

Array.prototype.setMaxSize = function(max) {
this.max = max
this.oldPush = this.push
}
Array.prototype.push = function(newElement) {
if (this.length < this.max) {
return this.oldPush(newElement)
}
return this.length
}
}

The problem is that you're setting Array.prototype.push to some function that you defined.

Then you're calling Array.prototype.setMaxSize, which sets this.oldPush to your own defined Array.prototype.push (not the original Array.prototype.push).

So this.oldPush and Array.prototype.push are both the same function which is the function you defined.

So your defined Array.prototype.push function will call this.oldPush which is also your defined Array.prototype.push function, so it just keeps calling itself forever.

This is an example of why it is considered bad practice to overwrite built-in Javascript prototypes and methods.

  1. Write a tiredForEach prototype function that runs a function on each element of an array, but makes the user wait a specified amount of time before calling it again.

    const a = ['chinese', 'african', 'korean']
    const callback = (e, i) => {
    console.log(e + i)
    }
    a.tiredForEach(callback, 180)
    /*
    prints out:
    chinese0
    african1
    korean2
    */
    a.tiredForEach(callback, 180)
    /*
    prints out:
    "Too tired. Please wait 180ms.
    */
    setTimeout(() => {
    // run tiredForEach after 190ms
    a.tiredForEach(callback, 180)
    }, 190)
    /*
    ... 190ms later....
    prints out:
    chinese0
    african1
    korean2
    */
Answer
  1. Tests

    describe('tiredForEach function', () => {
    jest.useFakeTimers()
    it('should call callback immediately when not tired', () => {
    const callback = jest.fn()
    const arr = ['Edna', 'Optimus', 'Minion']
    arr.tiredForEach(callback, 200)
    expect(callback).toHaveBeenCalled()
    })
    it('should not run function before time has passed', () => {
    const callback = jest.fn()
    const callback2 = jest.fn()
    const arr = ['Edna', 'Optimus', 'Minion']
    arr.tiredForEach(callback, 200)
    arr.tiredForEach(callback2, 200)
    expect(callback2).not.toHaveBeenCalled()
    })
    it('should work again once time has passed', () => {
    const callback = jest.fn()
    const arr = ['Edna', 'Optimus', 'Minion']
    arr.tiredForEach(callback, 200)
    jest.advanceTimersByTime(200)
    arr.tiredForEach(callback, 200)
    expect(callback).toHaveBeenCalledTimes(6)
    })
    })
  2. Shape

    Array.prototype.tiredForEach = function (cb, time) {
    return // ...
    }
  3. Explanation

    • Create an array prototype called tiredForEach that takes in a callback and time.
    • Check whether this.isTired is true,
      • If it is return a console.log() with the "too tired" text. Return stops the function
    • Assign this.isTired to true and this.waiTime to time.
    • Call a SetTimeout with an arrow function and time as parameters.
      • Inside the arrow function assign this.isTired to false.
    • e. return this.forEach(cb).
  4. Code

    Array.prototype.tiredForEach = function (cb, time) {
    if (this.isTired)
    return console.log(`Too tired. Please wait ${this.waitTime}ms.`)
    this.isTired = true
    this.waitTime = time
    /*
    NOTE: The argument function to setTimeout is written
    with () => {....}.
    If you write it using
    setTimeout(function {
    this.isTired
    ....
    then the code will not work.
    Explanation provided in the next section.
    */
    setTimeout(() => {
    this.isTired = false
    }, time)
    return this.forEach(cb)
    }
Debrief

We haven't covered telling what time it is per se, but we knew we would be storing some information as a property of the array, and we remembered how to make a function run after a certain amount of time using setTimeout. So we made a flag to be turned on when tiredForEach is called and turned off after the requested wait time.

Remember that when using an if check, if a variable is undefined it’ll just evaluate as falsey so it’s OK to say if (this.tired) even if this.tired has not been defined yet.

Master your skill by solving challenges

Complete the first eight JS3 challenges

Edit this page on Github