Make it to make it

いろいろ作ってアウトプットするブログ

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周り

過去にまとめた記事とgithubリポジトリを参照のこと。

mktmkt.hatenablog.com

github.com

プロトタイプチェーンと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をするコンポーネントを自作できる。

tylermcginnis.com

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 () {}