20200224のJavaScriptに関する記事は29件です。

マネーフォワードからの出金お知らせをラインに通知する

やったこと

Google APP Scriptでマネーフォワードからのメールを定期取得
LINE Notify APIを使用してメール本文をLINEに通知
image.png

なぜやるのか

  • 不正利用やサービスの解約し忘れで意図せず引き落とされてないか怖い
  • できるだけリアルタイムに通知を受け取りたい
  • 複数のクレジットカードを使用しているので、それぞれの明細を把握するのは手間
  • Gmail見る頻度低いので日常的に目に触れるLINEに通知したい

なぜGoogle APP Scriptか

  • マネーフォワードにLINE通知機能はない
  • IFTTTはLINE通知できるが、Gmailをアクション元に設定できない
  • ZapierはGmailをアクション元に設定できるが、LINE通知できない

--> 仕方ないのでGoogle APP Scriptで書く

本題

Gmail側の設定で送信元メールアドレスがマネーフォワードのメールにラベルを付ける

image.png
ラベルを付けるとlabel:"ラベル名"で検索できるようになります。

LINE通知に必要なトークンを取得する

LINEに通知はLINE Notify APIを使用します。

以下の記事が分かりやすく説明されていました。
[超簡単]LINE notify を使ってみる

Google APP Scriptで直近30分間に受信したラベルのついたメールを取得してLINEに通知

以下のコードを書く。

var lineToken = "ラインのトークン"
var interval = 30
var label = "ラベル名(今回はmoney_forward)"

function fetchMoneyForwardMail() {
  //取得間隔
  var now = Math.floor(new Date().getTime() / 1000);
  var time = now - (60 * interval);
  var searchCond = '(label:' + label + ' after:' + time + ')';

  var thread = GmailApp.search(searchCond);
  var messages = GmailApp.getMessagesForThreads(thread);
  var formattedMsgs = [];
  for (var i = 0; i < messages.length; i++) {
    formattedMsgs[i] = "\n【date】: " + messages[i].slice(-1)[0].getDate() +
      "\n【Subject】: " + messages[i].slice(-1)[0].getSubject() +
      "\n【Body】: \n" + messages[i].slice(-1)[0].getPlainBody().slice(0, 200);
    return formattedMsgs;
  }
}

function send_line(message) {
  var payload = {
    'message': message
  };
  var options = {
    "method": "post",
    "payload": payload,
    "headers": {
      "Authorization": "Bearer " + lineToken
    }
  };
  UrlFetchApp.fetch("https://notify-api.line.me/api/notify", options);
}

function main() {
  newMessages = fetchMoneyForwardMail()
  if (newMessages && newMessages.length > 0 && newMessages.length < 50) {
    for (var i = newMessages.length - 1; i >= 0; i--) {
      Logger.log(newMessages[i])
      send_line(newMessages[i])
    }
  }
}

Google APP Scriptの時間ベースイベントで上記処理を30分に一度実行する

以下のように設定します。
GASのスクリプトを指定した時間に自動で実行する方法

以上です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同期的に複数のファイルをform-dataを使って送信するサンプル

こんにちは、wattak777です。

一ファイルを送信する、というサンプルは幾つかあるのですが、requestとform-dataを組み合わせて送る場合、複数ファイルの場合、例えばforループで送るようにしても1つ目、2つ目、3つ目と送りきる前にどんどん送ってしまうため同期的に送ることが出来ません。

なので、ちょっとサンプルを作ってみました。

サーバー側はmulterを使った以下のサンプル。

server.js
var express = require( 'express' ) ;
var app = express() ;
var multer = require( 'multer' ) ;

app.post('/file_upload', multer({dest: ファイルを置くパス}).single('my_file'), function (req, res) {
    console.log(req.file.path, req.file.originalname) ;
    res.sendStatus(200) ;
});

var server = app.listen(12345, function() {
    console.log("listening at port %s", server.address().port) ;
});

で、本題のクライアント側は以下の実装。
request-promiseを使って同期的に送るようにしました。

client.js
const fs = require('fs') ;
const request = require('request-promise') ;

const FileNameList = [
    'test1.bin',
    'test2.bin',
    'test3.bin'
] ;
var FileNameIndex = 0 ;

var returnCode = httpPost() ;

function httpPost() {
    const FormData = {
        my_file: {
            value: fs.createReadStream(ファイルのパス + FileNameList[FileNameIndex]),
            options: {
                filename: FileNameList[FileNameIndex],
                contentType: 'application/octet-stream'
            }
        }
    }

    const options = {
        uri: "http://サーバーのIPアドレス:12345/file_upload",
        formData: FormData,
        method: 'post',
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    }

    var response = request(options)
        .then( function(body) {
            console.log( 'then :' + body ) ;
            onEnd() ;
        })
        .catch( function(err) {
            console.log( 'catch error :' + err ) ;
        }) ;
    return response.statusCode ;
}

function onEnd() {
    console.log( 'Index ' + FileNameIndex + ' is finish.' ) ;
    FileNameIndex = FileNameIndex + 1 ;
    if ( FileNameIndex >= FileNameList.length ) {
        console.log( 'End Operation.' ) ;
    } else {
        var res = httpPost() ;
    }
}

console.log( 'Start Operation.' ) ;

とやると、クライアント側の表示は以下のようになります。

$ node client.js
Start Operation.
then :OK
Index 0 is finish.
then :OK
Index 1 is finish.
then :OK
Index 2 is finish.
End Operation.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsで日本語入力中に未確定状態でinput boxにmaxlengthを超えた部分が表示されないようにするには

インプットボックスの文字数制限

最近、インプットボックスの文字数制限を実装する必要があり、日本語入力中の挙動で結構はまりました。とある方のアドバイスで解決したので、備忘録として残しておこうと思います。アドバイスしてくださった方、ありがとうございました。なお、今回はVue.jsでの実装で直面した問題でしたが、Vue.js以外でも(JavaScriptなら)同様のことが起こりうると思います。(より良い実装の情報があれば、ぜひ教えてください。)

シンプルにmaxlengthを指定した実装

以下ソースコードで問題ないと思いきや...

ソースコード

html
<div id=app>
  <input type="text" v-model="test" maxlength="5">
  {{test}}
</div>
Vue.js
const app = new Vue ({
  el: '#app',
  data: {
    test:''
  }
})

もし、挙動を試してみたい方は、以下codepenからお試しください。

See the Pen This form can't restrict max length on Japanese language input by Yasunori MATSUOKA (@YasunoriMATSUOKA) on CodePen.

微妙な状況に

しかし、この実装では日本語入力中に未確定状態では最大文字数のmaxlengthを超えた部分についてもインプットボックスに表示されてしまうという状況に悩まされていました。(Enter押す等して確定すれば、maxlength以降の文字は消えるのですが、その前の未変換の状態や変換中はmaxlength以上の文字が入力できているような印象をユーザーに与えてしまうという点で微妙な状況でした。)

解決策

inputイベント時にevent.target.valueの長さをチェックし、maxlengthを超えている場合はinput boxからフォーカスを外す(event.target.blur()する)

解決策のソースコード

html
<div id=app>
  <input type="text" v-model="test" maxlength="5" @input="onInput">
  {{test}}
</div>
Vue.js
const app = new Vue ({
  el: '#app',
  data: {
    test:''
  },
  methods: {
    onInput (event) {
      console.log('onInput', event)
      if (event.target.value.length > event.target.getAttribute("maxlength")) {
        event.target.blur()
      }
    }
  }
})

無理矢理感は否めませんが、これで、maxlengthを超えた際は、自動的に対象のinput boxからフォーカスが外れ、日本語入力が強制的に確定され、maxlength以降の仮入力中の文字がinput boxに表示されることが無くなりました。

試してみたい方は、以下のcodepenでお試しください。

See the Pen Restrict max length on Japanese language input by Yasunori MATSUOKA (@YasunoriMATSUOKA) on CodePen.

試したがうまくいかなかったもの

keydownイベント時にevent.target.valueの長さをチェックし、maxlengthを超えている場合はイベントを無効化する

うまくいかなかった実装のソースコード

html
<div id=app>
  <input type="text" v-model="test" maxlength="5" @keydown="onKeyDown">
  {{test}}
</div>
VUe.js
const app = new Vue ({
  el: '#app',
  data: {
    test:''
  },
  methods: {
    onKeyDown (event) {
      console.log('onKeyDownEvent', event)
      if (event.target.value.length >= 5) {
        if (event.code !== "Backspace" && event.code !== "Delete" && event.code !== "ArrowRight" && event.code !== "ArrowLeft" && event.code !== "ArrowUp" && event.code !== "ArrowDown" && event.code !== "PageUp" && event.code !== "PageDown" && event.code !== "Enter" && event.code !== "Space" && event.code !== "Tab") {
          event.preventDefault()
          console.log('preventDefault', event)
        }
      }
    }
  }
})

IME ONの状態での入力中に、入力をJavaScriptで制御するのは難しいようで、半角英数ならば意図した通に動くものの、日本語入力中には意図した通り動作させることができませんでした。
試してみたい方は、以下codepen上でお試しください。

See the Pen Try to apply max length restriction on input Japanese language (don't work well) by Yasunori MATSUOKA (@YasunoriMATSUOKA) on CodePen.

まとめ

半角英数で完結する言語うらやましい...日本語入力等のIME ONにする必要がある言語はこういうとき大変...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同期的に複数のファイルをform-dataを使って送るには

こんにちは、wattak777です。

一ファイルを送信する、というサンプルは幾つかあるのですが、requestとform-dataを組み合わせて送る場合、複数ファイルの場合、例えばforループで送るようにしても1つ目、2つ目、3つ目と送りきる前にどんどん送ってしまうため同期的に送ることが出来ないため、ちょっとサンプルを作ってみました。

サーバー側はmulterを使った以下のサンプル。

server.js
var express = require( 'express' ) ;
var app = express() ;
var multer = require( 'multer' ) ;

app.post('/file_upload', multer({dest: ファイルを置くパス}).single('my_file'), function (req, res) {
    console.log(req.file.path, req.file.originalname) ;
    res.sendStatus(200) ;
});

var server = app.listen(12345, function() {
    console.log("listening at port %s", server.address().port) ;
});

で、本題のクライアント側は以下の実装。
request-promiseを使って同期的に送るようにしました。

client.js
const fs = require('fs') ;
const request = require('request-promise') ;

const FileNameList = [
    'test1.bin',
    'test2.bin',
    'test3.bin'
] ;
var FileNameIndex = 0 ;

var returnCode = httpPost() ;

function httpPost() {
    const FormData = {
        my_file: {
            value: fs.createReadStream(ファイルのパス + FileNameList[FileNameIndex]),
            options: {
                filename: FileNameList[FileNameIndex],
                contentType: 'application/octet-stream'
            }
        }
    }

    const options = {
        uri: "http://サーバーのIPアドレス:12345/file_upload",
        formData: FormData,
        method: 'post',
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    }

    var response = request(options)
        .then( function(body) {
            console.log( 'then :' + body ) ;
            onEnd() ;
        })
        .catch( function(err) {
            console.log( 'catch error :' + err ) ;
        }) ;
    return response.statusCode ;
}

function onEnd() {
    console.log( 'Index ' + FileNameIndex + ' is finish.' ) ;
    FileNameIndex = FileNameIndex + 1 ;
    if ( FileNameIndex >= FileNameList.length ) {
        console.log( 'End Operation.' ) ;
    } else {
        var res = httpPost() ;
    }
}

console.log( 'Start Operation.' ) ;

とやると、クライアント側の表示は以下のようになります。

$ node form2_promise.js
Start Operation.
then :OK
Index 0 is finish.
then :OK
Index 1 is finish.
then :OK
Index 2 is finish.
End Operation.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

face-api.jsでニコラ○・ケ○ジになる

顔認識がjsだけでもそれなりに動くのを今更知ったので触ってみた。
顔晒すのに抵抗感がある古い人間なので文中画像少なめです。

成果物

Git: https://github.com/engabesi/face-nicolas
GithubPages: https://engabesi.github.io/face-nicolas/
2020-02-24_21h23_40.png

やること

  • expressでlocalhostを建てる
  • face-api.jsで顔の矩形を取る
  • 顔に画像を被せる
  • GithubPagesにdeploy

face-api.js

今回顔認識に使うライブラリはこちら
https://github.com/justadudewhohacks/face-api.js
jsだけでそれなりの精度とパフォーマンスを出せます。
複数人も認識可能。
ランドマークや表情、顔認証も出来ます。
demoはこちら
https://justadudewhohacks.github.io/face-api.js/face_and_landmark_detection/

注意点

face-api.jsはWebGLを使用しています。
その為ブラウザの設定でハードウェアアクセラレーションをオフにしている場合上手く動作しない可能性があります。

expressでlocalサーバーを建てる

android等でも気軽にテストするためにまずはlocalhostで動作するようにします。
後にgithubPagesも利用したいため、以下の構成にします。

構成
root/
├─ app.js
└─ docs/
    ├─ index.html
    ├─ images/
    └─ js/
       ├─ index.js
       └─ lib/
          ├─ face-api.min.js
          └─ models/
             ├─ tiny_face_detector_model-shard1
             └─ tiny_face_detector_model-weights_manifest.json

face-api.min.js, 及びmodels内ファイルは以下のrepoから取ってきます
https://github.com/justadudewhohacks/face-api.js/tree/master/dist
https://github.com/justadudewhohacks/face-api.js/tree/master/weights

expressを導入します。

shell
yarn init -y
yarn add express
docs\index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script defer src="./js/lib/face-api.min.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        width: 100vw;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      canvas {
        position: absolute;
      }
    </style>
  </head>
  <body>
    <video id="video" width="720" height="560" autoplay muted></video>
    <script defer src="js/index.js"></script>
  </body>
</html>
app.js
const express = require("express");
const app = express();

const path = require("path");
app.listen(8080, () => {
  console.log("Running at Port 8080...");
});

app.use(express.static(path.join(__dirname, "docs")));

app.use((req, res) => res.sendStatus(404));

これでshellにnode app.jsと打ち、http://localhost:8080/ にアクセスするとページが開きます(今は真っ白)

webcamera設定

docs\index.js
const startVideo = async video => {
  try {
    const constraints = { audio: false, video: {} };
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    video.srcObject = stream;
  } catch (error) {
    console.error(error);
  }
};

(async () => {
  const video = document.querySelector("video");
  await startVideo(video);
})();

これでlocalhostを開くとwebcameraの映像が取れているはずです。

顔の矩形を取る

まずmodelを読み込みます。

docs\index.js
+ const loadModels = async () => {
+   await Promise.all([
+    faceapi.nets.tinyFaceDetector.loadFromUri(`/js/lib/models`)
+  ]);
+ };

  (async () => {
    const video = document.querySelector("video");
+   await loadModels();
    await startVideo(video);
  })();

次に顔を認識して矩形描画処理を追加します。

docs\index.js
(async () => {
  const video = document.querySelector("video");
  await loadModels();
  await startVideo(video);
// --- ADD ---
  video.addEventListener("play", () => {
    // overlay canvas作成
    const canvas = faceapi.createCanvasFromMedia(video);
    document.body.append(canvas);
    const displaySize = { width: video.width, height: video.height };
    faceapi.matchDimensions(canvas, displaySize);

    const tinyFaceDetectorOption = {
      // default 416
      inputSize: 224,
      // default 0.5
      scoreThreshold: 0.5
    };
    setInterval(async () => {
      const results = await faceapi.detectAllFaces(
        video,
        new faceapi.TinyFaceDetectorOptions(tinyFaceDetectorOption)
      );
      if (results.length <= 0) return;
      // 検出結果をcanvasのサイズにリサイズ
      const resizedResults = faceapi.resizeResults(results, displaySize);
      // canvasの内容をクリア
      canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
      // 矩形描画
      faceapi.draw.drawDetections(canvas, resizedResults);
    }, 100);
  });
// --- ADD ---
})();

