JavaScript基础语法深度解析:从变量声明到异步编程的完整指南
1. 项目概述:为什么“全”的JavaScript基础语法笔记如此重要
最近在整理自己的技术仓库,翻出了几年前刚学前端时写的一摞JavaScript笔记。说实话,当时觉得把MDN文档和教程里的东西抄下来,分个类,就算“学会”了。直到后来真正上手做项目,被各种隐式类型转换、作用域链、异步回调坑得晕头转向,才明白一个残酷的事实:基础语法学得“全”不等于学得“透”。很多教程和所谓的“全语法”手册,只是把var,let,const的区别、if...else怎么写、for循环有几种形式罗列一遍,这远远不够。真正的“全”,是不仅要看到语法的表面形式,更要理解其背后的设计逻辑、运行时行为以及那些教科书里不常提,但实际开发中天天遇到的“坑点”。
这也是我决定重新梳理这份“JavaScript基础语法(全)”笔记的初衷。它不仅仅是一个查询手册,更像是一份从“知道”到“会用”再到“精通”的路线图。无论是刚入门的新手,还是工作一两年想回头夯实基础的中级开发者,都能从中找到价值。你会发现,很多让你头疼的面试题(比如“0.1+0.2为什么不等于0.3?”、“setTimeout里this指向谁?”),其根源都深埋在基础语法之中。掌握了这些,你不仅能写出更健壮的代码,在调试和阅读复杂源码时也会更加得心应手。
2. 核心概念深度解析:超越“Hello World”
很多初学者一上来就急着写函数、做特效,却忽略了JavaScript语言最底层的几个基石概念。这部分如果没吃透,后面的学习就像在沙地上盖楼,随时可能崩塌。
2.1 变量声明:var,let,const不仅仅是“新老交替”
你可能已经知道let和const是ES6引入的,用于解决var的一些“怪异”行为。但它们的区别远不止“作用域”和“重复声明”这么简单。
var的“提升”到底是什么?我们常说var声明的变量会“变量提升”。但准确地说,是声明被提升,而赋值留在原地。引擎在编译阶段会找到所有var声明,并在作用域顶部为其创建绑定,初始化值为undefined。
console.log(a); // 输出: undefined,而不是报错 var a = 10;这段代码实际执行顺序相当于:
var a; // 声明被提升到作用域顶部,初始化为undefined console.log(a); // 输出 undefined a = 10; // 赋值留在原地let和const的“暂时性死区”let和const也有提升,但和var有本质不同。它们被提升后,并不会被初始化为undefined,而是在声明语句执行之前,变量都处于“暂时性死区”,访问它会抛出ReferenceError。
console.log(b); // ReferenceError: Cannot access 'b' before initialization let b = 20;这个机制是为了让开发者养成良好的编程习惯:先声明,后使用。它从根本上避免了因变量意外为undefined而导致的隐蔽bug。
const的本质是“绑定不变”,而非“值不变”const声明的常量,其“绑定”不可变,即这个变量名不能再指向另一个内存地址。但对于复合类型(如对象、数组),其内部的值是可以改变的。
const person = { name: 'Alice' }; person.name = 'Bob'; // 这是允许的,修改的是对象内部的值 console.log(person); // { name: 'Bob' } person = { name: 'Charlie' }; // TypeError: Assignment to constant variable. // 这是不允许的,试图改变person变量本身的绑定(指向一个新的对象)理解这一点,就能明白为什么const在声明对象时依然常用——我们通常只是不希望变量被意外重新赋值,而不是冻结整个对象。
实操心得:在现代开发中,我的习惯是默认使用
const,只有当明确知道这个变量需要被重新赋值时,才使用let。几乎完全不用var。这能最大程度地保证代码的意图清晰,减少因意外重赋值导致的bug。
2.2 数据类型:动态类型的“静”与“动”
JavaScript是动态弱类型语言,但这不意味着我们可以不关心类型。相反,理解其七种原始类型和一种对象类型,是理解一切语言特性的前提。
原始类型的“值”与对象类型的“引用”这是最核心的区别。原始类型(String,Number,Boolean,Null,Undefined,Symbol,BigInt)的值是直接存储在栈内存中的。赋值或传参时,传递的是值的副本。
let a = 10; let b = a; // b是a值的一个独立副本 b = 20; console.log(a); // 10, a的值没有改变对象类型(Object,包括Array,Function,Date等)的值存储在堆内存中,变量名存储的是该对象在堆内存中的地址(引用)。赋值或传参时,传递的是这个引用的副本,而不是对象本身。
let obj1 = { count: 10 }; let obj2 = obj1; // obj2复制了obj1的引用,指向同一个对象 obj2.count = 20; console.log(obj1.count); // 20! 因为obj1和obj2指向同一个对象这个特性是许多bug的源头,尤其是在函数参数传递时。
typeof与instanceof的陷阱typeof对于原始类型(除null外)判断基本准确,但对于对象类型,除了function会返回'function',其他如数组、日期、正则等一律返回'object'。
typeof [] // 'object' typeof null // 'object' (这是语言设计的历史遗留bug) typeof function(){} // 'function'instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。它对于自定义对象和内置对象很有效,但在多个iframe或window环境下可能会失效,因为同一个内置类在不同全局环境下不相等。
更可靠的类型判断方法对于数组,使用Array.isArray()。 对于更通用的类型判断,可以使用Object.prototype.toString.call()。
Object.prototype.toString.call([]) // '[object Array]' Object.prototype.toString.call(null) // '[object Null]' Object.prototype.toString.call(new Date()) // '[object Date]'2.3 运算符与类型转换:隐式转换的“魔法”与“噩梦”
JavaScript的灵活很大程度上源于其复杂的隐式类型转换规则,这也是最容易让人迷惑的地方。
==与===的经典问题==(宽松相等)在比较前会进行类型转换,===(严格相等)不会。
0 == false // true (Number(0) == Number(false) => 0 == 0) 0 === false // false (类型不同) null == undefined // true (规范定义) null === undefined // false有一条简单的记忆规则:除了在明确知道需要类型转换的少数情况(如null == undefined),一律使用===。这能避免绝大多数因隐式转换导致的意外行为。
+运算符的重载+运算符既可用于数字相加,也可用于字符串连接。其规则是:如果任意一个操作数是字符串,则进行字符串连接;否则,将两个操作数转换为数字后相加。
1 + 2 // 3 (数字相加) 1 + '2' // '12' (字符串连接) true + false // 1 (true转为1, false转为0,数字相加) [] + {} // '[object Object]' ([]转为空字符串'', {}转为字符串'[object Object]') {} + [] // 0 (这里{}被解析为代码块,+[]将[]转为数字0,这是一个特例)理解这些规则,就能看懂很多看似诡异的表达式。
其他运算符的转换-,*,/,%等算术运算符会将操作数强制转换为数字。
'10' - '5' // 5 '10' * '2' // 20 '10' / '2' // 5 '10' % '3' // 1逻辑运算符&&,||,!以及条件语句if(),会将操作数强制转换为布尔值。假值只有:false,0,'',null,undefined,NaN。其他所有值都是真值,包括空数组[]和空对象{}。
避坑指南:永远不要依赖复杂的隐式转换来写业务逻辑。显式转换你的数据,使用
Number(),String(),Boolean()或parseInt(),parseFloat()。代码的清晰性和可预测性远比少写几个字符重要。
3. 流程控制与函数:构建逻辑的骨架
掌握了数据和运算符,接下来就是用它们来组织程序逻辑。这部分语法看似简单,但细节决定成败。
3.1 条件判断:不只是if...else
if...else是最基本的,但switch和三元运算符在特定场景下能让代码更简洁。
switch语句的严格比较switch内部使用===进行严格比较,不会进行类型转换。
let val = '1'; switch (val) { case 1: // 不会匹配,因为 '1' === 1 为 false console.log('Number 1'); break; case '1': console.log('String 1'); // 输出这个 break; }忘记break的穿透效应这是switch里最常见的错误之一。如果case后面没有break,代码会继续执行下一个case,直到遇到break或switch结束。有时可以利用这个特性实现多个case共享同一段逻辑,但大多数情况下,记得写break。
三元运算符的嵌套与可读性三元运算符condition ? expr1 : expr2非常适合简单的二选一赋值。
let status = score >= 60 ? '及格' : '不及格';但切忌过度嵌套,这会让代码难以阅读。
// 难以阅读的嵌套三元 let message = isMember ? (price > 100 ? '尊享折扣' : '会员折扣') : (price > 100 ? '大额优惠' : '无优惠'); // 更好的写法:用if...else或拆分成多行、多个变量3.2 循环迭代:从for到for...of
循环是处理集合数据的利器。除了经典的for循环,ES5的forEach和ES6的for...of提供了更现代的迭代方式。
for循环的经典与控制
for (let i = 0; i < array.length; i++) { // 可以使用 break 跳出循环 // 可以使用 continue 跳过本次迭代 }在循环体内修改array.length(比如删除元素)可能会导致意外行为,因为终止条件i < array.length在每次迭代前都会重新计算。
forEach的局限forEach为数组的每个元素执行一次回调函数,语法简洁。但它有两个主要局限:
- 无法使用
break或continue。如果想提前终止循环,只能通过抛出异常这种不优雅的方式,或者使用some/every方法替代。 - 它总是返回
undefined,无法链式调用。
for...of循环的强大for...of是ES6引入的,用于遍历可迭代对象(Array,Map,Set,String等)。
for (const item of array) { console.log(item); }它的优点在于语法简洁,且可以直接遍历值(而非索引)。对于Map和Set尤其方便。它支持break和continue。
遍历对象用for...infor...of不能直接遍历普通对象(Object)。遍历对象的可枚举属性,要用for...in循环。
for (const key in object) { if (object.hasOwnProperty(key)) { // 通常建议加上,只遍历对象自身的属性,不包含原型链上的 console.log(key, object[key]); } }3.3 函数:一等公民的威力
在JavaScript中,函数是“一等公民”,这意味着它可以被赋值给变量,作为参数传递,也可以作为另一个函数的返回值。这是函数式编程和许多高级模式的基础。
函数声明与函数表达式函数声明会被提升,你可以在声明前调用它。函数表达式(包括箭头函数)不会被提升。
// 函数声明 sayHello(); // 可以正常执行 function sayHello() { console.log('Hello'); } // 函数表达式 sayHi(); // TypeError: sayHi is not a function var sayHi = function() { console.log('Hi'); };箭头函数的this绑定这是箭头函数与普通函数最核心的区别。箭头函数没有自己的this,它的this继承自定义时所在的作用域。
function Counter() { this.count = 0; setInterval(() => { this.count++; // 这里的`this`指向Counter的实例 console.log(this.count); }, 1000); } const counter = new Counter(); // 对比普通函数 function Counter2() { this.count = 0; setInterval(function() { this.count++; // 这里的`this`指向全局对象(或undefined in strict mode),不是Counter2的实例! console.log(this.count); }, 1000); } const counter2 = new Counter2(); // 不会按预期工作此外,箭头函数不能用作构造函数(不能用new调用),也没有arguments对象(可用剩余参数...args替代)。
参数:默认值、剩余参数与解构ES6极大地增强了函数参数的处理能力。
// 默认参数 function greet(name = 'Guest') { console.log(`Hello, ${name}`); } // 剩余参数 (替代 arguments) function sum(...numbers) { return numbers.reduce((acc, cur) => acc + cur, 0); } sum(1, 2, 3, 4); // 10 // 参数解构 function printUser({ name, age = 18 }) { console.log(`${name} is ${age} years old.`); } printUser({ name: 'Alice' }); // Alice is 18 years old.经验之谈:在回调函数、需要固定
this指向的场景(如事件处理器、定时器),优先使用箭头函数。当需要函数有自己的this(如对象方法、构造函数)或需要使用arguments时,使用普通函数声明或表达式。
4. 对象与原型:理解JavaScript的面向对象
JavaScript的面向对象是基于原型的,这与基于类的语言(如Java)有根本不同。理解原型链是理解JavaScript对象系统的关键。
4.1 对象创建与属性描述符
创建对象的多种方式
- 对象字面量:最常用、最简洁的方式。
let obj = { key: 'value' }; Object.create():创建一个新对象,使用现有的对象作为新对象的原型。这是最直接体现原型继承的方式。const parent = { greet() { console.log('Hi'); } }; const child = Object.create(parent); child.greet(); // 'Hi', 从parent继承而来- 构造函数 +
new:这是ES5及之前模拟类的主要方式。 - ES6
class语法:本质是构造函数的语法糖,但更清晰易用。
属性描述符对象的每个属性都关联着一个“属性描述符”,它定义了属性的元信息。我们可以用Object.getOwnPropertyDescriptor()查看,用Object.defineProperty()定义或修改。
let obj = {}; Object.defineProperty(obj, 'readOnlyProp', { value: 42, writable: false, // 不可写 enumerable: true, // 可枚举(会在for...in中出現) configurable: false // 不可配置(不能删除,也不能再修改描述符) }); obj.readOnlyProp = 100; // 静默失败(严格模式下会报错) console.log(obj.readOnlyProp); // 42writable控制值能否被修改,enumerable控制属性是否出现在枚举操作中(如for...in,Object.keys()),configurable控制属性描述符本身能否被修改以及属性能否被删除。
4.2 原型与原型链:继承的基石
__proto__与prototype这是两个最容易混淆的概念。
__proto__(现已标准化为Object.getPrototypeOf()):这是一个存在于每个对象实例上的内部属性(访问器属性),指向该对象的原型。prototype:这是一个只存在于函数对象上的属性。当你使用new关键字调用一个函数作为构造函数时,新创建对象的__proto__就会指向该函数的prototype属性。
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log(`Hello, I'm ${this.name}`); }; const alice = new Person('Alice'); console.log(alice.__proto__ === Person.prototype); // true console.log(Object.getPrototypeOf(alice) === Person.prototype); // true (推荐方式) alice.sayHello(); // 先在alice自身找sayHello,没找到,沿原型链找到Person.prototype上的sayHello原型链查找机制当访问一个对象的属性时,JavaScript引擎会:
- 在对象自身属性中查找。
- 如果没找到,则沿着
__proto__指向的原型对象上查找。 - 继续在原型的原型上查找,直到找到属性或到达原型链的尽头(
null)。 - 如果最终没找到,返回
undefined。
这就是JavaScript实现继承的方式。Object.prototype是所有普通对象原型链的终点。
4.3 ES6 Class:更清晰的语法糖
class语法让基于原型的继承写起来更像传统的类。
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类的constructor this.breed = breed; } speak() { // 方法重写 super.speak(); // 调用父类的speak方法 console.log(`${this.name} barks.`); } static describe() { // 静态方法,属于类本身,不属于实例 console.log('A dog is a mammal.'); } } const myDog = new Dog('Rex', 'Husky'); myDog.speak(); Dog.describe();extends关键字建立了原型链。super在构造函数中用于调用父类构造器,在方法中用于调用父类方法。static定义静态方法或属性。
核心理解:
class只是语法糖,底层依然是原型继承。typeof Dog返回'function'。Dog.prototype上存放着实例方法,Dog本身(构造函数)上存放着静态方法。理解这一点,就能看透class的本质,也能更好地调试相关问题。
5. 异步编程:从回调地狱到现代方案
JavaScript是单线程的,异步编程是其处理耗时操作(如网络请求、文件读写)而不阻塞主线程的核心机制。其演进历程是理解现代前端开发的关键。
5.1 回调函数与“回调地狱”
最初的异步模式是回调函数。
function fetchData(callback) { setTimeout(() => { callback('Data received'); }, 1000); } fetchData(function(data) { console.log(data); // 如果需要基于第一次的结果进行第二次异步操作... fetchMoreData(data, function(moreData) { console.log(moreData); // 第三次异步操作... fetchEvenMoreData(moreData, function(evenMoreData) { console.log(evenMoreData); // ... 嵌套越来越深 }); }); });这种深度嵌套的代码难以阅读、调试和维护,被称为“回调地狱”或“金字塔厄运”。
5.2 Promise:链式调用的救星
Promise对象代表一个异步操作的最终完成(或失败)及其结果值。它有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。
基本用法
const promise = new Promise((resolve, reject) => { // 异步操作 setTimeout(() => { const success = Math.random() > 0.5; if (success) { resolve('Operation succeeded!'); } else { reject(new Error('Operation failed!')); } }, 1000); }); promise .then((result) => { console.log('Success:', result); return result + ' And more processing.'; // 可以返回一个值或新的Promise }) .then((processedResult) => { console.log('Processed:', processedResult); }) .catch((error) => { console.error('Error:', error); }) .finally(() => { console.log('This runs regardless of success or failure.'); });.then()用于处理成功状态,.catch()用于处理失败状态,.finally()无论成功失败都会执行。.then()和.catch()都会返回一个新的Promise,因此可以链式调用,这解决了回调嵌套的问题。
Promise的静态方法
Promise.all([promise1, promise2, ...]): 等待所有Promise都成功,或任意一个失败。返回一个包含所有成功结果的数组,顺序与输入一致。Promise.race([promise1, promise2, ...]): 等待任意一个Promise完成(成功或失败)。Promise.allSettled([promise1, promise2, ...]): 等待所有Promise都完成(无论成功失败),返回一个描述每个Promise结果的对象数组。Promise.any([promise1, promise2, ...]): 等待任意一个Promise成功。如果全部失败,则返回一个聚合错误。
5.3 Async/Await:以同步方式写异步代码
async/await是建立在Promise之上的语法糖,它让异步代码看起来和同步代码一样直观。
基本用法
async function fetchUserData() { try { console.log('Fetching user...'); const userResponse = await fetch('/api/user'); // await 会暂停执行,直到Promise完成 const user = await userResponse.json(); console.log('Fetching posts for', user.name); const postsResponse = await fetch(`/api/posts?userId=${user.id}`); const posts = await postsResponse.json(); return { user, posts }; } catch (error) { console.error('Failed to fetch data:', error); throw error; // 重新抛出错误,让调用者处理 } } // 调用 fetchUserData() .then(data => console.log(data)) .catch(err => console.error(err));async关键字用于声明一个函数是异步的,它总是返回一个Promise。await关键字只能在async函数内部使用。它会暂停函数的执行,等待后面的Promise完成,然后返回该Promise的结果。如果Promise被拒绝,await会抛出拒绝的原因,可以用try...catch捕获。
错误处理使用try...catch是处理async/await中错误最自然的方式,就像处理同步代码一样。
并行优化注意,上面的例子中,获取用户和获取帖子是串行的(等用户数据回来后才去请求帖子)。如果它们没有依赖关系,应该并行执行以提高效率。
async function fetchDataInParallel() { try { const [userResponse, postsResponse] = await Promise.all([ fetch('/api/user'), fetch('/api/posts') ]); const user = await userResponse.json(); const posts = await postsResponse.json(); return { user, posts }; } catch (error) { // 处理错误 } }最佳实践:在现代JavaScript开发中,
async/await已成为处理异步操作的首选。它极大地提高了代码的可读性和可维护性。但请记住,它并没有取代Promise,而是让使用Promise变得更加容易。在需要并行执行多个独立异步任务时,结合Promise.all等静态方法使用async/await,能写出既清晰又高效的代码。
6. 模块化:从脚本标签到现代工程
早期的JavaScript通过多个<script>标签引入,所有变量都暴露在全局作用域,导致命名冲突和依赖管理混乱。模块化解决了这些问题。
6.1 CommonJS与ES Modules的对比
CommonJS (CJS)主要用于Node.js环境。使用require()导入,module.exports或exports导出。
// math.js function add(a, b) { return a + b; } module.exports = { add }; // 或 exports.add = add; // main.js const math = require('./math.js'); console.log(math.add(2, 3));特点是同步加载模块,适合服务器端。
ES Modules (ESM)ES6引入的官方标准,现在被浏览器和现代Node.js原生支持。使用import导入,export导出。
// math.mjs (或 .js 文件,在package.json中设置"type": "module") export function add(a, b) { return a + b; } // main.mjs import { add } from './math.mjs'; console.log(add(2, 3));特点是静态化,在编译时就能确定依赖关系,支持异步加载和tree shaking(移除未使用代码)。
6.2 导出与导入的多种语法
命名导出与导入
// 导出 export const name = 'value'; export function func() {}; export class ClassName {}; // 或统一导出 const a = 1; const b = 2; export { a, b }; export { a as aliasA }; // 重命名导出 // 导入 import { name, func } from './module.js'; import { a as myA, b } from './module.js'; // 重命名导入 import * as Module from './module.js'; // 全部导入为一个命名空间对象 console.log(Module.name);默认导出与导入一个模块只能有一个默认导出,通常用于导出模块的主要功能。
// 导出 export default function myFunction() {}; // 导入 (可以任意命名) import myFunc from './module.js'; import whateverName from './module.js';混合导入
import defaultExport, { namedExport1, namedExport2 } from './module.js';6.3 在浏览器与Node.js中使用ESM
浏览器在HTML中,使用<script type="module">标签。
<script type="module"> import { add } from './math.js'; console.log(add(1, 2)); </script>注意:模块脚本默认是defer的,且具有自己的作用域,不会污染全局。
Node.js在.mjs文件中直接使用。在.js文件中,需要在package.json中设置"type": "module",或者在命令行使用--input-type=module标志。
模块化选择:对于新项目,尤其是前端项目,强烈推荐使用ES Modules。它是语言标准,得到现代构建工具(如Webpack, Vite, Rollup)和运行时的良好支持。对于Node.js后端项目,如果版本支持(Node.js 13.2.0+ 稳定支持),也推荐使用ESM,它是未来趋势。CommonJS主要用于旧的Node.js生态库或遗留项目。
7. 常用内置对象与API速览
JavaScript提供了丰富的内置对象和API来处理各种任务。这里列举几个最常用和易错的核心对象。
7.1 Array:数组的高阶方法
除了push,pop,shift,unshift,slice,splice等基础方法,ES5引入的高阶函数极大地提升了数组处理能力。
map,filter,reduce这是函数式编程的“三巨头”。
map: 遍历数组,对每个元素执行回调函数,返回由回调结果组成的新数组。原数组不变。const numbers = [1, 2, 3]; const doubled = numbers.map(num => num * 2); // [2, 4, 6]filter: 遍历数组,对每个元素执行回调函数,返回使回调函数返回true的元素组成的新数组。原数组不变。const evens = numbers.filter(num => num % 2 === 0); // [2]reduce: 遍历数组,将数组元素累积为单个值。接收一个回调函数(累加器,当前值,当前索引,原数组)和一个初始值。const sum = numbers.reduce((acc, cur) => acc + cur, 0); // 6 // 求最大值 const max = numbers.reduce((a, b) => Math.max(a, b), -Infinity);
find,findIndex,some,every
find: 返回数组中满足测试函数的第一个元素的值,否则返回undefined。findIndex: 返回索引。some: 测试数组中是否至少有一个元素通过了测试函数。every: 测试数组中的所有元素是否都通过了测试函数。
7.2 String:字符串的现代操作
字符串是不可变的,所有方法都返回新字符串。
includes(),startsWith(),endsWith(): 检查是否包含、以某字符串开头或结尾。repeat(count): 重复字符串。padStart(targetLength, padString),padEnd(): 填充字符串到指定长度。trim(),trimStart(),trimEnd(): 去除空格。- 模板字面量:使用反引号(
`)定义,可以嵌入表达式(${expression})和多行字符串。const name = 'World'; const greeting = `Hello, ${name}! This is a multi-line string.`;
7.3 Date与JSON
DateJavaScript的Date对象基于Unix时间戳(自1970年1月1日UTC以来的毫秒数)。注意月份是从0开始的(0代表一月)。
const now = new Date(); const specificDate = new Date('2023-12-25'); const timestamp = Date.now(); // 当前时间戳 // 常用方法 now.getFullYear(); // 年 now.getMonth(); // 月 (0-11) now.getDate(); // 日 (1-31) now.getHours(); // 时 now.toISOString(); // 转为ISO格式字符串 '2023-12-25T10:30:00.000Z'对于复杂的日期操作,推荐使用第三方库如moment.js(已进入维护模式)或更现代的date-fns、day.js。
JSONJSON对象用于在JavaScript对象和JSON字符串之间转换。
JSON.stringify(value[, replacer[, space]]): 将JavaScript值转换为JSON字符串。replacer可以是函数或数组,用于过滤或转换属性。space用于美化输出(缩进空格数)。const obj = { name: 'Alice', age: 30, city: undefined }; JSON.stringify(obj); // '{"name":"Alice","age":30}' // 注意:值为undefined的属性会被忽略JSON.parse(text[, reviver]): 解析JSON字符串,构造JavaScript值。reviver函数可以在返回之前转换解析结果。const jsonStr = '{"name":"Alice","age":30}'; const parsedObj = JSON.parse(jsonStr);
7.4 Map与Set:更专业的集合
Map类似于对象,是键值对的集合。但Map的键可以是任意类型(对象、函数、原始值),而普通对象的键只能是字符串或Symbol。Map还维护键值对的插入顺序,并且有更便捷的属性和方法(size,clear(),forEach)。
const map = new Map(); const keyObj = {}; map.set(keyObj, 'value associated with object'); map.set('name', 'Alice'); console.log(map.get(keyObj)); // 'value associated with object' console.log(map.size); // 2 for (let [key, value] of map) { console.log(key, value); }Set是值的集合,值唯一,没有重复值。
const set = new Set([1, 2, 3, 3, 4]); console.log(set.size); // 4 (重复的3被去重) set.add(5); console.log(set.has(4)); // true set.delete(2); // 数组去重 const uniqueArray = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]选择建议:当你需要键不是字符串,或者需要维护插入顺序,或者需要频繁增删键值对时,使用
Map。当你需要存储唯一值的列表,并高效检查值是否存在时,使用Set。对于简单的键值对集合,且键都是字符串,使用普通对象字面量{}通常更简洁高效。
8. 常见问题与调试技巧实录
理论学得再多,最终都要落到解决问题上。这里记录了一些我踩过的坑和总结的调试经验。
8.1 高频错误与排查思路
Uncaught TypeError: Cannot read property 'xxx' of undefined/null- 原因:试图访问
undefined或null值的属性。 - 排查:
- 检查变量是否已正确初始化。
- 检查异步操作(如API请求)是否已完成,数据是否已返回。
- 使用可选链操作符
?.进行安全访问:obj?.prop?.subProp。 - 使用默认值或条件判断:
const value = obj && obj.prop ? obj.prop : defaultValue;或const value = obj?.prop ?? defaultValue;(空值合并运算符)。
- 原因:试图访问
Uncaught ReferenceError: xxx is not defined- 原因:引用了一个未声明的变量。
- 排查:
- 检查变量名拼写是否正确。
- 检查变量作用域。在块级作用域(
{})内用let/const声明的变量,外部无法访问。 - 检查脚本加载顺序,确保依赖的库或模块已先加载。
Uncaught SyntaxError: Unexpected token- 原因:语法错误。通常是缺少括号、引号、逗号,或者使用了无效的字符。
- 排查:现代IDE或编辑器通常能高亮显示语法错误。仔细查看错误提示的行和附近代码。注意JSON字符串必须是双引号。
异步操作导致的时序问题
- 现象:代码逻辑看似正确,但结果不对,或者变量在预期的时间点没有值。
- 排查:
- 多用
console.log:在关键步骤打印变量状态和时间戳。 - 使用断点调试:在浏览器开发者工具的Sources面板或Node.js调试器中设置断点,单步执行,观察调用栈和作用域变量。
- 检查Promise状态:确保
.then()或await被正确使用,没有在Promise完成前就使用其结果。
- 多用
8.2 浏览器开发者工具实战技巧
Console不只是
log:console.table(arrayOrObject): 以表格形式展示数组或对象,非常清晰。console.dir(node): 以交互式列表形式显示DOM元素的属性。console.time(label)/console.timeEnd(label): 计算代码执行时间。console.trace(): 打印当前的调用栈,用于追踪函数调用来源。
Sources面板断点调试:
- 除了行号断点,还可以设置条件断点(右键行号->Add conditional breakpoint),只在表达式为真时暂停。
- 事件监听器断点:在右侧Breakpoints区域,可以勾选
click,XHR等事件,当事件触发时自动暂停。 - DOM变更断点:在Elements面板,右键DOM节点,选择“Break on” -> “subtree modifications”等,当DOM被修改时暂停。
Network面板分析请求:
- 查看请求/响应的头部、预览、响应时间。
- 使用“Disable cache”选项,确保总是获取最新资源。
- 模拟慢速网络(Throttling),测试页面在弱网下的表现。
Application面板管理存储:
- 查看和编辑
LocalStorage,SessionStorage,IndexedDB,Cookies。
- 查看和编辑
8.3 Node.js环境调试
使用
--inspect标志:node --inspect your-script.js然后在Chrome浏览器中打开
chrome://inspect,点击“Open dedicated DevTools for Node”进行远程调试,体验和浏览器调试几乎一样。内置的
debugger语句: 在代码中需要暂停的地方插入debugger;语句,然后以node inspect your-script.js或node --inspect-brk your-script.js启动,程序会在debugger处自动暂停。console的异步问题: 在Node.js中,console.log对于某些复杂对象(如深层嵌套对象、包含循环引用的对象)的输出可能是异步的或简化的。对于需要精确查看的对象,使用JSON.stringify(obj, null, 2)进行格式化输出,或者使用util.inspect(obj, { depth: null, colors: true })。
8.4 代码质量与性能小贴士
- 使用严格模式:在脚本或函数顶部添加
'use strict';。它能帮助发现一些潜在错误(如未声明变量、对只读属性赋值等),让代码更安全,也为未来JavaScript版本兼容性做准备。 - 避免全局变量:尽量将代码封装在模块、函数或块级作用域内,减少对全局作用域的污染。这是模块化的核心思想之一。
- 理解事件循环:对于复杂的异步代码,理解JavaScript的事件循环、宏任务与微任务(
setTimeout,Promise,async/await)的执行顺序至关重要。这能解释很多“为什么这段代码后执行”的问题。 - 善用Lint工具:使用ESLint等代码检查工具,并搭配Prettier等代码格式化工具。它们能在编写阶段就发现潜在的错误和不一致的风格,强制养成良好的编码习惯。