feat(docs): add JavaScript introduction components and content
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,766 @@
|
||||
<template>
|
||||
<div class="closure-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🎁</span>
|
||||
<span class="title">函数与闭包</span>
|
||||
<span class="subtitle">理解作用域链和闭包机制</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你有个<span class="highlight">背包</span>(函数),每次出门时都会把当时看到的
|
||||
<span class="highlight">风景</span>(外部变量)装进去。
|
||||
<span class="highlight">闭包</span>就是这个背包——即使离开了那个地方,你依然能拿出当时装的风景
|
||||
</div>
|
||||
|
||||
<div class="demo-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === tab.id }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 函数基础 -->
|
||||
<div v-if="activeTab === 'basic'" class="tab-content">
|
||||
<div class="function-showcase">
|
||||
<div class="code-panel">
|
||||
<div class="code-title">函数声明方式</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line comment">// 1. 函数声明</div>
|
||||
<div class="code-line">function greet(name) {</div>
|
||||
<div class="code-line indent">return "Hello " + name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// 2. 函数表达式</div>
|
||||
<div class="code-line">const greet = function(name) {</div>
|
||||
<div class="code-line indent">return "Hello " + name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// 3. 箭头函数 (ES6)</div>
|
||||
<div class="code-line">const greet = (name) => {</div>
|
||||
<div class="code-line indent">return "Hello " + name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// 简化版(单行可省略 return)</div>
|
||||
<div class="code-line">const greet = name => "Hello " + name</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playground">
|
||||
<div class="playground-title">试试调用函数</div>
|
||||
<div class="input-group">
|
||||
<input v-model="functionName" placeholder="输入你的名字" />
|
||||
<button @click="callFunction">调用</button>
|
||||
</div>
|
||||
<div class="output">
|
||||
<span v-if="functionResult" class="result">{{ functionResult }}</span>
|
||||
<span v-else class="placeholder">点击"调用"按钮看结果...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 闭包演示 -->
|
||||
<div v-else-if="activeTab === 'closure'" class="tab-content">
|
||||
<div class="closure-visual">
|
||||
<div class="scenario-selector">
|
||||
<button @click="closureScenario = 'counter'" :class="{ active: closureScenario === 'counter' }">
|
||||
计数器
|
||||
</button>
|
||||
<button @click="closureScenario = 'config'" :class="{ active: closureScenario === 'config' }">
|
||||
配置器
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="closureScenario === 'counter'" class="counter-demo">
|
||||
<div class="code-panel small">
|
||||
<div class="code-line">function createCounter() {</div>
|
||||
<div class="code-line indent">let count = 0 <span class="comment">// 私有变量</span></div>
|
||||
<div class="code-line indent">return function() {</div>
|
||||
<div class="code-line indent indent">count++</div>
|
||||
<div class="code-line indent indent">return count</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">const counter = createCounter()</div>
|
||||
</div>
|
||||
|
||||
<div class="closure-animation">
|
||||
<div class="closure-box">
|
||||
<div class="box-title">闭包环境</div>
|
||||
<div class="closure-var">
|
||||
<span class="var-label">count = </span>
|
||||
<span class="var-value">{{ counterValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls-area">
|
||||
<button @click="incrementCounter" class="action-btn primary">
|
||||
调用 counter()
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="explanation">
|
||||
<p><strong>发生了什么?</strong></p>
|
||||
<ul>
|
||||
<li><code>createCounter()</code> 执行后,局部变量 <code>count</code> 本该消失</li>
|
||||
<li>但返回的函数"记住"了这个变量(形成了闭包)</li>
|
||||
<li>每次调用 <code>counter()</code> 都在访问同一个 <code>count</code></li>
|
||||
<li>外部无法直接访问 <code>count</code>(实现了数据私有化)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="config-demo">
|
||||
<div class="code-panel small">
|
||||
<div class="code-line">function makeMultiplier(times) {</div>
|
||||
<div class="code-line indent">return function(n) {</div>
|
||||
<div class="code-line indent indent">return n * times</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">const double = makeMultiplier(2)</div>
|
||||
<div class="code-line">const triple = makeMultiplier(3)</div>
|
||||
</div>
|
||||
|
||||
<div class="multiplier-playground">
|
||||
<div class="function-list">
|
||||
<div class="func-item" @click="activeMultiplier = 'double'" :class="{ active: activeMultiplier === 'double' }">
|
||||
<div class="func-name">double = makeMultiplier(2)</div>
|
||||
<div class="func-desc">闭包捕获 times = 2</div>
|
||||
</div>
|
||||
<div class="func-item" @click="activeMultiplier = 'triple'" :class="{ active: activeMultiplier === 'triple' }">
|
||||
<div class="func-name">triple = makeMultiplier(3)</div>
|
||||
<div class="func-desc">闭包捕获 times = 3</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="multiplier-input">
|
||||
<input v-model.number="multiplyNumber" type="number" placeholder="输入数字" />
|
||||
<button @click="doMultiply">计算</button>
|
||||
</div>
|
||||
|
||||
<div v-if="multiplyResult" class="multiply-result">
|
||||
<span class="result-equation">{{ multiplyResult }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作用域链 -->
|
||||
<div v-else class="tab-content">
|
||||
<div class="scope-chain-demo">
|
||||
<div class="nested-visual">
|
||||
<div class="scope-level global">
|
||||
<div class="level-title">全局作用域</div>
|
||||
<div class="level-vars">
|
||||
<span class="var-tag">globalVar = "全局"</span>
|
||||
</div>
|
||||
|
||||
<div class="scope-level outer">
|
||||
<div class="level-title">外层函数作用域</div>
|
||||
<div class="level-vars">
|
||||
<span class="var-tag">outerVar = "外层"</span>
|
||||
</div>
|
||||
|
||||
<div class="scope-level inner">
|
||||
<div class="level-title">内层函数作用域</div>
|
||||
<div class="level-vars">
|
||||
<span class="var-tag">innerVar = "内层"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lookup-demo">
|
||||
<div class="lookup-title">🔍 变量查找过程(作用域链)</div>
|
||||
<div class="lookup-steps">
|
||||
<div class="lookup-step" v-for="(step, i) in lookupSteps" :key="i">
|
||||
<div class="step-num">{{ i + 1 }}</div>
|
||||
<div class="step-content">{{ step }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lookup-rule">
|
||||
<strong>查找规则:</strong>
|
||||
从当前作用域开始,逐层向外查找,直到全局作用域。找不到则报错 ReferenceError。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="activeTab === 'basic'">函数是 JavaScript 中的一等公民,可以赋值给变量、作为参数传递、作为返回值。箭头函数更简洁,且不绑定自己的 this。</span>
|
||||
<span v-else-if="activeTab === 'closure'">闭包是函数和声明该函数的词法环境的组合。它让函数可以访问外部作用域的变量,即使外部函数已经执行完毕。闭包常用于数据私有化、函数工厂、模块化等场景。</span>
|
||||
<span v-else>作用域链是 JavaScript 查找变量的机制。当访问一个变量时,引擎会先在当前作用域查找,找不到就去外层作用域找,直到全局作用域。这种机制让内层函数可以访问外层变量,形成了闭包的基础。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeTab = ref('basic')
|
||||
const functionName = ref('')
|
||||
const functionResult = ref('')
|
||||
const counterValue = ref(0)
|
||||
const closureScenario = ref('counter')
|
||||
const activeMultiplier = ref('double')
|
||||
const multiplyNumber = ref(null)
|
||||
const multiplyResult = ref('')
|
||||
|
||||
const tabs = [
|
||||
{ id: 'basic', label: '函数基础' },
|
||||
{ id: 'closure', label: '闭包' },
|
||||
{ id: 'scope', label: '作用域链' }
|
||||
]
|
||||
|
||||
const lookupSteps = ref([
|
||||
'内层函数访问 innerVar → 在当前作用域找到 ✓',
|
||||
'内层函数访问 outerVar → 当前找不到,向外层查找 ✓',
|
||||
'内层函数访问 globalVar → 继续向外,在全局作用域找到 ✓',
|
||||
'内层函数访问 unknownVar → 所有作用域都找不到 ✗ ReferenceError'
|
||||
])
|
||||
|
||||
const callFunction = () => {
|
||||
if (functionName.value.trim()) {
|
||||
functionResult.value = `Hello ${functionName.value}`
|
||||
}
|
||||
}
|
||||
|
||||
const incrementCounter = () => {
|
||||
counterValue.value++
|
||||
}
|
||||
|
||||
const doMultiply = () => {
|
||||
if (multiplyNumber.value !== null) {
|
||||
const times = activeMultiplier.value === 'double' ? 2 : 3
|
||||
multiplyResult.value = `${multiplyNumber.value} × ${times} = ${multiplyNumber.value * times}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.closure-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.function-showcase {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.code-panel {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.code-panel.small {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.code-title {
|
||||
color: #888;
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
padding: 0.1rem 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.code-line.indent {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.code-line.indent.indent {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
.code-line .comment {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.code-line :deep(code) {
|
||||
background: #333;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.playground {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.playground-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.input-group button {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.output {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
min-height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.output .result {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.output .placeholder {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.scenario-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.scenario-selector button {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.scenario-selector button:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scenario-selector button.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.closure-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.counter-demo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.closure-animation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.closure-box {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.closure-var {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.var-label {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.var-value {
|
||||
color: var(--vp-c-brand);
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.controls-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.explanation p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.explanation ul {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.explanation code {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.config-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.multiplier-playground {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.function-list {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.func-item {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.func-item:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.func-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.func-name {
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.func-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.multiplier-input {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.multiplier-input input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.multiplier-input button {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.multiply-result {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-equation {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scope-chain-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nested-visual {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.scope-level {
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scope-level.global {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.scope-level.outer {
|
||||
background: #e8f5e9;
|
||||
margin: 0.5rem 0 0.5rem 0.5rem;
|
||||
border-color: #c8e6c9;
|
||||
}
|
||||
|
||||
.scope-level.inner {
|
||||
background: #e3f2fd;
|
||||
margin: 0.5rem 0 0 0.5rem;
|
||||
border-color: #bbdefb;
|
||||
}
|
||||
|
||||
.level-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.level-vars {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.var-tag {
|
||||
background: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.lookup-demo {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.lookup-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.lookup-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.lookup-step {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lookup-rule {
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.function-showcase {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.counter-demo {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,620 @@
|
||||
<template>
|
||||
<div class="data-type-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🏷️</span>
|
||||
<span class="title">JavaScript 数据类型</span>
|
||||
<span class="subtitle">原始类型 vs 引用类型</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你在外面<span class="highlight">租了个储物柜</span>:
|
||||
<span class="highlight">原始类型</span>像是把东西直接拿回家(复制一份);
|
||||
<span class="highlight">引用类型</span>像是只拿了张写着地址的小纸条(共享同一个位置)
|
||||
</div>
|
||||
|
||||
<div class="type-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === tab.id }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<!-- 原始类型 -->
|
||||
<div v-if="activeTab === 'primitive'" class="primitive-types">
|
||||
<div class="type-grid">
|
||||
<div
|
||||
v-for="type in primitiveTypes"
|
||||
:key="type.name"
|
||||
class="type-card"
|
||||
@click="selectedType = type"
|
||||
:class="{ selected: selectedType?.name === type.name }"
|
||||
>
|
||||
<div class="type-icon">{{ type.icon }}</div>
|
||||
<div class="type-name">{{ type.name }}</div>
|
||||
<div class="type-example">{{ type.example }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedType" class="type-detail">
|
||||
<div class="detail-title">📝 {{ selectedType.name }} 详细说明</div>
|
||||
<div class="detail-desc">{{ selectedType.description }}</div>
|
||||
<div class="detail-note">
|
||||
<strong>💡 关键特性:</strong>{{ selectedType.note }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引用类型 -->
|
||||
<div v-else-if="activeTab === 'reference'" class="reference-types">
|
||||
<div class="comparison-box">
|
||||
<div class="compare-side">
|
||||
<div class="side-title">原始类型赋值</div>
|
||||
<div class="code-example">
|
||||
<div class="code-line">let a = 10</div>
|
||||
<div class="code-line">let b = a</div>
|
||||
<div class="code-line">b = 20</div>
|
||||
<div class="code-line result">// a = 10 (不变)</div>
|
||||
</div>
|
||||
<div class="visual-box">
|
||||
<div class="value-box">a = 10</div>
|
||||
<div class="arrow">复制</div>
|
||||
<div class="value-box">b = 20</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="compare-side">
|
||||
<div class="side-title">引用类型赋值</div>
|
||||
<div class="code-example">
|
||||
<div class="code-line">let obj1 = {x: 10}</div>
|
||||
<div class="code-line">let obj2 = obj1</div>
|
||||
<div class="code-line">obj2.x = 20</div>
|
||||
<div class="code-line result">// obj1.x = 20 (变了!)</div>
|
||||
</div>
|
||||
<div class="visual-box">
|
||||
<div class="ref-box">
|
||||
<div>obj1 →</div>
|
||||
<div class="memory-box">{x: 20}</div>
|
||||
</div>
|
||||
<div class="arrow">指向同一位置</div>
|
||||
<div class="ref-box">
|
||||
<div>obj2 →</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ref-types-list">
|
||||
<div class="ref-type-item" v-for="type in referenceTypes" :key="type.name">
|
||||
<div class="ref-icon">{{ type.icon }}</div>
|
||||
<div class="ref-info">
|
||||
<div class="ref-name">{{ type.name }}</div>
|
||||
<div class="ref-desc">{{ type.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 类型转换 -->
|
||||
<div v-else class="type-conversion">
|
||||
<div class="conversion-playground">
|
||||
<div class="input-section">
|
||||
<label>输入一个值:</label>
|
||||
<input v-model="inputValue" type="text" placeholder="试试输入 '123' 或 'hello'" @keyup.enter="convertType" />
|
||||
<button @click="convertType" class="convert-btn">转换</button>
|
||||
</div>
|
||||
|
||||
<div class="results-section">
|
||||
<div class="result-row">
|
||||
<span class="result-label">String():</span>
|
||||
<span class="result-value">{{ conversionResults.string }}</span>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="result-label">Number():</span>
|
||||
<span class="result-value" :class="{ error: conversionResults.number === 'NaN' }">
|
||||
{{ conversionResults.number }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="result-label">Boolean():</span>
|
||||
<span class="result-value">{{ conversionResults.boolean }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="falsy-values">
|
||||
<div class="falsy-title">⚠️ 转成 false 的值(falsy values):</div>
|
||||
<div class="falsy-list">
|
||||
<span v-for="val in falsyValues" :key="val" class="falsy-item">{{ val }}</span>
|
||||
</div>
|
||||
<div class="falsy-note">其他所有值(包括空数组 []、空对象 {})都转成 true</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="activeTab === 'primitive'">原始类型存储实际的值,赋值时复制值。它们是不可变的,修改后创建新值。</span>
|
||||
<span v-else-if="activeTab === 'reference'">引用类型存储的是内存地址的引用,赋值时复制引用。多个变量可以指向同一个对象,修改其中一个会影响所有引用。</span>
|
||||
<span v-else>类型转换是 JS 中常见的 bug 来源。理解 falsy values 和隐式转换规则能避免很多问题。使用 === 而不是 == 来避免自动类型转换。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeTab = ref('primitive')
|
||||
const selectedType = ref(null)
|
||||
const inputValue = ref('')
|
||||
const conversionResults = ref({
|
||||
string: '-',
|
||||
number: '-',
|
||||
boolean: '-'
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{ id: 'primitive', label: '原始类型' },
|
||||
{ id: 'reference', label: '引用类型' },
|
||||
{ id: 'conversion', label: '类型转换' }
|
||||
]
|
||||
|
||||
const primitiveTypes = [
|
||||
{
|
||||
name: 'Number',
|
||||
icon: '🔢',
|
||||
example: '42, 3.14, NaN',
|
||||
description: '数字类型,包括整数和小数。NaN 表示"不是数字"。',
|
||||
note: '所有数字都是浮点数,没有整数类型。特殊值:Infinity、-Infinity、NaN'
|
||||
},
|
||||
{
|
||||
name: 'String',
|
||||
icon: '📝',
|
||||
example: '"hello", \'你好\'',
|
||||
description: '字符串类型,用单引号或双引号包裹的文本。',
|
||||
note: '字符串是不可变的,任何操作都会返回新的字符串。'
|
||||
},
|
||||
{
|
||||
name: 'Boolean',
|
||||
icon: '✅',
|
||||
example: 'true, false',
|
||||
description: '布尔类型,只有两个值:真或假。',
|
||||
note: '常用于条件判断和逻辑运算。'
|
||||
},
|
||||
{
|
||||
name: 'Undefined',
|
||||
icon: '❓',
|
||||
example: 'let x; // x 是 undefined',
|
||||
description: '变量已声明但未赋值时的默认值。',
|
||||
note: '表示"缺少值"。主动赋值 undefined 没有意义。'
|
||||
},
|
||||
{
|
||||
name: 'Null',
|
||||
icon: '🕳️',
|
||||
example: 'let x = null;',
|
||||
description: '表示"空值"或"无对象"。',
|
||||
note: 'typeof null === "object" 是 JS 的历史 bug。'
|
||||
},
|
||||
{
|
||||
name: 'Symbol',
|
||||
icon: '🔑',
|
||||
example: 'Symbol("id")',
|
||||
description: 'ES6 新增,表示独一无二的值。',
|
||||
note: '常用于对象属性的键,防止属性名冲突。'
|
||||
},
|
||||
{
|
||||
name: 'BigInt',
|
||||
icon: '🔢',
|
||||
example: '9007199254740991n',
|
||||
description: 'ES2020 新增,表示任意大的整数。',
|
||||
note: '数字后面加 n。用于处理超大整数。'
|
||||
}
|
||||
]
|
||||
|
||||
const referenceTypes = [
|
||||
{
|
||||
name: 'Object',
|
||||
icon: '📦',
|
||||
description: '键值对集合,最常用的引用类型。数组、函数也是对象。'
|
||||
},
|
||||
{
|
||||
name: 'Array',
|
||||
icon: '📚',
|
||||
description: '有序的数据集合,实际上是特殊的对象。'
|
||||
},
|
||||
{
|
||||
name: 'Function',
|
||||
icon: '⚙️',
|
||||
description: '可执行的代码块,也是对象,可以赋值给变量。'
|
||||
},
|
||||
{
|
||||
name: 'Date',
|
||||
icon: '📅',
|
||||
description: '日期和时间对象。'
|
||||
},
|
||||
{
|
||||
name: 'RegExp',
|
||||
icon: '🔍',
|
||||
description: '正则表达式对象,用于模式匹配。'
|
||||
}
|
||||
]
|
||||
|
||||
const falsyValues = ['false', '0', '""', 'null', 'undefined', 'NaN']
|
||||
|
||||
const convertType = () => {
|
||||
const val = inputValue.value
|
||||
conversionResults.value = {
|
||||
string: String(val),
|
||||
number: Number(val).toString(),
|
||||
boolean: Boolean(val).toString()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-type-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.type-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.type-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.type-card.selected {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.type-example {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.type-detail {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detail-note {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.comparison-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.compare-side {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.side-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
padding: 0.15rem 0;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.code-line.result {
|
||||
color: #6a9955;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.visual-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.value-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.ref-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.memory-box {
|
||||
background: var(--vp-c-brand-soft);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ref-types-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ref-type-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ref-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.ref-name {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.ref-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.conversion-playground {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-section label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-section input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.convert-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.results-section {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.4rem 0;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.result-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.result-value.error {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.falsy-values {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.falsy-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.falsy-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.falsy-item {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.falsy-note {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-box {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,827 @@
|
||||
<template>
|
||||
<div class="prototype-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🧬</span>
|
||||
<span class="title">原型与继承</span>
|
||||
<span class="subtitle">理解 JavaScript 的原型链机制</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你有本<span class="highlight">秘籍</span>,上面记载了很多通用技能。当你需要某个技能时,
|
||||
先翻翻自己的<span class="highlight">技能书</span>,没有就去翻<span class="highlight">师傅的秘籍</span>,
|
||||
还没有就去翻<span class="highlight">师傅的师傅的秘籍</span>……这条<span class="highlight">查找链</span>就是原型链
|
||||
</div>
|
||||
|
||||
<div class="demo-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === tab.id }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 原型基础 -->
|
||||
<div v-if="activeTab === 'basic'" class="tab-content">
|
||||
<div class="concept-explanation">
|
||||
<div class="code-panel">
|
||||
<div class="code-title">创建对象的方式</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line comment">// 方式 1:对象字面量</div>
|
||||
<div class="code-line">const obj1 = { name: "对象1" }</div>
|
||||
<div class="code-line">obj1.__proto__ === Object.prototype <span class="comment">// true</span></div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// 方式 2:构造函数</div>
|
||||
<div class="code-line">function Person(name) {</div>
|
||||
<div class="code-line indent">this.name = name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line">const p = new Person("张三")</div>
|
||||
<div class="code-line">p.__proto__ === Person.prototype <span class="comment">// true</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prototype-visual">
|
||||
<div class="prototype-chain">
|
||||
<div class="chain-node" :class="{ active: chainLevel >= 0 }" @click="chainLevel = 0">
|
||||
<div class="node-title">对象实例 (p)</div>
|
||||
<div class="node-content">
|
||||
<div class="property">name: "张三"</div>
|
||||
<div class="proto-link">__proto__ →</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow" v-if="chainLevel >= 0">↓ 查找</div>
|
||||
|
||||
<div class="chain-node constructor" :class="{ active: chainLevel >= 1 }" @click="chainLevel = 1">
|
||||
<div class="node-title">Person.prototype</div>
|
||||
<div class="node-content">
|
||||
<div class="method">constructor: Person</div>
|
||||
<div class="proto-link">__proto__ →</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow" v-if="chainLevel >= 1">↓ 查找</div>
|
||||
|
||||
<div class="chain-node object" :class="{ active: chainLevel >= 2 }" @click="chainLevel = 2">
|
||||
<div class="node-title">Object.prototype</div>
|
||||
<div class="node-content">
|
||||
<div class="method">toString()</div>
|
||||
<div class="method">hasOwnProperty()</div>
|
||||
<div class="proto-link">__proto__ → null</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chain-explanation">
|
||||
<div v-if="chainLevel === 0">
|
||||
<strong>实例对象</strong>
|
||||
<p>访问 p.name 时,在自己的属性中找到 → 返回 "张三"</p>
|
||||
</div>
|
||||
<div v-else-if="chainLevel === 1">
|
||||
<strong>Person 原型</strong>
|
||||
<p>访问 p.toString() 时,实例中没有 → 向上查找 → Person.prototype 中没有 → 继续向上</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<strong>Object 原型(链的顶端)</strong>
|
||||
<p>找到了 toString() 方法!这是所有对象的祖先提供的方法。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原型继承 -->
|
||||
<div v-else-if="activeTab === 'inheritance'" class="tab-content">
|
||||
<div class="inheritance-demo">
|
||||
<div class="inheritance-code">
|
||||
<div class="code-title">原型继承示例</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line comment">// 父类构造函数</div>
|
||||
<div class="code-line">function Animal(name) {</div>
|
||||
<div class="code-line indent">this.name = name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">Animal.prototype.eat = function() {</div>
|
||||
<div class="code-line indent">return this.name + " 在吃东西"</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// 子类构造函数</div>
|
||||
<div class="code-line">function Dog(name, breed) {</div>
|
||||
<div class="code-line indent">Animal.call(this, name) <span class="comment">// 继承属性</span></div>
|
||||
<div class="code-line indent">this.breed = breed</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// 继承方法</div>
|
||||
<div class="code-line">Dog.prototype = Object.create(Animal.prototype)</div>
|
||||
<div class="code-line">Dog.prototype.constructor = Dog</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inheritance-visual">
|
||||
<div class="class-diagram">
|
||||
<div class="class-box parent">
|
||||
<div class="class-title">Animal (父类)</div>
|
||||
<div class="class-content">
|
||||
<div class="class-section">
|
||||
<div class="section-title">属性</div>
|
||||
<div class="section-item">name: String</div>
|
||||
</div>
|
||||
<div class="class-section">
|
||||
<div class="section-title">方法 (prototype)</div>
|
||||
<div class="section-item">eat()</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inherit-arrow">↓ 继承</div>
|
||||
|
||||
<div class="class-box child">
|
||||
<div class="class-title">Dog (子类)</div>
|
||||
<div class="class-content">
|
||||
<div class="class-section">
|
||||
<div class="section-title">属性</div>
|
||||
<div class="section-item">name: String</div>
|
||||
<div class="section-item">breed: String</div>
|
||||
</div>
|
||||
<div class="class-section">
|
||||
<div class="section-title">方法 (prototype)</div>
|
||||
<div class="section-item">eat() <span class="inherited">[继承]</span></div>
|
||||
<div class="section-item">bark() <span class="own">[新增]</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inheritance-playground">
|
||||
<div class="playground-title">试试创建实例</div>
|
||||
<div class="input-group">
|
||||
<input v-model="dogName" placeholder="狗狗名字" />
|
||||
<input v-model="dogBreed" placeholder="品种" />
|
||||
<button @click="createDog">创建</button>
|
||||
</div>
|
||||
<div v-if="dogInstance" class="instance-result">
|
||||
<div class="result-item">
|
||||
<span class="label">名字:</span>
|
||||
<span class="value">{{ dogInstance.name }}</span>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="label">品种:</span>
|
||||
<span class="value">{{ dogInstance.breed }}</span>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="label">调用 eat():</span>
|
||||
<button @click="callEat" class="action-btn">调用</button>
|
||||
<span v-if="eatResult" class="method-result">{{ eatResult }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- class 语法 -->
|
||||
<div v-else class="tab-content">
|
||||
<div class="class-syntax-demo">
|
||||
<div class="syntax-comparison">
|
||||
<div class="syntax-panel old">
|
||||
<div class="panel-title">ES5 构造函数</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">function Person(name) {</div>
|
||||
<div class="code-line indent">this.name = name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">Person.prototype.greet = function() {</div>
|
||||
<div class="code-line indent">return "你好,我是" + this.name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">const p = new Person("小明")</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="syntax-panel new">
|
||||
<div class="panel-title">ES6 class 语法</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">class Person {</div>
|
||||
<div class="code-line indent">constructor(name) {</div>
|
||||
<div class="code-line indent indent">this.name = name</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line indent">greet() {</div>
|
||||
<div class="code-line indent indent">return "你好,我是" + this.name</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">const p = new Person("小明")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="class-features">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎯</div>
|
||||
<div class="feature-title">更清晰的语法</div>
|
||||
<div class="feature-desc">class 语法让面向对象编程更直观,但本质还是基于原型</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<div class="feature-title">继承更简单</div>
|
||||
<div class="feature-desc">使用 extends 关键字实现继承,代码更简洁</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚠️</div>
|
||||
<div class="feature-title">注意</div>
|
||||
<div class="feature-desc">class 只是语法糖,底层仍然是原型链机制</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inheritance-example">
|
||||
<div class="code-title">class 继承示例</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">class Animal {</div>
|
||||
<div class="code-line indent">constructor(name) {</div>
|
||||
<div class="code-line indent indent">this.name = name</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line indent">eat() {</div>
|
||||
<div class="code-line indent indent">return this.name + " 在吃东西"</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">class Dog extends Animal {</div>
|
||||
<div class="code-line indent">constructor(name, breed) {</div>
|
||||
<div class="code-line indent indent">super(name) <span class="comment">// 调用父类构造函数</span></div>
|
||||
<div class="code-line indent indent">this.breed = breed</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line indent">bark() {</div>
|
||||
<div class="code-line indent indent">return "汪汪!"</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line">}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-points">
|
||||
<div class="point-title">🎯 核心要点</div>
|
||||
<ul class="point-list">
|
||||
<li>每个对象都有 <code>__proto__</code> 属性,指向其构造函数的 <code>prototype</code></li>
|
||||
<li>访问对象属性时,先在自身查找,找不到就沿着原型链向上查找</li>
|
||||
<li>原型链顶端是 <code>Object.prototype</code>,它的 <code>__proto__</code> 是 <code>null</code></li>
|
||||
<li><code>class</code> 是语法糖,本质仍然是原型继承</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="activeTab === 'basic'">JavaScript 通过原型链实现继承,而不是像其他语言那样使用类。每个对象都有一个原型对象,对象以其原型为模板、从原型继承方法和属性。这种"原型式继承"机制让 JavaScript 更加灵活。</span>
|
||||
<span v-else-if="activeTab === 'inheritance'">原型继承让对象可以共享方法,节省内存。子类通过原型链继承父类的方法,同时可以添加自己的方法。理解原型链是掌握 JavaScript 面向对象编程的关键。</span>
|
||||
<span v-else>ES6 的 class 语法让面向对象编程更加清晰易读,但它只是语法糖,底层仍然是原型链。使用 class 可以让代码更接近传统面向对象语言的风格,降低学习成本。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeTab = ref('basic')
|
||||
const chainLevel = ref(0)
|
||||
const dogName = ref('')
|
||||
const dogBreed = ref('')
|
||||
const dogInstance = ref(null)
|
||||
const eatResult = ref('')
|
||||
|
||||
const tabs = [
|
||||
{ id: 'basic', label: '原型基础' },
|
||||
{ id: 'inheritance', label: '原型继承' },
|
||||
{ id: 'class', label: 'class 语法' }
|
||||
]
|
||||
|
||||
const createDog = () => {
|
||||
if (dogName.value && dogBreed.value) {
|
||||
dogInstance.value = {
|
||||
name: dogName.value,
|
||||
breed: dogBreed.value
|
||||
}
|
||||
eatResult.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const callEat = () => {
|
||||
if (dogInstance.value) {
|
||||
eatResult.value = `${dogInstance.value.name} 在吃东西`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.prototype-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.concept-explanation {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.code-panel {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.code-title {
|
||||
color: #888;
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
padding: 0.1rem 0;
|
||||
}
|
||||
|
||||
.code-line.indent {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.code-line .comment {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.prototype-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.prototype-chain {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.chain-node {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chain-node:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.chain-node.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.chain-node.constructor {
|
||||
border-color: #c8e6c9;
|
||||
}
|
||||
|
||||
.chain-node.constructor.active {
|
||||
background: #e8f5e9;
|
||||
}
|
||||
|
||||
.chain-node.object {
|
||||
border-color: #bbdefb;
|
||||
}
|
||||
|
||||
.chain-node.object.active {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.property {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.method {
|
||||
background: #e3f2fd;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.proto-link {
|
||||
color: var(--vp-c-brand);
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.chain-arrow {
|
||||
text-align: center;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.chain-explanation {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.chain-explanation strong {
|
||||
color: var(--vp-c-text-1);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.chain-explanation p {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.inheritance-demo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.inheritance-code {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.7rem;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.inheritance-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.class-diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.class-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.class-box.parent {
|
||||
border-color: #c8e6c9;
|
||||
}
|
||||
|
||||
.class-box.child {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.class-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.class-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.class-section {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.inherited {
|
||||
color: #4caf50;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.own {
|
||||
color: var(--vp-c-brand);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.inherit-arrow {
|
||||
text-align: center;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inheritance-playground {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.playground-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.input-group button {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.instance-result {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.result-item .label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
.result-item .value {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.method-result {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.class-syntax-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.syntax-comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.syntax-panel {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.syntax-panel.new {
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
color: #888;
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.class-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.inheritance-example {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.key-points {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.point-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.point-list {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.point-list code {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.concept-explanation,
|
||||
.inheritance-demo,
|
||||
.syntax-comparison,
|
||||
.class-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,899 @@
|
||||
<template>
|
||||
<div class="this-context-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">🎯</span>
|
||||
<span class="title">this 与执行上下文</span>
|
||||
<span class="subtitle">理解 this 的指向规则</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
想象<span class="highlight">this</span>就像一个<span class="highlight">指针</span>,
|
||||
指向"当前正在执行的主角"。不同场景下,主角会变化——
|
||||
有时是<span class="highlight">对象自己</span>,有时是<span class="highlight">全局环境</span>,还有时完全取决于<span class="highlight">谁在调用</span>
|
||||
</div>
|
||||
|
||||
<div class="scenario-selector">
|
||||
<button
|
||||
v-for="scenario in scenarios"
|
||||
:key="scenario.id"
|
||||
@click="activeScenario = scenario.id"
|
||||
class="scenario-btn"
|
||||
:class="{ active: activeScenario === scenario.id }"
|
||||
>
|
||||
{{ scenario.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 方法调用 -->
|
||||
<div v-if="activeScenario === 'method'" class="scenario-content">
|
||||
<div class="split-view">
|
||||
<div class="code-panel">
|
||||
<div class="code-title">对象方法调用</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">const person = {</div>
|
||||
<div class="code-line indent">name: "张三",</div>
|
||||
<div class="code-line indent">greet: function() {</div>
|
||||
<div class="code-line indent indent">return "你好,我是" + this.name</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">person.greet() <span class="comment">// this → person</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual-panel">
|
||||
<div class="object-visual">
|
||||
<div class="object-box">
|
||||
<div class="object-title">person 对象</div>
|
||||
<div class="object-content">
|
||||
<div class="property">name: "张三"</div>
|
||||
<div class="method" @click="simulateMethodCall">
|
||||
greet: function() { ... }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-indicator">
|
||||
<div class="this-pointer">this →</div>
|
||||
</div>
|
||||
|
||||
<div v-if="methodCallResult" class="result-box">
|
||||
{{ methodCallResult }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-box">
|
||||
<div class="rule-title">规则:对象方法</div>
|
||||
<div class="rule-content">通过对象调用方法时,<code>this</code> 指向该对象</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 普通函数 -->
|
||||
<div v-else-if="activeScenario === 'function'" class="scenario-content">
|
||||
<div class="split-view">
|
||||
<div class="code-panel">
|
||||
<div class="code-title">普通函数调用</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">function show() {</div>
|
||||
<div class="code-line indent">return this === window</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">show() <span class="comment">// this → window (浏览器)</span></div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// 严格模式下是 undefined</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual-panel">
|
||||
<div class="function-visual">
|
||||
<div class="global-window">
|
||||
<div class="window-title">window (全局对象)</div>
|
||||
<div class="window-content">
|
||||
<div class="global-item">show 函数在这里</div>
|
||||
<div class="global-item">this → window</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mode-toggle">
|
||||
<button @click="strictMode = !strictMode" class="toggle-btn">
|
||||
{{ strictMode ? '严格模式:开' : '严格模式:关' }}
|
||||
</button>
|
||||
<div class="mode-result">
|
||||
this = {{ strictMode ? 'undefined' : 'window' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-box">
|
||||
<div class="rule-title">规则:普通函数</div>
|
||||
<div class="rule-content">
|
||||
非严格模式:<code>this</code> 指向全局对象<br>
|
||||
严格模式:<code>this</code> 是 <code>undefined</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 构造函数 -->
|
||||
<div v-else-if="activeScenario === 'constructor'" class="scenario-content">
|
||||
<div class="split-view">
|
||||
<div class="code-panel">
|
||||
<div class="code-title">构造函数调用</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">function Person(name) {</div>
|
||||
<div class="code-line indent">this.name = name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">const p1 = new Person("李四")</div>
|
||||
<div class="code-line">const p2 = new Person("王五")</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line comment">// p1.name = "李四"</div>
|
||||
<div class="code-line comment">// p2.name = "王五"</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual-panel">
|
||||
<div class="constructor-visual">
|
||||
<div class="constructor-process">
|
||||
<div class="process-step">
|
||||
<span class="step-num">1</span>
|
||||
<span>创建新对象</span>
|
||||
</div>
|
||||
<div class="process-arrow">↓</div>
|
||||
<div class="process-step">
|
||||
<span class="step-num">2</span>
|
||||
<span>this 指向新对象</span>
|
||||
</div>
|
||||
<div class="process-arrow">↓</div>
|
||||
<div class="process-step">
|
||||
<span class="step-num">3</span>
|
||||
<span>执行构造函数</span>
|
||||
</div>
|
||||
<div class="process-arrow">↓</div>
|
||||
<div class="process-step">
|
||||
<span class="step-num">4</span>
|
||||
<span>返回新对象</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="object-comparison">
|
||||
<div class="obj-instance">
|
||||
<div class="obj-title">p1</div>
|
||||
<div class="obj-content">name: "李四"</div>
|
||||
</div>
|
||||
<div class="obj-instance">
|
||||
<div class="obj-title">p2</div>
|
||||
<div class="obj-content">name: "王五"</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-box">
|
||||
<div class="rule-title">规则:new 调用</div>
|
||||
<div class="rule-content">
|
||||
使用 <code>new</code> 调用函数时,<code>this</code> 指向新创建的对象
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- call/apply/bind -->
|
||||
<div v-else-if="activeScenario === 'explicit'" class="scenario-content">
|
||||
<div class="split-view">
|
||||
<div class="code-panel">
|
||||
<div class="code-title">显式绑定 (call/apply/bind)</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">function greet() {</div>
|
||||
<div class="code-line indent">return "我是" + this.name</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">const person = { name: "小明" }</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">greet.call(person) <span class="comment">// 显式指定 this</span></div>
|
||||
<div class="code-line">greet.apply(person)</div>
|
||||
<div class="code-line">const bound = greet.bind(person)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual-panel">
|
||||
<div class="binding-visual">
|
||||
<div class="function-box">
|
||||
<div class="box-title">greet 函数</div>
|
||||
<div class="box-content">this.name</div>
|
||||
</div>
|
||||
|
||||
<div class="binding-methods">
|
||||
<div class="binding-item" @click="simulateCall" :class="{ active: bindingMethod === 'call' }">
|
||||
<div class="method-name">call(person)</div>
|
||||
<div class="method-desc">立即调用,this → person</div>
|
||||
</div>
|
||||
<div class="binding-item" @click="simulateApply" :class="{ active: bindingMethod === 'apply' }">
|
||||
<div class="method-name">apply(person)</div>
|
||||
<div class="method-desc">同 call,参数为数组</div>
|
||||
</div>
|
||||
<div class="binding-item" @click="simulateBind" :class="{ active: bindingMethod === 'bind' }">
|
||||
<div class="method-name">bind(person)</div>
|
||||
<div class="method-desc">返回新函数,this 固定</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="bindingResult" class="binding-result">
|
||||
{{ bindingResult }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-box">
|
||||
<div class="rule-title">规则:显式绑定</div>
|
||||
<div class="rule-content">
|
||||
<code>call/apply/bind</code> 可以显式指定 <code>this</code> 的指向
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 箭头函数 -->
|
||||
<div v-else class="scenario-content">
|
||||
<div class="split-view">
|
||||
<div class="code-panel">
|
||||
<div class="code-title">箭头函数的 this</div>
|
||||
<div class="code-block">
|
||||
<div class="code-line">const person = {</div>
|
||||
<div class="code-line indent">name: "小红",</div>
|
||||
<div class="code-line indent">greet: function() {</div>
|
||||
<div class="code-line indent indent">setTimeout(() => {</div>
|
||||
<div class="code-line indent indent indent">console.log(this.name)</div>
|
||||
<div class="code-line indent indent">}, 1000)</div>
|
||||
<div class="code-line indent">}</div>
|
||||
<div class="code-line">}</div>
|
||||
<div class="code-line"></div>
|
||||
<div class="code-line">person.greet() <span class="comment">// 输出 "小红"</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual-panel">
|
||||
<div class="arrow-function-visual">
|
||||
<div class="outer-context">
|
||||
<div class="context-title">外层作用域 (person)</div>
|
||||
<div class="context-content">
|
||||
<div class="context-item">this.name = "小红"</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-capture">
|
||||
<div class="capture-title">箭头函数捕获外层 this</div>
|
||||
<div class="capture-arrow">↑ 继承 this</div>
|
||||
</div>
|
||||
|
||||
<div class="inner-context">
|
||||
<div class="context-title">箭头函数内部</div>
|
||||
<div class="context-content">
|
||||
<div class="context-item">this → 外层的 this</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-box">
|
||||
<div class="rule-title">规则:箭头函数</div>
|
||||
<div class="rule-content">
|
||||
箭头函数没有自己的 <code>this</code>,它继承外层作用域的 <code>this</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-reference">
|
||||
<div class="reference-title">📋 this 指向速查表</div>
|
||||
<div class="reference-table">
|
||||
<div class="ref-row header">
|
||||
<span>调用方式</span>
|
||||
<span>this 指向</span>
|
||||
</div>
|
||||
<div class="ref-row">
|
||||
<span>obj.method()</span>
|
||||
<span>obj</span>
|
||||
</div>
|
||||
<div class="ref-row">
|
||||
<span>func()</span>
|
||||
<span>window / undefined</span>
|
||||
</div>
|
||||
<div class="ref-row">
|
||||
<span>new Func()</span>
|
||||
<span>新创建的对象</span>
|
||||
</div>
|
||||
<div class="ref-row">
|
||||
<span>func.call(obj)</span>
|
||||
<span>obj</span>
|
||||
</div>
|
||||
<div class="ref-row">
|
||||
<span>箭头函数</span>
|
||||
<span>外层作用域的 this</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>
|
||||
<span>this 的值是在函数调用时确定的,不是定义时确定的。关键要看"函数是如何被调用的",而不是"函数在哪里定义"。箭头函数是例外——它没有自己的 this,从外层作用域继承。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeScenario = ref('method')
|
||||
const strictMode = ref(false)
|
||||
const bindingMethod = ref('')
|
||||
const methodCallResult = ref('')
|
||||
const bindingResult = ref('')
|
||||
|
||||
const scenarios = [
|
||||
{ id: 'method', label: '对象方法' },
|
||||
{ id: 'function', label: '普通函数' },
|
||||
{ id: 'constructor', label: '构造函数' },
|
||||
{ id: 'explicit', label: 'call/apply/bind' },
|
||||
{ id: 'arrow', label: '箭头函数' }
|
||||
]
|
||||
|
||||
const simulateMethodCall = () => {
|
||||
methodCallResult.value = '你好,我是张三'
|
||||
setTimeout(() => {
|
||||
methodCallResult.value = ''
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const simulateCall = () => {
|
||||
bindingMethod.value = 'call'
|
||||
bindingResult.value = '我是小明 (通过 call 绑定)'
|
||||
}
|
||||
|
||||
const simulateApply = () => {
|
||||
bindingMethod.value = 'apply'
|
||||
bindingResult.value = '我是小明 (通过 apply 绑定)'
|
||||
}
|
||||
|
||||
const simulateBind = () => {
|
||||
bindingMethod.value = 'bind'
|
||||
bindingResult.value = '我是小明 (通过 bind 绑定,返回新函数)'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.this-context-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.scenario-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scenario-btn {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.scenario-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scenario-btn.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scenario-content {
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.split-view {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.code-panel {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.code-title {
|
||||
color: #888;
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
padding: 0.1rem 0;
|
||||
}
|
||||
|
||||
.code-line.indent {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.code-line.indent.indent {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
.code-line .comment {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.code-line code {
|
||||
background: #333;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.visual-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.object-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.object-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.object-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.object-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.property, .method {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.method {
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.method:hover {
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.arrow-indicator {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.this-pointer {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.rule-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.rule-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.rule-content {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rule-content code {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.function-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.global-window {
|
||||
background: #f5f5f5;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.window-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.global-item {
|
||||
background: white;
|
||||
padding: 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.mode-result {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.constructor-process {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.process-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-num {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.process-arrow {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.object-comparison {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.obj-instance {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.obj-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.obj-content {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.binding-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.function-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.box-content {
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.binding-methods {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.binding-item {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.binding-item:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.binding-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.method-desc {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.binding-result {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.arrow-function-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.outer-context, .inner-context {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.context-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.context-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.context-item {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.arrow-capture {
|
||||
text-align: center;
|
||||
background: var(--vp-c-brand-soft);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.capture-title {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.capture-arrow {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.quick-reference {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.reference-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.reference-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.ref-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.4rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.ref-row.header {
|
||||
background: var(--vp-c-brand-soft);
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.ref-row span:first-child {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.split-view {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="variable-scope-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">📦</span>
|
||||
<span class="title">变量与作用域</span>
|
||||
<span class="subtitle">理解 let、const、var 的区别</span>
|
||||
</div>
|
||||
|
||||
<div class="intro-text">
|
||||
想象你在<span class="highlight">家里</span>和<span class="highlight">公司</span>放东西:
|
||||
<span class="highlight">var</span>像是把东西贴在脑门上(哪都能看见),
|
||||
<span class="highlight">let</span>像是放在抽屉里(当前房间能用),
|
||||
<span class="highlight">const</span>像是焊死在地上的柜子(不能移动)
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<div class="code-block">
|
||||
<div class="code-line" v-for="(line, i) in codeLines" :key="i" :class="{ active: currentLine === i }">
|
||||
<span class="line-num">{{ i + 1 }}</span>
|
||||
<span class="line-code" v-html="highlightCode(line)"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visualization">
|
||||
<div class="scope-area global-scope">
|
||||
<div class="scope-title">全局作用域(房子外)</div>
|
||||
<div class="scope-vars">
|
||||
<div v-if="step >= 1" class="var-item" :class="{ error: step === 4 }">
|
||||
<span class="var-type">var</span>
|
||||
<span class="var-name">globalVar</span>
|
||||
<span class="var-value">= "外面"</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scope-area block-scope" v-if="step >= 2">
|
||||
<div class="scope-title">块级作用域(房间内)</div>
|
||||
<div class="scope-vars">
|
||||
<div v-if="step >= 2" class="var-item" :class="{ error: step === 4 }">
|
||||
<span class="var-type">var</span>
|
||||
<span class="var-name">blockVar</span>
|
||||
<span class="var-value">= "房间里"</span>
|
||||
</div>
|
||||
<div v-if="step >= 3" class="var-item let">
|
||||
<span class="var-type">let</span>
|
||||
<span class="var-name">blockLet</span>
|
||||
<span class="var-value">= "只有房间内能用"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="console-output">
|
||||
<div class="console-title">控制台输出</div>
|
||||
<div class="console-lines">
|
||||
<div v-for="(output, i) in consoleOutput" :key="i" class="console-line" :class="{ error: output.error }">
|
||||
{{ output.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="prevStep" :disabled="step === 0" class="control-btn">← 上一步</button>
|
||||
<span class="step-indicator">{{ step + 1 }} / {{ maxSteps }}</span>
|
||||
<button @click="nextStep" :disabled="step === maxSteps" class="control-btn">下一步 →</button>
|
||||
<button @click="reset" class="control-btn secondary">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>
|
||||
<span v-if="step === 0">var 没有块级作用域,会"泄漏"到外部;let 和 const 有块级作用域,只在声明的作用域内有效。</span>
|
||||
<span v-else-if="step === 1">var 声明的变量可以在全局作用域访问,容易造成命名冲突。</span>
|
||||
<span v-else-if="step === 2">var 可以重复声明,这在大型项目中容易导致难以排查的 bug。</span>
|
||||
<span v-else-if="step === 3">let 和 const 有块级作用域,在 if 块外部无法访问,更安全。</span>
|
||||
<span v-else>const 声明的变量不能重新赋值,let 可以。推荐优先使用 const,需要重新赋值时用 let。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const step = ref(0)
|
||||
const maxSteps = 5
|
||||
|
||||
const codeLines = [
|
||||
'var globalVar = "外面"',
|
||||
'if (true) {',
|
||||
' var blockVar = "房间里"',
|
||||
' let blockLet = "只有房间内能用"',
|
||||
'}',
|
||||
'// 尝试访问这些变量'
|
||||
]
|
||||
|
||||
const currentLine = computed(() => {
|
||||
const lineMap = [0, 1, 2, 3, 1, 4]
|
||||
return lineMap[step.value]
|
||||
})
|
||||
|
||||
const consoleOutput = ref([])
|
||||
|
||||
const scenarios = {
|
||||
0: { output: [] },
|
||||
1: { output: [{ text: 'globalVar = "外面"', error: false }] },
|
||||
2: { output: [{ text: 'globalVar = "外面"', error: false }, { text: 'blockVar = "房间里"', error: false }] },
|
||||
3: { output: [{ text: 'globalVar = "外面"', error: false }, { text: 'blockVar = "房间里"', error: false }, { text: 'blockLet = "只有房间内能用"', error: false }] },
|
||||
4: { output: [{ text: 'globalVar = "外面" ✓', error: false }, { text: 'blockVar = "房间里" ✓ (var 泄漏了!)', error: true }, { text: 'blockLet = 报错!let 不在块外部', error: true }] },
|
||||
5: { output: [{ text: '推荐:const name = "值" (不能改)', error: false }, { text: '需要改:let count = 0 (可以改)', error: false }, { text: '避免:var old = "过时了"', error: true }] }
|
||||
}
|
||||
|
||||
const nextStep = () => {
|
||||
if (step.value < maxSteps) {
|
||||
step.value++
|
||||
consoleOutput.value = scenarios[step.value].output
|
||||
}
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
if (step.value > 0) {
|
||||
step.value--
|
||||
consoleOutput.value = scenarios[step.value].output
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
step.value = 0
|
||||
consoleOutput.value = []
|
||||
}
|
||||
|
||||
const highlightCode = (line) => {
|
||||
return line
|
||||
.replace(/(var|let|const)/g, '<span class="keyword">$1</span>')
|
||||
.replace(/(".+?")/g, '<span class="string">$1</span>')
|
||||
.replace(/(\/\/.+)/g, '<span class="comment">$1</span>')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.variable-scope-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.intro-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-line.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.line-num {
|
||||
color: var(--vp-c-text-3);
|
||||
min-width: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.line-code :deep(.keyword) { color: #c586c0; }
|
||||
.line-code :deep(.string) { color: #ce9178; }
|
||||
.line-code :deep(.comment) { color: #6a9955; }
|
||||
|
||||
.visualization {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.scope-area {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.scope-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.scope-vars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.var-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.var-item.error {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
}
|
||||
|
||||
.var-item.let {
|
||||
background: #e8f5e9;
|
||||
border: 1px solid #c8e6c9;
|
||||
}
|
||||
|
||||
.var-type {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.var-name {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.var-value {
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.block-scope {
|
||||
margin-left: 1rem;
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.console-output {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
color: #d4d4d4;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.console-title {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.console-line {
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.console-line.error {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.control-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.control-btn.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.code-display {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user