これでscoreが記載された矩形が顔に被さって表示されるようになります。

顔に画像を被せる

検出結果から座標を取ってその位置に画像を被せてみます。

docs\index.js
(async () => {
  const video = document.querySelector("video");
+ // 画像セットアップ
+ const image = new Image();
+ image.src = `/images/cage_neutral.png`;
  await loadModels();
  await startVideo(video);
  video.addEventListener("play", () => {
    const canvas = faceapi.createCanvasFromMedia(video);
    document.body.append(canvas);
    const displaySize = { width: video.width, height: video.height };
    faceapi.matchDimensions(canvas, displaySize);
    const tinyFaceDetectorOption = {
      // default 416
      inputSize: 224,
      // default 0.5
      scoreThreshold: 0.5
    };
    setInterval(async () => {
      const results = await faceapi.detectAllFaces(
        video,
        new faceapi.TinyFaceDetectorOptions(tinyFaceDetectorOption)
      );
      if (results.length <= 0) return;
      const resizedResults = faceapi.resizeResults(results, displaySize);
      canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
-     // faceapi.draw.drawDetections(canvas, resizedResults);
+     resizedResults.forEach(detection => {
+       // 矩形のtopはデコあたりなので調整
+       const marginVal = 0.4;
+       // 矩形の情報はdetection.boxに格納されている
+       const width = detection.box.width;
+       const height = detection.box.height * (1.0 + marginVal);
+       const x = detection.box.x;
+       const y = detection.box.y - detection.box.height * marginVal;

+       canvas.getContext("2d").drawImage(image, x, y, width, height);
+     });
    }, 100);
  });
})();

これで自分の顔に画像を貼り付けることが出来ました。
他にもmodelやdetection時に.with~~とするだけで表情やランドマーク等の情報を取れます。
詳しくは公式README参照。自分のgitにあげている物にも画像調達に飽きて中途半端になっていますが表情を取るコードも記載してあります。

GithubPages

コードをrepositoryに上げたら
repository > settings > Options > GithubPages > Sourceを
master branch /docs folderにすればGithubPagesにデプロイすることが出来ます。
だから、ディレクトリ名をdocsにする必要があったんですね。

注意点

上記コードでは画像のpathを直打ちしています。
github.ioにデプロイする場合は問題ありませんが、github.io/SUB_REPO/にデプロイした場合、ルートURLがgithub.io扱いとなり、SUB_REPOが飛ばされてしまってpathがおかしくなってしまいます。
jekyllの使用をgithubは推奨していますが試して遊ぶだけの場合ちょっと面倒です。
暫定対策としてpath前に直接SUB_REPO名をくっつけてあげると一応動きます。

まとめ

たった数分でブラウザ完結の顔認識ができるなんて素晴らしい世の中になりました。
現状のコードだとスマートフォンで見た場合矩形がものすごく横長になってしまっているのでその辺の対応をしだすとヘビーになるかもしれませんがちょっと遊んで見るぐらいだと非常に有用だと思います。

他にもclmtrackr.jspico.js、更にはChromeの機能だけでできるShape Detection API等様々なライブラリがありますので是非触って遊んでみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kintoneで宣言的にフィールド値の検証を行う

先日、 Tynder という、TypeScript/JavaScript用のスキーマ検証ライブラリを公開しました。

tynder.png

Tynder は、TypeScriptのサブセット+独自の拡張文法から成るDSLによって

  1. 型の検査
  2. 単独の項目の必須・値の長さ・範囲や文字列パターンの検証
  3. 複数項目の相関や整合性検証の一部 (Union typeによる)

宣言的に行うことができます。
また、カスタムエラーメッセージを表示することができます。

今回はTynderを使用して、kintoneアプリのフィールド値を宣言的に検証したいと思います。

動機

kintoneは、標準で項目の必須チェック、文字列項目の文字数レンジチェック、数値項目のレンジチェック機能等を備えていますが、やや複雑な条件でのチェックを行うためには、JavaScriptによるカスタマイズを行う必要があります(例えば、条件付き必須、文字列のパターンマッチ)。
しかし、イベントハンドラ(または、分離した関数)で手続き的にチェックを記述するのは、可読性が低く、また、チェック対象のデータはフィールド型等のメタデータも含んでおり、データの階層が深く、正直触りたくありません。

完成イメージ

サブテーブルを含む各フィールドに検証エラーのエラーメッセージを表示します。
以下のサンプルでは、敢えて標準の必須項目等は設定していませんが、必須などのエラーが表示されました。

tynder-kintone-validation.png

設定方法

アプリの設定画面から、以下のJavaScriptファイルを追加してください。

コード

lib.js
// `event.record`をメタデータ(フィールド型等)を含まない普通のデータに変換します
function mapRecord(rec) {
    const ret = {};
    const keys = Object.keys(rec);
    const subTableMapper = x => mapRecord(x.value);
    for (const k of keys) {
        switch (rec[k].type) {
        case 'NUMBER':
            ret[k] = typeof rec[k].value === 'string' ?
                Number(rec[k].value.replace(/[,]/g, '')) :
                rec[k].value;
            break;
        case 'SUBTABLE':
            ret[k] = rec[k].value.map(subTableMapper);
            break;
        default:
            ret[k] = rec[k].value;
            break;
        }
    }
    return ret;
}

// サブテーブルのブランク行を削除します(新規行のみ)
function removeBlankTableRow(rec, tableFieldCode) {
    const validRecs = [];
    for (const r of rec[tableFieldCode].value) {
        if (r.id !== null) {
            validRecs.push(r);
            continue;
        }
        const keys = Object.keys(r.value);
        for (const k of keys) {
            const q = r.value[k];
            if (q.value !== void 0 && q.value !== null && q.value !== '') {
                validRecs.push(r);
                break;
            }
        }
    }
    rec[tableFieldCode].value = validRecs;
    return rec;
}

// フィールドにエラーメッセージを表示します
function displayValidationErrorMessages(event, ctx) {
    for (const m of ctx.errors) {
        const dp = m.dataPath.split('.').map(x => x.split(':').slice(-1)[0]);
        const fieldCode = dp[0];
        if (m.dataPath.includes('repeated).')) {
            const index = /\.\(([0-9]+):repeated\)\./.exec(m.dataPath);
            if (index) {
                const subFieldCode = dp[dp.length - 1];
                event.record[fieldCode].value[Number(index[1])].value[subFieldCode].error = m.message;
            }
        } else {
            if (event.record[fieldCode]) {
                event.record[fieldCode].error = m.message;
            }
        }
    }
    event.error = 'Validation error';
    return event;
}
app.js
// アプリのレコード型を定義します
// interfaceの各フィールドは、kintoneアプリのフィールドコードと一致させてください
const definition = `
/** サブテーブル */
interface Table {
    itemName: string;
    itemValue: number;
}

/** 全ステータス共通のフィールド */
interface AppBase {
}

/** アプリ (ステータス1) */
interface App1 extends AppBase {
    @minLength(2)
    name: string;
    amount: number;
    table: Table[];
}
// レコードのステータス毎に定義を持てば、殆どの条件付きチェックは
// 条件分岐を含まない形の分解できる
`;

// イベントハンドラ
kintone.events.on('app.record.edit.submit', function(event) {
    event.record = removeBlankTableRow(event.record, 'table');

    // スキーマ検証を行います
    const schema = tynder.compile(definition);
    const ctx = {checkAll: true, schema};
    const validated = tynder.validate(mapRecord(event.record), tynder.getType(schema, 'App1'), ctx);
    if (! validated) {
        const errText = JSON.stringify(ctx.errors, null, 2);
        console.error(errText);

        // エラーを表示します
        displayValidationErrorMessages(event, ctx);
    }
    return event;
});

さいごに

書かなくて良いコードは書かずに、楽しくプログラミングしたいですね?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MapBox GL JS でラベル付きマーカー

MapBox GL のサンプルにカスタムマーカーがあるので、それを参考に「ラベル付きのマーカー」を実現してみた。

image.png

動作するサンプルはこちら。

ソースコード

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset='utf-8' />
  <title></title>
  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
  <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
  <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.js'></script>
  <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.8.0/mapbox-gl.css' rel='stylesheet' />
  <link rel="stylesheet" type="text/css" href="./index.css" media="screen">
  <style>
  </style>
</head>
<body>

<div id='map'></div>

<template id="marker">
  <div class="marker-container">
    <span id="title" class="marker-title"></span>
    <img id="marker-icon" src="https://img.icons8.com/ios-filled/40/0000FF/marker.png">
  </div>>
</template>

<script src="./index.js"></script>

</body>
</html>

index.css

body {
  margin: 0;
  padding: 0;
}

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

.marker-container {
  display: flex; 
  flex-direction: column; 
  align-items: center; 
  cursor: pointer;
}

.marker-title {
  max-width: 100px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.marker-icon {
  height: 30px; 
  width: auto; 
  background-size: cover;
}

.mapboxgl-popup {
  max-width: 200px;
}

.mapboxgl-popup-content {
  text-align: center;
  font-family: 'Open Sans', sans-serif;
}

index.js

const geojson = {
  type: 'FeatureCollection',
  features: [{
    type: 'Feature',
    geometry: {
    type: 'Point',
      coordinates: [-77.032, 38.913]
    },
    properties: {
      title: 'Mapbox',
      description: 'Washington, D.C.'
    }
  },
  {
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [-122.414, 37.776]
    },
    properties: {
      title: 'Mapbox',
      description: 'San Francisco, California'
    }
  }]
};

const map = new mapboxgl.Map({
  container: 'map',
  center: [-96, 37.8],
  zoom: 3,
  style: {
    "version": 8,
    "sources": {
      "OSM": {
        "type": "raster",
        "tiles": ['http://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
        "tileSize": 256
      }
    },
    "layers": [{
      "id": "OSM",
      "type": "raster",
      "source": "OSM"
    }]
  }
});

geojson.features.forEach(marker => {

  // Create element for marker from template
  const template = document.getElementById('marker');
  const clone = document.importNode(template.content, true);
  const el = clone.firstElementChild;

  clone.getElementById('title').innerHTML = marker.properties.description;

  new mapboxgl.Marker(el, { anchor: 'bottom' })
    .setLngLat(marker.geometry.coordinates)
    .setPopup(new mapboxgl.Popup({ offset: 60, anchor: 'bottom' }) // add popups
    .setHTML('<h3>' + marker.properties.title + '</h3><p>' + marker.properties.description + '</p>'))
    .addTo(map);
});
  1. マーカーとして使用する HTML 要素を template タグで用意しておきます。
  2. マーカーごとに template から要素を生成(importNode で複製しないと同じ要素が使い回されるので注意)してラベルを設定、その要素から MapBox の Marker を生成します。

※地図を OpenStreetMaps にしているのは、MapBox の地図を表示するには AccessToken を設定する必要があるためです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Testable Redux ~ React と Redux のテスト戦略 ~

image

本記事では Redux を使用した場合の React コンポーネントに対するテスト方法を考察します。

Redux に接続された React コンポーネントは、コンポーネント内のプロパティを Redux Store の state と同期させています。 Redux に強く依存しているコンポーネントをどのように Testable にしていけば良いのでしょうか。記事のタイトルは、t-wada 先生の言葉をお借りして、Testable Redux とつけさせていただきました。

純粋な React コンポーネントをテストする

さて、まずはじめに純粋な React のコンポーネントに対するテスト方法を振り返ってみます。Facebook に習えば、jest の公式ドキュメントでも紹介されている jestenzyme を組み合わせた方法が一般的でしょう。

以下のようなシンプルなコンポーネントを例として使用します。

next

count を保持する state をもち、onClick で state を変更します。簡単のために Functional Component とし、State Hooks を使用します。後のテストのために各タグにはカスタムデータ属性(data-test)を付与しておきます。プロダクションビルドでカスタムデータ属性を取り除く方法もあります。こちらの記事にて詳細に解説されていました。

import React, { useState } from "react";
export default () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h3 data-test="count">count: {count}</h3>
      <button data-test="count-up" onClick={() => setCount(count + 1)}>⬆︎</button>
      <button data-test="count-down" onClick={() => setCount(count - 1)}>⬇︎</button>
    </div>
  );
};

これに対するテストは以下のように記述できます。初期値が 0 であること、ボタンをクリックして 1 に変わることを確認します。

import React from "react";
import { shallow } from "enzyme";
import Counter from "./PureReactCounter";

import testConfigure from "../../testConfigure";
testConfigure();

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  const Component = shallow(<Counter />);
  it("ボタンをクリックしてカウントアップする", () => {
    expect(Component.find("h3").text()).toEqual("count: 0");
    Component.find(sel("count-up")).simulate("click");
    expect(Component.find("h3").text()).toEqual("count: 1");
  });
});

なお、testConfigure には以下を記述しておきます。enzyme を使用する場合のお約束のようなものです。enzyme の installationにて解説されています。

import Enzyme from "enzyme";
import EnzymeAdapter from "enzyme-adapter-react-16";
export default () => Enzyme.configure({ adapter: new EnzymeAdapter() });

結果は 8ms でした。このテストでは shallow() を使用しています。Shallow レンダリングは、コンポーネントをユニットテストの範囲でテストできるように制限します。テストが子コンポーネントの動作を間接的にアサートしないようにしてくれます。コンポーネントは子コンポーネントに依存させず、常にステートレスに保つことによって Shallow レンダリングによるテストができ、高速です。

 PASS  src/components/PureReactCounter.test.tsx
  <Counter /> コンポーネント
     ボタンをクリックしてカウントアップする (8ms)

ちなみに、mount() は完全な DOM レンダリングを行います。DOM API とやり取りする可能性のあるコンポーネントである場合や、より高次のコンポーネントにラップされているコンポーネントをテストする必要がある場合に最適です。ただしその一方で多くの依存関係を考慮することによりテストに要する時間は増える傾向にあります。

 PASS  src/components/PureReactCounter.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
       ボタンをクリックしてカウントアップする (8ms)
    Fullレンダリング
       ボタンをクリックしてカウントアップする (8ms)

結果として実行時間はほとんど変わりませんでした。これは今回実装したコンポーネントが他のコンポーネントや API などと依存していないシンプルなコンポーネントであるためです。

Redux の使用を開始する

さて、先ほど実装したコンポーネントは、count を保持する state をもち、onClick で state を変更していました。この count という state を Redux の store で管理します。実装例を紹介する前に Redux のライフサイクルのおさらいをしましょう。

  1. View: ユーザが操作を行い、handleClick() などのイベント起動 function が実行される。
  2. ActionCreater: イベント起動 function は ActionCreater を通して action を生成する。
  3. Dispacther: action は Dispater に渡され、Reducer に流れる。
  4. Reducer: Reducer は action の type に応じて新しい state を返却する。

lifecycle

説明の粒度は荒いですが、おおまかにこのような流れで Redux による状態の管理が行われます。

さて、ディレクトリ構成は以下のようにして実装を進めていきます。

src/
├── index.tsx
├── App.tsx
├── components
│   ├── Counter.tsx
│   └── Counter.test.tsx
└── store
    ├── actions
    │   └── counterActions.ts
    ├── reducers
    │   └── counterReducer.ts
    └── store.ts

Action (actions/counterActions.ts)

Action の定義を行います。今回はカウントアップとカウントダウンする2種類の action があるので事前に定義しておき、他のファイルから参照できるようにしておきましょう。

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export interface ICountReducerAction {
  type: typeof INCREMENT | typeof DECREMENT;
}

Reducer (reducers/counterReducer.ts)

Reducer は飛んできた action の type に応じて state 返却する処理を書くのでした。INCREMENT の場合は +1DECREMENTの場合は -1された新しい state を返却しています。

import {
  ICountReducerAction,
  INCREMENT,
  DECREMENT
} from "../actions/counterActions";

const initialState = 0;
const counterReducer = (state = initialState, action: ICountReducerAction) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
};
export default counterReducer;

Store

