20200113のJavaScriptに関する記事は30件です。

初心者による フォームの扱い

概要

クライアントjavascriptにおいて、フォームはエンドユーザからの入力を受け取る代表的な手段である。Webページに必ずあるといってよいものであるため、十分に学習しておきたい。

入力ボックス/選択ボックス

入力ボックス、選択ボックスの入力を受け取る方法はvalueプロパティにアクセスするだけである。

sample.html
<form>
 <label for="name">名前:</label>
 <input id="name" name="name" type="text" size="30" />
 <input id="btn" type="button" value="送信" />
</form>
<div id="result">
</div>
sampl.js
document.addEventListener('DOMContentLoaded', function(){
 document.getElementById('btn').addEventListener('click', function() {
  let name = document.getElementById('name')
  //入力された内容をvalueで取得
  console.log(name.value)
 }, false)
}, false)

入力ボックス類とselectはvalueプロパティによってすべて同じ要領で取り出すことができる。また、これらの要素は設定するときもvalueプロパティに代入することですぐできる。

チェックボックス

チェックボックスの値にアクセスするのは、入力ボックスに比べて複雑です。

sample.html
<form>
 <div>
  <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label>
  <label><input type="checkbox" name="food" value="餃子" />餃子</label>  
  <label><input type="checkbox" name="food" value="焼肉" />焼肉</label>
  <input id="btn" type="button" value="送信" />
 </div>
</form>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 document.getElementById('btn').addEvenetListener('click', function() {
  let resulr = []
  let foods = document.getElementsByName('food')
  for(let i = 0, len = foods.length; i < len; i++){
   let food = foods.item(i)
   //チェックされているかを調べるには、checkedプロパティを使う
   if(food.checked){
    result.push(food.value)
   }
  }
  //配列を文字列にして出力
  window.alert(result.toString())
 }, false)
}, false)

チェックボックスを使う場合は、

checked : チェックされているかどうか
value : value="" で指定されている値

であることに注意する。
また、チェックボックスは複数とは限らず、オンオフの単一の場合も考えられる。その場合は特別に次のようにする。

sample.js
let onoff = getElementById('onoff')
if(onoff.checked){
 console.log(onoff.value)
}else{
 console.log('チェックされていません')
}

チェックボックスの値をjavascriptで設定する

sample.html
<form>
 <div>
  <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label>
  <label><input type="checkbox" name="food" value="餃子" />餃子</label>  
  <label><input type="checkbox" name="food" value="焼肉" />焼肉</label>
  <input id="btn" type="button" value="送信" />
 </div>
</form>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 //ここでのvalue引数は配列
 let setCheckValue = function(name, value) {
  let elems = document.getElementsByName(name)

  for(let i = 0, len = elems.length; i < len; i++){
   let elem = elems.item(i)
   if(value.indexOf(elem.value) > -1){
    elem.checked = true
   }
  }
 }

 setCheckValue('food', ['餃子', '焼肉'])
}, false)

ここでは、Array.indexOf(value)メソッドを使っている。これは、配列Arrayの中にvalueと等しいものがあるのかをチェックします。存在しなければ、-1を返すため-1よりも大きいと条件づければよい。

ラジオボタン

ラジオボタンはチェックボタンとほとんど同じ要領であるが、より汎用性を持つように汎用的なコードを別に関数として定義して、それを使うようにする。

sample.html
<form>
 <div>
  <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label>
  <label><input type="checkbox" name="food" value="餃子" />餃子</label>  
  <label><input type="checkbox" name="food" value="焼肉" />焼肉</label>
  <input id="btn" type="button" value="送信" />
 </div>
</form>
sample.js
document.addEvenetListener('DOMContentLoaded', function() {
 //自作関数を作る
 let getRadioValue = function(name) {
  let result = ''
  let elems = document.getElementsByName(name)

  for(let i = 0, len = elems.length; i < len; i++){
   let elem = elems.item(i)
   if(elem.cheked){
    result = elem.value
    //チェックされているのは一つしかない
    break
   }
  }
 return result
 }
 //自作関数を実行
 document.getElementById('btn').addEventListener('click', function() {
  window.alert(getRadioValue(food))
 }, false)
}, false)

関数を自作することで、汎用性を持たせることができます。

ラジオボタンの値をjavascriptで設定する

sample.js
<form>
 <div>
  <label><input type="checkbox" name="food" value="ラーメン" />ラーメン</label>
  <label><input type="checkbox" name="food" value="餃子" />餃子</label>  
  <label><input type="checkbox" name="food" value="焼肉" />焼肉</label>
  <input id="btn" type="button" value="送信" />
 </div>
</form>
sample.js
documnt.addEventListener('DOMContentLoaded', function() {
 let setRadioValue = function(name, value){
  let elems = gtElementsByName(name)

  for(let i = 0, len = elems.length; i < len; i++){
   let elem = elems.item(i)
   if(elem.value = value){
    elem.check = true
    //見つけ終わったら抜ける
    break
   }
  }
 }

 setRadioValue('food', '餃子')
}, false)

リストボックス

sample.html
<form>
 <div>
  <label for="food">好きな食べ物は?:</label>
  <select id="food" multiple>
   <option value="ラーメン">ラーメン</option>
   <option value="餃子">餃子</option>
   <option value="焼肉">焼肉</option>
  </select>
  <input id="btn" type="button" value="送信" />
 </div>
</form>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let getListValue = function(name) {
  let result = [] 
  //option要素はoptionsでアクセスする!
  let opts = document.getElementsById(name).options

  for(let i = 0, len = opts.length; i < len; i++){
   let opt = opts.item(i)
   //checkedではなくselectedを使うことに注意
   if(opt.selected){
    result.push(opt.value)
   }
  }
 return result
 }

 document.getElementById('btn').addEvenetListener('click', function() {
  window.alert(getListValue('food'))
 }, false)
}, false)

ここでは、option要素の集まりをoptionsプロパティによって取り出していることに注意する。チェックボックスでは、checkedプロパティを使っていたが、リストボックスでは、selectedプロパティを使う。

リストボックスの値をjavascriptで設定する

内容は上記のものと何ら変わらないため例だけ載せておく。

sample.html
<form>
 <div>
  <label for="food">好きな食べ物は?:</label>
  <select id="food" multiple>
   <option value="ラーメン">ラーメン</option>
   <option value="餃子">餃子</option>
   <option value="焼肉">焼肉</option>
  </select>
  <input id="btn" type="button" value="送信" />
 </div>
</form>
sample.js
document.addEvenetListener('DOMContentLoaded', function() {
 let setListValue = function(name, value) {
  let opts = document.getElementsByName(name)

  for(let i = 0, len = opts.length; i < len; i++){
   let opt = opts.item(i)
   if(value.indexOf(opt.value) > -1){
    opt.selected = true
   }
  }
 }

 setListValue('food', ['餃子', '焼肉'])
}, false)

参考資料

山田祥寛様 「javascript本格入門」

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

頼むからGraphQLのクエリを動的に組み立てるのはやめてくれ

主にapolloの話。

https://github.com/Shopify/graphql-js-client のようなクエリビルダを使ってるんだったら別。

ここでいう「動的に」というのは、下記のように通常の文字列結合で引数とかselection setを突っ込むようなコードを指している。

apolloClient.query(gql`
  query {
    procucts(${variables}) {
      id
    }
  }
`);

当たり前なんだけど、variables"first: 10) { __typename } users(first: 10" みたいな文字列を突っ込んだら、想定とは全く異なるクエリになって何が起こるかわかったもんじゃない。

変数はGraphQLのvariablesに格納するようにしろ。

apolloClient.query(gql`
  query MyQuery($first: Int!) {
    procucts(first: $first) {
      id
    }
  }
`, { variables: { first } }));

要は クエリは静的に決定された状態にしておけ というのが言いたいだけなんだけど、実行結果が想定外になる、というだけじゃなく、他にも弊害が色々ある。

例えば冒頭のコード、gql というTagged Templateを使ってて、apolloでコード書いたらよく出てくるんだけど、こいつは中身の文字列をparseしてGraphQLのASTに変換する役割を持っている。gql は一度parseしたASTは元のクエリ文字列をキーにしたObjectに格納してcacheとして利用するように実装されている。折角cacheを持っているのにそこにhitしなかったらメモリの無駄、というのもあるし、このcacheはただObjectにガンガン値突っ込んでるだけで、LRUとかそういうことは一切していないので、SSRの文脈でこのコードをNode.jsのサーバーで動かしたらメモリリークを引き起こす。

クエリの文字列をメモリ上に持ち続ける、というのは persisted queryの文脈などでも出てくる。この場合はクエリ文字列のhash値をキーに、parseしたASTやクエリ文字列を値にした辞書を中間層に保持することになるが、もしクエリが動的にコロコロ変わるようになっていたら、こういった仕組みだってやはり真当に動作しない。

大概のケースだと、codegenとかその手の静的な解析ツールを書けたときにこういう行儀の悪いコードは見つけられると思っていたけど、 apollo client:codegen コマンドだと特にエラーも警告も吐かずにそれっぽい型ファイルを生成してしまうのも質が悪い。

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

JavaScript で配列に特定の要素が含まれているか確認する

Array.prototype.indexOf()メソッドを使用して、配列に特定の要素が含まれているか確認する。

配列に特定の要素が含まれている場合はtrue、そうでない場合は、falseを返す。

console.log(['foo', 'bar', 'baz'].indexOf('foo') !== -1); // true
console.log(['foo', 'bar', 'baz'].indexOf('fo') !== -1); // false

Array.prototype.indexOf()

indexOf()メソッドは、引数に指定した内容と同じ配列要素を探し、最初の要素の添字を返す。
存在しない場合は、-1を返す。

console.log(['foo', 'bar', 'baz'].indexOf('foo')); // 0
console.log(['foo', 'bar', 'baz'].indexOf('bar')); // 1
console.log(['foo', 'bar', 'baz'].indexOf('ba')); // -1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptでユーザー定義のエラーを実装する

例外周りにあまり知見がなかったのでJavaScriptの標準のエラーから、TypeScriptでユーザー定義の例外を実装する方法までを調べました。

JavaScriptの標準のエラーオブジェクトについて

JavaScriptの標準のエラーオブジェクトは以下の表の通り7つあります。

エラーオブジェクト 説明
Error ランタイムエラーが発生した時に投げられます。ユーザー定義の例外の基底オブジェクトとして使用することもできます。
EvalError グローバルな eval() 関数に関連するエラーを示します。この例外はもう JavaScript から投げられませんが、EvalError オブジェクトは互換性のために残っています。
RangeError 値が配列内に存在しない、または値が許容範囲にない場合のエラーを表します。
ReferenceError 存在しない変数が参照された場合のエラーを表します。
SyntaxError 構文的に不正なコードを解釈しようとした場合のエラーを表します。
TypeError 値が期待される型でない場合のエラーを表します。
URIError グローバル URI 処理関数が誤った使い方をされたことを示すエラーです。

こんなに定義されていたのですね。恥ずかしながらRangeErrorしか知りませんでした:scream_cat:

TypeScriptでユーザー定義の例外を実装

先ほどの表ではAPIの通信エラーがあった場合、適切なエラーが見つからないのでErrorオブジェクトを使うことになります。

Errorオブジェクトは、ユーザー定義の例外の基底オブジェクトとして使用することもできます。

とのことなので、より適切なHttpRequestErrorというエラーを作ってみましょう。
まずErrorオブジェクトを継承しApplicationErrorをユーザー定義の基底オブジェクトを作成します。

ApplicationError.ts
export default class ApplicationError extends Error {

  constructor(public message: string) {
    super(message);
    this.name = "ApplicationError";
  }
}
// 最初はErrorインタフェースを実装していたのですが、
// 後ほど使うSentry.captureExceptionでErrorを継承する必要があった
// export default class ApplicationError implements Error {

ApplicationErrorを継承したHttpRequestErrorを作成。

HttpRequestError.ts
import ApplicationError from "./ApplicationError";

export default class HttpRequestError extends ApplicationError {

  constructor(public message: string, public status: number) {
    super(message);
    this.name = "HttpRequestError";
    this.message = `${status}: ${message}`
  }
}

throw new HttpRequestError("管理者に連絡してください", 500)

標準エラーとユーザー定義エラーにより異なるアクションを実行

標準エラーとユーザー定義のエラーに基づいて、エラーハンドラー内で異なるアクションを実行します。(Vueのエラーハンドラーについてはコチラ

errorHandler.ts
Vue.config.errorHandler = (error, vm, info) => {
  // ApplicationErrorの場合はユーザーにアラートを表示し
  if (error instanceof ApplicationError) {
    alert(error.message);
    if (error.stack) {
      Sentry.captureException(error);
    }
  }
  // ビルトインのエラーについてはユーザーに表示せず、Sentryなどで開発者に通知する
  if (error instanceof Error) {
    if (error.stack) {
      Sentry.captureException(error);
    }
  }
};

さいごに

JavaScriptの例外のベストプラクティスを探して見つけることができませんでした。
自分で調べた結果このような形に落ち着いたのでしばらくはこの形を使用していく予定です。

間違いやアドバイスがあれば教えていただけると嬉しいです!

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

JavaScriptでスムーススクロールを実装する

こんなやつ

Image from Gyazo
動作デモ → https://jsfiddle.net/zr36gcpb/

コード

HTML
<div class="scroll">
  <a class="scroll__button" href=".contents__1">Scroll</a>
  <a class="scroll__button" href=".contents__2">Scroll</a>
  <a class="scroll__button" href=".contents__3">Scroll</a>
  <a class="scroll__button" href=".contents__4">Scroll</a>
</div>

<div class="contents">
  <div class="contents__1">Contents1</div>
  <div class="contents__2">Contents2</div>
  <div class="contents__3">Contents3</div>
  <div class="contents__4">Contents4</div>
</div>
JavaScript
const smoothScroll = e => {
  const target = document.querySelector(e.target.getAttribute('href'));
  const position = target.getBoundingClientRect().top + window.scrollY;

  e.preventDefault();

  window.scrollTo({
    top: position,
    behavior: 'smooth'
  });
  //window.scrollToの引数のtopは「ドキュメントの左上を基準にした目標の要素のY座標のピクセル値」で、
  //behaviorにはsmoothを指定する事で挙動がスムーススクロールになります。
}

const buttons = document.querySelectorAll('.scroll__button');

buttons.forEach(button => {
  button.addEventListener('click', smoothScroll);
})

上記の関数smoothScrollを任意のaタグに走らせることで、擬似的にhrefで指定した要素にスムーススクロールするボタンになります。

やっている事は単純で、aタグに指定されているhrefの値を取得して、変数positionにその指定された要素の「ビューポートの左上からのY座標のピクセル数」と「現在スクロールしているY座標のピクセル数」を足したもの(=対象の要素の絶対位置)を格納しています。
そのpositionの値を使用して、window.scrollToで目標の位置までスクロールしています。
そして、もちろんaタグのデフォルトの動作をキャンセルするpreventDefaultも指定します。

もしaタグ以外で使用したい場合は、hrefの代わりにdata属性を指定してそれをgetAttributeすれば良いと思います。


出来ました。(*゚▽゚ノノ゙☆パチパチパチ

Image from Gyazo

※注意点
一部のブラウザでパラメーター部分が非対応なため、普通のスクロールになってしまう事がある。
ブラウザの実装状況(MDN)

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

初心者による DOM 属性値の取得/設定

概要

getElementById()等で要素ノードにアクセスできたら、次は、その要素ノードに何らかの処理をしたい。

特定の属性の取得

多くの属性は「要素ノードの同名のプロパティ」としてアクセスできるからです。たとえば、

sample.js
//取得
let url = link.href
//設定
link.href = 'https://google.com'

ただし、classに関してはclassNameになることが注意点です。この「属性とプロパティは一致しない場合がある」ことを意識したくないのであれば、次の方法がある。

sample.js
elem.getAttribute(name)
elem.setAttribute(name, value)
 //elem : 要素オブジェクト name : 属性名 value : 属性値

これを用いて書き直すと、

sample.js
let url = link.getAttribute('href')
link.setAttribute('href', 'https://yahoo.com')

getAttributeとsetAttributeを使うメリットとしては、
1. HTMLとjavascriptとで名前の相違を意識する必要はない。
2. (文字列として指定できるため)取得/設定する属性名を、スクリプトから動的に変更できる。
が挙げられる。

不特定の属性の所得

一つ一つ属性を取得するのではなく、ある特定の要素ノードに属する属性値をすべて取得したい場合はAttributesをつかう。例えば、

sample.html
<img id="logo" src="https://sample.to/image/1.png" height="57" width="10" alt="image" />
sample.js
document.addEventListener('DOMContentLoaded', function(){
 let logo = document.getElementById('logo')
 let attrs = logo.attributes
 for(let i = 0, let len = attrs.length: i < len; i++){
  let attr = attrs.item(i)
  //nameとvalueプロパティでアクセス。
  console.log(attr.name + ',' + attr.value)
 }
}, false)

//出力
id:logo
src:https://sample.to/image/1.png
height:57
width:10
alt:image

取り出した属性ノードの名前や値にアクセスするには、name/valueプロパティを使う。この例で使われている attrs はNamedNodeMapオブジェクトと呼ばれ、この属性値のマップを操作することも可能。次のように、

samplejs
//title属性を作成
let title = document.createAttribute('tilte')
title.value = 'ロゴ画像'
//マップに追加と削除
attrs.setNamedItem(title)
attrs.removeNamedItem(title)

テキストの取得/設定

要素配下のテキストを取得/設定するには、innerHTML / textContentというプロパティを利用する。

sample.html
<div id="result_text">
 <p style="color: red;">設定されていません!</p>
</div>
<div id="result_html">
 <p style="color: red;">設定されていません!</p>
</div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 documentGetElementById('result_text').textContent = 
  '<a href="https://google.com">Google</a>'
 documentGetElementById('result_html').innerHTML = 
  '<a href="https://yahoo.com">yahoo</a>'
}, false)

これで重要なことは、「配下の子要素/テキストを完全に書き換えている」ということです。しかし、決定的に違うところは、「与えられたテキストをHTMLとして認識するかどうか」である。一般的には、textContentプロパティを優先して利用することで、高速かつ安全に運用できる。
また、console.log()ではinnerHTMLプロパティはHTML文字列すべてを返すが、textContentはその中のテキストだけを取り出して出力することに注意する。

innerHTMLの注意点

innerHTMLプロパティを利用する場合、ユーザからの入力値をはじめ、外部からの入力値をそのまま渡さないようにしなければならない。

