2020年を迎えてのJavaScript Tips総ざらい
変数定義(var
/ let
/ const
)
var
使うことはもうほとんどないし、今さらだが、重要なポイント。
var: function scoped undefined when accessing a variable before it's declared let: block scoped ReferenceError when accessing a variable before it's declared const: block scoped ReferenceError when accessing a variable before it's declared can't be reassigned
分割代入(Object / Array destructuring)
リネームして分割代入することは有用。
const { f: firstName, l: lastName } = user; console.log(firstName) console.log(lastName)
React RouterのLinkコンポーネントにも使われている書き方。
render () { const { component: Component, to , replace, ...rest } = this.props return <Component {...rest} onPress={this.handlePress}/> }
ファンクションの引数に値を渡す際に、オブジェクト形式で渡す例。 引数が多い場合などは、順番にとらわれないし、渡す際にもよりリーダブルな書き方である。 ついでにデフォルト値なども設定しておくと便利。
function fetchResources ({ srcUrl, minStars = 0, maxStars = 5, createdDate }) { // (...snip) } fetchResources({ srcUrl: 'https://example.com/', minStars: 3, createdDate: new Date('01/01/2020').getTime(), });
Promise.all()
を使う際にも用いることができる。
function getUserData (user) { return Promise.all([ getProfile(user), getRepos(user) ]).then(([profile, repos]) => ({ profile, repos })) }
ここで欲しい機能として、必須の引数に対してはどのように処理するかである。 一つの対処法としては、requiredを判断するファンクションを別に用意することだ。
function isRequired (arg) { throw new Error(`${arg} is required`) } function logUserProfile ({ name = isRequired('name'), job = isRequired('job') }) { console.log(`${name}'s job is a ${job}`) } logUserProfile({ name: 'Sungjoon', job: 'Web developer' })
この例の場合は、jobの引数を渡さないとjob is required
というメッセージが出る。
ショートハンドプロパティとメソッド名(Shorthand properties / Method names)
ファクトリーファンクションのような形式で書く場合など。
function formatMessage (name, id, avatar) { return { name, id, avatar, timestamp: Date.now(), save () { // (...snip) } } }
コンピューティッドプロパティ名(Computed property names)
オブジェクトのキー・バリューを動的に生成する必要がある場合など。
function objectify (key, value) { return { [key]: value } } objectify('name', 'Park') // { name: 'Park' }
テンプレートリテラル(Template literals)
シングルコロンやダブルコロンも使える。
const year = 2020 const word = 'cool' const greetings = ` Happy new year ${year}! This year's gonna be "${word}" year!! ` console.log(greetings) // Happy new year 2020! // This year's gonna be "cool" year!!
コンパイル・ポリフィルどちらになるか?
ESNextの機能を使用しているとき、Babelを通すともちろんES5に対応するようにコンパイルが行われるが、fetch
などは新しく追加された機能(API)であることから、コンパイルは行われない。こういった機能に対してポリフィルを行う必要がある。
コンパイルされるもの
Arrow functions Async functions Async generator functions Block scoping Block scoped functions Classes Class properties Computed property names Constants Decorators Default parameters Destructuring Do expressions Exponentiation operator For-of Function bind Generators Modules Module export extensions New literals Object rest/spread Property method assignment Property name shorthand Rest parameters Spread Sticky regex Template literals Trailing function commas Type annotations Unicode regex
ポリフィルが必要なもの
ArrayBuffer Array.from Array.of Array#copyWithin Array#fill Array#find Array#findIndex Function#name Map Math.acosh Math.hypot Math.imul Number.isNaN Number.isInteger Object.assign Object.getOwnPropertyDescriptors Object.is Object.entries Object.values Object.setPrototypeOf Promise Reflect RegExp#flags Set String#codePointAt String#endsWith String.fromCodePoint String#includes String.raw String#repeat String#startsWith String#padStart String#padEnd Symbol WeakMap WeakSet
callback, promise, async/await周り
プロトタイプチェーンとES6クラス
Classはプロトタイプを用いたいわゆるPseudoclassical Instantiation
の糖衣構文だということ。
class Animal { constructor(name, energy) { this.name = name; this.energy = energy; } eat(amount) { console.log(`${this.name} is eating.`); this.energy += amount; } sleep(length) { console.log(`${this.name} is sleeping.`); this.energy += length; } play(length) { console.log(`${this.name} is playing.`); this.energy -= length; } static nextToEat(animals) { const sortedByLeastEnergy = animals.sort((a, b) => a.energy - b.energy); return sortedByLeastEnergy[0].name; } }
結局のところはES5である以下の書き方と同じもの。
function Animal(name, energy) { this.name = name; this.energy = energy; } Animal.prototype.eat = function(amount) { console.log(`${this.name} is eating.`); this.energy += amount; }; Animal.prototype.sleep = function(length) { console.log(`${this.name} is sleeping.`); this.energy += length; }; Animal.prototype.play = function(length) { console.log(`${this.name} is playing.`); this.energy -= length; }; Animal.nextToEat = function(animals) { const sortedByLeastEnergy = animals.sort((a, b) => a.energy - b.energy); return sortedByLeastEnergy[0].name; };
インスタンスの作成
new
キーワードで行う。implicitな処理が行われる。
const leo = new Animal('Leo', 7);
new
することによってfunction prototypeの中にthis
という空オブジェクトを作成し、それをリターンするということを裏で行っている。
for-in
文は副作用を生む
プロトタイプチェーンとES6クラスで述べた例に被せて、次のような構文を実行してみる。
const leo = new Animal('Leo', 7); for (const key in leo) { console.log(`Key: ${key}. Value: ${leo[key]}`); }
Class構文での実行結果
Key: name. Value: Leo Key: energy. Value: 7
Prototypeを利用した構文での実行結果
Key: name. Value: Leo Key: energy. Value: 7 // 副産物 Key: eat. Value: function (amount) { console.log(`${this.name} is eating.`); this.energy += amount; } Key: sleep. Value: function (length) { console.log(`${this.name} is sleeping.`); this.energy += length; } Key: play. Value: function (length) { console.log(`${this.name} is playing.`); this.energy -= length; }
この理由は、for-in文が列挙可能なプロパティ(enumerable property)の中身、すなわち継承先のprototypeの中身までイタレートしてしまうから。
回避するために、obj.hasOwnProperty
メソッドを用いることで防げる。
これによって、prototypeに内にあるプロパティのみに絞ることができる。
for (const key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo[key]}`); } }
もっとも、for-in文自体使わない方がよいが。
ちなみに、for-in文はESLintなどで使用禁止(guard-for-in
, no-restricted-syntax
)にできる。
モジュール化とCommonJS / ESモジュール
モジュール化のメリット
- リユーザビリティ(Reusability)
- コードをブロックとして使い回せる
- コンポーザビリティ(Composability)
- パーツを構成して一つのプロダクトを作る要領
- レバレッジ(Levarage)
- パーツさえ揃っていればどこでどう作られるかは関係ない
- アイソレーション(Isolation)
- 独立性を保って各パーツを分けて扱うことで、互いの依存関係やボトルネックを作らない
- オーガニゼーション(Organization)
- 各パーツが明確な境界を保つことで、組織が成り立つ
ESモジュールでのimport
import defaultExport from "module-name"; // default import import * as name from "module-name"; // universal import import { export } from "module-name"; // named import import { export as alias } from "module-name"; import { export1 , export2 } from "module-name"; import { export1 , export2 as alias2 , [...] } from "module-name"; import defaultExport, { export [ , [...] ] } from "module-name"; import defaultExport, * as name from "module-name"; import "module-name"; var promise = import(module-name);
Tree shaking
CommonJSでは、モジュールをファイル内のどこからでも、コンディショナルに読み込みできるが、ESモジュールではファイル最上部にimport文を書くこと以外を許容しない。
CommonJSの例
- import時は
module.exports = ...
- export時は
require(...)
if (pastTheFold === true) { require('./parallax') }
ESモジュールの例
- import時は
import
- export時は
export
ファイル上部以外でimportしようとすると、下記のエラーが出る。
(ESLintのimport/first
ルールで検知可能)
import' and 'export' may only appear at the top level
例外的に、ESモジュールでもダイナミックimportを可能にするproposalがある(現在ステージ3まできている)。
React + React Routerなどで使用する場合、ダイナミックimportをするコンポーネントを自作できる。
ReactのpropTypesやdefault propsの書き方、メソッドのバインド方法
Reactでの書き方だが、propTypesやdefault propsはclass内にまとめて書いてしまった方が簡潔で見やすい。
Class内に閉じ込めて書く
import React, { Component } from 'react'; import PropTypes from 'prop-types'; class UserInput extends Component { static propTypes = { username: PropTypes.string, }; static defaultProps = { username: 'John Doe', }; constructor(props) { super(props); const { username } = props; this.state = { username, }; this.handleChange = this.handleChange.bind(this); } handleChange(event) { this.setState({ username: event.target.value, }); } render() { const { username } = this.state; return ( <div> <h2>{username}</h2> </div> ); } } export default UserInput;
メソッドのバインドの仕方によるコンパイルの違い
class Animal { eat() {} sleep = () => {} } // Compiled to function Animal () { this.sleep = function () {} } Animal.prototype.eat = function () {}