先に定義した reducer を束ねて、storeを作成しましょう。今回 reducer は1つですが、複数使用することになることも考慮して combineReducers() を使用しています。

import { createStore } from "redux";
import { combineReducers } from "redux";
import counterReducer from "./reducers/counterReducer";
const reducer = combineReducers({ count: counterReducer });
export default createStore(reducer);

Component から Redux の store を参照する

Redux の state を View に表示できるようにコンポーネントを実装していきましょう。まず初めに純粋に Redux の store を直接参照するような実装を考えます。

ReactDOM.render()store から subscribe() します。

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./store/store";

const render = () => ReactDOM.render(<App />, document.getElementById("root"));
render();
store.subscribe(render);

コンポーネントは以下のように実装します。この場合はどうでしょう、テストは容易でしょうか。

import React from "react";
import store from "../../store/store";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

export default () => {
  const count = store.getState().count; // store から直接参照する
  return (
    <div>
      <h3 data-test="count">count: {count}</h3>
      <button onClick={() => store.dispatch({ type: INCREMENT })}>⬆︎</button>
      <button onClick={() => store.dispatch({ type: DECREMENT })}>⬇︎</button>
    </div>
  );
};

テストコードを振り返ってみましょう。このままでは Redux の store を参照ができないため、ボタンをクリックした後に発火するはずの Reducer が起動しません。結果として count は 0 のままです。

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const Component = shallow(<Counter />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(Component.find("h3").text()).toEqual("count: 0");
      Component.find(sel("count-up")).simulate("click"); // Redux store の state が変更されない
      expect(Component.find("h3").text()).toEqual("count: 1"); // 0 のまま
    });
  });
  describe("Fullレンダリング", () => {
    const Component = mount(<Counter />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(Component.find("h3").text()).toEqual("count: 0");
      Component.find(sel("count-up")).simulate("click");
      expect(Component.find("h3").text()).toEqual("count: 1");
    });
  });
});

テスト結果(クリックして開く)
 FAIL  src/components/DirectAccessReduxStore.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
      ✕ ボタンをクリックしてカウントアップする (11ms)
    Fullレンダリング
      ✕ ボタンをクリックしてカウントアップする (6ms)

  ● <Counter /> コンポーネント › Shallowレンダリング › ボタンをクリックしてカウントアップする

    expect(received).toEqual(expected) // deep equality

    Expected: "count: 1"
    Received: "count: 0"

      13 |       expect(Component.find("h3").text()).toEqual("count: 0");
      14 |       Component.find(sel("count-up")).simulate("click");
    > 15 |       expect(Component.find("h3").text()).toEqual("count: 1");
         |                                           ^
      16 |     });
      17 |   });
      18 |   describe("Fullレンダリング", () => {

      at Object.<anonymous> (src/components/DirectAccessReduxStore.test.tsx:15:43)

  ● <Counter /> コンポーネント › Fullレンダリング › ボタンをクリックしてカウントアップする

    expect(received).toEqual(expected) // deep equality

    Expected: "count: 1"
    Received: "count: 0"

      21 |       expect(Component.find("h3").text()).toEqual("count: 0");
      22 |       Component.find(sel("count-up")).simulate("click");
    > 23 |       expect(Component.find("h3").text()).toEqual("count: 1");
         |                                           ^
      24 |     });
      25 |   });
      26 | });

      at Object.<anonymous> (src/components/DirectAccessReduxStore.test.tsx:23:43)

Redux と React のコンポーネントが強く依存する関係になっており、React のコンポーネント単体としてテストしづらくなっています。この構成を変えていきましょう。まずは React と Redux の間の依存関係を切り離す方法を考えます。

react-reduxconnect() を使用して React と Redux の依存を引き剥がず

最も代表的な実装パターンとして react-reduxconnect() を使用する方法が挙げられます。これは、コンポーネントをマウントする時点で子コンポーネントや依存関係にある構成要素を考える必要がなく、シンプルです。

Counter コンポーネントと Redux store が強い依存関係にある実装から、なるべく疎結合になるように配慮します。いわゆる DI (Dependency Injection) の考え方にしたがって、Counter コンポーネントの store を外から props として注入できるように変更します。ただ単に props によって注入するだけではなく、Redux store との接合は connect() によって行います。詳細な手続きは公式チュートリアルを参照しましょう。このようなコンポーネントを HOC (Higher Order Component) と呼びます。詳細はこちらの記事が参考になります。

import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

interface ICounterState {
  counter: number;
}

interface ICounterProps {
  count: number;
  increment: any;
  decrement: any;
}

// props として store を流し込んで DI できるるようにする。
export const Counter = (store: ICounterProps) => {
  const { count, increment, decrement } = store;
  return (
    <div>
      <div data-testid="count">count: {count}</div>
      <button onClick={increment}>⬆︎</button>
      <button onClick={decrement}>⬇︎</button>
    </div>
  );
};

const mapStateToProps = (state: ICounterState) => ({
  count: state.counter
});
const mapDespatchToProps = (dispatch: Dispatch) => ({
  increment: () => dispatch({ type: INCREMENT }),
  decrement: () => dispatch({ type: DECREMENT })
});

// 第一引数の mapStateToProps は component に渡す props を制御する
// 第二引数の mapDespatchToProps は reducer を呼び出して、redux で管理している state を更新する
// Counter は取得したデータを props として扱いたい component を指定する
export default connect(mapStateToProps, mapDespatchToProps)(Counter);

さて、ここまでできれば勝てる気がしてきました。テストを書きます。

import React from "react";
import { mount, shallow } from "enzyme";
import { Provider } from "react-redux";
import { createStore } from "redux";
import sinon from "sinon";
import ConnectedCounter, { Counter } from "./ReactRedux";
import { reducer } from "../../store/store";
import testConfigure from "../../testConfigure";
testConfigure();
const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const props = {
      count: 0,
      increment: sinon.spy(), // カウントアップするボタンを押した挙動を確認するためにスパイを差し込む
      decrement: sinon.spy()
    };
    const shallowComponent = shallow(<Counter {...props} />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(shallowComponent.find("h3").text()).toEqual("count: 0");
      shallowComponent.find(sel("count-up")).simulate("click");
      expect(props.increment).toHaveProperty("callCount", 1);
    });
  });
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

上記のテストでは、Shallow レンダリングと Full レンダリングの2つの方法でテストを記述しました。

Shallow レンダリングでは Redux の store を含めず、コンポーネント単体でテストを行います。そのため、カウントアップするボタンを押した際のハンドラ関数は simon.spy() を使用してスパイを差し込んで挙動を確認します。

一方で、Full レンダリングの場合は Redux の store だけをモックとして作成し、<Provider>の store に DI します。こちらの手法の方が Redux の store を含めたテストにはなるのですが、実行時間が増える傾向にあるため注意が必要です。ちなみに実行時間を比較すると以下のようになります。

 PASS  src/components/container/ReactRedux.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
       ボタンをクリックしてカウントアップする (9ms)
    Fullレンダリング
       ボタンをクリックしてカウントアップする (44ms)

mount するのはインテグレーションテストだユニットテストではない、shallow の方が高速だ、などの論争があります。個人的な意見ですが、チーム開発を行う上ではシンプルかつ可読性を維持したテストコードを書いた方がむしろチームとしてアジリティが上がるのではないかと考えています。学習コストも高いですしね。
それでもプロダクトコードの量が大きくなるにつれて、テストの実行時間の増加によりアジリティが下がるケースもあります。shallow レンダリングと full レンダリングの実行時間の差は ms 程度ですが、塵も積もれば山となるということです。テストの方針を決めるのは難しいですね。

Redux Hooks を使用してコード量を減らす

怠惰で傲慢な我々は、現状に満足することはありません。上に挙げた実装は React と Redux の依存関係を引き剥がすことで、Shallow レンダリングによるテストが実現できるようになりました。その一方で少し複雑な実装をしなければならないように感じます。この章では Redux Hooks を使用して簡単に記述できる方法をご紹介します。

React の Hooks API は、Functional コンポーネントに対してローカルコンポーネントの state を使用できるようになる優秀な機能です。react-redux でも、既存の connect() を使用して実装された HOC (Higher Order Component) の代わりとして Hooks API を提供するようになりました。これを使用すると、前章の実装のようにコンポーネントをにラップして HOC を作るような面倒な作業は必要ありません。Redux store に subscribe して action を dispatch できます。

導入は非常に簡単です。useSelector()useDispatch() を使用しましょう。

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

export default () => {
  const count = useSelector((state: any) => state.count);
  const dispatch = useDispatch();
  const increment = () => { dispatch({ type: INCREMENT })};
  const decrement = () => { dispatch({ type: DECREMENT })};
  return (
    <div>
      <h3>count: {count}</h3>
      <button data-test="count-up" onClick={increment}>⬆︎</button>
      <button data-test="count-down" onClick={decrement}>⬇︎</button>
    </div>
  );
};

コードがとても短くなりました。かなりスマートです。あれ? でもおかしいですね、少し振り返ってみましょう。connect() を使用して HOC を作ったときには純粋な React コンポーネントと Redux との依存関係を引き剥がすことに成功していました。今回、Redux Hooks を使用した実装では、また依存関係が強くなってしまいました。これでは Full レンダリングを行うようなテストしか記述できません。

describe("<Counter /> コンポーネント", () => {
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

Redux Hooks でも Redux と React を分離する

基本的な考え方は今までと同様です。Redux と接続する部分を DI できるようにすれば OK です。useSelector()useDispatch() を使用している箇所を外に出してやりましょう。

import { useSelector, useDispatch } from "react-redux";
import { IRootState } from "../../store/store";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";
import { Dispatch } from "redux";

interface ICounterProps {
  count: number;
  increment: Dispatch;
  decrement: Dispatch;
}
export const Counter = (props: ICounterProps) => {
  const { count, increment, decrement } = props;
  return (
    <div>
      <h3>count: {count}</h3>
      <button data-test="count-up" onClick={increment}>⬆︎</button>
      <button data-test="count-down" onClick={decrement}>⬇︎</button>
    </div>
  );
};

export default (props: any) => {
  const count = useSelector<IRootState>(state => state.count);
  const dispatch = useDispatch();
  const _props = {
    count,
    increment: () => dispatch({ type: INCREMENT }),
    decrement: () => dispatch({ type: DECREMENT }),
    ...props
  };
  return <Counter {..._props} />;
};

当然といえば当然ですが、テストコードは react-reduxconnect() を使用した場合の実装と同じになります。

テストコード(クリックして開く)

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const props = {
      count: 0,
      increment: sinon.spy(),
      decrement: sinon.spy()
    };
    const shallowComponent = shallow(<Counter {...props} />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(shallowComponent.find("h3").text()).toEqual("count: 0");
      shallowComponent.find(sel("count-up")).simulate("click");
      expect(props.increment).toHaveProperty("callCount", 1);
    });
  });
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

さいごに

結局のところ、どのアプローチを採用した場合も実際の Redux store を使用した React コンポーネントのテストは高速です。Redux の設計は、Action、Reducer、State のそれぞれが互いに分離される方法のため、テストに非常に適しています。
このように Dependency Injection しやすい設計のもと Redux が作られているので、このようなシンプルな構造を取ることができました。今回はご紹介できませんでしたが、Redux SagaRedux Thunk を使用した場合のアーキテクチャでも同様にテストはシンプルに記述できます。React をとりまくエコシステムは素晴らしいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails+AxiosでCSRF対策用のトークンを使う設定

Rails+WebpackerのJavaScriptでAxiosを使うときは、POSTやPATCHで送信する際にCSRF対策用のトークンを指定する必要があります。送信のたびに指定するのは面倒なので、あらかじめ設定しておきましょう。

AxiosのInterceptorsの機能を使います。送信前にHTTPメソッドがPOST, PUT, PATCH, DELETEだったら、meta要素からトークンを取り出してHTTPヘッダにセットします。ここではjQueryを使っています。

サンプル: https://github.com/kazubon/blog-rails6-vuejs

app/javascript/axios_config.js
import Axios from 'axios';

Axios.interceptors.request.use((config) => {
  if(['post', 'put', 'patch', 'delete'].includes(config.method)) {
    config.headers['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content');
  }
  return config;
}, (error) => {
  return Promise.reject(error);
});

このJavaScritptをどこかで動かします。とりあえずapplication.jsに入れとけばいいでしょう。

app/javascript/packs/application.js
import '../axios_config';

あとは、ふつうにAxiosが使えます。

import Axios from 'axios';

Axios.post('/entries.json', { entry: this.entry });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

住所からGPS座標を、逆にGPS座標から住所を求める JavaScript (AppleScript)

概要

 住所から、そこのGPS座標(ジオタグ)を提供するWebサービスと、逆にGPS座標から住所名を提供するサービスがありますね。
 Google Maps の API などがありますが、無償で利用できるものとして、次の2つを選択しました。

住所やランドマーク名をGPS座標に変換するサービス

 Geocoding.jp
 住所だけでなく、ランドマーク名で、GPS座標が XML 形式で提供されます。

GPS座標から住所を求める

 農研機構の簡易逆ジオコーディングサービス