sample.html
<form>
 <label for="name">名前:</label>
 <input id="name" name="name" type="text" />
 <input id="btn" type="button" value="送信" />
</form>
<div id="result"></div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let name = document.getElementById('name')
 let result = document.getElementById('result')
 //この文が問題の文!
 result.innerHTML = 'こんにちは、' + name.value + 'さん!'
})

result.innerHTMLの文が好ましくないところである。なぜならば、innerNTMLはHTML認識可能な文を第三者が入力可能であるため、製作者が意図しないコードを入力してくる可能性がある。しかもそれを不特定多数の人がブラウザー上で実行することができるのである。このような脆弱性をクロスサイトスクリプティング(XSS)脆弱性という。innerHTMLではなくtextContentにすることが望ましい。

参考資料

山田祥寛様 「javascript本格入門」

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

便利ページ:QRコードスキャン

久しぶりの、「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」 のシリーズものです。

今回は、QRコードスキャンです。
QRコード生成はすでに実装してあったのですが、スキャンする方はありませんでした。

HTML5では、PCに接続されたカメラを扱うことができますので、ブラウザだけでスキャンできます。また、AndroidやiPhoneでのChromeでも動作するので、これでPCだけでなくスマホでも動きます。

いつもの通りGitHubにも上げてあります。
 https://github.com/poruruba/utilities

参考までに、以下にデモとしてアクセスできるようにしてあります。
 https://poruruba.github.io/utilities/

QRスキャンのためのライブラリ

以下を使わせていただきました。ありがとうございます。

cozmo/jsQR
 https://github.com/cozmo/jsQR

HTMLで以下ようにスクリプトをロードしておきます。
 <script src="dist/js/jsQR.js"></script>

ソースコード抜粋

肝心のJavascriptのソースコードです。重要部分のみ抜粋しています。

start.js
        qrcode_scan: function(){
            this.qrcode_video = $('#qrcode_camera')[0];
            this.qrcode_canvas = $('#qrcode_canvas')[0];

            if( this.qrcode_running ){
                this.qrcode_forcestop();
                return;
            }

            this.qrcode_running = true;

            this.qrcode_context = this.qrcode_canvas.getContext('2d');
            this.qrcode_timer = setTimeout(() =>{
                this.qrcode_forcestop();
            }, QRCODE_CANCEL_TIMER);

            return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, audio: false })
            .then(stream =>{
                this.qrcode_video.srcObject = stream;
                this.qrcode_draw();
            })
            .catch(error =>{
                alert(error);
            });
        },
        qrcode_draw: function(){
            this.qrcode_context.drawImage(this.qrcode_video, 0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);
            const imageData = this.qrcode_context.getImageData(0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);

            const code = jsQR(imageData.data, this.qrcode_canvas.width, this.qrcode_canvas.height);
            if( code ){
                this.qrcode_scaned_data = code.data;
                console.log(code);

                this.qrcode_forcestop();

                this.qrcode_context.strokeStyle = "blue";
                this.qrcode_context.lineWidth = 3;

                var pos = code.location;
                this.qrcode_context.beginPath();
                this.qrcode_context.moveTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                this.qrcode_context.lineTo(pos.topRightCorner.x, pos.topRightCorner.y);
                this.qrcode_context.lineTo(pos.bottomRightCorner.x, pos.bottomRightCorner.y);
                this.qrcode_context.lineTo(pos.bottomLeftCorner.x, pos.bottomLeftCorner.y);
                this.qrcode_context.lineTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                this.qrcode_context.stroke();

                this.qrcode_btn = 'QRスキャン開始';
            }else{
                if( this.qrcode_running )
                    requestAnimationFrame(this.qrcode_draw);
            }
        },
        qrcode_forcestop: function(){
            if( !this.qrcode_running )
                return;

            this.qrcode_running = false;

            if( this.qrcode_timer != null ){
                clearTimeout(this.qrcode_timer);
                this.qrcode_timer = null;
            }

            this.qrcode_video.pause();
            this.qrcode_btn = 'QRスキャン開始';
        },

ご参考までに、HTMLの方も。

index.html
    <label>scaned data</label>
    <div class="input-group">
        <span class="input-group-btn">
            <button class="btn btn-default clip_btn glyphicon glyphicon-paperclip" data-clipboard-target="#qrcode_scaned_data"></button>
        </span>
        <input id="qrcode_scaned_data" type="text" class="form-control" v-model="qrcode_scaned_data" readonly>
    </div><br>
    <button class="btn btn-primary" v-on:click="qrcode_scan()">{{qrcode_btn}}</button><br>
    <div>
        <img v-show="!qrcode_running && qrcode_scaned_data==''" id="qrcode_start" src="./img/qr_start.png" v-bind:style="qrcode_size"><br>
        <video v-show="qrcode_running" id="qrcode_camera" v-bind:style="qrcode_size" autoplay></video>
        <canvas v-show="!qrcode_running && qrcode_scaned_data!=''" id="qrcode_canvas" v-bind:style="qrcode_size"></canvas>
    </div>

解説

・qrcode_scan()
ボタン押下をトリガに、QRコードスキャンを開始します。
まずは、「navigator.mediaDevices.getUserMedia」を呼び出して、ユーザに対してカメラ利用の許可を聞いた後にカメラを起動させます。
起動した後、「this.qrcode_video.srcObject = stream;」でカメラ画像をWebページに表示させ、「qrcode_draw()」の呼び出しで、カメラ画像からQRコードを探します。
setTimeout()がありますが、一定時間QRコードスキャンで見つからなかったときに、QRコードスキャンを停止するためのものです。
ちなみに、ブラウザからカメラを利用するため、HTMLはHTTPSでホスティングしている必要があります。

・qrcode_draw()
カメラ画像からQRコードをスキャンします。
いったん、別のcanvasのcontextにコピーしたのち、QRコードスキャンのライブラリを呼び出します。
QRコードがあった場合には、QRコードの部分を青色の四角形で囲ってスキャン終了です。
もしQRコードがなかった場合には、「requestAnimationFrame」を呼び出して次のカメラ画像を待ちます。

以上

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

便利ページ:JavascriptでQRコードスキャン

久しぶりの、「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」 のシリーズものです。

今回は、QRコードスキャンです。
QRコード生成はすでに実装してあったのですが、スキャンする方はありませんでした。

HTML5では、PCに接続されたカメラを扱うことができますので、ブラウザだけでスキャンできます。また、AndroidやiPhoneでのChromeでも動作するので、これでPCだけでなくスマホでも動きます。

いつもの通りGitHubにも上げてあります。
 https://github.com/poruruba/utilities

参考までに、以下にデモとしてアクセスできるようにしてあります。
 https://poruruba.github.io/utilities/

QRコードスキャンのためのライブラリ

以下を使わせていただきました。ありがとうございます。

cozmo/jsQR
 https://github.com/cozmo/jsQR

HTMLで以下ようにスクリプトをロードしておきます。
 <script src="dist/js/jsQR.js"></script>

ソースコード抜粋

肝心のJavascriptのソースコードです。重要部分のみ抜粋しています。

start.js
        qrcode_scan: function(){
            this.qrcode_video = $('#qrcode_camera')[0];
            this.qrcode_canvas = $('#qrcode_canvas')[0];

            if( this.qrcode_running ){
                this.qrcode_forcestop();
                return;
            }

            this.qrcode_running = true;
            this.qrcode_btn = 'QRスキャン停止';
            this.qrcode_scaned_data = "";

            this.qrcode_context = this.qrcode_canvas.getContext('2d');
            this.qrcode_timer = setTimeout(() =>{
                this.qrcode_forcestop();
            }, QRCODE_CANCEL_TIMER);

            return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, audio: false })
            .then(stream =>{
                this.qrcode_video.srcObject = stream;
                this.qrcode_draw();
            })
            .catch(error =>{
                alert(error);
            });
        },
        qrcode_draw: function(){
            this.qrcode_context.drawImage(this.qrcode_video, 0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);
            const imageData = this.qrcode_context.getImageData(0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);

            const code = jsQR(imageData.data, this.qrcode_canvas.width, this.qrcode_canvas.height);
            if( code && code.data != "" ){
                this.qrcode_scaned_data = code.data;
                console.log(code);

                this.qrcode_forcestop();

                this.qrcode_context.strokeStyle = "blue";
                this.qrcode_context.lineWidth = 3;

                var pos = code.location;
                this.qrcode_context.beginPath();
                this.qrcode_context.moveTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                this.qrcode_context.lineTo(pos.topRightCorner.x, pos.topRightCorner.y);
                this.qrcode_context.lineTo(pos.bottomRightCorner.x, pos.bottomRightCorner.y);
                this.qrcode_context.lineTo(pos.bottomLeftCorner.x, pos.bottomLeftCorner.y);
                this.qrcode_context.lineTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                this.qrcode_context.stroke();
            }else{
                if( this.qrcode_running )
                    requestAnimationFrame(this.qrcode_draw);
            }
        },
        qrcode_forcestop: function(){
            if( !this.qrcode_running )
                return;

            this.qrcode_running = false;

            if( this.qrcode_timer != null ){
                clearTimeout(this.qrcode_timer);
                this.qrcode_timer = null;
            }

            this.qrcode_video.pause();
            this.qrcode_video.srcObject = null;
            this.qrcode_btn = 'QRスキャン開始';
        },

ご参考までに、HTMLの方も。

index.html
    <label>scaned data</label>
    <div class="input-group">
        <span class="input-group-btn">
            <button class="btn btn-default clip_btn glyphicon glyphicon-paperclip" data-clipboard-target="#qrcode_scaned_data"></button>
        </span>
        <input id="qrcode_scaned_data" type="text" class="form-control" v-model="qrcode_scaned_data" readonly>
    </div><br>
    <button class="btn btn-primary" v-on:click="qrcode_scan()">{{qrcode_btn}}</button><br>
    <div>
        <img v-show="!qrcode_running && qrcode_scaned_data==''" id="qrcode_start" src="./img/qr_start.png" v-bind:style="qrcode_size"><br>
        <video v-show="qrcode_running" id="qrcode_camera" v-bind:style="qrcode_size" autoplay></video>
        <canvas v-show="!qrcode_running && qrcode_scaned_data!=''" id="qrcode_canvas" v-bind:style="qrcode_size"></canvas>
    </div>

解説

・qrcode_scan()
ボタン押下をトリガに、QRコードスキャンを開始します。
まずは、「navigator.mediaDevices.getUserMedia」を呼び出して、ユーザに対してカメラ利用の許可を聞いた後にカメラを起動させます。
起動した後、「this.qrcode_video.srcObject = stream;」でカメラ画像をWebページに表示させ、「qrcode_draw()」の呼び出しで、カメラ画像からQRコードを探します。
setTimeout()がありますが、一定時間QRコードスキャンで見つからなかったときに、QRコードスキャンを停止するためのものです。
ちなみに、ブラウザからカメラを利用するため、HTMLはHTTPSでホスティングしている必要があります。

・qrcode_draw()
カメラ画像からQRコードをスキャンします。
いったん、別のcanvasのcontextにコピーしたのち、QRコードスキャンのライブラリを呼び出します。
QRコードがあった場合には、QRコードの部分を青色の四角形で囲ってスキャン終了です。
もしQRコードがなかった場合には、「requestAnimationFrame」を呼び出して次のカメラ画像を待ちます。

以上

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

Svelteで使えるCSSフレームワークまとめてみた

はじめに

Svelteで使えるCSSフレームワークを調べたのでまとめます。

sveltestrap

HP:https://bestguy.github.io/sveltestrap/
GitHub:https://github.com/bestguy/sveltestrap
Star:181
もとになったフレームワーク:BootStrap
※2020/01/13現在

Svelma

HP:https://c0bra.github.io/svelma/
GitHub:https://github.com/c0bra/svelma
Star:200
もとになったフレームワーク:Bulma
※2020/01/13現在

Svelte Material UI

HP:https://sveltematerialui.com/
GitHub:https://github.com/hperrin/svelte-material-ui
Star:546
もとになったフレームワーク:Material-ui
※2020/01/13現在

おわりに

ざっと調べると紹介した3つが使えそうだなと思いました。
これも使えるよとかありましたらコメントで教えてください!

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

react にて、画像リンク切れの際、imgタグを非表示にする

通常のhtmlにおいては、onerrorハンドラにてリンク切れのimgタグを非表示にできます。

<img src="original.png" alt="title" onerror="this.style.display='none'"/>

reactではこれができないため、以下のようにonErrorハンドラを作成します。

<img src={`${process.env.PUBLIC_URL}/logo192_.png`} onError={e => e.target.style.display = 'none'} />

サンプル

?以下のサンプルは、create-react-app直後のApp.jsを編集したものです。

App.js
import React from 'react';

function App() {
  return (
    <div className="App">
      <img border="1" src={`${process.env.PUBLIC_URL}/logo192.png`} />
      <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} />
      <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} onerror="this.style.display='none'" />
      <img border="1" src={`${process.env.PUBLIC_URL}/logo192_.png`} onError={e => e.target.style.display = 'none'} />
    </div>
  );
}

export default App;

参考

?Hide broken image link in Semantic UI React
?リンク切れの時に代替画像を表示したいときはこちら

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

【復習】JavaScriptで複数のclassに反映させる方法

困ったこと

classを指定しているのに一つしか反映されない。。。

理解と対応

classの場合は配列になるので一つ一つ取り出さなければいけない

そんな時に利用するのが
for文

for (  初期値;  繰り返す条件;  増減値  ) {
  // 繰り返す処理を書く
}

配列の取り出し方としては
box[1]やbox[2]などで取り出すため
変数を用いてbox[i]と記載しfor文で変数の値を増やしていけば取り出すことが可能

実際のコード

<style>
    .box1  {
        width: 100px;
        height: 100px;
        background: red;
    }


    .box2 {
        width: 100px;
        height: 100px;
        background: red;
    }
</style>


<body>
    <div class="box1" id="jj"></div>
    </br>
    <div class="box2" id="hh"></div>
    </br>
    <div class="box2" id="hh"></div>

<script>
 var box = document.getElementsByClassName('box2');
 for (var i = 0 ; i < box.length ; i++ ){
  box[i].style.background = 'pink';
 }
</script>


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

ejectなしのExpoアプリにStripe決済をWebViewを使って埋め込む(Checkout編)

Payment IntentとElementsを使った記事に引き続き、Stripe CheckoutでStripeによって生成されるページをWebViewで使用してみます。

Checkoutページに遷移させるにはStripe.jsのredirectToCheckoutメソッドを使用しますが、その際、skuを指定する場合(Checkout クライアント専用組み込み)と、サーバー側でセッションを作成してからsessionIdを指定する場合と2種類の方法があります。
クライアント専用組み込みの場合はサーバー側の実装が不要ですが、リダイレクト元に関して設定の必要と制限がある*ため、この記事では後者の方法をとります。

*URLスキームがhttp(s)に限られるので、WebView上の非ホスティングのHTMLから直接リダイレクトできない。

1. サーバーサイドの準備

PaymentIntentの時と同じように、今度はセッションを作成してアプリに返すAPIを作成します。
セッションの内容は適当に入れているので、用途に合わせて適宜変更してください。

functions/index.js
const functions = require('firebase-functions');

const app = require('express')();
const stripe = require('stripe')('sk_test_...'); // 秘密鍵
const cors = require('cors');
const bodyParser = require('body-parser');

app.use(require('body-parser').text());
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

const SUCCESS_URL = 'https://.../loading.html#succcess'; // 成功時のコールバックURL
const CANCEL_URL = 'https://.../loading.html#canceled'; // キャンセル時のコールバックURL

app.post('/createSession', async (req, res) => {
  const { amount, currency, locale } = req.body;
  const result = await stripe.checkout.sessions.create({
    success_url: SUCCESS_URL,
    cancel_url: CANCEL_URL,
    locale,
    payment_method_types: ['card'],
    line_items: [
      {
        name: 'test item',
        description: 'test description',
        amount,
        currency,
        quantity: 1
      }
    ]
  });
  res.json(result);
});

exports.api = functions.https.onRequest(app);

上記のように、Checkoutの際はsuccess_urlcancel_urlとしてコールバックURLを指定する必要があります。
最初はこれらのリダイレクト先をどこにもホスティングせずWebViewでなんとかしようとしたのですが思うように制御できなかったため、とりあえずローディング画面のようなページだけはどこかに用意することにしました。

成功/キャンセルページを作成

See the Pen dyPKpXg by mildsummer (@mildsummer) on CodePen.

ほとんど見えないページなのでなんでもよいです。
このような感じで何もしないページをどこかにホスティングして、先ほどのAPIでURLを返すようにします。

2. アプリ側を作成

完成イメージ

ボタンを押したらモーダル内のWebViewでCheckoutページを表示するような形にします。
アートボード 1-8.png

WebViewを使ったコンポーネントを作成

StripeCheckout.js
import React, { Component } from 'react';
import { View, ActivityIndicator } from 'react-native';
import * as PropTypes from 'prop-types';
import { WebView } from 'react-native-webview';

/**
 * Stripe決済をWebViewを介して行うコンポーネント
 */
class StripeCheckout extends Component {
  constructor(props) {
    super(props);
    this.state = {
      ready: false,
      session: null
    };
    this.init();
  }

  /**
   * セッションを作成
   * @returns {Promise<void>}
   */
  init = async () => {
    const { amount, currency, locale } = this.props;
    const response = await fetch('https://[リージョン]-[プロジェクトID].cloudfunctions.net/api/createSession', {
      method: 'POST',
      headers: {
        'Content-type': 'application/json'
      },
      body: JSON.stringify({
        amount,
        currency,
        locale
      })
    });
    const session = await response.json();
    this.setState({ session });
  };

  /**
   * WebViewからのデータを処理
   * @param event
   */
  onMessage = (event) => {
    const json = JSON.parse(event.nativeEvent.data);
    console.log(json);
  };

  /**
   * ページ読み込み完了時
   */
  onLoadEnd = (event) => {
    const { session } = this.state;
    const { url } = event.nativeEvent;
    const { onSucceeded, onCanceled } = this.props;
    if (url === session.success_url) {
      onSucceeded();
    } else if (url === session.cancel_url) {
      onCanceled();
    } else if (url !== 'about:blank') {
      this.setState({ ready: true });
    }
  };

