とんちむ日記

RubyとJavaScriptと猫が好きです

今さら聞けないJavaScriptのややこしいところ(スコープ、巻き上げ、this)

これはQiitaに昔書いてたやつを引っ越したものです。

①スコープについて

これについては基本なので、ご存知の方は多いと思いますが折角なのでES2015以降の場合も併せて書こうと思います。

変数のスコープはブロックではない(※variable文の場合)

varによって宣言された変数は、通常の{}で作るブロックではスコープを作りません。ifだろうがforだろうが作らないったら作りません。

{
  var a = "foo";
}

if(true){
  var b = "bar";
}

for(var i = 0; i < 1; i++){
  var c = "buz";
}

console.log(a); //=> "foo"
console.log(b); //=> "bar"
console.log(c); //=> "buz"

ではどうしたらスコープを作ることができるのかというと関数定義の{}ならスコープが作られます。

(function(){

  var a = "foo";

})();

console.log(a);
// ReferenceError: a is not defined
  

このように即時関数を最初に記述しておいてその中に処理を記述しているのを見たことがあると思います。ブラウザがJavaScriptのファイルを複数読み込んだ時に宣言した変数や関数が衝突しないように即時関数を使っているんですね。

ES2015以降だとletとconstが使用でき、それによって宣言された変数と定数では関数定義でなくてもブロックでスコープが作られます。

{
  let   a = "foo";
  const B = "bar";
}

console.log(a);
// ReferenceError: a is not defined
console.log(B);
// ReferenceError: B is not defined

BabelやTypeScriptなどでES2015以降の記法が許される環境であればvarによる宣言は使わなくていいでしょうね。

②変数と関数の巻き上げ(ホイスティング)

関数を定義する方法については2種類ありますね。

var func = function(){}; // 関数リテラルの代入

function func(){}; // 関数宣言

この二つですがきちんと使い分けないと思わぬところでハマります。

func(); // TypeError: func is not a function

var func = function(){
  console.log("hello");
};

上の関数リテラルの場合、読み込み時に代入の処理は行われませんから、funcは関数ではないと言われています。呼び出し時はまだundefinedですね。

func(); //=> hello

function func(){
  console.log("hello");
};

対してこちらの例ではfuncの呼び出しに成功しています。 代入などの処理は順に行われますが、宣言についてはそのスコープの最初で行われるからです。これを巻き上げ(ホイスティング)と呼びます。

巻き上げについては変数も同様です。先ほどの関数リテラルの場合のエラーがReferenceError: func is not definedではないことからもわかります。

var a = "foo";

function func(){
  console.log(a); // 1回目
  var a = "bar";
  console.log(a); // 2回目
}

func();

この場合、1回目の出力結果はfooではなくundefinedとなります。funcの最初でaが再度宣言されているためです。上のコードは下のコードのように動作しているわけです。

var a = "foo";

function func(){
  var a; // ここで宣言されている
  console.log(a);
  a = "bar";
  console.log(a);
}

func();

③5つのthis

JavaScriptのthisは状況によって指し示すものが異なります。 その例を見ていきましょう。

1.コンストラクタ呼び出し

function Person(name, age){
  this.name = name;
  this.age  = age;
}

var tom = new Person('tom', 20);

この場合Personのthisは戻り値となるオブジェクトを指します。

2.関数呼び出し

先ほどのPersonを関数として呼び出すとどうでしょう。

Person('hoge', 20);

console.log(window.name); //=> "hoge"

windowオブジェクトにnameというプロパティを追加してしまいました。これからわかることは関数呼び出し時のthisはグローバルオブジェクトを指しているということです。こういった意図しない挙動を防ぐにはstrictモードを使うといいですよ。

function Person(name, age){
  'use strict';
  this.name = name;
  this.age  = age;
}

Person('hoge', 20); //=> TypeError: Cannot set property 'name' of undefined

3.メソッド呼び出し

メソッド呼び出しというのはレシーバのある呼び出し方です。(jsでレシーバという言い方をするのかわかりませんが・・)要はobject.method();といった呼び出し方ですね。

var obj = {
  name: 'obj_name',
  method: function(){ console.log(this.name) }
};

obj.method(); //=> "obj_name"

この場合はobj自身がthisとなります。一番わかりやすい挙動ですねー。

4.applyかcallを使用する場合

Function.prototype.apply()Function.prototype.call()は呼び出し元の関数のthisと引数を置き換えて呼び出すことができるという面白い関数です。

var obj = { name: 'foo' };

function func(){
  console.log(this.name);
}

func.call(obj); //=> "foo"
func.apply(obj); //=> "foo"

関数呼び出しの際はthisはwindowなのでthis.nameundefinedになりそうですが、この例ではthisがwindowからobjに入れ替えられているため呼び出しに成功します。

