◆ ▶ でオブジェクト内の表示切り替え
  ◆ オブジェクト内の値は最初の表示時の値
◆ getter/setter も見れる
◆ 関数は常に固定表示
◆ 値型ごとに色分け
◆ 循環参照対応
◆ 大きな配列の分割表示なし
◆ JavaScript で取得できない部分は表示不可

コンソールだとオブジェクトの中身を詳細に確認できます

chrome-console

「▶」をクリックして内部データをいろいろ見れます
画面上で JavaScript を実行できるツールを作っているとそういう表示を画面上でしたくなります
普通にウェブページ上のツールを使っていて毎回コンソール開かないといけないのってなんか違いますからね

簡単なものなら JSON 出力で対応できますが もうちょっといろいろ情報がほしいです
あと自分で 「▶」 をクリックして必要なところだけ展開していける機能があると見やすいです

ライブラリ探してみても JSON を折りたたみできるようなのはありましたが 目的のものは見当たりませんでした
ということで作ったのがこれです

コード

コードはかなり雑なものです
今度 WebComponents を使って書き直そうかな と思ってるのでそのときに整理するつもりです

<!doctype html>
<meta charset="utf-8" />

<div id="target"></div>

<style>
.preview {
font-size: 12px;
line-height: 18px;
font-family: consolas;
color: #444;
padding-left: 16px;
position: relative;
}

.subpreview {
padding-left: 16px;
position: relative;
}

[data-has-inner]::before {
content: "▶";
font-size: 10px;
position: absolute;
left: 0;
display: block;
}

[data-has-inner][data-open]::before {
transform: rotate(90deg);
}

.preview-inner {
display: none;
}

[data-open]>.preview-inner {
display: block;
}

[data-type="number"] .preview-value {
color: blue;
}

[data-type="string"] .preview-value {
color: crimson;
}

[data-type="boolean"] .preview-value {
color: purple;
}

[data-type="symbol"] .preview-value {
color: green;
}

[data-type="undefined"] .preview-value {
color: gray;
}

#target .preview {
border-bottom: 1px solid #aaa;
}
</style>

<script>
const text = s => ({ text: s })
const html = h => ({ html: h })

const createPart = (tpls, ...values) => {
const escape = s => s.replace(/<>&"/g, m => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;" }[m]))
const htmlstr = v => (v ? (v.text ? escape(v.text) : v.html ? v.html : "") : "")
const part_html = tpls.reduce((a, n, i) => {
return a + htmlstr(values[i - 1]) + n
})
const t = document.createElement("template")
t.innerHTML = part_html
return t.content
}

//

const preview = value => {
const part = createPart`
<div class="preview" data-type="${text(typeof value)}" ${value instanceof Object ? text("data-has-inner") : text("")}>
<span class="preview-inline"><span class="preview-value">${inline(value)}</span></span>
</div>
`
part.querySelector(".preview-inline").ref = value
return part
}

const subpreview = (key, value) => {
const part = createPart`
<div class="subpreview" data-type="${text(typeof value)}" ${value instanceof Object ? text("data-has-inner") : text("")}>
<span class="preview-inline"><span class="preview-key">${text(key)}: </span><span class="preview-value">${inline(value)}</span>
</div>
`
part.querySelector(".preview-inline").ref = value
return part
}

const inline = value => {
if (value instanceof Object) {
if (typeof value === "function") {
return text("ƒ(){}")
} else if (Array.isArray(value)) {
return text(inlineArrayPreview(value))
} else {
return text(inlineObjPreview(value))
}
} else {
return text(String(value))
}
}

const inlineArrayPreview = arr => {
const result = []
const valuePreview = i => {
if (i in arr) {
const v = arr[i]
if (v instanceof Object) {
if (typeof v === "function") {
return "ƒ(){}"
} else if (Array.isArray(v)) {
return v.length ? "[...]" : "[ ]"
} else {
return Object.keys(v).length ? "{...}" : "{ }"
}
} else {
return String(v)
}
} else {
return ""
}
}
for (let i = 0; i < Math.min(arr.length, 5); i++) {
result.push(valuePreview(i))
}
if (arr.length > 5) {
result.push("...")
}

return "[" + result.join(", ") + "]"
}

const inlineObjPreview = obj => {
const ctor = obj.constructor
const name = ctor ? ctor.name : ""
const valuePreview = v => {
if (v instanceof Object) {
if (typeof v === "function") {
return "ƒ(){}"
} else if (Array.isArray(v)) {
return v.length ? "[...]" : "[ ]"
} else {
return Object.keys(v).length ? "{...}" : "{ }"
}
} else {
return String(v)
}
}
const inner = Object.entries(obj).slice(0, 3).map(([k, v]) => `${k}: ${valuePreview(v)}`).join(", ")

return `${name} {${inner}}`
}

const inner = value => {
if (!(value instanceof Object)) {
return []
}
const descs = Object.getOwnPropertyDescriptors(value)
const proto_entry = ["__proto__", { value: value.__proto__ }]
return [...Object.entries(descs), proto_entry].flatMap(([k, d]) => {
const result = []
if ("value" in d || d.get) {
result.push(subpreview(k, value[k]))
}
if (d.get) {
result.push(subpreview("get " + k, d.get))
}
if (d.set) {
result.push(subpreview("set " + k, d.set))
}
return result
})
}

target.addEventListener("click", eve => {
const pv = eve.target.closest(".preview-inline")
if (!pv) return
if (!pv.nextElementSibling) {
const part = createPart`<div class="preview-inner"></div>`
part.querySelector(".preview-inner").append(...inner(pv.ref))
pv.after(part)
}
const preview = pv.parentElement
if (preview.dataset.hasInner != null) {
preview.toggleAttribute("data-open")
}
})

target.append(preview(1))
target.append(preview("aa"))
target.append(preview(true))
target.append(preview(Symbol("s")))
target.append(preview(null))
target.append(preview())
target.append(preview({ a: 1, b: 2 }))
target.append(preview({ a: "a", b: [1, 2, { x: true, y: null }, [3]] }))
target.append(preview({ a: 10, get b() { return 2 }, set c(value) {} }))
target.append(preview([1, 2, 3]))
target.append(preview(function() {}))
target.append(preview(new RegExp("a", "g")))
target.append(preview(new class A { m() {} }()))
</script>

サンプル

⇩の結果です

preview(1)
preview("aa")
preview(true)
preview(Symbol("s"))
preview(null)
preview()
preview({ a: 1, b: 2 })
preview({ a: "a", b: [1, 2, { x: true, y: null }, [3]] })
preview({ a: 10, get b() { return 2 }, set c(value) {} })
preview([1, 2, 3])
preview(function() {})
preview(new RegExp("a", "g"))
preview(new class A { m() {} }())

dump-example

作ってみてわかったのですが constructor と prototype など循環参照するものがあるので事前に DOM に完全な状態で用意しておくことができません
クリック時にその中身を動的に作るという処理になります
その結果 Chrome のコンソールのように 「▶」 を開いたタイミングの値になります
開く前に更新されていれば更新後のものになるので ダンプ出力したタイミングとは異なります

一応 getter/setter も対応してます
Chrome だと配列が多いと展開時に分割されますが その機能は入れてないので全部表示されます
Promise の状態など JavaScript 取得できないものは表示できません