  render() {
    const { ready, session } = this.state;
    const {
      publicKey,
      style
    } = this.props;
    const html = session && `
      <!DOCTYPE html>
      <html>
        <head>
        </head>
        <body>
          <script src="https://js.stripe.com/v3/"></script>
          <script>
            var stripe = Stripe('${publicKey}');
            try {
              stripe
                .redirectToCheckout({ sessionId: '${session.id}' })
                .then(function(result) {
                  postMessage(result.error.message);
                }).catch(function(e) {
                  postMessage(e.message);
                });              
            } catch (e) {
              postMessage(e.message);
            }

            /**
             * アプリ側にデータを送る
             * @param data
             */
            function postMessage(data) {
              window.ReactNativeWebView.postMessage(JSON.stringify(data));
            }
          </script>
        </body>
      </html>
    `;
    return (
      <View style={style}>
        {!ready && <ActivityIndicator style={{ width: '100%', height: '100%' }}/>}
        {session && (
          <WebView
            onLoadEnd={this.onLoadEnd}
            javaScriptEnabled={true}
            source={{ html }}
            onMessage={this.onMessage}
            style={{
              width: '100%',
              height: '100%'
            }}
          />
        )}
      </View>
    );
  }
}

StripeCheckout.propTypes = {
  publicKey: PropTypes.string.isRequired,
  amount: PropTypes.number.isRequired,
  currency: PropTypes.string,
  locale: PropTypes.string,
  style: PropTypes.object,
  onSucceeded: PropTypes.func.isRequired,
  onCanceled: PropTypes.func.isRequired
};

StripeCheckout.defaultProps = {
  currency: 'JPY',
  locale: 'ja',
  style: null
};

export default StripeCheckout;

ActivityIndicatorなど適宜表示しつつ、コンポーネント初期化時に先ほどのAPIでセッションを作成し、その後WebViewを表示。
WebViewのロードイベントをハンドリングすることでどのページが読み込まれているかどうかを判断して、コールバックURLに遷移した時にprops経由で親要素に通知します。

画面を作成

上記のコンポーネントをモーダル内に表示して、支払いの流れを作ります。
モーダルはreact-native-modalを使用します。

App.js
import React, { Component } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Modal from 'react-native-modal';
import StripeCheckout from './StripeCheckout';

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isCheckout: false,
      succeeded: false
    };
  }

  /**
   * Checkoutモーダルを表示
   */
  checkout = () => {
    this.setState({ isCheckout: true });
  };

  /**
   * Checkoutをキャンセル
   */
  cancel = () => {
    this.setState({ isCheckout: false });
  };

  /**
   * Checkout成功
   */
  onSucceeded = () => {
    this.setState({ isCheckout: false, succeeded: true });
  };

  render() {
    const { isCheckout, succeeded } = this.state;
    return (
      <View style={styles.container}>
        <Text style={styles.title}>注文のテスト</Text>
        <TouchableOpacity onPress={this.checkout}>
          <View
            style={[
              styles.button,
              succeeded && styles.succeededButton
            ]}
          >
            <Text
              style={[styles.buttonText, succeeded && styles.succeededButtonText]}
            >
              {succeeded ? '注文が完了しました' : '注文する'}
            </Text>
          </View>
        </TouchableOpacity>
        <Modal
          isVisible={isCheckout}
          style={styles.modal}
          onBackButtonPress={this.cancel}
          onBackdropPress={this.cancel}
        >
          <StripeCheckout
            style={styles.modalInner}
            amount={1000}
            publicKey="pk_test_..." // 公開鍵
            onSucceeded={this.onSucceeded}
            onCanceled={this.cancel}
          />
        </Modal>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    height: '100%',
    width: '100%',
    backgroundColor: '#ffffff',
    alignItems: 'center',
    justifyContent: 'center'
  },
  title: {
    position: 'relative',
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 24
  },
  modal: {
    justifyContent: 'flex-end',
    margin: 0
  },
  modalInner: {
    height: '70%',
    backgroundColor: '#ffffff'
  },
  button: {
    position: 'relative',
    width: 240,
    height: 50,
    borderRadius: 25,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'orange'
  },
  succeededButton: {
    borderColor: 'orange',
    borderWidth: 2,
    backgroundColor: 'transparent'
  },
  buttonText: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#ffffff'
  },
  succeededButtonText: {
    color: 'orange'
  }
});

これで完成です。ダッシュボードでも決済が確認できました。

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

?祝日とかぶった予定を自動で削除する(Google Apps Scriptで)

やること: 祝日とかぶった予定を自動で削除する

会議や打ち合わせの予定を繰り返しイベント(予定)で登録していませんか?私もその1人です。
この機能、便利なのですが、イベントが祝日にかぶったとき、いちいち手で消すのが面倒なのですよね…
(例えば、毎週月曜に定例の会議を入れているけど、来週は祝日だから中止だ、とかのケースです)

今回は祝日の検知と、その日のイベントを自動キャンセルするというスクリプトを実践してみましょう。

やってみよう

手元で動かしているロジックは、もっといろんなことをやっているので、本質的な部分を切り出して紹介します。
(間違いを見つけられましたら、指摘してもらえると嬉しいです。)

言うまでもありませんが、自己責任でお願いします。予定を削除するメソッドが含まれますので、バックアップを取るとか、削除の部分はコメントアウトして動かすなど、各自対策していただければ。

function removeBusinessEventOnHoliday() {
  // 通知用(ここではSlackを使う)
  const slackApp = new SlackApp();

  // 評価期間(月単位)
  const from = Moment.moment().startOf("Month").toDate();
  const to = Moment.moment().add(1, "Months").startOf("Month").toDate();

  // Googleが公開している日本の祝日カレンダー
  const holidayCalendar = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com");
  // 自分のカレンダー
  const myCalendar = CalendarApp.getDefaultCalendar();

  // 日本の祝日カレンダーと既定のカレンダーの「特別休日」(職場の休みなど)を抽出
  [
    holidayCalendar.getEvents(from, to),
    dmyCalendar.getEvents(from, to, {
      search: "特別休日"
    })
  ].forEach(function (holidays) {
    holidays.forEach(function (holiday) {
      const holidayName = holiday.getTitle();
      const holidayFrom = holiday.getStartTime();
      const holidayTo = holiday.getEndTime();

      // 祝休日と重複する「定例」「共有会」を抽出
      ["定例", "共有会"].forEach(function (query) {
        myCalendar.getEvents(holidayFrom, holidayTo, {
          search: query
        }).forEach(function (event) {
          if (event.isRecurringEvent()) {
            const eventDate = Moment.moment(event.getStartTime());
            // 通知
            slackApp.sendText(
              eventDate.format("M/D") + "は[" + holidayName + "]のため、" +
              (event.isAllDayEvent() ? "" : eventDate.format("H:mm") + "開始の") + "[" + event.getTitle() + "]は取り消されます。"
            );
            // イベントの削除
            event.deleteEvent();
          }
        });
      });
    });
  });
}

まずは評価期間内の祝日を洗い出そう

私の場合、毎月1日にこのロジックを動かすようにしています。
なので評価期間は当月初~来月初の1ヶ月間です。
日付計算はMoment.jsを使っています。とても便利です。
ライブラリに組み入れて動かしてください。(参考: https://tonari-it.com/gas-moment-js-moment/)

  // 評価期間(月単位)
  const from = Moment.moment().startOf("Month").toDate();
  const to = Moment.moment().add(1, "Months").startOf("Month").toDate();

そして、Googleが公開している日本の祝日カレンダーと、自分のカレンダーから評価期間内の「お休み」を抽出します。
自分のカレンダーにも会社の休み(「特別休日」)が入っているので、2つのカレンダーから抽出しています。
会社が公休などをカレンダーで公開してくれているのなら、そうしたものを使いましょう。

<ポイント>
- 日本の祝日: Googleの日本の祝日カレンダー
- 会社の公休: 自分のカレンダーの「特別休日」というイベント

  // Googleの日本の祝日カレンダー
  const holidayCalendar = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com");
  // 自分のカレンダー
  const myCalendar = CalendarApp.getDefaultCalendar();

  // 日本の祝日カレンダーと既定のカレンダーの「特別休日」(会社の休みなど)を抽出
  [
    holidayCalendar.getEvents(from, to),
    myCalendar.getEvents(from, to, {
      search: "特別休日"
    })
  ].forEach(function (holidays) {
     // 後述
  });
}

祝日に行われているイベントを抽出しよう

祝日が取得できたら、祝日の開始~終了までに行われているイベントを抽出しましょう。

ただし、いくつか前提を設けています。
1. 消す対象は「定例」と「共有会」に限定しました。(お好みで)
2. 消す対象は「繰り返しイベント」に限定しています。
3. 祝日をまたぐようなイベントは想定しない。(冬期休暇とか)

      // 祝休日と重複する「定例」「共有会」を抽出
      ["定例", "共有会"].forEach(function (query) {
        defaultCalendar.getEvents(holidayFrom, holidayTo, {
          search: query
        }).forEach(function (event) {
          if (event.isRecurringEvent()) {
            // 通知…ここでは割愛

            // イベントの削除
            event.deleteEvent();
          }
        });
      });
    });

最後に

最後まで読んでくださってありがとうございます。
今回はちょっと簡単過ぎでしたでしょうか:sweat:

最初、祝日テーブルをスプレッドシートに置こうとしていたのですが、祝日のメンテナンスをしたくないので、Googleカレンダーを使わせてもらいました。
日本は祝日が多い国ですが、特にスクリプトが重いということはなかったです。月に1~2日程度ですからね。

それでは。

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

JavaScript: var/letのスコープとletによる巻き上げ

「入門JavaScriptプログラミング」
https://www.amazon.co.jp/dp/479815864X
この本でJavaScriptの学習中。

このタイトルをしておきながら
「ES5以前のJavaScriptを理解している人向けに、ES2015以降の機能について紹介する」
というやや凶悪(?)な一面を持つな本だ。

タイトルはその第1章の内容。
もうvarは使わない気がするが、2点知らなかった内容があったので記録に残す。

