如果你正准备入门前端,或者刚学完 HTML 和 CSS,想迈出 JavaScript 这一步,那这篇内容就是为你准备的。
先纠正一个常见认知:JavaScript 不是“很简单”的语言。它简单在语法层面,复杂在运行模型。很多人学了三个月,能看懂教程里的每一段代码,但自己写一个弹出框、一个数据筛选、一个点击排序功能时,还是会卡住。问题往往不是不努力,而是没有把核心语法的“运行规则”真正建立起来。
30 分钟能学会 JS 核心语法吗?能。但不是靠背语法,而是靠建立一个准确地“代码运行心理模型”。这篇文章不会把所有知识点都给你,那本身也不现实;我会挑出前端开发里最高频的变量、函数、数组对象操作、异步处理和模块化这几块,用最短路径帮你跑通。
读完本文,你能收获三样东西:
- 一套可以直接运行的 JS 最小知识体系,而不是碎片化语法清单;
- 对变量、函数、作用域、异步这些核心概念的准确理解,避开新手最常见的理解偏差;
- 一套自查和排错的方法,让报错变成你学习的线索,而不是劝退的理由。
建议你打开浏览器控制台(F12 或者鼠标右键检查),跟着每一节的示例敲一遍。只看不敲,30 分钟后就忘了;边看边敲,这些语法才会变成你自己的东西。
1. 这篇文章真正要解决的问题
很多前端初学者会陷入一个很尴尬的阶段:HTML 和 CSS 写得还行,一到 JavaScript 就感觉像换了个世界。教程看得懂,自己写不出;复制能运行,改需求就报错。这背后的原因,不是 JavaScript 真的那么难,而是学习顺序出了问题。
大部分人学 JS 是这样的顺序:变量、数据类型、运算符、if 判断、for 循环、函数……然后呢?然后学完了,还是不知道这些东西组合起来能干什么。因为这种按语法清单式推进的学习方式,缺少一个关键环节:用真实的运行场景把这些点串起来。
JavaScript 本身不是一门“背字典”的语言,它是一门“理解行为”的语言。你需要知道的不只是“let 声明变量、const 声明常量”,而是:
- 这段代码在什么时候执行?
- 变量在这个位置能不能被访问?
- 这个函数里的 this 到底是谁?
- 数据从接口返回时,为什么打印出来是 Promise 对象而不是数据本身?
这篇文章要解决的,就是这些问题。我把它称之为“运行视角”的 JS 核心语法学习方式。它不是零基础到精通的全套教程,而是帮你把最重要的几个语法模块,用最短时间建立起准确理解,让你后续学 DOM、学框架 Vue/React 时,不会被基础语法卡住。
如果你正在准备前端面试,这部分内容同样重要。无论面试题怎么变,变量提升、闭包、this 指向、Promise、事件循环这些考点,背后的底层支撑都是核心语法。把底层逻辑理解透了,面试题只是换了一层壳而已。
2. 运行环境准备:打开浏览器的“实验室”
学习 JavaScript 不需要安装复杂的开发环境。你手边的浏览器就是最好的学习工具。任何一个现代浏览器(Chrome、Edge、Firefox 都可以),按一下 F12 键,就能打开开发者工具,里面有 Console(控制台)面板。这就是你的 JS 实验场地。
在控制台里,你可以直接输入 JavaScript 代码,按回车立即看到执行结果。这种方式特别适合验证语法、测试函数、查看变量类型,反馈速度比写完整文件快得多。
2.1 浏览器控制台 vs Node.js:两种运行环境
JavaScript 有两个最常见的运行环境:
- 浏览器环境:内置了 DOM、BOM 等 API,可以操作页面元素、监听用户事件、发起网络请求。前端开发的核心环境。
- Node.js 环境:可以在服务器端运行 JavaScript,没有 DOM 和 BOM,但有文件系统、HTTP 服务等能力。一般用在后端开发、工具链构建。
如果你是刚开始学,建议直接用浏览器控制台,省去安装配置的步骤。当你需要写多行代码文件时,再考虑安装 Node.js。安装好后,运行这个命令验证环境:
node -v能看到版本号,说明 Node.js 就绪了。
2.2 在 HTML 中引入 JavaScript
在实际前端项目中,JS 代码不是直接写在控制台里的,而是通过<script>标签引入到 HTML 页面中。这是最标准的用法。
<!-- 文件路径:index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>JS 核心语法练习</title> </head> <body> <h1>打开控制台查看输出</h1> <!-- 方式一:内联方式 --> <script> console.log("我是内联脚本"); </script> <!-- 方式二:外部文件方式,推荐 --> <script src="main.js"></script> </body> </html>// 文件路径:main.js console.log("我是外部脚本文件 main.js");这里有一个新手容易踩的坑:如果把<script>标签放在<head>里,浏览器会先加载并执行 JS,再渲染下面的 HTML 内容。如果 JS 里要去获取某个 DOM 元素,而此时元素还没渲染出来,就会拿到 null,后续操作直接报错。
所以最常见的做法,是把<script>标签放在<body>标签的最后面。或者使用defer属性,让脚本在 HTML 解析完成后再执行。这个细节在你刚开始写交互时会频繁遇到,提前注意会省去很多排查时间。
3. 变量与数据类型:从“存东西”开始
变量是 JavaScript 中最基础的概念。你可以把它理解为一个带标签的盒子,里面存了数据。给盒子起个名字,以后通过名字就能取出里面的数据。
3.1 var、let、const:三个关键字怎么选
ES6 之前,JavaScript 声明变量只用var。ES6 引入let和const之后,现在的主流实践已经变成:默认用const,需要重新赋值时用let,项目中基本不再主动使用var。
var 的问题:它存在变量提升(var hoisting)和函数作用域。变量提升意味着你可以在声明之前使用变量,只不过值会是undefined,这种机制容易导致隐蔽的 bug。而且var只有函数作用域,没有块级作用域,循环里用var声明变量,循环结束后变量依然存在。
let 和 const 的改进:它们有块级作用域,并且不存在提升后的可用状态(在声明前访问会直接抛 ReferenceError,这个阶段叫做暂时性死区)。
// 示例 1:var、let、const 的区别 function testVar() { console.log(a); // undefined,不会报错,因为 var 提升了声明 var a = 10; console.log(a); // 10 } function testLet() { // console.log(b); // 这里会报错:Cannot access 'b' before initialization let b = 20; console.log(b); // 20 } testVar(); testLet();需要注意,const声明的是“变量指向的那个引用不能变”,而不是“值不可变”。如果const声明的是一个对象或数组,你依然可以修改对象的属性、数组的元素,只是不能把这个变量重新指向别的东西。
// 示例 2:const 声明的对象属性可以修改 const user = { name: "张三", age: 18 }; user.age = 19; // 合法,修改属性值 console.log(user); // { name: "张三", age: 19 } // user = {}; // 报错:Assignment to constant variable在所有后续示例中,我建议你统一养成习惯:优先用const声明数组、对象、函数引用;只有当这个变量后续确实需要重新赋值时,才改用let。
3.2 数据类型与 typeof 的“迷惑行为”
JavaScript 的数据类型分为两大类:
- 原始类型(Primitive Types):
string、number、boolean、undefined、null、symbol、bigint。 - 引用类型(Reference Types):
object、function。数组(Array)、日期(Date)、正则(RegExp)本质上都是 object 的特例。
这里有两个新手高频踩坑点:
坑点一:typeof null的结果是"object"。这是 JavaScript 语言设计早期遗留的 bug,官方后来也没有修复,因为修复会导致大量存量代码出问题。所以你判断一个变量是不是null时,不要用typeof,而是用=== null直接比较。
坑点二:typeof对函数会返回"function"。所以可以用typeof fn === 'function'来判断一个变量是不是函数,这个方法很常用。
// 示例 3:typeof 常见陷阱 console.log(typeof "hello"); // string console.log(typeof 42); // number console.log(typeof true); // boolean console.log(typeof undefined); // undefined console.log(typeof null); // object(注意这个历史遗留问题) console.log(typeof []); // object console.log(typeof {}); // object console.log(typeof function(){}); // function // 正确判断 null 的方式 const n = null; if (n === null) { console.log("n 是 null"); }另外一个容易混淆的是undefined和null的区别。简单理解:
undefined表示“声明了变量但还没有赋值”,是系统层面的“尚未定义”。null表示“已经有明确意图,值为空”,是开发人员主动设置的“空值”。
在实际开发中,接口返回数据、函数默认参数、对象属性缺失时,undefined出现的频率极高。你需要学会容忍它、判断它,而不是一看到undefined就认为代码出错了。
4. 函数与作用域:理解代码的执行边界
函数是 JavaScript 最重要的构成单元。市面上常说 JavaScript 是“函数式语言”,说的就是函数在这门语言里的地位。
4.1 函数的三种常见写法
// 写法一:函数声明(Function Declaration) function add(a, b) { return a + b; } // 写法二:函数表达式(Function Expression) const subtract = function (a, b) { return a - b; }; // 写法三:箭头函数(Arrow Function,ES6) const multiply = (a, b) => a * b; const sayHello = (name) => { return `你好,${name}`; }; console.log(add(1, 2)); // 3 console.log(subtract(5, 2)); // 3 console.log(multiply(2, 3)); // 6 console.log(sayHello("张三")); // 你好,张三三种写法的区别,新手阶段需要先记住两条关键规则:
- 函数声明有提升机制,可以先调用后声明;函数表达式和箭头函数没有提升,必须先定义后使用。
- 箭头函数没有自己的
this。它的this是定义时外层作用域的this。这个特性在回调函数、事件处理中非常有用,也是面试高频考点。
关于箭头函数中的this,用一个最小示例来说明:
// 示例 4:箭头函数中的 this 与外层保持一致 const obj = { name: "李四", // 普通函数写法 logName: function () { setTimeout(function () { console.log(this.name); // undefined,因为这里的 this 指向 window 或全局对象 }, 100); }, // 箭头函数写法 logNameArrow: function () { setTimeout(() => { console.log(this.name); // 李四,箭头函数沿作用域链找到了外层对象 }, 100); } }; obj.logName(); obj.logNameArrow();在浏览器环境下,普通函数里的setTimeout回调,this默认指向window(严格模式下为undefined),所以拿不到对象里的name。而箭头函数没有自己的this,会向定义时的外层作用域寻找,因此能正确访问obj.name。这也就是为什么现在写回调函数时,优先推荐箭头函数。
4.2 作用域与闭包
作用域决定了“变量在哪里可以被访问”。JavaScript 的作用域机制可以概括为:
- 全局作用域:在函数之外声明的变量,整个代码都能访问。
- 函数作用域:函数内部声明的变量,函数外面访问不到。
- 块级作用域:
let和const声明的变量,只在大括号(块)内有效。 - 作用域链:当内部函数访问一个变量时,会沿着外层作用域一层一层往上找,直到找到或到达全局作用域。
闭包则是一个这样的函数:它“记住”了定义它时的外部变量,即使外层函数已经执行完毕,内层函数仍然可以访问那些外部变量。这个特性在数据封装、防抖节流、React Hooks 实现中都有应用。
// 示例 5:闭包计数器 function createCounter() { let count = 0; return function () { count++; console.log("当前计数:", count); }; } const counter = createCounter(); counter(); // 当前计数: 1 counter(); // 当前计数: 2 counter(); // 当前计数: 3在这个示例里,createCounter返回的内部函数一直“霸占”着count变量,即使createCounter已经执行结束,count也没有被销毁。外部没有任何途径能直接修改count,只有通过内部函数才能让计数加一。这就是闭包最经典的用法:实现私有变量。
5. 数组与对象的常用操作:写业务代码的利器
学完变量和函数,你就具备了基本的代码表达能力。但实际上每天写前端业务,打交道最多的两大类数据结构是数组和对象。几乎所有从后端接口拿到的数据,最后都要经过数组和对象的转换操作,再渲染到页面上。
数组的常规操作很多,这里挑出使用率最高的几个:
push/pop/shift/unshift:数组尾部或头部添加、删除元素。forEach:遍历数组,执行回调函数。map:遍历数组,返回一个新数组,新数组长度不变,对每个元素做映射转换。filter:遍历数组,过滤出符合条件的元素,返回一个新数组。find:找到符合条件的第一个元素。reduce:将数组归并成一个值,处理求和、分组等场景十分方便。
// 示例 6:数组高频操作 const numbers = [1, 2, 3, 4, 5]; // forEach 执行遍历 numbers.forEach((num) => { console.log("当前数字:", num); }); // map 对每个元素做映射,返回等长新数组 const doubled = numbers.map((num) => num * 2); console.log(doubled); // [2, 4, 6, 8, 10] // filter 过滤出符合条件的元素 const evenNumbers = numbers.filter((num) => num % 2 === 0); console.log(evenNumbers); // [2, 4] // find 找到第一个符合的元素 const firstBigNumber = numbers.find((num) => num > 3); console.log(firstBigNumber); // 4 // reduce 归并为单个值 const sum = numbers.reduce((total, num) => total + num, 0); console.log(sum); // 15对象操作中,最核心的是属性访问、属性遍历、属性的增删改查。还有 ES6 解构赋值,它能把对象或数组里的值直接拆出来给变量,大量减少临时变量和重复访问。
// 示例 7:对象解构 const user = { name: "王五", age: 25, city: "北京" }; // 对象解构 const { name, age } = user; console.log(name); // 王五 console.log(age); // 25 // 也可以解构数组 const [first, second, third] = ["A", "B", "C"]; console.log(first); // A console.log(second); // B // 合并对象:展开运算符 const moreInfo = { city: "上海", email: "wangwu@example.com" }; const fullUser = { ...user, ...moreInfo }; console.log(fullUser); // { name: "王五", age: 25, city: "上海", email: "wangwu@example.com" }在真实项目里,你拿到接口返回的数据经常是嵌套结构。比如一个商品列表,每个商品有分类、有价格、有标签,你要从中提取某个分类下的所有商品名称,用数组和对象操作组合起来,几行就能完成。这也是为什么说学 JS 核心语法,重点不是背 API,而是学会组合。
6. 异步编程:理解回调、Promise 与 async/await
异步编程是整个 JavaScript 学习中最难跨过但又必须跨过的一道坎。它和同步代码的直觉很不一致:代码顺序不是从上到下依次执行,而是“先挂起,等待结果后再回来执行”。
6.1 为什么需要异步
页面上的一个按钮点击、向服务器发请求拿数据、定时器延迟执行——这些都是异步操作。如果 JavaScript 是同步的,发出请求后页面就会卡住,一直等服务器返回才能继续做别的事,用户体验会非常差。异步让 JavaScript 可以在等待耗时操作的同时,继续响应其他事件。
6.2 回调函数与回调地狱
最早处理异步的方式是回调函数:把“做完之后要执行的代码”作为参数传给函数。
// 示例 8:回调函数处理异步 console.log("开始请求数据"); function getUser(callback) { setTimeout(() => { const user = { id: 1, name: "赵六" }; callback(user); }, 1000); } getUser((user) => { console.log("拿到用户:", user); }); console.log("请求已发出,先执行后面的代码");运行这个示例,你会发现控制台的输出顺序是:
开始请求数据 请求已发出,先执行后面的代码 拿到用户: { id: 1, name: "赵六" }这就是典型的异步执行特征:后续代码不会阻塞等待,回调里的内容在延迟结束后才执行。
如果异步操作层层嵌套,代码就会变成著名的“回调地狱”。一层请求之后再发一层请求,大括号往下无限缩进,可读性和维护性都在崩溃边缘。
6.3 Promise:把异步流程整理成链式调用
Promise 的出现就是为了解决回调地狱。它有三种状态:pending(等待中)、fulfilled(成功)、rejected(失败)。状态一旦从 pending 变为 fulfilled 或 rejected,就不可再变。
// 示例 9:使用 Promise function getData() { return new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve("数据获取成功"); } else { reject("数据获取失败"); } }, 1000); }); } getData() .then((data) => { console.log("成功:", data); return "传递到下一个 then"; }) .then((message) => { console.log("上一个 then 的返回值:", message); }) .catch((err) => { console.log("失败:", err); }) .finally(() => { console.log("无论成功失败,最后都会执行"); });.then()接收成功回调,.catch()接收失败回调,.finally()无论成功失败都会执行。链式调用让异步代码的结构变得扁平清晰,不再一层嵌一层。
日常开发中,你使用第三方的请求库、后端接口调用库返回的基本都是 Promise 对象。所以你经常会遇到这样的情况:console.log(接口调用结果)打印出来的是一个 Promise,而不是数据,原因就在于此——你需要先.then()或者在async/await里等待它完成。
6.4 async/await:让异步代码看起来像同步
ES2017 引入的async/await进一步简化了 Promise 的使用。它让异步代码写起来和同步代码一样顺序执行,极大地提升了可读性。
// 示例 10:async/await 写法 function getData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("异步数据"); }, 1000); }); } async function fetchData() { console.log("开始获取数据"); const data = await getData(); console.log("数据返回:", data); return data; } fetchData().then((data) => { console.log("fetchData 完成后拿到:", data); });需要记住两个关键点:
await只能在async函数内部使用。await会暂停当前async函数的执行,等待后面的 Promise 进入 fulfilled 状态后,把结果取出来赋值给变量。整个过程中,主线程并没有被阻塞,其他事件照常处理。
如果要处理多个并行的异步任务,可以用Promise.all或Promise.allSettled。Promise.all遇到其中一个失败就会整体失败;Promise.allSettled会等待所有任务完成,返回每个任务的成功或失败结果,在实际业务中更适合那种“部分成功也允许继续”的场景。
// 示例 11:Promise.all 与 Promise.allSettled const task1 = Promise.resolve("任务1完成"); const task2 = Promise.reject("任务2失败"); const task3 = Promise.resolve("任务3完成"); Promise.all([task1, task2, task3]) .then((results) => console.log("all 结果:", results)) .catch((err) => console.log("all 失败:", err)); // 输出:all 失败: 任务2失败,因为有一个失败整体就失败 Promise.allSettled([task1, task2, task3]) .then((results) => { console.log("allSettled 结果:", results); // 会得到 [{status:"fulfilled", value:"任务1完成"}, {status:"rejected", reason:"任务2失败"}, ...] });这是前端面试常考的知识点,也是实际项目中管理多个接口依赖时一定会用到的能力。
7. 模块化:用 import / export 组织代码
当你写的代码越来越多,把所有内容堆在一个文件里是不现实的。模块化的思路是:一个文件就是一个模块,各自管理自己的逻辑,通过export导出对外暴露的内容,通过import引入外部依赖。
浏览器的原生 ES Module 是现在前端项目的标准方案。使用时需要在<script>标签加上type="module"属性。由于这涉及网络请求和跨域限制,直接双击 HTML 文件往往无法生效,推荐用本地开发服务器运行。
// 文件路径:math.js export function add(a, b) { return a + b; } export const PI = 3.14159; export default function multiply(a, b) { return a * b; }// 文件路径:main.js import multiply, { add, PI } from "./math.js"; console.log(add(2, 3)); // 5 console.log(PI); // 3.14159 console.log(multiply(2, 3)); // 6<!-- 文件路径:index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>ES Module 示例</title> </head> <body> <script type="module" src="main.js"></script> </body> </html>运行方式:如果你安装了 Node.js,最简单的是用npx serve启动一个本地静态文件服务器,然后浏览器访问生成的地址。这样 ES Module 就不会受到file://协议限制。
模块化还带来了一个额外的好处:命名冲突大大减少。每个模块有自己独立的作用域,不用再担心全局变量的污染问题。在 Vue、React 项目里,组件本质上就是一个模块,通过import引入其他组件,通过export default或命名导出向外暴露自己的组件内容。
8. 常见问题与排查思路
学习 JS 的过程中,报错是不可避免的。与其害怕报错,不如掌握一套快速定位问题的思路。下面整理初学者最常遇到的几个问题和排查方向。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
Cannot access 'x' before initialization | 在let/const声明前访问了变量,触发暂时性死区 | 检查变量声明位置的先后顺序 | 把变量声明和使用调整顺序,避免提前访问 |
函数内this是undefined或指向错误对象 | 普通函数的this由调用方式决定,而不是定义位置 | 在函数内打印this,观察调用场景 | 改用箭头函数,或用.bind()、.call()指定 this |
| 调用接口后打印 Promise 对象而不是数据 | 忘记用.then()或者await等待 Promise 完成 | console.log打印结果的类型,查看是否为 Promise | 在async函数中用await接收结果 |
| 从接口拿到的数据是字符串,不是对象 | 后端返回了 JSON 字符串,或未做解析 | 用typeof查看数据类型 | 用JSON.parse()解析,或用请求库的自动解析能力 |
forEach里使用async/await不生效 | forEach不会等待异步回调,执行时机不对 | 在回调中打印执行顺序 | 改用for...of循环,或使用Promise.all配合map |
开启严格模式后this变成undefined | 严格模式下,普通函数默认this不再指向全局对象 | 查看是否在脚本头部声明了'use strict' | 根据业务需要显式绑定 this,或使用箭头函数 |
const声明的数组 or 对象被整体重新赋值后报错 | 混淆了“引用不可变”和“值不可变” | 检查是否对 const 变量执行了重新赋值 | 修改属性/数组元素是合法的,重新赋值需要改用let |
JSON.stringify后丢失了方法和undefined | JSON 格式不支持函数与undefined值 | 查看序列化前后的数据结构差异 | 序列化前主动过滤方法字段,或使用 reviver 回调处理 |
8.1 一个实用的排错路径
遇到代码没有按预期运行,不要慌,按这个顺序排查:
- 看控制台报错信息。找到红色的错误提示,报错信息里往往直接指出了哪一行、什么类型的错误。
- 在疑似出错的位置前后加
console.log打印关键变量。确认变量到底有没有值、是什么类型、进没进到某段逻辑。 - 把复杂表达式拆开。比如
const result = getData().filter(...).map(...)报错时,先打印getData()的结果,确认它是不是一个数组。很多报错的根源是中间某个 API 的返回值不是预期类型。 - 判断是同步错误还是异步错误。同步错误可以加
try/catch捕获,异步错误要写在 Promise 的.catch或者 async 函数内部的try/catch里。
// 示例 12:try/catch 捕获异常 async function loadData() { try { const response = await fetch("https://example.com/api/user"); if (!response.ok) { throw new Error("HTTP 状态码异常: " + response.status); } const data = await response.json(); console.log("数据:", data); } catch (err) { console.log("请求出错:", err.message); } } loadData();建议不要直接在.then()里面抛错后就不管了。能捕获的错误尽量捕获,给用户一个可理解的提示,哪怕只是console.error("请求失败,请稍后重试"),也比控制台一片红更有价值。
9. 最佳实践与工程建议
语法入门之后,养成良好的编码习惯比多学几个 API 更重要。下面这些实践建议,是我认为是新手到进阶之间最关键的分水岭。
9.1 开启严格模式
在 JavaScript 文件顶部、或者函数体开头加上这一行:
'use strict';严格模式会禁止一些不安全或不规范的写法,比如给未声明的变量赋值、删除不可删除的属性等。它让错误在开发阶段就暴露出来,而不是上线后成为隐性问题。现代 ES Module 和多数构建工具会在默认严格模式下运行代码,但如果你写的是普通<script>脚本,建议手动开启。
9.2 使用有意义的命名
变量名和函数名要能表达意图。比如let a = 100不如let price = 100清晰;function fn()不如function calculateTotalPrice()清晰。这不是矫情,而是当项目规模变大、代码被多人维护时,好名字能省下海量的沟通成本。
函数命名建议用动词开头:getUserList、saveOrder、handleClick、formatDate。布尔变量用is、has、can开头:isLoading、hasPermission、canSubmit。
9.3 优先使用 const,减少副作用
前面反复提到:默认用const,需要变了才用let。这个习惯的价值在于,它给你的代码增加了隐含约束——这个变量不会在别处被意外重新赋值。变量越难被改变,代码的确定性越强,排查问题时心智负担越小。
9.4 用解构赋值替代大段赋值代码
解构赋值是让代码变干净的高性价比语法。从接口拿数据时,最常用到对象解构:
// 不推荐 const name = response.data.user.name; const age = response.data.user.age; // 推荐 const { data: { user: { name, age } } } = response;不过要注意,过深的解构会降低可读性。两层以内用解构很优雅,超过三层建议先做中间变量的拆分,保留代码的直观性。
9.5 避免深拷贝陷阱
当你把对象 A 赋值给 B 时,B 和 A 指向的是同一个内存地址。修改 B 的某个属性,A 也会跟着变。这就是“引用赋值”带来的副作用。如果你想要一个独立的副本,需要浅拷贝或深拷贝。
const original = { list: [1, 2, 3], name: "测试" }; // 浅拷贝:对象的第一层属性是独立的,但嵌套对象仍是引用 const shallowCopy = { ...original }; shallowCopy.list.push(4); console.log(original.list); // [1, 2, 3, 4],被影响了 // 深拷贝:完全独立,常用 JSON 方法 const deepCopy = JSON.parse(JSON.stringify(original)); deepCopy.list.push(5); console.log(original.list); // [1, 2, 3]JSON.parse(JSON.stringify(obj))是一种简单粗暴的深拷贝方式,但它有局限:函数、undefined、Symbol、日期对象、循环引用都会被处理掉或丢失。在项目里如果数据包含函数或日期,建议用更专业的深拷贝库,或者手动按字段复制。这个知识点也是前端面试的常见题型。
9.6 保持代码的单一职责原则
一个函数只做一件事。这也是派生自模块化思想的编码原则。handleSubmit就只负责提交逻辑,validateForm就只负责校验表单。不要一个函数里既发请求又改动多个全局状态又渲染表格,那样调试起来会非常痛苦。
如果你发现一个函数超过二三十行,或者你需要在注释里写明“这段是处理 A 的逻辑,那段是处理 B 的逻辑”,基本就可以考虑拆分了。
10. 30 分钟速成路径:一个可以落地的学习计划
最后,把这篇文章的知识结构浓缩成一个可以直接执行的学习计划。你不需要一次性完成,但建议在一周内连续做三天,每天 10 分钟也可以。关键在于持续接触,让这些模式逐渐变成你的肌肉记忆。
建议的学习节奏:
| 时间段 | 任务 | 验证方式 |
|---|---|---|
| 前 5 分钟 | 在浏览器控制台练习变量声明和数据类型判断 | 打印typeof结果 |
| 第 6-12 分钟 | 写三种函数定义方式,测试作用域 | 在函数内外分别访问变量,观察报错 |
| 第 13-18 分钟 | 用map、filter、reduce处理一个数组 | 把一组商品价格从数字转为“¥xx元”格式 |
| 第 19-24 分钟 | 用Promise+async/await模拟接口调用 | 延迟两秒后打印数据 |
| 第 25-30 分钟 | 写两个模块文件,用import/export互相引用 | 启动本地服务器,控制台看到模块输出 |
如果某个环节卡住了,优先回看本文对应的章节。更推荐的做法是:把你完成的小练习直接和某个小项目结合起来,比如做一个待办列表、做一个汇率换算工具。写一个真实的小应用,比单纯敲十遍语法更有效。
JavaScript 的核心语法并没有无穷多的知识点需要背诵。真正重要的事情集中在这么几个关键词上:变量与类型、函数与作用域、数据结构操作、异步流程控制、模块化组织。把这几个模型在脑子里跑顺,后面学 DOM 操作、学事件循环、学 Vue/React 框架,都会顺利很多。
把这篇文章收藏起来,方便下次复习。学编程的关键不是一次看懂,而是反复使用,直到这些语法像呼吸一样自然。现在,打开控制台,写下你的第一行代码吧。