jsPsychのためのjavascriptのキほん

公開:2020/12/24
更新:2020/12/24
13 min読了の目安(約11700字TECH技術記事

はじめに

私,恥ずかしながら,jsPsych による心理学実験作成チュートリアルというものを作りました。「jsPsych で実験を作りたい。でも全然プログラミングしたことない」という人を想定して作ったつもりです。しかしながら,自分の身の回りでチュートリアルを参考にしてくださった人からは,「javascript が分からないから自分の実験を作るときに結局どうしたらいいかよくわかんないんですよね」という声をいただきました。チュートリアルでは ifくらいは紹介しましたが,大体の文法は「こういうもんだと思っておいてください」ということで説明しておらず,キホンのキすらすっ飛ばしていたので,あのチュートリアルから他の実験に拡張するのは,プログラミング経験がないと確かに難しいだろうなぁと思いました。

ということで,反省の意も込めて,javascript の基本文法のうち,知っておくといいと思ったものをまとめることにしました。特に,オブジェクトと関数についての知識は jsPsych の見通しをよくすると思うので,少し力を入れて読んでもらいたいです。

本記事はチュートリアルの内容を補完するものですので,チュートリアルを読んだことがないよという方はまずそちらに目を通すことをオススメします。また,本記事は,Online Psychological Experiment Advent Calendar 2020の 25 日目(!)の記事です。

データ型

javascript を含め,プログラミング言語では,データ型が存在します。型によって使い方が異なります。javascript の公式(?)リファレンスには,8 つのデータ型があると書いてあります。本記事ではそのうち,数値,文字列,真偽値,配列[1],オブジェクトを紹介します。

数値 Number

数値です。各種演算ができます。% は剰余演算子で,左の項を右の項で割ったときの余りを返してくれます。**はべき乗演算子で,左の項を右の項で累乗した結果を返します。一般的な計算規則と同様に,乗算*, 除算\ が 加算・減算よりも優先されます。()で囲うことで計算順序を変えることができます。これも一般的な計算規則と同様ですね。

var a = 1;
var b = 2;
a + b; // 3
a - b; // -1
a * b; // 2
a / b; // 0.5
a % b; // 1
b ** 3; // 8
2 + a * b; // 4
(2 + a) * b; // 6

文字列 String

文字列です。各種クオーテーションで囲うことで文字列として認識されます。シングルクオーテーション', ダブルクオーテーション", バッククオーテーション`。シングルとダブルクオーテーションは同じように使います。バッククオーテーションは既存の変数を${変数名}とすることで文字列に組み込むこともできます。

var last = '田中';
var first = '太郎';
var fullname = `${last} ${first}`; // 田中 太郎
var age = 21
`${fullname} ${age}` // 田中 太郎 21歳

文字列は加算演算子+を使って他の文字列と連結することができます。文字列に変数を利用する場合は,バッククオーテーションを使うほうが便利そうですね。

last + first; // 田中太郎
var fullname2 = last + ' ' + first; // 田中 太郎
fullname2 + ' ' + age + '歳'; // 田中 太郎 21歳

ちなみに javascript では加算演算子の 2 つの項に文字列と数値を持ってきてもエラーを吐きません(他のプログラミング言語ではエラーを吐くことが多いです)。数値を文字列に変換して連結するので,数値同士の加算とは異なった結果が返ってきます。数値の加算を想定している場合は,どちらの項にも数値型が使われているか注意しましょう。

age + '1'; // 211 になる。22にはならない。

真偽値 Boolean

真偽値です。真trueか偽falseのどちらかの値をとります。主に if 文(後述)で使います。自分で変数にtruefalseを格納することはあまりありません。関係演算子や等値演算子による計算結果として得られたものを利用することが多いです。関係演算子には<, >, <=, >=などがあります。等値演算子には==, !=, ===, !==があります。=====は左右の項が同じかどうかを判定します。!=!==は左右の項が異なるかどうかを判定します。=の少ない方は,左右の項の型が異なる際に型の変換をしてから比較を行います。関係演算子と等値演算子のリファレンスはこちら

1 == 1; // true
1 == '1'; // true 型の変換を行うので同じと判定される
1 === 1; // true
1 === '1'; // false
1 != 2; // true 左右は異なるのでtrue
1 != '1'; // false 型変換の結果,異ならないと判定される
1 !== 1; // false
1 !== '1'; // true 型変換しないので,左右は異なると判定される

もちろん,比較の結果を変数に格納することもできます。

var shin = 1 == 1;
var gi = 1 === '1';

論理演算子を用いることで,複数の真偽値を一度に扱うことができる。&&は論理積(AND),||は論理和(OR)。&&||も並べる場合は,&&の方を先に実行します。()を使って判定の優先順位をつけることもできます。

1 > 0 && 2 > 0; // true AND true なので true
1 > 0 && 2 > 3; // true AND false なので false
1 > 2 && 2 > 0; // false AND true なので false
1 > 0 || 2 > 0; // true OR true なので true
1 > 0 || 2 > 3; // true OR false なので true
1 > 2 || 2 > 0; // false OR true なので true
1 > 0 || (2 > 0 && 2 > 3); // true ()はなくても&&の方を先に実行する
(1 > 0 || 2 > 0) && 2 > 3; // false

最初にも述べましたが,真偽値単体で使うことはほぼありません。if 文や for 文で使います。ちなみに,true, falseを数値型に変換すると,1, 0になります。各試行の正誤判定をして正反応数やその割合を算出するような実験では数値に変換してデータに保存すると集計がちょっと楽になります。

Number(true); // 1
Number(1 === 1); // 1
Number(false); // 0
Number(1 === 0); // 0

配列 Array

複数の値を保持するリストのようなオブジェクトです。jsPsychtimelinetimeline_variableの指定に使用するデータ型です。[]を使って配列を作成することができます。配列に入っている一つ一つは要素と呼ばれます。配列に入っている要素の型は異なっていても構いませんし,配列の中に配列を入れることも可能です。作成した変数を配列に入れることも可能です。

var arr1 = [1, 2, 3];
var arr2 = [1, 'あ', false];
var arr3 = [1, 'あ', [2, 4]];
[1, 'あ', fullname]; // [1, 'あ', '田中 太郎']
[1, 'あ', arr1]; // [1, 'あ', [1,2,3]]

もちろん,オブジェクト(後述)も配列の要素にすることができます。

var arr4 = [
  { a: 1, b: 2 },
  { c: 3, d: 4 }, // このように改行しても構わない
];
// [{ a: 1, b: 2 },{ c: 3, d: 4 },]; と同じ

要素となっているオブジェクトのキーを同じにすれば,timeline_variablesに使用される形式になりますチュートリアル第 5 回)。