基本:letとvarのscope

  • varは関数スコープ `` if (true) { var foo =bar`; }

console.log(foo); // "bar"
```

  • letはブロックスコープ `` if (true) { let foo =bar`; }

console.log(foo); // fooはブロックの外側に存在しないためエラー
```

この辺はただの前提。以下が本題。

varの関数スコープによる厄介な影響

<ul>
  <li>one</li>
  <li>two</li>
  <li>three</li>
  <li>four</li>
  <li>five</li>
</ul>

...

<script>
  var items = document.querySelectorAll;
  for (var i = 0; i < 5; i++) {
    var li = item[i];
    li.addEventListener('click', () => {
      alert(li.textContent + ':' + i);
    });
  }
</script>

上のコードはhtmlの各リストアイテムをクリックすると、対応したメッセージのアラートが表示される、ということを意図して書かれている。が、実際には期待通りには動作しない。どのリストアイテムをクリックしても、表示されるのは"five: 5"になる。

var ivar liは関数スコープなので、5回のforループの全てで共有されている。
つまりどのリストアイテムをクリックしても、結局は同じメッセージのアラートが表示される。
forの最後のループが終わった際、'i = 4'の時のループが終わった際には、i = 5, li = <li>five</li>になっているため、前述の通り必ず"five: 5"が表示される。

varの代わりにletを使えば意図した動作になる。
letはブロックスコープであるため、forの各ブロックごとにlet i, let liは独立して存在することになるからだ。

正直なところもうvarを自分で書くことはないと思うが、既存コードに存在する場合はこの点注意が必要になりそうだ。

letによる変数の巻き上げ

letによる変数による巻き上げとは、「変数が宣言されたスコープの内側では、変数がスコープ全体を消費する」と言うことだ。
これだけだとさっぱり分からないのでコードで説明する。

let num = 10;
const getNum = () {
  return num;
}
console.log( getNum() );

このコードはコンソールに10を表示する。getNum()内にnumの宣言が無い為、スコープチェーンの仕組みに則って、外側にあるlet num = 10;が用いられる。

let num = 10;
const getNum = () {
  let num = 5 // 変更箇所
  return num;
}
console.log( getNum() );

この場合はコンソールに5が表示される。これはまだ直感的だろう。

ではこれはどうだろう?
javascript
// 外側のnumのスコープここから
let num = 10;
const getNum = () {
// getNum()内のnumのスコープここから
console.log(num) // 変更箇所
let num = 5;
return num;
// getNum()内のnumのスコープここまで
}
console.log( getNum() );
// 外側のnumのスコープここまで

まずこの場合のconsole.log(num)let num = 10;let num = 5のどちらを参照するか?
正解はlet num = 5だ。この現象が「let による巻き上げ」だ。numがどこで宣言されていようと、numはスコープ全体を消費する。
(巻き上げ自体はvarでも同様のことが言える。varの場合は関数の単位でスコープ全体を消費する)

上のコードの場合もう1つ問題が発生する。宣言の前にnumが参照されるとどうなるか?とということだ。
具体的にはconsole.log(num)let num = 5よりも前に存在している。

この場合console.log(num)はエラーが発生する。letで宣言される関数が、宣言される前にスコープ内で実際にアクセスされた場合は、参照エラーとなる。
varの場合は参照エラーにはならないが値は常に未定義となる。なおこのようなエラーが発生する領域をTDZ(Temporal Dead Zone)と呼ぶ。

ifを絡めた面倒な話

これにifを絡めるとやや面倒な話になる。

let num = 0;
function getNum() {
  if (!num) {
    let num = 1;
  }
  return num;
}
console.log( getNum() );

この場合コンソールに表示されるのは、1...ではなく0になる。
以下のようにスコープを明示すると分かりやすくなる。

let num = 0;
function getNum() {
  if (!num) { // このnumは(スコープチェーンにより"let num = 0;"を参照)
    // if内のブロックのスコープここから
    let num = 1;
    // if内のブロックのスコープここまで
  }
  return num;
}
console.log( getNum() );

let num = 1はif内のブロックでのみ有効であり、ifの条件式におけるnum & return num;let num = 0;を参照している。
言われれば当たり前のことだが、慣れないと引っかかりそうな話ではある。

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

JavaScript勉強の記録その18: 数値の操作ができるメソッドまとめ

floorメソッド

index.js
let avg = 7.3333;
console.log(Math.floor(avg)); //切り下げ

//=>7

ceilメソッド

index.js
let avg = 7.3333;
console.log(Math.ceil(avg)); //切り上げ

//=>8

roundメソッド

index.js
let avg = 7.3333;
console.log(Math.round(avg)); //四捨五入

//=>7

toFixedメソッド

index.js
let avg = 7.3333;
console.log(avg.toFixed(3)); //少数点第3位まで表示
//=>7.333
console.log(avg.toFixed(2)); //少数点第2位まで表示
//=>7.33

randomメソッド

index.js
let avg = 7.3333;
console.log(Math.random()); //0~1までのランダムの数値を生成

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

JavaScriptの変数再代入でやっちまったこと

今回の原因

PHPをよく使っているのですが、
PHPの場合変数の宣言と代入はこのようにします。
これが今回のイージーミスに繋がったと言い訳しておきます。

name.php
$hoge = '犬'; // hogeという変数に「犬」という文字列が入りました。
echo $hoge; // 犬

$hoge = '猫'; // さっき宣言した変数hogeに、値が代入され「猫」に変わりました。
echo $hoge; // 猫

JavaScriptの変数宣言・代入

JavaScriptの場合の変数宣言と代入はこうです。

name.js
let hoge = ''; // hogeという変数に「犬」という文字列が入りました。

JavaScriptの変数への再代入

そして代入をしてみます。

間違い

name.js
let hoge = ''; // 宣言・代入
let hoge = ''; // 間違い。代入されません。
console.log(hoge) // hoge is not defined

正しい

name.js
let hoge = ''; // 宣言・代入
hoge = ''; // 正しい。
console.log(hoge) // 猫

反省

PHPだと変数の前には'$'をつけるので、JavaScriptでもうっかりつけてしまって、
「ファッッ?! 挙動がおかしい!!!」
となりました。気をつけます。

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

JavaScriptの変数代入でやっちまったこと

今回の原因

PHPをよく使っているのですが、
PHPの場合変数の宣言と代入はこのようにします。
これが今回のイージーミスに繋がったと言い訳しておきます。

name.php
$hoge = '犬'; // hogeという変数に「犬」という文字列が入りました。
echo $hoge; // 犬

$hoge = '猫'; // さっき宣言した変数hogeに、値が代入され「猫」に変わりました。
echo $hoge; // 猫

JavaScriptの変数宣言

JavaScriptの場合の変数宣言はこうです。

name.js
let hoge = ''; // hogeという変数に「犬」という文字列が入りました。

JavaScriptの変数への代入

そして代入をしてみます。

間違い

name.js
let hoge = ''; // 宣言
let hoge = ''; // 間違い。代入されません。
console.log(hoge) // 犬

正しい

name.js
let hoge = ''; // 宣言
hoge = ''; // 正しい。
console.log(hoge) // 猫

反省

PHPだと変数の前には'$'をつけるので、JavaScriptでもうっかりつけてしまって、
「ファッッ?! 挙動がおかしい!!!」
となりました。気をつけます。

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

【Nuxt.js】Validation基礎編:フォームバリデーション

前置き

validation.gif
正規表現を使ってフォームバリデーション!
今回はメールアドレス形式
hoge@hoge.hogeのみ送信可能にします?
間違った形式で送られることがないので
トラブル防止などにもなり便利ですね♪

初期準備

今回はコンポーネントは使わず
index.vueの1ページに全て書きます✍️
まずはFormタグと送信ボタンを用意。
基本的なフォーム構成は別記事で解説済み?
◾️【Nuxt.js】v-model実践編:オリジナルフォームの簡単な作り方
https://note.com/aliz/n/n5b9bd618399e

{{ Validation.result }}
入力した値それぞれに対応したテキストを表示。
・空の場合
・バリデーションしてNGの場合
・バリデーションしてOKの場合

index.vue
<template>
 <div class="page">
   <p>{{ Validation.result }}</p>
   <form @submit.prevent>
     <label>Mail
       <input
         type="text"
         placeholder="メールアドレスを入力"
         v-model="mail"
       >
     </label>
   </form>
   <button type="submit" @click="checkForm">
     送信
   </button>
 </div>
</template>

<script>
export default {
 data () {
   return {
     mail: "",
     Validation:{
       result: "",
     },
   }
 },
}
</script>

methodsを追加(正規表現)

【基礎文法】
Vueの公式ページをご確認ください。
https://jp.vuejs.org/v2/cookbook/form-validation.html#カスタムバリデーションの利用

index.vue
methods名: function (引数名) {
  var 変数名 = 正規表現;
  return 変数名.test(引数名);
}

【コード】
引数名は分かりやすくinputdataに、
変数名は正規表現を英語にしたregexに。
正規表現についてはこちらで解説しています。
◾️【Nuxt.js】正規表現基礎編①:よく使う表現を単語分割で解説!
https://note.com/aliz/n/n898319c9042d

index.vue
<script>
export default {
 data () {
   return {
     mail: "",
     Validation:{
       result: "",
     },
   }
 },
 methods:{
   checkString (inputdata){
     var regex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
     return regex.test(inputdata);
   }
 }
}
</script>

methodsを追加(送信可不可)

【構成】
送信ボタンを押すとcheckForm発動。
2つに分けて考えます。
これを分けるために変数mailBoolの
真偽値によってパターン分けしています?
◾️空入力&形式が間違った場合
 mailBool = false
◾️形式が正しい場合
 mailBool = true

【コード】

index.vue
<script>
export default {
 data () {
   return {
     mail: "",
     Validation:{
       result: "",
     },
   }
 },
 methods:{
   checkForm() {
     var mailBool = false
     if (!this.mail) {
       this.Validation.result="入力してください"
     }
     else if (!this.checkString(this.mail)){
       this.Validation.result="メールアドレス形式で入力してください"
     } else {
       mailBool = true
     }

     if(mailBool === true){
       this.Validation.result="送信に成功しました!"
       alert(this.mail + 'で送信しました。');
       console.log(this.mail);
       this.mail = "";
     }
   },
   checkString (mail){
     var regex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
     return regex.test(mail);
   }
 }
}
</script>

【解説】
◾️空入力&形式が間違った場合
 mailBool = false
 ・if (!this.mail)
   data内mail=inputの入力がfalse(空)なら
   該当テキストを表示
 ・else if (!this.checkString(this.mail))
   空ではないが、
   checkString関数でthis.mailを
   バリデーションしてfalseなら
   該当テキストを表示
 ・else { mailBool = true }
   どちらでもなく正しく入力された場合
   mailBool = trueにする

◾️形式が正しい場合
 mailBool = true
 ・this.mail = "";
   送信後はメールアドレスを
   残したままにせず空に戻す

全体コード

index.vue
<template>
 <div class="page">
   <p>{{ Validation.result }}</p>
   <form @submit.prevent>
     <label>Mail
       <input
         type="text"
         placeholder="メールアドレスを入力"
         v-model="mail"
       >
     </label>
   </form>
   <button type="submit" @click="checkForm">
     送信
   </button>
 </div>
</template>

<script>
export default {
 data () {
   return {
     mail: null,
     Validation:{
       result: "",
     },
   }
 },
 methods:{
   checkForm() {
     var mailBool = false
     if (!this.mail) {
       this.Validation.result="入力してください"
     }
     else if (!this.checkString(this.mail)){
       this.Validation.result="メールアドレス形式で入力してください"
     } else {
       mailBool = true
     }

     if(mailBool === true){
       this.Validation.result="送信に成功しました!"
       alert(this.mail + 'で送信しました。');
       console.log(this.mail);
       this.mail = "";
     }
   },
   checkString (inputdata){
     var regex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
     return regex.test(inputdata);
   }
 }
}
</script>

<style lang="scss" scoped>
.page {
 padding: 50px 20px;
 p {
   font-size: 24px;
   position: absolute;
   top: 10px;
 }
 label {
   display: block;
   margin-top: 5px;
 }
}
</style>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSer向け dart配列API

List関連の関数
https://api.dart.dev/stable/2.7.0/dart-core/List-class.html

Array.prototype.slice()

start-endの配列を抽出

var testList = [1, 2, 3];
testList.sublist(0,2); // => [1, 2]

Array.prototype.filter()

条件にあうものを抽出

var testList = [1, 2, 3];
testList.where((int item) => item >= 2).toList(); // => [2, 3]

Array.prototype.map()

mapで値を変換して返す

var testList = [1, 2, 3];
testList.map((int item) => Text(item)).toList(); // => [Text("2"), Text("3")]

Array.prototype.every()

全ての条件がtrue であれば true

var testList = [1, 2, 3];
testList.every((int item) => item >= 0).toList(); // => true
testList.every((int item) => item >= 2).toList(); // => false

Array.prototype.some()

1つの条件がtrueであれば true

var testList = [1, 2, 3];

testList..any((item) => item >= 2); // => true
testList..any((item) => item >= 4); // => false
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの「継承」はどう定義されるのか? 仕様書を読んで理解する

継承は、クラスベースのオブジェクト指向における基本的な概念のひとつであると信じられています。JavaScriptにもES2015以降はclass構文があり、extendsを用いてクラスの継承を記述することができます。また、それより以前もprototypeを通じてオブジェクト指向的なプログラムが書かれてきました。

この記事では、JavaScriptにおける「継承」がどのようなものであり、どのように定義されるのかを解説します。タイトルにある通り、今回はECMAScript仕様書に対する解説を中心とします。

仕様書はJavaScriptというプログラミング言語がどのようなものかを定義する文書であり、あなたが書いたJavaScriptプログラムは仕様書に書かれた通りの動きをすることになります1。したがって、たとえあなたが自分自身で書いたものだったとしても、JavaScriptプログラムの意味を完全な確信を持って理解したいならば、仕様書を読んで理解することがその唯一の手段なのです。

今回は継承というトピックを取り上げつつ、仕様書がJavaScriptという言語をどのように定義するのかの一端を理解するための、仕様書入門のような記事を目指しました。仕様書というだけで臆することなく、この記事を頼りに果敢に仕様書リーディングに挑戦してみましょう。

ウォーミングアップ:prototypeと継承

仕様書を読み始める前に、JavaScriptにおいて継承という機構がそもそもどのように実現されているのかを解説しておきます。もう知っているという方は次の節に進んでも構いません。

インスタンスの作成とinstanceof

JavaScriptにはinstanceofという演算子があります。これは、あるオブジェクトがあるクラスのインスタンスであるかどうかを判定する演算子です。

class MyClass {}

const normalObj = {};
const myClassObj = new MyClass();

console.log(normalObj instanceof MyClass);  // false
console.log(myClassObj instanceof MyClass); // true

逆に、あるクラスのインスタンスを作る方法は、このプログラムにもあるようにnewを使うことです。

なお、上のプログラムではclass構文を用いてクラスを宣言しましたが、これはES2015以降で使用できる方法です。それ以前はただの関数をクラスとして扱っていました。この関数はコンストラクタとして扱われ、newされるとその関数が呼ばれます。

function MyClass() {}

const myClassObj = new MyClass();

console.log(myClassObj instanceof MyClass); // true

prototypeによるインスタンスの特徴付け

クラス定義にはメソッドを含めることができます。クラス定義に書かれたメソッドは、当然ながらインスタンスから利用可能です。しかしながらhasOwnPropertyメソッドで調べると、インスタンスにそんな名前のプロパティは無いという結果になります。

class MyClass {
  method() {
    console.log("hi");
  }
}

const obj = new MyClass();
obj.method(); // "hi" と表示される

console.log(obj.hasOwnProperty("method")); // false と表示される

これはやや難しいところですが、我々にオブジェクトのプロパティ・メソッドとして見えるものは2種類あります。ひとつはオブジェクト自身のプロパティ、もうひとつはオブジェクトのプロトタイプ由来のプロパティです。{ foo: 123 }とかobj.bar = 456といった方法で宣言されるのはオブジェクト自身のプロパティである一方、クラスにて宣言されたメソッドはプロトタイプ由来のプロパティとなります。そして、hasOwnPropertyは前者に対してのみtrueを返すのです。

つまるところ、JavaScriptのオブジェクトは「連想配列 + プロトタイプ」として説明できます2。そして、JavaScriptのオブジェクト指向的側面を支えるのがプロトタイプなのです。昔を知っている方は「prototypeなんでES2015が出てお役御免になったでしょ?」とお思いかもしれませんが、class構文の裏を支えるのもやはりプロトタイプの機構です。

オブジェクトのプロトタイプは、何か別のオブジェクトです(無い場合もあります)。プロトタイプの機構は非常に単純なもので、あるオブジェクトが自身が持たないプロパティにアクセスされたとき、次にプロトタイプを探しに行くのです。

あるオブジェクトのプロトタイプを取得する手段がObject.getPrototypeOfです。MyClassのインスタンスの場合、そのプロトタイプはMyClass.prototypeになります。

class MyClass {
  method() {
    console.log("hi");
  }
}

const obj = new MyClass();

console.log(Object.getPrototypeOf(obj) === MyClass.prototype); // true

そして、obj.methodとして呼び出せるものは実はMyClass.prototype.methodなのです。methodはプロトタイプ由来のプロパティだったことになります。

console.log(obj.method === MyClass.prototype.method); // true

プロトタイプ関連のメソッド

先ほどObject.getPrototypeOfを紹介しましたが、他にも関連メソッドがあります。ひとつはisPrototypeOfです。これはオブジェクトが持つプロパティ(Object.prototypeに存在)であり、自分自身が与えられたオブジェクトのプロトタイプであるかどうかを判定します。例で見ると分かりやすいでしょう。

console.log(MyClass.prototype.isPrototypeOf(obj)); // true

つまるところ、これはobj instanceof MyClassと同じ意味となります。

もうひとつはObject.setPrototypeOfです。これは、既に存在するオブジェクトのプロトタイプをあとから書き換えることができるというたいへん強力な(そして遅い)メソッドです。

例えば、ただのオブジェクトである{}を、Object.setPrototypeOfを使ってあとからMyClassのインスタンスにできます。

class MyClass {
  method() {
    console.log("hi");
  }
}

const obj = {};

console.log(obj instanceof MyClass); // false
console.log(obj.method); // undefined

Object.setPrototypeOf(obj, MyClass.prototype);

console.log(obj instanceof MyClass); // true
obj.method(); // "hi" と表示される

Object.setPrototypeOfを使う機会は滅多に無いでしょう。MDNのページにも、こんなもの使うんじゃないぞという注意がでかでかと書いてあります。それよりも使いそうなのがObject.createです。これは新しいオブジェクトを作るときにプロトタイプを指定できるメソッドです。MyClassのインスタンスを作りたければこうです。

const obj2 = Object.create(MyClass.prototype);

console.log(obj2 instanceof MyClass); // true
obj2.method(); // "hi" と表示される

Object.create(MyClass.prototype)new MyClassと同様にMyClassのインスタンスを作ることができます。違いは、後者はMyClassのコンストラクタが呼ばれる一方で前者は呼ばれないことです。逆に言えば、new構文は「適当なプロトタイプを持つオブジェクトを作る」「コンストラクタを呼ぶ」という2段階の工程をまとめてやってくれる親切な構文だということです。

__proto__

読者の中には__proto__についてご存知の方も多いでしょう。オブジェクトをコピーする系の関数の脆弱性の原因によくなっているあれです。これはオブジェクトのプロトタイプが入っているという直球なプロパティです。

class MyClass {
  method() {
    console.log("hi");
  }
}

const obj = new MyClass();

console.log(obj.__proto__ === MyClass.prototype); // true
obj.method(); // "hi"

obj.__proto__ = null;

console.log(obj instanceof MyClass); // false
console.log(obj.method); // undefined

これはObject.getPrototypeOfObject.setPrototypeOfと同等の機能を有しています。見て分かる通り__proto__のほうが名前が怪しいので、基本的には避けましょう3

継承

次に、継承がJavaScriptでどう扱われているかを見ましょう。まず、instanceofisPrototypeOfの挙動です。

class SuperClass {
  method() {
    console.log("hey");
  }
}
class SubClass extends SuperClass {}

const obj = new SubClass();

obj.method(); // "hey" と表示される
console.log(obj instanceof SubClass, obj instanceof SuperClass); // true true
console.log(
  SubClass.prototype.isPrototypeOf(obj),
  SuperClass.prototype.isPrototypeOf(obj)
); // true true

このように、objSubClassのインスタンスですが、SbbClassSuperClassを継承しているためobjは間接的にSubClassのインスタンスとなります。instanceofisPrototypeOfはそれを認識して上記のような場合にもtrueを返します。

ところで、SubClassSuperClassを継承しているということはどのように表現されるのでしょうか。答えは、「SubClass.prototypeSuperClassのインスタンスである」です。確かめてみましょう。

class SuperClass {}
class SubClass extends SuperClass {}

console.log(SubClass.prototype instanceof SuperClass); // true
console.log(Object.getPrototypeOf(SubClass.prototype) === SuperClass.prototype); // true

SubClassのインスタンスであるobjSuperClassのメソッドを持っているのは次のように説明できます。すなわち、obj自身methodという名前のプロパティを持っていないためプロトタイプ(SubClass.prototype)に移譲されます。SubClass.prototypemethodという名前のプロパティを持っていないため、次はSubClass.prototypeのプロトタイプであるSuperClass.prototypeに移譲されます。ここでmethodが発見されます。

このような移譲の連鎖によって継承という機構が実現されています。これが、プログラマのレベルから見たJavaScriptの継承です。

最上位のベースクラスとしてのObject

実は、普通のオブジェクトはObjectのインスタンスであるということが知られています。さっそく試してみましょう。

const obj = {};

console.log(obj instanceof Object); // true
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

すでに何気なく登場していたhasOwnPropertyisPrototypeOfも全てのオブジェクト4が持つプロパティでしたが、その実態はObject.prototypeにあります。

console.log(obj.hasOwnProperty === Object.prototype.hasOwnProperty);

その意味で、全てのオブジェクトはObjectのインスタンスであると言えます。全てのクラスは暗黙のうちにObjectを継承しています。

ただし、上で「全てのオブジェクト」の述べましたが、ひとつ例外があります。実はプロトタイプが無いオブジェクトを作ることができるのです。JavaScriptプログラム上では、そのようなオブジェクトはプロトタイプがnullのオブジェクトとして現れます。例えば、プロトタイプが無いオブジェクトを新規に作るにはObject.create(null)とすればよいことが分かります。実際にやってみると、そのようなオブジェクトは確かにObjectのインスタンスではないことが分かります。

const obj = Object.create(null);

console.log(obj instanceof Object); // false
console.log(Object.getPrototypeOf(obj)); // null

実はObject.prototypeもまた、プロトタイプが無いオブジェクトです。Object.prototypeはその意味でプロトタイプチェーンの終端であるといえるのです。

プロトタイプの機構を仕様書で追う

さて、ウォーミングアップは以上です。ここからは、前節で触れたような内容がJavaScript仕様書でどのように定義されているのか見ていきましょう。

最初は、そもそも「オブジェクトは連想配列+プロトタイプである」という概念が仕様書にどう書かれているのか見ましょう。この記事では手とり足取り解説しますのでご安心ください。ただ、実際に自分で仕様書を開いたほうがついて来やすいかもしれません。仕様書へのリンクも改めて用意しておきます。

オブジェクトの定義

仕様書を散策する手がかりは目次と全文検索が基本ですが、今回は目次を眺めていくといいですね。オブジェクトに関することが書かれていそうな箇所はいろいろありますが、最初に目に付くのは6 ECMAScript Data Types and Valuesです。この章には、そもそも「JavaScriptにおける値とは何か」に関する定義が書かれています。6.1 ECMAScript Language Types では、JavaScriptの値の種類が1つずつ定義されています。

今回はオブジェクトに興味があるので、6.1.7 The Object Typeを見ることになります。いくつかの文を引用します。以降、訳はすべて筆者によるものです。

An Object is logically a collection of properties. Each property is either a data property, or an accessor property.

(訳)オブジェクトは論理的にはプロパティの集まりです。プロパティは、データプロパティかアクセサプロパティのどちらかです。

logically(論理的には)という言葉の意味がつかめない人もいるかもしれません。これは、仕様書では処理系における実際のメモリ配置とかそういった部分には踏み入らないことを意味しています。仕様書に書かれていることを処理系(ブラウザなど)がどう実装するかは自由であり、どんな手段であろうと仕様書に書かれている通りに動けば構わないのです。仕様書ではオブジェクトについて「プロパティの集まりである」という要件が満たされていればよく、それが実際どう実装されているのかは興味がありません。

この要件は仕様書がJavaScriptという言語を定義するための最低限の定義とも言えるものであり、あくまで実際の実装に踏み入らずに定義をベースとして言語を定めるという姿勢の現れです。このことが論理的という言葉で表現されています。

プロパティについてはさらに記載があります。

Properties are identified using key values. A property key value is either an ECMAScript String value or a Symbol value.

(訳)プロパティはキー値によって識別されます。プロパティのキー値は文字列かSymbolのどちらかです。

アクセサプロパティ(ゲッタとセッタで定義されるプロパティのことです)は置いておくとしても、「オブジェクトは連想配列である」ということがここまでで書かれていることになります。キーによって識別されるデータ(プロパティ)の集まりというのはまさに連想配列のことだからです。

次に6.1.7.2 Object Internal Methods and Internal Slotsに目を向けます。この節ではインターナルスロット(内部スロット)という概念が定義されています。インターナルスロットは、言うなれば仕様書内からしか見えないプロパティです。各オブジェクトはそれぞれインターナルスロットを持っており、仕様書のアルゴリズムはインターナルスロットを見たり書いたりすることができます。実際のプログラムからはインターナルスロットは不可視です。

また、インターナルメソッドというものもあり、これはインターナルスロットの関数版です。この節ではインターナルメソッドの定義が中心です。なお、ここでessential internal methods(必須インターナルメソッド)についても定義されています。これは全てのオブジェクトが持つインターナルメソッドのことです。

お察しの通り、オブジェクトのもうひとつの特徴である「プロトタイプを持つ」という点はこのインターナルスロットによって表現されます。しかし、ここまで見てきた箇所を探してもプロトタイプに関する記載はありません。そもそもここで具体的に定義されているのは必須インターナルメソッドだけです。

ということで再び目次を眺めると、9.1 Ordinary Object Internal Methods and Internal Slotsが関係ありそうです。以下に関連部分を引用します。なお、文中に出てくるordinaryオブジェクトというのは普通のオブジェクトのことで、我々が普段扱うオブジェクトは大抵がordinaryオブジェクトです。

All ordinary objects have an internal slot called [[Prototype]]. The value of this internal slot is either null or an object and is used for implementing inheritance.

(訳)全てのordinaryオブジェクトは[[Prototype]]というインターナルスロットを持ちます。このインターナルスロットの値はnullまたはオブジェクトであり、継承を実装するのに用いられています。

このように、インターナルスロットは[[ ]]で囲われた名前を持ちます。この文には[[Prototype]]について「継承を実装するために用いられる」としかありませんので、これまで説明したような機構が具体的にどう実装されているのかを理解するにはさらに仕様書を読み進める必要があります。

Object.createの定義を読む

手始めに、Object.createがどう定義されているのか、仕様書を読み解いてみましょう。このメソッドは、指定されたプロトタイプでオブジェクトを作るだけという簡単な挙動なので定義も比較的読みやすいものとなっています。

仕様書の目次からそれっぽいところを探すと、Object.createの定義は19.1.2.2 Object.create(O, Properties)に見つかります。定義はやはり短いですね。

The create function creates a new object with a specified prototype. When the create function is called, the following steps are taken:

  1. If Type(O) is neither Object nor Null, throw a TypeError exception.
  2. Let obj be ObjectCreate(O).
  3. If Properties is not undefined, then
    a. Return ? ObjectDefineProperties(obj, Properties).
  4. Return obj.

(訳)create関数は与えられたプロトタイプを持つ新しいオブジェクトを作成します。create関数が呼ばれたとき、次の手順が実行されます。

  1. もし Type(O) が Object でも Null でもなければ、TypeError例外を発生させる。
  2. objをObjectCreate(O)とする。
  3. もしPropertiesがundefined以外なら、
    a. ? ObjectDefineProperties(obj, Properties)を返す。
  4. objを返す。

このように、組み込み関数の定義は案外直感的です。関数の動作がこのように自然言語で書かれています。また、Type, ObjectCreate, ObjectDefinePropertiesという別の関数の呼び出しが含まれています。これらは abstract operation(抽象操作?)と呼ばれ、仕様書内で定義された関数です。abstract operationはランタイムに利用できる何らかの組み込み関数に対応するものではなく、仕様書内でのみ参照・利用されるものです。

また、ステップ3-aに?という記法があります。これは5.2.3.4 ReturnIfAbrupt Shorthandsで定義されているものであり、簡単に言えば「抽象操作でエラーが発生したらそのエラーを伝播させる」という挙動を表す省略記法です。JavaScriptプログラムでは発生したエラーは自動的に伝播しますが、仕様書のアルゴリズムのレベルでは明示的に伝播させなければいけません。この操作は頻出なので、?という短い記法が用意されているのです。また、?のほかに!という種類があり、これはその操作が失敗しないことを表すものです5

さて、Object.createの定義を読むと分かる通り、処理の本体はObjectCreateという抽象操作にあるようです。次はこちらを読みましょう。

多くの定義はこのように、通常の言葉で書かれた説明と、番号付きリストで書かれたアルゴリズム部分から成ります。リスト部分だけ見れば動作はちゃんと定義されていますが、読み手の分かりやすさのために最初の説明が書かれています。

The abstract operation ObjectCreate with argument proto (an object or null) is used to specify the runtime creation of new ordinary objects. The optional argument internalSlotsList is a List of the names of additional internal slots that must be defined as part of the object. If the list is not provided, a new empty List is used. This abstract operation performs the following steps:

  1. If internalSlotsList is not present, set internalSlotsList to a new empty List.
  2. Let obj be a newly created object with an internal slot for each name in internalSlotsList.
  3. Set obj's essential internal methods to the default ordinary object definitions specified in 9.1.
  4. Set obj.[[Prototype]] to proto.
  5. Set obj.[[Extensible]] to true.
  6. Return obj.

(訳)抽象操作ObjectCreateは引数proto(オブジェクトまたはnullである)を取り、新しいordinaryオブジェクトをランタイムで作るために使用されます。オプショナル引数internalSlotsListは、作成されるオブジェクトに対して定義されなければならない追加のインターナルスロットのリストです。このリストが渡されなかった場合は、空リストが用いられます。この抽象操作は次の操作を実行します。

  1. internalSlotsListが存在しない場合は、新しい空リストとする。
  2. internalSlotsListで示されたそれぞれのインターナルスロット名を備えた新しいオブジェクトを作り、それをobjとする。
  3. objの必須インターナルメソッドを9.1で定義されたデフォルトの内容で作成する。
  4. obj.[[Prototype]]をprotoとする。
  5. obj.[[Extensible]]をtrueにする。
  6. objを返す。

読むと分かる通り、「新しいオブジェクトを作成し、必要なインターナルスロットを用意する」という内容です。ObjectCreateは普通のオブジェクトを作る操作なので、インターナルメソッドの動作はデフォルトの内容です。そして、問題の[[Prototype]]インターナルスロットもちゃんとここでセットされています。

このように、インターナルスロットはちゃんとオブジェクトが作成されるたびに明示的に用意されています。たいへん健気ですね。そもそも「結局オブジェクトを作るってどういうことなの?」と思った方もいるかもしれませんが、それについては仕様書の守備範囲ではありません。そこに踏み込まなくても言語は定義できるからです。

Object.getPrototypeOfの定義も見てみる

では、次にObject.getPrototypeOfの定義はどうなっているか見てみましょう。とはいっても、ここまで読んだ皆さんはその定義がどうなっているか容易に想像できるでしょう。与えられたオブジェクトの[[Prototype]]インターナルスロットの値を返せばいいのです。

19.1.2.12 Object.getPrototypeOf(O)

When the getPrototypeOf function is called with argument O, the following steps are taken:

  1. Let obj be ? ToObject(O).
  2. Return ? obj.[[GetPrototypeOf]]​().

(訳)getPrototypeOf関数が引数Oで呼ばれたとき、次の手順が実行されます。

  1. objをToObject(O)とする。
  2. obj.[[GetPrototypeOf]]​()を返す。

実際の定義は非常に短いですが、どうも一筋縄ではいかないようです。最初のToObjectは与えられた値がオブジェクト以外だったらオブジェクトに変換する抽象操作です(nullとundefiendはエラーになります)。

問題は2で、実際にプロトタイプを返す処理が[[GetPrototypeOf]]インターナルメソッドに移譲されています。その理由はProxyの存在です。Proxyは「プロトタイプを取得する」という操作に対してカスタムされた挙動を定義することができます。このことを、仕様書では「[[GetPrototypeOf]]インターナルメソッドの挙動が異なる」という形で定義しているのです。この記事では触れませんが、Proxyの定義を読むとそのようなことが書いてあります。

では、今回は普通のオブジェクトを相手しているので、普通のオブジェクトの[[GetPrototypeOf]]の挙動を追いましょう。Object.createの定義を読んだときに見たように、普通のオブジェクトに対する必須インターナルメソッドの定義は9.1に書かれています。[[GetPrototypeOf]]の定義はこうです。

9.1.1 [[GetPrototypeOf]]​()
When the [[GetPrototypeOf]] internal method of O is called, the following steps are taken:

  1. Return ! OrdinaryGetPrototypeOf(O).

9.1.1.1 OrdinaryGetPrototypeOf(O)
When the abstract operation OrdinaryGetPrototypeOf is called with Object O, the following steps are taken:

  1. Return O.[[Prototype]].

(訳)Oの[[GetPrototypeOf]]内部メソッドが呼ばれた場合、次の操作が実行されます。
1. ! OrdinaryGetPrototypeOf(O)を返す。

9.1.1.1 OrdinaryGetPrototypeOf(O)
抽象操作OrdinaryGetPrototypeOfがオブジェクトOで呼ばれた場合、次の操作が実行されます。

  1. O.[[Prototype]]を返す。

ということで、やっと[[Prototype]]が登場しました。ここに書いてある通り、普通のオブジェクトは[[GetPrototypeOf]]​()が呼ばれたらそのオブジェクトの[[Prototype]]内部スロットの値が返されます。これでObject.getPrototypeOfの説明がつきましたね。

プロパティアクセスの定義を見る

次は、プロトタイプによる継承機構の本体とも言えるプロパティアクセス(obj.foo)の定義を見てみます。ここには、無かったらプロトタイプを辿るという挙動が定義されているはずです。今回は構文定義がスタート地点となります。構文の定義はいくつかの章にまとまっており、obj.fooは式なので12 ECMAScript Language: Expressionsの中にあります。具体的には12.3.2.1 Runtime Semantics: Evaluationです。

MemberExpression . Identifier の定義

以下は MemberExpression . Identifier という構文の実行の定義です。

MemberExpression: MemberExpression.IdentifierName

  1. Let baseReference be the result of evaluating MemberExpression.
  2. Let baseValue be ? GetValue(baseReference).
  3. Let bv be ? RequireObjectCoercible(baseValue).
  4. Let propertyNameString be StringValue of IdentifierName.
  5. If the code matched by this MemberExpression is strict mode code, let strict be true, else let strict be false.
  6. Return a value of type Reference whose base value component is bv, whose referenced name component is propertyNameString, and whose strict reference flag is strict.

(訳)
1. MemberExpressionを実行し、その結果をbaseReferenceとする。
2. baseValueを? GetValue(baseReference)とする。
3. bvを? RequireObjectCoercible(baseValue)とする。
4. propertyNameStringをIdentifierNameのStringValueとする。
5. もしこのMemberExpressionがstrictモードのコードならstrictをtrueとし、それ以外ならstrictをfalseとする。
6. base value componentがbvであり、refereced name componentがpropertyNameStringであり、strict reference flagがstrictであるようなReference型の値を返す。

これを読むと、ちょっと様子がおかしいですね。最後の行を見ると「Referenceを返す」というよく分からないことが書いてあります。Referenceとは仕様書内でのみ用いられる値で、プロパティ(または変数)へのアクセスそのものを表す特殊な値です。6.2.4 The Reference Specification Typeには以下のように記載されています。

A Reference is a resolved name or property binding. A Reference consists of three components, the base value component, the referenced name component, and the Boolean-valued strict reference flag. The base value component is either undefined, an Object, a Boolean, a String, a Symbol, a Number, or an Environment Record. A base value component of undefined indicates that the Reference could not be resolved to a binding. The referenced name component is a String or Symbol value.

(訳)Referenceは変数またはプロパティに対する解決済のバインディングです。Referenceはbase value component, referenced name component, そして真偽値を値に持つstrict reference flagという3つの構成要素から成ります。base value componentはundefinedか、真偽値、文字列、シンボル、数値またはEnvironment Recordです。base value componentがundefinedの場合、そのReferenceがバインディングに解決できなかったことを表します。referenced name componentは文字列またはシンボルです。

バインディングというのは、変数やプロパティの中身ではなく、変数・プロパティそれ自身を指す言葉です。要するに、obj.fooと書いた段階ではまだ実際にobjからfooというプロパティが取り出されているわけではないのです。代わりに、「objfooにアクセスする」という情報がそのまま入ったReferenceが返されるのです。

Referenceがどのように活躍するかについては筆者の過去記事でも扱っています。詳細はそちらに譲りますが、参照の機構は代入などを仕様化する際に役立っています。代入の際は「どのオブジェクトのどのプロパティに代入するか」という情報を取り回す必要があり、それがまさにReferenceです。

さて、上の定義で行われているのはReferenceを作ることだけであり、実際に参照を解決してobj.fooの値を得るのは別の箇所で行われます。具体的にはこれを行うのはGetValueです。このGetValueは非常に出番の多い抽象操作です(上の定義にも出てきていますね)。値が欲しいのに参照が渡されるかもしれない画面では、参照を値に解決するためにGetValueが用いられます。

GetValueの定義を読む

GetValue(V)の定義を以下に引用します。訳すほど複雑なことは書いていないので日本語訳は省略します。

  1. ReturnIfAbrupt(V).
  2. If Type(V) is not Reference, return V.
  3. Let base be GetBase(V).
  4. If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
  5. If IsPropertyReference(V) is true, then
    a. If HasPrimitiveBase(V) is true, then
     i. Assert: In this case, base will never be undefined or null.
     ii. Set base to ! ToObject(base).
    b. Return ? base.[[Get]]​(GetReferencedName(V), GetThisValue(V)).
  6. Else base must be an Environment Record,
    a. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V)) (see 8.1.1).

