☰
JavaWeb学习日记:JavaScript从基础到实战,打通前后端交互
2026/10/16 4:32:57 网站建设 项目流程

从Servlet一路学到JSP,我一度觉得JavaWeb也就是“请求-处理-响应”这条路了。直到第一次在浏览器控制台敲下一行JavaScript、看到页面内容被我当场改掉的那一刻,才意识到自己之前做的页面有多“死”。这篇学习日记(二)就来聊聊我在JavaWeb学习进程中重新认识的JavaScript,它不是可有可得的点缀,而是让后端代码真正落地成用户体验的那一層关键粘合层。如果你也正在学Servlet、JSP、JDBC,却发现自己写出的页面交互生硬、表单验証全靠后端弹回,那这篇内容应该能帮你少走不少弯路。


1. 为什么JavaWeb开发绕不开JavaScript

1.1 光靠JSP做出来的页面,总有点“不争气”

学JSP的时候,我做过一个简易的用户注册页面:表单一提交,Servlet接收参数,查库、校验、再往浏览器吐回一段“注册失败,用户名已存在”的提示。功能上没毛病,可用户填完一大堆信息,点一次注册,错一个用户名就得整页刷新重来,别说用户体验了,我自己用着都烦躁。

这种模式的核心问题是:所有判断都发生在服务器端。用户每做一次操作,都得等浏览器把请求发出去、服务器处理完、再把整个页面重新渲染回来。而很多校验其实不需要后端参与——用户名格式、两次密码是否一致、必填项是否为空,这些完全可以在用户点击提交按钮的瞬间,由浏览器里的JavaScript先检查一遍。

说白了,JavaWeb开发中学习JavaScript的第一驱动力不是“前端流行”,而是服务端渲染模式天然笨重,我们需要一门能在浏览器里直接执行的语言,把一部分逻辑从服务器搬到客户端。

1.2 JavaScript在JavaWeb项目里的三个核心阵地

结合我目前经历的实践,JavaScript在JavaWeb开发中的价值可以归纳成三块:

表单前端校验。这是最基础、也最直接的需求。在Servlet处理业务逻辑之前,JavaScript先在浏览器端拦截非法数据,减少无意义的网络请求。实测下来,加了前端校验之后,后端接收到的无效请求能降不少,开发调试的效率也跟着提升。

DOM操作与动态交互。页面元素可以按用户行为动态增删改。比如点击“添加”按钮,表格里多出一行;下拉框选中不同选项,联动显示不同的子内容。这类体验是JSP固定的渲染逻辑做不到的。

Ajax异步请求。在不刷新整个页面的前提下,向Servlet发送请求并获取响应数据。一开始我常拿它做“检查用户名是否被占用”的功能:输入框失焦后发一个请求,后台查完返回结果,页面上直接显示“该用户名可用”或“已被注册”,全程页面不刷新。

1.3 给JavaWeb学习者的JavaScript学习路线

我自己摸索出来的顺序是:先过基础语法,重点掌握变量、函数、对象、数组;然后学DOM操作与事件;接着学Ajax与Fetch;再回到ES6+的新特性补课;最后有余力再看框架。在JavaWeb学习阶段,学到Fetch能够配合Servlet做异步交互,已经足够支撑大部分课程设计和实际项目。

不建议一上来就抱着框架啃。框架解决的是复杂前端工程的架构问题,而JavaWeb学习阶段的核心矛盾还停留在“页面交互与异步请求”。地基没打牢,直接上框架很容易出现“写了半天不知道自己在写什么”的情况。


2. 用Java的思维对比着学JavaScript

2.1 变量声明:从强类型到弱类型

Java是强类型语言,每个变量在声明时就确定了类型,编译期会做大量检查。JavaScript是弱类型语言,变量可以随时指向任何类型的数据。我刚转过来时最大的不适感就在这里,所以特别想提醒同样从Java起步的同学:别用写Java的眼光去审JavaScript代码,否则每一步都会觉得“这也能行?”。

JavaScript声明变量有三个关键字:var、let、const。var是ES5时代的产物,声明会提升到函数作用域顶部,而且不具备块级作用域,写循环时很容易造成变量共享的问题。我在学习之初习惯性用var,结果多次踩到作用域相关的坑。ES6之后的主流实践是:默认用const,如果确定需要重新赋值才用let,var基本可以不用了。