配列の要素を取り出すためには取り出したい要素の位置(インデックス)を指定します。インデックスは 0 番目から始まるので注意してください。存在しないインデックスを指定した場合はundefinedが返されます。

arr1[0]; // 1
arr1[3]; // undefined

既存の配列の最後に要素を追加する場合は.push(新要素)とします。最初に要素を追加する場合は.splice(0,0,新要素)とします。

arr1.push(8);
arr2.splice(0, 0, fullname);

配列の要素数(長さ)は.lengthで取得することができます。入れ子構造の配列であっても,一番外側(?)の配列の要素数を返します。

arr4 = [1, 2, 3];
arr4.length; // 3
arr5 = [
  [1, 2, 3],
  [4, 5, 6],
];
arr5.length; // 2 2つの配列を要素とする配列なので長さは 2

その他色々なことができます。必要に応じて適宜ググってください。

オブジェクト Object

キー(key)と値(value)からなるデータ型です。連想配列と呼ばれることもあります。数値,文字列,真偽値以外のほとんどがオブジェクトです。そして,jsPsychで作っている変数のほとんどがオブジェクトです{キー: 値}というふうにしてオブジェクトを作成することができます。キーには文字列を使用します[2]が,値には様々なデータ型をとることができます。複数のキーおよび値を持つことができます。こういったオブジェクトのキー・値の組をプロパティ,特にキーのことをプロパティ名と呼ぶこともあります。

var obj1 = { a: 1 }; // キーの文字列にはクオーテーションはなくてもよい(あってもエラーにはならない)
// 値にはいろんなデータ型が使える
var obj2 = {
  key1: 1, // 数値
  key2: 'あ', // 文字列
  key3: true, // 真偽値
  key4: ['a', 'b', 'c'], // 配列
  key5: { hoge: 'geho' }, // オブジェクト
};

ここで重要なことは,obj2のような変数の宣言はjsPsychの試行を作成する際の宣言と同様だということです。

var trial = {
  type: 'html-keyboard-response',
  stimulus: 'hello world!',
  choices: ['f', 'j'],
  stimulus_duration: 1000,
  trial_duration: 2000,
  response_ends_trial: false, // 反応取得後に次の試行に移る場合は true にする
};

var block = {
  timeline: [trial],
  timeline_variables: hogehoge,
  repetitions: 3,
};

つまり,jsPsychの試行変数や,試行の系列・ブロックのための変数は,それだけではなんの動作もしないただのオブジェクトということです。これらのオブジェクトはjsPsych.init()という関数に入れることで心理学実験のように動作します。