1と2は、渡された値Vが参照以外だった場合の処理です。参照以外は何もせずにそのまま返されます。

3はGetBaseを呼び出していますが、これはVのbase value componentを取得するだけです。もしobj.fooを表すReferenceなら、base value componentはobjになります。

4はIsUnresolvableReference(V)ならばReferenceErrorを発生させます。これは、Vが「未定義の変数」への参照だった場合のことです。プログラム中で未定義の変数にアクセスした場合にエラーが発生するのはよく知られていますが、そのエラーはこのGetValueから発生しているのです。

5は、参照がプロパティへの参照か、それともただの変数への参照かによって分岐しています。今回はプロパティへの参照に興味があるのでifの中を見ましょう。結局のところ、よく見ると処理はbase.[[Get]]に移譲されていることが分かります。何やら先が長いですね。

[[Get]]の定義を読む

[[Get]]は見て分かるとおり内部メソッドであり、プロパティアクセスの処理を定義するものです。今回はやはり普通のオブジェクトにおける挙動を見ます。9.1.8 [[Get]]​(P, Receiver)です。なお、これはOrdinaryGet(O, P, Receiver)に移譲されています。

When the abstract operation OrdinaryGet is called with Object O, property key P, and ECMAScript language value Receiver, the following steps are taken:

  1. Assert: IsPropertyKey(P) is true.
  2. Let desc be ? O.[[GetOwnProperty]]​(P).
  3. If desc is undefined, then
    a. Let parent be ? O.[[GetPrototypeOf]]​().
    b. If parent is null, return undefined.
    c. Return ? parent.[[Get]]​(P, Receiver).
  4. If IsDataDescriptor(desc) is true, return desc.[[Value]].
  5. Assert: IsAccessorDescriptor(desc) is true.
  6. Let getter be desc.[[Get]].
  7. If getter is undefined, return undefined.
  8. Return ? Call(getter, Receiver).

1はAssertと書いてありますが、これはその地点で必ず満たされている条件を宣言するものです。仕様書は気をつけて書かれているため、Assertに反する状況に陥ることはありません(仕様書にバグが無ければ)。Assertは読み手が理解しやすいように提供されているものです。

2で[[GetOwnProperty]]という別の内部メソッドが使われています。深追いするのはやめておきますが、これはそのオブジェクトの与えられた名前のプロパティのプロパティデスクリプタを返す内部メソッドです。もしオブジェクトがそのプロパティを持たなければundefinedが返されます。

3の分岐は[[GetOwnProperty]]がundefinedを返した場合、つまりオブジェクトがそのプロパティを持っていなかった場合の処理です。最初に説明した通り、この場合にプロトタイプを辿るという動作が発生することになります。3の中身を見ると、まず[[GetPrototypeOf]]でOのプロトタイプを取得しています(少し前に確認した通り、普通のオブジェクトに対する[[GetPrototypeOf]]の挙動はそのオブジェクトの[[Prototype]]内部スロットの値を返すだけです)。ここではプロトタイプはparentという名前がついた変数に保存されています。

parnetがnullのとき、すなわちプロトタイプが無かった場合はundefinedを返すとあります。これが、「オブジェクトの存在しないプロパティにアクセスしたらundefiedが返る」という挙動を定義している箇所です。プロトタイプチェーンの末端(Object.prototype)まで見ても見つからなかった場合に最終的にここに行き着きます。

プロトタイプがあった場合はプロトタイプに処理が移譲されます。それを表すのが parnet.[[Get]]​(P, Receiver) という部分です。

4はオブジェクト自身がデータプロパティを持っていた場合にその値を返すという処理で、5以降はアクセサプロパティがあった場合の処理です。

ともかく、これでプロトタイプチェーンの機構を仕様書で確認することができました。「オブジェクト自身のプロパティをチェックし、無ければプロトタイプを見に行く」という処理が、普通のオブジェクトの[[Get]]内部メソッドの定義にほぼそのままの形で記述されていましたね。これは再帰になっているため、プロトタイプチェーンが長く連なっている場合にも正しく処理されます。

組み込みオブジェクトの継承構造

おまけ的な話題として、組み込みのオブジェクトの間の継承構造がどのように定義されているのかを見てみます。

普通のオブジェクトのプロトタイプ

我々がオブジェクトを作るとき、最も一般的なのは{}のようなオブジェクトリテラルを使って作る方法です。この方法で作られるのは普通のオブジェクトであり、しかも自動的にObject.prototypeをプロトタイプに持っています。このことは仕様書でどう定義されているのでしょうか。

今回は{}の挙動を調べたいので、オブジェクトリテラルを定義している部分を探しましょう。オブジェクトリテラルが式の一種であることを理解していれば探すのは難しくなく、目次のありそうな部分を眺めれば12.2.6 Object Initializerを見つけるのは容易いでしょう。文や式の場合は、Evaluationと書かれている部分を探しましょう。そこに実行時の挙動が定義されています。今回の場合は12.2.6.7 Runtime Semantics: Evaluationです。

ObjectLiteral:{}
1. Return ObjectCreate(%ObjectPrototype%).

はい、非常に単純ですね。ObjectCreateは既に出てきた抽象操作で、与えられたオブジェクトをプロトタイプとして、新しい普通のオブジェクトを作るものです。今回プロトタイプとして指定されているのは%ObjectPrototype%だそうです。

ここで何やら新しい記法が出てきました。このように% %で囲まれた名前はintrinsic objectと呼ばれ、要するに仕様書内で通用するグローバル変数のようなものです(書き換えられることはないので定数と呼ぶべきかもしれませんが)。仕様書のあちこちで使われるオブジェクトはこのようにintrinsic object(内部オブジェクト?)として定義され、仕様書内で簡単に参照できるようになっています。Object.prototypeに相当するオブジェクトは仕様書内でよく使われるので、%ObjectPrototype%として簡単に参照できるようになっています。

%ObjectPrototype%がどんなオブジェクトかということは、19.1.3 Properties of the Object Prototype Objectで定義されています。

The Object prototype object:

  1. is the intrinsic object %ObjectPrototype%.
  2. is an immutable prototype exotic object.
  3. has a [[Prototype]] internal slot whose value is null.

(訳) Object prototypeオブジェクトは、
1. 内部オブジェクト%ObjectPrototype%です。
2. immutable prototypeエキゾチックオブジェクトです。
3. [[Prototype]]内部スロットを持ち、その値はnullです。

この節では、「Object prptotypeオブジェクトというオブジェクトが存在する」ということを定義しています。つまり、ある種のオブジェクト作成の定義になっています。実際にこのオブジェクトを作るのは、もちろんJavaScript処理系が実行環境の準備中に勝手にやってくれます。オブジェクトを作成する以上、必要な内部スロットはちゃんと明示的に作ってあげなければいけません。そのため、[[Prototype]]内部スロットの存在及びその中身がここに明記されています。

Object prototypeオブジェクトはimmutable prototypeエキゾチックオブジェクトであるとされていますが、これの定義は9.4.7 Immutable Prototype Exotic Objectsにあります。

An immutable prototype exotic object is an exotic object that has a [[Prototype]] internal slot that will not change once it is initialized.

Immutable prototype exotic objects have the same internal slots as ordinary objects. They are exotic only in the following internal methods. All other internal methods of immutable prototype exotic objects that are not explicitly defined below are instead defined as in ordinary objects.

