尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript基础语法核心笔记:变量、类型、函数与事件机制

JavaScript基础语法核心笔记:变量、类型、函数与事件机制 说实话JavaScript 基础语法这门课我刚入行那会儿是真没当回事觉得不就是写个var、敲个function嘛能难到哪儿去。结果真正开始写项目、接手别人的代码、被线上 bug 追着跑的时候才发现当初基础没抠透的账全都在后面加倍还回来了。这份学习笔记算是我对自己这几年 JavaScript 使用经验的一次系统回炉。我不会把它写成教科书式的语法清单而是把我在实际开发里反复踩过的坑、验证过的写法、以及真正用得上的细节全部整理出来。内容覆盖变量声明、数据类型判断、运算符与隐式转换、函数、事件机制、报错调试这几个最核心的模块适合刚学完 HTML 和 CSS、准备进入 JavaScript 世界的初学者也适合写过一阵子代码但总觉得基础不扎实、想回头查漏补缺的同学。我尽量用大白话讲清楚每个知识点背后的“为什么”再配上可以直接跑起来的代码示例。这份笔记不追求面面俱到但凡是写出来的都是你日常开发里高频遇到的、绕不开的东西。1. 先说说这份学习笔记的定位和整体思路1.1 为什么要回头整理基础语法很多初学者会有个误区觉得基础语法就是背几个关键字、记几条规则随便看一遍就算过了。但 JavaScript 这门语言有个特点它看起来跟 C、Java 长得很像骨子里却是完全不同的运行逻辑。比如变量提升、隐式类型转换、this的指向规则、事件冒泡这些概念光看语法是看不出来的必须放到实际执行场景里去理解。我写这份笔记的另一个直接原因是身边陆续有同事问我一些“基础问题”“为什么我用比较两个值老出问题”“为什么这个this在函数里就变成 undefined 了”“为什么数组判断类型用typeof不管用”。这些问题单独看都是基础语法里的细节但连起来就是一个完整的知识拼图。与其每次帮人查一遍不如系统性地写一份可以反复查阅的笔记也算是对自己知识体系的一次沉淀。1.2 基础语法学习的整体路线JavaScript 基础语法这块我的建议是按照“变量与类型 → 运算符与流程控制 → 函数 → 对象与数组 → 事件与 DOM → 异步”这条主线去推进而不是今天看一个知识点、明天看另一个。原因很简单后面的每一个环节都在依赖前面的概念。你连let和var的作用域都分不清后面写循环、写闭包、写事件处理函数的时候一定会一头雾水。这里我用一个表把我建议的学习顺序和对应要解决的问题列出来学习阶段核心内容需要搞懂的关键问题第一阶段变量声明、数据类型、类型判断var/let/const 有什么区别、typeof 能判断什么第二阶段运算符、流程控制、循环隐式转换规则、与的差异、分支怎么组织第三阶段函数、作用域、闭包函数表达式与声明区别、this指向、参数处理第四阶段数组与对象操作引用类型特性、常用方法、解构赋值第五阶段DOM 操作与事件事件绑定、事件流、事件委托第六阶段异步与模块化回调、Promise、async/await、模块导入导出这份笔记对应的是前三个阶段的重点内容后三个阶段等我把异步那块再单独整理一份笔记出来。别贪多地基打牢了后面真的会快很多。2. 变量、数据类型与类型判断翻车率最高的前三名2.1 var、let、const 到底怎么选三个声明关键字是 JavaScript 给新手的第一关。我见过很多教程直接告诉你“能用const就用const其次let别用var”这个结论是对的但它背后的原理如果没搞清楚以后还是会踩坑。先说var。它在 ES6 之前是唯一的变量声明方式有两个明显的问题一是函数作用域没有块级作用域的概念二是变量提升声明会被“提升”到作用域顶部但赋值还在原位。举个我自己调试过的例子function testVar() { console.log(name); // undefined不是报错 var name zhangsan; if (true) { var age 30; // var 没有块级作用域 } console.log(age); // 30外部也能访问到 }这段代码在别的语言里早就报错了但在 JavaScript 里能正常跑正是因为var的变量提升和函数级作用域。console.log(name)打印出 undefined 而不是抛异常这个行为很容易让人误以为代码没问题实际上逻辑可能已经错了。let和const是 ES6 引入的块级作用域变量声明解决了上面两个问题。let声明的变量只在当前代码块内有效而且存在“暂时性死区”——在声明之前访问变量会直接报ReferenceError。const则是常量声明声明时必须赋值且不能重新赋值。注意这里是不能“重新赋值”但对对象和数组这种引用类型const只锁住变量指向的地址不锁内部属性const user { name: lisi }; user.name wangwu; // 允许修改属性没问题 // user { name: zhaoliu }; // 报错重新赋值不允许 const arr [1, 2, 3]; arr.push(4); // 允许 // arr [1, 2]; // 报错我的实际建议是默认用const确定需要重新赋值时才用let。这样代码的意图更清晰别人看你的代码时看到const就知道这个变量不会重新指向新值减少了阅读理解成本。2.2 数据类型全景基本类型和引用类型要分清JavaScript 有 7 种基本类型和 1 种引用类型。基本类型包括string、number、boolean、null、undefined、symbol、bigint引用类型主要是object而数组、函数、日期、正则这些本质上都是object的变体。这个分类不是考试知识点它直接决定了你代码里的赋值行为。基本类型按值传递拷贝的是实实在在的值引用类型按引用传递变量里存的是内存地址。看个我实际遇到过的问题let a { count: 1 }; let b a; // b 拿到的是 a 的引用不是拷贝 b.count 2; console.log(a.count); // 2a 也被改了 let c hello; let d c; d world; console.log(c); // helloc 没变基本类型互不影响这个现象我在好几个新手同事的代码里都见过。他们本想复制一个对象出来做临时修改结果不小心把原对象也改了排查了半天。解决办法是用展开运算符...做浅拷贝let user { name: tom, info: { age: 20 } }; let copy { ...user }; // 浅拷贝只复制第一层 copy.info.age 25; console.log(user.info.age); // 25嵌套的 info 对象还是同一个引用 // 深拷贝需要用递归或者 structuredClone let deepCopy structuredClone(user); deepCopy.info.age 30; console.log(user.info.age); // 25这次互不影响structuredClone是后来才有的原生深拷贝 API兼容性已经足够好遇到需要深拷贝的场景不用再手动写递归了。2.3 typeof 的局限与更可靠的类型判断typeof是新手最先接触的类型判断工具但它有几个著名的坑我整理一下表达式结果说明typeof 42number正常typeof strstring正常typeof trueboolean正常typeof undefinedundefined正常typeof nullobject历史遗留 bug但没法修typeof []object数组也是 objecttypeof function(){}function函数有特殊处理typeof null返回object是 ES 规范里没法修改的历史问题所以判断一个值是不是真的null时得用value null这种全等比较而不是依赖typeof。判断数组也不能用typeof得用Array.isArray()或者Object.prototype.toString.call()。日常开发里我总结了一套自己的判断习惯判断基本类型用typeof判断null用全等判断数组用Array.isArray()需要精确判断对象类型时用Object.prototype.toString.call()Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call({}); // [object Object] Object.prototype.toString.call(new Date()); // [object Date] Object.prototype.toString.call(/regex/); // [object RegExp]还有一个在实战中非常高频的需求判断一个值是否“为空”。我推荐用“真值”结合显式判断比如判断一个输入框提交前是否填了内容function isEmpty(value) { return value null || value undefined || value ; }这里刻意没有用!value因为!0、!false、!都是 true直接把数字 0 和布尔 false 都判成空了容易误伤。3. 运算符、流程控制与隐式转换逻辑出错往往不在逻辑3.1 隐式类型转换加号到底是拼接还是加法JavaScript 的运算符是隐式类型转换的重灾区也是新手最容易怀疑人生的地方。规则其实很明确只要两侧有一个是字符串那它做的就是字符串拼接如果两侧都不是字符串才把操作数转成数字做加法。console.log(1 2); // 12字符串拼接 console.log(1 2); // 3数字加法 console.log(1 2); // 12 console.log( 1); // 1 console.log(5 - 1); // 4注意减号没有拼接语义会转数字 console.log(5 * 2); // 10这里有个特别容易踩的坑从输入框拿到的一定是字符串直接做加法会变成拼接。比如用户输入价格和数量你写price count结果得到的是12.934这样的字符串而不是乘法结果。解决办法是显式转换用Number()、parseInt()或者parseFloat()let price Number(12.9); // 12.9 let count Number(3); // 3 let total price * count; // 38.7 let num1 parseInt(12px); // 12能解析出前置数字 let num2 parseFloat(3.5rem); // 3.5我自己写代码的习惯是“显式优于隐式”任何从外部拿到的值在使用前先做类型转换绝不让 JavaScript 帮我猜。这样代码清晰也不会被隐式转换坑到。3.2 与 全等比较是唯一推荐会先做隐式转换再比较不做转换直接比较值和类型。我在项目里的规矩就一条永远用永远别用。因为的规则太绕了看几个例子你就知道为什么我这么坚决console.log(1 1); // true转数字后相等 console.log(0 ); // true空字符串转 0 console.log(0 false); // truefalse 转 0 console.log(null undefined); // true特殊规则 console.log([] ); // true数组会先转成字符串这些结果单看每条都有“规则可循”但放在一起就是灾难。我记得之前排查一个 bug表单提交时判断用户是否选择了选项用了if (selectedValue 0)结果空字符串也被当成 0 通过了校验导致提交了一堆空数据。从那以后我就在团队里立了规矩代码评审时看到直接打回让写出这行代码的人用改过来。在判断引用类型时也要注意它比较的是引用地址而不是内容let obj1 { id: 1 }; let obj2 { id: 1 }; console.log(obj1 obj2); // false内容一样但地址不同 let obj3 obj1; console.log(obj1 obj3); // true同一个引用3.3 流程控制的组织方式流程控制除了最基础的if...else之外实际开发里还要学会根据场景选择更合适的写法。我自己的选择标准是分支少、逻辑简单用if...else多个固定值匹配用switch或者对象映射纯粹二选一用三元表达式。对象映射这种方式是我在项目里用得越来越多的尤其是处理不同状态对应不同行为时比一长串if...else好看得多const statusMap { pending: 待处理, processing: 处理中, finished: 已完成, failed: 失败 }; function getStatusText(status) { return statusMap[status] || 未知状态; }这个写法有几个好处映射关系集中在对象里新增状态只需要加一行没有复杂的嵌套分支默认值用||兜底。比写四个if判断要清晰太多了。循环方面普通for循环适合知道循环次数的场景for...of适合遍历数组、字符串等可迭代对象for...in是遍历对象属性名用的但要注意它会把原型链上的可枚举属性也遍历出来所以我通常不推荐直接用它遍历对象而是用Object.keys()配合forEachconst user { name: tom, age: 25 }; Object.keys(user).forEach(key { console.log(key, user[key]); });4. 函数JavaScript 的一等公民4.1 函数声明、函数表达式与箭头函数函数在 JavaScript 里是“一等公民”意思是函数可以像普通值一样被赋值、传参、返回。这个特性是 JavaScript 能用回调、能用函数式编程的基础。函数有三种常见写法我分别说说它们在实际使用中的区别。函数声明有提升机制可以在声明之前调用函数表达式没有提升必须先定义后使用箭头函数是 ES6 的简洁写法但没有自己的this、不能用作构造函数。看代码感受一下// 函数声明可以提前调用 console.log(add(1, 2)); // 3 function add(a, b) { return a b; } // 函数表达式提前调用会报错 // console.log(subtract(5, 2)); // TypeError const subtract function(a, b) { return a - b; }; // 箭头函数 const multiply (a, b) a * b;我平时写代码的倾向是需要提升或者需要作为构造函数时用函数声明一次性使用的回调函数用箭头函数需要动态绑定this时才用普通函数表达式。箭头函数还有个好处是返回值隐式返回写短回调特别舒服const numbers [1, 2, 3, 4, 5]; const doubled numbers.map(n n * 2); // [2, 4, 6, 8, 10]这里注意箭头函数如果只有一条表达式可以省略return和大括号。但如果返回的是对象字面量需要加圆括号包裹const getUser () ({ name: tom, age: 18 }); // 必须加 ()否则会被当成函数体4.2 参数处理与 this 指向函数参数的默认值处理老写法是a a || 默认值但这有个问题如果传入的合法值是空字符串或者 0也会被替换成默认值。ES6 的默认参数语法更精确只有当参数为undefined时才启用默认值function greet(name 游客) { console.log(你好${name}); } greet(); // 你好游客 greet(tom); // 你好tom greet(); // 你好 空字符串不会触发默认值不定参数...args可以把所有剩余的实参收集成一个数组在处理参数数量不确定的函数时非常有用function sum(...numbers) { return numbers.reduce((acc, cur) acc cur, 0); } console.log(sum(1, 2, 3, 4)); // 10然后是this指向。这个真的是 JavaScript 里最劝退新手的点。我的理解方式是普通函数的this指向“调用它的那个对象”箭头函数的this指向“定义它时所在作用域的this”。最常见的坑是事件回调里用普通函数this指向了事件源元素而不是外层对象导致取不到数据。比如const userCard { name: 张三, init() { // 这里的 this 是 userCard document.getElementById(btn).addEventListener(click, function() { // 这里的 this 是 button 元素不是 userCard console.log(this.name); // undefined }); } };解决方案有两个一是把外层this存成self再在回调里用二是直接用箭头函数因为它会继承外层作用域的this。现代写法毫无悬念选箭头函数const userCard { name: 张三, init() { document.getElementById(btn).addEventListener(click, () { console.log(this.name); // 张三 }); } };4.3 回调函数和异步的入门理解函数作为参数传递是 JavaScript 异步编程的基础。虽然现在都用 Promise 和 async/await 了但回调仍然无处不在事件处理、数组方法、定时器、中间件全是回调。初学者容易对回调产生两个困惑一是“为什么函数可以不立刻执行”二是“怎么防止回调地狱”。关于第一个我的解释是把函数当成一个“委托任务”你不是当场执行它而是告诉系统“到某个时机再执行我”。定时器是最直观的例子setTimeout(() { console.log(两秒后执行); }, 2000); console.log(先执行); // 输出先执行 → 两秒后执行关于第二个困惑回调地狱是层层嵌套回调导致的代码横向膨胀解决思路是不要用}一层层叠而是使用 Promise 链式调用或者 async/await 让代码恢复平铺结构。基础语法阶段先理解回调的“传递行为”异步专项后面单独展开。5. 事件机制语法怎么落地到页面上5.1 事件绑定的三种方式JavaScript 在浏览器里最核心的工作就是跟用户交互事件机制就是交互的入口。事件绑定有三种方式但日常开发里推荐用的只有一种半。第一种是把事件写在 HTML 属性里比如onclickhandleClick()这个方式现在基本被淘汰了因为行为跟结构耦合在一起不好维护。第二种是 DOM 属性赋值方式element.onclick handler这种方式只能绑一个处理函数后绑的会覆盖先绑的。第三种是addEventListener方式可以绑多个处理函数还支持控制是否在捕获阶段执行是唯一推荐的标准方式const btn document.getElementById(btn); btn.addEventListener(click, () { console.log(第一次绑定); }); btn.addEventListener(click, () { console.log(第二次绑定); }); // 点击后两个回调都会执行addEventListener还有一个很实用的能力是{ once: true }参数让事件只触发一次对于某些一次性操作比如首次点击提示特别方便btn.addEventListener(click, showGuide, { once: true });5.2 事件对象、事件冒泡与事件委托事件处理函数的第一个参数就是事件对象里面包含了跟这次事件相关的所有信息目标元素、坐标、按键、时间戳等。document.addEventListener(click, (event) { console.log(event.target); // 实际点击的元素 console.log(event.clientX); // 点击的 X 坐标 console.log(event.clientY); // 点击的 Y 坐标 console.log(event.type); // click });事件冒泡是指事件从最内层的目标元素开始逐级向上传播到父元素、再传到 document。如果不了解冒泡你可能会遇到这种情况点击列表里某一项结果整个列表的点击事件都触发了。实际项目里冒泡配合“事件委托”反而是个很好用的模式与其给 100 个列表项分别绑事件不如只给父容器绑一个事件通过event.target判断实际点的是谁const list document.getElementById(list); list.addEventListener(click, (event) { const item event.target.closest(li); if (item) { console.log(点击了第, item.dataset.index, 项); } });这里用了closest(li)它的作用是向上查找最近的li元素这样就算用户点的是li内部的文字或图标也能准确定位到这一项。事件委托的好处是占用的监听器少、内存占用低而且后续动态新增的列表项不需要额外绑定就能响应事件。如果你需要阻止冒泡用event.stopPropagation()需要阻止默认行为比如跳转、提交刷新页面用event.preventDefault()。6. 运行时报错与调试别怕报错怕的是看不懂6.1 高频报错类型速查表学习 JavaScript 的过程中报错是家常便饭。我把这几年遇到频率最高的报错归纳成一张表方便对照排查报错信息含义常见场景解决思路ReferenceError: xxx is not defined变量未声明拼错变量名、用了未定义的值检查变量声明和拼写TypeError: Cannot read property xxx of undefined在 undefined/null 上读取属性异步数据未返回、接口字段缺失先判空再用value?.xxx可选链SyntaxError: Unexpected token语法错误少写括号、花括号不配对、中英文符号混用看报错位置附近的代码检查括号TypeError: xxx is not a function调用了不存在的方法把对象当函数调用、方法名拼错确认类型和是否存在该方法RangeError: Maximum call stack size exceeded栈溢出无限递归、死循环检查递归终止条件我自己在调试时特别强调一个习惯先看报错的第一行再看它指出的行号位置。很多新手一看报错就慌直接翻代码从头看到尾其实浏览器控制台已经告诉你了问题在哪一行顺着查就行。6.2 调试技巧console、断点与性能分析console.log当然是最常用的调试方式但它有个效率问题改一次代码刷新一次页面非常费时间。我的调试路线是先在控制台输出关键变量确认数据对不对如果还定位不了直接在浏览器开发者工具里打断点逐步执行看每一行的变量变化。打断点的操作很简单在 Sources源代码面板里找到对应的 JS 文件点击行号打上断点然后刷新页面代码执行到这一行就会停下来。这时候可以在右侧的 Scope作用域面板里看到所有变量的当前值也可以单步执行一行一行走观察每一步的效果。这个方法对排查循环问题、复杂函数内部逻辑特别有效比盲目加console.log高效太多。还有一个我常用的技巧给console.log加上描述前缀方便在多条日志中区分const userId u_1001; const orderList getOrders(); console.log(当前用户ID:, userId); console.log(订单列表:, orderList);真到了生产环境日志可不能随便打会影响性能。生产代码里的调试信息要么不写要么通过构建工具剔除这是我踩过坑后养成的习惯——线上页面一打开控制台几百条日志排查性能问题的时候完全分不清哪条是核心信息。7. 实战里的高频细节看起来小用错了很难受7.1 保留两位小数到底怎么选“JavaScript 保留两位小数”这个话题在零散问题里一直很热门因为我确实踩过坑。toFixed(2)是很多人第一反应但它有个让人头疼的问题返回的是字符串而不是数字。const price 3.14159; console.log(price.toFixed(2)); // 3.14注意是字符串 console.log(typeof price.toFixed(2)); // string如果你直接拿这个结果去做数值计算3.14 2会得到3.142又是一个隐式转换的坑。所以要么用Number(price.toFixed(2))转回来要么用更精确的式子const num 3.14159; const rounded Math.round(num * 100) / 100; // 3.14真正的数字另外toFixed的舍入规则在某些边界情况也反直觉比如(1.005).toFixed(2)得到的是1.00而不是1.01原因与浮点数精度有关。如果你要做金额计算我建议用Math.round(value * 100) / 100这个基础做法或者直接引入处理精确小数的库无论如何别依赖toFixed来做业务上的金额舍入判断。7.2 模板字符串的实用价值ES6 的模板字符串用反引号包裹除了能插值还可以直接写多行这个特性在拼 HTML 片段时特别好用。我写动态渲染的列表时经常这样拼模板const users [{ name: 张三, age: 20 }, { name: 李四, age: 22 }]; const listHtml users.map(user li classuser-item span classuser-name${user.name}/span span classuser-age${user.age}岁/span /li ).join(); document.getElementById(userList).innerHTML listHtml;注意拼接数组时用了join()不然默认会在每个元素中间加逗号生成一堆带逗号的 HTML这个细节我第一次写的时候也漏了。7.3 字符串综合处理的常见组合字符串操作用途极广我整理几个高频组合去首尾空格用trim()判断是否包含子串用includes()按分隔符拆分用split()把数组拼回字符串用join()批量替换要用正则加replace()let input hello world ; console.log(input.trim()); // hello world console.log(input.includes(world)); // true console.log(a,b,c.split(,)); // [a, b, c] console.log([a, b].join(-)); // a-b console.log(2025-01-01.replace(/-/g, /)); // 2025/01/01这里replace(/-/g, /)里的g是全局匹配标记不加的话只替换第一个匹配到的-很多人在这里吃过亏。8. 学习笔记之外的几点体会聊到这儿这份笔记的主体内容差不多告一段落。最后分享几个我回炉基础语法后的个人体会。第一基础语法的价值不在于背下来多少规则而在于建立起“遇到问题能判断它属于哪一类”的能力。报错信息是 TypeScript 还是运行时错误、是类型问题还是作用域问题、是同步逻辑还是异步时序问题这些分类能力全靠对基础的熟悉程度。我在实际开发里排查问题的速度明显快于同事不是因为我聪明而是因为我见过这些坑、知道它们大概率出现在哪儿。第二写代码时永远不要指望 JavaScript 帮你“猜意图”。、隐式转换、var提升这些特性任何一次“图省事”的使用都可能在某次重构或者数据异常时变成回旋镖。用显式转换、用、用let/const这些习惯一开始可能觉得繁琐但长期算下来省下的排查时间远大于多敲的那几个字符。第三如果这份笔记里的代码示例你能不看答案自己写一遍再刻意改几个参数观察结果变化那学习效果会比单纯“看懂”好很多。我带过的每一个上手快的新人共同特点都是愿意亲手敲代码、亲手制造 bug、再亲手解决 bug。基础语法没有捷径但它也不需要你死记硬背多写多踩坑自然就熟了。
返回列表