JavaScript基础语法详解:变量、函数、闭包与常见陷阱
2026/9/24 21:56:31 网站建设 项目流程

JavaScript这门语言,搁在十年前可能还只是网页上跑跑小特效的“脚本小子玩具”,但放到今天,它已经成了整个前端生态的地基。不管你是想学Vue、React,还是想用Node.js做后端,甚至是想玩点Three.js、WebGPU这种3D可视化,JS的基础语法都是绕不过去的第一道门。我见过太多人一上来就冲框架,结果被原型链、闭包、this指向来回折磨,其实就是地基没打牢。这篇就把JavaScript的基础语法系统拆一遍,从变量、数据类型到函数、对象、异步,该讲的坑一个不落,适合完全零基础的新手,也适合学了一半感觉哪里不对、想回炉重铸的初学者。

1. 入门之前的三个认知:JavaScript到底在做什么

1.1 浏览器里的“翻译官”

你先记住一句话:JavaScript是运行在浏览器里的脚本语言。它的核心职责,就是让你打开的网页从“死”的变成“活”的。你点按钮弹窗、下拉刷新、轮播图自动播放、表单提交前校验,背后全是JS在干活。

区别于HTML和CSS一个管结构、一个管样式,JavaScript管的是行为。HTML像是毛坯房的水泥墙和承重柱,CSS是装修刷漆,JS就是水电——管你按下开关灯亮不亮、拧开水龙头水出不出。没有JS的页面只能看,有了JS的页面才能“用”。

这里要额外说一句题外话,常有人把JavaScript和Java搞混,两者除了名字有点像,关系基本等于“老婆饼和老婆”。JavaScript由网景公司1995年发明,当时纯粹是为了在浏览器里做动态交互,后来因为Java太火才故意起了个碰瓷的名字。你学JS压根不用先学Java,两者语法思路也有很大差别。

1.2 解释执行和编译执行的差别

JavaScript是一门解释型语言,写一行、执行一行,不需要像C++或Java那样先编译成机器码再运行。浏览器里内置了JS引擎(最常见的是V8引擎),它读一行源码、翻译一行、跑一行。这带来的直接好处是:你新建一个HTML文件,随手写几行script标签,用浏览器打开就能立刻看到结果,学习反馈极其快。

但也因为这一点,JS对代码写的顺序很敏感。你在一行里用了后面才声明的变量,很可能直接报错挂掉,不像编译语言那样有前置检查的过程。所以初学阶段一定养成一个习惯:JS代码是从上往下、逐行解释执行的,写完一行可以先想一下这一行会不会蹦出个错误。

1.3 运行环境不只有浏览器

记住“JavaScript只能在浏览器里跑”这句话,放在今天已经不够准确了。2009年Node.js项目发布之后,JS就能脱离浏览器,直接在操作系统上运行,做后端服务、爬虫、命令行工具都行。不过底层那套核心语法(变量、函数、对象、循环)完全一样,变的只是周围的环境API。

所以用一句话概括:JavaScript的核心语法是一套独立于环境的“通用能力”——它教你怎么描述数据、计算流程、组织逻辑;而浏览器提供了window、document这类对象,Node.js提供了fs、http这类模块。这篇主要讲核心语法,但我会在合适的地方提一下两者差异,免得你跑到Node环境里用document好一通找不着北。

2. 变量与数据类型:先把手里的材料理清楚

2.1 let、const、var到底怎么选

第一次接触JS,最先遇到的一定是声明变量。老教程会让你写var,但现在的实际开发中,var基本退居二线,主流的推荐是:默认用const,明确知道这个值要重新赋值,再换letvar能不用就不用。

为什么?var有两个坑:一是没有块级作用域,只有函数级作用域,循环里一不小心就污染外部变量;二是存在变量提升,你可以先使用后声明,结果拿到一个undefined,排查起来特别恶心。

// var 的典型坑:循环里 i 到处乱跑 for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出:3 3 3,而不是 0 1 2