ちなみに Function.prototype.bind() というのもありますが、これは呼び出し元の関数のthisや引数を置き換えた新しい関数を返すものです。 部分適用した関数を作ることも出来る面白い関数ですよ。(部分適用とよく混同される概念にカリー化というものがあって・・・・・長くなるのでやめます)

5.アロー関数内で定義されたthis

アロー関数はES2015で登場した関数の新しい定義方法です。

function Person(name){
  this.name = name;  
  this.show = () => {
    console.log(this.name)
  }
}

let p1 = new Person("foo");
let p2 = new Person("bar");
p1.show.call(p2); //=> "foo"

このコードの実行結果はp1.show()と呼び出したときと同じです。アロー関数式でのthisは定義するコンテキストのthisに固定されます。なので実行時にthisが変わることがありません。もうbind(this)しなくてもいいんです。 ただし、さきほどのcall(),apply(),bind()で変更することもできません。

おしまい。

javascriptのvalidationをchainする

きっかけ

  • いちいちバリデーションする時にif() ~とかするのがめんどくさい。
  • chainでバリデーションを実行したい
  • Railsのvalidationみたいにerrors.full_messagesみたいにまとめてエラーメッセージ取れたら嬉しい

・・と思ってまたライブラリでも作ろうかと思ってたけど調べていたらvalidator.jsのissueにこんなのがあった

var validator = require('validator');

function ValidatorChain(str) {
    this.str = str;
}

Object.keys(validator).forEach(function (fn) {
    ValidatorChain.prototype[fn] = function () {
        var args = Array.prototype.slice.call(arguments);
        args.unshift(this.str);
        if (!validator[fn].apply(validator, args))
            throw Error(fn + ' failed with ' + this.str);
        return this;
    };
});

exports.check = function (str) {
    return new ValidatorChain(str);
};

check('foo@bar.com').isEmail().isLength(5, 64);

引用元: https://github.com/chriso/validator.js/issues/407#issuecomment-118561692

「様々なユースケースに対応するためにあえて標準ではchainできるようにしてない」 みたいなニュアンスの事が書いてあって、上のコードでchainできるようになる。

とはいえこれだとエラーメッセージはカスタマイズしにくいし、chainを全部通してから全部のエラーメッセージを表示するとかってのもできない。あとクライアントサイドなら別にthrowしなくてメッセージだけでいいって事もあるかもしれない。

と思って上に手を入れてこんな感じにしてみた。

import validator from 'validator'
import messages from './messages.js'

function ValidatorChain(str, messages) {
  this.str = str;
  this.errors = [];
  this.messages = messages;
}

Object.keys(validator).forEach(function (fn) {
  ValidatorChain.prototype[fn] = function (...args) {
    args.unshift(this.str);
    if (!validator[fn].apply(validator, args)) {
      const message = this.messages(fn, args)
      this.errors.push(message)
    }
    return this;
  };
});

function check(str, messages) {
  return new ValidatorChain(str, messages);
}

console.log(check('foo', messages).isEmail().isLength(5, 64).errors);

こうすればmessages.jsをこちらで用意すればメッセージをカスタマイズできるし、全てのバリデーションを通した結果の全エラーメッセージを取得できる。

messages.jsは以下のように作る。args[this.str, ...呼び出したvalidatorの引数]という感じになる。

export default function(fn, args){
  const messages = {
    isEmail: `${args[0]}は正しいEmailの形式ではありません`,
    isLength: `${args[1]}文字から${args[2]}文字以下で入力してください`
  }
  return messages[fn]
}

babelでトランスパイルして実行してみた。

$ node build/index.js
=> [ 'fooは正しいEmailの形式ではありません', '5文字から64文字以下で入力してください' ]

errorメッセージの有無でバリデーションの成否を判断するとか、もしくはValidatorChainのプロパティにエラーがあったかの状態を追加すればいいんじゃないかなと思う。

Google Chromeがimportに対応した

対応するって話は知ってはいましたがとうとう対応したんですね。ちょっと興奮して久しぶりにブログを更新してます。

ES2017も正式勧告されたこのご時世にimportを知らないwebエンジニアは少数だと思いますが、使い方はこちら

Safariも最新版はiOSを含めて対応してると小耳に挟んだので他のブラウザ実装状況はどうだろうかとcan i useで調べて見ました。

今のところこんな感じみたいでEdgeやFirefoxもデフォルトでは対応していないもののNot supported by default, but can be enabled. とあるので設定変更するとかで有効にできるようです。

というわけでやってみた。

// hello.js
const Hello = "hello"
export default Hello
// sample.js
import Hello from './hello.js'
console.log(Hello)
<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script type="module" src="./sample.js"></script>
</body>
</html>

chromeはローカルのファイルだと制限があるのでpythonのサーバーを立ち上げます。

$ python3 -m http.server

