Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation on moving off of Underscore to ES6/7 #28

Merged
merged 7 commits into from
Oct 29, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion style/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
* [Avoid href="#" for JavaScript triggers](#avoid-href-for-javascript-triggers)
* [Use modules, not global variables](#use-modules-not-global-variables)
* [ES6/7 rules](#es67-rules)
* [Use => instead of bind(this) ](#use--instead-of-bind)
* [Use => instead of bind(this) ](#use--instead-of-bindthis)
* [Use backticks for string interpolation](#use-backticks-for-string-interpolation)
* [Do not use ES6 classes for React classes](#do-not-use-es6-classes-for-react-classes)
* [Do not use async/await or generators](#do-not-use-asyncawait-or-generators)
* [Do not use Set or Map ](#do-not-use-set-or-map)
* [Use let and const for new files; do not use var ](#use-let-and-const-for-new-files-do-not-use-var)
* [Library rules](#library-rules)
* [Use $ for jQuery](#use--for-jquery)
* [Don't use Underscore](#dont-use-underscore)

----

Expand Down Expand Up @@ -552,3 +553,52 @@ Yes:
```js
$(".some-class span").hide();
```

#### Don't use Underscore

We use ES6/7 which includes many of the features of Underscore.js! Using Underscore should be avoided in favor of these native language features.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just popping in to add context, tweaks, & insights for some of the items in this PR (expect multiple comments).
Disclaimer: Behaviors may differ from Underscore to Lodash so I'll be speaking from the Lodash point of view for the most part. Take from the comments what you will, no pressure. [Update] Continued on #43 😎

We use ES6/7 which includes many of the features of Underscore.js! Using Underscore should be avoided in favor of these native language features.

Lodash has over 200 totally modular methods and Underscore has ~115 methods, which is far more than the handful of ES6/7 built-ins and constructs. The line above gives me the vibe that ES6/7 can full-on replace many/most of Underscore/Lodash which is not really the case. jQuery and Underscore have both had the you-may-not-need treatment so I figure you all, especially Resig, would be sensitive to that perception.

Lodash embraces, enhances, and works great combo'ed with ES6/7. If possible, I'd like to avoid perpetuating the sentiment that Lodash/Underscore and ES6/7 are somehow mutually exclusive or that Lodash/Underscore are somehow outdated / no-longer-useful. JavaScript will always have gaps that need filling and Lodash will continue to adapt and evolve to fill them.

More code tweaks / comments below...


There are a couple of methods that are sufficiently complicated and don't have a direct equivalent so instead we have a [custom-built](https://lodash.com/custom-builds) copy of [lodash](https://lodash.com/) containing only those specific methods. You can find this file at: `third_party/javascript-khansrc/lodash/lodash.js` along with instructions on how to build it and exactly what methods are included.

What follows is a method-by-method set of equivalents for what Underscore provides and what you could be using in ES6/7 instead:

Method | Use... | ...instead of
--------- | ------------------------------------- | ----------------------
bind | `fn.bind(someObj, args)` | `_.bind(fn, someObj, args)`
bind | `(a, b) => { ... }` <sup>[1](#u1)</sup> | `_.bind(function(a, b) { ... }, this)`
bindAll | `obj.method = obj.method.bind(someObj);` <sup>[2](#u2)</sup> | `_.bindAll(someObj, "method")`
clone | No alternative at the moment! <sup>[3](#u3)</sup> |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the spread operator, like we use to replace _.extend? Is {...x} equivalent to _.clone(x)?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matchu

What about the spread operator, like we use to replace _.extend? Is {...x} equivalent to _.clone(x)?

For the Underscore use of _.clone, which is shallow and restricted to arrays and objects, spread over an object may do for the object case. However, in Lodash _.clone handles arrays, array buffers, objects, regexes, dates, typed arrays, and in v4 maps / sets. In addition, Lodash supports deep cloning values with _.cloneDeep.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matchu I'm mostly concerned about the case of _.cloneDeep (or equivalent). Doing {...x} with any properties that are an object is going to cause very-unexpected problems. In this case I'd probably just recommend importing the method from lodash, if/when we do end up needing it.

debounce | Our custom lodash build. |
defer | `setTimeout(fn, 0);` | `_.defer(fn);`
delay | `setTimeout(fn, 2000);` | `_.delay(fn, 2000);`
each (array) | `array.forEach((val, i) => {})` | `_.each(array, (val, i) => {})`
each (array) | `for (const val of array) {}` | `_.each(array, fn)`
each (object) | `for (const [key, val] of Object.entries(obj)) {}` | `_.each(obj, fn)`
extend (new) | `{...options, prop: 1}` | `_.extend({}, options, {prop: 1})`
extend (assign) | `Object.assign(json, this.model.toJSON())` | `_.extend(json, this.model.toJSON())`
filter | `array.filter(checkFn)` | `_.filter(array, checkFn)`
has (array) | `array.includes(value)` | `_.has(array, value)`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has (array) | array.includes(value) | _.has(array, value)

The has (array) check should be _.includes(array, value).

has (object) | `obj.hasOwnProperty(value)` <sup>[4](#u4)</sup> | `_.has(obj, value)`
isArray | `Array.isArray(someObj)` | `_.isArray(someObj)`
isFunction | `typeof fn === "function"` | `_.isFunction(fn)`
isString | `typeof obj === "string"` | `_.isString(obj)`
keys | `Object.keys(obj)` | `_.keys(obj)`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keys | Object.keys(obj) | _.keys(obj)

_.keys(obj) also smooths over inconsistencies in environments handling strings, arguments objects, and arrays (treating them as dense). For example, in ES5 Object.keys('hi') would throw, while in ES6 Object.keys('hi') returns ['0','1'].

last | `someArray[someArray.length - 1]` <sup>[5](#u5)</sup> | `_.last(someArray)`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last | someArray[someArray.length - 1] 5 | _.last(someArray)

_.last is handy as it guards against nullish someArray values and avoids the length of 0 minus 1 lookup of -1.

map | `array.map(mapFn)` | `_.map(array, mapFn)`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map | array.map(mapFn) | _.map(array, mapFn)

One cool bit is that Lodash and Underscore allow callback shorthands.

Like _.map(array, 'id) instead of:

array.map(function(value) {
  return value != null && value.id;
});

and _.filter(array, { a:{ b:[3], c:true } }) instead of:

array.filter(array, function(value) {
  return value != null && value.a != null &&
    Array.isArray(value.a.b) && value.a.b.includes(3) && 
      value.a.c === true;
});

An added bonus is that Lodash enables shortcut fusion for its chaining syntax, so for example:

_(arr10k).map(square).filter(isOdd).slice(0, 4).value();
// => is 8 iterations

arr10k.map(square).filter(isOdd).slice(0, 4);
// => is 20,000 iterations

max | `Math.max(...array)` | `_.max(array)`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max | Math.max(...array) | _.max(array)

_.max and _.min avoid max arguments limits across environments and
allow computed extremum values by iteratee, e.g. _.max(objects, 'id')

object | <pre>Object.entries(obj).reduce(<br>(result, [key, val]) => {<br>&nbsp;&nbsp;&nbsp;&nbsp;result[key] = value;<br>&nbsp;&nbsp;&nbsp;&nbsp;return result;<br>})</pre> | <pre>\_.object(\_.map(obj, (val, key) => {<br>&nbsp;&nbsp;&nbsp;&nbsp;return [key, value];<br>})</pre>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_.object(_.map(obj, (val, key) => {
  return [key, value];
})

Lodash has _.mapValues and _.mapKeys for object mapping and Underscore has _.mapObject.
For this example though it looks like you're wanting to use _.pairs.

omit (array) | `array.filter(prop => !props.includes(prop))` | `_.omit(array, props)`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omit (array) | array.filter(prop => !props.includes(prop)) | _.omit(array, props)

While _.omit can be performed on an array that's not a common use case and doesn't align to the ES6 example.

If you want to remove elements from an array in Lodash/Underscore there's non-mutating methods like _.difference, _.without and mutating methods in Lodash like _.pull, _.pullAt, and _.remove.

omit (object) | <pre>Object.keys(obj).reduce((result, prop) => {<br>&nbsp;&nbsp;&nbsp;&nbsp;if (!props.includes(prop)) {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result[prop] = attrs[prop];<br>&nbsp;&nbsp;&nbsp;&nbsp;}<br>}, {})</pre> | `_.omit(obj, props)`
once | `$(...).one("click", ...)` | `$(...).on("click", _.once(...))`
once | <pre>{<br>&nbsp;&nbsp;&nbsp;&nbsp;method: () => {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (this._initDone) { return; }<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;this._initDone = true;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;...<br>&nbsp;&nbsp;&nbsp;&nbsp;}<br>}</pre>| `{ method: _.once(() => { ... }) }`</pre>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, the source code for this looks really messy, but the end result is really nice!

once | <pre>var getResult = () => {<br>&nbsp;&nbsp;&nbsp;&nbsp;let val = $.when(...).then(...);<br>&nbsp;&nbsp;&nbsp;&nbsp;getResult = () => val;<br>&nbsp;&nbsp;&nbsp;&nbsp;return val;<br>};</pre> | <pre>var getResult = _.once(() => {<br>&nbsp;&nbsp;&nbsp;&nbsp;return $.when(...).then(...);<br>});</pre>
sortBy | `result = result.sort((a, b) => a.prop - b.prop)` | `_.sortBy(result, "prop")`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sortBy | result = result.sort((a, b) => a.prop - b.prop) | _.sortBy(result, "prop")

The _.sortBy method in Lodash/Underscore performs a stable sort and handles nullish values. In contrast, Array#sort is not stable and has been known, in engines like V8, to produce different sort results for small arrays and large ones.

sortedIndex | Our custom lodash build. |
throttle | Our custom lodash build. |
values | `Object.values(obj)` | `_.values(obj)`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values | Object.values(obj) | _.values(obj)

Something to keep in mind is the shims provided by babel can weigh in heavier than Lodash (kitchen-sink) and, depending on the version, may be larger than jQuery itself.

For a different take you might also dig my talk on shims and libs.


1. To be used when you're creating a function and immediately binding its context to `this`. <b id="u1"></b>
2. Or use a loop if binding multiple methods. <b id="u2"></b>
3. No alternative at the moment! If you need it then you should add it to the compiled version of lodash and then update this guide to mention that it now exists! <b id="u3"></b>
4. While we recommend using `obj.hasOwnProperty(prop)` it is possible that the object could have a method named `hasOwnProperty` that does something else, causing this call to break. The likelihood of this happening is extremely slim - but if you're developing something that you wish to work absolutely everywhere you may want to do something like `Object.prototype.hasOwnProperty.call(obj, prop)`. <b id="u4"></b>
5. If you don't care about destructively modifying the array, you can also use `someArray.pop()``. <b id="u5"></b>