(従来の「https://www.finds.jp/」 は、2020年4月1日から完全に https://aginfo.cgk.affrc.go.jp/index.html.ja に移行するとのことですが、既にこのURLが使えるようになっています。)
 GPS座標を渡すと、住所が JSON 形式で提供されます。

ソース

 このソースは OS X の「スクリプトエディタ.app」の JavaScript で書いています。
これをスクリプトエディタで新規に JavaScript としてコピペして、ユーザーディレクトリ、ライブラリー、script フォルダーに保存しておくと、メニューバーのスクリプトアイコンから直接起動できて便利です。

住所やランドマークからGPS座標を求める JavaScript

adrs2gps_java.scpt
// 住所等から緯度、軽度を求める
// https://www.geocoding.jp/ のサービスを利用
// 戻り値は XML 形式

var app = Application.currentApplication()
app.includeStandardAdditions = true

var baseURL = 'https://www.geocoding.jp/api/?q='

var adr = app.displayDialog("住所またはランドマーク名を入力", {defaultAnswer: ""}).textReturned
var ascAdr = encodeURIComponent(adr)
var URL = baseURL + ascAdr

var ans = app.doShellScript('curl -s ' + URL)

if (ans.indexOf('error') > 0) {
    app.displayDialog('該当する住所が見つかりませんでした。')
    } else {
    var jyuusho = ans.split('<address>')[1].split('</address>')[0]
    var ido = ans.split('<lat>')[1].split('</lat>')[0]
    var keido = ans.split('<lng>')[1].split('</lng>')[0]
    app.displayDialog(jyuusho + 'の緯度,経度は\r' + ido + ', ' + keido + ' です。\r緯度,経度をクリップボードにコピーします。')
    app.setTheClipboardTo(ido + ',' + keido)
}


GPS座標から住所を求める JavaScript

gps2adrs_java.scpt
// 緯度、軽度から住所を求める
// https://www.finds.jp/ → https://aginfo.cgk.affrc.go.jp/ws/rgeocode.php のサービスを利用
// 2020/4/1 以降は「https://aginfo.cgk.affrc.go.jp/ws/rgeocode.php」に完全移行
// 戻り値は JSON 形式

var app = Application.currentApplication()
app.includeStandardAdditions = true

var baseURL = '"https://aginfo.cgk.affrc.go.jp/ws/rgeocode.php?json\&'
var latlon = app.displayDialog("緯度,経度を入力", {defaultAnswer: ""}).textReturned
var lat = latlon.split(',')[0]
var lon = latlon.split(',')[1]
var URL = baseURL + 'lat=' + lat + '\&lon=' + lon + '"'
var ans = app.doShellScript('curl -s ' + URL)
var jsonData = JSON.parse(ans)
var adrs = ''
switch (jsonData['status']) {
    case 400: app.displayDialog("データを取得できませんでした。"); break;
    case 200:
    case 202:
        var pname = jsonData['result']['prefecture']['pname']
        if (pname !== undefined) adrs = adrs + pname
        var mname = jsonData['result']['municipality']['mname']
        if (mname !== undefined) adrs = adrs + mname
        try {var sect = jsonData['result']['local'][0]['section'] }
        catch(e) {}
        if (sect !== undefined) adrs = adrs + sect
        try {var hnumb = jsonData['result']['local'][0]['homenumber'] }
        catch(e) {}
        if (hnumb !== undefined) adrs = adrs + hnumb
        try {var aza = jsonData['result']['aza'][0]['name'] }
        catch(e) {}
        if (aza !== undefined) adrs = adrs + aza
        app.displayDialog(lat + ', ' + lon + 'の住所\r' + adrs + '\nをクリップボードにコピーします。')
        app.setTheClipboardTo(adrs)
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML5 の pattern 属性で全角英数と半角カタカナを排除するバリデーションの正規表現サンプル

サンプルコード

サンプルコード前半の input 要素にて、全角英数と半角カタカナを排除するバリデーションを実施。
サンプルコード後半の script 要素にて、排除する範囲の文字を表示。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>The pattern attribute</title>
</head>
<body>
  <form method="get">
    <input
      type="text"
      name="foo"
      size="32"
      title="全角英数と半角カタカナは入力不可"
      placeholder="アイウエオ12345"
      pattern="^[^\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF61-\uFF9F]*$"
      required
    >
    <input type="submit" value="送信">
  </form>

<script>
document.write('全角数字: ');
for(let c=0xFF10; c<=0xFF19; c++){
  document.write(String.fromCodePoint(c));
}
document.write('<br>');
document.write('全角英字(大文字): ');
for(let c=0xFF21; c<=0xFF3A; c++){
  document.write(String.fromCodePoint(c));
}
document.write('<br>');
document.write('全角英字(小文字): ');
for(let c=0xFF41; c<=0xFF5A; c++){
  document.write(String.fromCodePoint(c));
}
document.write('<br>');
document.write('半角カタカナ: ');
for(let c=0xFF61; c<=0xFF9F; c++){
  document.write(String.fromCodePoint(c));
}
document.write('<br>');
</script>
</body>
</html>

実行結果

実行環境: macOS Catalina + Google Chrome 79.0.3945.130

入力前の表示

html5-input-pattern-1.png

未入力で送信ボタンを押した場合

html5-input-pattern-2.png

排除する文字が含まれている状態で送信ボタンを押した場合

html5-input-pattern-3.png

この例では title 属性で指定したテキストも表示されているが、このあたりの表示デザイン等はブラウザ依存になっている。「指定されている形式で入力してください。」が他のブラウザでは異なる文言になっていたり、「全角英数と半角カタカナは入力不可」というテキストが表示されなかったりするので注意。

排除する範囲の文字

今回のバリデーションで排除する文字の一覧 (サンプルが script 要素で出力している内容)。

全角数字: 0123456789
全角英字(大文字): ABCDEFGHIJKLMNOPQRSTUVWXYZ
全角英字(小文字): abcdefghijklmnopqrstuvwxyz
半角カタカナ: 。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚

Unicode コードポイントによる範囲。

  • 全角数字: 0xFF10 〜 0xFF19
  • 全角英字(大文字): 0xFF21 〜 0xFF3A
  • 全角英字(小文字): 0xFF41 〜 0xFF5A
  • 半角カタカナ: 0xFF61 〜 0xFF9F

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptのことをバックエンドと思い込んだ男子大学生がGoogle App Scriptで優勝する記事です

初めに

 自分の勤めている職場で、GASを使用した職場内の業務効率化をするプログラムの改善を自分がした際に行ったことをこの記事で挙げます。もしGasを使ってバックエンドっぽいことをしたい方は参考にしてみてください。(バックエンドといっても、SpreadsheetをDBみたいに使うだけです。あれ、これってバックエンドて言えなくね?

本題

今まで使用していた表の機能

  • スタッフごとに一か月間の日勤(昼間に働く)の時間、夜勤の時間、休憩時間の情報を取得し、表をGoogleSpreadsheetのシートへ作成・表示する。
  • スタッフの勤務状況はGoogleカレンダーで管理されているので、スタッフの勤務状況はGoogleカレンダーIDから取得する。
  • もし新規のスタッフの勤務状況を追加または既存のスタッフの勤務状況を削除して表を作成する際は、GASのコードへ直接追加するスタッフ名とそのカレンダーIDを追加するか、削除するスタッフ名とそのカレンダーIDを削除しなくてはいけない。⇒GASを扱える人がいないと追加と削除が難しい

改善する際の要件

  • 表を作成する際に、もしスタッフを追加または削除する場合はGASのコードへじかに書き込まなくても良い状態にする。

ではどうするか

SpreadSheatにDBの役割を持ったシートを作る。そこへ表に必要なスタッフ名とそのカレンダーIDを登録することで、もし追加または削除が起きた際でも該当者のシートへの追加または削除をすることにより対応する。

実際のコード

ただ関数の中に処理を書き込んでもよかったのですが、それだと面白くないし処理の変更にも対応しにくいので、行う処理ごとにMVC的な役割を持たせ、役割ごとに関数内の処理を定めてみました。(実際のファイル名はcode.gasですが、そのままここでコードを見せると文字の色が変わらず見づらいので、ファイル名をcode.jsにしています。)

Model

基本的にここではクライアント(表の作成者)からのデータの取得、DB周りの処理を行います。
ここで行うことは以下の7つです。

  1. DBの追加に関するクライアントからの入力値を取得する。
  2. DBの削除に関するクライアントからの入力値を取得する。
  3. クライアントからの入力を基にスタッフの追加を実行する。
  4. クライアントからの入力を基にスタッフの削除を実行する。
  5. 表の作成に関するクライアントからの入力値を取得する。
  6. 表を作成する際に必要なデータをDBから取得する。
  7. DBからスタッフ名とそのカレンダーのIDのデータを取得する。

GASでのコード

code.js
//1. DBの追加に関するクライアントからの入力値を取得する。
function getStaffInfo() {
  const name = Browser.inputBox('登録者の名前を入力してください','お名前', Browser.Buttons.OK_CANCEL);
  switch(name) {
    case 'cancel':
     Browser.msgBox("処理を終了します。");
     break;

    case "":
     Browser.msgBox("名前が空です。処理を終了します。",Browser.Buttons.OK);
     break;

    default:
      const getId = Browser.inputBox('登録者のカレンダーIDを入力してください。','ID', Browser.Buttons.OK_CANCEL);
      switch(getId) {
        case 'cancel':
          Browser.msgBox("処理を終了します。");
          break;

        case "":
          Browser.msgBox("カレンダーIDが空です。処理を終了します。",Browser.Buttons.OK);
          break;

        default:
          return [name, getId];
      }
  }
}

//2. DBの削除に関するクライアントからの入力値を取得する。
function getStaffName() {
  const name = Browser.inputBox('該当者の名前を入力してください','お名前', Browser.Buttons.OK_CANCEL);
  switch(name) {
    case 'cancel':
     Browser.msgBox("処理を終了します。");
     break;

    case "":
     Browser.msgBox("名前が空です。処理を終了します。",Browser.Buttons.OK);
     break;

    default:
     return name;
  } 
}

//3. クライアントからの入力を基にスタッフの追加を実行する。
function writeDB(sheet, array) {
  let n = 1;

  while(sheet.getRange(n, 1).getValue()) {
    n++;
    continue;
  } 

  for(let i=0; i < array.length; i++) {
         sheet.getRange(n, i+1).setValue(array[i]);
        }
}

//4. クライアントからの入力を基にスタッフの削除を実行する。
function deleteDB(sheet, name) {
  let n = 1;

  while(sheet.getRange(n, 1).getValue() !== name) {
    if(sheet.getRange(n, 1).getValue() === "") {
       Browser.msgBox("該当者がいません。処理を終了します。",Browser.Buttons.OK);
       return;
     }
      n++;
      continue;
    }
   sheet.deleteRow(n);
}

//5. 表の作成に関するクライアントからの入力値を取得する。
function getTime() {
  const year = Browser.inputBox('YYYYの形式で入力してください。例 2018','取得年', Browser.Buttons.OK_CANCEL);
  if (year == 'cancel'){
    Browser.msgBox("処理を終了します。",Browser.Buttons.OK);
    return;
  }
  if (year.match(/^20[0-9].$/) == null){
    Browser.msgBox("入力形式が違います。最初からやり直してください。",Browser.Buttons.OK);
    return;
  } else {
    const month = Browser.inputBox('MMの形式で入力してください。例 04 ','取得月', Browser.Buttons.OK_CANCEL);
    if (month == 'cancel') {
      Browser.msgBox("処理を終了します。",Browser.Buttons.OK);
      return;
    }
    if (month.match(/^(01|02|03|04|05|06|07|08|09|10|11|12)$/) == null) {
       Browser.msgBox("入力形式が違います。最初からやり直してください。",Browser.Buttons.OK);
       return;
    }
    return [year, month];
  }
 }

//6. 表を作成する際に必要なデータをDBから取得する。
function getInfoFromDB(sheet) {
  let info = {};
  let n = 1;

  while(sheet.getRange(n, 1).getValue() !== "") {
    const key = sheet.getRange(n, 1).getValue();
    const value = sheet.getRange(n, 2).getValue();
    info[key] = value;
    n++;
    continue;
  }
  return info;
}

//7. DBからスタッフ名とそのカレンダーのIDのデータを取得する。
function getResult(info, year, month) {
  let result = {}
  const startDay = new Date(year +'/'+ month + '/'+ 1);
  const lastDay = new Date(startDay.getFullYear(), startDay.getMonth() + 1,0, 0);//このままでは月の最終日が含まれない
  lastDay.setDate(lastDay.getDate()+1);//月の最終日を含める

  for(let key in info) {
       const calendar = CalendarApp.getCalendarById(info[key]);
       const events = calendar.getEvents(startDay, lastDay);
       let day = 0;
       let night = 0;
       let rest = 0;
       let money = 0;
    //スタッフごとの日勤の時間と夜勤の時間の合計を求める
     for(let i=0; i<events.length; i++) {
          const endTime = events[i].getEndTime().getHours();
          const workEnd = (events[i].getEndTime().getHours()*60 + events[i].getEndTime().getMinutes())/60;
          const workStart = (events[i].getStartTime().getHours()*60 + events[i].getStartTime().getMinutes())/60;
       //夜勤の場合
          if(endTime > 22.0) {
           day += workEnd - workStart - 1.0;
           night += workEnd - 22.0;
          } else {
       //日勤の場合
           day += workEnd - workStart;
          }
       //休憩時間の合計を求める
          const str = events[i].getDescription();//イベントの説明欄から休憩時間を取得
          const convert = str.replace(/[0-9]/gi, function(s) { return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);});//全角数字を半角文字へ変換

          switch(convert) {
            case '休憩:30分':
              rest += 0.5;
              break;

            case '休憩:1時間':
              rest += 1.0;
          }
         }
    //スタッフの月の給料を求める
       money += (day - rest)*1000 + night*1250;

       result[key] = {'day':day, 'night':night, 'rest':rest, 'money':money};    
      }
  return result;
}

View

ここでは行うのは、表をSpreadsheetへ作成することとSpreadsheetのバーに関数を登録することです。

GASでのコード

code.js
//関数の登録
function onOpen() {
  const myMenu=[
    {name: "スタッフを登録する", functionName: "addData"},
    {name: "スタッフを削除する", functionName: "deleteData"},
    {name: "勤務時間と給料の合計を出力", functionName: "resultController"}
  ];

   SpreadsheetApp.getActiveSpreadsheet().addMenu("メニュー",myMenu); 
}

//表の作成
function createResultTable(sheet, year, month, result) {
 sheet.getRange(2, 2).setValue(year+''+month+'');
 sheet.getRange(3, 2).setValue('スタッフ名');
 sheet.getRange(3, 3).setValue('通常勤務時間');
 sheet.getRange(3, 4).setValue('夜間勤務時間');
 sheet.getRange(3, 5).setValue('休憩時間');
 sheet.getRange(3, 6).setValue('今月の給料');
 sheet.getRange(3, 7).setValue('※通常勤務・休憩:1000円/h、夜間勤務:1250円/1hで換算');
 sheet.getRange(3, 2, 1, 5).setBackground('#fff2cc');

 let i = 1;
 for(let key in result) {
      sheet.getRange(3+i, 2).setValue(key);
      sheet.getRange(3+i, 3).setValue(result[key]['day']);
      sheet.getRange(3+i, 4).setValue(result[key]['night']);
      sheet.getRange(3+i, 5).setValue(result[key]['rest']);
      sheet.getRange(3+i, 6).setValue(result[key]['money']);
      i++;
     }
}

Controller

ここではModelとViewの処理を結びつけます。(Controllerとして扱っても良いのかな?)

GASでのコード

code.js
//DBの追加に関するController
function addData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('DB');
  const info= getStaffInfo();

  if(info == null) return;

  writeDB(sheet, info);
}

//DBの削除に関するController
function deleteData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('DB');
  const name = getStaffName();

  if(name == null) return;

  deleteDB(sheet, name);
}

//表の作成のController
function resultController() {
 let box;
  //年と月を取得
  box = getTime();
  if(box == null) return;//
  const timeData = {'year':box[0], 'month':box[1]};

  //DBシートからスタッフの勤務データを取得
  const sheetDB = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('DB');
  const staffData = getInfoFromDB(sheetDB);

  //記入の対象のシートを取得
  const sheet = SpreadsheetApp.getActiveSheet();

  //スタッフごとで勤務時間と給料の合計を取得
  const result = getResult(staffData, timeData['year'], timeData['month']);

  //表の作成
  createResultTable(sheet, timeData['year'], timeData['month'], result);
}

終わりに

 LaravelなどでMVCを扱おうと思ってもそんなに気軽にできないけど、GASであれば手軽&実用的にMVC(っぽいもの)を扱えるので、バックエンドの学習の一環としてGASをやるのは面白いな~と思いました。
実際のバックエンドを触るのも面白いですが、今後もGASでWebアプリケーションとかを作ってみるなどして、お手軽疑似バックエンドで遊んでみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptのfor in(オブジェクトのプロパティ名についての繰り返し)

for-in文は、オブジェクトに存在するプロパティについて、プロパティの名前を順不同で取り出していき、処理を実行する構文です。

for in の書き方

for([プロパティ名を格納する変数] in [オブジェクト]) {
  
}

指定したオブジェクトについて、プロパティ名を順不同で取り出し、inの前に定義した変数に格納してから処理を実行します。この時、繰り返しはすべてのプロパティについて処理を行うと終了します。

for in の例

var sum = 0;
var obj = { a:1, b:2, c:3 };
for(var name in obj) { // オブジェクトの中のプロパティ名を取り出す。
  sum += obj[name];
}

参考

http://www.ituore.com/entry/javascript-for#for-in%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E5%90%8D%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%81%AE%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js (サーバサイド JavaScript) 事始め

はじめに

TypeScriptを覚えたいのですが、まずはNode.jsの勉強から始めた方が良さそうな感じでしたので、Node.jsの勉強を始めました。

Noda.jsのインストール

公式サイトからインストーラーをダウンロードしてインストールします。

nodejs1.jpg

LTS版の方が無難だと思います。
Node.jsはmacやLinuxのイメージが強かったのですが、Windowsでも問題なく動きました。

環境

OS:Windows 10 Pro 64bit
DB:SQL Server 2019(Cent OS 8 on Hyper-V)
node.js:v12.16.1
npm:v6.13.4
Editor:Visual Studio Code

作業フォルダの作成

今回は「D:\Node」を作業フォルダにしました。

Hello World

作業フォルダに以下のファイルを作成します。

sample01.js
var http = require('http');

var server = http.createServer( function(req, res) { 
    res.writeHead( 200, {'Content-Type': 'text/plain'});
    res.write('Hello World'); 
    res.end(); 
}); 