http://localhost:8000/index.htmlchromeで開くと・・ コンソールにhelloと表示されました。やったね!

ちなみにnode_modulesには対応してないのでそういうの使いたければ適当な場所に置いといて読み込むとか、これまで通りWebpackなどのモジュールバンドラーを使う必要があるそうです。

とは言ってもグローバル汚染しなくてもライブラリを扱えるのは素晴らしいです。

他のブラウザも早く対応して欲しいですね。

モック用のJSONやCSVなどのデータを大量にかつ簡単に作成できるMockarooを試してみた

どうも、フロントエンジニアになりたい人です。

js側から叩くAPIがまだサーバーサイドで実装されてないけど、js側の処理も先にやっとかないといけないって状況ありますよね・・多分。僕は今のところ経験したことないけどきっとある。

あと、テスト用のデータ欲しいとか。簡単なjsのデモ作りたいけどそのためにAPIを実装するのも面倒だとか、そもそもサーバーサイド作れないとか。

そこでAPIの仕様さえ決まってれば1000件のデータまで無料でJSONCSVや TSVとかなんならSQLもいけるらしい)ファイルを作ってくれるサービスMockarooを見つけました。

こちらが初期状態です。 f:id:tonchix:20170509161216p:plain

Field NameはDBでいうカラム名ですね。Typeはどういうデータを入れたいかを指定します。ランダムでそれっぽいデータを作って入れてくれます。Fakerっぽいやつだと思って貰えば良いかと。

Optionにあるblankは空(DBでいうとNULL)が入る割合を指定できます。50%だったら1000件作れば500件ほどのデータは指定したフィールドに値がないものになります。

fxは生成されるデータを加工したい場合にここで指定できるようです。用意されている関数を使うこともできますし、Rubyが書けるならRubyを使って加工することもできます。例えばthis.upcaseとやれば全部大文字にするとか。

こんな感じで英語わかんなくてもぱっと見た感じ簡単に作れそうですよね。 でもちょっとだけ悩んだのが、こういうデータが欲しい時。

{
  "friends": [
    { "first_name": "foo", "last_name": "bar" },
    { "first_name": "hoge", "last_name": "huga" }
  ]
{

以下のようにすれば良いみたいです。

f:id:tonchix:20170509155905p:plain

↓こういうのが欲しい場合は 上の例からJSON Arrayのフィールドを無くせば良いです。

{
  "friends": {
    "first_name": "foo",
    "last_name": "bar"
  }
}

また、単純な配列が欲しいだけならば、フィールド名[2]とかやると[]の中で指定した大きさの配列になります。

あとはダウンロードして適当なところに置いてjsから呼び出せばOK!やったー。しかも無料の範囲なら登録不要みたいですよ、楽ですねー。

他にも公式のnpm packageもあってこちらはAPIkeyが必要なので登録する必要がありますが、無料プランだと1日200リクエストまで使えます。詳細はリンク先をみてもらえればなんとなく雰囲気掴めると思います。

実際に試してみました。下のようにclient.generateとすればPromiseを返してくれるようです。generateに渡すcountの値が1の場合はPromiseから取り出す値は配列ではないので注意してください。

const Mockaroo = require('mockaroo');

const client = new Mockaroo.Client({
  apiKey: '******' //自分のAPIKeyを入れてね
});

client.generate({
  count: 10,
  fields: [{
    name: 'id',
    type: 'Row Number'
  },{
    name: 'friends.first_name',
    type: 'First Name'
  },{
    name: 'friends.last_name',
    type: 'Last Name'
  }]
}).then(records => {
  for(let record of records){
    console.log(record.id, record.friends.first_name, record.friends.last_name);
  }
    console.log(records[0]);
});

結果は以下のようになります。(最後のはちょっと整形してます)

1 'Susy' 'Ingarfill'
2 'Jakob' 'Duligal'
3 'Lanie' 'Jamieson'
4 'Joanna' 'Betham'
5 'Magdalena' 'Castledine'
6 'Roxine' 'Cogdon'
7 'Bea' 'Trevaskis'
8 'Gertruda' 'Bannard'
9 'Dorelle' 'Moors'
10 'Bruno' 'Dorot'
{ 
  id: 1,
  friends: { 
    first_name: 'Ermin',
    last_name: 'Rosenfarb'
  }
}

簡単だし便利ですねー。

ただ日本語対応してないっぽいので日本語のデータ欲しいならばfxのとこで無理やり日本語データ作るか、これじゃなくてFakerとか使うかする必要がありますね。

サイトではSQLも作れるのでDBにテストデータを作るのにも便利だなーと思いました。僕はRailsを主に使うのでその辺の必要性はないですが、データを作るのが面倒な開発環境ならそこそこ使えるかも知れませんね。

もっといい方法あるよーって場合はぜひ教えてください。