JavaScript基础 - 语法与DOM
JavaScript 基础 - 语法与 DOM
HTML 搭骨架, CSS 穿衣服, 到这里为止页面还是死的: 点按钮没反应, 数据也不会变。JavaScript (下面都简称 JS) 就是让页面活过来的编程语言。这篇是整条前端路线里最重要的一篇, 因为后面的 Vue 说到底就是换了种方式写 JS, 这里的每个语法点 (尤其 ES6 那批) 到时候都会高频出现。
我是带着 Java 基础来学的, 所以这篇笔记的写法是: 和 Java 一样的东西一笔带过, 只记不一样的和 Java 里没有的。
JS 是什么, 在哪里跑
JS 代码由浏览器里的引擎直接解释执行, 不用编译, 没有 main 方法, 从上到下顺着跑。把它和页面连起来有三种写法:
代码块收起展开
<!-- 1. 外部文件, 推荐: 结构和逻辑分离 -->
<script src="script.js" defer></script>
<!-- 2. 直接写在 HTML 里, 小例子用 -->
<script>
console.log('Hello'); // console.log 就是前端的 System.out.println
</script>
<!-- 3. 写在标签属性里, 别这么干, 难维护 -->
<button onclick="alert('点击了')">点击</button>defer 值得解释一下: script 标签默认是”加载到就立刻执行”, 如果它写在 <head> 里, 执行时 body 还没解析, 找页面元素会找不到。
defer 让脚本等 HTML 全部解析完再跑, 所以加了它就能放心把 script 放头部。
不加 defer 的老写法是把 script 放 </body> 前面, 效果类似。
变量: let 和 const
代码块收起展开
let count = 0; // 会变的量
const name = '张三'; // 不会变的量, 类似 Java 的 final
var old = 1; // 老写法, 有作用域坑, 新代码别用我的习惯 (也是主流习惯): 默认 const, 确实要重新赋值才用 let。注意 const 锁的是”变量指向谁”, 不是”内容不能改”: const arr = [] 之后照样可以 arr.push(1), 这一点和 Java 的 final 引用一模一样。
数据类型: 动态类型的世界
JS 和 Java 最大的观感差异: 变量不声明类型, 一个变量一会儿装字符串一会儿装数字都合法, 类型错误要到运行时才炸。写惯静态类型的人前期会很不适应, 这也是后面 TypeScript 存在的理由 (工具链篇再说)。
代码块收起展开
let str = 'hello'; // 字符串, 单双引号都行
let num = 42; // 数字, 不分 int/double, 就一种 number
let ok = true; // 布尔
let nothing = null; // 主动表示"空"
let notDefined; // 声明了没赋值, 就是 undefined
typeof num; // 'number', 运行时查类型null 和 undefined 的区别我这么记: undefined 是”根本没给过值”, null 是”我特意放了个空进去”。
比较运算必须记一个坑: <mark> 会偷偷做类型转换, 1 </mark> '1' 竟然是 true。所以永远用 <mark>= (值和类型都相等), 就像 Java 里比字符串永远用 equals 不用 一样, 属于肌肉记忆级别的规矩。
另外两个 Java 没有但很好用的运算符:
代码块收起展开
let value = input ?? '默认值'; // 空值合并: input 是 null/undefined 时才用默认值
let email = user?.email; // 可选链: user 是 null 时不炸, 整体返回 undefined可选链约等于把 Java 里 if (user != null) user.getEmail() 那串判空压缩成一个问号, 香。
模板字符串
反引号包裹, 里面 ${} 直接插值, 还能换行:
代码块收起展开
let msg = `你好, ${name}, 明年${age + 1}岁`;有了它就再也不用 '你好, ' + name + ... 这种拼接了。Vue 模板里的 {{ }} 插值思路同源, 先在这里习惯。
数组: 前端的 Stream API
代码块收起展开
let arr = [1, 2, 3, 4, 5];
arr.push(6); // 尾部加
arr.pop(); // 尾部删
arr.length; // 长度是属性不是方法, 没有括号真正的重头戏是这批函数式方法, 用起来和 Java 的 Stream 几乎一个手感, 只是不用先 .stream() 也不用最后 .collect():
代码块收起展开
arr.map(x => x * 2); // 映射: [2, 4, 6, 8, 10]
arr.filter(x => x > 3); // 过滤: [4, 5]
arr.find(x => x > 3); // 找第一个: 4
arr.some(x => x > 3); // 有没有满足的: true
arr.reduce((sum, x) => sum + x, 0); // 归约: 15, 第二个参数是初始值
arr.includes(3); // 包含: true
arr.join(','); // 拼成字符串: '1,2,3,4,5'这批方法必须练熟, 熟到条件反射。Vue 里渲染列表、筛选数据、算合计, 全是它们。
对象: 长得像 Map 的东西
JS 的对象不用先写类, 直接字面量造:
代码块收起展开
let user = {
name: '张三',
age: 25,
greet() {
console.log(`你好, 我是${this.name}`);
}
};
user.name; // 点号访问
user['age']; // 也可以像 Map 一样用键访问
user.email = 'x@y.com'; // 随时加属性, 没有类定义拦着你
delete user.age; // 随时删
Object.keys(user); // 所有键
Object.entries(user); // 键值对数组, 类似 Java 的 entrySet对 Java 人的理解捷径: 它像一个键固定写法更宽松的 HashMap, 又能挂方法。没有类也能干活这件事, 前期不习惯, 后来真香。
解构和展开: Vue 代码里的常客
代码块收起展开
// 解构: 把对象/数组按名字拆出来
let { name, age } = user; // 等价于两行赋值
let { name: userName } = user; // 拆出来顺便改名
let [first, second] = [1, 2, 3]; // 数组按位置拆
// 展开: 把对象/数组摊开
let copy = { ...user }; // 浅拷贝一个对象
let merged = { ...user, age: 26 }; // 拷贝并覆盖某字段
let all = [...arr1, ...arr2]; // 合并数组这两个语法在 Vue 项目里出现频率极高 (组件接参数、改状态、传 props 全用), 现在就要看熟。
函数: 从 function 到箭头函数
代码块收起展开
// 经典写法
function greet(name) {
return `你好, ${name}`;
}
// 箭头函数, 等价于 Java 的 lambda
const greet2 = (name) => `你好, ${name}`; // 单行时连 return 都省了
const add = (a, b) => a + b;
// 默认参数和可变参数
function log(msg = '空') { }
function sum(...nums) { // 相当于 Java 的可变参数 int... nums
return nums.reduce((t, n) => t + n, 0);
}箭头函数基本就是 lambda, 上面数组方法里已经在大量用了。它和 function 有个深层区别: 箭头函数没有自己的 this。现在不用深究, 记一个结论: 回调、数组方法里无脑用箭头函数, 就不会掉 this 的坑。
控制流 (if/else、switch、for、while、三元) 和 Java 几乎完全一样, 不抄了, 只记两个 Java 没有的:
代码块收起展开
for (let item of arr) { } // for...of 遍历数组的值, 相当于 Java 的 for-each
for (let key in obj) { } // for...in 遍历对象的键, 注意是键不是值DOM: JS 操作页面的手
浏览器把 HTML 解析成一棵对象树, 叫 DOM (Document Object Model)。HTML 里每个标签都对应树上一个节点对象, JS 拿到节点就能改它, 页面立刻跟着变。这就是”JS 让页面动起来”的全部原理。
选元素
代码块收起展开
let box = document.querySelector('.box'); // 选第一个匹配的, 参数就是 CSS 选择器
let items = document.querySelectorAll('li'); // 选所有匹配的querySelector 系列是现代标准答案, 参数直接复用 CSS 篇学的选择器语法 (#id、.class、div > p 都能用), 老 API (getElementById 那批) 见到认识就行。
改内容、样式、属性
代码块收起展开
box.textContent = '新文本'; // 改文本 (安全, 内容只当文字)
box.innerHTML = '<b>加粗</b>'; // 改 HTML (会解析标签, 别塞用户输入, 有注入风险)
box.style.color = 'red'; // 单改一条内联样式
box.classList.add('active'); // 更推荐: 加减 class, 样式写在 CSS 里
box.classList.toggle('open'); // 有则删无则加
img.src = 'new.jpg'; // 常用属性直接点出来改
input.value; // 表单输入框的当前值, 读写都靠它增删节点
代码块收起展开
let li = document.createElement('li'); // 造节点
li.textContent = '新的一项';
list.append(li); // 挂到父节点尾部
li.remove(); // 删掉自己事件: 给页面装监听器
代码块收起展开
let btn = document.querySelector('#saveBtn');
btn.addEventListener('click', (event) => {
console.log('点击了', event.target); // event.target 是触发事件的元素
});模式上就是 Java GUI 那套监听器/回调: 注册一个函数, 事件发生时浏览器来调它。常用事件名: click、input (每敲一个字符触发)、change (值改完失焦触发)、submit (表单提交)、keydown。
表单提交有个必知的坑: 浏览器默认会刷新页面提交表单, 要自己接管就得拦下来:
代码块收起展开
form.addEventListener('submit', (e) => {
e.preventDefault(); // 阻止默认的刷新式提交
// 这里自己处理数据
});定时器
代码块收起展开
let id = setTimeout(() => console.log('2秒后执行一次'), 2000);
clearTimeout(id); // 取消
let id2 = setInterval(() => console.log('每秒一次'), 1000);
clearInterval(id2); // 不清理会一直跑, 记得清localStorage: 浏览器里的小仓库
代码块收起展开
localStorage.setItem('name', '张三'); // 存, 永久有效 (除非手动清)
localStorage.getItem('name'); // 取
localStorage.removeItem('name'); // 删
// 只能存字符串, 对象要先序列化, 和 Redis 存 JSON 一个思路
localStorage.setItem('user', JSON.stringify(user));
let saved = JSON.parse(localStorage.getItem('user'));还有个 sessionStorage, API 一模一样, 区别是关掉标签页就清空。
异步: JS 最特别的部分
JS 在页面里只有一根线程, 如果发个网络请求要傻等三秒, 这三秒页面就完全卡死, 点啥都没反应。所以 JS 的规矩是: 耗时操作一律异步, 发起之后立刻继续往下走, 结果好了再回来通知你。
“通知你”的写法演进了三代, 三代都会在真实代码里遇到:
第一代, 回调函数: 把”完成后干嘛”作为函数传进去。嵌套两三层就成了金字塔, 人称回调地狱。
第二代, Promise: 把”未来才有的结果”包成对象, 用链式 .then 接:
代码块收起展开
fetch('/api/users')
.then(response => response.json()) // 上一步的结果传给下一步
.then(data => console.log(data))
.catch(err => console.log(err)); // 链上任何一环出错都落到这第三代, async/await: 让异步代码长得像同步, 可读性最好, 现在的默认写法:
代码块收起展开
async function getUsers() { // async 标记这个函数里有异步操作
try {
let response = await fetch('/api/users'); // await: 等结果, 但不卡死页面
let data = await response.json();
console.log(data);
} catch (err) { // 熟悉的 try/catch 又回来了
console.log(err);
}
}await 只能出现在 async 函数里。理解要点: 它”等”的时候线程没有被阻塞, 浏览器照常响应用户, 结果到了再回到这行继续。
fetch: 前端的 HTTP 客户端
fetch 是浏览器内置的请求 API, 角色相当于 Java 里的 HttpClient/RestTemplate。GET 见上面, POST 长这样:
代码块收起展开
let response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // 告诉后端我发的是 JSON
body: JSON.stringify({ name: '张三', age: 25 }) // 请求体必须手动序列化
});写 Spring 接口时接的 @RequestBody, 就是从这儿发出去的。有个隐蔽坑先记着: fetch 对 404/500 这类 HTTP 错误码并抛异常, 要自己查 response.ok, 这个坑到 Vue 请求层封装时会正式处理。
串起来: 数据驱动的萌芽
三个小例子把语法、DOM、事件连起来。注意它们共同的形状: 数据变了, 手动去更新页面。
// 计数器: 状态是 count, 每次点击后手动同步到页面
let count = 0;
btn.addEventListener('click', () => {
count++;
display.textContent = count; // 这行"手动同步"就是日后 Vue 替我们干的活
});// 列表渲染: 数据数组循环造 DOM
let users = [{ name: '张三', age: 25 }, { name: '李四', age: 30 }];
users.forEach(u => {
let li = document.createElement('li');
li.textContent = `${u.name}, ${u.age}岁`;
list.append(li);
});// 表单校验: 拦截提交, 检查通过才放行
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!emailInput.value.includes('@')) {
alert('邮箱格式错误');
return;
}
console.log('提交', emailInput.value);
});写多了就会烦: 每次改数据都要惦记着改 DOM, 改漏一处页面就和数据对不上。这个痛点先攒着, 完整示例篇会攒到顶点, 然后 Vue 篇一次性解决。