letconst是ES6以后引入的块级作用域变量,代码写进哪个大括号,就只在大括号内生效。const声明的变量不能被重新赋值,但注意:它只是锁定了“变量指向的那个地址”,如果这个地址是对象或数组,对象内部的属性、数组里的元素依然可以修改。新手特别容易在这踩坑,以为const声明的数组就完全不能动了,其实arr.push()照样能跑。

2.2 七种数据类型快速过一遍

JS有七种内置数据类型:number(数字)、string(字符串)、boolean(布尔)、undefined(未定义)、null(空值)、symbol(符号)、bigint(大整数)。另外再算上引用类型object(对象),口袋里的材料就这些。

我用一个简单示例让每种类型都现身:

let age = 18; // number let name = "张三"; // string let isOk = true; // boolean let nothing; // undefined,声明了但没有赋值 let empty = null; // null,主动设置成空 let sym = Symbol("id"); // symbol,一般用来做唯一标识 let bigNum = 10n; // bigint,在数字后面加n let person = { name: "李四" }; // object

这里面最容易混淆的是undefinednull。简单记:undefined是JS自己给的,表示“这个变量根本没有值”;null是你主动塞进去的,表示“这个位置我故意清空了”。想检测类型,统一用typeof操作符,但它有个著名的bug:typeof null返回的是"object"而不是"null",这是初版设计遗留问题,官方没有修,你只需要在面试题里见到它不慌就行。

2.3 类型转换与隐式转换的坑

JS是弱类型语言,变量可以随时从字符串变成数字,类型之间的转换很自由,但自由意味着容易出一些“意想不到”的结果。最常见的是+运算符的双重身份:在数字之间是加法,但一旦有一方是字符串,就会变成拼接。

console.log("3" + 4); // "34",字符串拼接 console.log("3" - 1); // 2,减号没有拼接语义,所以转成数字计算 console.log("3" * "2"); // 6,全部转数字

我当年第一次看到"3" - 1输出2的时候,整个人愣了一下。这里有个规律:+见到字符串就干拼接,其他运算符(减、乘、除、取余)会尽量把字符串转成数字再计算。如果你要用表单输入的值做算术,一定记得先转换:Number("3")转成数字,String(3)转成字符串,或者用隐式技巧+"3"也能变成数字3。

3. 运算符与控制流:让代码学会做判断

3.1 运算符优先级,别硬背,用小括号

JS运算符有几十个,优先级从高到低排了一长串表。新手与其背那张表,不如记一条土办法:拿不准的地方,一律加小括号。小括号在JS里优先级最高,想先算什么就先算什么。举个例子:

let result = 2 + 3 * 4; // 14,乘法优先 let result2 = (2 + 3) * 4; // 20,小括号先算

这句话听着像废话,但实际写代码时非常管用。我见过线上事故,就是因为某个运算符优先级记错,导致位运算结果和预期差了十万八千里。另外有个特别常见的坑:=是赋值,==是宽松相等,===是严格相等。===会先比较类型、再比较值,类型不同直接false,强烈推荐一律用===。而==会在比较前做一堆隐式转换,比如"0" == false居然返回true,这种诡异的行为只会给项目埋雷。

控制台里经常有人执行javascript:void(o)来避免页面跳转,其实就是利用了void运算符——它负责执行一段表达式并永远返回undefined,浏览器默认不会对undefined做任何跳转处理,这属于运算符冷知识的实战应用。

3.2 条件语句:if、switch、三元表达式

条件判断是代码做决策的入口。最基础的是if...else if...else结构,适合分支不太多的情况。当分支特别多,比如要根据状态码返回不同文案,用switch会更清晰,它可以避免一长串else if恶心的嵌套。ES6之后还出现一种更简练的写法:三元表达式条件 ? 值A : 值B,适合“条件成立给A,否则给B”这种单行逻辑。

