Try T.M Engineer Blog

多摩市で生息するエンジニアが「アウトプットする事は大事だ」と思って始めたブログ

Jestについて勉強してみた

最近、Jestを使ってテストコードを頻繁に書き始めたので、自身の使い方整理のため、以下雑にメモを残しておく。

test('null', () => {
  const n = null;
  expect(n).toBeNull(); // null一致
  expect(n).toBeDefined(); // undefined以外一致
  expect(n).not.toBeUndefined(); // undefinedではない
  expect(n).not.toBeTruthy(); // Truthyではない
  expect(n).toBeFalsy(); // Falsyである
});

test('0', () => {
  const z = 0;
  expect(z).not.toBeNull(); // nullではない
  expect(z).toBeDefined(); // undefined以外一致
  expect(z).not.toBeUndefined(); // undefinedではない
  expect(z).not.toBeTruthy(); // Truthyではない
  expect(z).toBeFalsy(); // Falsyである
});

test('数値', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3); // value > 3
  expect(value).toBeGreaterThanOrEqual(3.5); // value >= 3.5
  expect(value).toBeLessThan(5); // value < 5
  expect(value).toBeLessThanOrEqual(4.5); // value <= 4.5
  expect(value).toBe(4); // value = 4
  expect(value).toEqual(4); // value = 4
});

test('浮動小数点', () => {
  const value = 0.1 - 0.2;
  //expect(value).toBe(-0.1);         このように書くと、丸め込み誤差が原因で期待通りに動作しない
  expect(value).toBeCloseTo(-0.1); // これならば正しく動く
});

test('正規表現', () => {
  expect('team').not.toMatch(/I/); // 正規表現に一致しない
  expect('Christoph').toMatch(/stop/); // 正規表現に一致
});

test('配列', () => {
  const shoppingList = [
    'diapers',
    'kleenex',
    'trash bags',
    'paper towels',
    'milk',
  ];
  
  expect(shoppingList).toContain('milk'); // 配列に含まれているか
  expect(new Set(shoppingList)).toContain('milk'); // Setに含まれているか
});

test('例外', () => {
  function compileAndroidCode() {
    throw new Error('you are using the wrong JDK');
  }

  expect(() => compileAndroidCode()).toThrow(); // throwされているか
  expect(() => compileAndroidCode()).toThrow(Error); // throwのErrorが呼ばれているか
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK'); // messageの一致
  expect(() => compileAndroidCode()).toThrow(/JDK/); // 正規表現も可
});

test('予定', () => {
  test.todo('テストの予定を立てれる')
})

test('モック', () => {
  // モック関数を作る
  const mockFn = jest.fn()
  mockFn
    .mockImplementationOnce( () => {
      return '返り値1'
    })
    .mockImplementationOnce( () => {
      return '返り値2'
    })
    .mockImplementationOnce( () => {
      return '返り値3'
    })

  expect(mockFn()).toBe('返り値1') // 1回目の呼び出し
  expect(mockFn()).toBe('返り値2') // 2回目の呼び出し
  expect(mockFn()).toBe('返り値3') // 3回目の呼び出し

  // モックインスタンスを作る
  const mockInsFn = jest.fn()
  mockInsFn
    .mockImplementation( () => {
      return {
        hoge: jest.fn()
          .mockImplementationOnce( () => {
            return 'hoge関数の返り値1'
          })
          .mockImplementationOnce( () => {
            return 'hoge関数の返り値2'
          }),
        fuga: () => {
          return 'fuga関数の返り値'
        }
      }
    })

  const mockIns = new mockInsFn()
  expect(mockIns.hoge()).toBe('hoge関数の返り値1') // hoge関数の1回目の呼び出し
  expect(mockIns.hoge()).toBe('hoge関数の返り値2') // hoge関数の2回目の呼び出し
  expect(mockIns.fuga()).toBe('fuga関数の返り値') // fuga関数の呼び出し
})

// CDKを呼び出す上で必要なものを定義
import { SynthUtils } from '@aws-cdk/assert'
import * as cdk from '@aws-cdk/core'
import { SampleStack } from '../src/deploy/sampleStack'

test('スナップショット', () => {
  const app = new cdk.App()
  const stack = new SampleStack(app, `SampleStack`, {
    stage: 'dev',
    roleName: 'sampleRole'
  })
  expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot()
})