// Java中的变量声明 String name = "Tom"; int age = 18; // JavaScript中的变量声明 const name = "Tom"; let age = 18;

需要注意的是,const只是限制变量引用的绑定不能改变,并不代表变量的值不可变。比如const arr = []之后继续向数组里push元素,是完全合法的。这一点跟Java中final修饰引用类型的语义是一致的,拿这个类比一下就很好理解。

2.2 函数:Java方法之外的另一个世界

Java中方法必须隶属于类,方法重载要靠参数列表区分,方法本身不能作为参数传来传去。JavaScript的函数是一等公民,它可以被赋值给变量、作为参数传递、作为返回值返回,这种特性是Java初学者最容易产生理解障碍的地方。

我第一次接触“回调函数”时,绕了好半天才想明白:所谓回调,其实就是把一个函数当作参数传给另一个函数,由对方在合适的时机调用。写定时器的时候,setTimeout的第一个参数就是一个函数,等到时间到了它再执行。

// Java中常见写法:new Thread(runnable).start(),Runnable匿名内部类 // JavaScript中: function sayHello() { console.log("Hello JavaScript"); } setTimeout(sayHello, 1000); // 也可以直接传匿名箭头函数 setTimeout(() => { console.log("一秒后执行"); }, 1000);

把函数看作一种“可搬运的执行单元”,再理解forEach、map、filter这些数组方法就顺了。它们本质上是接收函数参数、在内部遍历数组并调用传入函数的高级循环。

2.3 对象与原型:别拿Java类硬套

Java中一切对象都有自己的类,类定义了属性和行为,对象是类的实例。JavaScript的对象模型完全不同,它的核心是对象字面量与原型链。

// Java中需要先定义类,再实例化 public class User { private String username; private String password; // 构造器、getter/setter... } // JavaScript中直接创建对象 const user = { username: "Tom", password: "123456", showInfo() { return this.username + " " + this.password; } };

JavaScript中创建对象的方式比Java灵活得多,但这不代表它不需要组织代码的机制。函数被当成构造函数使用时可以用new来创建实例,ES6又提供了class语法糖,让熟悉Java的人更容易上手。不过底层的继承关系靠的是原型链,而不是Java那样严格的类层级。理解这一点之后,再看第三方库里“扩展现有对象”的写法就不会觉得奇怪了。

还有一个概念跟JavaWeb开发强相关:JSON。很多人刚接触时觉得JSON就是JavaScript对象,其实JSON只是一种轻量级的数据交换格式,它有自己独立的语法规则,比如属性名必须用双引号括起来。但正因为语法高度相似,在JavaWeb开发中我们才能拿JavaScript一行代码把JSON字符串解析成对象,拿去做页面渲染。

2.4 异步模型:从多线程到事件循环

Java后端处理并发时,常用多线程方案,线程各自执行,出了问题可以catch异常。JavaScript在浏览器环境是单线程的,但整体异步能力并不弱,靠的是事件循环机制:同步代码先执行,异步任务(网络请求、定时器、事件回调)被挂起,等到触发条件满足后再进入队列依次执行。

这个模型理解起来有个坎:代码顺序不一定等于执行顺序。

console.log("开始"); setTimeout(() => { console.log("延时任务"); }, 0); console.log("结束"); // 实际输出顺序:开始 -> 结束 -> 延时任务

明明延时设的是0毫秒,却依然在“结束”之后执行。这是因为定时器回调被放进了任务队列,要等当前同步代码执行完才轮到它。类似的,Ajax请求的回调函数不会立刻执行,必须等服务端返回、事件循环轮到它。这解释了为什么在请求返回之前访问响应数据会得到undefined。

面对异步回调的层层嵌套,现代JavaScript提供了Promise和async/await。Promise把异步操作包装成一个对象,用then串联后续步骤;async/await则把异步代码写得像是同步代码。JavaWeb阶段能把Promise链写明白,就已经很够用了。


3. JavaWeb场景下的JavaScript实操

3.1 表单校验:拦截在提交之前

我做的第一个实际功能是注册表单校验。核心思路:在form的onsubmit事件中调用校验函数,返回true才允许表单提交,返回false则阻止提交。

