写 JavaScript 基础语法这块,网上教程多到泛滥,但大多数都犯同一个毛病:概念讲了一堆,代码却没几行。我这些年带新人、自己踩坑,最深的体会就是——JavaScript 这玩意儿,光看是看不出来的,必须一行一行敲出来才能形成手感。
这篇文章我就换个路子,纯以代码为主,把变量声明、函数、字符串数组对象、循环、DOM 操作这些基础语法全部拆成可直接运行、可直接复制的示例来讲。适配刚学完 HTML/CSS 想进一步接触前端逻辑的读者,也适合学过一点 JS 但总感觉“看得懂、写不出”的朋友。每个知识点我都会放上可以直接跑起来的代码,后面再补两句实际项目里才会遇到的坑,保证你看完能自己动手改着玩。
1. 先把地基打牢:变量、类型和运算符
1.1 声明变量:var、let、const 到底怎么选
初学者最容易困惑的就是 Javascript 声明变量有三种方式:var、let、const,初学者最开始可能只学 var,后来发现 let 和 const 又出现了。先看代码再讲道理:
var oldSchool = 1; let current = 2; const fixed = 3; oldSchool = 100; // 可以重赋 current = 200; // 可以重赋 // fixed = 300; // 报错:Assignment to constant variable这三行的区别主要体现在作用域和可变性上。var 是函数级的,存在变量提升,你在函数中间声明的 var 变量,实际上会被提升到函数顶部,只是赋值动作还在原位。let 和 const 是块级的,用一对花括号包起来的作用域内生效,出去就访问不到。const 的本质是“变量指向的这个引用不能变”,如果你存的是对象,对象内部的属性照样可以改:
const config = { theme: 'dark' }; config.theme = 'light'; // 合法,没有改引用,只是改了属性 // config = {}; // 报错我在项目里给团队定的规矩很粗暴:默认全部 const,只有当这个变量明确需要在后面被重新赋值时才改为 let,var 在代码评审时直接打回。这样做的原因是约束越多,出错的概率越小。你可以把变量名想象成一个箱子,const 是那种上了锁的箱子,你不能换箱子,但里面的东西可以折腾;let 是普通箱子,随时可以整个换掉。实际编码时,比如循环里的计数器用 let,请求配置、DOM 元素引用这类不需要改变指向的都用 const,代码的可维护性会明显上一个档次。
1.2 数据类型与类型转换
JavaScript 里数据类型分两大类:基本类型和引用类型。基本类型包括 number、string、boolean、null、undefined、symbol、bigint,引用类型就是 object,数组和函数其实都是 object 的特殊形态。有一个老掉牙但新手必踩的坑:
console.log(typeof null); // "object" console.log(typeof undefined); // "undefined" console.log(typeof []); // "object"typeof null 返回 "object" 是语言设计时的历史遗留问题,你没法改它,只能记住这个坑。真正判断一个值是不是数组,要用 Array.isArray()。
类型转换是语法里最容易出幺蛾子的地方。加法运算符有点特殊,只要两边有一个字符串,结果就会变成字符串拼接:
console.log(1 + '2'); // "12" console.log('5' - 1); // 4 console.log('5' + 1); // "51" console.log(6 * '3'); // 18减、乘、除、取余这些运算符会尝试把两边转成数字,所以 '5' - 1 结果是 4。这种隐式转换在生产环境里经常就是 bug 的来源。我建议你在做算术运算前,主动用 Number() 对入参做一次显式转换,或者用 parseInt 指定进制:
const input = '42px'; const num = parseInt(input, 10); // 42,第二个参数指定十进制,防止旧浏览器把 '08' 当八进制 const height = Number('4.5'); // 4.5 const isNum = !isNaN(height); // true注意 parseInt 会从字符串开头解析直到遇到非数字字符为止,'42px' 解析出来是 42,但 Number('42px') 的结果是 NaN。这俩行为完全不同,用哪个看场景,如果字符串可能带单位,用 parseInt;如果必须是全数字字符串,用 Number 并在得到 NaN 时做兜底处理。
1.3 严格相等、模板字符串与逻辑运算符
判断相等是最容易出错的语法点,我见过太多线上 bug 都是 == 导致的,比如某个值从接口返回是字符串 '0',本地判断 if (res.status == 0) 时没问题,但如果返回值是空字符串 '',用 == 比较 null 时也会通过,逻辑就乱了。所以我的建议很简单:写判断一律用 ===,不要用 ==。只有一种情况我允许用 ==,就是同时判断 null 和 undefined 时:if (val == null) 可以直接拦下这两个值,这是 == 少有的有用场景。
字符串这里,模板字符串是 ES6 之后我用得最频繁的语法之一,写法是用反引号包起来,变量用 ${} 内嵌:
const user = { name: '李雷', level: 3 }; const message = `用户${user.name}当前等级为${user.level},权限${user.level >= 3 ? '已开放' : '受限'}`; console.log(message); // 输出:用户李雷当前等级为3,权限已开放模板字符串里的 ${} 可以塞任意 JavaScript 表达式,三元运算、函数调用都行,这让拼字符串变得非常丝滑。以前用加号拼接一堆引号和小括号的日子,实在不想回去了。
逻辑运算符这里有两个新朋友值得记一下:空值合并 ?? 和可选链 ?.。这一对组合在实际业务里太常用了:
const response = { data: null }; const len = response.data?.list.length ?? 0; // response.data 为 null,直接短路得到 undefined,再被 ?? 兜底为 0?. 的含义是:如果前面的值是 null 或 undefined,整个表达式直接返回 undefined,不再继续往后取属性。?? 的含义是:左边的值如果是 null 或 undefined,就返回右边的兜底值。注意 ?? 不能和 || 混用而不加括号,因为它们的优先级规则不同,新手经常会在这里报语法错误。
2. 函数是第一等公民
2.1 函数声明、函数表达式与箭头函数
JavaScript 函数有三种最常见的写法,各自有各自的脾气:
// 函数声明 function add(a, b) { return a + b; } // 函数表达式 const subtract = function(a, b) { return a - b; }; // 箭头函数 const multiply = (a, b) => a * b;函数声明有一个特点叫“声明提升”,你在函数定义之前调用它也是合法的,因为整个函数会被提升到作用域顶部。函数表达式和箭头函数没有这个待遇,必须先定义再调用,否则报错。箭头函数最直观的区别是写法简洁,单条表达式可以省略 return 和花括号,上面 multiply 这一行就代表了完整的函数体。
但箭头函数真正的关键差异在 this 绑定。普通函数的 this 是调用时确定的,谁调用它,this 就指向谁。箭头函数没有自己的 this,它用的 this 是定义时所在作用域的 this。看这段代码:
const btn = document.querySelector('#btn'); btn.addEventListener('click', function() { console.log(this); // 指向 btn }); btn.addEventListener('click', () => { console.log(this); // 指向外层作用域的 this,比如 window });这个差异在处理事件回调时极其重要。如果你在事件回调里需要访问按钮自身,用普通函数;如果你在回调里需要继续使用外层对象的方法,比如 Vue 或 React 组件里的实例方法,用箭头函数会更省心,不需要额外 const self = this。我的习惯是:独立函数、需要动态 this 的场景用 function,回调函数、需要保持外层 this 的场景无脑箭头函数。只有在定义对象方法、需要 this 指向对象本身时,老老实实写 function。
2.2 默认参数与 rest 参数
函数参数处理也有一些很实用的语法糖。默认参数让你不用再在函数体里写一坨判断:
// 旧写法 function greet(name) { const userName = name || '朋友'; console.log('你好,' + userName); } // 新写法 function greet(name = '朋友') { console.log(`你好,${name}`); } greet(); // 你好,朋友 greet('韩梅梅'); // 你好,韩梅梅注意默认参数只有在传入 undefined 时才生效,如果调用时显式传了 null,默认值不会启用,name 会变成 null。这个细节很容易被忽略,实际上传 null 表示调用者故意给一个空值,和没传是两码事。
rest 参数用来接收多余参数,和 arguments 对象的区别是:rest 参数是真数组,可以用数组的 map、filter 等方法直接操作,arguments 是类数组对象,得先转一下才能用。写法如下:
function logAll(prefix, ...args) { console.log(prefix + ':'); args.forEach((item, index) => console.log(`${index + 1}. ${item}`)); } logAll('参数列表', '苹果', '香蕉', '西瓜');rest 参数必须放在参数列表的最后一位,它会收集所有剩余参数,这个用法在封装通用函数时非常顺手,比如写一个日志工具、给第三方库做一层封装适配时,rest 参数能帮你把不定数量的参数原样透传。
2.3 作用域与闭包入门
作用域是 JavaScript 新手最容易懵的节点。var 的函数作用域和 let 的块级作用域差异,在循环里面会引发一个非常经典的问题:
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i)); // 输出 3 3 3 } for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j)); // 输出 0 1 2 }var 声明的 i 是函数作用域下的同一个变量,循环结束后它的值是 3,三个 setTimeout 回调读到的都是最终值。let 声明的 j 在每次循环迭代中都是一个新的绑定,每个回调捕获的是各自迭代里的 j。这个例子背下来,面试和工作都够用了。
闭包这个概念,用代码解释最直观。所谓闭包,就是函数内部嵌套了另一个函数,并且内部函数引用了外部函数的变量:
function createCounter() { let count = 0; return function() { count++; return count; }; } const counterA = createCounter(); console.log(counterA()); // 1 console.log(counterA()); // 2 const counterB = createCounter(); console.log(counterB()); // 1外部函数 createCounter 执行完之后,正常来说它的局部变量 count 应该被垃圾回收,但因为内部函数还引用着 count,count 就被保留了下来。这就是闭包的作用:让函数记住并持续访问它定义时的作用域中的变量。实际应用中,防抖函数、节流函数、函数工厂这些场景都依赖闭包。你不用一开始就想着搞懂所有细节,先能把上面这个计数器跑通,理解“内层函数持有外层变量”这一点就够了。后面写事件监听、写组件时,会发现闭包无处不在。
3. 数据操控:字符串、数组、对象与 JSON
3.1 字符串的实用操作
字符串在业务开发中操作极其频繁,表单校验、接口拼接、文案格式化天天要用。这里直接上一批高频方法:
const str = ' JavaScript是一门灵活的语言 '; str.length; // 省略空格前的长度 str.trim(); // 去掉首尾空格 str.includes('Script'); // true,判断是否包含 str.startsWith('Java'); // true,前缀判断 str.endsWith('语言'); // true,后缀判断 str.indexOf('灵活'); // 9,找不到返回 -1 str.slice(0, 10); // 截取前 10 个字符 str.replace('灵活', '强大'); // 替换第一个匹配 str.toUpperCase(); // 转大写 str.split(''); // 按字符拆成数组replace 方法默认只替换第一个匹配项,如果想把所有匹配项都替换掉,得用全局正则表达式替换。截取字符串有三个相近方法:slice、substring、substr,最后一个已经废弃,不建议再使用。slice 和 substring 的最大区别是 slice 支持负数参数,表示从末尾倒数,比如 slice(-3) 取最后三个字符,substring 会把负数当 0 处理,行为完全不同。
字符串和正则表达式是绝配,做手机号脱敏、邮箱校验这类需求非常高效:
const phone = '138-1234-5678'; const masked = phone.replace(/^(\d{3})-\d{4}-(\d{4})$/, '$1-****-$2'); console.log(masked); // 138-****-5678 const email = 'user@example.com'; console.log(email.match(/^[\w.-]+@[\w-]+\.\w+$/)?.[0]);正则的学习不用一次到位,你先记住几个元字符就够用:\d 匹配数字,\w 匹配字母数字下划线,^ 匹配开头,$ 匹配结尾,* 表示重复零次或多次,+ 表示一次或多次,花括号可以指定次数。字符串处理相关的正则需求,基本靠这几个组合就能覆盖。
3.2 数组的增删改查与迭代
数组操作是 JavaScript 里最重要的基本功之一,后端返回的列表数据在前端基本都要靠数组方法处理。先看增删改查的基础方法:
const list = ['苹果', '香蕉']; list.push('西瓜'); // 末尾添加,返回新长度 list.pop(); // 末尾删除 list.unshift('葡萄'); // 开头添加 list.shift(); // 开头删除 list.splice(1, 1, '菠萝'); // 从索引1开始删1个,插入'菠萝' list.indexOf('香蕉'); // 查索引,找不到返回 -1 list.includes('葡萄'); // 判断是否存在splice 是数组里最万能的方法,一个方法就能完成插入、删除、替换三种操作。注意 splice 是原地修改原数组的,如果你不想改原数组,要用 slice 先拷一份。很多人经常把这两个方法搞混,记住一个口诀:splice 是直接改,slice 是拷贝切片。
迭代数组是业务代码里的大头,map、filter、reduce 这三个必须烂熟于心:
const orders = [ { name: '衬衫', price: 99, count: 2 }, { name: '牛仔裤', price: 199, count: 1 }, { name: '帽子', price: 59, count: 3 } ]; // map:映射,把每个元素变成新的值,长度不变 const names = orders.map(o => o.name); // ['衬衫', '牛仔裤', '帽子'] // filter:筛选,保留满足条件的元素 const bigOrders = orders.filter(o => o.count >= 2); // 衬衫和帽子 // reduce:累加,把所有元素收敛成一个值 const totalPrice = orders.reduce((sum, o) => sum + o.price * o.count, 0); // 99*2 + 199*1 + 59*3 = 574map 适合“一对一转换”,比如后端给你的是字符串状态码,前端要显示汉字,用 map 映射成新数组是最干净的写法。filter 适合“按条件筛选”,比如筛选已完成的订单。reduce 的适用面更广,本质上任何数组的聚合操作都能用它实现,除了求和,还可以用来数组去重、按条件分组、计算最大值。这三个方法配合箭头函数写出来,代码非常简洁,比 for 循环加临时变量的老写法好看得多,也更不容易出错。
3.3 对象和 JSON:前后端都绕不开的格式
对象字面量是 JavaScript 最常见的结构,它和 JSON 长得像,但不是一个东西。JSON 是数据交换格式,键名必须用双引号;对象是内存中的数据结构,键名一般是标识符。两者靠 JSON.parse 和 JSON.stringify 互相转换:
const obj = { name: '小红', info: { age: 18, city: '北京' }, tags: ['前端', '篮球'] }; // 对象转 JSON 字符串 const jsonStr = JSON.stringify(obj); console.log(jsonStr); // {"name":"小红","info":{"age":18,"city":"北京"},"tags":["前端","篮球"]} // JSON 字符串转对象 const parsed = JSON.parse(jsonStr); console.log(parsed.name); // 小红这里要注意一个经典坑:JSON 里的 undefined、函数、Symbol 在序列化时会直接被丢弃,NaN 和 Infinity 会被转成 null。如果你发现接口传参丢了字段,先检查是不是值恰好是 undefined。另外 JSON.stringify 常用的第三个参数用于格式化输出,调试时用 JSON.stringify(obj, null, 2) 能看出层级结构,非常实用。
对象解构赋值是我日常写业务代码最依赖的语法,一行代码就能把后端返回的字段抽出来用:
const res = { code: 0, data: { list: [], total: 100 }, message: 'success' }; // 一次性解构出需要的字段 const { code, data: { list, total }, message = '默认值' } = res; console.log(code, list, total, message);解构的本质是“按结构提取”,嵌套对象也能一路点下去,提取过程中还能顺手给字段重命名,比如 const { data: payload } = res。这个语法在代码里用熟了之后,你会发现自己不再需要写一堆中间变量去接返回值。需要提醒的是解构时如果某个属性不存在且没有默认值,得到的是 undefined,后续访问它的子属性会报 TypeError,所以从接口拿数据时尽量给默认值,比如把上面代码改成 const { data: { list = [] } = {} } = res,就能避免后端偶尔少返回字段引起的崩溃。
4. 流程控制与循环:让代码学会绕弯
4.1 条件判断的几种姿势
if/else 是入门课程,但在实际开发里怎么写更清晰是有讲究的。先看一个最简单的场景:
function getStatusText(status) { if (status === 1) return '待付款'; if (status === 2) return '已付款'; if (status === 3) return '已发货'; return '未知状态'; }我看到过太多人把 return 改成赋值给一个变量,然后末尾 return 一次,那样反而多了一次分支判断。既然每个分支都能直接返回,就不要在分支里做赋值操作,写得越短越不容易出错。
三元表达式适合处理二选一的场景,超过两个分支就会显得很难读:
const isLogin = user.token ? true : false; const text = score >= 60 ? '及格' : '不及格'; // 嵌套三元表达式,可读性差,不建议写 const level = score >= 90 ? '优' : score >= 60 ? '中' : '差';嵌套三元表达式虽然能写,但维护的时候你会后悔,建议别用。switch 适合做等值匹配且分支较多的情况,但早期的 switch 有个坑:如果不写 break 就会发生“落空”,继续执行下一个 case,这个特性叫 fallthrough。如果只是做简单的状态映射,我推荐直接用对象字面量,简洁而且没有 break 污染:
const STATUS_MAP = { 1: '待付款', 2: '已付款', 3: '已发货' }; const text = STATUS_MAP[status] ?? '未知状态';这种方式可读性、可维护性都比 switch 好,新增一个状态只需要加一行,不用担心落空问题。
4.2 循环语句:for、while、for...of、forEach
循环是 JavaScript 里重复操作的基本手段,但选对循环方式能省很多事。老牌的 for 循环功能最强,可以灵活控制退出条件:
for (let i = 0; i < 5; i++) { if (i === 2) continue; // 跳过第2次 if (i === 4) break; // 第4次结束时退出 console.log(i); // 0 1 3 }这个循环你可以精确控制变量从几开始、什么时候结束,而且 break 和 continue 都能用。但日常业务里,如果你要遍历的就是一个数组,那 for...of 更合适:
const fruits = ['苹果', '香蕉', '橘子']; for (const fruit of fruits) { console.log(fruit); } // 需要索引下标时可以配 entries() for (const [index, fruit] of fruits.entries()) { console.log(index, fruit); }for...of 是 ES6 新增的统一遍历方式,任何可迭代对象都能用,字符串也能 for...of 遍历出每个字符。forEach 是数组自身的方法,它的特点是写法极简,但不能用 break 和 continue,想提前退出只能抛异常,这个限制在实际业务里有点难受。所以我的取舍标准很简单:需要退出的循环用 for 或 for...of,纯遍历不过滤用 forEach,要从一个数组生成另一个数组用 map。遍历对象时,用 for...in 会拿到对象的属性名,但现在更推荐 Object.keys() 或 Object.entries() 配合数组的循环方法:
const config = { theme: 'dark', lang: 'zh', debug: true }; Object.keys(config).forEach(key => { console.log(`${key}: ${config[key]}`); });遍历对象时用 Object.entries() 可以直接同时得到键和值:
for (const [key, value] of Object.entries(config)) { console.log(`${key}: ${value}`); }5. DOM 与事件:让页面真正动起来
5.1 获取元素与修改页面内容
JavaScript 基础语法学到一定程度,必然要和页面打交道。浏览器环境下的 JS 一大核心能力就是操作 DOM,也就是文档对象模型。
先看怎么获取一个页面元素:
// 旧方法,但依然有效 const title = document.getElementById('title'); // 推荐方法,支持 CSS 选择器 const firstBtn = document.querySelector('.btn'); const allBtns = document.querySelectorAll('.btn'); // 返回 NodeList // 综合使用 const video = document.querySelector('video');getElementById 性能略好,querySelector 更灵活,能选到任何你用 CSS 能选中的元素。querySelectorAll 返回的不是数组,是 NodeList,它虽然也能用 forEach,但不能直接用 map、filter,需要时可以先展开成数组:[...document.querySelectorAll('.item')]。
拿到元素以后,最常见的是修改文本和修改样式:
const el = document.querySelector('#box'); el.textContent = '新的文本内容'; el.innerHTML = '<strong>富文本内容</strong>'; el.style.color = '#ff6600'; el.style.display = 'none';textContent 和 innerHTML 的区别要特别注意:如果你把用户输入的内容塞进 innerHTML,等于给了别人注入脚本的机会,轻则页面渲染出错,重则 XSS 漏洞。业务里我基本只用 textContent,只有明确要渲染后端返回的可信 HTML 时才会用 innerHTML,而且必须经过转义或安全过滤。
操作样式时,style 属性对应的是行内样式,优先级很高。像视频旋转这种需求,一行代码就能搞定:
const video = document.querySelector('video'); video.style.rotate = '-90deg';style.rotate 对应的就是 CSS 里的 rotate 属性,这是 2022 年以后浏览器支持的写法,比老式的 transform: rotate(-90deg) 更简洁直接。如果你需要知道某个元素当前被应用了什么样式,可以用 getComputedStyle 方法读取计算后的样式值,但注意它返回的是一个只读对象。
5.2 事件监听、事件对象与事件委托
页面交互的核心是事件,也就是用户点击、鼠标划过、键盘输入时你绑定的回调函数会执行。基础绑定方式很简单:
const btn = document.querySelector('#btn'); btn.addEventListener('click', function(event) { console.log('点击了按钮', event.target); });addEventListener 是绑定事件的推荐方式,最大的好处是同一个元素可以绑定多个同名事件,多个回调会依次执行,不会互相覆盖。老式的 onclick 属性或 btn.onclick = fn 是赋值语法,后写的会覆盖先写的,一个元素只能有一个 onclick。
事件处理函数里会收到一个事件对象 event,它有几个常用属性要记住:
- e.target:真正触发事件的那个元素
- e.currentTarget:绑定事件监听的元素
- e.preventDefault():阻止默认行为
- e.stopPropagation():阻止事件继续冒泡
- e.key:键盘事件里按下的键值
有一个很有用的技巧叫事件委托,场景是这样的:一个列表里有几十个 li,如果给每个 li 都绑监听器,性能浪费且代码繁琐。事件委托的原理是,点击事件会从最内层元素一路冒泡到祖先元素,所以只需要给祖先元素绑定一个监听,然后在回调里判断点击的是谁:
const list = document.querySelector('#todoList'); list.addEventListener('click', (e) => { const item = e.target.closest('li'); // 向上查找最近的 li if (!item) return; // 点击的不是 li,直接返回 console.log('点击了', item.textContent); });closest 方法从当前元素开始往上找,能找到匹配选择器的祖先元素。这段代码运行后,不管列表里有多少个 li,哪怕是动态新增的 li,不需要重新绑定就能生效,这就是事件委托最实用的地方。新加的列表项也能被监听到,因为这个监听器在父级身上,子元素发生变化不影响。
6. 常见运行时报错与排查技巧
6.1 报错类型速查:看到报错别再慌
JS 在浏览器里跑起来遇到报错是家常便饭,新手最常见的恐慌点是打开控制台看到一片红字就不知道从何下手。其实报错信息里已经给了足够线索,关键是学会分类。我把最常见的几类整理成一张表:
| 报错类型 | 报错信息范例 | 出现原因 | 解决办法 |
|---|---|---|---|
| ReferenceError | xxx is not defined | 变量未声明就直接使用 | 检查变量名是否拼错,是否忘了声明 |
| TypeError | Cannot read properties of undefined | 在 undefined 或 null 上访问属性 | 用可选链 ?. 或用条件判断兜底 |
| SyntaxError | Unexpected token | 语法写错了,少括号少逗号等 | 按报错提示的行号逐行检查 |
| RangeError | Maximum call stack size exceeded | 函数无限递归调用导致栈溢出 | 检查递归有没有终止条件 |
其中 TypeError 出现频率最高,常规原因是接口返回的数据结构里某个嵌套字段不存在。比如 res.data.list 里 list 没返回,你直接 res.data.list.length 就会炸。这种情况最好的处理方式是在取值时做好防御,也可以用解构默认值的方式避免崩溃:
// 防御式写法 const list = res.data?.list ?? []; console.log(list.length);在控制台调试时,我也建议养成一个习惯:报错信息里包含文件名和行号,点击报错链接能直接跳到对应代码位置,再配合附近几行的代码一起看,绝大多数问题都能定位。遇到 SyntaxError 时优先看报错提示的“当前位置”,因为 JS 解析器有时会把出错位置标得比较靠后,真正的漏括号可能在前面的某一行。
6.2 javascript:void(0) 是什么
很多人在网上复制代码时会看到这样写的超链接:
<a href="javascript:void(0)" onclick="doSomething()">点击</a>这里 javascript:void(0) 的意思是“执行一段空的 JavaScript 表达式,并且不产生任何返回值”,作用就是让点击链接时不发生页面跳转。javascript: 这个前缀在浏览器地址栏和 href 属性里会被当成 JS 代码执行,void(0) 表示对 0 求值但返回 undefined,这样浏览器就不会用返回值替换当前页面。以前还有程序员在 href 属性里直接写 javascript:xxx() 来实现点击执行函数,但这种方法现在不推荐了,因为容易被浏览器安全策略拦截,而且代码混杂在 HTML 属性里难以维护。
如果你在谷歌浏览器里点了这种链接发现没有任何反应,有时候是因为页面的某些安全策略或扩展拦截了 javascript: 协议,有时候是因为 onclick 绑定的函数本身报错了。排查时先打开开发者工具看控制台有没有报错,再确认事件绑定是否成功。如果只是想做一个“看起来像按钮的链接”,我建议直接改用 button 元素,或者把 href 换成 # 并在点击事件里调用 e.preventDefault() 阻止默认行为:
// 推荐做法 document.querySelector('#link').addEventListener('click', (e) => { e.preventDefault(); doSomething(); });6.3 调试工具使用技巧
排查问题最快的路径不是到处加 console.log 碰运气,而是学会用调试工具。浏览器开发者工具的 Sources 面板里可以打断点:点击代码行号处就会生成一个红色断点,页面执行到这一行时会暂停,然后你可以在右侧面板查看当前所有变量的值。这是我看代码执行流程最常用的方式。
console 对象也远不止 console.log 一个方法:
const users = [ { name: '小明', age: 20 }, { name: '小红', age: 22 } ]; console.table(users); // 表格形式展示,看数组和对象非常直观 console.log('当前用户:%o', users[0]); // %o 可以格式化对象打印 // 耗时测试 console.time('循环耗时'); for (let i = 0; i < 10000; i++) { /* 模拟工作 */ } console.timeEnd('循环耗时');在代码里直接写 debugger 语句也能触发断点,效果和手动打断点一样。不过发布到线上的代码要记得去掉 debugger,这个语句会导致浏览器调试器自动暂停,正常用户如果碰巧开着控制台就会看到页面卡住,体验很差。建议在自己的本地环境用,或者用打包工具在构建时统一移除。
7. 一轮边学边练的代码合集
学了语法不练就是白学,这里放一段综合示例,把前面讲到的变量、函数、数组、对象、循环、DOM 操作都揉在一个小场景里。需求:页面上有一个输入框和一个按钮,用户输入数字后,点击按钮生成对应数量的列表项,并把总价显示出来:
<input type="number" id="priceInput" placeholder="输入价格"> <button id="genBtn">生成列表</button> <ul id="list"></ul> <p id="total"></p>const input = document.querySelector('#priceInput'); const btn = document.querySelector('#genBtn'); const listEl = document.querySelector('#list'); const totalEl = document.querySelector('#total'); function createItems(count) { // 用 map 生成数据数组 const items = Array.from({ length: count }, (_, index) => ({ id: index + 1, price: 10 * (index + 1), name: `商品${index + 1}` })); // 用数组方法筛选和求总价 const validItems = items.filter(item => item.price > 0); const total = validItems.reduce((acc, item) => acc + item.price, 0); // 用 map 拼接列表项 HTML,再一次性写入 listEl.innerHTML = validItems.map(item => `<li>${item.name}:¥${item.price}</li>` ).join(''); totalEl.textContent = `总价:${total} 元`; } btn.addEventListener('click', () => { const count = Number(input.value) || 0; if (count <= 0) { alert('请输入大于0的数字'); return; } createItems(count); });这段代码把数组的 map、filter、reduce 全部用上了,同时演示了 DOM 操作和事件绑定。你完全可以把它复制到一个 HTML 文件里,用浏览器直接打开运行,改改 prices 的生成规则,算算不同逻辑下的总价,体会一下数据流是怎么从输入走向页面渲染的。练完之后你会发现,语法点之间的串联感比单独背任何一个知识点都重要。
我个人练手 JavaScript 的经验是:不必一开始就啃厚书,先把基础语法过一遍,然后给自己定一个小目标,比如写一个待办清单、做一个简易计算器、做一个猜数字游戏,过程中遇到不会的报错再回头翻语法文档。语法是工具,项目是实战,只有频繁地在实战中调用语法,你的代码手感才会真正建立起来。这篇里写的所有内容,都是我在这个过程中反复踩过、反复修正才记住的,希望你顺着代码敲一遍,能少走一些弯路。