/*
// スナップショットの結果
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`スナップショット 1`] = `
Object {
  "Resources": Object {
    "sampleRole5138FD4B": Object {
      "Properties": Object {
        "AssumeRolePolicyDocument": Object {
          "Statement": Array [
            Object {
              "Action": "sts:AssumeRole",
              "Effect": "Allow",
              "Principal": Object {
                "Service": "lambda.amazonaws.com",
              },
            },
          ],
          "Version": "2012-10-17",
        },
        "ManagedPolicyArns": Array [
          Object {
            "Fn::Join": Array [
              "",
              Array [
                "arn:",
                Object {
                  "Ref": "AWS::Partition",
                },
                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
              ],
            ],
          },
        ],
        "RoleName": "sampleRole",
      },
      "Type": "AWS::IAM::Role",
    },
  },
}
`;
*/

感想

Jest は使ってみた感じ、むちゃくちゃ便利だ。

とにかくモックが簡単で、関数やインスタンスが作りやすい印象を受けた。

スナップショットも取得することができ、イマドキ感も感じる。

試しに、CDKのCloudFormationテンプレートのスナップショットを取得してみたが、これは使えそうだ。個人的には、CloudFromationのテンプレートテストはそこまで必要性を感じていないのだが、スナップショット残しておくだけでも、差分確認ができるという点で便利だと思えた。

まだまだ使い慣れていないので、ドキュメントを読みつつ、さらなる使い方を学んでいきたい。

Node.js Streamを勉強してみた

Node.jsを理解していくにあたり、Streamの理解は必須になってくるので、Streamのドキュメントを(まずは、さらっと理解程度に)読んでみた。

Streamとは?

Node.jsでストリーミングデータを操作するためのインターフェースのこと。インターフェースなので、ストリームという実装があるわけではなく、概念が決まっているだけ。

その概念は色々なもの( fs, process.stdin/stdout/stderrなど, HTTP request on the server/response on the client)で使用される。

ストリームには、以下4種類のインターフェースが定義されている。

  • Writable: データを書き込むことができるストリーム
  • Readable: データを読み取ることができるストリーム
  • Duplex: ReadableとWritableの両方ができるストリーム
  • Transform: Duplexデータ読み込みおよび書き込み時に、データの変更、変換ができるストリーム

オブジェクトモード

Node.js APIによって作成されたすべてのストリームは、文字列または、Bufferオブジェクトでのみ動作する。

バッファリング

WritableとReadable共にデータを内部バッファに保存する仕組みを持つ。

理由としては、一度に大量のデータを読み込み、または書き込みを行わないために、予め閾値(highWaterMark)を決めており、その分のデータを内部バッファに保存するためだそう。

注意点として、highWaterMarkは閾値であり、制限ではないとのこと。「一般に、厳密なメモリ制限は適用されません」とのこと。

EventEmitter

ストリームは、EventEmitterを継承している。EventEmitterとはイベント駆動でプログラムを動かすライブラリのこと。

たとえば以下の場合、 .onで、イベント名と実行内容を定義して、 .emit(イベント名)でイベントを発火させて実行する。

import EventEmitter from 'events'
const event = new EventEmitter();

console.log('1')
event.on('event', () => { // `.on`でイベントと実行内容を定義
    console.log('2')
})
console.log('3')

event.emit('event') // `.emit`でイベントを発火。結果は、132となる

ストリームもこのEventEmitterを継承しているため、動かし方も上記のような方法になる。

import fs from "fs"

const readStream = fs.createReadStream("assets/sample1.txt", { encoding: "utf-8" })
readStream.on("data", chunk => console.log(chunk)); // `.on`でイベント名と実行内容を定義

ストリームの種類とイベント・API

Writable: 書き込み

用意されているイベント: close, drain, error, finish, pipe, unpipe

Readable: 読み込み

用意されているイベント: close, data, end, error, pause, readable, resume

Duplex & Transform: 書き込み、読み込み、変換

用意されているイベント: WritableとReadableの両方

stream API

ストリームを実装できるように、同期版APIが用意されている。

stream/promises API

ストリームを実装できるように、非同期版APIが用意されている。(ぇ、そうなの?)

感覚を掴んでみる

Node.jsの Fyle System (fs) モジュールは、Streamの仕組みが使われている。
createReadStreamcreateWriteStreamを使えば、Streamを生成できるので、 createReadStream を使ってStreamの感覚を掴んでみることにする。

import * as fs from "fs"

// こうすることで、28byteずつ読み込む
const readStream = fs.createReadStream("assets/sample1.txt", {
  encoding: "utf-8",
  highWaterMark: 28
});