<form action="/register" method="post" onsubmit="return validateForm()"> <input type="text" id="username" name="username" placeholder="用户名"> <span id="usernameTip"></span><br> <input type="password" id="password" name="password" placeholder="密码"> <span id="passwordTip"></span><br> <button type="submit">注册</button> </form> <script> function validateForm() { const username = document.getElementById("username").value.trim(); const password = document.getElementById("password").value.trim(); let valid = true; if (username.length < 3 || username.length > 12) { document.getElementById("usernameTip").textContent = "用户名长度需在3~12个字符之间"; valid = false; } else { document.getElementById("usernameTip").textContent = ""; } if (password.length < 6) { document.getElementById("passwordTip").textContent = "密码长度不能少于6位"; valid = false; } else { document.getElementById("passwordTip").textContent = ""; } return valid; } </script>

这里有一个细节值得注意:onsubmit="return validateForm()"必须写return,否则函数返回的false无法传递到事件处理流程中,表单照样会提交。我第一次就漏了这个return,头皮挠了半天才发现问题。

前端校验虽然好用,但绝不能作为唯一防线。用户完全可以绕过浏览器直接构造请求,因此Servlet端必须做同样的校验。这一点我在学Servlet时已经有体会了,JavaScript不是替代后端校验,而是减少无效请求、提升体验的辅助手段。

3.2 DOM操作与事件处理

DOM把HTML文档解析成一棵对象树,JavaScript通过操作这棵树来实现页面动态效果。最常用的API是document.getElementById("id")和document.querySelector("选择器"),前者按id取元素,后者可以按CSS选择器规则取元素,灵活度更高。

事件绑定方面,早期写法是元素上的onclick="函数名()",简单直接,缺点是一个事件只能绑定一个处理函数。更推荐的方式是addEventListener:

const btn = document.getElementById("checkBtn"); btn.addEventListener("click", function() { const info = document.getElementById("infoDiv"); info.style.display = info.style.display === "none" ? "block" : "none"; });

addEventListener可以给同一个元素的同一个事件绑定多个处理函数,而且可以方便地移除。它还支持事件冒泡阶段的捕获控制,这在做事件委派时特别有用。

事件委派是我后来才学会的技巧。比如一个动态生成的列表,每个li都要响应点击,最初的做法是生成一个li就绑定一个事件,操作繁琐而且难以维护。正确做法:把事件绑定在父容器上,利用事件冒泡,通过event.target判断实际点击的元素。

const list = document.getElementById("userList"); list.addEventListener("click", function(event) { if (event.target.tagName === "LI") { console.log("点击了用户:" + event.target.textContent); } });

对于后端主攻的同学来说,DOM操作容易陷入“学着学着变成写前端”的困惑。我的体会是:掌握常用元素的增删改查、样式切换、textContent与innerHTML的区分,就可以应对90%的课程设计和实际工作了,不需要死磕每个API。

3.3 Ajax异步请求:前后端打通的关键

学完Servlet之后,我实现过一个“用户名查重”的前后端联动功能,逻辑虽然简单,却是第一次让我体会到什么是“异步”。前端发送Ajax请求,Servlet查库返回JSON,页面局部显示结果,全程没有刷新。

最早的实现使用原生XMLHttpRequest:

const xhr = new XMLHttpRequest(); xhr.open("GET", "/checkUsername?username=" + encodeURIComponent(username), true); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { const result = JSON.parse(xhr.responseText); document.getElementById("msg").textContent = result.available ? "可用" : "已被占用"; } }; xhr.send();

后来改用fetch,写法直观不少:

fetch("/checkUsername?username=" + encodeURIComponent(username)) .then(response => response.json()) .then(data => { document.getElementById("msg").textContent = data.available ? "可用" : "已被占用"; }) .catch(error => { console.error("请求出错", error); });

在使用过程中有几个容易踩的点:

URL编码。用户名如果包含中文或特殊字符,直接拼在URL里可能出问题,用encodeURIComponent转一下更稳妥。

POST请求的Content-Type。Servlet端如果用request.getParameter("xxx")取值,前端POST数据时用application/x-www-form-urlencoded;如果约定传递JSON,后端就要用request.getReader()读取JSON字符串并自行解析。前后端对传参格式一定要约定好,这是联调时最常见的矛盾来源。

