JavaScript基础语法与DOM操作实战课堂笔记

第一部分:JavaScript基础语法

1.1 什么是JavaScript

老师强调:纯HTML和CSS只能做静态页面,就像一张漂亮的宣传单,只能看不能互动。而实际网页需要动态交互------轮播图自动切换、下拉菜单响应鼠标悬停、点击按钮执行操作,这些都需要JavaScript来实现。

HTML/CSS/JS 分工:

技术 作用 比喻
HTML 页面结构 骨架
CSS 页面样式 衣服
JavaScript 交互逻辑 大脑

1.2 变量声明

JavaScript是弱类型语言 ,使用varlet声明变量,无需预先声明类型。

复制代码
// 弱类型:同一个变量可以存储不同类型的数据
var a = 10;        // 数字
var b = 20.5;      // 小数
var c = 'aaa';     // 字符串
var d = true;      // 布尔值
var e;             // undefined(未定义)
var f = null;      // null(空值)

var vs let:

关键字 变量提升 初始化 说明
var ✅ 会提升 初始化为undefined 可以先使用后定义
let ✅ 会提升 不初始化(暂时性死区) 定义前使用会报错

老师强调 :JavaScript会把所有变量声明自动"提升"到作用域顶部。var就像先把空盒子放好(盒子里是undefined),let则是盒子位置定了但没放东西前不让碰。

1.3 数据类型

1.3.1 基本类型
类型 说明 示例
数字类型 不区分整数和小数 1020.5
字符串 单引号或双引号均可 'aaa'"hello"
布尔类型 只有两个值 truefalse
undefined 未定义(未赋值) var e;
null 空值 var f = null;

老师强调 :JavaScript中所有数值统一为数字类型,不像C/Java那样细分为intshortfloat。字符串用单引号或双引号都行,前后匹配即可。

1.3.2 复合类型
类型 表示方式 说明
数组 [] 元素类型不固定,可嵌套
对象 {} 键值对(属性:值)
函数 function 函数本身也是一种"值"

数组示例:

复制代码
var arr = [
    10.5,                    // 数字
    20,                      // 数字
    "demo",                  // 字符串
    true,                    // 布尔值
    undefined,               // undefined
    null,                    // null
    [10, 20.5],              // 嵌套数组
    { name: "zhangsan" },    // 嵌套对象
    function(a, b) {         // 嵌套函数
        console.log(a);
        console.log(b);
    }
];

console.log(arr[8]);  // 输出函数体
arr[8]();             // 调用函数

对象示例:

复制代码
var obj = {
    name: "demo",
    age: 20,
    friend: {
        name: "lisi",
        age: 21
    },
    play: ["足球", "篮球"],
    fly: function(a, b) {
        console.log(a);
        console.log(b);
    }
};

// 访问方式
console.log(obj.name);           // 点语法
console.log(obj['name']);        // 中括号语法
console.log(obj.friend.name);    // 嵌套访问
console.log(obj.play[0]);        // 数组元素
obj.fly(10, 20);                 // 调用函数

