
作者:包寒吴霜jsPsych心理学实验与问卷编程指南(上篇)zhuanlan.zhihu.com
个人简介:psychbruce.github.io

上一篇文章初步介绍了jsPsych编程的基本套路和框架结构,简单讲解了什么是网络前端、什么是网络后端,并重点讨论了中文乱码、本地运行和线上运行等常见问题。有了第1~7章的内容作铺垫,我们就可以正式进入第8~14章的jsPsych编程了!
总目录(上篇1~7章,下篇8~14章)
- jsPsych是什么?
- jsPsych初体验:跑个示例玩一玩
- jsPsych的三种编写套路
- jsPsych的基本框架结构
- 中文乱码问题
- 本地运行与数据记录
- 线上运行与数据记录
- 简单入门:呈现指导语的几种方式
- 简单入门:调查个人背景变量的几种方式
- 技能提升:呈现Likert量表的几种方式
- 技能提升:编写按键反应程序的几种方式
- 拓展边界:小括号、中括号、大括号暗藏的玄机
- 拓展边界:自编JavaScript函数满足个性化需求
- 总结
本文用于讲解的代码是我之前编写并分享在GitHub上的一个示例程序“exp_demo”。
exp_demo(在线体验链接)psychbruce.github.io exp_demo(代码下载地址)github.com
在讲解具体的jsPsych(JavaScript)代码之前,有必要先推荐一些值得参考的基础教程。读者可以配合这些基础教程与本文一同享用,效果更佳。
- jsPsych官方教程系列
- jsPsych官网
- 一个反应时任务的完整示例
- jsPsych的核心功能(API)
- jsPsych的拓展插件(Plugins)
- jsPsych官网
- W3School系列
- HTML 教程 | W3School
- HTML 参考手册 | W3School
- JavaScript 教程 | W3School
- JavaScript 参考手册 | W3School
- 菜鸟教程系列
- HTML 教程 | 菜鸟教程
- JavaScript 教程 | 菜鸟教程
- JavaScript 和 HTML DOM 参考手册 | 菜鸟教程
- MDN系列
- HTML 教程 | MDN
- HTML 参考手册 | MDN
- JavaScript 教程 | MDN
- JavaScript 参考手册 | MDN
注:W3School、菜鸟、MDN三个系列的教程可以任选其一,内容大同小异。

在上一篇文章中,我们已经提到,编写jsPsych实验程序有三种套路,并且强烈推荐使用「纯JavaScript编写」,而非借助R语言或R Markdown间接编写。
具体来说,我们既可以在HTML文档里添加<script></script>标签并在其内部直接写JavaScript代码,也可以把JavaScript代码放到单独的.js文档中。推荐第二种做法,因为区分.html和.js可以让代码结构更清晰,也更方便日后重复利用或重新组合使用。
这里以RStudio作为代码编辑器,新建并编辑HTML和JavaScript文档。