响应状态与异常处理。后端返回的非200状态需要单独处理,网络异常也要捕捉,不能只关注成功分支。有一次后端500,前端却以为是数据正常返回,页面显示了“undefined”,排查了半天才发现是异常逻辑没写。

3.4 动态渲染用户信息

学会了Ajax请求之后,我又做了一个小功能:从Servlet获取用户列表JSON,动态渲染成表格。展示过程中用到数据遍历与模板字符串拼接,这也是JavaScript在数据展示层面的典型应用。

fetch("/userList") .then(response => response.json()) .then(users => { const tbody = document.getElementById("userTbody"); let html = ""; users.forEach(user => { html += `<tr> <td>${user.id}</td> <td>${user.username}</td> <td>${user.email}</td> </tr>`; }); tbody.innerHTML = html; });

这里用了ES6的模板字符串:用反引号包裹字符串,${表达式}直接插入变量。相比以前一层层用加号拼接字符串、还要小心引号逃逸的写法,可读性和维护性好太多了。对Java后端来说,这很像Java 15之后的文本块,把多行文本的内容原样保留,变量直接嵌入。

不过需要注意:动态渲染时如果数据来自不可信来源,直接在innerHTML里插入用户输入内容可能造成XSS注入风险。学习阶段也许不在意,但从安全习惯的角度,建议优先采用textContent赋值,或者对用户输入做HTML转义。这个我在后面排查问题时又回想起来,原来服务端做输出转义与前端做DOM净化,本质是同一个安全问题在浏览器侧的表现。


4. 学习JavaScript时踩过的坑

4.1 var的变量提升与全局污染

我早期写JavaScript习惯沿用var,结果在循环里找元素点击事件的老问题就碰上了:循环中用var i,等到点击事件触发时,i已经变成了循环结束时的最大值。当时百思不得其解,后来才明白var不具备块级作用域,循环变量实际上是同一个变量,事件回调闭包捕获的是同一个绑定的最终值。

改用let后,每次循环都会创建独立的块级作用域绑定,问题就消失了。

for (let i = 0; i < buttons.length; i++) { buttons[i].addEventListener("click", function() { console.log("按钮序号:" + i); }); }

var带来的另一个麻烦是全局污染。在函数外部或脚本顶层直接用var声明,会挂到window对象上,多个脚本之间容易互相干扰。学习阶段养成好习惯:所有变量尽量声明在函数或模块作用域内,配合let、const使用,减少莫名其妙的状态冲突。

4.2 this指向容易飘

Java中this的含义固定在当前实例上,JavaScript里this的指向却取决于函数的调用方式,这是从Java转向JavaScript时最头疼的点之一。

普通函数调用时,this指向全局对象(浏览器里是window);对象方法调用时,this指向该对象;事件处理函数里,this指向触发事件的元素。而箭头函数的this不取决于调用方式,而是继承自定义时所在的词法作用域。

当年学到这里,我选择直接记结论:在事件回调中要访问外层对象时,用箭头函数或提前把this存成变量。