server.listen(3000);
console.log('サーバを起動しました'); 

コマンドプロンプトを起動し、作業フォルダに移動します。

D:
CD Node

Node.jsを起動します。
起動方法は「node ファイル名」です

node sample01.js

問題がなければ、コマンドプロンプトに以下の様に表示されます。

サーバを起動しました

http://localhost:3000
にアクセスします
nodejs2.jpg

ブラウザに「Hello World」と表示されればOKです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt質問所、aLizサークルを開設しました!!

この度noteサークルを開設しました✨?
皆様の質問の、ぜ〜んぶに答えます!!!?

?こんな方にオススメです♪
・質問できるコミュニティがない…
・ググってみたけど分からない…
・やってみたもののエラーで動かない…
・初学者同士のコミュニケーションを取りたい!
・付け焼き刃の知識ではなく論理的に理解したい!

?参加リンク
aLiz CLUB
https://note.com/aliz/circle

?以下サークルの詳細です!
・Nuxtをメインとしたサービス
・Nuxt初心者のサポートが中心
・これからプログラミング始めようとしている方でも参加OK
・質問したり、ディスカッションをするのがメイン

?aLizのポートフォリオ はこちら
https://note.com/aliz/n/nf1d2b5e16bc1#SsL5X

?入りたい!
参加していただいた方を
aLizのSlackにご招待させていただきます。
noteサークル内の掲示板では
対応しておりませんのでご注意ください。
今後は、「Nuxt案件を受けたけど不安?」
そんな方のガッツリサポート版も作る予定です!
お楽しみに♪

?サークルに入れないという方はこちら!
まずnote IDを作る必要があるので
そちらの手順を細かく解説してくれています。
https://note.com/kanoyuri/n/n97c0f4191bb5

皆様のご参加
お待ちしております??

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの「演算子」を全種類言えますか? ~演算子とは何かを完全に理解する~

JavaScriptプログラムの基本的な構成要素のひとつが演算子です。多くの方が普段のJavaScriptプログラミングで演算子を使っているでしょう。

では、あなたは「演算子とは何か」という問いに答えられますか? 演算子と演算子以外の違いはどこにあるのでしょうか?

演算子とは何かという定義は、人によって考え方が違うでしょう。筆者の個人的な考えとしては、演算子は「1つ以上の式から別の式を構成する構文を特徴づけるトークン」であると考えています。

しかし、JavaScriptには仕様書があります。仕様書はJavaScript (ECMAScript) に関する最も信頼できる情報源ですから、何が演算子で何が演算子でないのかについてもたいへん強力な基準を与えてくれることが期待できます。

そこで、この記事では何が仕様書で演算子と呼ばれているのかについて全て解説します。併せて、演算子っぽいけど演算子とは呼ばれていないものについても解説します。この記事を読んで、「JavaScriptの演算子とは何か」について正確な理解を得ましょう。

ついでに、折角ですからそれぞれの演算子に対して簡単な解説を挟みます。初心者の方はJavaScriptの演算子を網羅的に理解する機会とし、中級者以上の方はこの機会に演算子について復習しましょう。

なお、演算子は英語でoperatorと呼ばれます。この記事では、仕様書でoperatorと呼ばれている構文を出てくるのが早い順に全て列挙します。この記事で参照するのはDraft ECMA-262 / February 15, 2020 版です。

グルーピング演算子 ( )

最初の演算子は( )です。これは(a + b) * 2とかに出てくる( )であり、計算の順序を定める効果を持ちます。

( )は演算子に見えないという方がいるかもしれませんが、12.2.10 The Grouping OperatorでGrouping Operatorと呼ばれているので演算子に数えます。

new演算子

newは、new Array(10)のような式に現れる演算子であり、クラス等のコンストラクタを呼びインスタンスを作る効果があります。この例の場合はlengthは10のArrayインスタンスとなります1

