
这篇文章把JavaScript语句从基础到实战掰碎了讲:从变量声明(var/let/const)、条件语句(if/switch)、循环语句(for/while)这些“地基”,到函数语句、异常处理(try/catch),再到异步场景下的Promise、async/await实战,每部分都结合真实开发场景讲清“为什么用”“怎么用对”。比如帮你搞懂“let为什么比var更安全”“for…of和for…in到底该选谁”,甚至解决“async函数里的await为什么会阻塞”这类高频问题。
不管是刚入门的新手想打牢基础,还是有经验的开发者想补全知识漏洞,看完这篇都能真正“吃透”JavaScript语句,写代码时更稳、更高效。
你有没有过这种情况?学JavaScript的时候,变量声明用var、let、const总搞混,写循环时变量越界,异步请求的逻辑越理越乱?我之前刚入门时也这样——明明背了语法,写代码还是踩坑:比如用var声明循环变量,点每个列表项都弹出最后一个索引;写异步请求嵌套五六个then,代码像“金字塔”;甚至因为没加try/catch,接口报错直接让页面崩溃。直到后来我把这些语句的底层逻辑和场景用法摸透,再写代码才顺了起来。今天就把我整理的“吃透JS语句”的方法分享给你,不用记复杂概念,跟着大白话走,马上能用。
先把基础语句的“底层逻辑”掰碎,别再死记硬背
很多人学语句总陷在“背语法”里,但其实逻辑比语法重要100倍。比如变量声明的三个关键字,我之前踩过的坑比你想的还多——去年帮朋友做todo列表,用var声明循环变量i,结果点每个todo都弹出“第3项”(明明只有2个项),查了半天才知道:var是函数作用域,整个循环里的i是同一个变量,点击事件触发时,i已经变成循环结束的数值了。后来换成let(块级作用域),每轮循环都生成新的i,问题立马解决。
为了让你一眼看清区别,我做了个对比表,都是我踩坑踩出来的经验:
声明方式 | 作用域 | 是否可重复声明 | 是否可修改 |
---|---|---|---|
var | 函数内生效(函数作用域) | 是(重复声明会覆盖) | 是 |
let | {}内生效(块级作用域) | 否(重复声明报错) | 是(变量值可改) |
const | {}内生效(块级作用域) | 否(重复声明报错) | 否(但对象/数组的属性可改) |
现在我写代码,90%的场景用let和const:const用来声明不变的值(比如接口地址、配置项),let用来声明会变的变量(比如循环索引、表单输入值)。var?除非要兼容IE8这种老浏览器,否则我碰都不碰——毕竟块级作用域比函数作用域“安全”太多。
再说说条件语句,if和switch的区别。我之前写过一个“判断用户会员等级”的代码,用if写了五六个分支,后来改成switch,代码直接少了一半:
// 原来的if写法
if (level === 1) {
showPrivilege('普通会员:享9折优惠');
} else if (level === 2) {
showPrivilege('高级会员:享8折+免运费');
} else if (level === 3) {
showPrivilege('VIP会员:享7折+专属客服');
} else {
showPrivilege('游客:需登录才能享受优惠');
}
// 改成switch后
switch (level) {
case 1:
showPrivilege('普通会员:享9折优惠');
break; // 千万别漏break!
case 2:
showPrivilege('高级会员:享8折+免运费');
break;
case 3:
showPrivilege('VIP会员:享7折+专属客服');
break;
default:
showPrivilege('游客:需登录才能享受优惠');
}
你看,是不是更清爽?我 的规律是:判断“范围”用if(比如“年龄>18”“分数在80-100之间”),判断“具体值”用switch(比如会员等级、商品类型)。对了,switch里的break一定要加——我之前漏写过一次,结果从case 1直接跑到case 3,输出了两个优惠信息,排查了半小时才找到问题。
循环语句也是高频踩坑点,比如for、while、for…of的区别。我之前遍历数组用for循环写let i=0; i,后来发现for...of更省心:
for (let item of arr) { console.log(item); }
——不用管索引,直接拿元素。而for...in呢?是用来遍历对象属性的,比如遍历用户信息:
const user = { name: '张三', age: 25, gender: '男' };
for (let key in user) {
if (user.hasOwnProperty(key)) { // 过滤原型链上的属性
console.log(${key}:${user[key]}
);
}
}
但要注意,for…in会遍历对象原型链上的属性(比如toString方法),所以一定要加hasOwnProperty
判断——我之前没加,结果把原型上的方法也打印出来了,差点把朋友的项目搞崩。
还有循环里的异步问题,你肯定遇到过:
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出3个3,不是0、1、2!
}, 1000);
}
为什么?因为var是函数作用域,循环里的i是同一个变量,setTimeout执行时,i已经变成3了。解决办法很简单:把var改成let——let是块级作用域,每轮循环都会生成新的i,这样输出就是0、1、2了。我现在写循环,只要涉及异步,必用let声明索引。
常用场景下的语句技巧,帮你少走90%的弯路
基础逻辑搞懂后,接下来要学场景化技巧——比如“什么时候用箭头函数”“怎么处理异步超时”,这些才是真正能提升效率的东西。
先讲函数语句:函数声明vs函数表达式vs箭头函数。我之前写回调函数时踩过坑:
// 函数声明:会“提升”(浏览器解析时先读函数)
setTimeout(sayHi, 1000); // 正确,1秒后执行
function sayHi() {
console.log('Hi');
}
// 函数表达式:不会提升(必须先声明再调用)
setTimeout(fn, 1000); // 报错!fn未定义
const fn = function() {
console.log('Hi');
};
箭头函数呢?是ES6的“语法糖”,比普通函数更简洁,但没有自己的this——我之前写对象方法用了箭头函数,结果this指向window,输出undefined:
const user = {
name: '张三',
sayName: () => {
console.log(this.name); // 输出undefined
}
};
后来改成普通函数就好了:
const user = {
name: '张三',
sayName: function() {
console.log(this.name); // 输出“张三”
}
};
所以我 箭头函数适合“不关心this”的场景(比如数组遍历、回调函数),而对象方法、构造函数就用普通函数——毕竟this指向对了,代码才不会错。
再说说异常处理(try/catch),这是我踩过最疼的坑之一。去年做一个“提交表单”的功能,调用接口时没加try/catch,结果接口返回500错误,直接导致页面崩溃——用户点了提交没反应,还以为按钮坏了。后来我加上try/catch,不仅捕获了错误,还给出友好提示:
async function submitForm(formData) {
try {
const res = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
});
const data = await res.json();
if (data.code === 0) {
alert('提交成功!');
} else {
alert(提交失败:${data.msg}
);
}
} catch (err) {
console.error('接口错误:', err);
alert('网络有点问题,稍后再试~');
}
}
现在我处理异步请求、操作DOM、解析JSON这些可能出错的场景,必加try/catch——总比让用户看到白屏好。
最后讲异步语句,Promise和async/await是处理异步的“神器”。我之前处理“同时获取用户信息和订单列表”的需求,用Promise.all并行请求,比一个个等快了3倍:
// 并行请求,等两个都完成再处理
Promise.all([fetchUser(), fetchOrders()])
.then(([user, orders]) => {
renderUser(user);
renderOrders(orders);
})
.catch(err => {
console.error('请求失败:', err);
});
而async/await是Promise的“语法糖”,写起来更像同步代码,比如上面的例子改成async/await:
async function getInfo() {
try {
const [user, orders] = await Promise.all([fetchUser(), fetchOrders()]);
renderUser(user);
renderOrders(orders);
} catch (err) {
console.error('请求失败:', err);
}
}
是不是比then链清爽多了?我现在写异步代码,90%用async/await,只有需要并行请求时用Promise.all。
还有一个超实用的技巧:处理异步超时。比如请求超过5秒就提示“超时”,我之前做图片上传功能时用过:
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController(); // 用来终止请求
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId); // 请求成功,清除超时定时器
return res.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error('请求超时,请稍后再试');
}
throw err; // 其他错误原样抛出
}
}
用户上传大图片时,超过5秒就会看到“上传有点慢,换张小点的图试试?”的提示,再也不会因为超时而摸不着头脑了。
如果你按我讲的这些方法试了——比如把var换成let、用switch代替多分支if、用async/await优化异步代码——欢迎回来告诉我效果!有问题也可以留言,我尽量帮你解答。毕竟学JS语句,踩坑不是目的,踩完坑能记住才是。我之前踩过的坑,现在都变成了“经验”,希望你也能一样~
你肯定遇见过用var声明循环变量的坑吧?之前我帮朋友写todo列表,用var i=0循环生成列表项,结果点每个项都弹出“第3项”——明明只有2个项啊!后来查了半天才搞懂,var是“函数作用域”,整个循环里的i就一个,等点击事件触发时,i已经跑完循环变成3了。换成let就不一样了,let是“块级作用域”,每轮循环都会新生成一个i,各自存着当前的索引,点哪个就弹哪个,再也不串号了。
再说说const,我现在写代码几乎每天都用它——比如声明接口地址、配置参数,像const BASE_URL = ‘https://api.example.com’,这样就不会不小心被改成别的地址,稳得很。但你别误会,const不是说声明的东西彻底不能变,如果是对象或者数组,比如const user = {name: ‘张三’},你改user.name = ‘李四’是完全可以的——const管的是变量本身的“引用”(就是这个变量只能指向这个对象),但对象里面的属性想改还是能改的,这点要记清楚,别踩坑。
其实这三个关键字的核心区别就仨点:作用域、能不能重复声明、值能不能改。var是老派写法,函数作用域,能重复声明也能改值,但容易出问题;let是块级作用域,不能重复声明,但值能改,日常用变量(比如循环索引、表单输入值)用它准没错;const也是块级,但不能重复声明,值也不能直接改,适合放那些固定不变的东西(比如接口地址、配置项)。现在开发里谁还常用var啊?除非要兼容特别老的浏览器,不然优先用let和const,既能避免变量泄露,又能让代码更清晰,我自己用了两年,没再踩过类似的坑。
var、let、const的核心区别是什么?
三者的区别主要在作用域、重复声明和值的可修改性上:var是函数作用域,可重复声明且值能改;let是块级作用域({}内生效),不能重复声明但值能改;const也是块级作用域,不能重复声明且值不能直接改(但对象/数组的属性可以修改)。日常开发中,优先用let和const,var尽量少用。
for…of和for…in分别适合什么场景?
for…of适合遍历数组、字符串等“可迭代对象”的元素,直接拿到具体值,不用管索引;for…in适合遍历对象的属性(比如用户信息的键值对),但要注意加hasOwnProperty
过滤原型链上的属性。简单说,遍历“元素”用for…of,遍历“对象属性”用for…in。
async函数里的await为什么会“阻塞”?
await的“阻塞”是指暂停当前async函数的执行,等待后面的Promise完成,但不会阻塞整个JavaScript线程(比如其他同步代码还是会正常运行)。比如你用await等接口请求时,async函数会停在这一步,直到请求返回结果,再继续执行后面的代码——这样写起来像同步代码,逻辑更清晰。
try/catch应该用在哪些场景?
try/catch适合包裹“可能出错”的代码,比如异步接口请求(防止网络错误)、操作DOM(比如获取不存在的元素)、解析JSON(比如后端返回非法JSON)。用了try/catch后,即使代码出错,也能捕获错误并给用户友好提示(比如“网络有点问题”),而不是让页面崩溃。
箭头函数为什么不能用来写对象方法?
因为箭头函数没有自己的this
,它的this
会继承外层作用域的this
(比如全局的window)。如果用箭头函数写对象方法(比如user.sayName = () => { ... }
),调用时this
指向的是window,而不是对象本身,导致拿不到对象的属性(比如this.name
会是undefined)。所以对象方法最好用普通函数。