const userCard = { name: "Tom", init() { const self = this; document.getElementById("btn").addEventListener("click", function() { // 这里的 this 是按钮元素,不是 userCard console.log(self.name); }); } };

后来改用箭头函数,写法更简洁:

const userCard = { name: "Tom", init() { document.getElementById("btn").addEventListener("click", () => { console.log(this.name); }); } };

4.3 回调地域与Promise排解

我第三次写异步功能时,把“获取用户信息、再获取其订单、再获取订单详情”这三步请求串成嵌套回调,代码丑到自己都不想看第二眼:

fetch("/user/1") .then(res => res.json()) .then(user => { fetch("/user/" + user.id + "/orders") .then(res => res.json()) .then(orders => { fetch("/order/" + orders[0].id) .then(res => res.json()) .then(detail => { console.log(detail); }); }); });

后来尝试用async/await重写,结构瞬间清爽:

async function loadOrderDetail() { const userRes = await fetch("/user/1"); const user = await userRes.json(); const orderRes = await fetch("/user/" + user.id + "/orders"); const orders = await orderRes.json(); const detailRes = await fetch("/order/" + orders[0].id); const detail = await detailRes.json(); console.log(detail); }

await会暂停当前异步函数的执行,等待Promise完成后再继续往下走,但它并不会阻塞整个浏览器线程。这个点在脑子里转了几天才真正理解:它“暂停”的粒度是函数内部的执行流程,而不是整个程序。

注意:await只能在async函数中使用。如果看到“await is only valid in async function”的报错,多半是外层函数没加async。

4.4 字符串拼接与引号逃逸

JavaWeb初期写JavaScript,经常要拼HTML字符串。层层加号与引号混在一起,尤其还要在HTML里嵌JavaScript代码时,转义写起来非常痛苦。

// 痛苦的写法 var html = '<a href="javascript:void(0);" onclick="showUser(\'' + name + '\')">查看</a>';

改用模板字符串之后:

const html = `<a href="javascript:void(0);" onclick="showUser('${name}')">查看</a>`;

不过这里还要提醒一句:模板字符串拼接用户输入直接进HTML时,要小心注入问题。前端拼接内部可知内容还好,拼接外部数据时留个心眼,宁可使用textContent赋值或者对特殊字符做转义处理。


5. 常见问题排查与进一步学习建议

5.1 快速排查速查表

症状常见原因解决思路
页面显示空白,控制台无报错JavaScript文件没有引入,或引入的路径不对打开网络面板确认脚本文件是否加载成功,检查路径与部署目录
控制台报“Cannot read property of undefined”某个变量还是undefined时就访问了它的属性在访问前置变量或调用处打印日志,确认数据是否返回
表单提交没有经过校验函数忘了写return validateForm()检查onsubmit属性是否带return
循环绑定点击事件,序号总是最后一个var循环变量被后续迭代覆盖改用let声明循环变量
fetch请求能发出,但后端拿不到参数Content-Type与Servlet读取方式不匹配确认约定的是form编码还是JSON,后端用对应的读取方式
页面局部刷新变成整页刷新按钮默认提交行为没阻止调用event.preventDefault()或使用button type="button"
中文数据乱码URL编码或响应编码不一致前端用encodeURIComponent,后端设置统一的字符编码过滤器
代码没改动但效果时好时坏浏览器缓存了旧JavaScript文件开发阶段禁用缓存,或在引用路径后加版本参数

5.2 调试技巧

浏览器F12打开DevTools,Console面板可以直接执行任意JavaScript表达式,这是我学习时最常用的实验场。Sources面板可以给JavaScript代码打断点,一步一步观察变量值的变化,比纯靠console.log定位问题高效得多。

Network面板值得单独介绍给JavaWeb学习者。每一次Ajax请求的URL、请求头、请求体、响应状态、响应体都看得一清二楚,联调阶段绝大多数前后端问题都能在这里找到线索。有一次后端接口联调失败,前端一直报跨域,我在Network面板里发现请求根本没有发出,最后问题出在按钮被误触发了两次事件,跟跨域毫无关系。

5.3 学习节奏与后续方向

JavaWeb阶段学JavaScript建议以“够用”为底线,但不以“会用”为终点。我个人的节奏是:先用JavaScript把页面交互做出来,配合Servlet完成异步请求,把学习项目跑通;然后回头补ES6+语法细节,理解Promise、模块化、解构赋值等特性;最后才考虑是不是要接触前端框架。

如果后续课程设计需要做更复杂的前端界面,可以尝试引入组件化框架,但不要在一开始就陷入构建工具与工程化的泥潭。JavaWeb阶段的重心仍然是后端逻辑与数据流,JavaScript是给你补上从前端视角理解系统的能力,最终目标是把一个完整的请求链路串起来:浏览器事件到JavaScript代码,再到Ajax请求,再到Servlet处理,再返回数据渲染到页面。

这个链路一旦在心中打通,再往后学什么框架、什么中间件,都只是在这条主线上增加新的角色而已。


说点学习之外的个人体会。JavaScript对我这种Java出身的人来说,最大的障碍不是语法,而是思维模型。给它一个变量,它可以随时换类型;给它一个函数,它可以被传来传去;给它一个异步流程,它不按代码顺序执行。这些特性初看都是“缺点”,可正是它们撑起了前端交互的灵活性。

我自己的建议是:不要在语法层面停留太久,尽快把JavaScript放进JavaWeb的真实场景里用起来。写一个表单校验、做一个用户名查重、渲染一份用户列表,比抽象地刷一百道语法题更管用。遇到页面不刷新却更新了数据的那一刻,你会觉得之前读过的那些枯燥概念一下全都连接起来了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询