☰
JavaScript基础语法核心要点:变量、类型转换与函数避坑指南
2026/10/10 23:55:16 网站建设 项目流程

初学 JavaScript 的时候,我总觉得基础语法不过是“背规则”,var、let、const 记一记,for 循环会写,函数会调,就算过关了。直到真正上手写项目、被各种隐式转换和变量提升坑到怀疑人生,才意识到基础语法的每一个细节都值得认真对待。这篇笔记是我系统重学 JavaScript 基础语法后的第一篇整理,围绕变量声明、数据类型与转换、运算符、控制流和函数这五个核心模块,把容易忽略、容易踩坑的地方全部拆开讲清楚。无论你是刚入门的初学者,还是写过一阵子但没系统梳理过的开发者,这篇笔记都能帮你把地基打得更扎实。

1. 变量声明:var、let、const 的那些微妙差异

1.1 作用域:函数级与块级的天壤之别

很多人一开始写 JavaScript 只认一个 var,因为它是这门语言最初就有的声明方式。但 var 的作用域规则非常特殊,它属于函数作用域,而不是块级作用域。这意味着在 if、for、while 这些花括号内部用 var 声明的变量,在花括号外面依然能够访问到。

if (true) { var message = "hello"; } console.log(message); // 输出 hello,不会报错 if (true) { let scoped = "world"; } console.log(scoped); // 报错:scoped is not defined

这段代码是理解 var 与 let 差异的第一个切口。var 声明的 message 虽然在 if 块内,但它的作用域是整个函数。而 let 声明的 scoped 是块级变量,脱离块之后就会销毁。实际编码中,块级作用域能帮我们避免非常多的命名冲突和内存占用问题,所以在现代 JavaScript 里,let 已经全面替代了 var 在日常代码中的位置。

for 循环是最能体现差异的场景。用 var 写循环变量,循环结束后变量依然存在,而且在回调函数里会出现经典的“循环变量共享”问题,后文讲到闭包时还会再涉及。用 let 写循环变量,每次迭代都会创建一个新的绑定,这就是 let 在 for 循环中和 var 行为不一样的根本原因。

1.2 变量提升:理解“先使用后声明”背后的机制

变量提升是 var 声明最著名的坑。所谓提升,并不是变量真的被移动到了作用域顶部,而是 JavaScript 引擎在编译阶段会先扫描当前作用域内的 var 声明,将其登记到作用域的顶部,并默认初始化为 undefined。这个过程发生在任何代码执行之前。

console.log(a); // 输出 undefined,而不是报错 var a = 10;

在大多数编程语言里,使用一个尚未声明的变量会直接抛出错误,但 JavaScript 的 var 会输出 undefined。原因就是声明被提升了,但赋值没有被提升。把上面的代码理解成下面这个等价过程就明白了:

var a; console.log(a); a = 10;

let 和 const 也会提升,但提升的方式完全不同。它们同样会被登记到作用域顶部,但在初始化之前访问会进入暂时性死区,引擎会抛出 ReferenceError 错误。这个差异是刻意设计的,目的是尽早暴露“变量未初始化就被使用”的编程错误。

console.log(b); // 报错:Cannot access 'b' before initialization let b = 20;

我个人在写代码时的习惯是:能用 const 就用 const,确实需要重新赋值的变量才使用 let,任何时候都不在新代码里使用 var。这样做的收益非常直接——你不需要思考这个变量会不会被意外修改,代码的可读性和可维护性都大幅提升。

1.3 const 的本质:绑定不可变,而非值不可变

const 有一个非常普遍的误解,很多人以为 const 声明一个对象后,对象里的属性就不能再改了。这个理解是错的。const 保证的是变量绑定不可变,也就是说你不能再给这个变量重新赋值,但对象内部的属性、数组里的元素,都可以自由修改。

const config = { name: "demo", }; config.name = "new name"; // 合法,可以修改属性 config = {}; // 报错:Assignment to constant variable const list = [1, 2, 3]; list.push(4); // 合法,可以修改数组内容 list = []; // 报错:Assignment to constant variable