(訳)immutable prototypeエキゾチックオブジェクトは、作られた後に[[Prototype]]内部スロットが変更されないようなエキゾチックオブジェクトです。

Immutable prototypeエキゾチックオブジェクトは普通のオブジェクトと同じ内部スロットを持ちます。Immutable prototypeエキゾチックオブジェクトは次の内部メソッド(訳注:[[SetPrototypeOf]])のみが普通のオブジェクトと異なります。以下で明示的に定義されていない他の内部メソッドは、普通のオブジェクトと同様に定義されます。

目ざとい方は先ほどの%ObjectPrototype%の定義について「必須インターナルスロットの定義がないじゃないか」とお思いになったかもしれませんが、上記に「以下で明示的に定義されていない他の内部メソッドは、普通のオブジェクトと同様に定義されます」という文があるためクリアしていると考えられます。

エキゾチックオブジェクトというのは、内部メソッドの動作が普通のオブジェクトと事なるオブジェクトのことであり、いくつか種類があります。そのうちのひとつであるimmutable prototypeエキゾチックオブジェクトは、[[SetPrototypeOf]]という内部メソッドの動作が普通のオブジェクトと違います。

[[SetPrototypeOf]]はObject.setPrototypeOf経由で呼び出される内部メソッドであり、要するにオブジェクトのプロトタイプをあとから変更するためのものです。immutable prototypeエキゾチックオブジェクトはこれを許可せず、[[SetPrototypeOf]]の挙動を何もしないように変更することで、プロトタイプの書き換えを防いでいます。

Object prototypeオブジェクトがimmutable prototypeエキゾチックオブジェクトとして定義されていることにより、Object.prototyppeのプロトタイプはnullに固定されています。実際、以下のようにこれを書き換えようとするとエラーが発生します。

const p = Object.create(null);
Object.setPrototypeOf(Object.prototype, p);
// エラーメッセージ(Google Chromeの場合):
// Uncaught TypeError: Immutable prototype object '#<Object>' cannot have their prototype set

話がそれましたが、このように仕様書内で存在が定義されているオブジェクトというのは数多くあります。一部はintrinsic objectとして名前が付けられており、6.1.7.4 Well-Known Intrinsic Objectsにそれが列挙してあります。現在のところこれは111種類あります。

組み込みオブジェクトの継承関係:配列の場合

最後に、組み込みオブジェクトの継承関係を見ておきましょう。例えば、ArrayはObjectを継承しているため、Objectのメソッドは配列に対して使用することができます。

const arr = [1, 10, 100];

console.log(arr.hasOwnProperty("1")); // true

これは仕様書でどのように定義されているでしょうか。とはいっても、話は難しくありません。すでにご存知の通り、「ArrayがObjectを継承している」というのは「Array.prototypeがObjectのインスタンスである」ということ、言い換えれば「Array.prototypeのプロトタイプがObject.prototypeである」ということです。これを確かめればいいわけです。

ここまで読んだ皆さんなら、仕様書の「Array.prototypeというオブジェクト」を定義している部分を読めばいいというのはすぐに分かるでしょう。早速目次から探しましょう。

すると、22.1.3 Properties of the Array Prototype Objectが見つかります。Properties ofとあるタイトルは若干ミスリーディングですが、Array Prototype Objectそのものもここで定義されています。

The Array prototype object:

  • is the intrinsic object %ArrayPrototype%.
  • is an Array exotic object and has the internal methods specified for such objects.
  • has a "length" property whose initial value is 0 and whose attributes are { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }.
  • has a [[Prototype]] internal slot whose value is the intrinsic object %ObjectPrototype%.

NOTE
The Array prototype object is specified to be an Array exotic object to ensure compatibility with ECMAScript code that was created prior to the ECMAScript 2015 specification

(訳)Array prototypeオブジェクトは、

  • 内部オブジェクト%ArrayPrototype%です。
  • Arrayエキゾチックオブジェクトであり、対応する内部メソッドを持ちます。
  • "length" プロパティを持ちます。これは0で初期化されており、[[Writable]]属性がtrue、[[Enumerable]]属性がfalse、[[Configurable]]奥世がfalseです。
  • [[Prototype]]内部スロットを持ち、その値は%ObjectPrototype%です。

NOTE
Array prototypeオブジェクトはArrayエキゾチックオブジェクトであると定められていますが、これはECMAScript 2015 以前に作られたコードとの互換性を保証するためです。

後方互換性の関係でいろいろと書いてありますが、4つ目の項目にArray.prototypeの[[Prototype]]スロットの中身は%ObjectPrototype%であると明記されています。

まとめ

この記事では、JavaScriptにおける継承の機構を仕様書のレベルで解説しました。[[Prototype]]内部スロットの存在や実際のプロパティアクセスの挙動を理解を通じて、JavaScriptにおけるプロトタイプベースのオブジェクト指向がどのように実現されているかを仕様書に見出しました。

仕様書の読み方については比較的丁寧に解説しましたが、アルゴリズム部分も自然言語で書かれているため読むのに必要な知識は多くありません。恐らく最も大変なのは、目的の記述を見つけることでしょう。この記事では基本的に目次から探すものとして解説しましたが、素早く目的の記述を見つけるためには仕様書のどこに何が書かれているのか把握することが重要です。仕様書の目次にひと通り目を通しておくのは効果的でしょう。

冒頭でも述べた通り、仕様書を読んで理解することがJavaScriptプログラムの意味を正確に理解するための唯一の方法です。常日頃からclassの構文にお世話になっている方も多いと思いますが、そのインスタンスのメソッドを呼び出すということがどういうことなのか、この記事を全部読んだ方は半分くらいは理解できたかと思います(もう半分はそのときのthisの扱いです。今回はプロトタイプの扱いに絞ったのでthisの扱いがどうなるのかは省略しました)。

仕様書を読むというのはハードルが高く感じられるかもしれませんが、ちゃんと仕様書が存在するという点でJavaScriptは幸せな部類です。仕様書さえ読めばプログラムの意味に確信が持てるというのは実はたいへんありがたいことなのです。仕様書が無い言語では、コンパイラの動作がどうなっているかにまで立ち入らなければプログラムの意味の真なる理解が達成できないかもしれないのですから。その点で、仕様書というのはプログラムの意味に関する優れた抽象化レイヤーとして働いているのです。

残念なことに、世の中の誰も彼もが自分の書いたプログラムの意味を理解しているわけではありません。それどころか、この記事の長さからも分かるように、プログラムの意味を把握するというのは決してハードルが低い行為ではありません。実際のところ、意味が分からずに書いたプログラムであってもプログラムは動いてしまいます。それが良いことなのか悪いことなのかという答えを筆者は持っていませんから、この機会に考えてみてはいかがでしょうか。


  1. 処理系にバグがある場合は別ですが。また、重箱の隅に目を向けると処理系の間で意見が割れているようなところもあります。 

  2. それでも説明しきれないオブジェクトもいくらかあり、それらはエキゾチックオブジェクトと呼ばれています。 

  3. この名前は、昔は非標準の機能だったことの名残です。現在は一応標準に含まれています(Annex B)。 

  4. すぐ後に解説しますが、例外があります。 

  5. ?や!という記法にはCompletion Recordから値を取り出すという意味もあるのですが、今回はそこには触れません。 

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

初心者向けVue.js × Railsでのアプリ作成(ToDoリスト編)

Vue.jsとRailsでアプリを作る記事が少ない!!!(泣)

私はRubyエンジニアで普段Railsを使って仕事をしているのですが、業務でVue.jsをメンテする必要があるため、空いた時間に勉強をしています。
ですが、Vue.jsとRailsでアプリを作る記事が少ない印象をうけ、勉強するのに少し不便でした。

Vue.js × Railsの記事が少ないと言っても探せばそれなりに見つかるのですが、私みたいなフロントエンドの事よくわかってない人間には、理解するのに時間がかかったりします。

ネットで記事をあさったり、そもそもJavaScriptが良くわかってないので、JavaScriptから勉強し直してみたり、Vue.js × Railsでアプリを作るだけにしては非常に遠回りしてしまいました。

この記事について

この記事は、私みたいにVue.js × Railsのアプリ作成で遠回りな勉強をしている人をなくす事を目的としています。

初心者向けにVue.js × Railsでアプリを作る記事を書いて、実装のイメージを掴んでもらえれば、私のような遠回りはなくなるはず。。。1度小さいアプリを作ってしまえば、理解度がグッと上がり、他の記事も読みやすくなるのできっと大丈夫!

また、この記事は私がRubyエンジニアなので、Rubyエンジニアから見てVue.jsをどう実装しようかという視点になってます。

この記事を読む対象のレベル

Vue.js × Rails両方ともチュートリアルをやったくらいのレベルを対象としています。
Vue.jsもRailsもだいたいこんな感じというのがわかっていれば作れると思います。

どんなものを作るか? Vue.js × Railsそれぞれの役割とは?

この記事では、Vue.js × RailsでミニマムなToDoリストアプリを作っていきます。定番ですね。

私はToDoアプリを作ろうとした時、Vue.js × Railsがそれぞれどんな役割をしているのかよくわからず、実装のイメージが掴めなかったのですが、いろんな記事を読んだ結果、RailsでAPIを作り、APIへのリクエストと返ってきたデータの表示をVue.jsで行うというのが多かったです。今回もこの役割分担でアプリを作っていきます。

スクリーンショット 2020-01-01 3.16.36.png

WEB業界での経験が浅い、もしくはこれからWEB業界を目指す方はAPIのイメージがつかみにくいかもしれませんが、一言で言うとURLのリクエストを受けたら、URLに応じたデータを返すものです。

この役割のイメージが分かればRailsの部分をFirebaseに置き換えようとか、Vue.jsをReactに置き換えようとか応用が効くような気がします。

(注)私は現時点でFirebaseもReactも詳しくないので応用が効く気がするというだけ。。。詳しい方がいたら教えて下さると助かります。

完成後のイメージ

スクリーンショット 2020-01-13 14.31.32.png

  • テキストボックスにタスクを入力して追加ボタンを押すとリストに追加されて表示される。
  • チェックボックスをチェックすると取り消し線が引かれる
  • 削除ボタンを押すと削除

実際に作ってみよう

RailsでAPIを作る

では、実際にアプリを作ってきます。まずはRailsでAPIを作るところから。
以下のコマンドで、Railsアプリを作ります。--webpack=vueをするとwebpackとVue.jsのインストールが出来ます。

rails new todo_list --webpack=vue

「webpackとかまた新しい言葉出すなよ!」って方は以下のリンクを見てください!
【5分でなんとなく理解!】Webpack入門
Webpackとは、js、cssなどフロントで作るファイルをバンドリングしてくれるものです。

ToDoリストを表示するHome画面を作成

ToDoを表示するHome画面を作るため、コントローラーを作成します。

rails g controller home

作成したコントローラーにindexだけ追加しておきましょう。

/app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
end

viewからVue.jsが呼び出せるか試しますために追加します。

/app/views/home/index.erb
<%= javascript_pack_tag 'hello_vue' %>

routes.rbに以下を追加。

/config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end

rails sしてlocalhost:3000にアクセスします。
以下のような画面が表示されてればOKです。

スクリーンショット 2020-01-02 23.00.33.png

ちなみにVue.jsを変更したら

bin/webpackで更新してあげる必要があります。(重要)

APIの処理を作る

まずは、ToDoリストにタスクを追加するためにモデルを作っていきます。

rails generate model Task name:string is_done:boolean

ルーティングに以下を追加します。
表示用のhomeとデータを返すAPI用のapi::tasksを追加。

/config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'

  namespace :api, format: 'json' do
    resources :tasks, only: [:index, :create, :destroy, :update]
  end
end

リクエストを受けたらデータを返すため、Tasksのコントローラーを作ります。

rails g controller Api::Tasks

マイグレーションします。

rails db:migrate

コントローラーの中身は以下のような感じ。

/app/controllers/api/tasks_controller.rb
module Api
  class TasksController < ApplicationController
    skip_before_action :verify_authenticity_token

    def index
      @tasks = Task.order('created_at DESC')
    end

    def create
      @task = Task.new(task_params)

      if @task.save
        render json: @task, status: :created
      else
        render json: @task.errors, status: :unprocessable_entity
      end
    end

    def destroy
      Task.find(params[:id]).destroy!
    end

    def update
      Task.find(params[:id]).toggle!(:is_done)
    end

    private def task_params
      params.require(:task).permit(:name, :is_done)
    end
  end
end

APIを返す時は、htmlではなくJSONで返してあげたいので以下を追加します。
自分は実際にWEB業界に入るまでJSONに馴染みがなかったのですが、以下の形で書きます。

/app/views/api/tasks/index.json.jbuilder
json.set! :tasks do
  json.array! @tasks do |task|
    json.extract! task, :id, :name, :is_done, :created_at, :updated_at
  end
end

APIの動作確認

DBにデータを入れて確認してみましょう。
コンソールを立ち上げます。

rails c

Taskモデルにデータを追加してみましょう。

Task.create(name: 'テスト用タスク')

もう一度サーバー立ち上げ

rails s

以下のアドレスで追加したデータがJSONでデータが返ればOK。

http://localhost:3000/api/tasks.json

スクリーンショット 2020-01-02 23.40.55.png

Vue.jsでフロント作成

コンポーネント

コンポーネントとは、名前付きの再利用可能な Vue インスタンスです。
再利用出来そうなパーツごとにコンポーネントを区切って実装するのが、どうやら重要らしい。
今回、最小限の構成でアプリを構成するためコンポーネントについては省こうか迷ったのですが、重要なので組み込みます。

わかりやすいイメージで言うと、ヘッダー、フッター、サイドナビ等は色んなページで再利用するのでコンポーネントを分けて実装するといった感じでしょうか。
今回もヘッダーとToDoリストを表示するボディ部分でコンポーネントを分けて実装したいと思います。

では、まず以下のようにToDoリスト表示部分を作って下さい。

/app/views/home/index.erb
<div id="app">
  <navbar></navbar>
</div>

<%= javascript_pack_tag 'todo' %> # todoに変更する事に注意

はヘッダーのコンポーネントを表示します。
<%= javascript_pack_tag 'todo' %>でapp/javascript/packs配下のtodo.jsファイルを読み込みます。

ヘッダーの作成

まずはヘッダー部分のコンポーネントを用意します。

/app/javascript/packs/components/header.vue
<template>
  <h1>ToDoリスト</h1>
</template>

次に/app/views/home/index.erbから呼び出されているapp/javascript/packs/todo.jsにコンポーネントの設定をしていきます。
以下のように書くとapp/views/home/index.erb<navbar></navbar>/app/javascript/packs/components/header.vueをマウントして表示してくれるようです。

/app/javascript/packs/todo.js
import Vue from 'vue/dist/vue.esm.js'
import Header from './components/header.vue'

var app = new Vue({
  el: '#app',
  components: {
    'navbar': Header
  }
});

Vue.jsの読み込み設定

webpackはVue.jsの読み込み方がわからないので以下を実行します(重要)

/config/loaders/stylus.js
module.exports = {
  test: /\.styl$/,
  use: [
    'style-loader', 'css-loader', 'stylus-loader'
  ]
}
/config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const { VueLoaderPlugin } = require('vue-loader')
const vue = require('./loaders/vue')
const stylus = require('../loaders/stylus') // 作ったstylusをrequireする

environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin())
environment.loaders.prepend('vue', vue)
module.exports = environment
environment.loaders.prepend('stylus', stylus) // 作ったstylusをロード

webpackを再読み込みしてからサーバーを再起動しましょう(重要)

bin/webpack
rails s

rails sしてヘッダーが表示されて入ればOKです

スクリーンショット 2020-01-13 14.10.00.png

ToDoリストを表示するボディ部分

axiosというライブラリを使って、フロントエンドからHTTPリクエストをします。
以下のコマンドでyarnでaxiosを追加して下さい。

yarn add axios

ToDoアプリのメイン部分の実装です。解説は後ほど詳しく説明します。

/app/javascript/packs/components/index.vue
<template>
  <div>
    <div>
      <input v-model="newTask" placeholder="to doを追加して下さい">
      <div v-on:click="createTask">
        <i>追加</i>
      </div>
    </div>
    <ul>
      <li v-for="(task, index) in tasks">
        <input type="checkbox" v-model="task.is_done" v-on:click="update(task.id, index)">
        <span v-bind:class="{done: task.is_done}">{{ task.name }}</span>
        <button v-on:click="deleteTask(task.id, index)">削除</button>
      </li>
    </ul>
  </div>
</template>

<script>
  import axios from 'axios';

  export default {
    data: function () {
      return {
        tasks: [],
        newTask: ''
      }
    },
    mounted: function () {
      this.fetchTasks();
    },
    methods: {
      fetchTasks: function () {
        axios.get('/api/tasks').then((response) => {
          for(let i = 0; i < response.data.tasks.length; i++) {
            this.tasks.push(response.data.tasks[i]);
          }
        }, (error) => {
          console.log(error, response);
        });
      },
      createTask: function () {
        if(this.newTask == '') return;

        axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => {
          this.tasks.unshift(response.data);
          this.newTask = '';
        }, (error) => {
          console.log(error, response);
        });
      },
      deleteTask: function (task_id, index) {
        axios.delete('/api/tasks/' + task_id).then((response) => {
          this.tasks.splice(index, 1);
        }, (error) => {
          console.log(error, response);
        });
      },
      update: function (task_id) {
        axios.put('/api/tasks/' + task_id).then((response) => {
        }, (error) => {
          console.log(error);
        });
      }
    }
  }
</script>

作ったindex.vueを読み込んであげましょう。

/app/javascript/packs/todo.js
import Vue from 'vue/dist/vue.esm.js'
import Header from './components/header.vue'
import Index from './components/index.vue' // 追加

var app = new Vue({
  el: '#app',
  components: {
    'navbar': Header,
    'contents' : Index // 追加
  }
});

ヘッダーのしたにindex.vueを表示するため、<navbar></navbar>の下に<contents></contents>を追加します。

/app/views/home/index.erb
<div id="app">
  <navbar></navbar>
  <contents></contents>
</div>

<%= javascript_pack_tag 'todo' %>

チェックボックスが押されたら取り消し線を表示するためcss追加。

app/assets/stylesheets/home.scss
#app li > span.done {
  text-decoration: line-through;
}

rails sして動くか確認してみて下さい。
実際にToDoリストを追加してみましょう。

スクリーンショット 2020-01-13 16.31.25.png

ToDoアプリのコード解説メモ

