すいません分かりました。

String.match は RegExp[@@match] を読んでて、その中で RegExp.exec を呼んでいる。
RegExp.exec は当然そのまま RegExp.exec を呼んでいる。
だからそのままだと当然挙動は同じになるのだけど、String.matchの場合、
> 4. If matchStr is the empty String, then
> a. Let thisIndex be ? ToLength(? Get(rx, "lastIndex")).
> b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
> c. Perform ? Set(rx, "lastIndex", nextIndex, true).
https://tc39.es/ecma262/#sec-regexpbuiltinexec
って書いてあったわ。
つまりマッチ結果が空文字だと必ず一文字進めるようになっており、「文頭マッチのフラグを落とす」仕様にはなってない。
この仕様でいいのか?という気もするが、とにかくそうなっているようだ。
お騒がせしました。