let score = 85; // if 写法 if (score >= 90) { console.log("优秀"); } else if (score >= 60) { console.log("及格"); } else { console.log("不及格"); } // switch 写法 let status = 2; switch (status) { case 1: console.log("待审核"); break; case 2: console.log("已通过"); break; default: console.log("未知状态"); } // 三元表达式写法 let tip = score >= 60 ? "通过" : "未通过";

初次接触三元表达式可能觉得它别扭,用熟了之后,你会发现它的语义非常紧凑,尤其适合在函数返回值、给变量赋初值时使用。但嵌套三元表达式一定少写,比如a ? b : c ? d : e这种,性能没有区别,纯粹是阅读体验灾难,维护你代码的人会很想打你。

3.3 循环语句:for、while、for...of怎么选

要重复执行某段逻辑,就需要循环。JS里有多种循环,单人推荐按需求选型。最通用的是for循环,适合明确知道循环次数的情况,比如遍历数组索引。如果不知道次数、只知道结束条件,用while更顺手。比如让用户不停输入直到输入正确为止,就很适合while

ES6以后,遍历数组首选for...of,它可以直接拿到数组里的元素,不需要管索引,代码简洁很多。顺便提一个几乎必考的面试题:for...infor...of的区别。简单记:for...in遍历的是对象的键名(或者数组的下标),for...of遍历的是可迭代对象的值。你要是用for...in遍历数组,拿到的都是字符串形式的索引“0”“1”“2”,做加法运算时容易踩坑,所以遍历数组老老实实用for...of

let arr = ["html", "css", "js"]; // 老式 for 循环 for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } // for...of 更推荐 for (let item of arr) { console.log(item); }

4. 函数:把重复代码装进盒子

4.1 三种函数写法,搞清楚各自的血统

函数是JS里最重要的组织代码的方式,把一段可以重复用的逻辑封装起来,起个名字,需要时再调用。JS里有三种常见写法:

// 写法一:函数声明 function add(a, b) { return a + b; } // 写法二:函数表达式 const add = function (a, b) { return a + b; }; // 写法三:箭头函数(ES6) const add = (a, b) => a + b;

初学者经常纠结选哪种。我的建议是:箭头函数是现代开发的主流,代码短且在this处理上更符合直觉,先重点掌握它;函数声明适合需要提升的场景,因为函数声明会被整体提升到作用域顶部,所以你在声明之前调用也不会报错;函数表达式则介于两者之间,但记住它不会提升,必须先定义后调用。

函数另一个核心概念是作用域。函数内部用let声明的变量,外部访问不到;而函数外部声明的全局变量,函数内部可以读。这个规则像楼层房间:楼上能看到楼下的东西,楼下看不见楼上的。用好了作用域,可以避免大量命名冲突。

4.2 参数、剩余参数与默认值

函数参数是调用函数时传进去的“原料”。JS在参数上有个比较自由的设计:你声明了两个参数,但只传一个,另一个也不会报错,自动变成undefined。这种自由适合做容错,但也容易导致bug,所以ES6引入了默认参数:

function greet(name = "朋友") { console.log(`你好,${name}`); } greet(); // 你好,朋友 greet("张三"); // 你好,张三

...三个点,也就是剩余参数,是另一个高频考点。它可以把多个参数“收集”成一个数组,非常适合写参数个数不确定的函数。举个例子,写一个求任意数字总和的函数:

function sum(...nums) { return nums.reduce((total, n) => total + n, 0); } sum(1, 2, 3, 4); // 10

这种写法在处理动态数量数据时非常香,而且...还能做数组展开,在ES6里属于“万能三点符”,后面讲数组时还会碰到。

4.3 作用域与闭包的基础认知

闭包是JS里让新手头疼指数最高的概念。说人话:当一个函数返回了另一个函数,并且内部函数引用了外部函数的变量时,就产生了闭包——外部函数即便已经执行完毕,那个被引用的变量也不会被垃圾回收,而是被“封存”在内部函数的环境里。