学習し始めだと、Vue.jsのどの行が何をやっているのかわからない事があったのでメモ付きのコードをのせます。

まずはtemplate

<template>
  <div>
    <div>
      <input v-model="newTask" placeholder="to doを追加して下さい">
      # 追加ボタンを押すとcreateTaskを実行する
      <div v-on:click="createTask">
        <i>追加</i>
      </div>
    </div>
    <ul>
      # fetchしたタスク一覧(tasks)から一つずつtaskとindexを取り出す処理
      <li v-for="(task, index) in tasks">
        # チェックボックスが押されたらv-modelのis_doneを変更して取り消し線を引く
        # updateでAPI側のデータも更新
        <input type="checkbox" v-model="task.is_done" v-on:click="update(task.id, index)">
        # タスクの表示。v-bind:classでis_doneを参照して取り消し線が引かれるかどうか判定
        <span v-bind:class="{done: task.is_done}">{{ task.name }}</span>
        # 削除ボタンを押すとdeleteTaskを実行
        <button v-on:click="deleteTask(task.id, index)">削除</button>
      </li>
    </ul>
  </div>
</template>
<script>
  import axios from 'axios';

  export default {
    data: function () {
      return {
        tasks: [],
        newTask: ''
      }
    },
    mounted: function () {
      this.fetchTasks();
    },
    methods: {
      // APIからタスク一覧を取得
      fetchTasks: function () {
        axios.get('/api/tasks').then((response) => {
          for(let i = 0; i < response.data.tasks.length; i++) {
            this.tasks.push(response.data.tasks[i]);
          }
        }, (error) => {
          console.log(error, response);
        });
      },
      // 新しいタスク作成
      createTask: function () {
        // テキストボックスが空の場合はreturnして終了
        if(this.newTask == '') return;

        // apiへ追加リクエスト
        axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => {
          // unshiftで現在のtasksの先頭にタスクを追加
          this.tasks.unshift(response.data);
          // 追加したらテキストボックスを空にする
          this.newTask = '';
        }, (error) => {
          console.log(error, response);
        });
      },
      // タスク削除
      deleteTask: function (task_id, index) {
        // apiへ削除リクエスト
        axios.delete('/api/tasks/' + task_id).then((response) => {
          this.tasks.splice(index, 1);
        }, (error) => {
          console.log(error, response);
        });
      },
      // タスク更新。今回はis_doneのみ更新だが、タスク名とか色々更新するようカスタムしても良いと思う
      update: function (task_id) {
        // apiへ更新リクエスト
        axios.put('/api/tasks/' + task_id).then((response) => {
        }, (error) => {
          console.log(error);
        });
      }
    }
  }
</script>

まとめ

小さいアプリをとりあえず作ってみると理解度がかなり深まると思うので、今回のようなToDoアプリを作ってみると良いと思います。
かけ足で記事を書いてしまったのですが、これで私みたいな人間を救えるのか...???
今後も私のようにVue.js × Railsでアプリを作りたい人向けに記事を改訂して行きたいです。

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

JavaScript勉強の記録その17: joinとsplitを使った配列の操作

joinメソッド

joinというメソッドを利用することで、配列の要素を繋げて、1つの文字列として値を返すことができます。

以下の例ではjoinメソッドを利用して日付のようなデータを作成しています。
引数に渡した値で、異なる文字列が返ってきているのがわかるかと思います。

index.js
const d = [2020, 1, 13];
console.log(d.join('/'));
//=> 2020/1/13
console.log(d.join(''));
//=> 2020113
console.log(d.join());
//=> 2020,1,13

splitメソッド

splitというメソッドを利用することで、文字列を分割し、配列を作成することができます。
joinとは反対に、文字列から配列を作るメソッドです。

以下の例ではsplitメソッドを利用して文字列を分割し、配列を作っています。
引数に渡した値で文字列を区切り、新しく配列を返しています。

index.js
const t = '20:05:30';
console.log(t.split(':'));
//=>["20", "05", "30"]

splitメソッドを利用して、ことなる変数または定数に分割代入することもできます。

index.js
const t = '20:05:30';
const [hour, minute, second] = t.split(':'); 
console.log(hour)
//=> "20"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【three.js】Lineオブジェクトを活用して線がクールなWebページをつくる

See the Pen Draw Lines by three.js by MasatomoFukuda (@chitomo12) on CodePen.

Three.jsには様々な立体物を作成するジオメトリがありますが、単なる線を引くLineオブジェクトのような奥行きのない要素はめったに使われていない印象があります。

本記事ではそんなLineオブジェクトを使った線の引き方と、Lineオブジェクトを活用したWebページ例を紹介します。

1. Lineオブジェクトの使い方

Line( geometry: Geometry, material: Material );

Lineオブジェクトに表面素材を指定するMaterialと、三次元ベクトルの頂点座標を与えたGeometryを渡すことで空間内に線を引くことができます。

lines.html
        (...省略...)
        // マテリアルを設定
        const line_material=new THREE.LineBasicMaterial({ color:0xffffff });
        // ジオメトリを作成
        var line_geometry=new THREE.Geometry();
        // ジオメトリに頂点座標を追加
        line_geometry.vertices.push(
            new THREE.Vector3(-100,0,0),
            new THREE.Vector3(0,100,0),
            new THREE.Vector3(100,0,0),
        );
        var newline=new THREE.Line(line_geometry, line_material);
        scene.add(newline);

        tick();
        function tick() {       
          // カメラを回転させる
          rot+=0.5;
          const radian=Math.PI/180*rot;
          camera.position.x=500*Math.sin(radian);
          camera.position.z=500*Math.cos(radian);
          camera.lookAt(0,0,0);

          // レンダリング
          renderer.render(scene, camera);
          requestAnimationFrame(tick);
        }
        (...省略...)

実行例


See the Pen
Draw Lines 01 by three.js
by MasatomoFukuda (@chitomo12)
on CodePen.


より細かい情報は公式リファレンスをご参照に。
https://threejs.org/docs/#api/en/objects/Line

2. Lineを使ったWebページをつくる

では早速、Lineを活用した簡単なWebページを作っていきます。

まず、ただの線で印象深いグラフィックを作るにはどうすれば良いかを考えます。
兎にも角にもアイデア出しです。

ざっと思いついたのが次の3つの案です。

  1. 一本の線を動かしてみる
  2. 複数の線を配置して幾何形体を作る
  3. ランダムに線を配置して複雑な形態を作る

今回は「3. ランダムに線を配置して複雑な形態を作る」のアイデアを採用しました。

まずは、先程作成した線のパラメータを乱数に置き換え、複数描画してみます。
線の描画部分を以下の通りに書き換えてください。

lines.html
        // 線のグループを作る
        var lines=new THREE.Group();
        const line_material=new THREE.LineBasicMaterial({ color:0xffffff });
        // for文でランダムな線(newline)を複数作る
        for(var i=0;i<10;i++){
          var line_geometry=new THREE.Geometry();
          line_geometry.vertices.push(
              new THREE.Vector3(-Math.random()*100, 0, 0),
              new THREE.Vector3(0, Math.random()*100, 0),
              new THREE.Vector3(Math.random()*100, 0, 0),
          );
          var newline=new THREE.Line(line_geometry,line_material);
          // newlineをグループに追加
          lines.add(newline);
        }
        // linesグループをシーンに追加
        scene.add(lines);

大きな変更点として、複数の線をまとめるためのグループを作成し、線の頂点を乱数に変更しました。
実行すると下図のようになります。
image.png
すでに良い感じに見えますが、少し物足りなさは感じるのでどうにかしたい。

ここはシンプルに情報量を増やしたいと思います。
具体的には頂点の数を増やし、さらにz座標のパラメータもいじることで奥行きもつくっていきます。

さきほどのfor文内に、頂点を追加するための行を追加します。

lines.html
        for(var i=0;i<10;i++){
          var line_geometry=new THREE.Geometry();
          line_geometry.vertices.push(
              new THREE.Vector3(-Math.random()*300,0,-Math.random()*300),
              new THREE.Vector3(0,Math.random()*300,-Math.random()*300),
              new THREE.Vector3(Math.random()*300,0,-Math.random()*300),
              new THREE.Vector3(Math.random()*300,0,-Math.random()*300),
              new THREE.Vector3(Math.random()*300,0,-Math.random()*300)
          );
          var newline=new THREE.Line(line_geometry,line_material);
          lines.add(newline);
        }

実行すると次のようになります
979635255e2a1d2b734f2dd95682e806.gif
最初はただの線でしたが、ここまで来たら複雑な立体物を使いこなしている感が出てきたかと思います!

さて、今回作ろうとしているのはWebページのサンプルです。

今のままだと3D空間内を大きく移動するような迫力ある視覚経験はできると思いますが、ゲームではなくWebページなのでこれをもう少し落ち着いたUXにしたい。

カメラが大きく空間を動くのではなく、カメラは動かないままオブジェクトだけが回る、そういう空間を作ろうと思います。

lineオブジェクトの集合の中心が座標 (0,0,0) に来るようにし、ページ全体に線が均等に現れるように調整を加えます。
for文を次のように書き換えてください。

lines.html
        for(let i=0;i<20;i++){
          var line_geometry=new THREE.Geometry();
          line_geometry.vertices.push(
            new THREE.Vector3(0,0,0),
            new THREE.Vector3(Math.random()*400-200,Math.random()*400-200,Math.random()*400-200),
            new THREE.Vector3(Math.random()*400-200,Math.random()*400-200,Math.random()*400-200),
            new THREE.Vector3(Math.random()*400-200,Math.random()*400-200,Math.random()*400-200),
            new THREE.Vector3(Math.random()*800-400,Math.random()*400-200,Math.random()*800-400),
            new THREE.Vector3(Math.random()*800-400,Math.random()*400-200,Math.random()*800-400),
            new THREE.Vector3(Math.random()*400-200,Math.random()*4000-200,Math.random()*400-200)
          );
          var newline=new THREE.Line(line_geometry,line_material);
          lines.add(newline);
        }

そしてこれを実行すると下図のようになります。
ezgif.com-resize (1).gif
※画質が荒くてすみません? 実際の見た目はもっと鮮明です。

これでもう、一つの映像として十分強度が出てきたのではないでしょうか?

仕上げとして、WebページのCSSやナビゲーション要素などを配置し、マウス位置に応じたインタラクティビティも加えます。
追加要素を列挙すると以下の通り。

・背景を水色に変更。
・回転するオブジェクトが画面右(カメラに対して少し右)に来るよう、時間経過でx-z座標を移動するように設定。
・DIVタグでテキストを作成し、CSSでpositionプロパティにabsoluteを入れて良い感じの位置に配置。
・マウス位置に合わせて回転の速度を増減する。

こうして出来上がったものが、冒頭でも紹介した以下のページになります。

See the Pen Draw Lines by three.js by MasatomoFukuda (@chitomo12) on CodePen.

締め

lineオブジェクトを使った事例が意外と少なかったので色々調べてみたのがこの記事を書いたきっかけだったのですが、マテリアルやシェーダーなど複雑な要素を用いることなく、ただの線を引くだけでそれなりに見映えするWebページを1つ作ることができたのは個人的にも意外な発見でした。

この記事がthree.jsをもっと楽しむきっかけになれば幸いです。

参考

公式リファレンス:https://threejs.org/docs/index.html#manual/en/introduction/Drawing-lines
Three.js入門サイト - ICS MEDIA:https://ics.media/tutorial-three/

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

Unity+Googleスプレッドシート+GASでサーバーレスのデータベースシステムを実現する?

前書き

unity1weekをきっかけに、Unity+Googleスプレッドシート+GASで簡易ランキング機能を作ってみました。この仕組みで、ランキングだけではなく、任意のデータをアップロード・ダウンロードできれば、一応汎用的なデータベースとして使えるのではないかと思いました。例えばゲームのマスタデータをGoogleスプレッドシートに保存することで、アプリのバージョン更新なしでゲームの調整ができてまうとか、ゲームの最新バージョンをGoogleスプレッドシートに書き込んで、アプリ起動時にそれを取得して古ければ強制アップデートポップアップを出すとか、さらにチャットやユーザー情報の管理もGoogleスプレッドシートでやりとりするなど、ユースケースがどんどん湧いてきます。
ということで、実装してみました。

使い方

プロジェクトはUmbrella(トトロなので)という名でGithubに公開しました。最新パッケージのダウンロードはこちら。ちなみに、Umbrellaには単純なデータ通信管理システムDatabase以外に、簡易ランキングシステムRankingも含まれています。興味ある方はぜひ合わせていじってみてください。

GAS側

  1. 新しいGoogleスプレッドシートを作成する。
  2. メニューのツールからスクリプト エディタをクリックする。
  3. Assets/Umbrella/Database/Database.gsの内容をコード.gsにコピーする。
  4. メニューのファイル > 保存でプロジェクトに名前をつけて保存する(保存にちょっと2、3秒ぐらい掛かるかも)。
  5. メニューの公開 > ウェブアプリケーションとして導入...をクリックする。
  6. ウェブアプリケーションとして導入のポップアップにて、次のユーザーとしてアプリケーションを実行に自分のアカウントを、アプリケーションにアクセスできるユーザー全員(匿名ユーザーを含む)を設定する。
  7. 導入をクリックして、現在のウェブアプリケーションのURLの下に書いてあるURLをコピーする。
  8. 「認証が必要です」のポップアップが出たら@zk_phiさんの記事を参考にして認証を行ってください。

Unity側

  1. Assets/Umbrella/Database/DatabaseManager.prefabをデータをやりとりしたいシーンのヒエラルキーにドラッグ&ドロップする。
  2. プレハブインスタンスのインスペクターから、App URLのフィールドに先ほどコピーしたGASのウェブアプリケーションURLをペーストする。
  3. Default Sheetフィールドに使いたいGoogleスプレッドシートのデフォルトシート名を入力する。
  4. スクリプト内で、データを送信したい場合はDatabaseManager.Instance.SendDataAsync(data, handleResponseCallback, sheetName)を呼び、データを取得したい場合はDatabaseManager.Instance.GetDataAsync(key, handleResponseCallback, sheetName)を呼ぶ。また、メソッドの前にyield returnを付ければデータ取得後の処理(handleResponseCallback)の実行完了まで待つことができる。
  5. 具体的な使い方はサンプルシーンとスクリプトを参照してください。

デモ

  • Googleスプレッドシートにデータを送信します。デモではデータ名と値のペアで複数データを送信しています。
    send_data.gif

  • Googleスプレッドシートにあるデータを更新します。デモではデータ名を指定して新しい値を送信しています。
    update_data.gif

  • 他のクライアントとしてデータを送信します。Umbrellaではクライアントを識別するユニークIDをUnity側で生成してPlayerPrefsに保存しているため、PlayerPrefsをクリアしないまま送信すると既存のデータを上書きすることになります。
    send_another_data.gif

  • Googleスプレッドシートにあるデータを取得します。デモではデータ名のリストで複数のデータを取得しています。
    get_data.gif

  • セル参照で範囲内のデータを一気に取得する方法もあります。
    get_data_by_cell.gif

実装の抜粋

GAS側の処理

GASはUnityから送ってきたデータに基づき、スプレッドシートの中身を更新します。

function doPost(e) {
  var request = e.parameter;
  var method = request[CONST.Method];

  if(method == CONST.SaveData){
    return saveData(request);
  }else if(method == CONST.GetData) {
    return getData(request);
  }

  return ContentService.createTextOutput("Error: Invalid method");
}

saveDatagetDataの実装詳細はここでは省略しますが、GASのAPIコールはコスト高いため、処理速度を高めにはできる限りAPIコールの回数を減らす必要があります。例えばセルデータの取得で、各セルでsheet.getRange().getValue()の代わりに、予めvar data = sheet.getDataRange().getValues()で指定範囲のセルデータを一括で配列に格納し、後で配列から値を取るなどの策が考えられます。また、データの書き込みがある場合、排他処理(ロック)を入れる必要がありますが、一行のデータをまとめて一括でappendRow()を使えばセル単位でロックをかける手間をなくすテクニックもあります。appendRow()は不可分操作(Atomic Operation)なので排他処理が要らないからです。

UnityからGASへの送信

簡易のデータベース機能なので、特に通信の仕様とかは決めなく(暗号化??)、完全にJSON形式で送信しています。JSON解析は軽量のMiniJsonを導入しています。

public CustomYieldInstruction SendDataAsync(MonoBehaviour context, string methodName, string sheetName, Dictionary<string, object> data, Action<object> handleResponse = null)
{
    var strData = Json.Serialize(data);

    var formData = new List<IMultipartFormSection>();
    formData.Add(new MultipartFormDataSection("method", methodName));
    formData.Add(new MultipartFormDataSection("sheet", sheetName));
    formData.Add(new MultipartFormDataSection("data", strData));

    bool complete = false;
    context.StartCoroutine(CT_SendData(formData, status => complete = status, handleResponse));

    return new WaitUntil(() => complete);
}

WWWFormがLegacyになったので、IMultipartFormSectionでフォームデータを作成しています。フォームデータにGAS側で呼び出したいメソッド名、使いたいシート名とデータ内容を入れています。また、外部でyield returnをつけて待たせられるように、返り値のタイプをCustomYieldInstructionにしてcompleteがtrueになるまで処理を止めることを可能にしています。

UnityWebRequestPostメソッドでフォームデータを送信しています。

private IEnumerator CT_SendData(List<IMultipartFormSection> formData, Action<bool> updateStatus, Action<object> handleResponse = null)
{
    updateStatus(false);

    var www = UnityWebRequest.Post(_appURL, formData);

    Debug.Log("<color=blue>[GSSDataService]</color> Start sending data to Google Sheets.");

    yield return www.SendWebRequest();

    if (www.isNetworkError || www.isHttpError)
    {
        Debug.LogError($"<color=blue>[GSSDataService]</color> Sending data to Google Sheets failed. Error: {www.error}");
    }
    else
    {
        Debug.Log("<color=blue>[GSSDataService]</color> Sending data to Google Sheets completed");
        try
        {
            var response = Json.Deserialize(www.downloadHandler.text);
            string message = response as string;
            if (message != null && message.Contains("Error")) Debug.LogError($"<color=blue>[GSSDataService]</color> Getting data from Google Sheets failed. {message}");
            else handleResponse?.Invoke(response);
        }
        catch (InvalidCastException e)
        {
            Debug.LogError($"<color=blue>[GSSDataService]</color> Parsing result from Google Sheets failed. Error: {e.Message}");
        }

    }

    updateStatus(true);
}

返ってきた結果にError文字列(GAS側で入れている)が含まれたらエラーログを書き出し、なければ結果を処理するhandleResponseメソッドを呼び出します。