ただし,試行変数などそれ自体はただのオブジェクトとはいえ,適当なプロパティ名を使用していいわけではありません。jsPsych.init()が適切に動作して意図した実験が実行されるためには,公式リファレンスで指定されているプロパティ名やデータ型を使用する必要があります。

また,配列の節でも言及しましたが,timeline_variablesに指定しているのは,オブジェクトを要素とする配列です。このとき作成するオブジェクトのプロパティ名は任意のもので構いません。実際にその timeline_variables を使用する箇所ではjsPsych.timelineVariable("プロパティ名")でプロパティ名を作成者自身が指定して値を参照することができるからです。

オブジェクトのプロパティの値は,オブジェクト.プロパティ名 あるいは オブジェクト["プロパティ名"]で取得することができます。obj2key4key5のように値が配列やオブジェクトになっている場合は,それぞれに対応する値の取り出し方を利用すれば,続けて値を取り出すことができます。取り出した値を変数に格納することもできます。

obj2.key1; // 1
obj2['key4']; // ['a', 'b', 'c']
obj2.key4[1]; // 'b'
obj2.key5.hoge; // 'geho'
var hohoho = obj2['key5']['hoge'];

また,値の呼び出し方に少し変更を加えるだけで,プロパティの値を更新することができます。具体的にはオブジェクト.プロパティ名 = 値 あるいは オブジェクト["プロパティ名"] = 値とします。もし存在しないプロパティ名を指定した場合は,新しいプロパティを追加します。

obj1.b = 2; // 追加 obj1 は { a: 1, b: 2} になる
obj1['b'] = 'あ'; // 更新 obj1 は { a: 1, b: 'あ'}

したがって,以下のような試行変数の宣言も可能です。極端な例であって,する必要は全く無いです。

var trial = {};
trial.type = 'html-keyboard-response';
trial.stimulus = 'hello world!';
trial.choices = ['f', 'j'];

構文

if

条件式が返す真偽値に応じて処理を分岐させることができます(チュートリアルで紹介しているので省略)。

for

同じ処理を繰り返し実行することができます。書いている時間がなかったので省略しますが,調べたらすぐヒットしますし,jsPsychで使うことはそんなに無い気もします。

関数

関数は一連の処理をまとめたものです。jsPsychでの実験作成においてオブジェクトと同じくらい重要な役割を持っています。関数はfunction 関数名(引数){まとめたい処理}で作成することができます。引数とは{}内の処理で使用したい外部の値のことです。作成後,呼び出すことで{}内の処理が実行されます。計算の結果を変数に格納することもできます。

// 作成
function square(num) {
  return num * num;
}
// 呼び出し
square(3); // 9
var kekka = square(5);

ここでは引数に 3 や 5 を指定し,関数内でそれを使った処理(2 乗する)をしています。returnは処理の結果を関数の外に吐き出しています。これがないと,処理の結果を得ることができません。

function square(num) {
  num * num; // return がない
}
square(3); // undefined になる

すでに存在する変数を変更する場合はreturnはなくても目的の処理を実行することができます。

var x = 3;
function add(num) {
  x = x + num;
}
add(2);
x; // 5

また,引数を必要としないような関数を作ることも可能です。

var x = 3;
function add2() {
  x = x + 2;
}
add2();
x; // 5

ただし,上の 2 つの例のような関数の作成は極力避けるべきです。外部の変数を参照しないほうが予期しない動作を防げますし,関数に引数を指定できるようにしたほうが汎用性が上がるからです。

もちろん,引数は複数とることができますし,複数行に渡る処理を記述しても構いません。以下の関数は,プログラミングでよく扱われる fizzbuzz 問題(Wikipedia)を参考にしたものです。第 1 引数に入れた数値に応じて,FizzBuzzが返ってきます。ここではFizzあるいはBuzzの判定の基準となる数字を変えられるようになっています[3]

function fizzbuzz(num, call_fizz, call_buzz) {
  var result = '';
  if (num % call_fizz == 0 && num % call_buzz == 0) {
    result = 'FizzBuzz';
  } else if (num % call_fizz == 0) {
    result = 'Fizz';
  } else if (num % call_buzz == 0) {
    result = 'Buzz';
  } else {
    result = `${num}`;
  }
  return result;
}

関数の宣言は,var 関数名 = function(){}でも可能です[4]。さらに,関数はオブジェクトのプロパティの値に持ってくることもできます。

var sqrt = function (num) {
  return num ** 0.5;
};
var obj3 = {
  heihokon: sqrt, // 既存の関数を指定するならカッコをつけない
  sanjo: function (num) {
    num ** 3;
  },
};
obj3.sqrt(2); // 1.414213...
obj3.sanjo(2); // 8