老师强调 :访问数据时要分清当前是对象还是数组------对象用点(.数组用中括号([索引]。就像剥洋葱一样,一层层往里找。

1.3.3 强类型 vs 弱类型数组
语言类型 数组存储方式 访问速度 原因
强类型(C/Java) 内存连续存储 每个元素固定大小
弱类型(JS) 分散存储 元素类型不同,需额外查找

老师强调:强类型语言数组在内存中是连续存储的,访问效率高;弱类型语言数组可以混合存放不同类型的数据,虽然灵活但内存分配不连续,访问速度会慢一些。

1.4 函数

1.4.1 函数的两种定义方式
复制代码
// 1. 命名函数
function change(a, b) {
    console.log(a);
    console.log(b);
}

// 2. 匿名函数(赋值给变量)
var run = function(a, b) {
    console.log(a);
    console.log(b);
};

// 3. 命名函数赋值给变量(函数名失效)
var jump = function change(a, b) {
    console.log(a);
    console.log(b);
};

// 调用方式
change(10, 20);    // 命名函数直接调用
run(10, 20);       // 匿名函数通过变量调用
jump(10, 20);      // 此时只能通过jump调用,change失效

老师强调 :把函数逻辑赋给变量后,原函数名就失效了。这就像给一个人起了外号后,本名反而没人叫了。匿名函数从定义到调用都只有一种固定方式,不会出现这种混淆,更推荐使用。

1.4.2 函数作为值
复制代码
// 函数可以存储在数组或对象中
var arr = [
    function(a, b) { console.log(a + b); }
];

var obj = {
    add: function(a, b) { console.log(a + b); }
};

arr;   // 输出30
obj.add(10, 20);  // 输出30

1.5 window 与 document

复制代码
// window ------ 浏览器窗口对象("老大")
console.log(window);

// document ------ window下的属性,代表整个HTML文档
console.log(document);
console.dir(document);  // 以对象形式展示

// alert() 和 console.log() 都是window的方法(可省略window)
window.alert("hello");
alert("hello");         // 等价
对象 说明
window 代表整个浏览器窗口,所有全局对象的"老大"
document window的属性,代表整个HTML文档

老师强调console.log()以HTML标签形式输出,console.dir()以对象属性形式展示,能更清晰地呈现文档的底层数据结构。

第二部分:DOM操作基础

2.1 什么是DOM

老师强调 :DOM(Document Object Model)是JavaScript对象与HTML元素之间的桥梁。JavaScript对象和HTML元素之间存在一一映射关系------操作JS对象就能控制页面元素,就像操纵提线木偶。

复制代码
DOM = Document(文档)+ Object(对象)+ Model(模型)

2.2 查找元素

2.2.1 五种查找方法
方法 说明 返回值
getElementById('id') 根据ID查找 单个元素对象
getElementsByClassName('class') 根据class查找 元素数组
getElementsByTagName('标签名') 根据标签名查找 元素数组
querySelector('选择器') 根据CSS选择器查找 第一个匹配元素
querySelectorAll('选择器') 根据CSS选择器查找 所有匹配元素数组
复制代码
// 1. 根据ID查找(返回单个对象)
var obj = document.getElementById("aa");

// 2. 根据class查找(返回数组)
var arr = document.getElementsByClassName("demo");

// 3. 根据标签名查找(返回数组)
var arr = document.getElementsByTagName("div");

// 4. 根据选择器查找(返回第一个)
var obj = document.querySelector(".demo");

// 5. 根据选择器查找(返回所有)
var arr = document.querySelectorAll(".demo");

老师强调 :根据ID查找时不需要加# ,因为方法名已说明是按ID查询。即使页面上有多个相同ID,该方法也只会返回第一个匹配项,找不到返回null

2.2.2 通过关系查找

利用元素自身属性遍历父子、兄弟关系:

复制代码
var aa = document.getElementById("aa");

// 父子关系
console.log(aa.parentNode);          // 父节点
console.log(aa.parentElement);       // 父元素
console.log(aa.children);            // 所有子元素(仅标签)
console.log(aa.childNodes);          // 所有子节点(含文本)
console.log(aa.firstChild);          // 第一个子节点(含文本)
console.log(aa.firstElementChild);   // 第一个子元素(仅标签)
console.log(aa.lastChild);           // 最后一个子节点(含文本)
console.log(aa.lastElementChild);    // 最后一个子元素(仅标签)

// 兄弟关系
console.log(aa.previousSibling);          // 前一个兄弟节点(含文本)
console.log(aa.previousElementSibling);   // 前一个兄弟元素(仅标签)
console.log(aa.nextSibling);              // 后一个兄弟节点(含文本)
console.log(aa.nextElementSibling);       // 后一个兄弟元素(仅标签)

childNodes vs children:

属性 包含内容 示例
childNodes 所有子节点(含文本节点) 长度可能为7
children 仅子元素节点(标签) 长度可能为3

老师强调childNodes会包含文本节点(如换行、空格),而children只包含标签元素。firstChild可能是文本节点,而firstElementChild一定是标签元素。

2.3 DOM加载顺序问题

复制代码
// ❌ 错误:元素还未加载
var obj = document.getElementById("aa");  // 返回null
obj.style.background = "red";             // 报错

// ✅ 解决方案1:将script放到body底部
// ✅ 解决方案2:使用defer属性延迟加载
<script src="js/demo.js" defer></script>
// ✅ 解决方案3:使用window.onload
window.onload = function() {
    var obj = document.getElementById("aa");
    obj.style.background = "red";
};

老师强调 :必须等待整个窗口加载完成后再执行DOM操作,否则代码可能找不到目标元素。window.onload确保所有内容加载完毕后再执行。

第三部分:DOM修改操作

3.1 修改内容

属性 说明 适用元素
innerText 纯文本内容(不解析HTML) 普通标签
innerHTML HTML内容(解析标签) 普通标签
value 表单控件的值 input、textarea等
复制代码
var aa = document.getElementById("aa");

// innerText vs innerHTML
aa.innerText = "<h1>hello</h1>";   // 显示为文本 "<h1>hello</h1>"
aa.innerHTML = "<h1>hello</h1>";   // 显示为加粗标题 "hello"

// input使用value
var bb = document.getElementById("bb");
console.log(bb.value);        // 获取输入框的值
bb.value = "新内容";          // 设置输入框的值

老师强调innerHTML会解析HTML标签,innerText只会作为纯文本处理。input的值存在value属性里,用innerHTML是无效的。

3.2 修改样式

复制代码
var aa = document.getElementById("aa");

// CSS属性名 → JS驼峰命名
aa.style.backgroundColor = "pink";   // background-color → backgroundColor
aa.style.fontSize = "50px";          // font-size → fontSize
aa.style.color = "red";

CSS属性名转换规则:

CSS JavaScript
background-color backgroundColor
font-size fontSize
text-decoration textDecoration

老师强调style本身是个包含多个CSS属性的对象,所有连字符命名的CSS属性必须转为小驼峰命名

3.3 修改属性

复制代码
var aa = document.getElementById("aa");

// 原生属性:直接用点语法
console.log(aa.className);    // class属性在JS中叫className
aa.className = "ppppp";

console.log(aa.id);
aa.id = "newId";

// 自定义属性:使用getAttribute/setAttribute
console.log(aa.getAttribute("run"));    // 获取自定义属性
aa.setAttribute("index", "xxxxxx");     // 设置自定义属性
aa.setAttribute("run", "yyyyy");        // 修改已有属性
属性类型 获取方式 设置方式
原生属性(id、class等) 对象.属性名 对象.属性名 = 值
自定义属性 getAttribute('属性名') setAttribute('属性名', '值')

第四部分:DOM增删改查操作

4.1 创建元素

复制代码
// 方法1:createElement() 创建新元素
var newNode = document.createElement("h1");
newNode.innerText = "新元素";
newNode.style.color = "gold";
newNode.className = "pppp";
console.log(newNode);

// 方法2:cloneNode() 复制已有元素
var oldNode = document.querySelector("h1");
var newNode = oldNode.cloneNode(true);   // true=深拷贝(含子元素)
var newNode = oldNode.cloneNode(false);  // false=浅拷贝(仅标签本身)

4.2 添加元素

复制代码
// 获取容器
var container = document.getElementById("aa");
var target = document.querySelector("p");

// appendChild():末尾添加
container.appendChild(newNode);

// insertBefore():在指定元素前插入
container.insertBefore(newNode, target);

// replaceChild():替换指定元素
container.replaceChild(newNode, target);

老师强调appendChild把新节点作为最后一个子元素添加。insertBefore需要两个参数:第一个是要添加的新节点,第二个是作为参照的现有节点。

4.3 删除元素

复制代码
var target = document.querySelector("p");

// 方法1:先找父容器再删除
var container = document.getElementById("aa");
container.removeChild(target);

// 方法2:通过parentElement向上查找删除
target.parentElement.removeChild(target);

老师强调 :删除元素必须通过父节点 调用removeChild()。可以通过parentElement属性向上查找父节点。

4.4 DOM操作总结

操作 方法 说明
getElementByIdquerySelector 查找元素
createElement + appendChild 创建并添加元素
parent.removeChild(child) 删除子元素
修改innerTextstyleclassName 修改元素内容/样式/属性

第五部分:事件绑定

5.1 事件绑定方式

复制代码
// 方式1:直接在元素上绑定
document.onclick = function() {
    document.body.style.background = "red";
};

// 方式2:获取元素后绑定
var obj = document.getElementById("aa");
obj.onclick = function() {
    obj.style.background = "yellow";
};

5.2 常用事件

事件 触发时机
onclick 鼠标单击
ondblclick 鼠标双击
onmouseenter 鼠标移入
onmouseleave 鼠标移出
onfocus 获得焦点
onblur 失去焦点
onload 页面加载完成

5.3 this关键字

复制代码
var arr = document.getElementsByClassName("demo");

for (var i = 0; i < arr.length; i++) {
    arr[i].onclick = function() {
        // this 指向当前被点击的元素
        this.style.background = "yellow";
    };
}

老师强调this在事件处理函数中指向触发该事件的元素本身 ,比用数组索引更可靠。点击哪个元素,this就代表哪个元素。

5.4 事件绑定常见陷阱

问题:循环绑定事件时变量i的值不对

复制代码
var arr = document.getElementsByClassName("demo");

// ❌ 错误:所有点击都输出3
for (var i = 0; i < arr.length; i++) {
    arr[i].onclick = function() {
        console.log(i);   // i总是3
    };
}

// ✅ 正确:使用this
for (var i = 0; i < arr.length; i++) {
    arr[i].onclick = function() {
        this.style.background = "yellow";
    };
}

老师强调 :事件绑定时,循环中的i在点击时才取值,此时循环已结束,i变成最终值。使用this可以避免这个问题。

第六部分:To-Do List 综合实战

6.1 功能概述

这是一个完整的待办事项管理应用,包含增、删、改、查四个核心操作。

功能 操作 DOM操作
添加 输入内容,点击"添加"按钮 createElement + appendChild
完成 点击"完成"按钮 修改style.textDecoration
修改 点击"修改"按钮(待实现) 替换内容为输入框
删除 点击"删除"按钮 removeChild

6.2 HTML结构

复制代码
<div class="todolist">
    <!-- 添加区域 -->
    <div class="addbox">
        <input type="text" class="info">
        <input type="button" class="btn" value="添加">
    </div>
    <!-- 表格展示区域 -->
    <table border="1">
        <thead>
            <tr>
                <th>事项</th>
                <th width="200px">操作</th>
            </tr>
        </thead>
        <tbody>
            <!-- 动态生成的行 -->
        </tbody>
    </table>
</div>

6.3 CSS样式核心代码

复制代码
/* 重置 */
* { padding: 0; margin: 0; }

/* 容器居中 */
.todolist {
    width: 400px;
    margin: 200px auto;
}

/* 添加区域 - 弹性盒子 */
.todolist .addbox {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
}

.todolist .addbox .info {
    width: 80%;
    padding: 5px;
    outline: none;
}

.todolist .addbox .btn {
    width: 15%;
    padding: 5px;
    background: #feca57;
    border: none;
}

/* 悬停效果:内阴影 */
.todolist .addbox .btn:hover {
    box-shadow: -2px -2px #ff9f43 inset;
}

/* 表格边框合并 */
.todolist table {
    width: 100%;
    border-collapse: collapse;
}

/* 表头 */
.todolist table thead tr {
    background: #feca57;
}

.todolist table tr th {
    padding: 5px;
    font-size: 14px;
}

.todolist table tr td {
    padding: 4px;
    font-size: 13px;
}

/* 第二列居中 */
.todolist table tr td:nth-child(2) {
    text-align: center;
}

/* 三个按钮样式 */
.todolist table tr td .finish {
    padding: 3px;
    width: 58px;
    border: none;
    background: #ff9ff3;
}

.todolist table tr td .update {
    padding: 3px;
    width: 58px;
    border: none;
    background: #48dbfb;
}

.todolist table tr td .delete {
    padding: 3px;
    width: 58px;
    border: none;
    background: #ff6b6b;
}

/* 悬停内阴影 */
.todolist table tr td .finish:hover {
    box-shadow: -2px -2px #f368e0 inset;
}
.todolist table tr td .update:hover {
    box-shadow: -2px -2px #0abde3 inset;
}
.todolist table tr td .delete:hover {
    box-shadow: -2px -2px #ee5253 inset;
}

6.4 JavaScript完整逻辑

javascript 复制代码
window.onload = function() {
    // 1. 查找元素
    var addBtn = document.querySelector('.btn');
    var infoInput = document.querySelector(".info");
    var tbody = document.querySelector("tbody");

    // 2. 添加功能
    addBtn.onclick = function() {
//之所以所有功能都在添加的inclick里
//因为"完成"和"删除"按钮是在"添加"按钮的点击事件里动态创建的,所以它们的点击事件(onclick)必
//在这个动态创建的过程中进行绑定。
//拿出去 = 页面加载时绑定一次,之后新增的按钮都没有事件,所以完全失效。
        // 获取输入内容,去除首尾空格
        var text = infoInput.value.trim();
        
        // 空内容不添加
        if (text) {
            // 创建行
            var tr = document.createElement("tr");
            
            // 第一列:事项内容
            var tdOne = document.createElement("td");
            tdOne.innerText = text;
            
            // 第二列:三个按钮
            var tdTwo = document.createElement("td");
            tdTwo.innerHTML = 
                '<input type="button" class="finish" value="完成">' +
                '<input type="button" class="update" value="修改">' +
                '<input type="button" class="delete" value="删除">';
            
            // 组装行
            tr.appendChild(tdOne);
            tr.appendChild(tdTwo);
            tbody.appendChild(tr);
            
            // 清空输入框
            infoInput.value = "";
        }

        // 3. 完成功能(每次添加后重新绑定)
        var finishList = document.getElementsByClassName("finish");
        for (var i = 0; i < finishList.length; i++) {
            finishList[i].onclick = function() {
                // 找到当前行的事项单元格
                var target = this.parentElement.previousElementSibling;
                target.style.textDecoration = "line-through";
                target.style.color = "#888";
                this.style.background = "#aaa";
            };
        }

        // 4. 删除功能(每次添加后重新绑定)
        var deleteList = document.getElementsByClassName("delete");
        for (var i = 0; i < deleteList.length; i++) {
            deleteList[i].onclick = function() {
                // 找到当前行
                var targetNode = this.parentElement.parentElement;
                // 二次确认
                if (confirm("确定要删除吗")) {
                    tbody.removeChild(targetNode);
                }
            };
        }
    };
};

6.5 功能实现详解

6.5.1 添加功能
复制代码
// 核心步骤
var text = infoInput.value.trim();   // 1. 获取输入(去除首尾空格)
if (text) {                          // 2. 空值判断
    var tr = document.createElement("tr");  // 3. 创建行
    var tdOne = document.createElement("td");
    tdOne.innerText = text;                 // 4. 设置内容
    var tdTwo = document.createElement("td");
    tdTwo.innerHTML = '三个按钮的HTML';      // 5. 创建操作列
    tr.appendChild(tdOne);                  // 6. 组装
    tr.appendChild(tdTwo);
    tbody.appendChild(tr);                  // 7. 添加到表格
    infoInput.value = "";                   // 8. 清空输入框
}

老师强调trim()方法能自动去除字符串首尾空格,但不会处理中间空格。获取输入值必须在点击事件触发后执行,因为初始时输入框是空的。

6.5.2 完成功能
复制代码
// 通过class查找所有"完成"按钮
var finishList = document.getElementsByClassName("finish");
for (var i = 0; i < finishList.length; i++) {
    finishList[i].onclick = function() {
        // this → 当前点击的"完成"按钮
        // parentElement → td → previousElementSibling → 事项单元格
        var target = this.parentElement.previousElementSibling;
        target.style.textDecoration = "line-through";  // 中划线
        target.style.color = "#888";                    // 变灰
        this.style.background = "#aaa";                 // 按钮置灰
    };
}
6.5.3 删除功能
复制代码
var deleteList = document.getElementsByClassName("delete");
for (var i = 0; i < deleteList.length; i++) {
    deleteList[i].onclick = function() {
        // this → 当前"删除"按钮 → parentElement(td) → parentElement(tr)
        var targetNode = this.parentElement.parentElement;
        if (confirm("确定要删除吗")) {    // 二次确认
            tbody.removeChild(targetNode);
        }
    };
}

老师强调alert()是普通提示框,confirm()是带确定/取消的询问框。点击"确定"返回true,点击"取消"返回false

6.5.4 修改功能(课后思考)

老师强调:技术实现没有唯一标准答案,修改功能可以有多种实现方式:

  1. 就地编辑 :点击"修改",当前行的td内容替换为<input>,默认显示原文本,修改完成后更新
  2. 弹窗编辑 :使用prompt()弹出输入框,获取新值后更新
  3. 下方独立编辑区:在表格下方设置独立的编辑输入框
复制代码
// 思路1:就地编辑(伪代码)
updateBtn.onclick = function() {
    var td = this.parentElement.previousElementSibling;
    var oldText = td.innerText;
    td.innerHTML = '<input type="text" value="' + oldText + '">' +
                   '<input type="button" value="确认修改">';
    // 确认按钮绑定事件,获取新值并更新
};

效果如图:

附录一:DOM查找方法速查表

方法 语法 返回值 示例
按ID getElementById('id') 单个元素 document.getElementById('aa')
按Class getElementsByClassName('class') 数组 document.getElementsByClassName('demo')
按标签 getElementsByTagName('标签') 数组 document.getElementsByTagName('div')
CSS选择器(单个) querySelector('选择器') 单个元素 document.querySelector('.demo')
CSS选择器(全部) querySelectorAll('选择器') 数组 document.querySelectorAll('.demo')

关系查找属性速查

属性 说明 是否含文本节点
parentNode 父节点 -
parentElement 父元素 -
children 所有子元素 ❌ 仅标签
childNodes 所有子节点 ✅ 含文本
firstChild 第一个子节点 ✅ 含文本
firstElementChild 第一个子元素 ❌ 仅标签
lastChild 最后一个子节点 ✅ 含文本
lastElementChild 最后一个子元素 ❌ 仅标签
previousSibling 前一个兄弟节点 ✅ 含文本
previousElementSibling 前一个兄弟元素 ❌ 仅标签
nextSibling 后一个兄弟节点 ✅ 含文本
nextElementSibling 后一个兄弟元素 ❌ 仅标签

附录二:事件类型速查表

事件 触发时机 适用元素
onclick 鼠标单击 所有元素
ondblclick 鼠标双击 所有元素
onmouseenter 鼠标移入 所有元素
onmouseleave 鼠标移出 所有元素
onfocus 获得焦点 input、textarea
onblur 失去焦点 input、textarea
onload 页面加载完成 window

附录三:核心易错点速查表

类别 易错点 正确理解
变量声明 以为JS需要声明类型 var/let声明,不区分类型
变量提升 以为先使用后定义会报错 var会提升,值为undefined
数据类型 混淆undefinednull undefined=未赋值,null=空值
数组类型 以为JS数组元素类型统一 JS数组可混合存放多种类型
对象访问 用中括号访问对象属性 对象用.,数组用[索引]
innerText 以为能解析HTML标签 innerText是纯文本,innerHTML解析标签
input取值 innerHTML获取input值 input用value属性
className 直接使用class属性 JS中class→className
样式属性名 写CSS连字符属性名 转驼峰:background-colorbackgroundColor
getElementById # 不需要,直接传id值
getElementsByClassName 以为返回单个元素 返回数组,需用索引访问
循环绑定事件 循环内用i访问元素 使用this获取当前元素
动态元素事件 页面加载时一次性绑定 每次添加后需重新查找并绑定
删除元素 直接删除元素自身 必须通过父节点调用removeChild()
相关推荐
pluviophile_s1 小时前
数据结构:第7讲:图
数据结构·笔记
林森lsjs1 小时前
零基础吃透二叉树:定义、遍历与高频算法 —数据结构柒
java·开发语言·数据结构·算法·二叉树
疯狂打码的少年1 小时前
【数据库技术】数据库三级模式结构(外模式/模式/内模式)
数据库·笔记·oracle
wind100321 小时前
Qt5Widgets.dll 缺失报错|游戏软件无法启动完整修复教程
开发语言·qt·游戏·电脑
Su米苏1 小时前
针对使用@vue-office 出现的包异常找不到问题
前端·javascript·vue.js
坚定信念,勇往无前1 小时前
electron打包mac ,apple m4出错:ffprobe是x86,不是arm64
前端·javascript·electron
我叫张土豆2 小时前
在 OpenClaw Java 对话框输入一句话后,系统是如何运行的?
java·开发语言·人工智能
牛油果子哥q2 小时前
C++模板万字深度精讲:函数/类模板特化、全特化与偏特化、SFINAE机制、类型萃取、模板元编程入门、工程实战落地
开发语言·c++·面试
专业抄代码选手2 小时前
01|从手写 DOM 到虚拟 DOM:把 UI 从“操作”变成“描述”
前端·javascript·react.js