// 28byteずつ"data"イベントが発火する
readStream.on("data", chunk => console.log(chunk))
// すべての読み込みが完了したときにイベントが発火する
readStream.on("end", () => console.log('すべての読み込みが完了しました。'))

Streamについて、調べていると以下のような事を仰っている記事に辿り着いた。

qiita.com

Promise におけるフロー制御に Stream を導入することが難しい

なるほど。これを読むと、たしかにStreamとPromiseの相性の悪さがわかります。

"data"イベントを使用するのではなくfor awaitを使って制御するのが良さそうですね。以後、参考にさせていただきますm(= =)m

import * as fs from "fs"

// こうすることで、28byteずつ読み込む
const readStream = fs.createReadStream("assets/sample1.txt", {
  encoding: "utf-8",
  highWaterMark: 28
});

// すべての読み込みが完了したときにイベントが発火する
readStream.on("end", () => console.log('すべての読み込みが完了しました。'))

// async/awaitを使用する
async function main() {
  for await ( const chunk of readStream ) {
    console.log(chunk)
  }  
}

main()

まとめ

というわけで、今回は以下を学んだ。

そもそもStreamを学びたいと思った理由は、AWS S3のファイルを取得した際、型がStreamだったので「やべぇ・・・わからん!」となったのがキッカケ。 とりあえず、それを解決する程度には理解できたと思う。。。

  • Streamはインターフェースであり、Node.jsの様々なモジュールで使用されている。
  • StreamはEventEmitterであり、イベント駆動型でPromiseでのフロー制御には気をつけること。

AWS CDKを使ってSwagger + API Gateway + Lambdaを作る話

最近、AWS CDKに挑戦している。

正直、最初はCDKを使うメリットをあまり感じられなかったのだが、慣れてくるとCDKを使うほうがテンプレートの管理がしやすく、書くスピードも上がって効率が良くなっていることが実感できる。

後から気づいた事なのだが、CDKにはCfnテンプレートの内容をそのままコードで書けるCfnXXX関数が用意されている。

そのため「うわっ、これ書くの難しそう・・・」と思ったら、CfnXXX関数を使うのが良さそうだ。

CDKを使うと試してみたくなるのが、Swagger + API Gateway + Lambdaのパターン。

今までのCfnテンプレートでSwaggerを使うとなると、どうしても手作業のコピー&ペーストが必要だった。しかし、CDKを使うとimportで読み取ることができるので、コピー&ペーストが不要。

というわけで、さっそくやってみる。

Swaggerファイル )を用意する。

{
  "openapi": "3.0.1",
  "info": {
    "title": "Swagger Sample",
    "description": "Swagger Sample",
    "version": "1.0.0"
  },
  "paths": {
    "/request": {
      "get": {
        "operationId": "testGetOperation",
        "responses": {
          "200": {
            "description": "successful.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "x-amazon-apigateway-integration": {
          "type": "AWS_PROXY",
          "httpMethod": "POST",
          "uri": "あとで上書きする",
          "connectionType": "INTERNET",
          "payloadFormatVersion": "2.0",
          "timeoutInMillis": 30000
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Body"
              }
            }
          }
        },
        "x-codegen-request-body-name": "body"
      }
    }
  },
  "components": {
    "schemas": {
      "Body": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          }
        }
      }
    }
  }
}

というわけで、さっそくStackの中身を書いていこう。

import * as cdk from '@aws-cdk/core'
import * as httpApi from '@aws-cdk/aws-apigatewayv2'
import * as iam from '@aws-cdk/aws-iam'
import * as logs from '@aws-cdk/aws-logs'
import * as lambda from '@aws-cdk/aws-lambda'
import openapiJson from '../swagger/openapi.json'