個人的にはこれが演算子というのは認めがたいのですが、仕様書が演算子であると言っているので演算子です(12.3.5 The new Operator

インクリメント・デクリメント演算子

++--です。これは文句なしに演算子ですね。12.4.4 Postfix Increment Operator12.4.7 Prefix Decrement Operatorが該当します。仕様上は前置と後置が別々の演算子として定義されています。まあ構文が違いますから妥当ですね。

foo++++foofooに1を足し、foo----fooは1を引きます。前置と後置の違いは返り値であり、前置の場合は計算の後のfooの値が、後置の場合は計算の前のfooの値が返ります。

delete演算子

12.5.3 The delete Operatorで定義されている演算子で、delete foodelete obj.fooのような構文を持ちます。(すでにnewが出ていますが)演算子といっても必ずしも記号ではないという例ですね。

delete演算子は与えられた変数やプロパティを削除する演算子です。

プロパティの削除

プロパティの削除は話が簡単です。delete演算子によって削除されたプロパティは文字通り存在しなくなります。次の例では最初obj{ foo: 123 }でしたが、deleteの後はobj{}と同じになります。

const obj = { foo: 123 };
console.log(obj.foo); // 123

delete obj.foo;

console.log(obj.foo); // undefined
console.log(obj.hasOwnProperty("foo")); // false

ただし、configurable属性がfalseに設定されたプロパティは削除できません。そのようなプロパティはObject.definePropertyなどの手段で作ることができます(第3引数のオブジェクトでconfigurable: trueを指定しなければデフォルト値のfalseとなります)。

const obj = {};
Object.defineProperty(obj, "foo", {
  value: 456
});
console.log(obj.foo); // 456

delete obj.foo; // エラーが発生(strictモードの場合)

configurablefalseの場合は、プロパティは消えませんがエラーも起こりません。

変数の削除

delete演算子は変数も削除できます。しかし、皆さんがこの機能を使うのは非常にまれでしょう。なぜなら、strictモードではdeleteで変数を消すのは禁止されており、文法エラーとして扱われるからです。

また、strictモードでなかったとしても、どんな変数でも消せるわけではありません。消すことができる変数はたいへん限られています。具体的には、消すことができるのはグローバル変数のみです。それも、varを使わずに宣言されたグローバル変数に限ります2。ちなみに、strictモードではvarを使わずにグローバル変数を作ることはできない(存在しない変数にいきなり代入するとエラーが発生する)ので、strictモードではdeleteは無意味となります。この意味でも、deleteによる変数の削除は非常に非strictモード的な機能であると言えます。

// varを使わずに作られたグローバル変数
a = 123;
// varを使って作られたグローバル変数
var b = 456;

delete a;
delete b;

console.log(typeof a); // "undefined" (aが消えた)
console.log(typeof b); // "number" (bは消えていない)

function foo() {
  var abc = 123;
  console.log(abc); // 123
  delete abc;       // abc はグローバル変数ではないので消されない
  console.log(abc); // 123
}
foo();

ただし、deleteが消すことができる変数はもう一つあります。それはeval経由で作られた変数です。この場合は、varで作られた変数であっても消すことができます。

function foo() {
  eval("var abc = 123;");
  console.log(abc); // 123
  delete abc;
  console.log(abc); // abcが存在しないのでエラーが発生
}

たとえevalで作られた変数であっても、上の例のfooの中で作られた変数abcfoo内のローカル変数となります。eval経由で作られた変数の場合、特別にローカル変数をdelete演算子で消すことができるのです。

delete演算子の返り値

delete演算子はを作る構文であり、式ということは値があります。delete演算子の返り値は真偽値であり、基本的には「削除に成功したらtrue、失敗したらfalse」です。ただし、最初から存在しない変数を削除しようとしていた場合はtrueになるので要注意です。

全く使わない演算子である割に何だか奥が深いですね。

void演算子

void 式という構文を持つ演算子で、式を評価するものの、結果を無視して常にundefinedを返します(12.5.4 The void Operator)。

そんなもの何に使うのかと思いきや、実はundefinedを得るという重要な使いみちがあります。undefinedを使いたいとき、void 0と書けば3文字節約できます。

また、単に文字数を節約するだけでなく、変数undefinedが信頼できない場面でも有効です。というのも、我々はundefinedと書けばundefinedという値を得ることができますが、これはundefinedという名前のグローバル変数がundefinedが入った状態で用意されているからです。ということは、undefinedという名前の変数を作ってしまえばundefinedと書いてもundefinedが得られないのです。voidはこのような状況でもundefinedを供給することができます。

{
  const undefined = "hi";

  console.log(undefined); // "hi"
  console.log(void 0); // undefined
}

例えば、TypeScriptで??演算子(左辺がnullまたはundefinedなら右辺を返し、それ以外なら左辺を返す)は次のようにトランスパイルされます。void 0が使われていることが分かりますね3

// トランスパイル前
const foo = bar ?? 0;
// トランスパイル後
const foo = (bar !== null && bar !== void 0 ? bar : 0);

ちなみに、nullは変数ではなくリテラルなので、nullと書けば常にnullであることが保証されます。

typeof演算子

typeof 式とすると、そのの値に応じていろいろな文字列が得られる演算子です(12.5.5 The typeof Operator)。プリミティブは細かに種類を判別することができますが、オブジェクトに関しては"object""function"の2種類の結果しかありません。プリミティブに対しては、"string", "number", "boolean", "bigint", "symbol", "undefined"という種類の結果が得られます。比較的有名な話ですが、typeof nullは歴史的経緯により"object"となります。これを"null"にしようという試みもありましたが、頓挫しました。

存在しない変数に対してtypeof 変数としてもランタイムエラーではなく"undefined"が得られることから、グローバル変数が存在するかどうかを判別する貴重な手段として重宝されています(これだとundefinedが入っているグローバル変数が存在している可能性も残りますが、それが問題になることはほとんどありません)。

単項+演算子

ここからはしばらく演算子らしい演算子が続きます。

12.5.6 Unary + Operatorで定義される単項+演算子は、+式の形で用いることで与えられた式の値を数値に変換します。

数値に変換するのはNumber(式)でもできるのですが、短いプログラムが好きな人達には+式が好まれています。これはTypeScriptでも認められており、TypeScriptでは+に渡された値がnumber型以外でも型エラーとはなりません(結果はnumber型になります)。

console.log(+"1e4"); // 10000
console.log(+true); // 1
console.log(+{ toString(){ return "-345" } }); // -345

ちなみに、与えられる式がBigIntの場合は+式Number(式)で違いがあります。

console.log(Number(12345n)); // 12345
console.log(+12345n); // ランタイムエラーが発生

これは、BigIntはNumberという明示的な方法を使って数値に変換しなければいけないということです。そうなっている理由は、BigIntからNumberへの変換では数値の精度が落ちる可能性がありバグの元となるため、暗黙のうちにこれが行われるのは望ましくないからです。

単項-演算子

単項演算子(12.5.7 Unary - Operator)は-式という形で、の符号を反転させた数値を得る演算子です。-はBigIntにも対応しており、符号が反転されたBigIntが返ります。一見対称性が高いように見える+-ですが、BigIntに関してはこのような非対称性があるのです。

const bignum = 12345n;
console.log(-bignum); // -12345n
console.log(+bignum); // ランタイムエラー

BigInt以外が渡された場合は+と同様に全部数値(Number)に変換されます(ただし、オブジェクトをプリミティブ値に変換してBigIntになった場合はBigIntとして扱われます)。

ちなみに、TypeScriptでは-演算子にnumber型・bigint型以外を与えるのはコンパイルエラーとなります。これも+と非対称的な点ですね。

~演算子

ビット反転の演算子です(12.5.8 Bitwise NOT Operator ( ~ ))。一見何も説明する必要が無さそうなビット反転演算子ですが、JavaScriptでは一筋縄ではいきません。二つほど説明すべきことがあります。

一つは、数値(Number)のときは数値は32ビットとして扱われるということです。JavaScriptではそれ以上の数値も扱うことができますが、ビット演算の場合はそれ以上のビットは切り捨てられます。また、結果は符号あり32ビット整数として解釈されます。

console.log(~0); // -1 (0xffffffff は2の補数表現で-1を表したものであるため)
console.log(~-1); // 0
console.log(~0x100000000); // -1 (32ビットを超えるので0と同様に扱われる)

もう一つはBigIntの場合についてです。~演算子はBigIntも扱うことができますが、BigIntは任意精度の整数であるためビット数という概念がなく、「全てのビットを反転する」などということができないのです。そのため、BigIntのビット反転演算は別の方法で定義されています。

6.1.6.2.2 BigInt::bitwiseNOT ( x )によれば、xがBigIntの場合「~xの値は-x-1nである」と定義されています。例えば、~123n-124nとなります。これは、通常の数値が32ビット整数に収まっている場合の挙動に一致しますから、それに倣ったものです。別の言い方をすれば、BigInt値を無限のビット数を持つ値であるとみなしてビット演算を行なっているとも言えます。

!演算子

これは言うことがあまりありません。与えられた式を真偽値に変換して真偽を反転させる演算子です(12.5.9 Logical NOT Operator ( ! ))。truefalseを逆にしたいときに使いましょう。また、値を真偽値に変換するのにBoolean(値)ではなく!!値として文字数を節約する人もいます。

**演算子

ES2016で密かに追加された演算子で、累乗を意味します(https://tc39.es/ecma262/#sec-exp-operator)。例えば2 ** 38です。また、この演算子は右結合で、x ** y ** zx ** (y ** z)として解釈されます。

この演算子は構文に関して面白い点が1つほどあります。それは、-2 ** 4のような式が構文エラーとなることです。つまり、**の左に単項+演算子・単項-演算子を使えないのです。これは、-2 ** 4(-2) ** 4なのか-(2 ** 4)なのか分かりにくいからです。エラーを避けるには、必要に応じてこのように括弧を補う必要があります。

ECMAScriptの比較的新しい機能では、このように解釈が曖昧な構文ができそうな場合に、どちらかに寄せるのではなくどちらも許さないというデザインが取られることがあります。**はその先駆けのひとつです。

**はNumberとBigIntのどちらにも対応していますが、BigIntは**の右が負の数の場合にランタイムエラーとなります。**の左が整数で右が負の数の場合結果が分数となり、BigIntでは表現できないからです。

ちなみに、JavaScriptでは0の0乗は1です。

*, /, %演算子

乗法の演算子です。仕様書では12.7 Multiplicative Operatorsとして3種類まとめて定義されています。算術演算子全てに言えることですが、NumberとBigIntを混ぜて演算することはできません。両辺が同じ型である必要があります。

割り算に関しては、0で割った場合の挙動がNumberとBigIntで異なります。Numberの場合は左辺の値に応じてInfinityやNaNといった値が発生しますが、BigIntの場合は0で割るとランタイムエラーです。

このようにBigIntの計算はランタイムエラーが発生しやすいものになっています。これはNaNなどに起因する変なバグをなるべくわかりやすく防ぎたいという方向性と、そもそもBigIntにはNaNとかInfinityの概念がないという現実によるものです。

+, -演算子

こちらは単項ではなく二項演算子の+-で、やはり仕様書では12.8 Additive Operatorsで定義されています。特に何の変哲もありませんが、+は足し算だけでなく文字列の連結にも使われます。

ビットシフト演算子

12.9 Bitwise Shift Operatorsで定義される3つの演算子<<, >>, >>>です。ビット演算なので、Numberの場合には32ビット符号付き整数として扱われるのも~の場合と一緒です。ビットシフト演算子はNumberとBigIntで大きく扱いが異なる演算子で、似たような計算でもNumberとBigIntで異なる結果となります。

console.log(1 << 33);   // 2
console.log(1n << 33n); // 8589934592n

console.log(4 << -1);   // 0
console.log(4n << -1n); // 2n

console.log(4 >> -2);   // 0
console.log(4n >> -2n); // 16n

BigIntの場合は話が単純で、ビットシフトはただ単に値に2の累乗数を左辺に掛けたり割ったりする計算となります。

一方Number型の場合は、まず右辺の数値(ビットシフトの距離を表す数値)が下位5ビットを取ることによって0〜31の整数に変換されます。

console.log(123 << 2); // 246
console.log(123 << 32); // 123 (32は0に変換されるのでビットシフトは起こらない)

>>>>>の違いは左辺が負の数のときに現れます。>>は符号を保ったままま右シフトが行われる一方、>>>は符号を無視します。また、>>>の結果は常に正の整数です。

console.log(-5 >> 1);  // -3
console.log(-5 >>> 1); // 2147483645

console.log(-5 >> 0);  // -5
console.log(-5 >>> 0); // 4294967291

>>>>>の挙動の違いは、最上位ビットの扱いと右ビットシフトにより空く部分をどうするかです。BigIntの場合は最上位ビットという概念がありませんが、これはBigIntでは>>>をサポートしないという方法で解決します。

console.log(-5n >> 1n);  // -3n
console.log(-5n >>> 1n); // ランタイムエラーが発生

6種類の比較演算子

12.10 Relational Operatorsでは、<, >, <=, >=, instanceof, inという6種類の演算子がRelational Operators (比較演算子)として紹介されています。最初の4種類はいいとして、残りの2つが異質ですね。正直、この6種類をひとまとめに扱う意味がよく分かりませんが、仕様がそう言っているのですからこれらは仲間なのです。

とはいえ、まずは最初の4種類から見ていきます。

数値・文字列の比較演算子

<, >, <=, >=は、数値の比較だけでなく文字列同士の比較を行うことができます。両辺が文字列だった場合、文字同士をコードポイント4で比較することによる辞書順で大小判定が比較されます。文字列と数値の比較になった場合は数値に寄せられます。

instanceof演算子

instanceof演算子は演算子の中で最も長いものです。obj instanceof Classで、objClassのインスタンスであるかどうかを判定して真偽値を返します。インスタンスであるかどうかの判定は、objのプロトタイプチェーンを辿ってコンストラクタにClassを持つものが存在するかどうかを調べることで行われます。

ただし、実はinstanceofの挙動はクラス側の[Symbol.hasInstance]メソッドで制御できます。

class Foo {
  static [Symbol.hasInstance](obj) {
    return obj.foo === 100;
  }
}
const obj = { foo: 0 };
console.log(obj instanceof Foo); // false

obj.foo = 100;
console.log(obj instanceof Foo); // true

in演算子

in演算子はオブジェクトが指定した名前のプロパティを持つかどうか判定して真偽値を返すプロパティです。Object.prototype.hasOwnPropertyとは異なり、プロトタイプチェーンを辿ってプロパティがあるかどうか判定します。

console.log("hasOwnProperty" in {}); // true

等値演算子

等価演算子は、12.11 Equality Operatorsで定義されている==, !=, ===, !==の4種類の演算子です。日本語で何と呼べば分からない演算子第一位(当社比)です。

基本的には===, !==だけ使っていれば事足りる場面がほとんどで、==!=は避けるべきです。これは、==は両辺の型が違う場合に型変換によって型を揃えて比較しようとするからです。詳しいことはこちらの既存記事をご参照ください。

ビット演算の二項演算子

ビット演算の二項演算子は12.12 Binary Bitwise Operatorsで定義されています。もうすこし短い訳があればいいのですが、「ビット演算の二項演算子」というのはいささか直訳的ですね。

これらもビット演算なので、Numberの場合は32ビット整数に変換の上ビット演算が行われる一方、BigIntの場合はビット数の制限がありません。

console.log((2 ** 32) | 1);    // 1
console.log((2n ** 32n) | 1n); // 4294967297n

二項論理演算子

二項論理演子と言われて何のことかお分かりでしょうか。これは12.13 Binary Logical Operatorsの直訳であり、||&&のことです。また、ES2020からは??が追加されました。??はプロポーザル時代はNullish Coalescing Operator(nullish合体演算子)と呼ばれていましたが、仕様書にはこの用語は出てきません。x ?? yという式はCoalesceExpressionと呼ばれているので、Coalescing OperatorとかCoalesce Operatorと呼ぶのはありかもしれません。

意味は今さらなので省略しますが、これらに共通する特徴は短絡評価です。つまり、左辺を先に評価して、結果が左辺となることが分かったなら右辺は評価されません。

結合の優先順位は&& > || > ??ですが、??の右に||&&が来ることはできません。

w && x || y ?? z   // OK ((w && x) || y) ?? z と同じ意味

w && x ?? y || z   // 構文エラーが発生
(w && x ?? y) || z // OK ((w && x) ?? y) || z と同じ意味

これは、||??は左辺の値が偽(あるいはnullish)の値だったときのフォールバックとして付加されることが多いところ、x ?? y || zとしたときの挙動が分かりにくいことによる処置です。

条件演算子

条件演算子はオペランドを3つ持つ唯一の演算子であり、それゆえ三項演算子と呼ばれることもあるやつです。仕様書を見れば、正式名称が条件演算子 (Conditional Operator) であることは一目瞭然ですね(12.14 Conditional Operator ( ? : ))。

代入演算子

代入演算子は=, +=, -=, *=, /=, %=, <<=, >>=, >>>=, &=, |=, ^=, **=の総称です()。

&&=||=は存在しませんが、これらを追加するというプロポーザルは存在します。2020年2月現在ではStage 2です。これらは+=などとは多少意味が違う点があります。具体的には、a += ba = a + bと同じである一方、a ||= ba || (a = b)と同じ意味になります。つまり、aが真なら代入すら行われないのです。このことは、セッタによって代入に副作用が仕込まれていた場合に目に見える違いとなって現れるのです。

コンマ演算子

JavaScriptでは,は色々な場面で使われますが、実は演算子としての,も存在します。それがコンマ演算子(12.16 Comma Operator ( , ))です。コンマ演算子は一番結合度の低い演算子となっています。

x, yという式は、xyを順に評価してyの値を返します。xは評価こそされるもののその結果は使用されません。これは、何だかvoid演算子と似ているところがあります。コンマ演算子を我々が普段使うことはほとんどありませんが、適当なminifierを用いてソースコードを最小化するとコンマ演算子が結構使われているのを見ることができます。

無理やりコンマ演算子を使ってみるとこんな感じです。

for (let i=0; i < 10; i++) console.log("i is"), console.log(i);

for文のボディ部分は{ }を使わなければ書ける文はひとつだけですが、コンマ演算子を用いると複数の関数呼び出しを一つの式で表すことができます。一つの式で表すことができればそれは式文というひとつの文になりますから、{ }無しでfor文の中に複数の関数呼び出しを書くことができるのです。こうすると、{ }で関数呼び出しを並べるよりも文字数を2文字ほど省略できます。たいへん嬉しいですね。

コンマは他の構文要素でも使われる記号ですから、コンマ演算子が活躍できる場所は限られています。例えば、関数呼び出しの引数リストの中ではコンマ演算子は使えません。,は引数の区切りとして扱われるからです。無理やりコンマ演算子を使うには、( )を使ってひとつの式であると明示する必要があります。

f(x, y);   // (x, y)というひとつの式を渡しているのではなくxとyという2つの式を渡している
f((x, y)); // (x, y)というひとつの式を渡している

以上でECMAScriptで演算子 (operator) と呼ばれているものを全て列挙できました。

演算子なのかどうか微妙なもの

仕様書はJavaScriptという言語を定める絶対的な基準となる文書ですが、人間が作るものですからミスもあります。仕様書には、演算子でないと思われるものを演算子と呼んでいるように見える箇所が2箇所あります。折角ですから、この記事で触れておきます。

super

Table 7: Additional Essential Internal Methods of Function Objectsは関数オブジェクトが持つ内部スロットを定義しています(内部スロットについてはJavaScriptの「継承」はどう定義されるのか? 仕様書を読んで理解するで詳しく解説しています)。その2行目、[[Construct]]内部メソッドの説明に次のように書かれています(強調は筆者)。

Creates an object. Invoked via the new or super operators.

newまたはsuper演算子」と書かれており、superが演算子であるかのような記述があります。一方、superは仕様書の12.3.7 The super Keywordで定義されており、こちらでは「superキーワード」と呼ばれていますからsuperは演算子でないと思われます。

ちなみに、superはクラス宣言の中で使うことができる構文であり、コンストラクタの中のsuper()で親クラスのコンストラクタを呼び出したり、super.fooで親クラスのプロパティ・メソッドを参照することができます。

Optional Chaining

Optional ChainingはES2020で追加された?.という構文で、foo?.barfoo.barと似た動作をしますが、foonullundefinedのときはエラーにならずに結果がundefinedとなります。Optional Chainingについては以下の記事で詳しく解説しています。

仕様書では?.による構文はOptional Chainとだけ呼ばれており(12.3.9 Optional Chains)、演算子とはされていません。しかしながら、一箇所だけ?.を演算子と呼んでいる箇所があります。それは12.3.1.1 Static Semantics: Early Errorsです。NOTEの中に次のような記述があります(強調は筆者)。

...so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without optional chaining operator:

foo.bar.は演算子とはどこにも書かれていませんから、そこから類推すると?.が演算子とは考えにくいですね。上記の記述はミスであると考えられます。

演算子ではないもの

ここまでで触れられなかったものは、仕様書でまったく演算子と呼ばれていないので明らかに演算子ではありません。どのようなものが演算子でないのか確認しましょう。

関数呼び出しの( )

func(1, 2, 3)のような関数呼び出しに出てくる( )は演算子ではありません。個人的にはnew Class(1, 2, 3)newが演算子であることを考えると少し違和感がありますが、我々ごときが感じる違和感など仕様書の前には塵ひとつほどの価値しかありません。

プロパティアクセスの.?.

?.は先ほど見たように一箇所だけ演算子と呼ばれているため微妙ですが、.についてはまったく演算子と呼ばれていません。

各種リテラル

次の式にあるような{ }[ ]は演算子ではなく、リテラルと呼ばれています。

const obj = { foo: 0 };
const arr = [1, 2, 3];

括弧だから演算子ではないというわけでもないのが難しいところです。最初に見たように( )は演算子でした。

yieldawait

これらはyield 式await 式という形で使うことができる式であり、一見すると演算子でもおかしくありません(構造としてはtypeof 式などと全く同じです)。

しかし、仕様ではこれらは演算子とはされず、YieldExpressionAwaitExpressionのように定義されていますから、「yield式」や「await式」と呼ぶのが正しそうです。

スプレッド構文・レスト構文の...

ES2015以降、JavaScriptには...という構文がたびたび登場します。...はスプレッド構文とレスト構文に大別され、前者は配列リテラルやオブジェクトリテラルの中、そして関数呼び出しの引数リストの中で使われる一方、後者は分割代入の配列・オブジェクトパターンや、関数宣言時の引数リストで使われます。

...は「スプレッド演算子」のように呼ぶ人が多くいるものの、仕様書では全く演算子と呼ばれていません。...を演算子と呼んでいた方はぜひ今日から改めましょう。

これが演算子ではない理由は、「式を作らない」からであると推察しています。すなわち、...は配列リテラルなど、他の構文の一部としてしか使うことができず、単独で式をなすものではないのです。

const arr = [...foo]; // ←これはOK
const arr = ...foo;   // ←こんな構文は無い

変数宣言の=

先ほど解説した通り=は代入演算子の一種ですが、実は変数宣言のときの=は厳密には代入演算子ではありません。

let foo = 123; // ←この = は代入演算子ではない
foo = 456;     // ←この = は代入演算子

前者は変数宣言の構文の一部(Initializer)として、代入演算子とは別個に定義されているのです。また、関数引数などのデフォルト値の定義にも=が使われますが、こちらもInitializerであり代入演算子ではありません。

実際に仕様書を見ると、Initializerの定義には次のよう書かれています(画像で引用)。

initializer.png

一方で、代入演算子の=AssignmentExpressionの定義の中で定義されています(画像で引用)。

assignment-expression.png

この2つを見ると、それぞれに=が使われており、同じ=という記号でも出自を異にするものであることが分かります。我々が普段=を使う際はそんなことをいちいち気にしなくても何とかなってしまうのですが。

まとめ

この記事ではECMAScriptに存在する「演算子」を全種類(ES2020時点)解説しました。併せて、演算子っぽいが演算子ではないものも解説しました。

結局演算子と演算子でないものの違いは何なのかという問いについては、残念ながら単純明快な答えはありません。演算子は必ず式を作るという点は間違い無さそうですが、式を作るなら何でも演算子というわけではありません(foo.bar.await式のawaitは演算子ではありませんでしたね)。演算子であると定義されているものが演算子であると言うほか無いのです。

いずれにせよ、どれが演算子でどれが演算子でないかを丸暗記することに意味はありません。プログラムを書く身としては、どんな方法でもいいのでJavaScriptの構文を理解し、使いこなせるようになることが重要なのです。

その一方で、仕様書はJavaScriptプログラミングにおいて唯一完全に信頼できる情報源であり、質の高いプログラムを書くにあたって仕様書が果たす役割は小さくありません。ですから、JavaScriptの解説記事を書くような場合には演算子とそれ以外の区別ができていない(=仕様書を理解していない)と非常に信頼性が低くなります。皆さんもJavaScriptの解説記事を書くときはぜひ正確性に気をつけましょう。また、JavaScriptの解説記事を読む際も演算子をひとつの試金石としてみるのはいかがでしょうか。


  1. 配列の場合は実際にはただのインスタンスではなくexotic objectが作られるのでちょっと特殊ですが。 

  2. 関数の中ではないトップレベルのコードにおいては、varで宣言された変数はグローバル変数として扱われます。一方、letconstで宣言された変数はグローバル変数として扱われません。 

  3. nullまたはundefinedであることを判定するならbar != nullでいいのではと思われたかもしれませんが、!=を使うとdocument.allがdocument.all == nullと判定されてしまうのでまずいのです。 

  4. JavaScriptの文字列はUTF-16でエンコーディングされているため正確にはコードユニットですが。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PlayCanvasでサンディちゃんを歩かせるゲームを作るぞ☆(第6回)~他エンティティのイベントを拾う~

こんなん作ってます。

https://playcanv.as/b/iAPwWXqY/
※本解説よりも開発が進んでいることもございますのでご了承ください。

一つのエンティティの衝突判定をUI用のエンティティに伝えるにはどうする?

例えばゴールポスト的な赤い円柱があって
スクリーンショット 2020-02-24 1.10.38.png

これに触れるとダイアログがでる。
スクリーンショット 2020-02-24 1.10.56.png

PlayCanvasは各エンティティにスクリプトを持たせてプログラミングされますが、円柱とUIは違うエンティティです。

どうやってエンティティ間でやりとりをしたらよいのでしょうか?

答えはユーザマニュアル>スクリプティング>通信にありました。
https://developer.playcanvas.com/ja/user-manual/scripting/communication/

通信ってのはなかなか聞かない表現かもしれませんが、他のエンティティのイベントを拾ってくることだと考えてください。

スクリプト間の通信には

  • スクリプト属性で参照づけられているエンティティのみから拾ってくるイベント
  • メインアプリケーション(this.app)を中央ハブとして、全てのエンティティから拾ってこれるアプリケーションイベント

の2種類があります。
どうみても後者が便利です(笑)

エンティティにサンディちゃんが衝突した際にUI部品へ衝突判定を伝えるよう、アプリケーションイベントを使ってプログラミングしていきます。

衝突判定

以下のスクリプトを作成します。

hitSandy.js
var HitSandy = pc.createScript('hitSandy');

// initialize code called once per entity
HitSandy.prototype.initialize = function() {
    this.entity.collision.on('triggerenter', this.onTriggerEnter, this);
};

// update code called every frame
HitSandy.prototype.update = function(dt) {

};

HitSandy.prototype.onTriggerEnter = function(entity) {
    this.app.fire('hitSandy:hit', this.entity.name, entity.name);
};

このスクリプトが適用されているエンティティの当たり判定に、何かしら剛体が衝突した際に「hitSandy:hit」というイベントを発火するようにしました。

ちなみに「hitSandy:hit」イベントを受け取った側は衝突した2つのエンティティの名前を受け取ることができます。

このスクリプトをPlayCanvasでサンディちゃんを歩かせるゲームを作るぞ☆(第4回)で作成したtemplatesディレクトリ内の「start」「goal」エンティティに適用してください。

(例)
スクリーンショット 2020-02-24 1.39.24.png

衝突判定をUIで表示する

イベントの通知を受け取るスクリプトの初期化関数でイベントの受け取りと、イベントを受けたときのイベントハンドラを作成。
今回の場合、当たり判定の衝突を検知したら「onSandyHit」関数をコールする。
衝突イベントからは衝突した2つのエンティティの名前も届いて、「onSandyHit」関数ではname1,name2の引数がそれを受け取るようになっています。

ui.js
// initialize code called once per entity
Ui.prototype.initialize = function() {
・・・
    // initializeメソッドに以下を追加
    // listen for the player:move event
    this.app.on('hitSandy:hit', this.onSandyHit);

    // remove player:move event listeners when script destroyed
    this.on('destroy', function() {
        this.app.off('hitSandy:hit', this,onSandyHit);
    });
}

//onSandyHitメソッドを新規追加
Ui.prototype.onSandyHit = function(name1,name2) {
    // 第1引数は衝突された側のエンティティの名前、第2引数は衝突したエンティティの名前
    // メッセージを作成。
    var caption = "";
    var message = "";
    if(name1=="start"){
        caption = "START";
        message = name2 + " start!!";
    }else if(name1 == "goal"){
        caption = "GOAL";
        message = name2 + " reach the goal!!";
    }

    // ダイアログにメッセージを載せる
    var disp_dialog = document.getElementsByClassName('pos_top_center')[0];
    disp_dialog.style.display = "block";
    disp_dialog.getElementsByClassName('caption')[0].innerHTML = caption;
    disp_dialog.getElementsByClassName('message')[0].innerHTML = message;

};

これでLaunchしてみましょう。

START
スクリーンショット 2020-02-24 16.26.20.png

GOAL
スクリーンショット 2020-02-24 16.27.01.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascript(node.js)でのsubstring

substringメソッド(引数一つ)

文字列 . substring ( 開始位置 )

・引数の「開始位置」から最後の文字までを返します。
・最初の1文字目の位置は0から始まります。
・Stringオブジェクトのメソッドです。

const a = "あいうえお";

console.log(a.substring(2)); // うえお

console.log(a.substring(3)); // えお

console.log(a.substring(4)); // お

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascript(node.js)のsetHeaderとwriteHeadの違い

setHeader

ヘッダー情報を設定する

response.setHeader(名前,);

writeHead

ヘッダー情報を出力する

response.writeHead(コード番号,メッセージ);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TweenMaxで「0→任意の値→0」という値の変化をつくる

TweenMaxで「0→任意の値→0」という値の変化をつくる

hoge.js
var obj = {
  propA: 0,
}

var forth = 10;
var tm = TweenMax.to(obj, 5, {
        onUpdate : function(){
            //進捗を0-1の値で返すtweenmaxのメソッド
            var progress = this.progress();
            //三角関数で0から180度のとき0→1→0を返すことを利用
            var num = Math.sin(progress * 180 * Math.PI / 180);
            //強度の値をかけることで、任意の時間に「0→任意の値→0」という値の変化をつくる
            var final = num * forth;
            final = Math.round(final * 100)/ 100;
            obj.propA = final;
            console.log(obj.propA);
        }
    });

もっと簡単な方法がある気がするす
toを続けて書く...?しかし任意のイージングのなかで変化させたい...ゆえにこのように記述しています

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack入門して仲良くなりたい~Plugin編~

仲良くなりたい

今回の記事ではBabelPluginについて書きます。

Babel

突然出てきたBabel…
まずは、役割の確認を行います。

Babel is a JavaScript compiler
Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Here are the main things Babel can do for you:

引用: https://babeljs.io/docs/en/

要するに、最新のJavaScriptの記述を汎用的に使えるものに変換(トランスパイル)してくれるみたいです。

// アロー関数(変換前)
[1, 2, 3].map((n) => n + 1);

// 変換後
[1, 2, 3].map(function(n) {
  return n + 1;
});

これを導入することで、開発時は最新の記法を使用するがブラウザに反映させるファイルは変換しているので、ファイルの考慮がいらないというわけです。

Babel導入

babel

それでは早速、導入していきます。
必要なパッケージを入れます。

npm i -D @babel/core@7.8.4 babel-loader@8.0.6 
npm i -S @babel/cli@7.8.4 @babel/preset-env@7.8.4

ちなみに前回の記事で紹介したts-loaderを使えばTypeScriptをES5にコンパイルしてくれるっぽいです。ここの章はBabelの基本的な動作が見たいのでJavaScriptで記述します。

babel-core

babelの本体です。

babel-loader

おなじみのローダーですね。
バンドルが実行される前にbabelを作動させるためのloaderです

babel-preset-env

トランスパイルするためのパッケージです。

webpack.config.jsの設定追加

webpack.config.js
const path = require("path");
const outputPath = `${__dirname}/dist`;

// モジュールどうやって出力するか記述します
module.exports = {
  // 実行環境設定
  mode: "development",
  // エントリー ポイントを決めます
  entry: "./src/app.js", //js
  output: {
    // どこにバンドルファイルを出力するか決まます
    path: outputPath,
    // 出力先のファイル名を決めます
    filename: "bundle.js"
  },
  // loaderの設定
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      }
    ]
  },
  devServer: {
    contentBase: outputPath
  },
