└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # The Secret Life of Objects 2 | 3 | In [chapter 6](http://eloquentjavascript.net/06_object.html) of [Eloquent JavaScript](https://github.com/marijnh/Eloquent-JavaScript) there is an example that was hard for me to understand. So with this repo, I hope to make it easier for newbies like myself to understand it. 4 | 5 | Before starting I recommend watching these videos. 6 | 7 | [`Map`](https://www.youtube.com/watch?v=bCqtb-Z5YGQ&index=2&list=PL0zVEGEvSaeEd9hlmCXrk5yUyqUag-n84) by [mpjme](https://twitter.com/mpjme?lang=en) 8 | 9 | [`Reduce`](https://www.youtube.com/watch?v=1DMolJ2FrNY&index=4&list=PL0zVEGEvSaeEd9hlmCXrk5yUyqUag-n84) by [mpjme](https://twitter.com/mpjme?lang=en) 10 | 11 | And [Object-Oriented JavaScript](https://www.youtube.com/watch?v=PMfcsYzj-9M) by [James Shore](https://twitter.com/jamesshore) 12 | 13 | 14 | Our goal is making a table like this 15 | 16 | ``` 17 | name height country 18 | ------------ ------ ------------- 19 | Kilimanjaro 5895 Tanzania 20 | Everest 8848 Nepal 21 | Mount Fuji 3776 Japan 22 | Mont Blanc 4808 Italy/France 23 | Vaalserberg 323 Netherlands 24 | Denali 6168 United States 25 | Popocatepetl 5465 Mexico 26 | ``` 27 | out of this array 28 | ```js 29 | var MOUNTAINS = [ 30 | {name: "Kilimanjaro", height: 5895, country: "Tanzania"}, 31 | {name: "Everest", height: 8848, country: "Nepal"}, 32 | {name: "Mount Fuji", height: 3776, country: "Japan"}, 33 | {name: "Mont Blanc", height: 4808, country: "Italy/France"}, 34 | {name: "Vaalserberg", height: 323, country: "Netherlands"}, 35 | {name: "Denali", height: 6168, country: "United States"}, 36 | {name: "Popocatepetl", height: 5465, country: "Mexico"} 37 | ]; 38 | ``` 39 | Pretty simple right? not for me. I had a hard time understanding what's going on. 40 | 41 | This is the entire program that does this. 42 | 43 | ``` javascript 44 | const MOUNTAINS = require("./mountains"); 45 | 46 | function rowHeights(rows) { 47 | return rows.map(function(row) { 48 | return row.reduce(function(max, cell) { 49 | return Math.max(max, cell.minHeight()); 50 | }, 0); 51 | }); 52 | } 53 | 54 | function colWidths(rows) { 55 | return rows[0].map(function(_, i) { 56 | return rows.reduce(function(max, row) { 57 | return Math.max(max, row[i].minWidth()); 58 | }, 0); 59 | }); 60 | } 61 | 62 | function drawTable(rows) { 63 | var heights = rowHeights(rows); 64 | var widths = colWidths(rows); 65 | function drawLine(blocks, lineNo) { 66 | return blocks.map(function(block) { 67 | return block[lineNo]; 68 | }).join(" "); 69 | } 70 | 71 | function drawRow(row, rowNum) { 72 | var blocks = row.map(function(cell, colNum) { 73 | return cell.draw(widths[colNum], heights[rowNum]); 74 | }); 75 | return blocks[0].map(function(_, lineNo) { 76 | return drawLine(blocks, lineNo); 77 | }).join("\n"); 78 | } 79 | 80 | return rows.map(drawRow).join("\n"); 81 | } 82 | 83 | function repeat(string, times) { 84 | var result = ""; 85 | for (var i = 0; i < times; i++) 86 | result += string; 87 | return result; 88 | } 89 | 90 | function TextCell(text) { 91 | this.text = text.split("\n"); 92 | } 93 | 94 | TextCell.prototype.minWidth = function() { 95 | return this.text.reduce(function(width, line) { 96 | return Math.max(width, line.length); 97 | }, 0); 98 | }; 99 | 100 | TextCell.prototype.minHeight = function() { 101 | return this.text.length; 102 | }; 103 | 104 | TextCell.prototype.draw = function(width, height) { 105 | var result = []; 106 | for (var i = 0; i < height; i++) { 107 | var line = this.text[i] || ""; 108 | result.push(line + repeat(" ", width - line.length)); 109 | } 110 | return result; 111 | }; 112 | 113 | function UnderlinedCell(inner) { 114 | this.inner = inner; 115 | } 116 | 117 | UnderlinedCell.prototype.minWidth = function() { 118 | return this.inner.minWidth(); 119 | }; 120 | 121 | UnderlinedCell.prototype.minHeight = function() { 122 | return this.inner.minHeight() + 1; 123 | }; 124 | 125 | UnderlinedCell.prototype.draw = function(width, height) { 126 | return this.inner.draw(width, height - 1).concat([repeat("-", width)]); 127 | }; 128 | 129 | function dataTable(data) { 130 | var keys = Object.keys(data[0]); 131 | var headers = keys.map(function(name) { 132 | return new UnderlinedCell(new TextCell(name)); 133 | }); 134 | var body = data.map(function(row) { 135 | return keys.map(function(name) { 136 | var value = row[name]; 137 | if (typeof value == "number") 138 | return new RTextCell(String(value)); 139 | else 140 | return new TextCell(String(value)); 141 | }); 142 | }); 143 | return [headers].concat(body); 144 | } 145 | 146 | function RTextCell(text) { 147 | TextCell.call(this, text); 148 | } 149 | 150 | RTextCell.prototype = Object.create(TextCell.prototype); 151 | 152 | RTextCell.prototype.draw = function(width, height) { 153 | var result = []; 154 | for (var i = 0; i < height; i++) { 155 | var line = this.text[i] || ""; 156 | result.push(repeat(" ", width - line.length) + line); 157 | } 158 | return result; 159 | }; 160 | 161 | console.log(drawTable(dataTable(MOUNTAINS))); 162 | ``` 163 | 164 | Let's get started 165 | 166 | ```js 167 | console.log(drawTable(dataTable(MOUNTAINS))); 168 | ``` 169 | This is where everything begins. We send our `MOUNTAINS` array to `dataTable` function. 170 | 171 | ```js 172 | function dataTable(data) { 173 | var keys = Object.keys(data[0]); 174 | var headers = keys.map(function(name) { 175 | return new UnderlinedCell(new TextCell(name)); 176 | }); 177 | var body = data.map(function(row) { 178 | return keys.map(function(name) { 179 | return new TextCell(String(row[name])); 180 | }); 181 | }); 182 | return [headers].concat(body); 183 | } 184 | ``` 185 | 186 | Let's go through it line by line. 187 | ```js 188 | var keys = Object.keys(data[0]); 189 | // date[0] -> {name: "Kilimanjaro", height: 5895, country: "Tanzania"}, 190 | // keys -> [ "name", "height", "country"] 191 | ``` 192 | It uses [`Objecet.keys`](https://davidwalsh.name/object-keys) method to get the keys of one of our objects in the `data` array. 193 | 194 | ``` javascript 195 | var headers = keys.map(function(name) { 196 | return new UnderlinedCell(new TextCell(name)); 197 | }); 198 | /* headers -> 199 | * [ 200 | * UnderlinedCell { inner: TextCell { text: ['name'] } }, 201 | * UnderlinedCell { inner: TextCell { text: ['height'] } }, 202 | * UnderlinedCell { inner: TextCell { text: ['country'] } }, 203 | * ] 204 | */ 205 | ``` 206 | As you can see in our final table we have a header that is nicely underlined. We use `map` method on our `keys` to make an object for each key. So what we assign to `headers` is an array containing three `UnderlinedCell` objects. 207 | Our objects are constructed with two constructors `TextCell` and `UnderlinedCell`. Let's take a look at how they're defined before continuing with our `dataTable` function. 208 | 209 | ``` javascript 210 | function TextCell(text) { 211 | this.text = text.split("\n"); 212 | } 213 | ``` 214 | `String.prototype.split` returns an array of strings. so `this.text` is an array, something like `["country"]` 215 | 216 | `TextCell` is a constructor, therefore, when used with `new` keyword it implicitly returns `this`. 217 | 218 | ``` javascript 219 | TextCell.prototype.minWidth = function() { 220 | return this.text.reduce(function(width, line) { 221 | return Math.max(width, line.length); 222 | }, 0); 223 | }; 224 | 225 | TextCell.prototype.minHeight = function() { 226 | return this.text.length; 227 | }; 228 | 229 | TextCell.prototype.draw = function(width, height) { 230 | var result = []; 231 | for (var i = 0; i < height; i++) { 232 | var line = this.text[i] || ""; 233 | result.push(line + repeat(" ", width - line.length)); 234 | } 235 | return result; 236 | } 237 | ``` 238 | Each instance of `TextCell` (which indicates a cell in the final table) inherits these methods. 239 | 240 | `minWidth()` returns a number indicating the minimum width of its cell (in characters). It uses `reduce` on `this.text` in case `this.text` has more than one element (more than one line) which in our example it never does. 241 | 242 | `minHeight()` returns a number indicating the minimum height that its cell requires (in lines). In our example it's always 1. `length` of `this.text` indicates the minimum height that a given cell needs. 243 | 244 | We use `draw` to tell a cell to draw itself. It takes two parameters `width` and `height` and returns an array, something like `[ 'Kilimanjaro ' ]`. 245 | 246 | `width` is the maximum width of a particular column that our cell is going reside in. We need this to add extra spaces in order to make our cells in a column aligned with each other. 247 | 248 | `height` is the height of a particular row. If a cell should, say takes two line, height will be 2. In our example, it's always 1. Remember underlined cells of header uses a slightly different version of `draw`. We'll see them in a moment. 249 | 250 | We go through the body of loop `height` times. 251 | 252 | `draw` uses a helper function `repeat` to add those extra spaces. `repeat` function takes a string and a number and it returns the given string concatenated with itself number of times. e.g. `repeat("bat! ", 3)` returns `"bat! bat! bat! "`. 253 | 254 | ``` javascript 255 | function repeat(string, times) { 256 | var result = ""; 257 | for (var i = 0; i < times; i++) 258 | result += string; 259 | return result; 260 | } 261 | ``` 262 | 263 | We figure out how many extra spaces we should add with subtracting length of the widest cell in our column (`width`) from length of current cell that we are drawing (`line.length`). e.g: `kilimanjaro` with the `length` of `11` belongs to the first column. The widest cell in the first column is `"Popocatepetl"` with the `length` of `12`. So we only need one space after `kilimanjaro`. `result` will be `[ 'Kilimanjaro ' ]`. 264 | 265 | We're not done with constructors just yet. `TextCell` will be used for regular cells. For underlined cells (that is headers) we have another constructor `UnderlinedCell`. Fortunately, it inherits most of the goodies from `TextCell`. 266 | 267 | ``` javascript 268 | function UnderlinedCell(inner) { 269 | this.inner = inner; 270 | } 271 | UnderlinedCell.prototype.minWidth = function() { 272 | return this.inner.minWidth(); 273 | }; 274 | UnderlinedCell.prototype.minHeight = function() { 275 | return this.inner.minHeight() + 1; 276 | // so minHeight of the header will be 2 277 | }; 278 | UnderlinedCell.prototype.draw = function(width, height) { 279 | // this method returns an array. e.g. [ 'name ', '------------' ] 280 | return this.inner.draw(width, height - 1).concat([repeat("-", width)]); 281 | }; 282 | ``` 283 | Something that you shouldn't forget about `UnderlinedCell` is that it takes `TextCell` objects as its argument so `inner` is a `TextCell` object. 284 | 285 | `UnderlinedCell.prototype.minHeight` adds `1` to the `height` of the cell because of dashes under headers that take one extra line more than regular `TextCell` cells. 286 | ``` 287 | name height country 288 | ------------ ------ ------------- 289 | ``` 290 | 291 | Also for `draw` method we should consider that extra line. We send `height - 1` to `TextCell.prototype.draw` (because we don't want to add a bare empty line with the second evaluation of its loop) but we `concate` the returned array from `TextCell.prototype.draw` (remember `draw` returns an array) with `[repeat("-", width)]`. 292 | 293 | Let's imagine we want to draw `name` cell. It's in the first column. The widest cell in the first column is `"popocatepetl"` with the `length` of `12` therefore `width` is `12`. And height is `2` because `UnderlinedCell.prototype.minHeight` returns `2`. With the help of `[repeat("-", 12)]` we get `['------------']`. 294 | 295 | After concatenation, what actually `UnderlinedCell.prototype.draw` returns for `name` cell is this: 296 | 297 | ``` javascript 298 | [ 'name ', '------------' ] 299 | ``` 300 | 301 | Let's get back to our `dataTable` function. 302 | 303 | ```js 304 | function dataTable(data) { 305 | var keys = Object.keys(data[0]); 306 | var headers = keys.map(function(name) { 307 | return new UnderlinedCell(new TextCell(name)); 308 | }); 309 | 310 | var body = data.map(function(row) { 311 | return keys.map(function(name) { 312 | // keys = [ "name", "height", "country"] 313 | var value = row[name]; 314 | if (typeof value == "number") 315 | return new RTextCell(String(value)); 316 | else 317 | return new TextCell(String(value)); 318 | }); 319 | }); 320 | 321 | return [headers].concat(body); 322 | } 323 | ``` 324 | It's time to examine how `body` of our table gets created. 325 | 326 | `body` is an array with 7 elements. Each element is an array (containing three `TextCell` objects) that makes one `row` of our table, something like: 327 | ``` 328 | [ 329 | TextCell { text: [ 'Kilimanjaro' ] }, 330 | TextCell { text: [ '5895' ] }, 331 | TextCell { text: [ 'Tanzania' ] } 332 | ] 333 | ``` 334 | For each invocation of outer `map`, `row` is something like this: `{name: "Kilimanjaro", height: 5895, country: "Tanzania"}`. 335 | 336 | For each `row` we `map` through our `keys` to get the `value` for each key. Then we inspect the `value` to see if it's a `"number"` or not. We do that because we want numbers to be aligned to the right of their cells. 337 | If `value` is a number we pass it to `RTextCell` constructor which is a slight variation of `TextCell` constructor. 338 | 339 | ``` javascript 340 | function RTextCell(text) { 341 | TextCell.call(this, text); 342 | } 343 | RTextCell.prototype = Object.create(TextCell.prototype); 344 | RTextCell.prototype.draw = function(width, height) { 345 | var result = []; 346 | for (var i = 0; i < height; i++) { 347 | var line = this.text[i] || ""; 348 | result.push(repeat(" ", width - line.length) + line); 349 | } 350 | return result; 351 | }; 352 | ``` 353 | 354 | It inherits everything from `TextCell` except for `draw`. And as you can see the only difference in `draw` method is this line: 355 | 356 | ``` javascript 357 | result.push(repeat(" ", width - line.length) + line); 358 | ``` 359 | It puts those extra spaces before `line` instead of after it. So the result is something like `[ '..5895' ]` (I used `.` instead of space because GitHub removes extra spaces). 360 | 361 | Getting back to our `dataTable` function, its last line is : 362 | 363 | ``` javascript 364 | return [headers].concat(body); 365 | ``` 366 | By concatenating `[headers]` with `body` we get an array of 8 elements. One element for our `headers` and the rest of them for `body`. Each element which is an array of three objects represents a `row` in the final table. 367 | 368 | Phew! We are finally done with `dataTable` function. Remember everything started with this line. 369 | 370 | ```js 371 | console.log(drawTable(dataTable(MOUNTAINS))); 372 | ``` 373 | 374 | We sent `MOUNTAINS` to `dataTable` and we saw that it returns an array with 8 elements, something like this: 375 | 376 | ``` 377 | [ [ UnderlinedCell { inner: [Object] }, 378 | UnderlinedCell { inner: [Object] }, 379 | UnderlinedCell { inner: [Object] } ], 380 | [ TextCell { text: [Object] }, 381 | TextCell { text: [Object] }, 382 | TextCell { text: [Object] } ], 383 | [ TextCell { text: [Object] }, 384 | TextCell { text: [Object] }, 385 | TextCell { text: [Object] } ], 386 | ...etc 387 | ``` 388 | This array, returned by `dataTable` is the argument of `drawTable` function. Let's see how this function looks like. 389 | 390 | ``` javascript 391 | function drawTable(rows) { 392 | var heights = rowHeights(rows); 393 | var widths = colWidths(rows); 394 | 395 | function drawLine(blocks, lineNo) { 396 | return blocks.map(function(block) { 397 | return block[lineNo]; 398 | }).join(" "); 399 | } 400 | 401 | function drawRow(row, rowNum) { 402 | var blocks = row.map(function(cell, colNum) { 403 | return cell.draw(widths[colNum], heights[rowNum]); 404 | }); 405 | return blocks[0].map(function(_, lineNo) { 406 | return drawLine(blocks, lineNo); 407 | }).join("\n"); 408 | } 409 | 410 | return rows.map(drawRow).join("\n"); 411 | } 412 | ``` 413 | 414 | Let's go through it line by line. 415 | 416 | ``` javascript 417 | var heights = rowHeights(rows); 418 | ``` 419 | 420 | This is the `rowHeights` function. 421 | 422 | ``` javascript 423 | function rowHeights(rows) { 424 | return rows.map(function(row) { 425 | return row.reduce(function(max, cell) { 426 | return Math.max(max, cell.minHeight()); 427 | }, 0); 428 | }); 429 | } 430 | ``` 431 | In order to find `height` of each `row` we use `rowHeights` function. It returns `[ 2, 1, 1, 1, 1, 1, 1, 1 ]`, an array of numbers. Each number represents `height` of one row. `height` of first row is `2` becuase it's the header which has a underline. (Remember `this.inner.minHeight() + 1`?) 432 | 433 | It uses `reduce` to go through all the cells of each `row` and asks each cell what is your `minHeight`? Then it returns max `height` of each row (remember there are three cells/objects in each `row`) to the `map` function and `map` produces an array of heights for all the `rows`. 434 | 435 | The second line of `drawTable` function is this 436 | 437 | ``` javascript 438 | var widths = colWidths(rows); 439 | ``` 440 | This is `colWidths` function. 441 | 442 | ``` javascript 443 | function colWidths(rows) { 444 | return rows[0].map(function(_, i) { 445 | return rows.reduce(function(max, row) { 446 | return Math.max(max, row[i].minWidth()); 447 | }, 0); 448 | }); 449 | } 450 | ``` 451 | In our table we have three columns. We need to know how wide each column should be in order to make straight columns. We give `width` of each column to `draw` method of our cells to tell them our many extra space they should add to themselves. To find out `width` of each column we use `colWidths` function. 452 | 453 | `colWidths` needs to know how many columns we have so it maps through `rows[0]` in order to use its index parameter `i`. And `i` of course gonna be `0`, `1`, `2` because we have three objects/cells in each `row`. Again we use `reduce` to go through all `rows` and check `width` of their `i` cell. At the end we have this `[ 12, 6, 13 ]` an array of three number. 454 | 455 | `12` for the first column because `"Popocatepetl".length` is `12`. 456 | 457 | `6` for the second column because `"height".length` is `6`. 458 | 459 | `13` for the the third column because `"United States".length` is `13`. 460 | 461 | Getting back to our `drawTable` function, it returns this 462 | 463 | ``` javascript 464 | return rows.map(drawRow).join("\n"); 465 | ``` 466 | It maps through `rows` and calls `drawRow` function for each `row`. Let's take a look at this function and see how it works. 467 | 468 | ``` javascript 469 | function drawRow(row, rowNum) { 470 | var blocks = row.map(function(cell, colNum) { 471 | return cell.draw(widths[colNum], heights[rowNum]); 472 | }); 473 | return blocks[0].map(function(_, lineNo) { 474 | return drawLine(blocks, lineNo); 475 | }).join("\n"); 476 | } 477 | ``` 478 | As you can see it takes a `row` (for each invocation) and its corresponding `rowNum`. 479 | For each `row`, `drawRow` returns a string. For example for the second `row` which is this 480 | 481 | ``` javascript 482 | [ 483 | TextCell { text: [ 'Kilimanjaro' ] }, 484 | TextCell { text: [ '5895' ] }, 485 | TextCell { text: [ 'Tanzania' ] } 486 | ] 487 | ``` 488 | 489 | It returns `"Kilimanjaro....5895.Tanzania....."`. I replaced spaces with `.` to make it visually clearer. 490 | 491 | `blocks` is an array of arrays. Each inner array will be a cell in the final table. 492 | For e.g if `rowNum == 0` (so it's our header) blocks is. 493 | 494 | ``` javascript 495 | [ 496 | [ 'name ', '------------' ], 497 | [ 'height', '------' ], 498 | [ 'country ', '-------------' ] 499 | ] 500 | ``` 501 | Each inner array has two element because height of first `row` is 2 (`heights[0] --> 2`). 502 | 503 | Or for `rowNum == 1`, `blocks` is `[ [ 'Kilimanjaro.' ], [ '..5895' ], [ 'Tanzania.....' ] ]`. (I replaced spaces with `.`) 504 | 505 | `blocks` is made with the help of `map`. We go through all the cells in each `row` and in that we use `draw` method on each cell object to ask them to draw themselves. We pass height of current `row` and `width` of corresponding column for each cell to `draw` method. And `draw` in turn returns an array for each cell, something like `[ 'Kilimanjaro ' ]`; 506 | 507 | Now that we know what is `blocks` we can go through second part of `drawRow` function. 508 | 509 | ``` javascript 510 | return blocks[0].map(function(_, lineNo) { 511 | return drawLine(blocks, lineNo); 512 | }).join("\n"); 513 | ``` 514 | When `rowNum` is `0` (so we are working on header) `blocks[0]` is `[ 'name ', '------------' ]` so `lineNo` will be `0` and `1` other than that `lineNo` is always `0` because `blocks[0]` will be something like `[ 'Kilimanjaro ' ]` which has only one element. 515 | We pass whole `blocks` array to the `drawLine` function. `drawLine` is a helper function that is local of `drawTable` function. 516 | 517 | ``` javascript 518 | function drawLine(blocks, lineNo) { 519 | return blocks.map(function(block) { 520 | return block[lineNo]; 521 | }).join(" "); 522 | } 523 | ``` 524 | All it does is making a string out of an array of arrays. It does this in two step. First it flattens the `blocks`. For example for a `blocks` like: 525 | 526 | ``` javascript 527 | [ [ 'Kilimanjaro ' ], [ ' 5895' ], [ 'Tanzania ' ] ] 528 | ``` 529 | It makes 530 | 531 | ``` javascript 532 | [ 'Kilimanjaro ', ' 5895', 'Tanzania ' ] 533 | ``` 534 | And then it uses `join(" ")` to make a string by joining all the elements of this array with one space in between. 535 | At the end `drawLine` returns `"Kilimanjaro....5895.Tanzania....."`, again I replaced spaces with `.`. 536 | 537 | When we are working on header `drawLine` will be invoked twice. Once with `lineNo` of `0` another with `lineNo` of `1`. 538 | In turn, two separate line will be generated. One for headers (that is `name`, `height` and `country`) and another for those dashes. 539 | 540 | Now that we know what `drawLine` does we can continue with `drawRow` function. 541 | 542 | ``` javascript 543 | return blocks[0].map(function(_, lineNo) { 544 | return drawLine(blocks, lineNo); 545 | }).join("\n"); 546 | ``` 547 | Actually, there's not much to continue. `drawLine` returns a string to the `map` and `map` returns an array. This array has one or two elements depend on the `row` that we are working on. If it's the header it has two elements. 548 | 549 | ``` javascript 550 | [ 'name height country ', 551 | '------------ ------ -------------' ] 552 | ``` 553 | And for all the other rows it has one element something like this: 554 | 555 | ``` javascript 556 | [ 'Kilimanjaro 5895 Tanzania ' ] 557 | ``` 558 | Notice it's still an array but what we want is a string. So again we use `join("\n")` but this time with line feed as its separator. 559 | So to make it more clear, this is what we get for header (first `row`): 560 | 561 | ``` 562 | "name height country \n------------ ------ -------------" 563 | ``` 564 | and for the second `row` we get: 565 | 566 | ``` 567 | "Kilimanjaro 5895 Tanzania " 568 | ``` 569 | This process will be repeated for each `row`, remember `drawRow` is in a `map` method. 570 | 571 | ``` javascript 572 | return rows.map(drawRow).join("\n"); 573 | ``` 574 | This is the array that `rows.map(drawRow)` returns 575 | 576 | ``` javascript 577 | [ 'name height country \n------------ ------ -------------', 578 | 'Kilimanjaro 5895 Tanzania ', 579 | 'Everest 8848 Nepal ', 580 | 'Mount Fuji 3776 Japan ', 581 | 'Mont Blanc 4808 Italy/France ', 582 | 'Vaalserberg 323 Netherlands ', 583 | 'Denali 6168 United States', 584 | 'Popocatepetl 5465 Mexico ' ] 585 | ``` 586 | And for the last time we use `join("\n")` to make a string out of this array. And that's it, we got our table. This is the string that `console.log` logs. 587 | 588 | ``` javascript 589 | console.log(drawTable(dataTable(MOUNTAINS))); 590 | ``` 591 | 592 | ``` 593 | name height country 594 | ------------ ------ ------------- 595 | Kilimanjaro 5895 Tanzania 596 | Everest 8848 Nepal 597 | Mount Fuji 3776 Japan 598 | Mont Blanc 4808 Italy/France 599 | Vaalserberg 323 Netherlands 600 | Denali 6168 United States 601 | Popocatepetl 5465 Mexico 602 | ``` 603 | --------------------------------------------------------------------------------