export class ApigwSample extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const stack = cdk.Stack.of(this)

    // Lambdaの定義
    const rolelambda = new iam.Role(this, 'lambdaSampleRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'service-role/AWSLambdaBasicExecutionRole'
        ),
      ],
      path: '/lambda/',
    })

    const lambdaSampleFunction = new lambda.Function(this, 'lambdaSampleFunction', {
      runtime: lambda.Runtime.NODEJS_14_X,
      code: lambda.Code.fromAsset('lambda/functions'),
      functionName: 'lambdaSampleFunction',
      handler: 'index.lambdaHandler',
      environment: {
        TZ: 'Asia/Tokyo',
      },
      role: rolelambda
    })

    const lambdaFunctionCurrentVersionAlias = new lambda.Alias(
      this,
      'lambdaSampleAlias',
      {
        aliasName: 'dev',
        version: lambdaSampleFunction.currentVersion,
      }
    )
    lambdaFunctionCurrentVersionAlias.addPermission(
      'lambdaSampleCurrentAlias',
      {
        principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
        action: 'lambda:InvokeFunction',
        sourceArn: `arn:aws:execute-api:${stack.region}:${stack.account}:*/*/*/request`
      }
    )

    new logs.LogGroup(this, 'lambdaSampleLogGroup', {
      logGroupName: '/aws/lambda/' + lambdaSampleFunction.functionName,
      retention: logs.RetentionDays.ONE_DAY,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    })

    // APIGateway(HttpAPI)の定義
    const apigwSampleHttp = new httpApi.HttpApi(this, 'apigwSampleHttp', {
      apiName: 'ApigwSample',
      createDefaultStage: false,
    })
    const apigwSampleHttpNode = apigwSampleHttp.node.findChild('Resource') as httpApi.CfnApi
    apigwSampleHttpNode.body = openapiJson
    delete apigwSampleHttpNode.name
    delete apigwSampleHttpNode.protocolType

    new httpApi.HttpStage(this, 'apigwSampleHttpStage', {
      httpApi: apigwSampleHttp,
      autoDeploy: true,
      stageName: 'dev',
    })

        // 上記で作成したlambdaのArnを設定する必要があるため、openapi.jsonを上書きする
    openapiJson['paths']['/request']['get']['x-amazon-apigateway-integration'][
      'uri'
    ] = `arn:aws:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${lambdaFunctionCurrentVersionAlias.functionArn}/invocations`

    new cdk.CfnOutput(this, 'apigwSampleHttpUrl', {
      value: apigwSampleHttp.apiEndpoint
    })
  }
}

TypeScriptで書いているけど、型推論してくれるので、自分で型を定義することはほとんどありません。

APIGatewayを作成する時、Swaggerを使用すると、Swagger内にAPIGatewayの設定内容がほとんど書かれているため、Cfnのテンプレートを書く時にはbodyにSwaggerの内容を転記する必要があった。

なので、CDKではhttpApi.CfnApiを使用してbodyに直接をSwaggerの内容を設定する方がすっきり書けそう・・・
今回はhttpApi.HttpApiを使用してみたのだが、 自動生成されてしまうnameprotocolTypeを生成後に削除する必要があった。

あとは、x-amazon-apigateway-integration。さすがにArnの情報まではSwagger内に書けないので、Lambda定義後に上書きが必要。

あとは、簡単にStackを書けば、CDKからCfnのテンプレートを作るところまでが完成。

import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { ApigwSample } from '../lib/apigw-sample';

const app = new cdk.App();
new ApigwSample(app, 'CdkSampleStack');

次は、Lambdaを書いていきます。

CDKに合わせてLambdaもTypeScriptで書いていくことにします。

import { APIGatewayProxyEventV2 } from 'aws-lambda'

export interface LambdaHandlerResult {
  StatusCode: number,
  Message: string
}

export async function lambdaHandler(event: APIGatewayProxyEventV2): Promise<LambdaHandlerResult> {
  return {
    StatusCode: 200,
    Message: 'Successfully'
  }
}

「ビルドどうしよう・・・」と悩んでいたら、クラメソさんのココの記事を参考にさせていただきました。

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'development',
  target: 'node',
  entry: {
    functions: path.resolve(
      __dirname,
      './lambda/functions/index.ts',
    ),
  },
  // 依存ライブラリをデプロイ対象とするか設定(対象はpackage.json参照)
  // devDependencies:開発時に必要なライブラリを入れる
  // dependencies:実行時に必要なライブラリを入れる
  externals: [
    nodeExternals({
      modulesFromFile: {
        exclude: ['dependencies'],
        include: ['devDependencies'],
      },
    }),
  ],
  output: {
    filename: '[name]/index.js',
    path: path.resolve(__dirname, './lambda'),
    libraryTarget: 'commonjs2',
  },
  // 変換後ソースと変換前ソースの関連付け
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        // ローダーが処理対象とするファイルを設定
        test: /\.ts$/,
        exclude: /node_modules/,
        // 先ほど追加したts-loaderを設定
        use: [
          {
            loader: 'ts-loader',
          },
        ],
      },
    ],
  },
  // import時のファイル指定で拡張子を外す
  // https://webpack.js.org/configuration/module/#ruleresolve
  resolve: {
    extensions: ['.ts', '.js'],
  },
};