function createCounter() { let count = 0; return function () { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2

闭包的实际价值,是让某些变量拥有“私有”属性,外部不能随便改,只能通过函数暴露出去的方法操作。初学阶段不用强求彻底理解闭包,但要对它有个印象,因为后面的防抖节流、柯里化、模块化全都会碰到它。等你在实际项目里踩过一次闭包变量被意外共享的坑,你自然就懂它了。

5. 数组与对象:最常用的两个数据结构

5.1 数组的增删改查与遍历

数组是一串有序数据的集合。JS里创建一个数组很简单:let arr = [1, 2, 3]。往尾部追加元素用push,删除尾部用pop,删除头部用shift,在头部插入用unshift。这四个方法都能改变原数组,日常增删就靠它们。

查找方面,indexOf()可以返回元素位置,找不到返回-1;ES6的includes()返回布尔值,更直观。而更现代的做法是find、filter这种“声明式”方法,比如从一个数组里找出所有偶数:

let numbers = [12, 5, 8, 130, 44]; let evens = numbers.filter((n) => n % 2 === 0); // [12, 8, 130, 44]

几乎每本教程都会强调的mapreduce也属于必学方法:map把一个数组映射成另一个数组,长度不变,适合做数据转换;reduce把整个数组归并成一个值,适合做累加、聚合。我在工作中碰到的大量数据处理逻辑,到最后都能落在这几个方法上。遍历数组最推荐的还是for...of,但遇到需要对每个元素做加工并生成新数组时,map会比你手写循环优雅得多。

5.2 对象的创建、读取与this指向

对象在JS里就是一组“键值对”,用大括号括起来。你可以把对象想象成一张档案表,键是字段名,值是具体内容。最基础的操作有三个:创建、读值、改值。

let person = { name: "李四", age: 25, skills: ["JS", "Python"], sayHi() { console.log(`你好,我是${this.name}`); } }; // 读取 person.name; // 李四 person["age"]; // 25,也可以用方括号 // 修改 person.age = 26; // 新增 person.city = "上海";

这里有个绕不过去的点:this指向。以person.sayHi()这种方式调用对象方法时,方法内部的this指向person对象本身,可以拿到this.name;但如果把这个方法单独取出来赋值给一个变量再调用,this就会指向window(或undefined),运行时结果就完全变了。这也是高频面试里常考的对象方法this指向问题。解决思路是用箭头函数,或者在调用时用bind绑定。

5.3 JSON与对象的互相转换

JSON是一种轻量级数据交换格式,长得非常像JS对象,但有一些区别:它的键名必须加双引号,字符串也必须用双引号,不能写函数和undefined。前端从后端拿数据,最常见的就是JSON字符串,你用JSON.parse()把它转成JS对象,用JSON.stringify()把JS对象转成JSON字符串传给后端,这两个方法就是前后端数据通信的“翻译官”。

let jsonStr = '{"name":"张三","age":20}'; let obj = JSON.parse(jsonStr); // 变成对象 console.log(obj.name); // 张三 let str = JSON.stringify(obj); // 变回字符串 console.log(str); // {"name":"张三","age":20}

初学阶段容易把对象和JSON混在一起,记住一句口诀:对象是内存里的“活数据”,JSON是网络传输的“文本数据”。转换过程中如果遇到格式不正确,JSON.parse会直接抛出异常,所以建议在解析时用try...catch包一下。

6. 字符串、正则与日期:处理日常数据的工具箱

6.1 字符串的常用方法与模板字符串

字符串是前端打交道最多的数据类型,从接口返回的文案,到用户输入的内容,全是字符串。ES6以前拼接字符串靠+,又繁琐又容易漏,ES6推出了模板字符串,用反引号包住内容,${}直接嵌入变量:

let name = "张三"; let score = 90; console.log(`姓名:${name},成绩:${score}`); // 姓名:张三,成绩:90

字符串本身自带大量方法,优先记住这几个:

  • string.length拿到长度
  • string.toUpperCase()/toLowerCase()转大小写
  • string.trim()去掉首尾空格,表单校验必备
  • string.includes("关键词")判断是否包含某个子串
  • string.split("分隔符")把字符串拆成数组
  • string.replace("旧", "新")替换第一次匹配到的内容

顺带一提,热搜里有人问“javascript 合并字符串”,其实就是把两个字符串拼接成一个。最简单的方式是+或模板字符串,但如果字符串很多,用数组join更高效:["a", "b", "c"].join("-")得到"a-b-c",这个技巧在处理路径、拼接URL参数时非常实用。

6.2 正则表达式,一个字符匹配家族

正则表达式是处理字符串的“高级模式匹配工具”。很多人一看到那堆/^...$/就头皮发麻,但它其实不难。它的作用就两件事:一是判断某个字符串符不符合规则,二是把匹配到的内容提取或替换掉。

JS里创建一个正则最简单是直接字面量写法:let reg = /^1[3-9]\d{9}$/。这段匹配的就是国内手机号:以1开头,第二位是3到9,后面9位任意数字,总共11位。用reg.test(手机号)返回true或false,就能立即做表单校验。

常用元字符可以整理成一张速查表:

元字符含义示例
\d数字字符,等价于[0-9]/\d/能匹配 "1"
\w单词字符,包括数字字母下划线/\w/能匹配 "a"
\s空白字符,空格、换行、tab/\s/能匹配 " "
.除换行外的任意字符/a.c/匹配 "abc"
*前一个字符出现0次或多次/ab*c/匹配 "ac"、"abc"
+前一个字符出现1次或多次/ab+c/匹配 "abc"
?前一个字符出现0次或1次/ab?c/匹配 "ac"、"abc"
^匹配开头/^a/匹配 "abc"里的a
$匹配结尾/c$/匹配 "abc"里的c

刚开始不用记全,用的时候查表就行。重点是理解思路:正则就是把你要匹配的“形状”用符号描述出来。常见场景比如手机号、邮箱、URL、身份证号校验,网上都能搜到模板,拿过来稍微改改就能用。反过来,如果正则写得太复杂,还会拖慢性能,实在复杂的场景可以用拆分成多个正则逐一判断,代码反而更清晰。

6.3 Math、日期与异常处理

JS里处理数值计算和日期,主要靠内置的Math对象和Date对象。Math没法用new创建,它就是一个全局工具类。常用的有:

  • Math.round(4.6)四舍五入到整数,得到5
  • Math.floor(4.9)向下取整,得到4
  • Math.ceil(4.1)向上取整,得到5
  • Math.random()生成0到1之间的随机数,左闭右开
  • Math.max(1, 9, 3)取最大值

生成随机整数有一个通用公式:Math.floor(Math.random() * (max - min + 1)) + min,左闭右闭区间。做抽奖、随机点名都靠它。

日期方面,let now = new Date()就能拿到当前时间。要注意月份从0开始,所以new Date(2024, 0, 1)是2024年1月1日。格式化成“YYYY-MM-DD”要自己拼:

let now = new Date(); let year = now.getFullYear(); let month = String(now.getMonth() + 1).padStart(2, "0"); let day = String(now.getDate()).padStart(2, "0"); console.log(`${year}-${month}-${day}`);

异常处理用try...catch...finally。代码在try里出错了,catch能接住错误,不打断整个程序运行,finally里面的代码无论是否出错都会执行。这个机制在解析JSON、调用接口、加载外部资源这种外部因素较多的地方特别重要,因为你没法保证外部数据永远符合预期。

try { let data = JSON.parse('{"name":"张三"'); } catch (err) { console.log("解析失败,兜底处理:", err.message); }

7. 运行时错误与调试:把报错当成提示

7.1 常见报错类型与含义

新手最容易在“报错”面前手足无措,其实报错是JS给你的提示,而不是惩罚。看懂报错,能力就涨一半。最常见的几类报错:

  • SyntaxError:语法错误。少写了括号、引号没闭合,代码根本没法解析。这种错最直接,打开控制台看哪一行标红了。
  • ReferenceError:引用错误。用了某个不存在的变量,常见于拼写错误,或者变量还没声明就使用。
  • TypeError:类型错误。最常见的是把一个基本类型当函数调用,或者读取undefined的属性。比如let a; a.b = 1就会报这个。前端开发中报Cannot read properties of undefined基本就是这货,排查思路是看看对象有没有真正拿到数据。
  • RangeError:范围错误。比如递归调用太深,超出最大调用栈大小。

控制台的报错信息里,红色的错误类型可以帮你快速定位方向,后面的message是具体描述,再点右边的文件名和行号,能直接跳转到源码位置。这套“读报错”的流程建议从一开始就养成。

7.2 控制台调试技巧:比console.log更高级的方式

日常调试,大家最顺手的是console.log()到处打点,但打点只能看不能改,遇到复杂逻辑效率很低。这里分享几个实用技巧:

第一个是用console.table()打印数组或对象,它在控制台以表格形式展示,一眼就能看到结构,比log一长串清晰很多。第二个是用console.time()console.timeEnd()统计代码运行时间,用来排查性能问题,秒级搞定。第三个是浏览器开发者工具的断点调试:在源代码面板点击行号加断点,刷新页面后代码会停在断点处,鼠标悬停在变量上能看到当前值,配合单步执行(Step Over、Step Into)能精确追踪每一行。这个方法在处理复杂循环、递归时效率远超打点。

还有一个比较“野”的技巧:直接在浏览器控制台写代码操作页面。比如有些人会用一行document.querySelector("video")找到视频元素,再给它绑定事件或修改样式,来实现一些页面上的自定义效果。但注意这只是本地调试行为,不应该写到正式代码里,否则会带来安全和维护问题。

7.3 常见问题速查表

现象可能原因排查方向
页面提示please enable javascript to continue.浏览器禁用了JavaScript,不是代码问题检查浏览器设置,把JS开关打开
控制台报xxx is not defined变量未声明或拼写错误搜一下变量名,看是不是少了个字母
用变量方法时报undefined is not a function变量不是函数,或者函数属性拼错了打印变量类型,确认它到底是个啥
数组遍历里用了for...in,输出的是索引不是值遍历方式选错改用for...offorEach
后端返回的数据解析失败JSON字符串格式不对,或网络返回的是HTML先把接口返回内容打印出来看看
this的值不是预期的对象调用方式导致this指向变化箭头函数、bind,或手动改成普通函数调用
页面白屏没有任何报错可能是JS加载失败,或某段代码抛异常被吞在Network面板看脚本是否加载成功,全局搜trycatch

这张表是从大量实际报错里浓缩出来的,新学JS的读者可以先存着,遇到问题对着查。

最后分享一点实战体会

我学JavaScript的过程走了不少弯路。最早照着书敲代码,敲完也不知道每一行到底是为了解决什么问题;后来开始直接做小项目,遇到报错就查,查完写笔记,进步速度反而比死磕教程快得多。所以我不太建议你把基础语法从头到尾背完再动手,更实用的路径是:先把这篇文章里涉及的语法概念过一遍,然后立刻找一个小目标——比如做一个待办清单、做一个随机抽奖页面——边写边补知识。语法这个东西,用三个月自然会内化,光看是看不会的。

顺带再提醒一点:很多人在入门阶段容易把Python的语法习惯带到JS里,比如缩进、变量命名风格。语言之间互相借鉴没问题,但写JS时最好照着JS社区的约定来:变量和函数用驼峰命名,常量用全大写,每个语句结尾加不加分号看团队规范,但别一会儿加一会儿不加。代码是写给人看的,规范统一比“能跑”更重要。希望这篇基础语法全解析能帮你把根扎稳,后面学DOM、异步、框架的时候,你会感谢现在的自己。

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

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

立即咨询