如何在 JavaScript 中實作間隔重複演算法 SM-2
How to Implement Spaced Repetition (SM-2) in JavaScript
Spartan Rich 示範如何用 JavaScript 實作 SM-2 間隔重複排程,按用戶的回憶評分更新 repetitions、interval 和 easeFactor,並計算下次複習日期。文中説明評分為 0 至 5,並提醒評分按鈕設計、逾期複習及用戶時區是實際部署時需要處理的問題。
I spent a weekend convinced I could write a better review scheduler than the one Piotr Wozniak published in 1987. I couldn't. What I ended up with was a cleaner understanding of SM-2, the algorithm behind SuperMemo and, for most of its life, Anki. If you're adding any kind of "review this later" feature to an app, it's the place to start.
This post walks through SM-2 in plain JavaScript, points out the parts that trip people up, and covers what I'd change before shipping it to real users.
The idea in one paragraph
Hermann Ebbinghaus measured his own forgetting in the 1880s and found that memory drops fast after you learn something, then levels off. Each time you successfully recall it, the drop gets slower. Spaced repetition takes advantage of that: review an item right before you'd forget it, and push the next review further out every time you get it right. SM-2 is a small set of rules for deciding how far out "further" is.
What each card needs to store
Every item you schedule carries three numbers:
-
repetitions: how many times in a row it's been recalled correctly -
interval: days until the next review -
easeFactor: how quickly the interval grows, starting at 2.5
Plus a due date, which you calculate from the interval.
After each review the user grades their recall from 0 to 5. In the original paper 5 means perfect recall, 3 means correct but hard, and anything below 3 counts as a failure.
The code
function sm2(card, quality) {
let { repetitions, interval, easeFactor } = card;
if (quality >= 3) {
if (repetitions === 0) {
interval = 1;
} else if (repetitions === 1) {
interval = 6;
} else {
interval = Math.round(interval * easeFactor);
}
repetitions += 1;
} else {
repetitions = 0;
interval = 1;
}
easeFactor =
easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
if (easeFactor < 1.3) easeFactor = 1.3;
const due = new Date();
due.setDate(due.getDate() + interval);
return { repetitions, interval, easeFactor, due };
}
const fresh = { repetitions: 0, interval: 0, easeFactor: 2.5 };
let card = sm2(fresh, 4); // due tomorrow
card = sm2(card, 4); // due in 6 days
card = sm2(card, 5); // due in about 15 days
That's the whole algorithm. Twenty-odd lines.
The ease factor formula, decoded
The ease update looks like line noise, so plug in numbers. A grade of 5 adds 0.1. A grade of 4 adds nothing. A 3 subtracts 0.14. A 0 subtracts 0.8, though the floor of 1.3 stops it from going lower than that.
So a card you keep finding hard grows its interval slowly, around 1.3x per review at worst. A card you always nail grows faster and faster. The 1.3 floor matters more than it looks. Without it, a card you failed a few times would end up with an ease near zero and get shown to you every day forever.
Three things that bite you in production
Grades. Nobody wants six buttons. Anki settled on four (Again, Hard, Good, Easy), and most apps go even simpler. If you map "Again / Good / Easy" onto 1 / 4 / 5, the algorithm still behaves sensibly. I'd avoid letting users pick 0 through 5 directly. They'll use 3 and 5 and nothing in between.
Late reviews. SM-2 assumes people review on the due date. They don't. If a card was due 10 days ago and the user still remembers it, that's a stronger signal than an on-time success, but plain SM-2 ignores it. A common patch is to compute the next interval from the actual days elapsed instead of the scheduled interval when the answer is correct. Small change, much better behaviour for people who go on holiday.
Timezones. I set due with setDate above to keep the demo short. In a real app, store due dates as calendar days in the user's timezone, or someone in Kolkata will get tomorrow's cards at 5:30 in the morning and wonder what broke. I've shipped that bug. It's embarrassing.
Is SM-2 still worth using?
For a side project or an MVP, yes. It's predictable, it's easy to debug, and you can explain it to a user in two sentences.
It isn't state of the art anymore, though. Anki added FSRS (Free Spaced Repetition Scheduler) as an option in version 23.10. It fits a memory model to each user's actual review history instead of using fixed constants. If your app ends up with lots of review data per user, FSRS is worth a look. If you're starting from zero data, SM-2 gets you shipping this week.
The harder problem sits upstream of the scheduler
The scheduler turned out to be the easy part. The hard part was deciding what to put on the card in the first place.
Most of what I want to remember doesn't arrive as flashcards. It comes from a YouTube talk, a podcast episode, a long article, a reel someone sent me. Turning a 40-minute video into three or four reviewable ideas is real work, and most people (me included) never do it. So the videos sit in Watch Later and the scheduler has nothing to schedule.
That's the gap Recl is built around. You save videos, reels, podcasts or articles, it summarizes them, sorts them into topics, and then uses spaced repetition to bring the key points back before they fade. The scheduling logic is the same family of ideas as the code above. The difference is that the cards come from what you already consume, so you don't have to write them yourself.

A saved article turned into a short summary card in Recl's feed.
If you're building something similar, my advice is to spend less time tuning the interval math and more time on getting content into the system with as little friction as possible. A perfect scheduler with an empty queue helps nobody.
Try it yourself
Copy the function above, make a dozen cards from something you read this week, and grade yourself honestly for two weeks. You'll get a feel for the intervals faster than from any paper. And if you'd rather the cards wrote themselves from the stuff you save, give Recl a try.
I'm Spartan Rich, and I build Recl, an AI learning app that turns saved videos, podcasts and articles into summaries and spaced-repetition reviews.
來源:DEV Community · dev.to