また、クラメソさんの別記事で、aws-lambda-nodejsというモジュールが追加されたというのがあり、最初はこれを使おうかな・・・と思ったのですが、まだベータ版なので、今回は見送りました。

AWSのリソースをTypeScriptを使って全部書けると意外と楽だったので、Now!!でJavaScript/TypeScriptを勉強している身としては、CDKやLambdaをどんどん書いていこう!という気持ちになれました。

いやぁ・・・CDKすごいっ!

AWS CDKに挑戦する話

最近、JavaScript / TypeScriptの学習に取り組んでいるので、これを機会にCloud Formationのテンプレートを書く方法もYAMLからAWS CDKに移行しようと思う。

AWS CDKを学ぶにあたり、AWS CDK Workshopをオススメされたので、やってみた。

AWS CDK Workshopでは、最初はAPI Gateway + Lambdaの簡単なところから、中盤にテスト、後半はCodePiplineを使ったCDを学ぶことができる。

今までYAMLで書いていたところを、if文クラス関数を使って書くことができるのは、新鮮に感じることができた。

今後は、可能なかぎりCloud Formationのテンプレートを書く時はAWS CDKを使って書くようにして、慣れていきたい。

あと、AWS CDKのデプロイ時に以下エラーで躓いたので、残しておく。

Unable to resolve AWS account to use. It must be either configured when you define your CDK or through the environment

このエラーメッセージはCDKの中では有名で、解決策としてawsconfigcredencialファイルを(空でも)作成しておくというのがある。
このエラーメッセージに出くわしたら、このissuesを参考にすること。

しかし、私の場合、上記の設定をしてもエラーを解決できなかった。 cdk doctorコマンドを使用して、潜在的な問題がないかチェックしたところ、どうやら環境変数AWS_CONFIG_FILEAWS_SHARED_CREDENTIALS_FILEが 設定されているとデプロイできなくなる模様。
こちらの環境変数を削除したら、無事デプロイできるようになった。(めでたし、めでたし)

JavaScript Primerを読んだ話

JavaScriptへの理解を深めるために、JavaScript Primerを読み始めた。
第一部を読み終えたので、理解定着のためはじめて知った事大事だと思った事を書いておく。

セミコロンは付けておいた方が良い。

セミコロンを省略して書いている人のコードをよく見かける(私自身も省略して書く)のだが、「暗黙的なものへ頼ると意図しない挙動が発生する」との記述があり、セミコロンは常に書くようにしたほうが良いとのこと。

何故varがダメだったのか?

私がJavaScriptのコードを書くようになった時点で、既にletがあり、なんとなくvarは使ってはいけない風潮があったので特に気にはしていなかった。
しかし、「歴史を知ることは同じ過ちを繰り返さない事に繋がる」と、おじいちゃんに言われていた事を思い出し、歴史を知ることにした。

varがダメな理由は、大きく以下の2つがある。

  1. 同じ名前の変数を再定義できる
  2. 変数の巻き上げと呼ばれる意図しない挙動がある

1.は以下の通り。

var x = 'hoge'
var x = 'fuga'
console.log(x) // fuga

2.は以下の通り。巻き上げとは、ブロックスコープを無視して最も近い変数に紐づけてしまうこと。

// var宣言より前に参照してもエラーにならない
console.log(x); // => undefined
var x = "varのx";

function fn() {
    // 内側のスコープにあるはずの変数`x`が参照できる
    console.log(x); // => undefined
    {
        var x = "varのx";
    }
    console.log(x); // => "varのx"
}
fn();

たしかに、こんな事ができてしまうと意図しない挙動が起きる可能性が高そうです。
letが実装されたのも頷けます。

Nullish coalescing演算子

私の場合、PHPの方で先に知ったかもしれない・・・
JavaScriptにもあったんですね。nullishな値なのでundefinedもちゃんと含みます。

// 左辺がnullishであるため、右辺の値の評価結果を返す
console.log(null ?? "右辺の値"); // => "右辺の値"
console.log(undefined ?? "右辺の値"); // => "右辺の値"

// 左辺がnullishではないため、左辺の値の評価結果を返す
console.log(true ?? "右辺の値"); // => true
console.log(false ?? "右辺の値"); // => false
console.log(0 ?? "右辺の値"); // => 0
console.log("文字列" ?? "右辺の値"); // => "文字列"

Arrow Functionの特徴