最後,関数の基本として知っておくべきことは,引数名はなんでもいいということです。引数に指定した値を関数内で参照するための名前なだけなので,numだろうがcall_fizzだろうがai, marumaru, morimoriなんでも構いません。大事なのは,どういう処理に使用される引数なのか推測しやすい名前にすることです

jsPsych での利用

さて,それではjsPsychでの実験作成のどこで使用できるかについて紹介します。様々なところで使用できるのですが,よく扱われているのは試行変数のon_starton_finishではないでしょうか。具体的な例として,サイモン課題を題材とした拙著チュートリアルの試行変数を見てみます。元のものから少し改変しています。また,timelineVariables()を使用しているので,それに使っているオブジェクトの配列trial_typesも合わせて記載しています。

var trial_types = [
  { letter: 'L', pos: 'left', cond: 'cong' },
  { letter: 'R', pos: 'left', cond: 'incong' },
  { letter: 'L', pos: 'right', cond: 'incong' },
  { letter: 'R', pos: 'right', cond: 'cong' },
];

var trial = {
  type: 'html-keyboard-response',
  stimulus: function () {
    var pos = jsPsych.timelineVariable('pos', true);
    var letter = jsPsych.timelineVariable('letter', true);
    return `<div class="text_${pos}">${letter}</div>`;
  },
  choices: ['f', 'j'], // 入力キーの指定
  trial_duration: 1000, // 試行の持続時間
  on_finish: function (data) {
    data.key = jsPsych.pluginAPI.convertKeyCodeToKeyCharacter(data.key_press);
    if (data.letter == 'L') {
      data.correct = Number(data.key == 'f');
    } else {
      data.correct = Number(data.key == 'j');
    }
  },
};

まず,stimulusには具体的な刺激ではなく,関数を指定しています。この関数に引数はありません。timelineVariable(プロパティ名, true)を使って,timeline_variables(ここではtrial_types)内のオブジェクトから対応するプロパティ名の値を取得し,変数posletterに格納しています。そして,それらに html タグも合わせた文字列を返しています。

次に,on_finishにも関数を指定しています。この関数は引数を一つ(data)必要とします。関数内では引数で与えられた値datakeyというプロパティを追加しています。さらに,letterというプロパティなども利用しながら.keyfjかを判定し,その結果を数値にしてcorrectというプロパティとしてdataに追加しています。.プロパティ名で値を取得したり追加したりできているようですから,このdata引数にはどうやらオブジェクト型の値が割り当てられるようになっているということがわかります。on_finishに指定した関数は,試行が終わったタイミングで実行されるのですが,内部で自動的にその試行のデータrtkey_pressが入ったオブジェクトが引数に渡されます。今回指定した関数では,そのデータにさらにいろんなデータを追加する処理をしていると言えます。

上のコード例には含まれていませんが,on_startというプロパティに関数を指定することができます。試行の開始時に実行され,引数には試行変数それ自体が渡されます。そのため,以下の例のように,試行変数のあるプロパティ(ここではduration)を試行ごとに変更することができます。

var trial2 = {
  // 色々省略
  on_start: function (trial) {
    trial.duration = trial.duration + 100;
  },
};

ちなみに,上の例の通り,試行変数の変数名とon_start の関数の引数名は一致していなくても構いません。

おわりに

今回は jsPsychで実験を作る際に知っておきたいjavascriptのキホンのキを紹介しました。オブジェクトと関数を意識すれば結構実験作成時の見通しが良くなるんじゃないかと思います。また,関数はコード全体を整理するのにも役立ったりしますので,jsPsych 用のオブジェクトのプロパティon_starton_finishで使用する以外でも是非活用してみてください。

不定期にはなりますが,今後も引き続き心理学実験・研究法に関する Tips を共有していきます。いいね・サポートをいただけると大変励みになりますので,ぜひそちらもよろしくお願いします。

脚注
  1. リンク先を見てもわかるように,配列は別個のデータ型として扱われていません。正確にはオブジェクトの一種ですが,使う際には別物という認識で使うことが多いので分けて紹介します。 ↩︎

  2. 数値もキーとして使用できますが,値を取り出すときに[]を使った方法しか使用できないので注意が必要です。シンボルというデータ型もキーに使用できますが,今回の記事ではそもそもシンボル型自体を紹介していないので,説明は割愛します。 ↩︎

  3. 実際はそれぞれの引数が自然数じゃないと動かないようにしたりするべきかもしれません。 ↩︎

  4. アロー関数 var 関数名 => () {}というものもあります。 ↩︎

この記事に贈られたバッジ