理解这一点非常重要,否则你会陷入“用 const 声明对象后,修改属性居然不报错,是不是我代码有问题”的困惑。要彻底冻结一个对象,可以使用 Object.freeze(),但它只能浅冻结,嵌套对象依然可以修改,这是另一个话题了,基础语法阶段先记住 const 的本质即可。

2. 数据类型与类型转换:为什么 1 + "1" 不等于 2

2.1 七种数据类型与 typeof 的坑

JavaScript 目前有七种数据类型,其中六种是原始类型,一种是引用类型。原始类型包括 string、number、boolean、undefined、null、symbol,引用类型就是 object,数组、函数、日期、正则等都属于 object 的分支。还有一个 bigint 类型,专门应对超大整数,ES2020 才正式纳入规范。

typeof 是检测类型最常用的运算符,但它有几个臭名昭著的历史性坑点,新手第一次遇到基本都会懵:

typeof null; // "object" typeof []; // "object" typeof function () {}; // "function" typeof undefined; // "undefined" typeof Symbol(); // "symbol"

typeof null 返回 "object" 是 JavaScript 诞生初期的设计失误,因为 null 的二进制表示是全部为 0,引擎就把它判定为对象了。这个 bug 牵涉到太多存量代码,官方一直没有修复。所以判断 null 要用 value === null,而不是 typeof 的结果。typeof 数组返回 "object" 也不够精确,要识别数组可以用 Array.isArray() 方法。

判断一个对象是不是普通对象,常用的方法是 Object.prototype.toString.call(),它返回类似 "[object Array]" 或 "[object Object]" 的字符串,比 typeof 精确得多。这段代码我在项目里写了几百次,基础语法阶段就记住这个套路,后面会少走很多弯路。

2.2 隐式转换:加号的双重身份与宽松相等

JavaScript 的隐式类型转换是最容易引发 bug 的地方,没有之一。加号运算符会被同时用于数学加法与字符串拼接,具体执行哪种操作,取决于操作数的类型。只要有一个操作数是字符串,加号就会把另一个操作数转换成字符串进行拼接。

console.log(1 + "1"); // "11" console.log(1 + 1); // 2 console.log(1 + true); // 2,true 被转成 1 console.log("1" + true); // "1true" console.log(1 + null); // 1,null 被转成 0 console.log(1 + undefined); // NaN,undefined 被转成 NaN

看到 1 + true 等于 2,第一次接触的同学可能会觉得匪夷所思,但这背后是布尔值参与数学运算时的转换规则,true 转 1,false 转 0。null 在数值转换时变成 0,undefined 则变成 NaN。这一串规则不需要刻意死记,你需要建立的是一种“先想类型,再想结果”的直觉。

宽松相等运算符 == 会触发隐式转换,这也是很多 bug 的源泉。明确规则是:null == undefined 成立,NaN 不等于任何东西包括它自己,字符串和数字比较时字符串会被转成数字,布尔值会先转成数字再参与比较。

console.log(0 == ""); // true console.log(0 == false); // true console.log("" == false); // true console.log([] == ""); // true console.log([] == 0); // true

这一串结果看起来非常离谱,但追根溯源都是调用了对象的 toString 或 valueOf 方法进行原始值转换。现代开发规范普遍要求使用严格相等 === 和 !==,因为它们在比较之前不会做任何类型转换,只有类型和值都相同才返回 true。这是一个非常低成本的规避策略,强烈建议从一开始就养成这个习惯。

2.3 NaN 与数字安全性的边界

NaN 是一个特殊值,表示“不是一个数字”。它的产生场景非常多,比如把字符串解析为数字失败、数学运算中出现 undefined、0 除以 0 等。NaN 最反直觉的特性是它不等于它自己:

console.log(NaN === NaN); // false

这导致你不能用 x === NaN 来判断一个值是不是 NaN,必须使用全局函数 Number.isNaN()。注意全局的 isNaN() 函数有隐式转换的问题,isNaN("hello") 会先尝试把 "hello" 转成数字,转换失败后返回 true,这就不够纯粹。Number.isNaN() 则不会做类型转换,严格判断值本身是否为 NaN。