後書き

Unity+Googleスプレッドシート+GASでサーバーレスの簡易データベース機能を作ってみました。当然いくつか問題もあります。

  • 完全JSON形式でデータのやりとりをしているため、複雑のデータ構造に対応できない(自分でシリアライザとデシリアライザを書くなどの工夫が要る)。
  • 安全性一切考えていない(Google神がいい感じにしてくれるはず)。
  • 負荷検証や処理速度を計測・比較していないので不明(使った肌感だと耐えられレベルの遅延)。
  • そもそもGoogleのサービスに制限がある。URL Fetch callsだと、無料のGmailアカウントで1日2万回までしか呼べないので、大規模や非常に頻繁な通信に向いていない。

しかしながら個人プロジェクトレベルのものとしては十分機能できるのではないかと思います。何より、Googleスプレッドシートならではの機能が使えて、直接データを一目瞭然で見たり、気軽にデータを修正したりすることができる点から、普通のSQLデータベースよりも便利かもしれません?(?)

おまけ

Umbrellaにランキング機能もついているので、それの使い方も紹介します。
1. Assets/Umbrella/Ranking/RankingManager.prefabをランキングを表示したいシーンのヒエラルキーに置いておく。
2. Assets/Umbrella/Ranking/RankingSettings.assetのインスペクターから、App URLフィールドにGASのウェブアプリケーションURLをコピーする。
3. Ranking Request Settingsフィールドに、ランキングの種類ごとで配列に要素を入れていく。Ranking Nameはランキングの名前で、Ranking Numberは上位何位まで取得するかを指定し、Order Byは昇順(ASC)か降順(DESC)を選べる。
4. スクリプト内でRankingManager.Instance.SendScoreAsync(playerName, score, handleResponseCallback, rankingRequestIndex)を呼んでスコアを送信し、RankingManager.Instance.GetRankingListAsync(handleResponseCallback, rankingRequestIndex)でランキングリストを取得できる。同様に、メソッドの前にyield returnを付ければデータ取得後の処理(handleResponseCallback)の実行完了まで待つことができる。
5. 具体的な使い方はサンプルシーンとスクリプトを参照してください。

  • Googleスプレッドシートにスコア送信します。
    send_score.gif

  • Googleスプレッドシートにあるスコアを更新します。
    update_score.gif

  • Googleスプレッドシートのランキングリストを取得します。
    get_ranking.gif

参考

  1. UnityのWebGL出力に簡単に無料でグローバルランキングを実装できる仕組みを考えてみた
  2. GAS で「一部のスコープへのアクセス権限がありません」と怒られたときの対処法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript勉強の記録その16: 文字列の操作

文字列の長さを取得する

JavaScriptには文字列を操作する為の方法がいくつか用意されてありますので、メモとして書き残します。

index.js
const str = 'hello';
console.log(str.length);
//=>5

文字へのアクセス

文字へのアクセスの方法は、文字列[インデックス番号]とすることで可能です。
以下の例ではhelloという文字列の1番目と2番目にアクセスし、コンソールに表示しています。

index.js
const str = 'hello';
console.log(str[1]);
//=>e
console.log(str[2]);
//=>l
console.log('hello'[1]);
//=>e

substring()を利用して、文字列の一部を切り出す

substring()という文字列を操作するメソッドを使えば、文字列の一部を切り出すことができます。
substring(切り出しを開始したいインデックス番号、切り出しを終了したいインデックス番号)というように引数を取れば利用することができます。

具体的な使い方としては以下です。
substringメソッドの第一引数は2で第二引数を4とすることで、「ll」のみを取ることができます。

index.js
const str = 'hello';
console.log(str.substring(2, 4));
//=>ll
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数のファイルをアップロードしてExpressサーバーで保存する

概要

クライアントアプリからExpressサーバーへの複数ファイルのアップロード時にハマったので、
方法を記載します。

環境

開発環境はMacでNodebrewを使っています。

$ nodebrew 
nodebrew 1.0.1
$ node -v
v12.14.1

単一ファイルのアップロード

まず単一ファイルのアップロード方法を記載します。

Expressサーバー立ち上げ

express-generatorでExpressサーバーを作成します。

$ npm install -g express-generator

express-generatorをインストールしてexpressコマンドが使えるようになったので
アプリを作成します。

$ express --view=ejs express-app

   create : express-app/
   create : express-app/public/
   create : express-app/public/javascripts/
   create : express-app/public/images/
   create : express-app/public/stylesheets/
   create : express-app/public/stylesheets/style.css
   create : express-app/routes/
   create : express-app/routes/index.js
   create : express-app/routes/users.js
   create : express-app/views/
   create : express-app/views/error.ejs
   create : express-app/views/index.ejs
   create : express-app/app.js
   create : express-app/package.json
   create : express-app/bin/
   create : express-app/bin/www

   change directory:
     $ cd express-app

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=express-app:* npm start

上の手順の通りにサーバーを立ち上げます。

$ cd express-app/
$ npm install
$ npm start

以下のURLにアクセスしてサーバーに接続できることを確認します。
http://localhost:3000

スクリーンショット 2020-01-12 15.52.33.png

アップロードAPI作成

サーバーを立ち上げることができたので、一度停止してアップロードAPIを作成します。
app.jsを以下のように修正します。

app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
// 追加
var uploadRouter = require('./routes/upload');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);
// 追加
app.use('/upload', uploadRouter);

FormDataを処理するためにmulterを追加します。
https://github.com/expressjs/multer

$ npm install multer

routesフォルダ内にupload.jsを作成します。
destで指定したフォルダ内にアップロードされたファイルが保存されます。

upload.js
var express = require('express');
var multer  = require('multer');
var upload = multer({ dest: 'uploads/' });
var router = express.Router();

router.post('/', upload.single('file'), function(req, res, next) {
  console.log(req.file);
  console.log(req.body);
  res.send('upload success');
});

module.exports = router;

これでAPIが作成できたのでnpm startでサーバーを立ち上げておきます。

アップロードフォーム作成

クライアントアプリ側に以下のようなファイルを作成してアップロードフォームを用意します。
Ajaxを使ってファイルと一緒に適当なパラメータも送信しています。

upload.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Uploader</title>
<script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>
</head>
<body>
    <div>
        <input type="file" id="upload">
        <input type="button" id="uploadButton" value="送信">
    </div>
<script>
$(function(){
    $('#uploadButton').click(function() {
        const files = $('#upload')[0].files;
        const formData = new FormData();
        formData.append('file', files[0]);
        formData.append('hoge', 123);

        $.ajax({
            url: 'http://localhost:3000/upload',
            method: 'post',
            data: formData,
            processData: false,
            contentType: false
        }).done(function(res){
            console.log(res);
        }).fail(function(err) {
            console.log(err);
        })
    })
});
</script>
</body>
</html>

ファイルアップロード

Expressサーバーを立ち上げている状態で上で作成したupload.htmlをブラウザで開いて、
ファイルを選択後に送信ボタンを押下します。

Expressのコンソールに以下のような値が出力されています。
req.fileにアップロードしたファイルの情報、req.bodyにパラメータが格納されています。

{
  fieldname: 'file',
  originalname: 'upload_file.txt',
  encoding: '7bit',
  mimetype: 'text/plain',
  destination: 'uploads/',
  filename: '12dee747383a844dd7d1888578cf720e',
  path: 'uploads/12dee747383a844dd7d1888578cf720e',
  size: 247379
}
[Object: null prototype] { hoge: '123' }
POST /upload 200 5.874 ms - 14

multerのdestにuploadsを指定したので、Expressアプリのルートディレクトリにuploadsフォルダが作成されて
その中にアップロードしたファイルが保存されています。

もし送信ボタン押下時にクロスドメインのエラーが発生する場合は、
以下のようにExpressサーバーにCORSの許可設定を行なってください。

CORS対応

ファイルアップロード時にクロスドメインエラーが発生した場合はExpressにCORSの許可を設定する必要があります。
方法はいくつかありますが、今回はcorsモジュールを使用します。

$ npm install cors

上のコマンドを実行後にapp.jsに以下を追記してください。
今回は全リクエストを許可としていますが、本番運用などする際は適切に設定を行なってください。

app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// 追加
var cors = require('cors');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var uploadRouter = require('./routes/upload');

var app = express();

// 追加
app.use(cors());

この状態でサーバーを立ち上げてファイルアップロードを行うとクロスドメインエラーが発生しないようになっています。

複数ファイルのアップロード

前置きが長くなりましたが、次に複数ファイルのアップロードを行います。

アップロードAPI作成

まずExpressに複数ファイルのアップロードAPIを作成します。
比較用に単一ファイル用のAPIも残しています。

upload.js
var express = require('express');
var multer  = require('multer');
var upload = multer({ dest: 'uploads/' });
var router = express.Router();

// 単一ファイルアップロード
router.post('/', upload.single('file'), function(req, res, next) {
  console.log(req.file);
  console.log(req.body);
  res.send('upload success');
});

// 追加
// 複数ファイルアップロード
router.post('/multiple', upload.array('files'), function(req, res, next) {
  console.log(req.files);
  console.log(req.body);
  res.send('multiple upload success');
});

module.exports = router;

アップロードフォーム作成

クライアント側にも複数ファイルのアップロードフォームを追加します。
ポイントは複数ファイルをFormDataに追加する際に'files'を指定する部分です。

upload.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Uploader</title>
<script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>
</head>
<body>
    <!-- 単一ファイルアップロードフォーム -->
    <div>
        <input type="file" id="upload">
        <input type="button" id="uploadButton" value="送信">
    </div>
    <!-- 追加 -->
    <!-- 複数ファイルアップロードフォーム -->
    <div>
        <input type="file" id="multipleUpload" multiple>
        <input type="button" id="multipleUploadButton" value="送信">
    </div>
<script>
$(function(){
    // 単一ファイルアップロード
    $('#uploadButton').click(function() {
        const files = $('#upload')[0].files;
        const formData = new FormData();
        formData.append('file', files[0]);
        formData.append('hoge', 123);

        $.ajax({
            url: 'http://localhost:3000/upload',
            method: 'post',
            data: formData,
            processData: false,
            contentType: false
        }).done(function(res){
            console.log(res);
        }).fail(function(err) {
            console.log(err);
        })
    })

    // 追加
    // 複数ファイルアップロード
    $('#multipleUploadButton').click(function() {
        const files = $('#multipleUpload')[0].files;
        const formData = new FormData();
        for (let i = 0; i < files.length; i++) {
            formData.append('files', files[i]);
        }
        formData.append('hoge', 123);

        $.ajax({
            url: 'http://localhost:3000/upload/multiple',
            method: 'post',
            data: formData,
            processData: false,
            contentType: false
        }).done(function(res){
            console.log(res);
        }).fail(function(err) {
            console.log(err);
        })
    })
});
</script>
</body>
</html>

ファイルアップロード

Expressサーバーを立ち上げている状態で、先ほどと同じようにブラウザから複数ファイルを選択してアップロードを行います。
Expressのコンソールでファイルの情報が出力されて、uploadsフォルダにファイルが保存されていることが確認できます。

[
  {
    fieldname: 'files',
    originalname: 'upload_file 2.txt',
    encoding: '7bit',
    mimetype: 'text/plain',
    destination: 'uploads/',
    filename: '2c8436e8d77723dfaf7a75e38fe1785c',
    path: 'uploads/2c8436e8d77723dfaf7a75e38fe1785c',
    size: 247379
  },
  {
    fieldname: 'files',
    originalname: 'upload_file.txt',
    encoding: '7bit',
    mimetype: 'text/plain',
    destination: 'uploads/',
    filename: '5ed6ed7671d65f269dfaa2c456b2b95b',
    path: 'uploads/5ed6ed7671d65f269dfaa2c456b2b95b',
    size: 247379
  }
]
[Object: null prototype] { hoge: '123' }
POST /upload/multiple 200 19.628 ms - 23

まとめ

単一ファイルのアップロードは割と簡単に実装できましたが、
複数ファイルのアップロードでのクライアントからのFormDataへの追加と
サーバーでの保存がなかなかうまくいかずハマってしまいました。

Formタグを使えばもう少し簡単に実装できたかもしれませんが、
今回は使用せずに実装したかったためこのような方法になりました。

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

Chromebook向けにツールを作る

はじめに

Chromebookって便利ですよね?ただ痒いところに手が届かないことも多々あり、解決すべくツールを作ろうと思い立ちました。

どのアーキテクチャ、フレームワークが向いてる?

Linuxが動いたりAndroidアプリが使えたり、Chrome OSネイティブアプリの存在感が皆無だったりしたのでざっくり纏めてみました。

Chrome App

  • JavaScript向けのChrome APIsを使って、manifestを定義すればChromeアプリが作れる。APIの使い方はsampleを見るのがよい。
  • Windows、Mac、Linux向けが途絶えたりしてChrome OS向けだけ残った状況。
  • Native Clientという低レイヤー用のSDKもあるようだ。

Chrome Extension

  • Chrome Browser内で閉じられる場合はこれ。

Progressive Web App

  • Chrome AppではなくPWAがメインストリームなのでしょうか。ファイル操作とかのAPIが使えないのでツール開発には向かなそう。

Android App

  • Chrome OSをサポートしたAndroidアプリもChrome OSアプリというようだが、さくっと作れるものではない。

Linux上で動かす

  • ツールを動かすためにCrostiniを有効にはしたくない。立ち上がるの遅いし、メモリ食うし。

Flutterアプリ

  • Flutter SDKはChromebookもサポートしてるけど、ツールを作るのにここまでしたくない。

結論

現状(バージョン: 79)でtabletで動かすケースではChrome Appがベターですかね。
ファイル操作だけ出来ればいいならNative File System使うのがいいかも。

ハローワールド

  • Chrome browserを開いて、拡張機能のデベロッパーモードを有効にする。
  • samplesから参考になりそうなのを見つけて、「パッケージ化されていない拡張機能を読み込む」で開く。
  • アプリのアイコンがメニュー登録されているので、実行する。
  • デバッグは拡張機能の一覧からアプリの「ビューを検証」のバックグラウンドページ、index.htmlをクリックしてChrome DevToolsを開く。
  • 修正したら拡張機能の一覧からアプリをリロードしてデバッグする。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】2種類の恐竜画像を任意の数だけ表示させてみる。

はじめに

会社で使っている言語以外にも、何か身につけたい。

仕事では全く違う言語を使っていることもあって、楽しくないと新しい知識が吸収できなくなっている。
作って楽しい気分になるプログラムを考えてみた。

今回登場のメンバーはステゴディアスとインドミナスレックス。
ボタンを押すと、この二匹のどれかが、任意の数だけ表示されるプログラムを作ってみた。

stedia.jpg
indominas.jpg

1.今回のポイント

  • 任意の数だけイメージタグを作りたい。
  • idはわかりやすいように数字を入れたい。
  • 乱数(ここで表示する画像数を決定する)
  • インドミナスの画像が沢山表示されたら、攻めの姿勢で、ステゴディアスが表示されたならソースの実行確認などを念入りに行って守りを固めるとよいでしょう

2.実際のソース

dino_print_random.html
<!DOCTYPE html>
任意の数だけ恐竜を表示<br>
<form name="test">
<input type="button" value="dinosor_print!!" onClick="OnClickmake()"/>
</form>
<div id = "dino"></div>
<body>
<script type="text/javascript">

function OnClickmake(){
    var min=2;
    var max=6;
    //表示する画像の個数
    var num_dino=Math.floor( Math.random() * (max + 1 - min) ) + min;
    //表示する画像の種類
    var type0=Math.floor( Math.random() * (max + 1 - min) ) + min;
    appendImage(num_dino,type0);
}


function appendImage(num1,d_type) {
    if (!document.createElement || !document.getElementById) return;

    alert(num1);
    var str_dino="";
    //1つのイメージタグに設定する内容
    var c_dino="";
    //イメージのid
    var str_id="";
    //4以下ならばステゴディアス
    if(d_type<4){
        img_src="./stedia.jpg";
    //4以上ならインドミナスレックス   
    }else{
        img_src="./indominas.jpg";
    }
    for(var i=0;i<num1;i++){
        //イメージのid ダブルコーテーションはエスケープ(\)が必要
        str_id="\""+"img"+i+"\"";
        c_dino="<img id="+str_id+ " src = "+img_src+" width=\"360\" height=\"240\"><br><br>";
        str_dino=str_dino+c_dino;
    }
    document.getElementById("dino").innerHTML = str_dino;

}
</script>
</body>
</html>

実行結果

上記の2つの画像にそれぞれstedia.jpg,indominas.jpg
と名前を付けて、ソースと同じフォルダにおいてブラウザで実行すると
下記のような画像が表示されます。

dino_random_image1.jpg

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

JavaScript勉強の記録その15: 配列にオブジェクトを格納するデータ構造

配列とオブジェクトを組み合わせたデータ構造

配列にオブジェクトを格納されてあるようなデータ構造もあります。
以下の例では、pointsという定数に配列が代入してあり、その配列は2つのオブジェクトを持っています。
プロパティーへのアクセスは、 配列名[インデックス番号].キー と記述することで可能になります。

index.js
const points = [
  {x: 100, y: 180},
  {x: 120, y: 160},
];

console.log(points[0]);
//=>{x: 100, y: 180}
console.log(points[0].x);
//=>100
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript勉強の記録その14: Object.keys(オブジェクト名)を利用してオブジェクトのプロパティーを操作

Objet.keys(オブジェクト名)を利用して、オブジェクトのプロパティーを操作

オブジェクトのプロパティーの値を全て取り出したい場合は、forEachは使えません。なぜならforEachは配列に対するメソッドだからです。

代わりにObject.keys(オブジェクト名)というメソッドを使い、オブジェクトのキーを取得し、取得したキーを利用してオブジェクトの値を取り出す方法があります。

まず、以下の例ではObject.keys()の基本的な動きを確認できます。

index.js
const point = {
  x: 100,
  y: 180,
};

const keys = Object.keys(point);
console.log(keys);
//=>["x", "y"]

keys.forEach(key => console.log(`Key: ${key}`));
//=>Key: x
//=>Key: y

Object.keys()を使うことで、キーを取得して配列に格納しているのがわかるかと思います。
この仕組みを利用してループ処理をしてあげれば、オブジェクトの各プロパティーへアクセスすることができます。

index.js
const point = {
  x: 100,
  y: 180,
};

const keys = Object.keys(point);
console.log(keys);
//=>["x", "y"]

keys.forEach(key => console.log(`Key: ${key} Value: ${point[key]}`));
//=>Key: x Value: 100
//=>Key: y Value: 180
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む