【温馨提示】
(1)为了避免中文乱码,记得存储为 UTF-8编码格式,详见上一篇文章。
(2)在HTML中必须 「导入」与实验有关的.js文件,包括jsPsych脚本库里的拓展插件.js和我们自己编写的.js(如 <script src="experiment.js"></script>,可参考上一篇文章和GitHub代码示例),否则JavaScript程序不会在网页中起作用。
8 / 简单入门:呈现指导语的几种方式
jsPsych包含很多拓展插件(Plugins),其本质就是一个个单独的.js文件,为呈现刺激、记录反应以及更复杂的需求提供了便利。我们总览一下jsPsych最基础/最常用的几个模块:
- 全屏/取消全屏
- fullscreen
- 一页/多页指导语
- instructions
- 任何HTML元素(文字、图片、视频、按钮、表单……)
- html‑button‑response
- html‑keyboard‑response
- html‑slider‑response
- 图片
- image‑button‑response
- image‑keyboard‑response
- image‑slider‑response
- 音频
- audio‑button‑response
- audio‑keyboard‑response
- audio‑slider‑response
- 视频
- video‑button‑response
- video‑keyboard‑response
- video‑slider‑response
其中,比较常用的一个是html-keyboard-response模块,用于呈现任何HTML元素并收集键盘反应。例如,呈现一页指导语、请被试按任意键继续,编写起来很简单:
JS文件(命名为“experiment.js”):
// 定义实验材料(Trial或Block)
var welcome = {
type: "html-keyboard-response",
stimulus: "欢迎参与我们的实验,按任意键继续"
};
// 定义实验流程(时间线)
var timeline = [welcome];
// 运行实验(总控制)
jsPsych.init({
timeline: timeline
});
注:一个实验有且仅有一个jsPsych.init()代码块,其他通过var定义的变量 { } 都是“积木”,最后按呈现顺序放在一个timeline序列 [ ] 里面并传给jsPsych.init()即可。
HTML文件(命名为“index.html”,建议不要更改名称):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Experiment Demo</title>
<link href="jspsych-6-2/css/jspsych.css" rel="stylesheet" type="text/css">
<script src="jspsych-6-2/jspsych.js"></script>
<script src="jspsych-6-2/plugins/jspsych-html-keyboard-response.js"></script>
</head>
<body>
<script src="experiment.js"></script>
</body>
</html>
注:下文将不再重复展示HTML,因为其内容基本不变,只需在<head></head>内部添加<script></script>,导入需要用到的jsPsych插件即可。
现在,我们往JS里面加入“亿点点”细节:
// 定义实验材料(Trial或Block)
var welcome = {
type: "html-keyboard-response",
stimulus:
"<p style='font: bold 42px 微软雅黑; color: #B22222'>
欢迎参与我们的实验</p>
<p style='font: 30px 微软雅黑; color: black'><br/>
<按任意键继续><br/><b>实验过程中请勿退出全屏</b>
<br/><br/></p>
<p style='font: 24px 华文中宋; color: grey'>
中国科学院心理研究所<br/>2020年</p>",
post_trial_gap: 100
};
// 定义实验流程(时间线)
var timeline = [
welcome
];
// 运行实验(总控制)
jsPsych.init({
timeline: timeline,
on_finish: function() {
jsPsych.data.get().localSave("csv", "data.csv"); // download from browser
document.write("<h1 style='text-align:center; height:500pt; line-height:500pt'>实验结束,感谢您的参与!</h1>");
}
});
在这个JS代码中,我们给指导语添加了字体和颜色(通过HTML标签及其属性进行设置),并且对欢迎页设置了trial后的延迟时间(即做完按键反应后,延迟100ms才进入下一步)。同时,在最后的运行模块里,通过on_finish参数,我们添加了两步完成后的操作,包括保存数据到本地(使用“jsPsych.data.get().localSave()”函数)和呈现结束语(使用“document.write()”函数)。
值得一提的是,JavaScript涉及到了「面向对象编程」(Object Oriented Programming)。例如,每个HTML文档都会成为一个“document对象”,而document对象自带很多“属性”(如title、body等)和“方法”(如write()、getElementById()等)。那么我们就可以用英文句点“.”来获取对象内的属性、使用对象内的方法:document.body、document.write()、jsPsych.data.get()……
【关于面向对象编程】
所谓“面向对象编程”,并不是让程序猿坐在他(可能还没有的)“对象”面前枯燥地写代码哦。举个例子,如果想让一只喵星人卖萌,那么……
(1)函数式编程的做法是:beingcute(cat01)
(2)面向对象编程的做法是:cat01.beingcute(),前提是cat01这个对象或者它所属的cat类具有beingcute()这个方法 【关于HTML标签】
(1)<p></p>标签界定了一个段落,字体和颜色都可以在标签中设置,例如 <p style='font: 24px 华文中宋; color: grey'>你的文字内容</p>。
(2)<br/>为HTML里的换行符,相当于其他程序语言里的“n”。
(3)编写刺激材料文本时,可以在行末使用反斜杠“”进行「代码里的换行」(不是真正的换行,不会影响呈现在网页中的效果),这样有利于代码清晰化,也有利于编写多行文本。见上面的例子。
(4)更多关于HTML的知识,请参考本文开篇推荐的HTML教程。
呈现效果:

除了这种方式外,我们也可以通过jsPsych的instructions模块呈现多页指导语,在呈现之前还可以使用call-function模块运行一些代码来改变网页整体的背景色、字体等视觉效果(这种修改对之后呈现的其他内容也依然有效)。
// 使用instructions呈现指导语(可以连续呈现多屏)
var instr = {
type: "instructions",
pages: [
"<p style='text-align: left'>
指导语:<br/>
下面有一系列陈述,<br/>
请表明你对这些陈述的同意程度。<br/><br/>
1 = 非常不同意<br/>
2 = 不同意<br/>
3 = 比较同意<br/>
4 = 不确定<br/>
5 = 比较同意<br/>
6 = 同意<br/>
7 = 非常同意</p>",
],
show_clickable_nav: true,
allow_backward: false,
button_label_previous: "返回",
button_label_next: "继续",
};
// 设置网页整体的背景色、字体等
var set_html_style = {
type: "call-function",
func: function() {
document.body.style.backgroundColor = "rgb(245, 245, 245)"; // "#F0F0F0"
document.body.style.color = "black"; // font color
document.body.style.fontSize = "24px"; // 1px = 0.75pt; px = pt * DPI / 72
document.body.style.fontFamily = "等线";
document.body.style.fontWeight = "bold"; // or "normal"
},
};
// 定义实验流程(时间线)
var timeline = [
set_html_style, // 先设置背景色和字体
instr, // 再呈现指导语
];
// 运行实验(总控制)
jsPsych.init({
timeline: timeline,
on_finish: function() {
jsPsych.data.get().localSave("csv", "data.csv");
}
});
呈现效果:

不同模块既有各自特殊的参数,也有共同的参数(如data、on_start、on_load、on_finish、post_trial_gap;详见Parameters Available in All Plugins - jsPsych)。上面的两个例子中就用到了on_finish和post_trial_gap等共同参数。
9 / 简单入门:调查个人背景变量的几种方式
在问卷调查方面,jsPsych也提供了几个不同的Plugins:
- survey‑multi‑choice(单选题)
- survey‑multi‑select(多选题)
- survey-text(文本填空题)
- survey‑likert(Likert量表)
- survey‑html‑form(自定义HTML表单)
如果你希望在一页中呈现多个题目,那么可以使用上述这些模块,例如
var Sex = {
name: "Sex",
prompt: "你的性别",
options: ["男", "女", "其他"],
horizontal: true,
required: true
};
var Birth = {
name: "Birth",
prompt: "你的出生年代",
options: ["60后", "70后", "80后", "90后", "00后"],
horizontal: false,
required: true
};
var survey_page = {
type: "survey-multi-choice",
preamble: "请先填写基本信息",
questions: [Sex, Birth],
button_label: "下一页"
};
但不推荐这么做,因为这种“一页多题”的方式存在两个问题:一是不够灵活,一页只能放同一类型的题目;二是在最后导出的数据中,多个题目及其答案是以JSON形式存放在同一个单元格里的,提取数据需要额外的操作,这就会给数据处理带来不必要的麻烦!
因此,本文建议:一页只放一题,并且不必拘泥于survey类模块限定好的功能。例如,同样是问性别,我们还可以使用html-button-response模块,以按钮形式呈现选项;同样是问年龄,我们还可以使用survey-html-form模块,然后借助HTML标签<input>规定只能输入范围在16~90之间的两位数字,并设为必答题。
下面是一些实例。
var Sex = {
type: "html-button-response",
data: {varname: "Sex"},
stimulus: "你的性别",
choices: ["男", "女", "其他"]
};
var Age = {
type: "survey-html-form",
data: {varname: "Age"},
preamble: "你的年龄",
html: "<p><inp<p><input name='Q0' type='number' placeholder='16~90'
min=16 max=90 oninput='if(value.length>2) value=value.slice(0,2)'
required /></p>",
button_label: "继续"
};
var Birth = {
type: "survey-html-form",
data: {varname: "Birth"},
preamble: "你的生日",
html: "<p><input name='Q0' type='date' value='2000-01-01' required /></p>",
button_label: "继续"
};
var Email = {
type: "survey-html-form",
data: {varname: "Email"},
preamble: "你的邮箱",
html: "<p><input name='Q0' type='email' /></p>",
button_label: "继续"
};
var School = {
type: "survey-html-form",
data: {varname: "School"},
preamble: "你的学校",
html: "<p><select name='Q0' size=10>
<option>北京大学</option>
<option>清华大学</option>
<option>中国人民大学</option>
<option>北京师范大学</option>
</select></p>",
button_label: "继续"
};
var Language = {
type: "survey-multi-select",
data: {varname: "Language"},
questions: [{
prompt: "你会哪些语言?",
options: ["汉语", "英语", "日语", "韩语", "西班牙语", "其他"],
horizontal: false,
required: false
}],
button_label: "继续"
};
var timeline = [
Sex, Age, Birth, Language, School, Email,
];
jsPsych.init({
timeline: timeline,
on_finish: function() {
jsPsych.data.get().localSave("csv", "data.csv");
}
});
(上述代码有所简化,完整版请见GitHub:experiment.js)
10 / 技能提升:呈现Likert量表的几种方式
呈现Likert量表也有几种不同的方式,它们各有优劣。
(1)survey-likert模块
【特点】一页多题;最终数据存储在同一行,而非一题一行,给数据处理添麻烦;默认样式的选项按钮太小,被试作答比较麻烦,很考验被试的耐心。
// 全局变量:Likert量表标签
var likert_7 = ["1", "2", "3", "4", "5", "6", "7"];
// 以生活满意度量表(SWLS)为例
var SWLS = {
type: "survey-likert",
preamble: "请表明你对下列陈述的同意程度<br/>
(1 = 非常不同意,7 = 非常同意)",
questions: [
{name: "SWLS1", labels: likert_7, prompt: "我的生活在大多数情况下接近我的理想状态"},
{name: "SWLS2", labels: likert_7, prompt: "我的生活条件非常好"},
{name: "SWLS3", labels: likert_7, prompt: "我对我的生活感到满意"},
{name: "SWLS4", labels: likert_7, prompt: "目前为止我已经得到了生活中我想得到的重要东西"},
{name: "SWLS5", labels: likert_7, prompt: "如果生活可以重来,我还愿意过现在这样的生活"},
],
randomize_question_order: false,
button_label: "继续"
};
(2)html-slider-response模块(单题或多题嵌套于timeline内)
【特点】一页一题;滑动条形式;必须设置起始点;每做完一题要手动“继续”。
// 例子:单题量表(名字偏好)
var NameLiking = {
type: "html-slider-response",
data: {varname: "NameLiking"},
stimulus: "总体而言,你在多大程度上喜欢自己的名字?<br/>(1 = 非常不喜欢,9 = 非常喜欢)",
labels: ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
min: 1, max: 9, start: 5,
button_label: "继续",
require_movement: true
};
// 例子:多题量表(生活满意度)
var SWLS = {
timeline_variables: [
{data: {i: 1}, s: "我的生活在大多数情况下接近我的理想状态"},
{data: {i: 2}, s: "我的生活条件非常好"},
{data: {i: 3}, s: "我对我的生活感到满意"},
{data: {i: 4}, s: "目前为止我已经得到了生活中我想得到的重要东西"},
{data: {i: 5}, s: "如果生活可以重来,我还愿意过现在这样的生活"},
],
timeline: [{
type: "html-slider-response",
data: jsPsych.timelineVariable("data"),
stimulus: jsPsych.timelineVariable("s"),
labels: ["1", "2", "3", "4", "5", "6", "7"],
min: 1, max: 7, start: 1,
prompt: "<p style='font-size: 20px; font-weight: normal'>
请表明你对该陈述的同意程度<br/>
(1 = 非常不同意,7 = 非常同意)</p>",
button_label: "继续",
require_movement: true,
post_trial_gap: 50
}],
randomize_order: false
};
(3)html-button-response模块(单题或多题嵌套于timeline内)
【特点】一页一题;按钮形式;每做完一题直接进入下一题,被试方便;数据中每题存储为单独一行,数据处理方便。
// 自定义函数:将Button序号转换为Likert量表得分
function addRespFromButtonScale(data, scale_name) {
data.scale = scale_name;
data.varname = scale_name + data.i;
data.response = parseInt(data.button_pressed) + 1; // raw: 0, 1, 2, ...
}
// 例子:多题量表(生活满意度)
var SWLS = {
timeline_variables: [
{data: {i: 1}, s: "我的生活在大多数情况下接近我的理想状态"},
{data: {i: 2}, s: "我的生活条件非常好"},
{data: {i: 3}, s: "我对我的生活感到满意"},
{data: {i: 4}, s: "目前为止我已经得到了生活中我想得到的重要东西"},
{data: {i: 5}, s: "如果生活可以重来,我还愿意过现在这样的生活"},
],
timeline: [{
type: "html-button-response",
data: jsPsych.timelineVariable("data"),
stimulus: jsPsych.timelineVariable("s"),
choices: ["1", "2", "3", "4", "5", "6", "7"],
prompt: "<p style='font-size: 20px; font-weight: normal'>
请表明你对该陈述的同意程度<br/>
(1 = 非常不同意,7 = 非常同意)</p>",
button_html: "<p><button class='jspsych-btn' style='font-size: 20px; font-weight: normal'>%choice%</button></p>",
on_finish: function(data) { addRespFromButtonScale(data, "SWLS"); },
post_trial_gap: 50
}],
randomize_order: false
};
经过比较和体验,第3种方式更合理,因此推荐使用第3种呈现Likert量表。
11 / 技能提升:编写按键反应程序的几种方式
相比于普通的问卷调查和Likert量表,按键反应实验范式种类繁多,实现方式也更灵活,我们需要对jsPsych不同模块以及JavaScript语言有一个更全面的了解。jsPsych官网已经提供了大量的例子,jspsychr包自动生成的Rmd模板也提供了一个Stroop任务的例子。除了最基础的html-keyboard-response模块,jsPsych还为一些常见的按键反应任务设计了专门的模块:
- categorize-html
- categorize-image
- categorize-animation
- iat-html
- iat-image
- same-different-html
- same-different-image
- serial-reaction-time
- serial-reaction-time-mouse
- visual-search-circle
接下来,我们以测量内隐态度的经典范式“外在情感性西蒙任务(EAST)”为例,展示一个完整的按键反应程序(主要用到html-keyboard-response模块和categorize-html模块)。对该范式的原理感兴趣的读者,可以自行搜索相关文献学习,这里就不跑题了。
代码详见:experiment.js
呈现效果:






12 / 拓展边界:小括号、中括号、大括号暗藏的玄机
在编写JavaScript程序时,我们经常会和小括号、中括号、大括号打交道。那么,这三者到底有什么讲究呢?
- 小括号 ( ) 界定的是函数及其参数
- 例:jsPsych.data.get().localSave("csv", "data_exp_demo.csv")
- 中括号 [ ] 界定的是Array序列
- 例:var likert_5 = ["1", "2", "3", "4", "5"];
- 例:var timeline = [block1, block2, block3];
- 例:choices: ["f", "j"]
- 大括号 { } 界定的是JSON数据或JS函数代码块
- 例:var fixation = {type: "html-keyboard-response", stimulus: "+", choices: jsPsych.NO_KEYS};
- 例:on_finish: function(data) { addRespFromButton(data); }
( ) 内的多个参数之间、[ ] 内的多个元素之间、{ } 内的多个键值对之间,都需要用英文逗号进行分隔。最后一项后面的逗号可加可不加,但JavaScript脚本和自定义函数内部的每一行后面最好都加一个英文分号。
例如:
var set_html_style = {
type: "call-function", // 这里的逗号一定要加
func: function() {
// 函数内部的每一行后面都要加分号(虽然不加也没事)
document.body.style.backgroundColor = "rgb(245, 245, 245)";
document.body.style.color = "black";
document.body.style.fontSize = "24px";
document.body.style.fontFamily = "等线";
document.body.style.fontWeight = "bold";
}, // 这里的逗号可加可不加(因为已经是JSON数据的最后一项)
}; // 这里的分号一定要加(虽然不加也没事)
13 / 拓展边界:自编JavaScript函数满足个性化需求
虽然jsPsych已经提供了非常丰富的脚本库/拓展插件,但JavaScript的“坑”很大,借助原生JavaScript的强大功能,我们可以优化实验程序、实现更复杂的功能、满足个性化的需求。
例如,我们想给实验程序加一个倒计时页面,实现阅读指导语至少10秒才能进入下一页(按钮从不可用变为可用),可以怎么做?示例代码如下:
function timer() {
var second = document.getElementById("timer");
var button = document.getElementsByClassName("jspsych-btn")[0];
if(second.innerText > 1) {
second.innerText = second.innerText - 1;
} else {
button.innerText = "继续";
button.disabled = false;
}
}
var btn_timer_html = "<style onload='setInterval(timer, 1000)'></style><button class='jspsych-btn' style='font: normal 20px 等线' disabled=true>%choice%</button>";
var warmup = {
type: "html-button-response",
stimulus: "<p>请做好准备……</p>",
choices: ["<span id='timer'>10</span>秒后可继续"],
button_html: btn_timer_html
};
呈现效果:

又如,我们想给滑动条量表添加一个实时刻度反馈,实现每滑动选择一次就在滑动条下方实时反馈相应的刻度数值,可以怎么做?示例代码如下:
function setSliderAttr(event="onmouseup") {
document.getElementById("jspsych-html-slider-response-response").setAttribute(event, "addSliderValue()");
}
function addSliderValue(element_id="slider-value") {
document.getElementById(element_id).innerHTML = document.getElementById("jspsych-html-slider-response-response").value;
document.getElementById("jspsych-html-slider-response-next").disabled = false;
}
var NameLiking = {
type: "html-slider-response",
data: {varname: "NameLiking"},
on_load: function() { setSliderAttr(); },
stimulus: "总体而言,你在多大程度上喜欢自己的名字?<br/>(1 = 非常不喜欢,9 = 非常喜欢)",
labels: ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
min: 1, max: 9, start: 5,
prompt: "<b id='slider-value'>_</b><br/><br/>",
button_label: "继续",
require_movement: true
};
呈现效果:

当然,没有人拍拍脑袋就能知道这些解决办法——我们一方面要借助搜索引擎、各类教程、参考手册来学习借鉴已有的代码,另一方面要查看jsPsych不同模块的.js源代码来了解其底层构建过程,从而实现灵活运用!
14 / 总结
读到这里,你可能会再次产生疑惑:为什么要学jsPsych,甚至JavaScript编程呢?它到底有什么优势?如果只是发问卷,用问卷星或Qualtrics它不香吗?——没错,很香。如果你的需求是问卷调查,或是仅涉及随机分配的简单实验,不涉及按键反应或更为复杂的实验,那么用问卷星或Qualtrics当然绰绰有余,而且他们作为成熟平台,已经提供了非常完善的功能(还不用学编程)。
相比之下,jsPsych最大的优势在于可以更灵活地设计问卷和实验,并且支持实验程序的本地和网络运行。与学习E-Prime、Inquisit等专门化且收费的软件相比,学习普遍适用且免费的HTML和JavaScript(以及jsPsych脚本库)具有几乎一样的时间成本但明显更高的学习回报——毕竟HTML和JavaScript是跨领域通用的语言,应用广泛,甚至Qualtrics问卷平台也提供了JavaScript拓展功能(Qualtrics JavaScript Question API)。
本系列文章(上、下两篇)提供了一个初步但相对全面的jsPsych实验与问卷编程指南。篇幅有限,抛砖引玉。希望能激发各位读者的兴趣,对各位读者有帮助!