console.log(isNaN("hello")); // true,因为 "hello" 转数字失败 console.log(Number.isNaN("hello")); // false,因为 "hello" 不是 NaN 这个值

还有个相关的坑是浮点数的精度问题。JavaScript 的 number 类型是基于双精度浮点数标准存储的,0.1 + 0.2 的结果不是 0.3,而是 0.30000000000000004。这不是 JavaScript 的 bug,所有使用二进制浮点数的编程语言都有这个问题。基础语法阶段不需要深究实现原理,但你要知道不能直接用 === 判断浮点数运算结果,需要引入一个误差范围来进行比较。

3. 运算符与表达式:从比较规则到短路求值的细节

3.1 逻辑运算符的真值判定与短路规则

逻辑运算符 && 和 || 并不总是返回布尔值,这是一个被很多教程忽略的知识点。它们的求值结果是操作数本身,而不仅仅是 true 或 false。&& 的规则是:如果左边是假值,返回左边;如果左边是真值,返回右边。|| 的规则是:如果左边是真值,返回左边;如果左边是假值,返回右边。

console.log("a" && "b"); // "b" console.log(0 && "b"); // 0 console.log("a" || "b"); // "a" console.log(0 || "b"); // "b" console.log(0 || undefined); // undefined

这个特性在实战中非常有用。比如给函数参数设置默认值,过去常见的是使用逻辑或:param = param || defaultValue。但这个写法的缺陷在于,当传入的值本身就是假值(0、空字符串、false)时,会被意外地替换成默认值。在现代代码里,更推荐用 ?? 空值合并运算符来处理默认值,它只在左边是 null 或 undefined 时才返回右边。

const a = 0 || 10; // 10,意外替换 const b = 0 ?? 10; // 0,符合预期 const c = undefined ?? 10; // 10,符合预期

短路求值指的是逻辑运算符在执行过程中,根据左侧的结果提前结束计算,不再执行右侧表达式。这个机制可以用来安全地访问可能不存在的属性,比如 obj && obj.name,obj 为 null 时直接返回 null,不会去访问 name 导致报错。现代 JavaScript 提供了 ?. 可选链运算符,写法更简洁:obj?.name。基础语法阶段把 && 的短路规则理解透,后续看可选链的实现也会更通透。

3.2 三元表达式:嵌套是坏味道的信号

三元表达式是 if 语句的精简形式,语法是 condition ? expr1 : expr2。它能在一行内完成条件赋值,可读性通常优于等价的 if 语句。但三元表达式一旦嵌套,代码就会变得极难阅读。

const result = score >= 90 ? "优秀" : score >= 60 ? "及格" : "不及格";

这种嵌套写法虽然合法,可读性却很差,尤其是条件再多一层的时候,整行代码就成了一团乱麻。我自己的经验是:三元表达式最多处理一个条件分支,超过一个分支就用 if 或 switch 改写。代码首先是给人看的,其次是给机器执行的,这个顺序绝对不能颠倒。

3.3 运算符优先级:别考验自己和读者的记忆力

JavaScript 运算符优先级是一个庞大而复杂的表,完整记住并不现实。关键是识别几个容易出问题的组合。最简单有效的防御策略是加括号,而不是依赖优先级规则。

const isAdult = age > 18 && hasId === true; const isValid = (a > b) || (c < d);

还有一个比较隐蔽的坑是赋值运算符的返回值。赋值表达式会返回被赋的值,所以在 if 条件里写 if (x = 5) 实际上是先给 x 赋值 5,再判断 5 的布尔真值,结果是 true。如果本意是判断相等却错用了赋值运算符,程序不会报错,行为却完全错误,这种 bug 最难排查。现代 lint 工具一般都会针对这个模式给出警告。

4. 控制流:if、switch 与循环里的坑

4.1 真假值对照:哪些值会被判定为 false

if 语句执行时会对条件表达式进行布尔转换,在 JavaScript 中只有六个假值:false、0、空字符串 ""、null、undefined、NaN。其余所有值,包括空数组 []、空对象 {}、字符串 "false"、数字 -0,都会被转换为 true。