これはだいたい知っている内容だったんですが、1つ気になる記述が・・・

  • arguments変数を参照できない

arguments変数って何?

function fn() {
    // `arguments`はインデックスを指定して各要素にアクセスできる
    console.log(arguments[0]); // => "a"
    console.log(arguments[1]); // => "b"
    console.log(arguments[2]); // => "c"
}
fn("a", "b", "c");

おぉ、引数を定義していないのに要素にアクセスできるのか!!
正直知らなかった(= = ;;

オブジェクトの複製

本書にはshallow copydeep copyのサンプルコードが載っていたので、これは是非とも参考にしたい。

in演算子とObject#hasOwnPropertyの違い

  • hasOwnPropertyメソッド ・・・ そのオブジェクト自身が指定したプロパティを持っているかを判定する。
  • in演算子 ・・・ オブジェクト自身が持っていなければ、そのオブジェクトの継承元であるprototypeオブジェクトまで探索して持っているかを判定する。
const foo = {
  foo: "Foo",
  bar: {
    bar: "Bar"
  }
};

// そのオブジェクト自身が指定したプロパティを持っているかを判定
console.log(foo.hasOwnProperty('foo')); // true
console.log(foo.hasOwnProperty(toString)); // false

// prototypeオブジェクトまで探索して持っているかを判定
console.log('foo' in foo); // true
console.log('toString' in foo); // true

こういった細かい違いは、はじめて知りました(= = ;;

JavaScript文字コードの話

このあたりはとても興味深い話だった。
JavaScript文字コードとしてUnicodeを採用しいて、エンコード方式としてUTF-16を採用している。と書いてあって、最初は「ふーん」だったのですが、そこからCode PointCode Unitの話になると、「あれ?これ知ってないとマズくない?」と思いました。

配列をフラットに・・・

Array#flatメソッドというのがあるようです。これは初めて知りました(= = ;;
おぉ、便利そう・・・

const array = [[["A"], "B"], "C"];

// 引数なしは 1 を指定した場合と同じ
console.log(array.flat()); // => [["A"], "B", "C"]
console.log(array.flat(1)); // => [["A"], "B", "C"]
console.log(array.flat(2)); // => ["A", "B", "C"]

// すべてをフラット化するには Infinity を渡す
console.log(array.flat(Infinity)); // => ["A", "B", "C"]

正規表現を使った文字取得

String#matchAllメソッドというのがあるようです。これも初めて知りました(= = ;; 今までRegExp#execメソッドで使っていた実装していたところが、String#matchAllメソッドで置き換え可能っぽいですね。

const str = "ABC あいう DE えお";
const alphabetsPattern = /[a-zA-Z]+/g;
// matchAllはIteratorを返す
const matchesIterator = str.matchAll(alphabetsPattern);
for (const match of matchesIterator) {
    // マッチした要素ごとの情報を含んでいる
    console.log(`match: "${match[0]}", index: ${match.index}, input: "${match.input}"`);
}
// 次の順番でコンソールに出力される
// match: "ABC", index: 0, input: "ABC あいう DE えお"
// match: "DE", index: 8, input: "ABC あいう DE えお"

クロージャ

本書のクロージャーの説明は「どういった仕組みでクロージャーを実現しているのか?」をメインに書いており、とても面白かったし、わかり易い説明だった。
クロージャー・・・あまり使う機会がないなぁと思いつつ、以下で使われてるよー。と書いてあり、このあたりも意識していきたい。

this

これは色々なところで記事が書かれていて、よく目にします。
が・・・正直、本書が一番わかり易い。

プロトタイプオブジェクトとプロトタイプチェーン

JavaScriptは、プロトタイプベースのオブジェクト指向言語であるわけですが、他にプロトタイプベースのオブジェクト指向言語ってあるんですかね?

  • プロトタイプオブジェクト ・・・ JavaScriptの関数オブジェクトのprototypeプロパティに自動的に作成される特殊なオブジェクト。
  • プロトタイプチェーン ・・・ オブジェクト自身からPrototype内部プロパティへと順番に探す仕組みのこと。

例外処理

「throw文はあらゆるオブジェクトを例外として投げられますが、基本的にErrorオブジェクトのインスタンスを投げることを推奨」という事が書いてあり、ちょっとドキッとしました。

Map / Set

Set便利ですよね。よく使います。
Weakシリーズ(WeakMap / WeakSet)の存在を初めてしりました。(= = ;;
普通のMap / Setとは挙動が異なる様なので、使いどころを選びそうです。