// bundle.jsのコードを見やすくする
  devtool: "inline-source-map"
};

.babelrc

このファイルでは、babelのコンパイル方の設定を行います。

{
  "presets": ["@babel/preset-env"]
}

トランスパイル実行

今回バンドルするファイルはアロー関数を記述したものを使います。

allow.js
export const allow = array => {
  var mappedArray = array.map(n => n + 1);
  return mappedArray;
};

buildを実行して、Babelによってトランスパイルされたファイル部分を見てみます

bundle.js
/***/ "./src/allow.js":
/*!**********************!*\
  !*** ./src/allow.js ***!
  \**********************/
/*! exports provided: allow */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "allow", function() { return allow; });
var allow = function allow(array) {
  var mappedArray = array.map(function (n) {
    return n + 1;
  });
  return mappedArray;
};
/***/ }),

なんだかよさげですね!

html出力を自動化

今までは、distフォルダ内のindex.htmlを事前に用意していましたが、これもビルド時に自動生成するようにします。

html-webpack-plugin

ここでプラグインのを使ってこの問題の解決を目指します。プラグインとはwebpackを拡張するものと考えてよさそうです。
htmlファイルの自動生成にはhtnl-webpack-pluginが必要なので早速導入します。

npm install -D html-webpack-plugin@3.2.0

次にwebpack.config.jsの編集を行います。

webpack.config.js
const path = require("path");
const outputPath = `${__dirname}/dist`;
// プラグインの読み込み
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/app.js",
  output: {
    path: outputPath,
    filename: "bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      }
    ]
  },
  devServer: {
    contentBase: outputPath
  },
  devtool: "inline-source-map",
  // ここを追加
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html"
    })
  ]
};

plugins

ここのプロパティは配列で、使用するプラグインを追加していきます。

ここまで編集が終えたら実行してみてください。
distフォルダ内にbundle.jsindex.htmlが出力されているはずです。

最後に

ここまで、webpackを使っていろいろ遊んでみました。
まだまだ、多くのLoaderやPluginが存在します。
ですが、これだけ遊んだら基本的なことがわかっているので調査も身近なものに感じると考えています。

次は、React入門して仲良くなりたいと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Three.jsの超基本2

前回の記事に引き続き、学習した内容のド忘れ防止のためにThree.jsの基本的な使い方をまとめます。

前回の記事はこちら -> Three.jsの超基本

Group(グループ)

複数のMesh(物体)を、まとめて管理できる。
Group化すると、複数の物体を同時に動かすことができる。

(function() {
  var scene;
  var head;
  var body;
  var group;

  // ステージを追加
  scene = new THREE.Scene();

  // 物体を作成
  head = new THREE.Mesh(
      new THREE.BoxGeometry(20, 20, 20),
      new THREE.MeshLambertMaterial({ color: 0xff0000 })
  );
  head.position.set(0, 40, 0);

  body = new THREE.Mesh(
    new THREE.BoxGeometry(40, 60, 40),
    new THREE.MeshLambertMaterial({ color: 0xff0000 })
  );
  body.position.set(0, 0, 0);

  // グループを作成し、headとbodyを追加
  group = new THREE.Group();
  group.add(head);
  group.add(body);

  // ステージにグループを追加
  scene.add(grooup)
})();

OrbitControls(カメラを制御)

カメラを自動で動かしたり、マウス操作でカメラを動かすことができます。

マウス操作でカメラを動かす

(function() {
  var stage = document.getElementById('stage');
  var controls;

  // カメラコントローラを作成(カメラ, DomElement)
  controls = new THREE.OrbitControls(camera, stage);
})();

自動でカメラを動かす

(function() {
  var stage = document.getElementById('stage');
  var controls;

  controls = new THREE.OrbitControls(camera, stage);
  // カメラの自動回転
  controls.autoRotate = true;
  // 回転スピードの調整
  controls.autoRotateSpeed = 8.0;

  function render() {
    // 60fpsの間隔で繰り返し処理を実行(描画する)
    requestAnimationFrame(render);
    // カメラを再描画
    controls.update();
    // 描画する
    renderer.render(scene, camera);
  }

  render();
})();

Shadow(影をつける)

光源に対して物体の影を、別の物体に落とすことができる。

(function() {
  // 影を有効化
  renderer.shadowMap.enabled = true;
  // ライトの影表示を有効化
  light.castShadow = true;
  // 物体の影を有効化
  box.castShadow = true;
  // 影を受ける設定を有効化
  plane.receiveShadow = true;
  // カメラの領域設定
  light.shadow.camera.position.set(0, 100, 0);
  light.shadow.camera.left   = -200;
  light.shadow.camera.right  =  200;
  light.shadow.camera.top    =  200;
  light.shadow.camera.bottom = -200;
})();

カメラヘルパーで領域を可視化する

var shadowHelper = new THREE.CameraHelper(light.shadow.camera);
scene.add(shadowHelper);

Texture(テクスチャを物体に貼り付ける)

作成した物体に、画像を貼り付けることができる。
※サーバーを立てずに行うと、クロスドメイン系のエラーが発生する場合がある。

(function() {
  var loader = new THREE.TextureLoader();

  // 先にテクスチャを読み込んでから物体を生成する
  loader.load('img/logo.png', function(texture) {
    createBox(texture);
    render();
  });

  function createBox(texture) {
    box = new THREE.Mesh(
    new THREE.BoxGeometry(40, 40, 40),
    // テクスチャを貼り付ける際は「map」を使う
    new THREE.MeshBasicMaterial({ map: texture })
    );
    box.position.set(0, 0, 0);
    scene.add(box);
  }
})();

Font(3Dテキストを描画する)

指定したフォントで、3Dテキストを描画することができる。

(function() {
  var loader = new THREE.TextureLoader();

  // フォントファイルを読み込んだ後に物体を生成する
  loader.load('js/helvetiker_regular.typeface.json', function(font) {
    createText(font);
    render();
  });

  function createText(font) {
    text = new THREE.Mesh(
    new THREE.TextGeometry('SampleText!!', {
      // フォントを指定
      font: font,
      // フォントサイズを指定
      size: 24,
      // フォントの厚みを指定
      height: 4
    }),
    // 「side: THREE.DoubleSide」で両面を描画する
    new THREE.MeshBasicMaterial({ color: 0xf39800, side: THREE.DoubleSide })
    );
    text.position.set(-100, 0, 0);
    scene.add(text);
  }
})();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebase 条件分岐をしてデータの読み取り(Firestore)

はじめに

FirebaseのデータベースFirestoreで、条件分岐をしてからデータを取得する例です。
また、取得したデータの数をカウントしてます。

私の環境
・Ionic + Angular

whereを使う

eventの中身がこんな感じだとする。

event.model.ts
export class Event {
         constructor(
           public id?: string,
           public title?: string,
           public periods?: boolean,
         ) {}
       }
home.page.ts
import { AngularFireAuth } from '@angular/fire/auth';
import { AngularFirestore } from '@angular/fire/firestore';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage implements OnInit {
private eventCount;
 private periodCount;


constructor(
    private afAuth: AngularFireAuth,
    private db: AngularFirestore
  ) { }

  ngOnInit() {
    this.afAuth.auth.onAuthStateChanged((user) => {
      if (user != null) {

      // events以下の全てのドキュメントを取得してカウント
        this.db
          .collection(`users/${user.uid}/events`)
          .snapshotChanges()
          .subscribe(c => {
            this.eventCount = c.length;
          });

      // events以下のドキュメントでperiodsプロパティがtrueのドキュメントを取得
        this.db
           .collection(`users/${user.uid}/events`, ref =>
             ref.where('periods', '==', true)
           )
           .snapshotChanges()
           .subscribe(c => {
             this.periodCount = c.length;
           });
      }
    });
  }

参考

https://stackoverflow.com/questions/49026589/angular-firestore-where-query-returning-error-property-does-not-exist-on

}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 演算子⓵ 四則演算+?

はじめに

この記事では、四則演算や余剰(余り)に使用する演算子について解説します。

四則演算

sample.js
let num;
let string;

num = 5 + 3; //変数numに5と3の和を代入する

string = "ES" + "6"; //変数stringに"ES"と"6"を連結した文字列を代入する

string = "ES" + 6; //変数stringに"ES"と6を連結した文字列を代入する

num = 7 - 2; //変数numに7から2を引いた値を代入する

num = 4 * 5; //変数numに4と5の積を代入する

num = 6 / 2; //変数numに6で2を割った値を代入する

演算子はそれぞれ役割があります。
+
数値の加算、文字列の連結
-
数値の減算
*
数値の乗算
/
数値の除算

余剰、べき乗、その他

sample.js
let num;
num = 5 % 2; //変数numに5で2を割った余りの値を代入する

num = 10 ** 2; //変数numに10の2乗を代入する

num = 10++; //変数numに10を代入し、10+1

num = ++10; //変数numに10+1を代入する

num++; //変数num+1

num = 6--; //変数numに6を代入し、6-1

num = ++6; //変数numに6-1を代入する

num--; //変数num-1

let string = "3";
let num2 = 5;

num = +string; //変数numに、数値の変数stringを代入する

num = -num2; //変数numに、負の値の変数num2を代入する

これらの演算子の役割は、以下の通りです。
%
数値を割った余り(余剰)
**
数値のべき乗
++
数値に+1 前置記法なら先に+1して値を返し、後置記法なら値を返してから+1
--
数値に-1 ++と同じく、前置記法と後置記法がある
+
数値を正の数にする 文字列でも可能
-
数値を負の数にする 文字列でも可能

最後に

⓶では、型を調べる演算子をとりあげようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるプログラミング学習ログ 247日目

100日チャレンジの247日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。

すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

247日目は、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのプロトタイプチェーンを深堀りする

プロトタイプチェーンとは

オブジェクトが継承元プロトタイプを参照し、そのプロトタイプがまたその継承元プロトタイプを参照し.....のような連鎖のことをプロタイプチェーンといいます。

プロタイプチェーンの仕組み
1. 指定したオブジェクトでプロパティの存在を調べる
2. なかった場合__proto__が参照する先で存在を調べる
3. それでもなかった場合__proto__が参照する・・・(ループ)
4. 最終的にnullになるまで行う。nullならundefinedを返す

図で理解するJavaScriptのプロトタイプチェーン - Qiitaより引用

配列のプロトタイプチェーン

図で理解するJavaScriptのプロトタイプチェーン - Qiitaで説明されていた配列のプロトタイプチェーンについて補足・解説します。
※以下のコード及び画像は記事内より引用です。

