我教过不少刚接触代码的人发现大家学 JavaScript 都会经历同一个尴尬阶段跟着教程能写出例子合上教程就不知道下一步该干嘛。原因很简单——基础概念没有变成自己的。这篇文章是「JavaScript 基础」系列的第一讲目标只有一个把变量、类型、函数、作用域、对象、数组和异步这些最核心的地基概念讲透让你知道它们为什么存在、怎么用、以及哪些地方最容易踩坑。它适合完全零基础的朋友也适合学了一半回头补基础的人。你不需要安装任何复杂的工具打开浏览器按下 F12 就能跟着动手。1. 别急着敲代码先用地图理解 JavaScript 的真实面貌1.1 它到底能做什么比你以为的宽得多很多人第一次接触 JavaScript是从网页上的弹窗或者表单校验开始的。但它的能力边界远不止这些在浏览器里它可以操作页面上的元素、响应用户的点击和输入、向服务器请求数据、做动画、处理视频音频几乎所有现代网页交互的背后都有它的影子。出了浏览器Node.js 让 JavaScript 可以运行在服务器上写接口、处理文件、操作数据库都不在话下。再往外延伸小程序、桌面应用、甚至物联网设备上的脚本都有 JavaScript 的身影。换句话说只要你在和前端打交道或者对全栈、跨端开发感兴趣JavaScript 都是一道绕不开的门槛。框架再方便写到最后拼的还是基础。如果你以后要深入 Vue、React 这些框架面试时被反复追问的原生概念也正是这篇文章要讲的东西。1.2 JavaScript、Java 和 ECMAScript别把名字搞混了刚入门时很容易被几个名字绕晕。JavaScript 和 Java 是两门完全不同的语言只是因为诞生时的商业推广需要借了 Java 的名字语法上有相似之处但本质毫无关系。你不需要会 Java 才能学 JavaScript。ECMAScript 则是一门语言规范JavaScript 是对它的实现。平时大家说的 ES6、ES2015指的就是 ECMAScript 的版本。ES6 是 2015 年发布的版本引入了 let、const、箭头函数、类、模块化等一大批特性是现代前端开发的起点。你现在看到的大多数教程和框架代码默认都建立在 ES6 之上。所以当你看到ES6 语法这个词时可以把它理解为更现代、更推荐的 JavaScript 写法。1.3 环境准备一个浏览器加一个编辑器就够了学习基础阶段不需要搭复杂的工程化环境。我建议你这样起步打开 Chrome 或 Edge按 F12笔记本可能是 FnF12打开开发者工具切换到 Console控制台面板。在下面的输入框里直接输入console.log(hello world)回车你会立刻看到输出。想写多行代码时可以用任意一个顺手的文本编辑器新建一个index.html用script标签包裹 JavaScript 代码然后在浏览器里打开这个文件。!DOCTYPE html html langzh head meta charsetUTF-8 / title基础练习/title /head body script console.log(hello world); /script /body /html控制台是学习阶段最好的朋友。你可以把任何一段困惑的代码直接丢进去运行立刻看结果比反复查文档要快得多。这个习惯我从入门一直用到现在。2. 变量与数据类型新手最容易被类型转换坑到的地方2.1 var、let 和 const 三兄弟怎么选变量是数据的容器。ES6 之后声明变量有三种方式。很多人一开始搞不清差别其实从选择策略上非常简单默认用const需要重新赋值时用let至于var尽量不主动使用只在自己读老代码时能看懂就行。声明方式作用域可重复声明变量提升推荐度var函数作用域可以有但值是 undefined不推荐let块级作用域不可以语法上会报暂时性死区变量会变时用const块级作用域不可以同上默认首选const经常让人误会的地方是它不代表值不可变而是代表这个变量的绑定不可变。你可以修改对象内部的属性但不能重新给整个变量赋值。const person { name: 张三 }; person.name 李四; // 没问题改的是对象内部 person { name: 王五 }; // 报错Assignment to constant variablevar的坑则体现在作用域上。比如for (var i 0; i 3; i)里的i循环结束后依然存在可能污染外层变量。这在老代码里是经典的 bug 来源let就是为了解决这类问题诞生的。2.2 七种内置类型与 typeof 的返回值JavaScript 的数据类型主要分为原始类型和引用类型。原始类型包括 number、string、boolean、undefined、null、symbol、bigint引用类型主要是 object数组、函数、日期等都是对象的一种。判断类型最常用的工具是typeof操作符但它的返回结果有几个非常容易踩的坑。typeof 123; // number typeof abc; // string typeof true; // boolean typeof undefined; // undefined typeof null; // object ← 历史遗留bug typeof [1, 2, 3]; // object ← 数组也是对象 typeof function() {}; // function ← 特殊处理typeof null返回object是语言设计之初留下的历史 bug因为 null 的二进制表示全是零被类型判断误判成了对象。这个坑你记住就好。判断数组不能用typeof要用Array.isArray()判断一个对象是否为空可以拿Object.keys(obj).length看属性数量。2.3 强制转换和让人崩溃的隐式转换类型转换分为两种你用代码主动转换的叫显式转换运算时 JavaScript 自己偷偷帮你转的叫隐式转换。显式转换通常很安全Number(12); // 12 Number(12px); // NaN解析失败 String(123); // 123 Boolean(0); // false Boolean(0); // true非空字符串都算true隐式转换则是写基础代码时最容易栽跟头的地方。最经典的例子是加法运算1 1; // 11加号里遇到字符串就变成拼接 1 - 1; // 0减号会把字符串转成数字 5 * 2; // 10和的区别也在这里体现。0 false的结果是true因为会先做类型转换再比较0 false的结果是false因为要求类型和值都完全一致。我的建议非常简单粗暴永远使用和!把类型转换的隐性逻辑挡在门外。3. 函数与作用域理解 JavaScript 的执行逻辑3.1 声明函数与函数表达式看似一样实际上有差别函数是 JavaScript 的一等公民意思是它可以像普通值一样被赋值、被传递、被返回。写函数有几种常见方式// 函数声明 function add(a, b) { return a b; } // 函数表达式 const add2 function(a, b) { return a b; }; // 箭头函数 const add3 (a, b) a b;函数声明和函数表达式有一个关键差别提升。函数声明会被整体提升到作用域顶部所以你在声明之前调用它也不会报错函数表达式则像变量一样在赋值之前调用会得到undefined或者报错。箭头函数是 ES6 的语法糖写法简洁但它没有自己的arguments对象也没有自己的this这两个问题下面会说到。3.2 作用域与作用域链变量都去哪儿找作用域就是变量有效的范围。ES6 之后JavaScript 有全局作用域、函数作用域和块级作用域三种。var只有函数作用域而let和const是块级作用域一对大括号就是一个块。let x 1; function outer() { let x 2; function inner() { console.log(x); } inner(); } outer(); // 输出2当内层函数访问一个变量时会先从自己的作用域找找不到就往外一层找直到全局作用域这条链路就是作用域链。执行到inner时它自己的作用域里没有x于是往outer的作用域找到了x 2。理解这点之后很多为什么这里能访问那个变量的问题就迎刃而解了。3.3 闭包一个被概念吓住、其实很好懂的机制闭包的本质是一个函数能够记住并访问它定义时所在的作用域即使这个函数在定义的作用域之外被调用。口说无凭看个经典例子function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2createCounter已经执行完了但返回的那个函数仍然可以通过作用域链访问到count变量于是这个变量被记住了。这就是闭包最常见的用途之一封装私有变量。外部没有办法直接修改count只能通过counter这个函数来操作它。闭包好用但要注意被闭包引用的变量会常驻内存不会被垃圾回收。所以如果一个闭包长期存在且没有释放可能造成内存占用。正常业务里不用过度紧张但大量循环里创建闭包时要留个心眼。3.4 this 指向调用方式说了算this是 JavaScript 里劝退新手最多的话题之一其实核心规则只有一条this的值取决于函数被谁调用。function showThis() { console.log(this); } showThis(); // 浏览器里是 window严格模式下是 undefined const obj { name: 对象, showThis: function() { console.log(this.name); } }; obj.showThis(); // 对象因为是 obj 调用的 setTimeout(function() { console.log(this); // window普通函数在定时器回调里丢失了调用者 }, 1000);箭头函数有点特殊它没有自己的this它会从定义它的外层作用域继承this。这个特性在异步回调里非常有用可以避免this丢失的问题。如果你在 React 或 Vue 里写过一个定时器内的this.setState却发现报错大概率就是普通函数和箭头函数this差异导致的。4. 数组与对象日常开发中使用频率最高的数据结构4.1 对象点号和方括号两种访问方式对象是键值对的集合几乎所有复杂的业务数据都可以用它表达。创建一个对象很简单const person { name: 张三, age: 30, full name: 张三丰, }; person.name; // 张三 person[name]; // 张三 person[full name]; // 张三丰属性名有空格必须用方括号点号写法更简洁但属性名是变量时必须要用方括号。比如const key name; person[key]才能取到值。删除属性用delete person.age遍历对象可以用Object.keys、Object.values、Object.entries三个方法它们分别返回属性名数组、属性值数组、键值对数组。新手最容易忽略的一点是对象是引用类型。赋值时复制的是引用不是内容。const a { x: 1 }; const b a; // b 和 a 指向同一个对象 b.x 2; console.log(a.x); // 2a 也被改了想真正复制一个对象可以用展开运算符const b { ...a }这只是浅拷贝嵌套对象仍然是共享的深拷贝要另想办法不过基础阶段先把浅拷贝的坑记住就好。4.2 数组的高频方法与原地操作的区别数组大概是日常开发里被用最多的数据结构。它的方法分成两类会修改原数组的和不会修改原数组的。会修改原数组的常用方法push尾部加、pop尾部删、shift头部删、unshift头部加、splice任意位置增删、sort排序、reverse反转。不会修改原数组、而是返回新数组的常用方法map逐一映射、filter筛选、slice切片、concat拼接。还有一个很实用的find返回第一个满足条件的元素。const nums [1, 2, 3, 4, 5]; const doubled nums.map(n n * 2); // [2, 4, 6, 8, 10] const even nums.filter(n n % 2 0); // [2, 4] const first nums.find(n n 3); // 4sort有一个反直觉的默认行为它按字符串而不是数字排序。[10, 9, 2, 1].sort()的结果是[1, 10, 2, 9]因为字符串比较是从第一位字符开始的。正确写法是arr.sort((a, b) a - b)。数组去重也简单[...new Set(arr)]一行就搞定。4.3 解构赋值与展开运算符让代码瘦身解构赋值是 ES6 里性价比极高的语法糖它让你从对象或数组中拆包变得非常方便。const person { name: 张三, age: 30, job: 工程师 }; const { name, age } person; // 从对象里取两个属性 console.log(name, age); // 张三 30 const arr [1, 2, 3, 4]; const [a, b, ...rest] arr; // a1, b2, rest[3,4] console.log(rest); // [3, 4] // 给解构设置默认值很常用 const { job 未知 } person; // job 有值就取没有就用默认值展开运算符...则可以把数组或对象的内容打散复制。const arr2 [...arr]复制数组const person2 { ...person, age: 31 }复制对象并覆盖某个属性。这种写法在 React 的不可变数据更新里尤其常见理解了展开运算符读框架代码会轻松很多。5. 异步编程入门从回调到 Promise理解 JS 的非阻塞特性5.1 为什么单线程的语言需要异步JavaScript 是单线程语言也就是说它一次只能做一件事。如果某件事耗时很长比如请求服务器数据、读取文件、等待定时器而你又必须等它完成才能继续页面就会卡住。这就是异步存在的意义把耗时的操作挂起先执行后面的代码等耗时操作完成了再回来处理结果。一个最直观的例子console.log(开始); setTimeout(() { console.log(1秒后执行); }, 1000); console.log(结束);输出顺序是开始、结束、1秒后执行。结束不会等定时器跑完再输出。很多人误以为setTimeout是延迟1秒执行更准确的理解是最短1秒后把回调放进执行队列。如果前面的代码执行时间超过1秒回调还得继续排队。5.2 回调函数与回调地狱异步的原始形态回调函数就是等某件事完成后再执行的函数。上面的setTimeout接受的箭头函数就是一个回调。异步操作最常见的写法就是传一个回调进去操作完成时调用。如果多个异步操作有先后依赖就会写成嵌套结构setTimeout(() { console.log(第一步完成); setTimeout(() { console.log(第二步完成); setTimeout(() { console.log(第三步完成); }, 1000); }, 1000); }, 1000);这就是回调地狱。它带来的问题不只是难看更重要的是错误处理分散、逻辑顺序混乱、后期维护困难。各大框架社区早就意识到这个问题于是 Promise 登场了。5.3 Promise 与 async/await把嵌套拉平Promise 是对异步结果的抽象它有三种状态pending进行中、fulfilled成功、rejected失败。状态只能从 pending 变为 fulfilled 或 rejected一旦变化就不能再改。function getData() { return new Promise((resolve, reject) { setTimeout(() { const success true; if (success) { resolve(请求到的数据); } else { reject(请求失败); } }, 1000); }); } getData() .then(data console.log(data)) .catch(error console.log(error));then接收成功结果catch接收失败原因。依赖关系通过链式的.then表达嵌套变成了平铺。在.then里返回一个 Promise还能继续往下串错误统一由最后的.catch捕获。async/await是 Promise 的语法糖让异步代码长得像同步代码async function run() { try { const data await getData(); console.log(data); } catch (error) { console.log(error); } } run();await会暂停当前函数等 Promise 出结果再继续。注意await只能在async函数里使用async函数始终返回一个 Promise。多个没有依赖关系的异步任务用Promise.all并发执行比逐个await快得多。const [user, posts] await Promise.all([ fetchUser(), fetchPosts() ]);异步是 JavaScript 进阶路上绕不过去的一道坎但把回调、Promise、async/await 这条线理顺之后后面看网络请求、API 封装都会顺畅很多。6. 新手必踩的坑与调试思路6.1 相等判断、分号和引号的细节基础阶段的报错和逻辑问题不少都集中在几个细节点上。第一个是相等判断前面说过一律使用不要用。第二个是分号JavaScript 会自动插入分号但自动插入并不总是符合你的预期比如以[或(开头的行很容易跟前一行产生奇怪的联动。我的建议是要么老老实实写分号要么坚持无分号风格保持一致最重要不要混用。第三个是字符串引号嵌套。单引号字符串里再用单引号会直接语法报错但双引号包单引号、单引号包双引号都没问题。ES6 新增的模板字符串用反引号包裹可以直接写变量和换行const name 张三; const message 你好${name} 今天学习JavaScript了吗;第四个是重复声明。同一个作用域里let或const声明同一个变量会直接报错这是新手经常碰到的 SyntaxError。同名的var则可以重复声明这是 var 的又一个历史包袱。6.2 调试不只是 console.logconsole.log是入门阶段的好朋友但它不是唯一工具甚至有时候会误导你。比如打印一个对象控制台里显示的是对象当前状态而不是打印那一刻的快照你展开它时看到的可能是修改后的值。几个更高效的玩法console.table([{ name: 张三, age: 30 }, { name: 李四, age: 25 }]); // 表格化展示数据多时一目了然 console.time(耗时); // 中间是某段操作 console.timeEnd(耗时); // 输出该段代码的执行耗时真正定位复杂 bug 时我建议用断点调试。在浏览器开发者工具的 Sources 面板里找到你的脚本文件点击行号打一个断点然后刷新页面。代码会执行到断点处暂停此时你可以在右侧查看所有作用域变量的当前值还能单步执行、跳进函数内部。这个体验比一个一个加console.log然后刷新猜原因要高效得多。6.3 读报错信息的正确姿势与排查顺序新手看到红色报错第一反应往往是慌其实报错信息是最高效的排错线索。一个典型的报错长这样Uncaught TypeError: person.name is not a function at anonymous:1:15第一行是错误类型和消息TypeError说明是类型相关的错误person.name is not a function说明你尝试当函数调用一个不是函数的值。第二行是出错位置1:15指文件第1行第15列点击可以跳转到具体代码。常见错误类型我列个快速对应表错误类型常见含义排查方向ReferenceError某个变量未定义检查拼写、是否漏写声明TypeError对不合适的类型做了操作检查对象是否 undefined、属性是否存在SyntaxError语法错误检查引号、括号、分号是否配对RangeError数值超出合法范围检查递归是否缺少出口、数组长度是否异常排查顺序也有讲究先看控制台里最顶部的第一条报错修复它然后再运行再看。很多时候后面一堆报错都是第一条引发的连锁反应第一条解决了后面的就消失了。如果报错信息看不懂把原文复制去搜索引擎旁边加上你使用的技术栈基本都能找到讨论帖。但切忌盲目复制答案要看懂别人解释的原理再动手修。我自己学基础时最有效的做法是把文章里的每一个例子亲手敲一遍然后故意改坏它看它会报什么错。报错不是失败是学习的一部分。这个系列后续会继续讲 DOM 操作和实战小案例如果你在照着敲代码的时候被某个报错卡住了先深吸一口气把报错信息读完再试着拆解它——读完这条报错你就已经比刚才又进一步了。