学JavaScript到第五天,我已经不再满足于控制台里打印hello world了。这一天手机里的搜索记录几乎被同一个开头的词塞满:javascript运行时报错、javascript函数、javascript:void(0)、vue javascript项目、为什么elmessage还是提示未定义……每一个热搜词背后,都是一个今天踩过的坑。如果你也刚开始学JS,或者刚把一个JS项目跑起来,这篇就当我的day5学习笔记兼避坑日记看吧,里面提到的每个词都是真实搜索过、确实绕了弯的路。
1. 第五天的高频词,几乎全是报错和看不懂的语法
1.1 让人摸不着头脑的 javascript:void(0)
第五天打开某个老项目的页面,满屏a标签的href里都是javascript:void(0)。第一次见到这串东西,我下意识觉得是什么高深魔法,后来一查才发现逻辑非常简单。
void是JavaScript里的一个运算符,作用是计算后面的表达式,然后永远返回undefined。void(0)就是计算数字0,然后返回undefined。把它写在href里,配合javascript:这个伪协议,浏览器点击链接时会执行后面的代码,而不是发生页面跳转。所以<a href="javascript:void(0)">相当于告诉浏览器:点击这里,执行一段什么都返回不了的JS,别跳转。
那为什么不直接用href="#"?这是我今天踩的第一个小坑。#会让页面跳到顶部,同时在URL后面拼一个锚点,如果页面已经滚到很下面,点击之后视野突然回到顶部,体验非常糟糕。javascript:void(0)则完全不会动URL,不会动滚动位置。很多老代码用它来挂接JS事件,比如在onclick里写逻辑,再配合一个“什么都不干的href”保证回车键、中键等特殊操作不会触发默认跳转。
补充一个今天学到的点:现在新项目里其实已经不太推荐直接写
javascript:void(0)了,更常见的做法是<a href="javascript:;"></a>或者干脆用<button>元素。但在读老代码、改老页面的时候,见到这一串要立刻反应过来它在干什么,而不是一头雾水。
1.2 运行时报错:第五天的第一课是学会读报错
今天搜索“javascript运行时报错”的次数,大概比前面四天加起来都多。整理下来,最常见的就三类:
| 报错类型 | 错误信息长这样 | 常见原因 |
|---|---|---|
| ReferenceError | xxx is not defined | 变量没声明就使用,或者拼错了变量名 |
| TypeError | Cannot read properties of undefined | 在一个undefined的值上读取属性或调用方法 |
| SyntaxError | Unexpected token | 语法写错,比如少了个括号、逗号放在不该放的位置 |
发现一个规律:JavaScript的报错信息其实非常诚实,它会直接告诉你“哪个变量没定义”“哪一行读到了undefined”。我一开始看到一堆红色英文就慌,后面学会了一个笨办法——先看报错信息里的文件名和行号,再看错误类型,最后往前找“出错的那一行到底引用了哪个变量”。百分之八十的问题,这样三步就能定位。
今天还发现一个容易误导新手的地方:控制台报错的行号有时候是压缩后的JS文件行号,不是源码行号。这种情况需要先在Sources里打开源文件,或者配置sourcemap,否则照着报错行号去源码里找,怎么都找不到对应代码。
1.3 地址栏里旋转视频的那行“小魔法”
热搜里有一条很长很具体的内容:javascript:v = document.queryselector('video');v.style.rotate = '-90deg';v.s(后面大概是被截断了)。这条其实是很多人在浏览器里执行的“整活代码”:把网页里的视频元素选中,然后旋转90度。它用的API并不复杂——document.querySelector('video')用来选中第一个video标签,element.style.rotate是CSS里比较新的旋转属性,和transform: rotate()类似但写法更直接。
这种地址栏执行JS的方式在浏览器里确实有效,但有个前提:浏览器会把地址栏输入的javascript:开头内容当作一段脚本执行,页面当前的环境变量、DOM元素都能访问到。所以你可以在任意网页上试,比如把页面标题改成别的字符串,或者给所有图片加个边框。
不过要泼一盆冷水:这东西只能用来临时调试或者自己本地玩,不要指望它成为一个“功能”。一旦刷新页面,所有修改全部还原。真正要做的功能还是得写进代码里。另外,现在的现代浏览器出于安全考虑,对地址栏执行JS做了一些限制,某些场景下会直接把输入内容当成搜索词处理,所以这个玩法越来越像“时代的眼泪”了。
2. 跨端调用与网络请求:第五天开始面对真实世界
2.1 C#执行JavaScript:把JS引擎焊进后端
今天搜索词里出现“c# 执行javascript代码”,第一反应是:C#是后端语言,JavaScript是前端语言,这俩为啥能扯到一起?后来搞明白了,实际开发里真的有这种需求。
常见场景是:后端C#服务需要执行一段由前端或业务方编写的JS规则脚本,比如动态的折扣计算、风控规则、报表公式。规则经常变,如果每次变更都发版后端,成本太高。于是干脆把规则写成JS字符串存进数据库,后端运行时调用JS引擎执行。
在C#里执行JS,主流方案有这么几种:
Jint:纯.NET实现的JavaScript解释器,不需要额外安装原生依赖,适合在Windows/Linux服务器上使用。ClearScript:微软开源的脚本引擎封装,底层用V8或者JScript,性能更好,但需要在部署环境里带原生库。- 通过
Selenium或Playwright启动真实浏览器来执行JS,这种方式最重,一般用于端到端自动化测试,而不是常规业务。
用Jint的代码大概长这样:
var engine = new Jint.Engine(); engine.Execute("function add(a, b) { return a + b; }"); var result = engine.Invoke("add", 3, 4); Console.WriteLine(result); // 7这里的关键思路是:C#只负责把JS字符串丢给引擎,然后拿结果,两边通过“字符串和执行结果”通信。既然是字符串,就需要注意转义、异常捕获和超时控制,否则一段恶意或者死循环的JS脚本可能把整个服务拖垮。生产环境下一定要限制脚本执行时间和资源消耗,这是很多人容易忽略的坑。
2.2 OC和JavaScript互相调用:iOS的H5桥接逻辑
“oc和javascript互相调用”也是今天的高频词。在iOS开发里,WKWebView加载H5页面后,原生OC和网页JS经常要互相传数据,比如H5里点击按钮要唤起原生分享面板,原生操作完成后再把结果回传给H5。
我梳理了一下,正常流程是两条链路:
- JS调OC:在OC里通过
WKUserContentController注册一个消息处理器,H5那边用window.webkit.messageHandlers.xxx.postMessage(数据)发消息。OC在代理方法里接收消息内容。 - OC调JS:OC直接调用
webView.evaluateJavaScript("js函数名(参数)")执行一段JS字符串。
示例代码,OC侧注册处理器:
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; [config.userContentController addScriptMessageHandler:self name:@"share"]; WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];JS侧触发原生:
window.webkit.messageHandlers.share.postMessage({title: '分享标题', content: '分享内容'});这里最大的坑是内存泄漏。addScriptMessageHandler会对self强引用,如果self又持有webView,就会形成一个循环引用,导致控制器永远无法释放。常规做法是在页面销毁时主动调用removeScriptMessageHandlerForName,或者在中间加一层代理对象来打破循环。
理解桥接原理其实不难:JS运行在浏览器内核里,OC运行在原生进程中,二者隔着进程边界。所有“互相调用”本质上都是把数据序列化成字符串或JSON对象,通过系统提供的那条消息通道递过去。想通这一点,无论是OC、Swift还是安卓的addJavascriptInterface,就都不难理解了。
2.3 fetch API的各种语法:今天不用再写XHR了
以前学AJAX,绕不开XMLHttpRequest,那套事件回调写多了确实头疼。今天查“javascript的fetchapi 各种语法”,发现fetch才是这个时代应该用的东西。它是浏览器原生提供的网络请求接口,返回的是Promise,配合上async/await以后,代码平铺直叙,阅读体验好很多。
最基础的GET请求:
const res = await fetch('https://api.example.com/users'); if (!res.ok) { throw new Error('HTTP ' + res.status); } const data = await res.json(); console.log(data);带参数的POST请求:
const res = await fetch('https://api.example.com/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'day5', password: '123456' }) }); const result = await res.json();这里有几个容易踩的细节:
fetch默认携带的credentials是same-origin,跨域请求如果想要带上cookie,需要显式设置credentials: 'include'。- 响应体只能消费一次。
res.json()之后不能再调res.text(),如果既要JSON又要原始文本,得先拿res.text(),再自己JSON.parse。 - 网络断开或超时,
fetch抛出的错误类型是TypeError,而HTTP 404、500这类的非2xx状态码并不会走到catch里,只有res.ok是false。所以代码里一定要先判断res.ok。
今天还用AbortController试了一下请求超时控制,核心是这么写的:
const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); try { const res = await fetch(url, { signal: controller.signal }); // ... } catch (err) { if (err.name === 'AbortError') { console.log('请求超时'); } } finally { clearTimeout(timeoutId); }这套逻辑在真实项目里几乎必用,不然一个慢接口能挂起页面半天。
3. Vue项目里自动导入ElementPlus,为什么ElMessage还是未定义
3.1 自动导入是怎么“自动”的
搜索词里有一组明显偏向Vue工程化的:“vue javascript项目”“自动导入elementplus”“为什么elmessage还是提示未定义”。这组合在一起,基本就是今天最有代表性的踩坑连环套。
先说自动导入。ElementPlus是一个Vue 3组件库,传统用法是在main.js里全局注册:
import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' app.use(ElementPlus)这么做省事,但会把整个组件库打进打包产物,项目一大,首屏体积直线上升。于是就有了按需自动导入的方案:通过unplugin-auto-import和unplugin-vue-components这两个Vite插件,在编译阶段扫描模板里用到的组件名和API名,自动生成对应的import语句。
vite.config.js里大致配置如下:
import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default { plugins: [ AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] }配置完之后,模板里直接写<el-button>,编译时插件会自动帮你引入ElButton组件和它的样式。表面上代码里看不到import,但产物里已经按需包含了,这就是它“自动”的工作原理。
3.2 ElMessage未定义的真正原因:按需导入只管模板
很多人在模板里用<el-button>、<el-input>都没问题,但一写ElMessage.success('保存成功')就报“ElMessage is not defined”。我今天也卡在这个问题上很久。
原因在于:unplugin-vue-components只处理模板中的组件标签,它扫描的是<template>里的内容。而ElMessage、ElNotification、ElMessageBox这一类是“通过函数调用的组件”,它们在模板里没有对应的标签,组件插件根本不知道你用了它们,自然就不会自动引入。
解决方法是给AutoImport单独配置ElementPlus的API解析器,让它认识这些函数式API:
AutoImport({ imports: ['vue'], resolvers: [ElementPlusResolver()] })有了这个配置,代码里直接写ElMessage.success,插件编译时会自动在文件顶部插入:
import { ElMessage } from 'element-plus'注意:
ElementPlusResolver()默认不会自动处理API的样式,通常还需要配合unplugin-element-plus的样式导入,或者在项目入口手动引入基础样式。这也是为什么有些同学配置了自动导入后,ElMessage能弹出来,但样式乱糟糟。
另外还有一个隐蔽问题:如果你在<script setup>里明明写了ElMessage的import,同时又开着自动导入插件的API解析,有些版本会生成重复声明,造成编译告警。这种情况可以关掉自动导入对某个API的解析,或者统一走自动导入,不要混着写。工程化的坑大多是这种“约定冲突”。
3.3 Vue2安全检测报“脆弱的JavaScript库”:别只点掉警告
热搜词里还有“vue2 安全检测提示 脆弱的javascript库”。这其实是npm或CI安全扫描工具给出的依赖漏洞提示,常见于老项目,因为Vue2生态停更后,很多间接依赖的版本停留在很旧的阶段。
安全扫描报某某库“脆弱”,通常指的是这个库存在已知CVE漏洞,比如原型污染、正则拒绝服务等。大多数情况下,我们拿到的不是真正的攻击,而是“存在理论风险”。
处理这件事的通用排查链路:
- 运行
npm audit查看漏洞详情,看是直接依赖还是间接依赖。 - 如果是间接依赖,优先尝试
npm audit fix自动修复到兼容版本。 - 不能自动修复的,看漏洞公告里修复版本,手动修改package.json里的版本范围。
- 版本范围跨度过大、可能破坏兼容性时,使用
overrides强制指定间接依赖版本,或者用patch-package打补丁。 - 无论如何,修完要跑一遍回归测试,尤其是老项目,依赖大版本升级经常带来样式或接口行为的变化。
这个坑给我的体会是:安全扫描报警不等于立刻被攻破,但也别直接忽略。最稳妥的做法是把依赖升级纳入日常迭代,而不是等到扫描器亮红灯才处理。老项目升级依赖的时候,一次只动一个包,测一个包,比一次性全升要安全得多。
4. 箭头函数、模板变量和坐标数组:那些看着眼熟却说不清的语法
4.1 箭头函数:到底解决了什么问题
今天搜索“javascript 箭头函数”的不止我一个。箭头函数写法太简洁了:
const add = (a, b) => a + b;但这时我产生了一个疑问:它跟普通函数只是少写几个字母的区别吗?显然不是。箭头函数最核心的差异在于this的指向。
普通函数的this是在调用时决定的,谁调用它,this就指向谁。箭头函数没有自己的this,它会捕获定义时所处作用域的this,而且一旦捕获就无法改变。
用一个生活化的类比:普通函数像“看人下菜碟”,今天领导叫你,this就是领导;明天同事叫你,this就变成同事。箭头函数像“孩子随父姓”,在哪个作用域里出生,this就永远是谁。
这也解释了为什么箭头函数不能作为构造函数,不能用new来调用,因为构造新对象需要自己的this,而箭头函数根本没有。此外它也没有arguments对象,如果需要类数组参数,得用剩余参数...args。
写代码时的一个实用经验:在Vue 3的setup、React的函数组件里,事件回调、定时器回调都尽量用箭头函数,这样this能稳定指向组件实例或外层作用域。以前人们经常写var self = this来绕过this指向问题,有了箭头函数之后,这个写法基本可以退休了。
4.2 模板变量与模板字符串:告别加号地狱
还记得第一天学字符串拼接的时候,写'Hello, ' + name + '!',连接词一多,字符串中间全是加号和引号,眼都花了。今天搜“javascript 模板变量”终于找到了正主:ES6的模板字符串。
模板字符串用反引号包裹,变量通过${}嵌入:
const name = 'day5'; const message = `Hello, ${name}!`; console.log(message); // Hello, day5!它最大的优势不只是少加几个加号,而是支持多行字符串。前端拼接HTML时,以前要用'<div>' + '<span>' + text + '</span>' + '</div>',现在直接:
const html = ` <div class="card"> <span>${title}</span> </div> `;配合上简化版的${}表达式,可以放心写复杂的条件判断、甚至是函数调用,反正每个${}里都是一段合法的JS表达式。模板字符串在Vue、React里也特别常见,Vue模板里的插值语法、React的JSX表达式都延续了这种“变量嵌进去”的直觉。
4.3 用数组表达坐标:数据结构的第一次实战
热搜词里有“javascript 坐标表达数组”。光看描述有点像算法题的内容。点进去之后发现,是在问“怎么用数组表示一个坐标点”。最常见的无非两种:
[x, y],一个二维数组表示点的横纵坐标。[{x: 3, y: 5}],一个对象数组,每个对象里有x和y字段。
选哪种不是随便定的。如果只是单纯处理一组同样结构的点,比如画布上的多个顶点,用[[1,2], [3,4], [5,6]]最紧凑,配合数组解构,遍历起来非常顺手:
const points = [[1, 2], [3, 4], [5, 6]]; for (const [x, y] of points) { console.log(`x=${x}, y=${y}`); }但如果每个点还附带颜色、名称、大小这些属性,那就该用对象数组:
const markers = [ { x: 10, y: 20, label: '起点' }, { x: 50, y: 60, label: '终点' } ];这里真正想提醒的是:数据结构是为访问方式服务的。你选择数组还是对象,取决于后续是要“遍历计算”还是“按名字取字段”。第五天能把这个问题想明白,后面学Map、Set、二叉树这些,就会轻松很多。
5. 学习资源的岔路口:百炼成仙、悟透JavaScript与禁用的JS
5.1 《JavaScript百炼成仙》和《悟透JavaScript》:两本书的配合用法
热搜词里同时出现“javascript百炼成仙”和“悟透javascript 美绘版 pdf”,说明这个阶段的人已经开始主动找书读了。
先说《JavaScript百炼成仙》。这书的形式很特别,用玄幻小说的壳子讲JS语法,主角修炼“JS内力”,每一章练一个知识点。对于刚学到第五天的我来说,这种形式最大的价值是降低枯燥感。JS语法其实不复杂,但AP和练习之间隔着一层“抗拒感”,一本能让人连续读下去的书,对新手非常友好。
《悟透JavaScript》则是另一路子。它不教你怎么写一个按钮的点击事件,而是深入讲对象、原型、闭包、作用域链这些语言内核概念。它的标题起得很直白——把JS“悟透”,不满足于会调API,而要理解它为什么这么设计。
我的建议是两本配合着来:先用《百炼成仙》把语法和常用API顺一遍,手不生了,再回头看《悟透JavaScript》的闭包、原型链,会突然有“原来之前写的代码是这个意思”的顿悟感。顺序反过来的话,很容易被底层概念劝退。
另外说一句,网上搜“pdf”确实出了很多电子版,但作者写书也要吃饭,有条件尽量支持正版。学编程资料很多,不差这一本书的钱。
5.2 网页设计案例:第五天适合动手做的小页面
今天还看到“javascript网页设计案例”这个词。这个阶段看再多书,都不如做一个能交到别人手里的页面成长快。我给自己定了一个第五天的练手目标:做一个“今日待办清单”页面,需求就三条:
- 输入框里输入文字,点击添加按钮,列表新增一条。
- 每条待办前面有个复选框,勾选后文字加删除线。
- 点击删除按钮,这一条被移除。
这个案例麻雀虽小五脏俱全:DOM选择、事件绑定、数组操作、渲染更新全都能练到。写完之后还可以扩展一下,用localStorage把待办存下来,刷新页面还在,这就是一个完整的“数据持久化”体验。
做案例时的一个小技巧:把页面拆成三部分——HTML负责结构、CSS负责样式、JS负责行为。先把结构写死,再给它加样式,最后才写JS让页面“活”起来。很多人一上来就各种炫技,写一堆库,结果页面崩了都不知道是样式问题还是逻辑问题。第五天,老老实实按这个顺序来,出错概率会低很多。
5.3 禁用JavaScript之后:一个被忽视的健壮性问题
热搜词里出现“禁用javascript”,可能很多人就是顺手一搜,但这其实是一个非常好的前端思考题。如果用户在浏览器里禁用了JS,你的网页还能看吗?
JavaScript是让网页“动起来”的技术,但它不应该是网页“能看”的前提。有些站点把内容全用JS渲染,禁用JS之后只剩一个空白div,这对部分用户和搜索引擎爬虫都很不友好。比较合理的做法是渐进增强:先用HTML写好基础内容和静态结构,再用JS增强交互和动态功能。
在HTML里提供降级提示,用<noscript>标签:
<noscript> <div>您的浏览器未启用JavaScript,部分功能可能无法正常使用。</div> </noscript>这个标签在用户禁用JS时才会渲染,启用JS时不会显示。对于SPA这类必须依赖JS的应用,用<noscript>提示用户开启JS,是成本和体验之间比较均衡的兜底方案。
更深一层的原因是性能与可用性:如果核心内容必须要等JS下载完、解析完、执行完才能出现,那用户的网速一慢,整个页面就是白屏。越来越多开发者开始研究SSR、静态站点生成,目的之一就是让首屏内容不依赖JavaScript。第五天看到这个热搜词,能想到这一层,今天的学费就没白交。
晚上翻今天保存下来的代码片段,发现学习进度比我预想的要快。上午还在查void(0),下午已经在折腾Vite插件配置了。学习JS的过程就是这样,每一个“为什么报错”背后,都藏着一个真实的运行机制。明天准备把今天遇到的所有报错信息整理成一个速查表,下次再遇到,直接对号入座。