配列を生成
 var myArray = [];
 myArray.push("hoge"); // これはプロトタイプチェーン先のメソッドから呼べる
 myArray.isArray(); // undefined()を実行しているようなもの
 Array.isArray(myArray); // true isArrayの使い方はこう

image.png

① 配列の定義

空の配列を定義
 var myArray = [];

これはmyArray配列を定義、つまりmyArrayインスタンスを生成している以下の記述と同義です。

Arrayオブジェクトのインスタンス化
var myArray = new Array();


それと同時に、コンストラクタ関数であるArrayオブジェクトprototypeが、myArray.__proto__に代入されます。

__proto__は全てのオブジェクトが持つ、内部プロパティ。そのオブジェクト自体のprototypeへの参照を持つ。

なるほど。。__proto__とprototypeの違い | 武骨日記より引用

同時に起こる
myArray.__proto__ = Array.prototype;   // Arrayオブジェクトのプロトタイプを代入

② pushメソッドはどこにある?

配列に要素を追加するpushメソッド
 myArray.push("hoge");

myArrayオブジェクト自体にはpushメソッドは存在しません。
オブジェクトにプロパティが存在しない場合、プロトタイプチェーンの仕組みにより、そのオブジェクトの__proto__が参照する先でプロパティの存在有無を確認します。

①よりmyArray.__proto__ === Array.prototype;なので、myArrayArray.prototypeが持つpushメソッドを参照することができます。

myArrayがpushメソッドを使える理由
myArray.push("hoge");  // myArrayオブジェクトにはpushメソッドは存在しない。

  // myArray.__proto__を参照する。

myArray.__proto__ === Array.prototype  // true

  // Array.prototypeにpushメソッドがないか確認する。

Array.prototype.push  // Array.prototypeにはpushメソッドが存在するので、myArrayはpushメソッドを使える。

③ myArray.isArray()はなぜエラーになる?

なぜundefined?
 myArray.isArray(); // undefined()を実行しているようなもの

isArray()とは、コンストラクタ関数Arrayが持つメソッドの一つです。
myArrayはそのプロトタイプを含めてisArray()を所持していないので、③式はundefinedになります。

myArrayインスタンスはisArrayメソッドを参照できない
myArray.isArray();  // myArrayオブジェクトにはisArrayメソッドは存在しない。

myArray.__proto__ === Array.prototype  // myArray.__protoを参照するが、Array.prototypeにもisArray()は存在しない。

Array.prototype.__proto__ === Object.prototype  // Array.prototype.__protoを参照するが、Object.prototypeにもisArray()は存在しない。

Object.prototype.__proto__ === null  // Object.prototype.__proto__はnullなので③式はundefinedとなる。

ほぼ全てのオブジェクトはObject.prototypeを始祖に持ち1Object.prototypeプロトタイプチェーンの終端となります。

④ isArray()を正しく使うには?

インスタンスはconstructorプロパティを持っています。
そしてそのconstructorプロパティは、インスタンス化の元となったオブジェクト(継承元)のコンストラクタ関数を参照します。

つまり、myArray.constructorはその元となったオブジェクトであるArrayを参照しています。

myArrayのconstructorプロパティはArrayオブジェクトを参照する
myArray.constructor === Array;  // true

ArrayオブジェクトはisArrayメソッドを所持しているので、isArrayの正しい使い方は以下になります。

isArray()を正しく使うパターン
 Array.isArray(myArray);  // true

myArray.constructor.isArray(myArray);  // true

Array.prototype.constructor.isArray(myArray);  // true

以上が配列を定義した際のプロトタイプチェーン解説です。

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

すべてのオブジェクトは__proto__プロパティを持つので、それはObjectオブジェクトも同様です。
プロトタイプチェーンの末端であるObject.prototypeを参照するのかと思いきや、以下記述はfalseになります。

Object.__proto__の参照元はObject.prototypeではない
Object.__proto__ === Object.prototype  // false

それではObject.__proto__は何を参照しているのでしょうか?

答えはFunction.prototypeです。

Object.__proto__の参照元はFunction.prototype
Object.__proto__ === Function.prototype  // true

Function.prototypeは、Object.prototypeを参照することでプロトタイプチェーンの末端にたどり着きます。

Objectオブジェクトのプロトタイプチェーン
Object.__proto__ === Function.prototype  // true

Function.prototype.__proto__ === Object.prototype;  // true

Object.prototype.__proto__ === null  // true

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

Functionオブジェクト__proto__Function.prototypeを参照し、Objectオブジェクトと同様のチェーンを辿ります。

Functionオブジェクトのプロトタイプチェーン
Function.__proto__ === Function.prototype  // true

Function.prototype.__proto__ === Object.prototype;  // true

Object.prototype.__proto__ === null  // true

toStringメソッドのややこしい話

Objectオブジェクトの持つメソッドのひとつに、toString()があります。
Object.prototype.toString() - JavaScript | MDN

以下の例は同じtoStringメソッドを参照しているように見えますが、結果が異なります。

toStringメソッドの比較
Object.toString === Object.__proto__.toString  // true
Object.toString === Object.prototype.toString  // false


理由を調べるため、Object.toStringのプロトタイプチェーンを辿っていくと、
その参照先はFunction.prototypeを指していることが分かりました。

Object.toStringのプロトタイプチェーン
Object.toString  // ObjectオブジェクトにはtoStringメソッドは存在しない

  // Object.__proto__を参照する

Object.__proto__ === Function.prototype  // true

  // Function.prototypeにtoStringメソッドがないか確認する。

Function.prototype.toString  // Function.prototypeにはtoStringメソッドが存在するので、ObjectはtoStringメソッドを使える。

  // Function.prototype.__proto__も見てみる。

Object.prototype.toString  // 親(prototype)と子で同名のメソッドを持つ場合、親メソッドは子メソッドでオーバーライド(上書き)される。

Object.prototypeが持つtoStringメソッドと、Function.prototypeが持つtoStringメソッドではメソッドの内容が異なります。
Objectオブジェクトはその両者とも継承していますが、メソッドのオーバーライドにより、親(Object.prototype)のメソッドが子(Function.prototype)のメソッドにより上書かれています。

よって持っているtoStringメソッドの起源が異なることが、最初の例の真相です。

toStringメソッドの書き換え
Object.toString === Object.__proto__.toString  // true
  // 書き換えると
Function.prototype.toString === Function.prototype.toString  // true


Object.toString === Object.prototype.toString  // false
  // 書き換えると
Function.prototype.toString === Object.prototype.toString  // false 

Object.prototype.toString()Function.prototype.toString()は別のメソッドなんですね。

参考

図で理解するJavaScriptのプロトタイプチェーン - Qiita
なるほど。。__proto__とprototypeの違い | 武骨日記
継承とプロトタイプチェーン - JavaScript | MDN
Object - JavaScript | MDN
Object.prototype - JavaScript | MDN
Object.prototype.__proto__ - JavaScript | MDN
Object.prototype.toString() - JavaScript | MDN
Object.create() - JavaScript | MDN
Function - JavaScript | MDN
Function.prototype.toString() - JavaScript | MDN


  1. 例えばObject.create(null)のようなオブジェクトはプロトタイプを持たないので、Object.prototypeから継承を受けていない。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

e-typing(タイピングゲーム)で自作の文章で練習できるようにする

はじめに

e-typingという有名なタイピングゲームがあります。
https://www.e-typing.ne.jp/

これ自分で作った文章入力出来たら面白そうと思ってトライしてみました。

やったこと

  • e-typingを解析して仕組みを知る
  • 自分のサーバーに移植する
  • 自分で作った例文を読み込めるようにする
  • 実際にプレイしてみる

結果

実際に完成したもの。
typing
もちろんデザインとかシステムとか全く同じなのでただのe-typingクローンです。
しかし、ちゃんと自作の例文を使ってタイピング練習できます。

判明したこと

こちらで詳しく紹介していますが、e-typingの例文データは暗号化された状態でサーバーから送られてくることがわかりました。
それをJavaScriptでデコードした後に表示しているみたいです。

例文の元データはこんなかんじ

s0=#0#mmdlfjrxeedlekeglkemumbamlevtmhrkmdvamzzgzevtmybmbdmamzzbidlktuzjbevumxbx#1#jcemumvrytdvtxdwwbfvavlsxmdmumwxawfvambuvxevtmyrxpdvtvzzpnemtmhbfyemuvwwcmfmambkitemalgfpwemavxrl#2#mqdbgnbldbfbgklvufebgqldmodbgnxeihebonbxexerskmutqdbgqlbwvdbsqxvqxebskmekeebsqmtqbfrokvzqnfbonlblvfbgnmwvpdrgnvwesebokxbnfdbsqmwnwfbsqlembfrgkrvmbebowguvudbgnbmdlfbgqlgdhfrsnlmj#3#811

複合化キーみたいなものも含まれていて、とても面白い仕組みです。

さいごに

ほぼ丸パクリなので公開などは行わない予定です。
ただ、暗号化や解読の関数は紹介しているので興味があればチェックしてみてください。

以上になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Automation Anywhere A2019でPython/JavaScript/VBScriptのインラインスクリプティングでプログラミングを行う

Automation Anywhere A2019では、外部スクリプトや外部プログラムを呼び出すことができます。スクリプト言語で対応をしているのは Python / Javascript / VBScript の3つで、これらは別のファイルとして用意しておくこともできますし、Botにインラインスクリプトとして埋め込んで実行することもできます。別のファイルにしておく場合は、ローカルで作成したファイルをControl Roomにあらかじめアップロードすることになります。いずれの場合もスクリプトはローカルPCの環境で実行されます。

インラインスクリプティングの方法

Python

  1. Botエディターで、アクションパレットから「Python script: Open」を探してドラッグ&ドロップします。
    image.png
  2. 「Python script: Open」アイコンを選択してアクションの詳細を開きます。そこで「Manual input」を選択します。
    image.png
  3. 「Enter script here」のテキストボックスに以下のコードを貼り付けて保存します。
Inlinescript
from tkinter import messagebox
messagebox.showinfo('Python', 'Hello World')

image.png

4.最後に「Python script: Execute script」と、「Python script: Close」を探して、フローの最後にドラッグ&ドロップし保存すれば完成です。
image.png

注: Pythonコードを実行するには、ローカルPCにPython実行環境の構築が必要です。詳しくは @zamaezaaa さんの記事『A2019 Community EditionでPythonコードを動かす』を参照してください。Python実行環境がない環境でBotを実行した場合、Botで何も実行されずに正常終了しますが、Bot Agentがフリーズして次のBotが実行できなくなり、再起動が必要になる場合があります。

実行結果
image.png

参考情報

Javascript

  1. Botエディターで、アクションパレットから「JavaScript: Open」を探してドラッグ&ドロップします。
    image.png
  2. 「JavaScript: Open」アイコンを選択してアクションの詳細を開きます。そこで「Manual input」を選択します。
    image.png
  3. 「Enter script here」のテキストボックスに以下のコードを貼り付けて保存します。
Inlinescript
function getRandom() {
    return Math.floor( Math.random()*10000 );
}

これは0から10,000までの間の乱数を返す関数です。
image.png

4.「JavaScript: Run JavaScript」を探してフローの最後にドラッグ&ドロップし、アクションの詳細で、実行する関数名にgetRandom、出力の格納先に$prompt-assignment$変数を指定します。
image.png

5.最後に「JavaScript: Close」を探してフローの最後にドラッグ&ドロップし、その後ろにMessageBoxアクションを挿入して$prompt-assignment$変数を表示するように設定して保存すれば完成です。
image.png

実行結果
image.png

注: JavaScript の場合は、alertpromptwritelnのようなブラウザーが実装している画面出力や、WScript.EchoWScript.StdOut.WriteLineのようなWSHオブジェクトを利用するとエラーになったため、値の処理だけJavaScriptで実行して、結果はAutomation AnywhereのMessageBoxで表示するようにしました。

参考情報

VBScript

  1. Botエディターで、アクションパレットから「VBScript: Open」を探してドラッグ&ドロップします。
    image.png
  2. 「VBScript: Open」アイコンを選択してアクションの詳細を開きます。そこで「Manual input」を選択します。
    image.png
  3. 「Enter script here」のテキストボックスに以下のコードを貼り付けて保存します。
Inlinescript
MsgBox "Hello World"

image.png

4.最後に「VBScript: Run function」と、「VBScript: Close」を探して、フローの最後にドラッグ&ドロップし保存すれば完成です。
image.png

実行結果
image.png

参考情報


  1. 製品ドキュメントの内容は頻繁にアップデートされているので英語版のドキュメントを参照するのがお勧めです。 

  2. 最新の英語ドキュメントを見ると、Pythonの場合は関数に渡せる引数は1つだけ(Boolean, dictionary, list, number, string)、JavaScript/VBScriptの場合はリスト型変数を渡すことで複数の引数を渡せる(Boolean, Datetime, number, string etc.)とあります。ただし、A2019.10 (ビルド2545)では、VBScriptではアクションの詳細の「Arguments to the function」にてリスト型変数以外にも文字列や数値も入力可能となっています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新米フロントエンドエンジニア がお世話になってるサイトまとめ

はじめに

今月よりフロントエンドエンジニアとして働き始めたあくしおです。
業務で進めていく中で自分が普段参考にしていたり勉強で使用しているサイトを羅列していこうと思います。

「こんなもん参考にしてるうちはダメだぞ!」って声もあるかもしれませんが許してください

HTML編


HTML Reference - A free guide

スクリーンショット 2020-02-23 23.25.17.png

タグが非常に分かりやすくまとまっており(アルファベット順)画像の通り、そのタグがinline要素なのか
block要素なのかなどが視覚的に非常に分かりやすくなっています。

そのままタグをクリックするとそのタグについての説明が記載されています。
使い所やサンプルコードまで乗っているのでめちゃくちゃオススメのサイトです。
英語のサイトになるのですがChromeの拡張機能などで十分理解できるレベルに翻訳されるので英語アレルギーのある方もぜひ!

HTML入れ子チートシート

その名のとおりHTML5のそれぞれの要素が入れ子にできる「子要素」、包含されることが可能な「親要素」の一覧のルールを視覚化して表示してくれます。

CSS編


CSS Reference - A free guide

はい。先ほどのサイトの姉妹サイトです
HTML ReferenceのCSSバージョンだと思っていただければ問題ありません。
ここに長々と説明書くより元のサイト飛んでもらえれば一番分かりやすい

Sass

はい。CSSのメタ言語(CSSプリプロセッサ)Sassですね
読み方はサスなんでしょうか、サースなんでしょうか、今だに分かっていません。

いきなり脱線しましたがおすすめサイトはこちらになります。
SassMeister
スクリーンショット 2020-02-23 23.49.35.png

このように入力したSassのコードをリアルタイムでCSSにコンパイルしてくれるツールです。 Sassが使える環境を構築しなくても、手軽にSassを試すことができます。

有名なので知っている方も多いかと思います。
即座にコンパイル結果が出力されるのでSassの扱いに慣れてないうちは特にいいのではと思います。

JavaScript編

勉強サイトばかりです。

現代のJavaScriptチュートリアル
その名の通りチュートリアルです。
基本的なことから高度なトピックまで扱われているので個人的にイチオシサイトです

しかしかなりのボリュームがあるので毎日少しずつやっていくのがいいと思います。
ちなみにオススメしている本人は終わってません。すいません

icon-128x128.png
js-primer

二つ目はこちら
著書は有名なazuさんです。2016年よりAngular日本ユーザー会の代表を務めておられるようです (凄い...

こちらもJavaScriptを学び始める初心者の方にオススメです!
海外のチュートリアルを訳した感じの違和感?のある感じではなく最初から日本人の方が書かれているので自然に読めます!

終わりに

とりあえずパッと思いついたものだけですが羅列してみました。
他にもこんなサイトあるぞ、とかこのサイトもオススメや。みたいなのがあればコメントで教えてください!

それではみなさまお元気で

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む