if ([]) { console.log("空数组为真"); // 会执行 } if ({}) { console.log("空对象为真"); // 会执行 } if ("false") { console.log("字符串 false 为真"); // 会执行 }

这六个假值的对照表值得打印出来贴在显示器上。很多判断逻辑出错,根源都在于对空数组和空对象的布尔值判断产生了错误预期。尤其是在写后端接口判空逻辑时,如果用 if (data) 来判断一个数组是否为空,结果永远为真,你还要再用 data.length 进一步判断。

4.2 switch 的穿透现象与类型比较

switch 语句有一个非常古老却仍然被很多人忽略的问题:case 之间的穿透。当某个 case 匹配成功后,如果没有 break 终止,程序会继续执行下一个 case 的代码块。这种设计在某些场景下是有意为之的,比如多个 case 共享同一段逻辑,但更多时候是因为漏写 break 导致的意外。

const day = 2; switch (day) { case 1: console.log("周一"); case 2: console.log("周二"); default: console.log("默认"); } // 输出:周二、默认

上面这个例子,case 2 匹配后没有 break,程序继续执行了 default。这种 bug 在代码审查里出现的频率非常高。另外需要特别指出的是,switch 的比较使用的是严格相等 ===,不会做类型转换。所以 switch("2") 永远不会匹配 case 2,因为一个是字符串一个是数字。

写 switch 的规范是:每个 case 必须以 break、return、throw 中的一种结束,default 永远放在最后并且不要省略。能坚持这个规范,switch 相关的绝大多数问题都可以避免。

4.3 循环:for-in、for-of、forEach 怎么选

JavaScript 里有很多遍历方式,每种适用的场景完全不同。for 循环最原始但最灵活,可以控制循环变量、条件、累加方式,还能用 break 提前终止。for-in 用来遍历对象的键名,但要特别注意它会继承原型链上的可枚举属性,而且遍历顺序在不同环境下并不保证一致,实际开发中已经很少直接使用。

for-of 是 ES6 引入的遍历方式,适用于数组、字符串、Set、Map 等可迭代对象,直接获取元素值而不是索引,语义清晰。但 for-of 在普通对象上不能直接使用,因为普通对象默认不是可迭代的。forEach 是数组的高阶方法,写法简洁,但不能在回调中用 break 中断,要中断还得靠抛出异常或者改用 for-of。

const arr = [10, 20, 30]; for (const item of arr) { if (item === 20) break; // 可以中断 console.log(item); // 10 } arr.forEach((item) => { if (item === 20) return; // 只结束本次回调,循环继续 console.log(item); // 10, 30 });

我个人的选择策略是:数组遍历优先用 for-of 或 forEach,需要索引就用传统的 for 循环,需要中断用 for-of,对象遍历用 Object.keys() 配合 for-of,永远不要用 for-in 遍历数组。这套策略可以让遍历代码既清晰又高效。

5. 函数基础:声明方式、作用域链与参数陷阱

5.1 函数声明与函数表达式的区别

JavaScript 中创建函数的方式主要有两种:函数声明和函数表达式。函数声明使用 function 关键字直接定义,它会被整体提升,因此在声明之前调用也不会报错。函数表达式则是把函数赋值给一个变量,变量声明会被提升,但赋值不会,所以在赋值之前调用会报类型错误。

sayHello(); // 输出 "hello",函数声明被提升 function sayHello() { console.log("hello"); } greet(); // 报错:greet is not a function const greet = function () { console.log("hi"); };

这个差异在编码规范上有很大的影响。现代代码风格普遍推荐使用函数表达式或箭头函数,而不是函数声明,因为这样可以确保函数在定义之后才被使用,代码的执行顺序更可预测。箭头函数是 ES6 引入的简化写法,它没有自己的 this,也不适合作为构造函数使用,基础语法阶段先记住这些差异,深入理解 this 的细节需要在后续笔记里单独展开。

5.2 arguments 与剩余参数

在普通函数内部,有一个隐式的 arguments 对象,它包含了调用时传入的所有参数,即使函数声明里没有对应的形参。arguments 是一个类数组对象,有 length 属性但没有数组的方法。过去我们想把 arguments 转成真数组,需要写 Array.prototype.slice.call(arguments),非常绕。

ES6 之后引入了剩余参数语法,直接用 ...args 接收多余参数,得到的就是一个真数组:

function sum(...numbers) { return numbers.reduce((total, num) => total + num, 0); } console.log(sum(1, 2, 3)); // 6

剩余参数不仅写法简洁,语义也更清晰。需要注意剩余参数必须是函数的最后一个形参,否则解析会报错。在箭头函数中不能使用 arguments 对象,但可以使用剩余参数,这进一步强化了剩余参数在现代代码中的主导地位。

5.3 作用域链与闭包的底层逻辑

函数执行时查找变量的过程是沿着作用域链逐级向上搜索的。作用域链本质上是一个嵌套的函数作用域结构,内层函数可以访问外层函数的变量,但外层函数不能访问内层函数的变量。这个机制是理解闭包的基础。

闭包指的是函数与其定义时所在作用域的组合。当内层函数引用了外层函数的变量,并且在外层函数返回之后继续存活时,就形成了闭包。经典案例就是计数器:

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

createCounter 返回的函数引用了外层变量 count,即使外层函数已经执行完毕,count 依然存活在闭包中。这个特性让函数拥有私有状态,但同时也意味着被闭包引用的变量不会被垃圾回收,如果大量使用闭包,内存占用会随之增加。理解闭包的关键就是搞清楚变量查找顺着作用域链走,而不是按调用栈走。

6. 字符串、数组和对象的基础操作与常用套路

6.1 字符串不可变与常用方法

字符串是原始类型,一旦创建就不可修改。你以为在修改字符串的方法,实际上都是返回一个新字符串,原字符串保持不变。这个特性与其他语言中的可变字符串类型有本质区别。

let str = "hello"; str.toUpperCase(); console.log(str); // "hello",原字符串未变 const upper = str.toUpperCase(); // 必须接收返回值 console.log(upper); // "HELLO"

字符串的常用方法分成几类:查找类包括 indexOf、includes、startsWith、endsWith;截取类包括 slice、substring、substr,其中 substr 已废弃,不建议再使用;分割合并类包括 split、join;大小写转换类包括 toUpperCase、toLowerCase。还有一个非常常用的模板字符串特性,用反引号包裹内容,可以在字符串中嵌入变量:

const name = "张三"; const greeting = `你好,${name}`;

模板字符串还支持多行文本,不需要手动拼接换行符,这在生成长文本或 HTML 模板片段时尤其好用。

6.2 数组的增删改查与高阶方法

数组是最常用的数据结构。增删操作包括 push、pop、shift、unshift 四个基础方法,涉及的方法非常多,初始阶段需要区分哪些方法会修改原数组,哪些会返回新数组。push 和 pop 在数组尾部操作,shift 和 unshift 在数组头部操作,它们在性能和语义上完全不同。数组头部操作的时间复杂度是 O(n),因为需要移动所有元素,日常处理大数据量时要避免频繁使用 shift 和 unshift。

数组的高阶方法是函数式编程风格的精华,最常见的是 map、filter、reduce 三个。map 对每个元素执行函数并返回新数组,filter 根据条件过滤元素并返回新数组,reduce 把数组归约为一个值。这三个方法配合箭头函数,可以替代大量传统的 for 循环代码:

const nums = [1, 2, 3, 4, 5]; const doubled = nums.map((n) => n * 2); // [2, 4, 6, 8, 10] const evens = nums.filter((n) => n % 2 === 0); // [2, 4] const total = nums.reduce((sum, n) => sum + n, 0); // 15

使用这些高阶方法时,一个常见的错误是在 map 或 filter 的回调里忘记写 return。箭头函数如果函数体用了花括号,就必须显式 return 才能返回值。这是一个频率极高的低级错误,我自己也踩过好多次,代码不报错但结果永远是 undefined 或空数组,排查时要第一时间想到这个可能性。

6.3 对象属性的增删与遍历方式

对象是 JavaScript 中最灵活的容器。新增属性直接赋值即可,删除属性使用 delete 运算符。delete 的返回结果是布尔值,表示是否成功删除,但有很多边界情况会导致返回值为 true 却没有真正删除,比如属性本身是不可配置的。基础语法阶段先记住基本用法即可。

对象的键名会被自动转换成字符串,所以 obj[1] 和 obj["1"] 是同一个属性。Symbol 作为键名时不会被转换成字符串。遍历对象属性有几种方式:Object.keys() 返回自身的可枚举键名数组,Object.values() 返回值数组,Object.entries() 返回键值对数组。这三种方法在现代代码中已经完全取代了 for-in 循环。

const user = { name: "李四", age: 30 }; Object.keys(user).forEach((key) => { console.log(key, user[key]); }); Object.entries(user).forEach(([key, value]) => { console.log(key, value); });

解构赋值是 ES6 提供的又一利器,它可以快速从对象或数组中提取值到独立变量。对象解构按属性名匹配,数组解构按位置匹配。配合默认值语法,还能优雅地处理属性缺失的情况:

const { name, age = 18 } = user; const [first, second, ...rest] = [1, 2, 3, 4, 5]; console.log(name, age); // "李四" 30 console.log(first, rest); // 1 [3, 4, 5]

解构赋值在函数参数接收对象时最常用,可以在函数签名里直接提取需要的属性,代码瞬间变得精简且意图清晰。

7. 初学阶段最值得养成的三个好习惯

7.1 用严格模式约束自己

早期 JavaScript 的语法宽松得像没系安全带,很多看似无害的写法暗藏隐患。在代码文件或函数顶部加上 "use strict"; 指令,可以让引擎进入严格模式,对隐患行为抛出错误,而不是静默接受。使用未声明的变量、修改只读属性、重复声明参数等行为,在严格模式下都会被打回原形。

"use strict"; undeclared = 10; // 报错:undeclared is not defined

现代 ES 模块和大部分构建工具默认启用严格模式,但如果你还在写传统脚本文件,养成手动添加严格模式指令的习惯,能最大程度避免低级的语法陷阱。

7.2 在浏览器控制台里多动手实验

基础语法光看不练很难真正形成直觉。我学每一节内容时都会打开浏览器开发者工具的控制台,手动敲一遍示例代码,故意改参数,观察输出变化。尤其是在理解隐式转换和运算符优先级时,多做实验比背规则表有效得多。控制台还能直接查看对象的原型链和属性结构,对理解引用类型的内部机制非常有帮助。

console.log(0.1 + 0.2); console.log(typeof null); console.log([] + []);

对每一行输出都问自己“为什么”,回答不上来就去查文档,这个过程比任何教程都扎实。我翻看过很多初学者的代码,大部分错误的根源其实都是少做这一步实验。

7.3 用 lint 工具提前拦截低级错误

lint 工具可以在代码运行之前发现很多基础语法层面的问题。未使用变量、隐式类型转换、== 宽松比较、嵌套三元表达式、console 残留等,都能给出警告或错误提示。虽然基础语法学习阶段可以暂缓配置复杂的规则集,但提前体验 lint 的反馈机制,能让你在第一时间知道哪些写法是不被推荐、容易出问题的,比踩坑之后再回头改要高效得多。

我到现在依然记忆深刻的一次教训,是在一个项目里因为 number 和 string 的宽松比较导致误判,排查了三小时才定位到问题。从那之后,我写任何 JavaScript 代码都不会用 ==,这个习惯帮我避掉了大量潜在的逻辑错误。基础语法阶段的学习重点不是把所有规则背得滚瓜烂熟,而是建立一套针对代码行为的直觉判断体系,知道哪些写法容易出问题、出问题时如何快速定位、为什么会有这样的设计。这份笔记只是第一篇,后面的函数进阶、对象原型、异步机制等内容,都是在这个基础上逐层展开的。扎实的地基才是走得更远的前提。

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

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

立即咨询