对于JavaScript:JavaScript基础语法的解析

开篇介绍:

hello 大家,本篇博客我们来学习JavaScript基础语法

其实大家有了C++的基础之后,学习这个完全可以说是简单~

一、变量:编程世界的 "收纳盒",从根源讲透每一个细节

在正式讲语法之前,我们必须先搞懂一个最核心的问题:为什么编程里要有变量?变量到底是个什么东西?

1.1 变量的本质:给你的数据找一个 "带名字的收纳盒"

我们用一个所有人都能懂的生活场景来类比:你每天出门,要带钥匙、手机、耳机、口红、工牌这些东西,你不会直接把这些东西全都攥在手里,更不会随手扔在口袋里乱作一团 ------ 你一定会把它们放进一个带名字的收纳包里。比如:

  • 你有一个叫 "通勤包" 的包,里面放手机、钥匙、工牌;
  • 你有一个叫 "化妆包" 的包,里面放口红、粉底、散粉;
  • 你有一个叫 "数码包" 的包,里面放耳机、充电器、U 盘。

这个 **"带名字的收纳盒 / 包",就是编程里的 "变量"**;而你放进包里的钥匙、手机、口红,就是变量里要存的 "数据"。

这个类比,会贯穿我们整个变量的讲解,因为它能帮你彻底理解变量的所有特性:

  1. 包要有名字:你总不能喊 "那个包",别人不知道你说的是哪个,所以变量必须有 "变量名";
  2. 包能装东西:你可以把东西放进包里,这个动作在编程里叫 "赋值";
  3. 包能换东西:你可以把包里的手机拿出来,放进去一个平板,这个动作叫 "修改变量的值";
  4. 不同的包能装不同的东西:小卡包装钥匙,大背包装电脑,而 JavaScript 里的包是 "万能收纳盒"------ 今天装水,明天装米,后天装水果,想装什么就装什么,这就是我们后面要讲的 "动态类型"。

理解了这个本质,我们再来看变量的每一个语法细节,你就不会再是死记硬背,而是真正懂了 "为什么要这么写"。

1.2 变量的创建:声明 + 初始化,每一步都讲透

在 JavaScript 里,创建一个变量,分为两个核心步骤:声明 和 初始化。很多新手学不好变量,就是因为没把这两个步骤分开搞懂,我们一个一个来。

1.2.1 第一步:变量声明 ------ 告诉 JS 引擎 "我要一个收纳盒"

什么是 "声明"?声明就是你拿着身份证去物业登记:"我要租一个储物柜,编号叫 xxx"------ 也就是告诉 JavaScript 引擎,我要创建一个叫 xx 名字的变量,你给我在内存里留一块位置。

在 JavaScript 里,声明变量必须用关键字,最基础的是 var,后面我们会讲 let。这里我们先把 var 讲透。

**什么是关键字?**关键字就是 JavaScript 这门语言里,天生就带 "特殊身份" 的词。就像我们身份证上的 "姓名""性别""民族",是国家规定的、有固定含义的词,你不能随便用这些词给自己起名;同理,JavaScript 里的关键字,你也不能用来当变量名,因为引擎一看到这个词,就知道它要做什么固定的操作。

var 就是一个关键字,它的固定含义就是:我要声明一个变量。

声明变量的基础语法:

复制代码
var 变量名;

举个例子:

复制代码
var userName;

这句话翻译成人话就是:"JavaScript 引擎你好,我要声明一个变量,名字叫 userName,你给我在内存里留一个收纳盒的位置,盒子的名字就叫 userName!"

这一步做完,你就有了一个空的收纳盒,盒子有名字了,但里面还什么都没放。

1.2.2 第二步:变量初始化 ------ 给你的收纳盒放进东西

什么是 "初始化"?初始化就是你给已经声明好的变量(空收纳盒),第一次放进东西 ------ 也就是第一次给变量赋值。

这里我们要讲一个核心中的核心,90% 的新手入门都会在这里踩坑:JavaScript 里的 = 号,不是我们数学里的 "等于",而是一个 "赋值动作"。

我们数学里写 1 + 1 = 2,这里的 = 是 "判断两边的东西完全相等",是一个结果;但在 JavaScript 里,= 号是一个动作指令 ,它的含义是:把 = 号右边的东西,放进 = 号左边的收纳盒里。

这个概念,我用十遍、百遍的文字重复,就是要让你刻在脑子里:

= 号是 "从右往左放东西",不是 "两边相等"!= 号是 "从右往左放东西",不是 "两边相等"!= 号是 "从右往左放东西",不是 "两边相等"!

初始化的基础语法:

复制代码
变量名 = 要存的数据;

举个例子:

复制代码
var userName; // 第一步:声明变量,拿到一个叫userName的空收纳盒
userName = '张三'; // 第二步:初始化,把字符串'张三',放进userName这个收纳盒里

当然,我们 99% 的场景里,都会把声明和初始化写在同一行,这是更简洁、更推荐的写法:

复制代码
var userName = '张三'; // 声明+初始化一步完成:拿盒子+放东西,一次性做完

这句话翻译成人话就是:"JS 引擎,给我一个叫 userName 的收纳盒,同时把 ' 张三 ' 这个东西放进去!"

1.3 变量语法的细节规则:每一条都讲清 "为什么"

很多新手写代码,天天因为一些小细节报错,就是因为没把这些基础规则搞懂。我们把每一条规则都掰开,讲清 "是什么、为什么、不遵守会怎么样"。

1.3.1 必须用英文标点,绝对不能用中文标点

这是新手最最最容易犯的错误,没有之一。我们先把这条规则焊死在脑子里:

JavaScript 里所有的标点符号,必须是英文半角标点,绝对不能用中文全角标点!

**为什么?**因为 JavaScript 引擎是外国人开发的,它只认识英文的标点符号。就像你给一个只会说中文的人看一段全英文的话,他完全看不懂;同理,JS 引擎看到中文标点,根本不知道这是什么东西,只会直接报错。

在我们的眼里,中文分号 ; 和英文分号 ; 长得几乎一模一样,中文引号 "" 和英文引号 "" 也没差多少,但在 JS 引擎眼里,它们是完全不同的两个东西,就像汉字里的 "人" 和 "入",看着像,但意思完全不一样。

我们把所有新手常踩的中文标点坑,全部列出来,一个个讲:

  1. 分号 :语句结尾的分号,必须是英文 ;,不能是中文 ;
    • 错误写法:var name = '张三';(中文分号,直接报错)
    • 正确写法:var name = '张三';(英文分号,正常执行)
  2. 引号 :字符串的引号,必须是英文单引号 '' 或英文双引号 "",不能是中文 '' 或 ""
    • 错误写法:var name = "张三";(中文双引号,报错)
    • 错误写法:var name = '张三';(中文单引号,报错)
    • 正确写法:var name = '张三';(英文单引号,正常)
    • 正确写法:var name = "张三";(英文双引号,正常)
  3. 括号 :后面我们要学的函数、if 语句、循环里的括号,必须是英文 () {} [],不能是中文 () {} 【】
    • 错误写法:alert('你好');(中文括号,报错)
    • 正确写法:alert('你好');(英文括号,正常)
  4. 逗号 :多个变量声明、参数里的逗号,必须是英文 ,,不能是中文 ,
    • 错误写法:var name = '张三',age = 20;(中文逗号,报错)
    • 正确写法:var name = '张三', age = 20;(英文逗号,正常)

怎么避免踩这个坑? 写代码的时候,一定要把你的输入法切换到英文半角模式,尤其是输入标点的时候,一定要看一眼输入法状态。如果你的代码报了莫名其妙的语法错误,第一时间就去检查:是不是哪里用了中文标点!

1.3.2 语句结尾的分号:可以省略,但强烈建议你加上

很多新手会问:"我看别人的代码里,有的句子结尾没写分号,也能运行,那我到底要不要写?"

这里我给你一个绝对不会出错的结论:无论 JS 引擎支不支持省略分号,你都必须在每一个语句的结尾,加上英文分号;。

我们先讲清楚背后的原理,你就知道为什么要这么做了。

JavaScript 引擎有一个机制,叫做 ASI(Automatic Semicolon Insertion,自动分号插入)。简单说就是:如果你没写分号,JS 引擎会在解析代码的时候,自动给你 "猜" 哪里应该加分号,然后给你补上,让代码能正常运行。

但问题就出在 "猜" 这个字上 ------JS 引擎不是万能的,它很多时候会猜错,给你补到错误的位置,导致你的代码执行结果完全不对,甚至直接报错。

举一个最经典的、省略分号导致报错的例子:

复制代码
// 你写的代码,省略了分号
var a = 10
var b = 20
(function(){
  console.log(a + b)
})()

你以为 JS 引擎会给你补成分号,变成这样:

复制代码
var a = 10;
var b = 20;
(function(){
  console.log(a + b);
})();

但实际上,JS 引擎会把它解析成这样:

复制代码
var a = 10;
var b = 20(function(){
  console.log(a + b);
})();

它会把第二行的 20 和第三行的 ( 连在一起,认为你是想调用一个叫 20 的函数!但 20 根本不是函数,所以代码会直接报错:Uncaught TypeError: 20 is not a function。

这种错误,对于新手来说,根本找不到问题出在哪里 ------ 你明明看着代码没写错,却一直报错,就是因为省略分号导致的。

而你只要在每一个语句结尾都加上分号,就完全不会有这个问题,JS 引擎 100% 不会猜错你的意图。所以,哪怕省略分号能少打一个字符,我也强烈建议你:每一个语句结尾,都必须加上英文分号。

1.3.3 变量名的命名规则:不是什么名字都能起

变量名就像你的收纳盒的标签,不是什么字都能写的,必须遵守 JavaScript 的命名规则,否则会直接报错。我们把规则一条一条讲透,不仅告诉你 "不能这么做",还告诉你 "为什么不能这么做"。

规则 1:变量名只能包含 英文字母 (a-z,A-Z)、数字 (0-9)、下划线 (_)、美元符号 ($)

这是最基础的规则,变量名里只能出现这四类字符,其他任何字符都不行。

**为什么有这个规则?**因为 JS 引擎解析代码的时候,只有这四类字符,会被识别成 "变量名的一部分",其他字符都会被当成 "运算符、标点、分隔符",导致引擎解析错误。

合法的变量名:

复制代码
var age; // 纯字母
var user_name; // 字母+下划线
var $price; // 美元符号+字母
var student1; // 字母+数字
var myName$; // 字母+美元符号

非法的变量名(会直接报错):

复制代码
var user-name; // 错误!包含减号-,引擎会以为你要做减法运算
var my name; // 错误!包含空格,引擎会以为my和name是两个东西
var user@name; // 错误!包含@符号,引擎不识别
var 姓名; // 错误!虽然中文变量名在部分浏览器里能运行,但强烈不推荐,会有各种兼容问题
规则 2:变量名不能以数字开头

变量名的第一个字符,只能是字母、下划线、美元符号,绝对不能是数字。

为什么有这个规则? 因为 JS 引擎看到一个以数字开头的词,会第一时间认为你要写一个 "数字",而不是变量名。比如你写 var 1age;,引擎先看到 1,以为是数字,后面又看到 age,直接就懵了,不知道这是什么东西,只能给你报语法错误。

非法的变量名:

复制代码
var 1age; // 错误!以数字开头
var 2student; // 错误!以数字开头

合法的写法:

复制代码
var age1; // 正确!数字放在后面
var student2; // 正确!数字放在后面
规则 3:变量名严格区分大小写

JavaScript 是一门严格区分大小写 的语言,大写字母和小写字母,在引擎眼里是完全不同的字符。也就是说,Name、name、NAME、nAme,是四个完全不同的变量,互相之间没有任何关系。

举个例子:

复制代码
var name = '张三';
var Name = '李四';
var NAME = '王五';

console.log(name); // 输出:张三
console.log(Name); // 输出:李四
console.log(NAME); // 输出:王五

很多新手会在这里踩坑:声明的时候用的是小写 userName,用的时候写成了大写 Username,结果代码一直报 "变量未定义" 的错误,找了半天都找不到问题,就是因为没注意大小写。

规则 4:绝对不能用 JavaScript 的关键字和保留字当变量名

我们前面讲过,关键字是 JS 里天生带固定含义的词,比如 var、if、else、for、function 等等;保留字是 JS 现在没用到,但未来版本可能会当成关键字的词,比如 class、import、export 等等。

你不能用这些词当变量名,因为 JS 引擎一看到这些词,就会执行它固定的操作,而不会把它当成变量名。

举个错误的例子:

复制代码
var var = 10; // 错误!var是关键字,不能当变量名
var if = '你好'; // 错误!if是关键字,不能当变量名
var for = 20; // 错误!for是关键字,不能当变量名

常见的关键字和保留字 (不用死记硬背,写多了自然就记住了):var、let、const、function、if、else、for、while、do、switch、case、break、continue、return、true、false、null、undefined、class、import、export、default、try、catch、finally 等等。

1.3.4 变量名的命名规范:让你的代码人人都能看懂

上面的 "规则" 是必须遵守的,不遵守就会报错;而这里的 "规范" 是行业里的通用约定,不遵守不会报错,但会让你的代码变得非常难读,别人看了会骂街,你自己过一个月再看,也会忘了这个变量是干嘛的。

核心规范 1:变量名必须见名知意,绝对不要用 a、b、c 这种无意义的名字变量名的核心作用,就是让你看到名字,就知道这个变量里存的是什么数据。

反面例子(千万不要这么写):

复制代码
var a = '张三'; // 谁知道a是啥?名字?年龄?地址?
var b = 20; // 谁知道b是啥?
var c = 90; // 谁知道c是啥?

正面例子(推荐这么写):

复制代码
var userName = '张三'; // 一看就知道是用户名
var userAge = 20; // 一看就知道是用户年龄
var userScore = 90; // 一看就知道是用户分数

你可能觉得 "写短一点省事",但等你的代码超过 100 行,你就会发现:你自己都忘了 a、b、c 是干嘛的,还要翻到前面去看声明,反而更浪费时间。而见名知意的名字,你看一眼就懂,永远不会忘。

核心规范 2:使用小驼峰命名法小驼峰命名法是 JavaScript 行业里通用的变量命名规范,规则是:

  • 变量名由多个单词组成的时候,第一个单词的首字母小写,后面每个单词的首字母都大写。

例子:

复制代码
// 单个单词,全小写
var name = '张三';
var age = 20;

// 多个单词,小驼峰
var userName = '张三'; // user + Name
var userAge = 20; // user + Age
var userLoginTime = '2024-01-01'; // user + Login + Time
var productPrice = 199; // product + Price

这种写法的好处是:多个单词连在一起,每个单词的首字母大写,一眼就能看清是哪几个单词,可读性非常高。

1.4 变量的使用:先声明,后使用

变量就像动漫里的角色放技能 ------ 必须先喊出技能名字(声明变量),才能释放技能(使用变量)。如果你没声明变量,直接就用,JS 引擎会直接给你报一个致命错误:ReferenceError: 变量名 is not defined(引用错误:这个变量没定义)。

错误例子(直接使用未声明的变量):

复制代码
console.log(userName); // 报错!ReferenceError: userName is not defined
var userName = '张三';

这里要提一个 JavaScript 的特殊特性:变量提升 。简单说就是,用 var 声明的变量,JS 引擎会把它的 "声明" 部分,自动提升到代码的最顶部,但是 "赋值" 部分不会提升。

比如上面的代码,JS 引擎实际解析的时候,会变成这样:

复制代码
var userName; // 声明被提升到顶部
console.log(userName); // 输出 undefined,不会报错
userName = '张三'; // 赋值留在原地

虽然变量提升不会让代码报错,但会让代码的执行逻辑变得非常奇怪,你明明是先打印再声明,结果却输出了 undefined,很容易出 bug。所以,哪怕 JS 有变量提升的特性,我也强烈要求你:所有变量,必须先声明、再使用,绝对不要先使用、后声明。这是最规范、最不容易出错的写法。

1.5 变量的修改:给你的收纳盒换个东西

变量之所以叫 "变" 量,就是因为它的值是可以改变的。你可以随时给已经声明并初始化的变量,赋一个新的值,新的值会覆盖掉原来的值。

修改的语法:

复制代码
变量名 = 新的值;

举个例子:

复制代码
// 声明+初始化:给userName收纳盒放进'张三'
var userName = '张三';
console.log(userName); // 输出:张三

// 修改变量:把原来的'张三'拿出来,放进'李四'
userName = '李四';
console.log(userName); // 输出:李四

// 甚至可以修改成完全不同类型的值(动态类型特性)
userName = 20;
console.log(userName); // 输出:20

这里要注意:修改变量的时候,不需要再写 var 关键字了!var 只用来 "第一次声明变量",修改的时候直接写变量名 + 赋值就行。如果你重复用 var 声明同一个变量,虽然不会报错,但这是非常不好的写法,会让代码变得混乱,绝对不要这么做。

错误写法(重复声明):

复制代码
var userName = '张三';
var userName = '李四'; // 错误!重复用var声明同一个变量,虽然不报错,但非常不推荐

正确写法(修改):

复制代码
var userName = '张三';
userName = '李四'; // 正确!直接赋值修改,不用var

1.6 let 关键字:更现代的变量声明方式

除了 var,JavaScript 还支持用 let 关键字声明变量,它的基础用法和 var 完全一模一样:声明、初始化、修改、命名规则,都和 var 没有任何区别。

基础用法示例:

复制代码
// 声明+初始化
let userName = '张三';
console.log(userName); // 输出:张三

// 修改变量
userName = '李四';
console.log(userName); // 输出:李四

// 先声明,后初始化
let age;
age = 20;
console.log(age); // 输出:20

let 和 var 的核心区别,是作用域的不同(我们后面讲作用域的时候会详细拆解),这里你只要记住一个结论:现在写 JavaScript 代码,优先用 let 声明变量,不要用 var。

因为 let 修复了 var 的很多设计缺陷,比如变量提升、重复声明、块级作用域的问题,让代码更规范、更不容易出 bug。我们后面讲作用域的时候,会把两者的区别拆得明明白白,现在你只要会用 let 声明变量就行。

二、动态类型:JavaScript 变量的 "百变特性",彻底讲透

JavaScript 是一门动态类型语言,这是它和 C、C++、Java 这些静态类型语言最核心的区别之一。很多新手学不好 JS,就是因为没把动态类型的特性搞懂,我们用最通俗的话,把它讲得透透彻彻。

2.1 动态类型的两个核心特性

我们先把动态类型的两个核心特性讲清楚,再用例子和类比,让你彻底理解。

特性 1:变量的类型,是在程序运行过程中才确定的

在静态类型语言(比如 C++)里,你声明变量的时候,必须先告诉编译器 "这个变量是什么类型",比如 int a = 10;,你必须先写 int,告诉编译器这个变量是整数类型,编译的时候就确定了类型,运行的时候不能改。

但在 JavaScript 里,你声明变量的时候,完全不用告诉引擎 "这个变量是什么类型",引擎会在运行到赋值语句的时候,根据你赋的值,自动确定这个变量的类型。

举个例子:

复制代码
// 只声明,不赋值,引擎根本不知道这个变量是什么类型
var a;
console.log(typeof a); // 输出:undefined(未定义类型)

// 运行到这行赋值语句,引擎才确定:a是number数字类型
a = 10;
console.log(typeof a); // 输出:number

// 运行到这行赋值语句,引擎又确定:a现在是string字符串类型
a = '张三';
console.log(typeof a); // 输出:string

就像我们的收纳盒类比:静态类型语言的收纳盒,你买的时候就定死了 "这个盒子只能装水",不能装别的;而 JavaScript 的收纳盒,是万能盒子,你放进去什么东西,它就是什么类型的盒子,放数字就是数字类型,放字符串就是字符串类型。

特性 2:随着程序运行,变量的类型可以随时改变

这是动态类型最核心的特点:同一个变量,你可以先给它赋一个数字,再给它赋一个字符串,再赋一个布尔值,它的类型会跟着你赋的值,随时发生改变。

举个例子:

复制代码
// 声明变量,赋值为数字,类型是number
var a = 10;
console.log(a); // 输出:10
console.log(typeof a); // 输出:number

// 赋值为字符串,类型变成string
a = 'hello';
console.log(a); // 输出:hello
console.log(typeof a); // 输出:string

// 赋值为布尔值,类型变成boolean
a = true;
console.log(a); // 输出:true
console.log(typeof a); // 输出:boolean

// 赋值为null,类型变成object(历史遗留bug)
a = null;
console.log(a); // 输出:null
console.log(typeof a); // 输出:object

这种操作,在 C++、Java 这些静态类型语言里,是绝对不可能的 ------ 你声明了 int a = 10;,就永远不能给 a 赋一个字符串,编译器会直接报错。但在 JavaScript 里,这是完全合法的操作。

2.2 typeof 运算符:检测变量的当前类型

既然变量的类型可以随时变,那我们怎么知道一个变量当前是什么类型呢?用 typeof 运算符。

typeof 运算符的作用,就是检测一个变量 / 值的类型,返回一个小写的字符串,告诉你这个值的类型是什么。

语法(两种写法都可以,效果完全一样):

复制代码
typeof 变量名;
typeof(变量名);

不同类型的值,typeof 的返回结果,我们列一个全的表,每个都讲清楚:

变量的值 typeof 返回的字符串 说明
10、3.14、Infinity、NaN 'number' 所有数字、特殊数字值,都是 number 类型
' 张三 '、"hello"、'' 'string' 所有字符串,都是 string 类型
true、false 'boolean' 布尔值,都是 boolean 类型
只声明不赋值的变量 'undefined' 未定义的值,是 undefined 类型
null 'object' 这是 JavaScript 的历史遗留 bug!null 本身是 null 类型,但 typeof 会返回 'object',从 JS 诞生以来就一直没修,因为要兼容旧代码

举个完整的例子:

复制代码
// 数字类型
var num = 10;
console.log(typeof num); // 输出:'number'

// 字符串类型
var str = 'hello';
console.log(typeof str); // 输出:'string'

// 布尔类型
var bool = true;
console.log(typeof bool); // 输出:'boolean'

// 未定义类型
var und;
console.log(typeof und); // 输出:'undefined'

// null类型
var nul = null;
console.log(typeof nul); // 输出:'object'(历史bug)

2.3 动态类型的优缺点:灵活的代价

动态类型给 JavaScript 带来了极高的灵活性,写代码的时候不用纠结类型,开发速度非常快,但也带来了非常多的坑,我们客观地讲清楚。

优点:极致的灵活性,开发速度快

动态类型让你不用提前规划变量的类型,想赋什么值就赋什么值,写代码的时候不用被类型束缚,尤其是写小功能、小脚本的时候,速度非常快,不用写一堆类型声明的代码。

缺点:极其容易出 bug,大型项目难维护

灵活的代价,就是极其容易出类型相关的 bug。最经典的例子,就是我们之前提到的:用户输入两个数字,想做加法,结果变成了字符串拼接。

经典坑示例:

复制代码
// 让用户输入两个数字
var num1 = prompt('请输入第一个数字:'); // 用户输入10,num1是字符串'10',不是数字10
var num2 = prompt('请输入第二个数字:'); // 用户输入20,num2是字符串'20',不是数字20

// 你想做加法,结果变成了字符串拼接
var sum = num1 + num2;
alert('和是:' + sum); // 弹出:和是:1020,而不是你想要的30

这个 bug 的根源,就是动态类型:prompt 返回的永远是字符串,你以为是数字,结果做加法的时候变成了拼接。在静态类型语言里,这种错误在编译的时候就会被发现,根本不会让程序运行;但在 JS 里,它不会报错,只会给你一个错误的结果,新手根本找不到问题出在哪里。

给新手的核心建议 :虽然 JS 允许你随便改变变量的类型,但在实际开发中,绝对不要随便改变一个变量的类型。一个变量,一开始赋值为数字,就一直让它存数字;一开始赋值为字符串,就一直让它存字符串。保持变量类型的一致性,能帮你避开 90% 的类型相关 bug。

三、基本数据类型:JavaScript 的 "数据积木",每个类型拆到极致

JavaScript 里的数据,分为两大类:基本数据类型(也叫原始类型) 和 引用数据类型(也叫复杂类型)。我们这篇内容,只讲基础语法里的 6 种基本数据类型,引用类型(对象、数组、函数)我们后面单独拆开来,一个一个讲透。

6 种基本数据类型是:number、string、boolean、undefined、null、Symbol(ES6 新增,入门阶段暂时不用管,我们重点讲前 5 种)。

3.1 number 数字类型:JS 里所有的数字,都是一家人

在 C++、Java 这些语言里,数字会分成很多种类型:int(整数)、long(长整数)、float(单精度浮点数)、double(双精度浮点数)、short(短整数)等等,不同的类型有不同的存储范围和精度。

但在 JavaScript 里,所有的数字,不管是整数、小数、正数、负数,都统一用 number 类型来表示,没有单独的整数类型和小数类型。

这是 JS 数字类型最核心的特点,我们先把这个点刻在脑子里,再讲其他细节。

3.1.1 number 类型的基础写法

number 类型的写法非常多,我们把所有常见的写法,一个一个讲清楚。

1. 整数写法

最基础的整数,直接写数字就行,不用加任何引号:

复制代码
var num1 = 10; // 正整数
var num2 = -20; // 负整数
var num3 = 0; // 零
2. 小数写法(浮点数)

带小数点的数字,直接写就行,JS 会自动识别为小数:

复制代码
var price1 = 3.14; // 正小数
var price2 = -0.5; // 负小数
var price3 = 10.0; // 虽然写了小数点,但会被JS自动转换成整数10

这里要注意:如果小数的整数部分是 0,可以省略 0,直接写 .5,代表 0.5,但非常不推荐这么写,可读性太差,很容易看错:

复制代码
var num = .5; // 合法,但不推荐,等价于0.5
3. 科学计数法写法

如果数字特别大,或者特别小,我们可以用科学计数法来写,格式是:数字e+指数 或者 数字e-指数,代表 "数字乘以 10 的指数次方"。

举个例子:

复制代码
// 特别大的数字
var bigNum = 1.23e5; // 等价于 1.23 * 10^5 = 123000
var maxNum = 1.79e308; // JS能表示的最大数字

// 特别小的数字
var smallNum = 1.23e-4; // 等价于 1.23 * 10^-4 = 0.000123
var minNum = 5e-324; // JS能表示的最小正数字
3.1.2 数字的进制表示:二进制、八进制、十进制、十六进制

我们平时生活里用的是十进制 (满 10 进 1),但计算机底层用的是二进制 (满 2 进 1),为了方便写二进制数字,JS 还支持八进制 和十六进制的写法。

什么是进制?

进制,就是 "满多少进 1" 的计数规则。

  • 十进制:我们平时用的,有 0-9 十个数字,满 10 进 1,10 个 1 变成 1 个 10,10 个 10 变成 1 个 100;
  • 二进制:计算机用的,只有 0 和 1 两个数字,满 2 进 1,2 个 1 变成 1 个 10(二进制的 10,等于十进制的 2);
  • 八进制:有 0-7 八个数字,满 8 进 1,8 个 1 变成 1 个 10(八进制的 10,等于十进制的 8);
  • 十六进制:有 0-9、A-F(a-f)十六个数字,A 代表 10,B 代表 11,...,F 代表 15,满 16 进 1,16 个 1 变成 1 个 10(十六进制的 10,等于十进制的 16)。

为什么要有八进制和十六进制? 因为二进制写起来太长了,比如十进制的 10,二进制要写成1010(4 位),十进制的 100,二进制要写成1100100(7 位),写起来非常麻烦,很容易看错。

而:

  • 1 个八进制数字,正好对应 3 个二进制数字;
  • 1 个十六进制数字,正好对应 4 个二进制数字。

用八进制或十六进制,就能把很长的二进制数字,写得很短,比如二进制的1100100,十六进制写成64,一下子就短了很多,看起来也方便。

JS 里不同进制的写法

我们把每种进制的写法、规则、例子,都讲得明明白白。

1. 十进制:默认写法,不用加任何前缀

我们平时写的数字,默认都是十进制,不用加任何前缀:

复制代码
var num = 10; // 十进制的10
2. 二进制:以 0b 或 0B 开头

二进制数字,必须以 0b 或 0B 开头,后面只能跟 0 和 1,否则会报错:

复制代码
var bin1 = 0b1010; // 二进制的1010,等于十进制的10
var bin2 = 0B111; // 二进制的111,等于十进制的7

console.log(bin1); // 输出:10(浏览器会自动转成十进制显示)
console.log(bin2); // 输出:7
3. 八进制:以 0o 或 0O 开头(推荐),或以 0 开头(旧写法,不推荐)

八进制有两种写法:

  • 推荐写法 :以 0o 或 0O 开头,后面只能跟 0-7 的数字,否则报错。这是 ES6 之后的标准写法,不会和十进制混淆;
  • 旧写法 :以 0 开头,后面跟 0-7 的数字。这种写法非常不推荐,因为很容易和十进制混淆,比如你想写十进制的 012,结果 JS 会当成八进制的 12,等于十进制的 10,很容易出 bug。

例子:

复制代码
// 推荐写法
var oct1 = 0o12; // 八进制的12,等于十进制的10
var oct2 = 0O77; // 八进制的77,等于十进制的63

console.log(oct1); // 输出:10
console.log(oct2); // 输出:63

// 旧写法(不推荐)
var octOld = 012; // 八进制的12,等于十进制的10,很容易误以为是十进制的12
4. 十六进制:以 0x 或 0X 开头

十六进制数字,必须以 0x 或 0X 开头,后面可以跟 0-9、A-F(大小写都可以),A 代表 10,B 代表 11,...,F 代表 15:

复制代码
var hex1 = 0xA; // 十六进制的A,等于十进制的10
var hex2 = 0x1F; // 十六进制的1F,等于十进制的31(1*16 + 15 = 31)
var hex3 = 0XFF; // 十六进制的FF,等于十进制的255(15*16 +15 =255)

console.log(hex1); // 输出:10
console.log(hex2); // 输出:31
console.log(hex3); // 输出:255

十六进制在前端里非常常用,比如颜色值 #FFFFFF 就是十六进制的写法,代表白色。

3.1.3 number 类型的三个特殊值:Infinity、-Infinity、NaN

number 类型里,有三个非常特殊的值,它们虽然是 "数字类型",但和普通的数字不一样,我们一个一个讲透。

1. Infinity:正无穷大

Infinity 表示 "正无穷大",它是一个比任何数字都大的值。

什么时候会得到 Infinity? 当你的数字超过了 JavaScript 能表示的最大范围,就会变成 Infinity。JS 能表示的最大数字,可以用 Number.MAX_VALUE 来获取,它的值大约是 1.7976931348623157e+308,超过这个值,就会变成 Infinity。

举个例子:

复制代码
var maxNum = Number.MAX_VALUE;
console.log(maxNum); // 输出:1.7976931348623157e+308

// 最大数字乘以2,超过了范围,变成Infinity
console.log(maxNum * 2); // 输出:Infinity

// 1除以0,也会得到Infinity(注意:JS里1/0不会报错,会返回Infinity)
console.log(1 / 0); // 输出:Infinity
2. -Infinity:负无穷大

-Infinity 表示 "负无穷大",它是一个比任何数字都小的值。

**什么时候会得到 -Infinity?**当你的数字超过了 JavaScript 能表示的最小负数值,就会变成 -Infinity。

举个例子:

复制代码
var minNum = -Number.MAX_VALUE;
console.log(minNum); // 输出:-1.7976931348623157e+308

// 最小负数乘以2,超过范围,变成-Infinity
console.log(minNum * 2); // 输出:-Infinity

// -1除以0,会得到-Infinity
console.log(-1 / 0); // 输出:-Infinity
重要区分:负无穷大 ≠ 无穷小

很多新手会把这两个概念搞混,我们必须彻底讲清楚:

  • 负无穷大(-Infinity) :是一个比任何数字都小的数,比如 -1.79e308 * 2,它无限趋近于 "负的无限远";
  • 无穷小 :是一个无限趋近于 0 的数,值为 1 / Infinity,结果是 0,它无限趋近于 0。

这两个东西完全不是一回事,绝对不能搞混!

3. NaN:Not a Number(不是一个数字)

NaN 是 number 类型里最特殊的一个值,它的全称是 "Not a Number",翻译过来是 "不是一个数字",但它的类型却是 number(用 typeof 检测会返回 'number'),是不是很矛盾?

我们先讲清楚:**什么时候会得到 NaN?**当你做一个数学运算,但这个运算的结果 "不是一个合法的数字",就会得到 NaN。

所有常见的得到 NaN 的场景:

复制代码
// 1. 字符串减数字、乘数字、除数字(注意:加数字会变成拼接,不会得到NaN)
console.log('hello' - 10); // 输出:NaN
console.log('hello' * 10); // 输出:NaN
console.log('hello' / 10); // 输出:NaN

// 2. 0除以0
console.log(0 / 0); // 输出:NaN

// 3. 负数开平方
console.log(Math.sqrt(-1)); // 输出:NaN(Math.sqrt是开平方的函数)

// 4. Infinity 减 Infinity
console.log(Infinity - Infinity); // 输出:NaN

// 5. 字符串转数字失败
console.log(Number('hello')); // 输出:NaN(把'hello'转成数字,失败了,返回NaN)

NaN 的两个核心特性,必须刻在脑子里:

  1. NaN 和任何值都不相等,包括它自己!

    复制代码
    console.log(NaN == NaN); // 输出:false
    console.log(NaN === NaN); // 输出:false

    这是 JS 里唯一一个自己不等于自己的值,非常特殊。

  2. NaN 和任何值做任何数学运算,结果都是 NaN!

    复制代码
    console.log(NaN + 10); // 输出:NaN
    console.log(NaN - 10); // 输出:NaN
    console.log(NaN * 10); // 输出:NaN
    console.log(NaN / 10); // 输出:NaN
isNaN () 函数:判断一个值是不是 NaN

因为 NaN 不等于自己,所以我们不能用 == 或 === 来判断一个值是不是 NaN,必须用 JS 提供的 isNaN () 函数。

isNaN () 函数的作用 :它会先尝试把你传进去的值,转换成数字,如果转换后的结果是 NaN,就返回 true;如果转换后是合法的数字,就返回 false。

举个例子:

复制代码
// 合法的数字,返回false
console.log(isNaN(10)); // 输出:false
console.log(isNaN(3.14)); // 输出:false
console.log(isNaN(Infinity)); // 输出:false(Infinity是合法的数字类型)

// 能转换成数字的字符串,返回false
console.log(isNaN('123')); // 输出:false('123'能转成数字123)
console.log(isNaN('')); // 输出:false(空字符串能转成数字0)
console.log(isNaN(' ')); // 输出:false(空格字符串能转成数字0)

// 不能转换成数字的,返回true
console.log(isNaN('hello')); // 输出:true('hello'转不成数字)
console.log(isNaN(NaN)); // 输出:true(NaN本身就是非数字)
console.log(isNaN('hello' - 10)); // 输出:true

这里要注意一个坑:isNaN('') 会返回 false,因为空字符串会被转换成数字 0,很多新手会在这里踩坑,一定要注意。

3.1.4 经典坑:0.1 + 0.2 不等于 0.3

这是 JavaScript 里最经典的一个坑,也是所有使用 IEEE 754 浮点数标准的语言都会有的坑:0.1 + 0.2 的结果,不是 0.3,而是 0.30000000000000004。

我们先验证一下:

复制代码
console.log(0.1 + 0.2); // 输出:0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // 输出:false

为什么会这样? 我们用最通俗的话讲清楚背后的原理:计算机底层,所有的数字都是用二进制存储的。我们的十进制小数,比如 0.1,转换成二进制的时候,会变成一个无限循环的二进制小数,就像我们十进制里,没法精确表示 1/3,只能写成 0.333333... 无限循环一样。

而 JavaScript 的 number 类型,是用 64 位双精度浮点数存储的,它的存储空间是有限的,只能存下无限循环小数的前几十位,后面的就被截断了,所以存储的 0.1 和 0.2,本身就不是精确的 0.1 和 0.2,而是一个近似值。两个近似值加起来,自然就不是精确的 0.3 了。

怎么解决这个问题? 最常用的解决方法,就是把小数转换成整数来运算,运算完之后再转回小数。比如 0.1 + 0.2,我们可以先把两个数都乘以 10,变成 1 + 2 = 3,再除以 10,得到 0.3,就不会有精度问题了。

代码示例:

复制代码
var num1 = 0.1;
var num2 = 0.2;

// 先乘以10转成整数,相加,再除以10
var sum = (num1 * 10 + num2 * 10) / 10;
console.log(sum); // 输出:0.3
console.log(sum === 0.3); // 输出:true

如果是更多位的小数,比如 0.01 + 0.02,就乘以 100;0.001 + 0.002,就乘以 1000,以此类推。

另外,也可以用 toFixed() 方法,保留指定的小数位数,这个方法我们后面讲字符串的时候会详细讲。

3.2 string 字符串类型:JS 里的文本,每个细节都讲透

字符串,就是 JavaScript 里用来表示 "文本" 的类型,比如名字、地址、一句话、一段文字,都是用字符串类型来存储的。

我们把字符串的所有知识点,从基础写法到高级特性,一个一个拆透。

3.2.1 字符串的基础写法:单引号、双引号都可以

在 JavaScript 里,字符串必须用英文单引号 '' 或者 英文双引号 "" 包裹起来,两者没有任何区别,你喜欢用哪个就用哪个,行业里一般推荐用单引号。

基础示例:

复制代码
// 单引号写法(推荐)
var str1 = 'hello world';
var str2 = '张三';

// 双引号写法
var str3 = "hello world";
var str4 = "李四";

// 空字符串:引号里什么都不写
var emptyStr = '';

新手最容易犯的错误:

  1. 字符串不加引号:var str = hello;,引擎会以为 hello 是一个变量名,报 "未定义" 的错误;
  2. 用中文引号:var str = "hello";,直接报语法错误;
  3. 引号不配对:var str = 'hello;,只写了开头的引号,没写结尾的引号,直接报语法错误。
3.2.2 字符串里包含引号:两种解决方法

很多新手会遇到一个问题:我想在字符串里写引号,怎么办?比如我想表示 我的名字是"张三",如果直接写 var str = "我的名字是"张三"";,引擎会以为前两个引号是一对,后面的 "张三"" 是多余的,直接报错。

这里有两种完美的解决方法,我们一个一个讲透。

方法 1:单双引号搭配使用(最简单、最推荐)

如果你的字符串里要写双引号,那外层就用单引号包裹;如果字符串里要写单引号,外层就用双引号包裹。这样完全不用转义,代码最简洁,可读性最高。

举个例子:

复制代码
// 字符串里有双引号,外层用单引号
var str1 = '我的名字是"张三"';
console.log(str1); // 输出:我的名字是"张三"

// 字符串里有单引号,外层用双引号
var str2 = "我是'张三',今年20岁";
console.log(str2); // 输出:我是'张三',今年20岁
方法 2:用转义字符 \ 转义引号

如果你的字符串里既有单引号,又有双引号,或者你不想换外层的引号,就可以用转义字符 \(反斜杠),在你要写的引号前面加一个 \,告诉 JS 引擎:"这个引号不是字符串的结束,是字符串里的内容,不要解析它"。

举个例子:

复制代码
// 用\"转义双引号
var str1 = "我的名字是\"张三\"";
console.log(str1); // 输出:我的名字是"张三"

// 用\'转义单引号
var str2 = '我是\'张三\',今年20岁';
console.log(str2); // 输出:我是'张三',今年20岁

// 既有单引号又有双引号,用转义字符
var str3 = '他说:"我叫\'张三\'"';
console.log(str3); // 输出:他说:"我叫'张三'"
3.2.3 常用的转义字符

转义字符 \ 除了能转义引号,还能用来表示一些 "不方便直接输入的特殊字符",我们把最常用的转义字符,一个一个讲清楚,每个都给例子。

转义字符 含义 示例和效果
\n 换行符 '第一行\n第二行',输出的时候会分成两行显示
\t 制表符(相当于按一下 Tab 键) '姓名\t年龄\t分数',输出的时候中间会有一个大空格,用来对齐内容
\\ 反斜杠本身 因为 \ 本身是转义字符,所以你要表示一个反斜杠,必须写两个 \\
\' 单引号 转义单引号,让它变成字符串的内容
\" 双引号 转义双引号,让它变成字符串的内容

每个转义字符的详细示例:

  1. \n 换行符

    复制代码
    var str = '第一行\n第二行\n第三行';
    console.log(str);
    // 输出:
    // 第一行
    // 第二行
    // 第三行

    这个换行符,在 alert() 弹框里,和 console.log() 控制台里,都能生效,用来换行显示内容,非常常用。

  2. \t 制表符

    复制代码
    var str = '姓名\t年龄\t分数\n张三\t20\t90\n李四\t21\t85';
    console.log(str);
    // 输出:
    // 姓名    年龄    分数
    // 张三    20      90
    // 李四    21      85

    制表符可以让内容按列对齐,看起来非常整齐,在控制台打印表格的时候非常好用。

  3. \\ 反斜杠本身

    复制代码
    // 表示Windows文件路径:C:\Users\Desktop\test.txt
    var path = 'C:\\Users\\Desktop\\test.txt';
    console.log(path); // 输出:C:\Users\Desktop\test.txt

    如果你只写一个 \,引擎会以为你要转义后面的字符,所以必须写两个 \\,才能表示一个反斜杠本身。

3.2.4 字符串的 length 属性:获取字符串的长度

我们经常需要知道一个字符串里有多少个字符,这时候就可以用字符串的 length 属性。

语法:

复制代码
字符串变量名.length;

length 属性会返回一个数字,代表这个字符串里有多少个字符。注意:是字符的数量,不是字节的数量,一个中文汉字、一个英文字母、一个数字、一个空格,都算一个字符。

举个例子:

复制代码
// 英文字符串
var str1 = 'hello';
console.log(str1.length); // 输出:5(h、e、l、l、o,5个字符)

// 中文字符串
var str2 = '你好世界';
console.log(str2.length); // 输出:4(每个中文算一个字符)

// 带空格的字符串
var str3 = 'hello world';
console.log(str3.length); // 输出:11(中间的空格也算一个字符)

// 空字符串
var str4 = '';
console.log(str4.length); // 输出:0(空字符串长度是0)

// 全是空格的字符串
var str5 = '   ';
console.log(str5.length); // 输出:3(3个空格,每个算一个字符)

这里要注意一个新手容易踩的坑:空字符串 '' 和全是空格的字符串 ' ' 是完全不一样的。空字符串的 length 是 0,而全是空格的字符串,length 是空格的数量,很多新手会以为空格不算字符,其实是算的。

3.2.5 字符串拼接:用 + 号,彻底讲透

字符串拼接,就是把两个或多个字符串,首尾接在一起,变成一个新的字符串。在 JavaScript 里,字符串拼接用 + 号,这是最基础、最常用的字符串操作。

1. 纯字符串之间的拼接

如果 + 号的两边都是字符串,就会直接把两个字符串首尾接在一起,生成一个新的字符串。

举个例子:

复制代码
var str1 = '我的名字是';
var str2 = '张三';
var result = str1 + str2;
console.log(result); // 输出:我的名字是张三

// 多个字符串拼接
var str3 = '我今年';
var str4 = '20';
var str5 = '岁';
var result2 = str3 + str4 + str5;
console.log(result2); // 输出:我今年20岁
2. 字符串和其他类型的拼接:隐式类型转换

这是 + 号最核心的特性,也是新手最容易踩坑的地方:如果 + 号的两边,有一边是字符串,另一边是其他类型的值(数字、布尔值、null、undefined),JS 会自动把另一边的值,转换成字符串,然后再做拼接。

简单说就是:只要 + 号有一边是字符串,就一定会做字符串拼接,而不是数学加法。

我们用大量的例子,把所有情况都讲透:

复制代码
// 1. 字符串 + 数字:数字转成字符串,再拼接
console.log('100' + 100); // 输出:'100100'(不是200!是字符串拼接)
console.log(100 + '100'); // 输出:'100100'(不管字符串在左边还是右边,都是拼接)
console.log('年龄:' + 20); // 输出:'年龄:20'

// 2. 字符串 + 布尔值:布尔值转成字符串'true'或'false',再拼接
console.log('是否成年:' + true); // 输出:'是否成年:true'
console.log('是否学生:' + false); // 输出:'是否学生:false'

// 3. 字符串 + null/undefined:转成字符串'null'/'undefined',再拼接
console.log('值:' + null); // 输出:'值:null'
console.log('值:' + undefined); // 输出:'值:undefined'
3. 多个 + 号的运算顺序:从左到右

如果有多个 + 号连在一起,JS 会从左到右依次运算,前面的运算结果,会和后面的继续运算。

这是新手最容易踩坑的地方,我们用例子讲透:

复制代码
// 例子1:先算数字加法,再算字符串拼接
console.log(100 + 100 + '100'); 
// 运算顺序:
// 第一步:100 + 100 = 200(两边都是数字,做加法)
// 第二步:200 + '100' = '200100'(一边是数字,一边是字符串,做拼接)
// 最终输出:'200100'

// 例子2:先算字符串拼接,后面的全是拼接
console.log('100' + 100 + 100);
// 运算顺序:
// 第一步:'100' + 100 = '100100'(字符串拼接)
// 第二步:'100100' + 100 = '100100100'(还是字符串拼接)
// 最终输出:'100100100'

// 例子3:用括号改变运算顺序
console.log('和是:' + (100 + 100));
// 运算顺序:
// 第一步:先算括号里的 100 + 100 = 200
// 第二步:'和是:' + 200 = '和是:200'
// 最终输出:'和是:200'

这个例子,完美解决了我们之前提到的 "用户输入两个数字,加法变成拼接" 的坑。如果你想让两个数字先做加法,再和字符串拼接,就必须用括号把加法括起来,让它先运算。

解决用户输入数字相加的坑:

复制代码
var num1 = prompt('请输入第一个数字:');
var num2 = prompt('请输入第二个数字:');

// 错误写法:先拼接,再转数字
var sumWrong = num1 + num2;
alert('错误的结果:' + sumWrong); // 输出1020

// 正确写法1:用Number转成数字,再加法
var sumRight1 = Number(num1) + Number(num2);
alert('正确的结果1:' + sumRight1); // 输出30

// 正确写法2:用括号,先转数字再加法,再拼接
alert('正确的结果2:' + (Number(num1) + Number(num2))); // 输出30

3.3 boolean 布尔类型:只有两个值的 "开关类型"

布尔类型是 JavaScript 里最简单的类型,它只有两个值:true(真)和 false(假),没有其他任何值。

3.3.1 布尔类型的本质:"是" 与 "否" 的判断

布尔类型的核心作用,就是用来表示 "逻辑判断的结果"------ 一件事,要么是 "真的、成立的、对的",要么是 "假的、不成立的、错的",没有中间状态。

我们用生活里的例子理解:

  • "你是不是成年人?" 答案只有 "是(true)" 或 "否(false)";
  • "10 是不是大于 5?" 答案只有 "是(true)" 或 "否(false)";
  • "你有没有登录?" 答案只有 "是(true)" 或 "否(false)"。

所有的条件判断,最终都会得到一个布尔值,布尔类型就是 JS 里所有逻辑判断的基础,没有布尔类型,代码就没法做选择、没法做循环。

3.3.2 布尔类型的基础写法

布尔值只有 true 和 false 两个,直接写就行,不用加任何引号,而且必须全小写,不能写成 True 或 FALSE,否则会被当成变量名,报未定义的错误。

正确写法:

复制代码
var isAdult = true; // 是成年人
var isStudent = false; // 不是学生

console.log(isAdult); // 输出:true
console.log(isStudent); // 输出:false
console.log(typeof isAdult); // 输出:'boolean'

错误写法:

复制代码
var bool1 = True; // 错误!首字母大写,引擎会以为是变量名,报错
var bool2 = 'true'; // 错误!加了引号,变成了字符串,不是布尔类型
3.3.3 布尔值的隐式类型转换:true 转 1,false 转 0

当布尔值和数字一起做数学运算的时候,JS 会自动把布尔值转换成数字:true 会转换成 1,false 会转换成 0。

举个例子:

复制代码
console.log(true + 1); // 输出:2(true转成1,1+1=2)
console.log(false + 1); // 输出:1(false转成0,0+1=1)
console.log(true * 10); // 输出:10(true转成1,1*10=10)
console.log(false * 10); // 输出:0(false转成0,0*10=0)

重要提醒 :虽然 JS 支持布尔值做数学运算,但实际开发中,绝对不要这么写!

为什么?因为布尔值的作用是做逻辑判断,不是做数学运算。你写 true + 1,别人看你的代码,根本不知道你要干嘛,代码的可读性极差,而且非常容易出 bug。布尔值就应该只用在逻辑判断里,不要用来做数学运算,这是行业里的通用规范。

3.4 undefined 未定义类型:只有一个值的类型

undefined 类型,是 JS 里最特殊的类型之一,它只有一个值,就是 undefined。

3.4.1 什么时候会得到 undefined?

undefined 表示 "未定义"------ 一个变量被声明了,但没有被赋值,它的默认值就是 undefined。

所有常见的得到 undefined 的场景:

  1. 只声明变量,不赋值,默认值就是 undefined

    复制代码
    var a; // 只声明,不赋值
    console.log(a); // 输出:undefined
    console.log(typeof a); // 输出:'undefined'
  2. 访问对象不存在的属性,返回 undefined (后面讲对象的时候会详细说)

    复制代码
    var user = { name: '张三' };
    console.log(user.age); // 输出:undefined(user对象里没有age属性)
  3. 函数没有返回值,默认返回 undefined (后面讲函数的时候会说)

    复制代码
    function sayHello() {
      console.log('hello');
    }
    var result = sayHello();
    console.log(result); // 输出:undefined(函数没有return返回值)
  4. 调用函数时,参数没传,这个参数的值就是 undefined

    复制代码
    function sayHi(name) {
      console.log(name);
    }
    sayHi(); // 调用函数时没传name参数
    // 输出:undefined

其中,最基础、最常见的场景,就是第一种:只声明变量,不赋值,默认值就是 undefined。

3.4.2 undefined 和其他类型运算的结果

我们把 undefined 和其他类型做运算的结果,全部讲清楚,避免踩坑:

  1. undefined 和字符串拼接 :会把 undefined 转成字符串 'undefined',然后做拼接

    复制代码
    var a;
    console.log(a + '10'); // 输出:'undefined10'
  2. undefined 和数字做数学运算 :会把 undefined 转成数字 NaN,所以运算结果永远是 NaN

    复制代码
    var a;
    console.log(a + 10); // 输出:NaN
    console.log(a - 10); // 输出:NaN
    console.log(a * 10); // 输出:NaN
  3. undefined 做布尔转换 :会转成 false(后面讲条件语句的时候会说)

    复制代码
    var a;
    if (a) {
      console.log('会执行吗?'); // 不会执行,因为a是undefined,转成false
    }

3.5 null 空值类型:表示 "空的盒子"

null 类型,也是只有一个值的类型,它只有一个值 null,表示 "空值"。

3.5.1 null 的含义:主动赋值的 "空"

很多新手会把 null 和 undefined 搞混,我们用最通俗的话,讲清楚两者的核心区别:

  • undefined:相当于 "你连收纳盒都没准备好"------ 你只说了要一个盒子,但盒子还没拿出来,里面自然什么都没有,是 JS 引擎自动给你的默认值;
  • null:相当于 "你有一个收纳盒,盒子是空的,里面什么都没放"------ 你主动给这个变量赋值为 "空",表示这个变量未来会存一个值,但现在暂时是空的。

简单说:undefined 是被动的、默认的空;null 是主动的、人为的空。

null 的使用场景 :当你声明一个变量,未来要用来存一个对象 / 数组 / 复杂数据,但现在还没有这个数据,就可以先给它赋值为 null,告诉自己和其他开发者:"这个变量未来会存一个值,现在暂时是空的"。

举个例子:

复制代码
// 未来要存用户的信息,现在还没获取到,先赋值为null
var userInfo = null;
console.log(userInfo); // 输出:null
console.log(typeof userInfo); // 输出:'object'(历史遗留bug)

// 后面获取到用户信息了,再给它赋值
userInfo = { name: '张三', age: 20 };
3.5.2 null 和其他类型运算的结果

我们把 null 和其他类型运算的结果,全部讲清楚,并且和 undefined 做对比:

  1. null 和字符串拼接 :会把 null 转成字符串 'null',然后做拼接

    复制代码
    var b = null;
    console.log(b + '10'); // 输出:'null10'
  2. null 和数字做数学运算 :会把 null 转成数字 0,然后做运算(这是和 undefined 最大的区别!)

    复制代码
    var b = null;
    console.log(b + 10); // 输出:10(null转成0,0+10=10)
    console.log(b - 10); // 输出:-10(0-10=-10)
    console.log(b * 10); // 输出:0(0*10=0)
  3. null 做布尔转换 :会转成 false(和 undefined 一样)

    复制代码
    var b = null;
    if (b) {
      console.log('会执行吗?'); // 不会执行,null转成false
    }
3.5.3 null 和 undefined 的相等性判断

这里有一个 JS 的特殊规则,必须记住:

  • null == undefined 会返回 true(抽象相等,会做隐式转换);
  • null === undefined 会返回 false(严格相等,类型不同,一个是 null 类型,一个是 undefined 类型)。

举个例子:

复制代码
console.log(null == undefined); // 输出:true
console.log(null === undefined); // 输出:false

这个规则是 JS 诞生的时候就定下来的,不用纠结为什么,记住就行。实际开发中,我们一般用 ===,所以两者是不相等的。

3.5.4 为什么 typeof null 会返回 'object'?

很多新手会问:null 是 null 类型,为什么 typeof null 会返回 'object'?

这是 JavaScript 诞生的时候就存在的一个历史遗留 bug。在 JS 最初的版本里,值的存储是用 "类型标签 + 值" 的形式,对象的类型标签是 0,而 null 的值是全 0,所以它的类型标签被当成了 0,typeof 就把它当成了 object。

这个 bug 从 JS 诞生以来,一直没有被修复,因为修复它会导致大量的旧代码报错,为了兼容旧代码,就一直保留了下来。我们只要知道这个 bug 就行,不用纠结,实际开发中,判断一个值是不是 null,直接用 === null 就行。

四、运算符:JS 里处理数据的 "工具",每个运算符拆到极致

运算符,就是 JS 里用来处理数据的 "工具",比如用 + 做加法、用 == 做比较、用 && 做逻辑判断。我们把 JS 里所有基础的运算符,一个一个讲透,每个都讲清 "是什么、怎么用、有什么坑、什么时候用",没有任何一笔带过的内容。

4.1 算术运算符:做数学运算的工具

算术运算符,就是用来做数学加减乘除运算的,是最基础的运算符,我们一个一个讲。

运算符 含义 示例 结果
+ 加法(或字符串拼接) 10 + 20 30
- 减法 20 - 10 10
* 乘法 10 * 20 200
/ 除法 20 / 10 2
% 取余数(取模) 20 % 3 2
4.1.1 加法运算符 +

我们前面讲字符串拼接的时候,已经把 + 号讲得很透了,这里再总结一下它的两个核心作用:

  1. 两边都是数字:做数学加法;
  2. 至少有一边是字符串:做字符串拼接,另一边会自动转成字符串。

这里再补充一个点:+ 号如果只写在一个数字前面,会把这个值强制转换成数字,比如 +'123' 会得到数字 123,这是一个快速把字符串转成数字的小技巧。

举个例子:

复制代码
var str = '123';
var num = +str; // 把字符串'123'转成数字123
console.log(num); // 输出:123
console.log(typeof num); // 输出:'number'
4.1.2 减法运算符 -

减法运算符,只有一个作用:做数学减法。它会把两边的值,都自动转换成数字,再做减法,如果转换失败,结果就是 NaN。

举个例子:

复制代码
// 纯数字减法
console.log(20 - 10); // 输出:10

// 字符串数字减法:会自动转成数字
console.log('20' - '10'); // 输出:10(两边都转成数字,做减法)

// 非数字字符串减法:转不成数字,结果NaN
console.log('hello' - 10); // 输出:NaN

// 布尔值减法:true转1,false转0
console.log(true - 1); // 输出:0
console.log(10 - false); // 输出:10

这是减法和加法最大的区别:减法永远只会做数学运算,不会做字符串拼接,哪怕两边都是字符串,也会先转成数字再做减法。

4.1.3 乘法运算符 * 和除法运算符 /

乘法和除法,和减法一样,只会做数学运算,会把两边的值自动转成数字,转换失败就返回 NaN。

这里要重点讲一下除法:在 JS 里,除法不会做 "整数除法",哪怕两个整数能整除,结果也会是小数形式(虽然值和整数一样),如果不能整除,就会返回小数,这和 C++ 里的整数除法完全不一样,新手要注意。

举个例子:

复制代码
// 乘法
console.log(10 * 20); // 输出:200
console.log('10' * '20'); // 输出:200(转成数字做乘法)
console.log('hello' * 10); // 输出:NaN

// 除法
console.log(20 / 10); // 输出:2(能整除,结果是2)
console.log(5 / 2); // 输出:2.5(不会舍去小数部分,和C++不一样)
console.log(10 / 3); // 输出:3.3333333333333335
console.log('20' / '5'); // 输出:4(转成数字做除法)
console.log('hello' / 2); // 输出:NaN
4.1.4 取余数运算符 %(取模)

取余数运算符,是新手最容易搞不懂的运算符,我们用最通俗的话讲透:% 号,就是求 "除法里除不尽剩下的余数"。

比如 20 除以 3,商是 6,余数是 2,所以 20 % 3 的结果就是 2。

举个基础例子:

复制代码
console.log(20 % 3); // 输出:2(20除以3,商6余2)
console.log(10 % 2); // 输出:0(10除以2,商5余0)
console.log(7 % 5); // 输出:2(7除以5,商1余2)
console.log(5 % 7); // 输出:5(5除以7,商0余5)

负数的取余规则 :很多新手会问:负数的取余,结果是正还是负?JS 里的取余规则是:结果的符号,和被除数的符号一致。

举个例子:

复制代码
console.log(-3 % 2); // 输出:-1(被除数是-3,结果是负的)
console.log(3 % -2); // 输出:1(被除数是3,结果是正的)
console.log(-7 % 3); // 输出:-1
console.log(7 % -3); // 输出:1

这就是我们之前说的:判断一个数是不是奇数,不能写 num % 2 == 1 ,因为负奇数的取余结果是 -1,-1 == 1 是 false,会判断错误。正确的写法是 num % 2 != 0,或者 num % 2 === 1 || num % 2 === -1。

正确的奇偶判断:

复制代码
function isOdd(num) {
  return num % 2 !== 0; // 余数不是0,就是奇数
}

console.log(isOdd(3)); // 输出:true
console.log(isOdd(-3)); // 输出:true(正确)
console.log(isOdd(4)); // 输出:false

取余运算符的常用场景:

  1. 判断一个数的奇偶性;
  2. 判断一个数是不是另一个数的倍数 (比如 num % 3 == 0,就是 3 的倍数);
  3. 循环里的分页、隔行变色 (比如 i % 2 == 0,就是偶数行);
  4. 限制数字的范围 (比如 num % 10,结果永远在 0-9 之间)。

4.2 赋值运算符 & 复合赋值运算符

赋值运算符,就是用来给变量赋值的,我们最基础的 = 就是赋值运算符,还有一些复合赋值运算符,是 "算术运算 + 赋值" 的结合,能让代码更简洁。

4.2.1 基础赋值运算符 =

我们前面已经讲过无数次了,= 号是 "赋值动作",把右边的值,放进左边的变量里,这里就不再重复了,只强调一点:赋值运算符的优先级是最低的,永远是先算右边的表达式,再把结果赋给左边的变量。

举个例子:

复制代码
var a = 10 + 20 * 3;
// 先算右边的 20*3=60,再算10+60=70,最后把70赋给a
console.log(a); // 输出:70
4.2.2 复合赋值运算符

复合赋值运算符,就是把 "算术运算" 和 "赋值" 合并成一个运算符,让代码更简洁,不用写两遍变量名。

常用的复合赋值运算符表:

复合赋值运算符 等价于 示例 结果(初始 a=10)
+= a = a + b a += 5 a = 15
-= a = a - b a -= 5 a = 5
*= a = a * b a *= 5 a = 50
/= a = a / b a /= 5 a = 2
%= a = a % b a %= 3 a = 1

举个详细的例子:

复制代码
var a = 10;

// 等价于 a = a + 20;
a += 20;
console.log(a); // 输出:30

// 等价于 a = a - 10;
a -= 10;
console.log(a); // 输出:20

// 等价于 a = a * 2;
a *= 2;
console.log(a); // 输出:40

// 等价于 a = a / 4;
a /= 4;
console.log(a); // 输出:10

// 等价于 a = a % 3;
a %= 3;
console.log(a); // 输出:1

为什么要用复合赋值运算符?

  1. 代码更简洁 :不用写两遍变量名,尤其是变量名很长的时候,比如 userInfo.age += 1,比 userInfo.age = userInfo.age + 1 简洁太多了;
  2. 性能更好:JS 引擎只需要查找一次变量,性能稍微好一点(虽然现在几乎可以忽略,但也是一个优点)。

4.3 自增自减运算符:让变量加 1 或减 1 的快捷方式

自增自减运算符,是专门用来 "让变量的值加 1" 或 "减 1" 的,在循环里用得非常多。

运算符 含义
++ 自增 1,让变量的值加 1
-- 自减 1,让变量的值减 1

自增自减运算符,有两种写法:前置写法 (++a、--a)和 后置写法 (a++、a--),两者的核心区别,是 "加 1 / 减 1 的时机不同",这是新手最容易搞混的地方,我们用最通俗的话讲透。

4.3.1 前置自增 / 自减:先加 / 减,后用

前置写法 :运算符写在变量前面,比如 ++a、--a。

它的执行顺序是:先把变量的值加 1 / 减 1,然后再使用变量的新值。

举个例子:

复制代码
var a = 10;
var b = ++a; // 前置自增

// 执行顺序:
// 1. 先把a的值加1,a变成11
// 2. 再把a的新值11,赋给b

console.log(a); // 输出:11
console.log(b); // 输出:11

前置自减也是一样的逻辑:

复制代码
var a = 10;
var b = --a; // 前置自减

// 执行顺序:
// 1. 先把a的值减1,a变成9
// 2. 再把a的新值9,赋给b

console.log(a); // 输出:9
console.log(b); // 输出:9
4.3.2 后置自增 / 自减:先用,后加 / 减

后置写法 :运算符写在变量后面,比如 a++、a--。

它的执行顺序是:先使用变量当前的旧值,然后再把变量的值加 1 / 减 1。

举个例子:

复制代码
var a = 10;
var b = a++; // 后置自增

// 执行顺序:
// 1. 先把a当前的旧值10,赋给b
// 2. 再把a的值加1,a变成11

console.log(a); // 输出:11
console.log(b); // 输出:10

后置自减也是一样的逻辑:

复制代码
var a = 10;
var b = a--; // 后置自减

// 执行顺序:
// 1. 先把a当前的旧值10,赋给b
// 2. 再把a的值减1,a变成9

console.log(a); // 输出:9
console.log(b); // 输出:10
4.3.3 复杂例子拆解,彻底搞懂区别

我们用一个稍微复杂的例子,一步步拆解执行顺序,让你彻底搞懂前置和后置的区别。

例子:

复制代码
var a = 10;
var b = 5;
var c = a++ + ++b;

console.log(a); // ?
console.log(b); // ?
console.log(c); // ?

一步步拆解执行顺序:

  1. 先声明变量 a=10,b=5;
  2. 计算 c = a++ + ++b,从左到右执行:
    • 第一步:计算 a++(后置自增):先使用 a 的旧值 10,然后 a 加 1 变成 11;
    • 第二步:计算 ++b(前置自增):先把 b 加 1 变成 6,然后使用 b 的新值 6;
    • 第三步:把两个结果相加:10 + 6 = 16,赋给 c;
  3. 最终结果:
    • a = 11;
    • b = 6;
    • c = 16。
4.3.4 什么时候用前置,什么时候用后置?
  1. 如果你不需要使用自增 / 自减的返回值,只是想让变量加 1 / 减 1 :前置和后置的效果完全一样,没有任何区别。比如在 for 循环里:

    复制代码
    // 后置写法
    for (var i = 0; i < 10; i++) {
      console.log(i);
    }
    
    // 前置写法
    for (var i = 0; i < 10; ++i) {
      console.log(i);
    }
    // 两个循环的执行结果完全一样,都是输出0-9
  2. 如果你需要使用自增 / 自减的返回值:就根据你的需求,选择前置或后置。需要先改值再用,就用前置;需要先用旧值再改值,就用后置。

4.4 比较运算符:判断两个值的大小 / 是否相等

比较运算符,就是用来 "比较两个值的大小、或者是否相等" 的,所有比较运算符的返回结果,都是布尔值 true 或 false,用来给条件语句做判断。

比较运算符分为两大类:大小比较运算符 和 相等性比较运算符,我们一个一个讲透。

4.4.1 大小比较运算符
运算符 含义 示例 结果
< 小于 10 < 20 true
> 大于 10 > 20 false
<= 小于等于 10 <= 10 true
>= 大于等于 10 >= 20 false

基础用法:

复制代码
console.log(10 < 20); // 输出:true
console.log(10 > 20); // 输出:false
console.log(10 <= 10); // 输出:true(小于等于,只要满足其中一个就为true)
console.log(10 >= 5); // 输出:true

比较规则:

  1. 两边都是数字:直接比较数字的大小;

  2. 两边都是字符串 :按字符的 Unicode 编码,从左到右一个一个比较,不是按字符串的长度!

    复制代码
    console.log('a' < 'b'); // 输出:true('a'的Unicode编码是97,'b'是98)
    console.log('A' < 'a'); // 输出:true('A'的编码是65,比'a'小)
    console.log('10' < '2'); // 输出:true(先比较第一个字符'1'和'2','1'的编码比'2'小,所以直接返回true,不会看后面的0)
    console.log('apple' < 'banana'); // 输出:true(第一个字符'a' < 'b')

    这是新手最容易踩的坑:两个数字字符串比较,不是按数字大小,而是按字符编码比较,比如 '10' < '2' 会返回 true,一定要注意!

  3. 一边是数字,一边是字符串 :会把字符串转成数字,再比较大小;如果字符串转不成数字,就返回 false。

    复制代码
    console.log(10 < '20'); // 输出:true('20'转成20,10<20)
    console.log(100 > '20'); // 输出:true('20'转成20,100>20)
    console.log(10 < 'hello'); // 输出:false('hello'转成NaN,任何数和NaN比较都是false)
4.4.2 相等性比较运算符:== 和 === 的核心区别

这是 JavaScript 里最核心、最容易踩坑的知识点,我们用最大的篇幅,把它讲得明明白白,让你再也不会在这里踩坑。

运算符 含义 核心规则
== 抽象相等(宽松相等) 会做隐式类型转换,把两边的值转成相同类型,再比较值是否相等
=== 严格相等 不会做隐式类型转换,只有两边的值类型相同、并且值也相同,才会返回 true,否则返回 false
!= 抽象不相等 和 == 相反,会做隐式类型转换
!== 严格不相等 和 === 相反,不会做隐式类型转换
1. 严格相等 ===:最安全、最推荐的写法

=== 的规则非常简单,只有两步:

  1. 先看两边的值的类型是否相同,如果类型不同,直接返回 false;
  2. 如果类型相同,再看值是否相同,值相同就返回 true,值不同就返回 false。

举个例子:

复制代码
// 类型相同,值相同:返回true
console.log(10 === 10); // 输出:true
console.log('hello' === 'hello'); // 输出:true
console.log(true === true); // 输出:true
console.log(null === null); // 输出:true
console.log(undefined === undefined); // 输出:true

// 类型不同:直接返回false,不管值是不是看起来一样
console.log(10 === '10'); // 输出:false(一个是number,一个是string)
console.log(true === 1); // 输出:false(一个是boolean,一个是number)
console.log(null === undefined); // 输出:false(一个是null类型,一个是undefined类型)
console.log(false === 0); // 输出:false(一个是boolean,一个是number)

=== 不会做任何隐式类型转换,规则简单明了,不会有任何奇怪的结果,实际开发中,所有的相等性判断,都推荐用 === 和!==,绝对不要用 == 和!=。

2. 抽象相等 ==:会做隐式类型转换,坑非常多

== 会做隐式类型转换,把两边的值转成相同的类型,再比较值是否相等。它的转换规则非常复杂,有很多反直觉的结果,新手很容易踩坑。

我们把最常见的转换规则,用大白话讲清楚:

  1. 如果两边类型相同:和 === 的规则完全一样,比较值是否相等;

  2. 如果一边是数字,一边是字符串 :把字符串转成数字,再比较;

    复制代码
    console.log(10 == '10'); // 输出:true('10'转成10,值相等)
    console.log(0 == ''); // 输出:true(空字符串转成0)
    console.log(0 == ' '); // 输出:true(空格字符串转成0)
  3. 如果一边是布尔值 :把布尔值转成数字(true 转 1,false 转 0),再比较;

    复制代码
    console.log(true == 1); // 输出:true(true转1)
    console.log(false == 0); // 输出:true(false转0)
    console.log(true == '1'); // 输出:true(true转1,'1'转1)
    console.log(false == ''); // 输出:true(false转0,''转0)
  4. null 和 undefined :null == undefined 会返回 true,和其他任何值比较都返回 false;

    复制代码
    console.log(null == undefined); // 输出:true(JS的特殊规定)
    console.log(null == 0); // 输出:false
    console.log(undefined == 0); // 输出:false
  5. NaN :NaN 和任何值 == 比较都返回 false,包括它自己;

    复制代码
    console.log(NaN == NaN); // 输出:false

== 的反直觉坑示例:

复制代码
console.log('' == 0); // true
console.log('' == false); // true
console.log(0 == false); // true
// 这三个互相==,但它们明明是完全不同类型的值,非常反直觉

console.log(' \t\n ' == 0); // true(全是空格、换行、制表符的字符串,转成0)
console.log([] == ![]); // true(非常经典的坑,新手根本看不懂)

这些反直觉的结果,都是因为 == 的隐式类型转换导致的,你根本记不住所有的转换规则,也没必要记。最好的方法,就是永远不要用 ==,所有的相等性判断都用 ===,就能完美避开所有的坑。

4.5 逻辑运算符:处理多个布尔条件的工具

逻辑运算符,是用来 "组合多个布尔条件,得到最终的布尔结果" 的,常用在条件语句里,组合多个判断条件。

运算符 含义 核心规则
&& 逻辑与(并且) 一假则假:只有两边都是 true,结果才是 true;只要有一个是 false,结果就是 false
` ` 逻辑或(或者) 一真则真:只要有一个是 true,结果就是 true;只有两边都是 false,结果才是 false
! 逻辑非(取反) 真变假,假变真:true 变 false,false 变 true
4.5.1 逻辑与 &&:一假则假

&& 表示 "并且",只有当所有的条件都满足(都是 true),结果才是 true;只要有一个条件不满足(是 false),结果就是 false。

真值表:

左边的值 右边的值 结果
true true true
true false false
false true false
false false false

举个例子:

复制代码
// 场景:判断一个数是不是在10到20之间(大于10 并且 小于20)
var num = 15;
if (num > 10 && num < 20) {
  console.log(num + '在10到20之间');
} else {
  console.log(num + '不在10到20之间');
}
// 输出:15在10到20之间
4.5.2 逻辑或 ||:一真则真

|| 表示 "或者",只要有任意一个条件满足(是 true),结果就是 true;只有当所有条件都不满足(都是 false),结果才是 false。

真值表:

左边的值 右边的值 结果
true true true
true false true
false true true
false false false

举个例子:

复制代码
// 场景:判断一个数是不是小于10 或者 大于20
var num = 25;
if (num < 10 || num > 20) {
  console.log(num + '小于10或者大于20');
} else {
  console.log(num + '在10到20之间');
}
// 输出:25小于10或者大于20
4.5.3 逻辑非!:取反

! 表示 "取反",把 true 变成 false,把 false 变成 true。它是一个一元运算符,只需要一个操作数。

真值表:

原值 取反结果
true false
false true

举个例子:

复制代码
var isAdult = true;
console.log(!isAdult); // 输出:false

var isStudent = false;
console.log(!isStudent); // 输出:true

// 场景:判断一个数是不是不大于10
var num = 15;
if (!(num > 10)) {
  console.log(num + '不大于10');
} else {
  console.log(num + '大于10');
}
// 输出:15大于10

常用技巧:!! 把任意值转成布尔值 两个感叹号 !!,可以把任意值,转换成对应的布尔值,相当于调用 Boolean() 函数。

举个例子:

复制代码
console.log(!!'hello'); // 输出:true(非空字符串转true)
console.log(!!''); // 输出:false(空字符串转false)
console.log(!!10); // 输出:true(非0数字转true)
console.log(!!0); // 输出:false(0转false)
console.log(!!null); // 输出:false
console.log(!!undefined); // 输出:false
4.5.4 短路求值:逻辑运算符的核心特性

逻辑运算符有一个非常重要的特性,叫短路求值 :如果通过左边的值,已经能确定最终的结果了,就不会再执行右边的代码。

这个特性在实际开发中非常常用,我们一个一个讲透。

1. && 的短路求值:左边为 false,右边不执行

对于 a && b:

  • 如果 a 是 false,那么不管 b 是什么,最终结果一定是 false,所以 JS 引擎会直接短路,不会执行右边的 b;
  • 只有当 a 是 true 的时候,才会执行右边的 b,看 b 的结果。

举个例子:

复制代码
// 左边是false,右边的alert不会执行,不会弹框
false && alert('我不会执行');

// 左边是true,右边的alert会执行,会弹框
true && alert('我会执行');

实际开发中的常用场景:安全访问对象的属性当你要访问一个对象的属性,但不确定这个对象是不是 null/undefined 的时候,可以用 && 做安全判断,避免报错。

举个例子:

复制代码
var user = null;

// 如果user是null,直接访问user.name会报错:Cannot read properties of null
// console.log(user.name); // 会报错

// 用&&安全访问:user是null,左边是false,右边不会执行,不会报错
console.log(user && user.name); // 输出:null,不会报错

// 如果user有值,就会访问name属性
var user2 = { name: '张三' };
console.log(user2 && user2.name); // 输出:张三

这个技巧,在实际开发中非常常用,用来避免 "访问空对象的属性" 导致的报错。

2. || 的短路求值:左边为 true,右边不执行

对于 a || b:

  • 如果 a 是 true,那么不管 b 是什么,最终结果一定是 true,所以 JS 引擎会直接短路,不会执行右边的 b;
  • 只有当 a 是 false 的时候,才会执行右边的 b,看 b 的结果。

举个例子:

复制代码
// 左边是true,右边的alert不会执行,不会弹框
true || alert('我不会执行');

// 左边是false,右边的alert会执行,会弹框
false || alert('我会执行');

实际开发中的常用场景:给变量设置默认值当你要给一个变量赋值,如果这个变量有值,就用它本身的值;如果它是空的(null/undefined/ 空字符串),就用默认值,这时候就可以用 ||。

举个例子:

复制代码
// 场景:用户输入姓名,如果没输入,就用'匿名'作为默认值
var userName = prompt('请输入您的姓名:');
// 如果用户点击取消,userName是null;如果没输入内容,userName是空字符串,都会用'匿名'
var finalName = userName || '匿名';

alert('您好,' + finalName);

这个场景,在实际开发中非常常用,用来给变量设置兜底的默认值。

4.5.5 逻辑运算符的优先级

逻辑运算符的优先级:! > && > ||。

也就是说,JS 会先算 !,再算 &&,最后算 ||。如果你想改变运算顺序,就用括号 () 把要先算的部分括起来,括号的优先级是最高的。

举个例子:

复制代码
// 优先级:先算!false=true,再算true && true=true,最后算false || true=true
console.log(false || true && !false); // 输出:true

// 用括号改变顺序:先算false||true=true,再算!false=true,最后算true && true=true
console.log((false || true) && !false); // 输出:true

建议:不要去记优先级,只要你的逻辑比较复杂,就用括号把你想先算的部分括起来,代码可读性更高,也不会出错。

4.6 位运算和移位运算:了解即可

位运算和移位运算,是直接操作数字的二进制位的,在前端基础开发中用得非常少,一般只用在底层优化、算法、加密等场景,我们这里只做简单的了解,不用深入研究。

4.6.1 位运算符
运算符 含义 示例 结果
& 按位与:两个位都是 1,结果才是 1 5 & 3 1(5 是 101,3 是 011,按位与是 001=1)
` ` 按位或:只要有一个位是 1,结果就是 1 `5 3` 7(101 011 = 111=7)
~ 按位取反:0 变 1,1 变 0 ~5 -6
^ 按位异或:两个位不同,结果才是 1 5 ^ 3 6(101 ^ 011 = 110=6)

常用小技巧:

  • 用 num & 1 判断奇偶:结果是 1 就是奇数,0 就是偶数,比 %2 性能更好;
  • 用 num | 0 把数字取整,相当于舍去小数部分;
  • 用 ^ 交换两个变量的值,不用临时变量。
4.6.2 移位运算符
运算符 含义 示例 结果
<< 左移:所有位向左移,右边补 0 5 << 1 10(相当于乘以 2 的 1 次方)
>> 有符号右移(算术右移):所有位向右移,左边补符号位 5 >> 1 2(相当于除以 2 的 1 次方,取整)
>>> 无符号右移(逻辑右移):所有位向右移,左边补 0 5 >>> 1 2

左移一位,相当于乘以 2;右移一位,相当于除以 2 取整,比乘除法性能更好,在算法里常用。

五、条件语句:让代码 "会思考、做选择"

条件语句,就是让代码根据不同的情况,执行不同的代码块,实现 "逻辑判断"。就像我们生活里:"如果下雨,就带伞;否则,就不带伞",条件语句就是让代码做这种选择的。

JavaScript 里的条件语句有三种:if 语句、三元表达式、switch 语句,我们一个一个讲透。

5.1 代码块和代码执行顺序

在讲条件语句之前,我们先搞懂两个基础概念:代码块 和 代码执行顺序。

5.1.1 代码块

代码块,就是用一对大括号 {} 包裹起来的一段代码,JS 引擎会把大括号里的代码,当成一个整体来执行。

举个例子:

复制代码
{
  var a = 10;
  var b = 20;
  console.log(a + b);
}
// 大括号里的代码会被当成一个整体,从上到下执行

条件语句、循环语句、函数里的代码,都是放在大括号里的代码块。

5.1.2 代码的默认执行顺序:从上到下,依次执行

JS 代码的默认执行顺序,是从上到下,一行一行依次执行,前面的代码执行完,才会执行后面的代码。

而条件语句,就是打破这个默认顺序,让代码 "根据条件,选择执行某一段代码块,跳过另一段代码块"。

5.2 if 语句:最常用、最灵活的条件语句

if 语句是 JS 里最常用的条件语句,它有三种形式,我们一个一个讲透,从最简单的到最复杂的。

5.2.1 形式 1:单分支 if 语句(只有 if)

这是最简单的 if 语句,只有一个条件,满足条件就执行代码块,不满足就跳过。

语法:

复制代码
if (条件表达式) {
  // 条件为true时,要执行的代码块
}
// 后面的代码,不管条件是否满足,都会执行

执行流程:

  1. 先计算 条件表达式 的值,自动转换成布尔值;
  2. 如果转换后的值是 true,就执行大括号里的代码块;
  3. 如果转换后的值是 false,就跳过整个代码块,执行大括号后面的代码。

举个例子:

复制代码
var age = 20;

// 如果年龄大于等于18,就输出"你是成年人"
if (age >= 18)

var age = 20;

// 如果年龄大于等于18,就输出"你是成年人"
if (age >= 18) {
  console.log('你是成年人');
}
console.log('代码执行结束');
// 输出:
// 你是成年人
// 代码执行结束

在这个例子里,age 的值是 20,age >= 18 这个条件表达式的结果是 true,所以大括号里的代码会被执行,输出 "你是成年人";如果 age 的值是 15,age >= 18 的结果是 false,大括号里的代码就会被跳过,直接执行后面的 console.log('代码执行结束')。

这里要注意一个新手容易忽略的细节:如果 if 语句的代码块只有一行代码,大括号 {} 可以省略 ,但我强烈建议你永远不要省略大括号!

反面例子(省略大括号,非常不推荐):

复制代码
var age = 20;
if (age >= 18)
  console.log('你是成年人'); // 只有这一行属于if
  console.log('这一行永远会执行'); // 这一行不属于if,不管条件是否满足都会执行

在这个例子里,如果你省略了大括号,只有紧跟在 if 后面的第一行代码属于 if 语句,第二行代码是独立的,不管条件是否满足都会执行。如果你缩进了第二行,看起来像是属于 if,但实际上不是,非常容易出 bug。

正面例子(永远加上大括号,推荐):

复制代码
var age = 20;
if (age >= 18) {
  console.log('你是成年人');
  console.log('这一行只有条件满足时才会执行');
}
console.log('代码执行结束');

加上大括号,代码的逻辑非常清晰,哪些代码属于 if,哪些不属于,一眼就能看出来,永远不会出缩进导致的 bug。所以,哪怕只有一行代码,也请你务必加上大括号,这是行业里的通用规范。

5.2.2 形式 2:双分支 if...else 语句

单分支 if 语句只有 "满足条件就执行,不满足就跳过" 这一种选择;而双分支 if...else 语句,有两种选择:满足条件执行代码块 1,不满足条件执行代码块 2,二选一,一定会执行其中一个代码块。

语法:

复制代码
if (条件表达式) {
  // 条件为true时,执行这里的代码块1
} else {
  // 条件为false时,执行这里的代码块2
}

执行流程:

  1. 先计算 条件表达式 的值,自动转换成布尔值;
  2. 如果转换后的值是 true,执行 if 后面大括号里的代码块 1,然后跳过 else 后面的代码块;
  3. 如果转换后的值是 false,跳过 if 后面的代码块 1,执行 else 后面大括号里的代码块 2。

举个例子:

复制代码
var age = 15;

// 如果年龄大于等于18,输出"你是成年人";否则,输出"你是未成年人"
if (age >= 18) {
  console.log('你是成年人');
} else {
  console.log('你是未成年人');
}
console.log('代码执行结束');
// 输出:
// 你是未成年人
// 代码执行结束

在这个例子里,age 的值是 15,age >= 18 的结果是 false,所以会跳过 if 里的代码,执行 else 里的代码,输出 "你是未成年人";如果 age 的值是 20,就会执行 if 里的代码,跳过 else 里的代码。

if...else 语句,就像我们生活里的 "二选一":"如果明天不下雨,我们就去公园;否则,我们就去图书馆",一定会选其中一个做。

5.2.3 形式 3:多分支 if...else if...else 语句

如果你的选择不止两个,而是三个、四个甚至更多,就需要用多分支 if...else if...else 语句了。它可以有多个条件,从上到下依次判断,只要有一个条件满足,就执行对应的代码块,然后跳过后面所有的 else if 和 else;如果所有条件都不满足,就执行 else 里的代码块。

语法:

复制代码
if (条件表达式1) {
  // 条件1为true时,执行这里
} else if (条件表达式2) {
  // 条件1为false,条件2为true时,执行这里
} else if (条件表达式3) {
  // 条件1、2都为false,条件3为true时,执行这里
}
// ... 可以有任意多个 else if
else {
  // 所有条件都为false时,执行这里
}

执行流程:

  1. 先计算 条件表达式1 的值;
  2. 如果是 true,执行对应的代码块,然后跳过后面所有的 else if 和 else,整个 if 语句结束;
  3. 如果是 false,继续计算 条件表达式2 的值;
  4. 如果 条件表达式2 是 true,执行对应的代码块,跳过后面的;
  5. 以此类推,从上到下依次判断;
  6. 如果所有条件都是 false,执行 else 里的代码块。

举个例子:

复制代码
// 场景:根据分数,输出对应的等级
// 90分及以上:优秀
// 80-89分:良好
// 60-79分:及格
// 60分以下:不及格
var score = 85;

if (score >= 90) {
  console.log('优秀');
} else if (score >= 80) {
  console.log('良好');
} else if (score >= 60) {
  console.log('及格');
} else {
  console.log('不及格');
}
// 输出:良好

我们来一步步拆解这个例子的执行流程:

  1. score 的值是 85;
  2. 先判断第一个条件 score >= 90:85 >= 90 是 false,跳过;
  3. 继续判断第二个条件 score >= 80:85 >= 80 是 true,执行 console.log('良好');
  4. 执行完之后,跳过后面的 else if 和 else,整个 if 语句结束。

这里要注意一个非常重要的细节:else if 的顺序很重要,必须从严格到宽松,或者从高到低,不能写反了。

反面例子(顺序写反了,会出 bug):

复制代码
var score = 95;

// 错误!顺序写反了
if (score >= 60) {
  console.log('及格');
} else if (score >= 80) {
  console.log('良好');
} else if (score >= 90) {
  console.log('优秀');
} else {
  console.log('不及格');
}
// 输出:及格(这是错误的!95分应该是优秀)

为什么会出 bug?因为 score 是 95,第一个条件 score >= 60 就已经满足了,直接执行了 "及格",后面的条件根本不会再判断了,所以顺序非常重要,必须从最严格的条件开始写。

正面例子(顺序正确):

复制代码
var score = 95;

// 正确!从高到低,从严格到宽松
if (score >= 90) {
  console.log('优秀');
} else if (score >= 80) {
  console.log('良好');
} else if (score >= 60) {
  console.log('及格');
} else {
  console.log('不及格');
}
// 输出:优秀(正确)

另外,else 是可选的,你可以根据需求决定要不要加。如果没有 else,并且所有条件都不满足,就什么都不执行,直接跳过整个 if 语句。

5.3 falsy 值和 truthy 值:条件表达式里的隐式类型转换

在讲 if 语句的条件表达式时,我们一直说 "条件表达式的结果会自动转换成布尔值",但没有详细讲:哪些值会被转换成 true,哪些会被转换成 false?

这就是我们要讲的 falsy 值 和 truthy 值,这是 JavaScript 里非常重要的一个知识点,很多新手会在这里踩坑,我们用最详尽的文字讲透。

5.3.1 falsy 值:会被转换成 false 的值

falsy 值,就是 "在布尔上下文中(比如 if 语句的条件里),会被自动转换成 false 的值"。

JavaScript 里的 falsy 值,一共只有 6 个,我们把它们全部列出来,一个一个讲清楚:

falsy 值 说明
false 布尔值 false 本身
0 数字 0
-0 数字负 0(虽然很少见,但也是 falsy)
'' 或 "" 空字符串(引号里什么都没有,连空格都没有)
null null 空值
undefined undefined 未定义
NaN NaN 非数字

只有这 6 个值是 falsy 值,会被转换成 false;除此之外的所有值,都是 truthy 值,会被转换成 true。

举个例子,验证所有 falsy 值:

复制代码
// 1. false
if (false) {
  console.log('false 会执行吗?'); // 不会执行
} else {
  console.log('false 是 falsy 值'); // 会执行
}

// 2. 0
if (0) {
  console.log('0 会执行吗?'); // 不会执行
} else {
  console.log('0 是 falsy 值'); // 会执行
}

// 3. 空字符串 ''
if ('') {
  console.log('空字符串会执行吗?'); // 不会执行
} else {
  console.log('空字符串是 falsy 值'); // 会执行
}

// 4. null
if (null) {
  console.log('null 会执行吗?'); // 不会执行
} else {
  console.log('null 是 falsy 值'); // 会执行
}

// 5. undefined
var a;
if (a) {
  console.log('undefined 会执行吗?'); // 不会执行
} else {
  console.log('undefined 是 falsy 值'); // 会执行
}

// 6. NaN
if (NaN) {
  console.log('NaN 会执行吗?'); // 不会执行
} else {
  console.log('NaN 是 falsy 值'); // 会执行
}

这里要注意一个新手最容易踩的坑:全是空格的字符串 ' ' 不是 falsy 值,是 truthy 值!因为它不是空字符串,引号里有内容(空格),所以会被转换成 true。

踩坑示例:

复制代码
var str = '   '; // 全是空格的字符串,不是空字符串

if (str) {
  console.log('全是空格的字符串是 truthy 值,会执行这里'); // 会执行!
} else {
  console.log('不会执行这里');
}

很多新手会以为 "空格不算内容",但在 JavaScript 里,空格也是一个字符,全是空格的字符串不是空字符串,是 truthy 值,一定要注意!

5.3.2 truthy 值:会被转换成 true 的值

truthy 值,就是 "在布尔上下文中,会被自动转换成 true 的值"。

判断规则非常简单:只要不是上面列出的 6 个 falsy 值,就全都是 truthy 值。

常见的 truthy 值示例:

复制代码
// 1. 非0的数字(正数、负数都是)
if (10) {
  console.log('10 是 truthy 值'); // 会执行
}
if (-5) {
  console.log('-5 是 truthy 值'); // 会执行
}
if (3.14) {
  console.log('3.14 是 truthy 值'); // 会执行
}

// 2. 非空的字符串(哪怕只有一个字符,哪怕只有一个空格)
if ('hello') {
  console.log('非空字符串是 truthy 值'); // 会执行
}
if (' ') {
  console.log('只有一个空格的字符串也是 truthy 值'); // 会执行
}
if ('0') {
  console.log('字符串"0"也是 truthy 值'); // 会执行!注意:字符串"0"不是数字0,是truthy
}

// 3. 所有的对象(包括空对象 {})
if ({}) {
  console.log('空对象 {} 是 truthy 值'); // 会执行
}

// 4. 所有的数组(包括空数组 [])
if ([]) {
  console.log('空数组 [] 是 truthy 值'); // 会执行
}

// 5. 所有的函数
if (function(){}) {
  console.log('函数是 truthy 值'); // 会执行
}

这里再强调一个新手容易踩的坑:字符串 "0" 是 truthy 值,不是 falsy 值 !因为它是一个非空的字符串,虽然内容是 "0",但它不是数字 0,所以会被转换成 true,一定要注意区分数字 0 和字符串 "0"。

踩坑示例:

复制代码
// 数字0:falsy
if (0) {
  console.log('数字0不会执行这里');
} else {
  console.log('数字0是falsy'); // 会执行
}

// 字符串"0":truthy
if ('0') {
  console.log('字符串"0"是truthy,会执行这里'); // 会执行!
} else {
  console.log('不会执行这里');
}
5.3.3 falsy 和 truthy 的实际应用:简化条件判断

理解了 falsy 和 truthy 值,我们可以用它们来简化条件判断,让代码更简洁。

常见应用场景 1:判断变量是否有值 如果你想判断一个变量是不是 "有值"(不是 null、不是 undefined、不是空字符串),就可以直接把变量写在 if 的条件里,不用写 === null 或者 === undefined。

举个例子:

复制代码
var userName = prompt('请输入您的姓名:');

// 简化写法:直接写 userName
// 如果用户点击取消,userName是null(falsy);如果没输入内容,是空字符串(falsy)
if (userName) {
  alert('您好,' + userName);
} else {
  alert('您好,匿名用户');
}

这个简化写法,等价于下面的复杂写法,但简洁太多了:

复制代码
// 复杂写法(不推荐)
if (userName !== null && userName !== undefined && userName !== '') {
  alert('您好,' + userName);
} else {
  alert('您好,匿名用户');
}

常见应用场景 2:判断数组是否为空 注意:空数组 [] 是 truthy 值,所以你不能直接写 if (arr) 来判断数组是否为空,因为空数组也会返回 true。判断数组是否为空,应该用 arr.length === 0。

举个例子:

复制代码
var arr = []; // 空数组

// 错误写法:空数组是truthy,会执行if里的代码
if (arr) {
  console.log('数组不为空'); // 会执行,但这是错误的!
}

// 正确写法:用length判断
if (arr.length === 0) {
  console.log('数组为空'); // 会执行,正确
} else {
  console.log('数组不为空');
}

5.4 三元表达式:if...else 的简化写法

三元表达式,是 if...else 语句的一种简化写法,它可以用一行代码,实现 if...else 的二选一逻辑,让代码更简洁。

之所以叫 "三元",是因为它有三个操作数,是 JavaScript 里唯一一个有三个操作数的运算符。

5.4.1 三元表达式的语法
复制代码
条件表达式 ? 表达式1 : 表达式2;

三个部分的含义:

  1. 条件表达式:和 if 语句里的条件一样,会被自动转换成布尔值;
  2. 表达式1:如果条件为 true,就执行这个表达式,返回它的结果;
  3. 表达式2:如果条件为 false,就执行这个表达式,返回它的结果。

执行流程:

  1. 计算 条件表达式 的值,转成布尔值;
  2. 如果是 true,计算 表达式1 的值,返回这个结果;
  3. 如果是 false,计算 表达式2 的值,返回这个结果。
5.4.2 三元表达式的基础示例

我们用一个最经典的例子:"求两个数的最大值",来对比 if...else 和三元表达式的写法。

if...else 写法:

复制代码
var a = 10;
var b = 20;
var max; // 用来存最大值

if (a > b) {
  max = a;
} else {
  max = b;
}

console.log(max); // 输出:20

三元表达式写法:

复制代码
var a = 10;
var b = 20;
var max = a > b ? a : b; // 一行代码搞定

console.log(max); // 输出:20

我们来拆解这个三元表达式:

  • 条件表达式:a > b → 10 > 20 → false;
  • 因为条件是 false,所以返回 表达式2 的值,也就是 b 的值 20;
  • 把 20 赋给 max。

是不是比 if...else 简洁太多了?这就是三元表达式的优势:简单的二选一逻辑,用一行代码就能搞定,代码非常精炼。

再举一个例子:

复制代码
// 场景:判断年龄是否成年,成年返回'成年人',未成年返回'未成年人'
var age = 18;
var result = age >= 18 ? '成年人' : '未成年人';
console.log(result); // 输出:成年人
5.4.3 三元表达式的注意事项

虽然三元表达式很简洁,但也有一些注意事项,新手要注意:

1. 三元表达式是有返回值的,必须用变量接收,或者直接使用

三元表达式会返回一个结果(表达式 1 或表达式 2 的结果),所以你必须把这个结果赋给一个变量,或者直接用在别的地方(比如直接放在 console.log() 里),不能单独写一个三元表达式在那里。

错误写法:

复制代码
var a = 10;
var b = 20;
a > b ? a : b; // 错误!单独写在这里,没有用变量接收,也没有使用,虽然不会报错,但没有任何意义

正确写法:

复制代码
var a = 10;
var b = 20;
// 写法1:用变量接收
var max = a > b ? a : b;
console.log(max);

// 写法2:直接使用
console.log(a > b ? a : b); // 直接放在console.log里
2. 不要嵌套太多层三元表达式,可读性会变差

三元表达式可以嵌套,但如果嵌套超过两层,代码的可读性会变得非常差,别人根本看不懂你在写什么,这时候不如用 if...else if...else。

反面例子(嵌套太多,可读性差):

复制代码
var score = 85;
// 嵌套三层,可读性非常差
var result = score >= 90 ? '优秀' : (score >= 80 ? '良好' : (score >= 60 ? '及格' : '不及格'));
console.log(result);

虽然上面的代码能运行,但可读性太差了,别人看半天才能看懂逻辑,这时候不如用 if...else if...else,逻辑更清晰。

正面例子(简单的两层嵌套可以接受,或者用 if):

复制代码
var score = 85;
// 推荐用if...else if...else,逻辑更清晰
if (score >= 90) {
  console.log('优秀');
} else if (score >= 80) {
  console.log('良好');
} else if (score >= 60) {
  console.log('及格');
} else {
  console.log('不及格');
}

总结 :简单的二选一逻辑,用三元表达式,代码更简洁;复杂的多分支逻辑,用 if...else,可读性更高。

5.5 switch 语句:适合多分支 "等值判断" 的场景

switch 语句,是另一种多分支的条件语句,它特别适合 "根据一个变量的不同值,执行不同代码" 的场景,也就是 "等值判断" 的场景。

比如:"根据星期几(1-7),输出对应的中文"、"根据月份(1-12),输出对应的季节",这种场景用 switch 会比 if...else if 更清晰。

5.5.1 switch 语句的语法
复制代码
switch (表达式) {
  case 值1:
    // 表达式的结果 === 值1 时,执行这里的代码
    语句1;
    break; // 跳出switch语句,非常重要!
  case 值2:
    // 表达式的结果 === 值2 时,执行这里的代码
    语句2;
    break;
  case 值3:
    语句3;
    break;
  // ... 可以有任意多个 case
  default:
    // 所有 case 都不匹配时,执行这里的代码
    语句N;
    // default 后面可以不加 break,因为它是最后一个
}
5.5.2 switch 语句的执行流程

我们一步步拆解 switch 语句的执行流程,确保你彻底搞懂:

  1. 先计算 switch 后面 表达式 的值;
  2. 从上到下,依次把这个值,和每个 case 后面的 值 做严格相等比较(===);
  3. 如果找到匹配的 case(也就是 表达式结果 === case值),就执行这个 case 后面的所有语句,直到遇到 break 为止;
  4. 如果执行过程中遇到 break,就立即跳出整个 switch 语句,后面的 case 和 default 都不会再执行;
  5. 如果所有 case 都不匹配,就执行 default 后面的语句;
  6. default 是可选的,如果没有 default,并且所有 case 都不匹配,就什么都不执行。
5.5.3 switch 语句的基础示例

我们用一个最经典的例子:"根据输入的数字(1-7),输出对应的星期几",来讲解 switch 的用法。

举个例子:

复制代码
var day = 3; // 1代表星期一,2代表星期二,...,7代表星期日

switch (day) {
  case 1:
    console.log('星期一');
    break;
  case 2:
    console.log('星期二');
    break;
  case 3:
    console.log('星期三');
    break;
  case 4:
    console.log('星期四');
    break;
  case 5:
    console.log('星期五');
    break;
  case 6:
    console.log('星期六');
    break;
  case 7:
    console.log('星期日');
    break;
  default:
    console.log('输入有误,请输入1-7的数字');
}
// 输出:星期三

我们来一步步拆解这个例子的执行流程:

  1. switch 后面的表达式是 day,值是 3;
  2. 从上到下依次比较:
    • 和 case 1 比较:3 === 1 → false,跳过;
    • 和 case 2 比较:3 === 2 → false,跳过;
    • 和 case 3 比较:3 === 3 → true,匹配成功;
  3. 执行 case 3 后面的语句:console.log('星期三');;
  4. 遇到 break,立即跳出整个 switch 语句,后面的 case 4 到 default 都不会再执行。

如果 day 的值是 8,所有 case 都不匹配,就会执行 default 后面的语句,输出 "输入有误,请输入 1-7 的数字"。

5.5.4 重要!break 的作用:不要忘记写 break!

在 switch 语句里,break 的作用是立即跳出整个 switch 语句 ,这是 switch 里最最重要的一个知识点,90% 的新手用 switch 都会在这里踩坑,就是因为忘记写 break!

如果你忘记写 break,代码会发生 "穿透" 现象:匹配到一个 case 之后,会继续执行后面所有 case 的语句,直到遇到 break 或者 switch 结束,不管后面的 case 是否匹配!

反面例子(忘记写 break,发生穿透):

复制代码
var day = 3;

switch (day) {
  case 1:
    console.log('星期一');
    // 忘记写 break!
  case 2:
    console.log('星期二');
    // 忘记写 break!
  case 3:
    console.log('星期三');
    // 忘记写 break!
  case 4:
    console.log('星期四');
    // 忘记写 break!
  case 5:
    console.log('星期五');
    break;
  default:
    console.log('输入有误');
}
// 输出:
// 星期三
// 星期四
// 星期五

我们来拆解这个错误例子的执行流程:

  1. day 是 3,匹配到 case 3,执行 console.log('星期三');;
  2. 因为没有 break,代码继续向下穿透,执行 case 4 的 console.log('星期四');;
  3. 还是没有 break,继续向下穿透,执行 case 5 的 console.log('星期五');;
  4. 遇到 break,终于跳出了。

结果输出了三行内容,这显然不是我们想要的,就是因为忘记写 break 导致的。

正面例子(每个 case 后面都加上 break):

复制代码
var day = 3;

switch (day) {
  case 1:
    console.log('星期一');
    break; // 加上 break
  case 2:
    console.log('星期二');
    break; // 加上 break
  case 3:
    console.log('星期三');
    break; // 加上 break
  case 4:
    console.log('星期四');
    break; // 加上 break
  case 5:
    console.log('星期五');
    break; // 加上 break
  case 6:
    console.log('星期六');
    break; // 加上 break
  case 7:
    console.log('星期日');
    break; // 加上 break
  default:
    console.log('输入有误');
    // default 是最后一个,可以不加 break
}
// 输出:星期三(正确)

每个 case 后面都加上 break,就不会发生穿透了,只会执行匹配到的那个 case 的代码,逻辑完全正确。

特殊场景:利用穿透实现多个 case 执行同一段代码 虽然忘记写 break 会出 bug,但在某些特殊场景下,我们可以主动利用穿透 ,让多个 case 执行同一段代码。

举个例子:

复制代码
// 场景:根据月份,输出对应的季节
// 3、4、5月:春季
// 6、7、8月:夏季
// 9、10、11月:秋季
// 12、1、2月:冬季
var month = 4;

switch (month) {
  case 3:
  case 4:
  case 5:
    console.log('春季');
    break;
  case 6:
  case 7:
  case 8:
    console.log('夏季');
    break;
  case 9:
  case 10:
  case 11:
    console.log('秋季');
    break;
  case 12:
  case 1:
  case 2:
    console.log('冬季');
    break;
  default:
    console.log('输入有误,请输入1-12的月份');
}
// 输出:春季

在这个例子里,case 3、case 4、case 5 后面都没有写 break,所以不管匹配到这三个中的哪一个,都会穿透到 case 5 后面的代码,执行 console.log('春季'),然后遇到 break 跳出。这样就实现了 "多个 case 执行同一段代码" 的需求,代码非常简洁。

这是 switch 语句的一个高级技巧,利用穿透来简化代码,非常实用。

5.5.5 switch 语句的注意事项

除了 break,switch 还有一些注意事项,新手要了解:

1. switch 的比较是严格相等(===),不是抽象相等(==)

switch 里,表达式的结果和 case 后面的值,是用 严格相等 === 来比较的,不会做隐式类型转换,这一点非常重要!

举个例子:

复制代码
var num = '3'; // 注意:这是字符串 '3',不是数字 3

switch (num) {
  case 3: // 数字 3
    console.log('匹配到数字3');
    break;
  case '3': // 字符串 '3'
    console.log('匹配到字符串3');
    break;
  default:
    console.log('没有匹配到');
}
// 输出:匹配到字符串3

在这个例子里,num 是字符串 '3',case 3 是数字 3,'3' === 3 是 false,所以不会匹配;只有 case '3' 是字符串,'3' === '3' 是 true,所以会匹配到。

如果是用 if 语句写 num == 3,会做隐式类型转换,返回 true,但 switch 不会,一定要注意!

2. default 的位置不一定要在最后

虽然我们习惯把 default 写在最后,但它的位置其实是任意的,可以写在任何 case 之间,不过为了代码的可读性,强烈建议你把 default 写在最后。

注意 :如果 default 没有写在最后,并且它后面还有 case,那么 default 后面也必须加上 break,否则也会发生穿透!

反面例子(default 写在中间,没加 break):

复制代码
var day = 8;

switch (day) {
  case 1:
    console.log('星期一');
    break;
  default:
    console.log('输入有误');
    // 没加 break!
  case 2:
    console.log('星期二');
    break;
}
// 输出:
// 输入有误
// 星期二(发生穿透了!)

因为 default 写在中间,并且没加 break,所以执行完 default 之后,会继续向下穿透,执行 case 2 的代码,这显然不是我们想要的。

总结 :为了避免麻烦,永远把 default 写在最后 ,并且 default 后面可以不加 break。

5.5.6 switch 和 if...else if 的选择

什么时候用 switch,什么时候用 if...else if?我们给你一个清晰的判断标准:

  • 如果是 "等值判断" (根据一个变量的不同值,执行不同代码),并且值的数量比较多(超过 3 个),推荐用 switch,代码更清晰,比如 "根据星期几输出中文"、"根据月份输出季节";
  • 如果是 "范围判断" (比如大于、小于、在某个区间),或者逻辑比较复杂,必须用 if...else if,因为 switch 只能做等值判断,不能做范围判断,比如 "根据分数输出等级"、"根据年龄判断是否成年"。

六、循环语句:让代码 "重复执行"

循环语句,就是让代码 "重复执行某一段代码块" 的语句,是编程里非常重要的语句,比如 "打印 1-100 的所有数字"、"计算 1-100 的和"、"遍历数组里的所有元素",都需要用循环语句。

JavaScript 里的循环语句有三种:while 循环、do...while 循环、for 循环,我们一个一个讲透,每个都讲清楚 "是什么、怎么用、有什么区别、什么时候用"。

在讲循环语句之前,我们先搞懂两个核心概念:循环变量 和 循环的三要素。

6.1 循环的核心概念:循环变量和循环三要素

所有的循环语句,都有这两个核心概念,理解了它们,你就能理解所有的循环。

6.1.1 循环变量

循环变量,就是用来 "控制循环次数" 的变量,它会在循环过程中不断变化,直到满足循环结束的条件。

比如我们要 "打印 1-10 的数字",就可以用一个变量 i,从 1 开始,每次加 1,直到 10 结束,这个 i 就是循环变量。

6.1.2 循环的三要素

所有的循环,都必须包含这三个要素,缺一不可,否则就会出问题(比如死循环):

  1. 循环变量的初始化 :循环开始前,给循环变量一个初始值(比如 var i = 1);
  2. 循环条件 :判断循环是否继续执行的条件(比如 i <= 10),条件为 true 继续循环,条件为 false 结束循环;
  3. 循环变量的更新 :每次循环结束后,更新循环变量的值(比如 i++),让循环条件最终能变成 false,结束循环。

如果缺少了 "循环变量的更新",循环条件永远是 true,循环会永远执行下去,这就是死循环,会让浏览器卡死,一定要避免!

6.2 while 循环:最基础的循环

while 循环是最基础的循环语句,它的特点是 "先判断条件,再执行循环体"。

6.2.1 while 循环的语法
复制代码
// 1. 循环变量的初始化(写在while外面)
var 循环变量 = 初始值;

while (循环条件) {
  // 2. 循环体:要重复执行的代码
  // ...
  
  // 3. 循环变量的更新(写在循环体里面,最后面)
  循环变量的更新;
}
6.2.2 while 循环的执行流程

我们一步步拆解 while 循环的执行流程,确保你彻底搞懂:

  1. 先执行循环变量的初始化(这一步只在循环开始前执行一次);
  2. 计算循环条件的值,转成布尔值;
  3. 如果是 true,执行大括号里的循环体代码;
  4. 执行完循环体,执行循环变量的更新;
  5. 回到步骤 2,再次计算循环条件的值;
  6. 如果是 false,结束整个循环,执行循环后面的代码。
6.2.3 while 循环的基础示例

我们用一个最经典的例子:"打印 1-10 的所有数字",来讲解 while 循环的用法。

举个例子:

复制代码
// 1. 循环变量的初始化:i从1开始
var i = 1;

// 2. 循环条件:i <= 10 时继续循环
while (i <= 10) {
  // 循环体:打印当前的i
  console.log(i);
  
  // 3. 循环变量的更新:i每次加1
  i++;
}
// 输出:
// 1
// 2
// 3
// ...
// 10

我们来一步步拆解这个例子的执行流程(前几次循环):

  1. 初始化 :i = 1;
  2. 第一次判断条件 :i <= 10 → 1 <= 10 → true;
  3. 执行循环体 :console.log(1) → 输出 1;
  4. 更新循环变量 :i++ → i = 2;
  5. 第二次判断条件 :2 <= 10 → true;
  6. 执行循环体 :console.log(2) → 输出 2;
  7. 更新循环变量 :i++ → i = 3;
  8. ...... 以此类推;
  9. 直到 i = 11 时,判断条件 :11 <= 10 → false,结束循环。

完美!成功打印了 1-10 的所有数字。

6.2.4 重要!不要忘记更新循环变量!

在 while 循环里,循环变量的更新必须写在循环体里面,并且一定要写 !如果忘记更新循环变量,循环条件永远是 true,循环会永远执行下去,这就是死循环,会让浏览器卡死,电脑风扇狂转,一定要避免!

反面例子(忘记更新循环变量,死循环):

复制代码
var i = 1;

while (i <= 10) {
  console.log(i);
  // 忘记写 i++!!
}
// 死循环!永远输出 1,浏览器会卡死!

在这个例子里,i 的值永远是 1,i <= 10 永远是 true,循环会永远执行下去,永远输出 1,浏览器会卡死,一定要避免!

正面例子(记得更新循环变量):

复制代码
var i = 1;

while (i <= 10) {
  console.log(i);
  i++; // 记得更新循环变量!
}
// 正常执行,输出1-10
6.2.5 while 循环的进阶示例:计算 1-100 的和

我们再做一个进阶练习:"计算 1-100 所有数字的和",也就是 1 + 2 + 3 + ... + 100,结果是 5050。

思路分析:

  1. 需要一个变量 sum 来存总和,初始值是 0(因为加法的单位元是 0,0 加任何数都等于那个数);
  2. 循环变量 i 从 1 开始,到 100 结束;
  3. 每次循环,把 i 加到 sum 里(sum += i);
  4. 更新 i。

代码示例:

复制代码
// 1. 初始化循环变量 i,和总和 sum
var i = 1;
var sum = 0;

while (i <= 100) {
  // 把当前的 i 加到 sum 里
  sum += i; // 等价于 sum = sum + i;
  
  // 更新循环变量
  i++;
}

console.log('1-100的和是:' + sum);
// 输出:1-100的和是:5050

我们来拆解前几次循环的执行流程:

  1. 初始化:i = 1,sum = 0;
  2. 第一次循环:
    • 条件:1 <= 100 → true;
    • sum += i → sum = 0 + 1 = 1;
    • i++ → i = 2;
  3. 第二次循环:
    • 条件:2 <= 100 → true;
    • sum += i → sum = 1 + 2 = 3;
    • i++ → i = 3;
  4. 第三次循环:
    • 条件:3 <= 100 → true;
    • sum += i → sum = 3 + 3 = 6;
    • i++ → i = 4;
  5. ...... 以此类推;
  6. 最后一次循环(i=100):
    • 条件:100 <= 100 → true;
    • sum += i → sum = 4950 + 100 = 5050;
    • i++ → i = 101;
  7. 条件:101 <= 100 → false,结束循环;
  8. 输出 sum:5050。

完美!成功计算出了 1-100 的和。

6.3 break 和 continue:控制循环的流程

break 和 continue 是用来 "控制循环流程" 的关键字,它们可以让循环提前结束,或者跳过某一次循环,我们一个一个讲透。

6.3.1 break:立即结束整个循环

break 关键字,我们在 switch 语句里已经见过了,它在循环里的作用是:立即结束整个循环 ,不管循环条件是不是 true,只要遇到 break,循环就立刻停止,执行循环后面的代码。

我们用一个生活里的例子理解:你要吃 5 个李子,吃到第 3 个的时候,发现里面有半个虫子,你不想吃了,直接把剩下的都扔掉,结束吃李子的过程 ------ 这就是 break。

举个例子:

复制代码
// 场景:吃5个李子,吃到第3个就不吃了
var i = 1;

while (i <= 5) {
  if (i === 3) {
    // 吃到第3个了,遇到break,结束整个循环
    break;
  }
  console.log('我在吃第' + i + '个李子');
  i++;
}
console.log('不吃了');
// 输出:
// 我在吃第1个李子
// 我在吃第2个李子
// 不吃了

我们来拆解执行流程:

  1. i = 1:条件 true,i !== 3,输出 "我在吃第 1 个李子",i++ → 2;
  2. i = 2:条件 true,i !== 3,输出 "我在吃第 2 个李子",i++ → 3;
  3. i = 3:条件 true,i === 3,执行 break,立即结束整个循环 ,后面的 console.log 和 i++ 都不会执行;
  4. 执行循环后面的代码:输出 "不吃了"。

注意:在这个例子里,i 到 3 的时候,遇到了 break,循环直接结束了,所以第 3、4、5 个李子都没吃,并且 i++ 也没执行,i 的值还是 3。

进阶示例:找到 100-200 之间第一个 3 的倍数 需求:遍历 100-200 的数字,找到第一个是 3 的倍数的数字,打印出来,然后结束循环,不用找后面的了。

代码示例:

复制代码
var i = 100;

while (i <= 200) {
  if (i % 3 === 0) {
    // 找到了第一个3的倍数,打印出来,然后break结束循环
    console.log('找到了第一个3的倍数:' + i);
    break;
  }
  i++;
}
// 输出:找到了第一个3的倍数:102

100 除以 3 余 1,不是;101 除以 3 余 2,不是;102 除以 3 余 0,是,所以打印出来,然后 break 结束循环,后面的 103-200 都不会再找了,效率很高。

6.3.2 continue:跳过本次循环,继续下一次

continue 关键字的作用是:跳过本次循环剩下的代码,直接进入下一次循环(循环条件还是会判断的,循环变量的更新也别忘了)。

我们用生活里的例子理解:你要吃 5 个李子,吃到第 3 个的时候,发现里面有一只虫子,你把这个扔掉,继续吃第 4 个、第 5 个 ------ 这就是 continue,只跳过这一个,后面的继续。

举个例子:

复制代码
// 场景:吃5个李子,第3个有虫子,扔掉,继续吃后面的
var i = 1;

while (i <= 5) {
  if (i === 3) {
    // 第3个有虫子,跳过这次循环,直接进入下一次
    // 注意:continue之前一定要更新循环变量!否则会死循环!
    i++;
    continue;
  }
  console.log('我在吃第' + i + '个李子');
  i++;
}
// 输出:
// 我在吃第1个李子
// 我在吃第2个李子
// 我在吃第4个李子
// 我在吃第5个李子

我们来拆解执行流程:

  1. i = 1:条件 true,i !== 3,输出 "我在吃第 1 个李子",i++ → 2;
  2. i = 2:条件 true,i !== 3,输出 "我在吃第 2 个李子",i++ → 3;
  3. i = 3:条件 true,i === 3,执行 i++ → 4,然后执行 continue,跳过本次循环剩下的代码 (也就是 console.log),直接进入下一次循环;
  4. i = 4:条件 true,i !== 3,输出 "我在吃第 4 个李子",i++ → 5;
  5. i = 5:条件 true,i !== 3,输出 "我在吃第 5 个李子",i++ → 6;
  6. i = 6:条件 false,结束循环。

完美!第 3 个被跳过了,其他的都吃了。

重要!在 while 循环里用 continue,一定要在 continue 之前更新循环变量! 这是新手最容易踩的坑:在 while 循环里用 continue,如果 continue 之前没有更新循环变量,那么循环变量永远不会变,条件永远是 true,就会变成死循环!

反面例子(continue 之前没更新循环变量,死循环):

复制代码
var i = 1;

while (i <= 5) {
  if (i === 3) {
    // 错误!continue 之前没更新 i!
    continue;
  }
  console.log('我在吃第' + i + '个李子');
  i++;
}
// 死循环!i 永远是 3,永远卡在 continue 这里!

在这个例子里,i = 3 的时候,执行 continue,跳过了后面的 i++,所以 i 的值永远是 3,条件永远是 true,死循环了,一定要避免!

正面例子(continue 之前先更新循环变量):

复制代码
var i = 1;

while (i <= 5) {
  if (i === 3) {
    // 正确!continue 之前先更新 i!
    i++;
    continue;
  }
  console.log('我在吃第' + i + '个李子');
  i++;
}
// 正常执行

或者,你也可以把循环变量的更新放在 if 之前,这样就不用担心了,但这种写法比较少见,还是推荐上面的写法。

进阶示例:找到 100-200 之间所有 3 的倍数 需求:遍历 100-200 的数字,找到所有是 3 的倍数的数字,打印出来,不是 3 的倍数的就跳过。

代码示例:

复制代码
var i = 100;

while (i <= 200) {
  if (i % 3 !== 0) {
    // 不是3的倍数,跳过这次循环,继续下一次
    i++;
    continue;
  }
  // 是3的倍数,打印出来
  console.log('找到了3的倍数:' + i);
  i++;
}
// 输出:
// 找到了3的倍数:102
// 找到了3的倍数:105
// ...
// 找到了3的倍数:198

这个例子里,不是 3 的倍数的数字,就执行 continue 跳过,不打印;是 3 的倍数的,就打印出来,完美实现了需求。

6.3.3 break 和 continue 的区别

我们用一个表格对比 break 和 continue,让你一眼就能看清区别:

关键字 作用 对循环的影响
break 立即结束整个循环 循环彻底停止,执行循环后面的代码
continue 跳过本次循环剩下的代码 循环不停止,直接进入下一次循环

6.4 do...while 循环:至少执行一次的循环

do...while 循环是 while 循环的一个变体,它的特点是:先执行一次循环体,再判断条件 。也就是说,循环体至少会执行一次,哪怕条件一开始就是 false。

6.4.1 do...while 循环的语法
复制代码
// 1. 循环变量的初始化
var 循环变量 = 初始值;

do {
  // 2. 循环体:至少会执行一次
  // ...
  
  // 3. 循环变量的更新
  循环变量的更新;
} while (循环条件); // 注意:这里的分号不能省略!
6.4.2 do...while 循环的执行流程
  1. 先执行循环变量的初始化(一次);
  2. 直接执行循环体代码(第一次循环,不判断条件!);
  3. 执行循环变量的更新;
  4. 计算循环条件的值;
  5. 如果是 true,再次执行循环体;
  6. 如果是 false,结束循环。

核心区别 :while 是 "先判断,后执行";do...while 是 "先执行,后判断",所以 do...while 的循环体至少会执行一次。

6.4.3 do...while 循环的基础示例

我们用一个例子对比 while 和 do...while,让你看清它们的区别。

示例 1:while 循环,条件一开始就是 false

复制代码
var i = 11;

while (i <= 10) {
  console.log('while 循环的循环体执行了');
  i++;
}
console.log('while 循环结束');
// 输出:
// while 循环结束
// (循环体一次都没执行!)

因为 i 一开始就是 11,i <= 10 是 false,所以 while 循环的循环体一次都没执行,直接结束了。

示例 2:do...while 循环,条件一开始就是 false

复制代码
var i = 11;

do {
  console.log('do...while 循环的循环体执行了');
  i++;
} while (i <= 10);
console.log('do...while 循环结束');
// 输出:
// do...while 循环的循环体执行了
// do...while 循环结束
// (循环体执行了一次!)

虽然 i 一开始就是 11,i <= 10 是 false,但 do...while 是 "先执行,后判断",所以循环体还是执行了一次,然后才判断条件,结束循环。

这就是 do...while 最大的特点:循环体至少执行一次。

6.4.4 do...while 循环的实际应用场景

do...while 循环适合 "需要先执行一次,再根据结果判断是否继续" 的场景,比如 "让用户输入密码,输入正确就结束,输入错误就继续让用户输入"。

举个例子:

复制代码
// 场景:用户输入密码,正确密码是'123456',输入正确就结束,错误就继续
var password;

do {
  // 先让用户输入一次
  password = prompt('请输入密码:');
} while (password !== '123456'); // 如果输入的不是正确密码,继续循环

alert('密码正确,登录成功!');

这个例子里,我们需要先让用户输入一次密码,然后再判断是否正确,如果不正确就继续输入,这时候用 do...while 就非常合适,因为至少要让用户输入一次。

如果用 while 循环写,就需要先给 password 一个初始值,代码会稍微啰嗦一点,不如 do...while 直观。

6.4.5 do...while 循环的注意事项
  1. while 后面的分号不能省略! 在 do...while 循环里,while (循环条件) 后面必须加一个分号 ;,这是语法规定,虽然有些浏览器不加也能运行,但为了规范,必须加上!
  2. 同样要注意循环变量的更新,避免死循环;
  3. 同样可以用 break 和 continue,规则和 while 一样。

6.5 for 循环:最常用、最灵活的循环

for 循环是 JavaScript 里最常用、最灵活的循环语句,它把 "循环变量的初始化"、"循环条件"、"循环变量的更新" 这三要素,都写在 for 后面的括号里,结构非常清晰,特别适合 "知道循环次数" 的场景(比如 "循环 10 次"、"遍历 1-100")。

6.5.1 for 循环的语法
复制代码
for (表达式1:循环变量初始化; 表达式2:循环条件; 表达式3:循环变量更新) {
  // 循环体:要重复执行的代码
}

三个表达式的作用:

  1. 表达式 1 :循环变量的初始化,只在循环开始前执行一次;
  2. 表达式 2:循环条件,每次循环前都执行,判断是否继续循环;
  3. 表达式 3:循环变量的更新,每次循环体执行完后执行。
6.5.2 for 循环的执行流程

我们一步步拆解 for 循环的执行流程,这是最重要的部分,一定要彻底搞懂:

  1. 执行表达式 1(循环变量初始化):只在循环开始前执行一次;
  2. 执行表达式 2(循环条件):计算值,转成布尔值;
  3. 如果是 true,执行大括号里的循环体;
  4. 执行完循环体,执行表达式 3(循环变量更新);
  5. 回到步骤 2,再次执行表达式 2;
  6. 如果是 false,结束循环。
6.5.3 for 循环的基础示例

我们还是用最经典的例子:"打印 1-10 的所有数字",来对比 for 循环和 while 循环的写法。

while 循环写法:

复制代码
// 1. 初始化(写在外面)
var i = 1;
// 2. 循环条件(写在while后面)
while (i <= 10) {
  console.log(i);
  // 3. 更新(写在循环体里)
  i++;
}

for 循环写法:

复制代码
// 1. 初始化; 2. 条件; 3. 更新(都写在for后面的括号里)
for (var i = 1; i <= 10; i++) {
  console.log(i);
}
// 输出:1 2 3 ... 10

是不是比 while 循环简洁太多了?三个要素都写在一行里,结构非常清晰,一眼就能看清循环的初始值、条件、更新方式,这就是 for 循环的优势。

我们来一步步拆解这个 for 循环的执行流程:

  1. 执行表达式 1 :var i = 1(只执行一次);
  2. 执行表达式 2 :i <= 10 → 1 <= 10 → true;
  3. 执行循环体 :console.log(1);
  4. 执行表达式 3 :i++ → i = 2;
  5. 回到步骤 2 :执行表达式 2 → 2 <= 10 → true;
  6. 执行循环体 :console.log(2);
  7. 执行表达式 3 :i++ → i = 3;
  8. ...... 以此类推;
  9. 直到 i = 11,执行表达式 2 :11 <= 10 → false,结束循环。

完美!和 while 循环的结果一样,但代码更简洁,结构更清晰。

6.5.4 for 循环的进阶示例:计算 1-100 的和

我们再用 for 循环实现 "计算 1-100 的和",对比一下 while 的写法。

for 循环写法:

复制代码
var sum = 0;

// 循环变量 i 从1开始,到100结束,每次加1
for (var i = 1; i <= 100; i++) {
  sum += i;
}

console.log('1-100的和是:' + sum);
// 输出:5050

是不是比 while 循环简洁多了?所有循环相关的代码都写在 for 后面的括号里,循环体里只需要写核心逻辑 sum += i,代码非常精炼。

6.5.5 for 循环的注意事项
  1. 三个表达式都是可选的,但分号不能省略 for 循环的三个表达式都是可选的,你可以根据需求省略其中的一个或多个,但两个分号 ; 绝对不能省略!

    举个例子,省略表达式 1(初始化):

    复制代码
    var i = 1; // 初始化写在外面
    for (; i <= 10; i++) { // 表达式1省略了,但分号不能省
      console.log(i);
    }

    举个例子,省略表达式 3(更新):

    复制代码
    for (var i = 1; i <= 10;) { // 表达式3省略了,分号不能省
      console.log(i);
      i++; // 更新写在循环体里
    }

    举个例子,三个表达式都省略(死循环):

    复制代码
    for (;;) { // 两个分号不能省
      console.log('死循环');
    }

    这种写法是死循环,和 while (true) 一样,除非有 break,否则永远执行。

    虽然可以省略,但强烈建议你不要省略,把三个表达式都写全,代码结构最清晰,最不容易出 bug。

  2. 循环变量可以用 let 声明(推荐) 我们后面讲作用域的时候会说,用 let 声明循环变量,比用 var 更好,避免变量污染,推荐用 let。

    推荐写法:

    复制代码
    // 用 let 声明循环变量 i
    for (let i = 1; i <= 10; i++) {
      console.log(i);
    }
  3. for 循环里同样可以用 break 和 continue 规则和 while 循环完全一样,这里就不再重复了。

6.5.6 for 循环和 while 循环的选择

什么时候用 for,什么时候用 while?判断标准很清晰:

  • 如果知道循环的次数 (比如 "循环 10 次"、"遍历 1-100"),优先用 for 循环,结构更清晰;
  • 如果不知道循环的次数 (比如 "一直输入,直到输入正确为止"),用 while 循环 或者 do...while 循环。

6.6 循环的嵌套:循环里面还有循环

循环的嵌套,就是在一个循环的循环体里,再写另一个循环,外面的叫 "外层循环",里面的叫 "内层循环"。

循环嵌套在实际开发中也很常用,比如 "打印九九乘法表"、"打印二维图形(矩形、三角形)",都需要用循环嵌套。

6.6.1 循环嵌套的基础示例:打印矩形

我们用一个简单的例子:"打印一个 5 行 5 列的星号矩形",来讲解循环嵌套的用法。

需求:打印一个这样的矩形:

复制代码
*****
*****
*****
*****
*****

思路分析:

  1. 外层循环:控制 "行数",一共 5 行,所以外层循环 i 从 1 到 5;
  2. 内层循环:控制 "每一行的列数",每一行有 5 个星号,所以内层循环 j 从 1 到 5;
  3. 内层循环的循环体:打印一个星号(注意:不要换行);
  4. 内层循环结束后(也就是每一行打印完 5 个星号后),打印一个换行符 \n,让下一行从新的一行开始。

代码示例:

复制代码
// 外层循环:控制行数,5行
for (var i = 1; i <= 5; i++) {
  // 内层循环:控制每一行的列数,5列
  for (var j = 1; j <= 5; j++) {
    // 打印一个星号,不换行
    process.stdout.write('*'); // 注意:这里用 process.stdout.write 代替 console.log,因为 console.log 会自动换行
  }
  // 内层循环结束,打印一个换行符
  console.log();
}

注意 :在 Node.js 环境里,console.log() 会自动在结尾加一个换行符,所以我们用 process.stdout.write() 来打印星号,它不会自动换行;如果是在浏览器环境里,可以用 document.write() 或者拼接字符串的方式。

我们来拆解执行流程:

  1. 外层循环 i=1(第一行) :
    • 内层循环 j=1 到 5:打印 5 个星号 *****;
    • 内层循环结束,打印换行符;
  2. 外层循环 i=2(第二行) :
    • 内层循环 j=1 到 5:打印 5 个星号 *****;
    • 内层循环结束,打印换行符;
  3. ...... 以此类推;
  4. 外层循环 i=5(第五行)结束,整个循环结束。

完美!成功打印了一个 5 行 5 列的矩形。

6.6.2 循环嵌套的进阶示例:打印九九乘法表

我们再做一个经典的进阶练习:"打印九九乘法表"。

需求:打印这样的九九乘法表:

复制代码
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
...
1*9=9 ... 9*9=81

思路分析:

  1. 外层循环:控制 "行数",也就是 "第二个乘数",从 1 到 9;
  2. 内层循环:控制 "每一行的列数",也就是 "第一个乘数",从 1 到 当前的行数(i);
  3. 内层循环的循环体:打印 j*i=结果,后面加一个制表符 \t 对齐;
  4. 内层循环结束后,打印一个换行符。

代码示例:

复制代码
// 外层循环:i 是第二个乘数,从1到9
for (var i = 1; i <= 9; i++) {
  // 内层循环:j 是第一个乘数,从1到i
  for (var j = 1; j <= i; j++) {
    // 打印 j*i=结果,加制表符对齐
    process.stdout.write(j + '*' + i + '=' + (j * i) + '\t');
  }
  // 每一行结束,打印换行
  console.log();
}

完美!成功打印了九九乘法表。循环嵌套的核心就是:外层循环控制 "行",内层循环控制 "列",理解了这一点,所有的循环嵌套问题都能迎刃而解。

结语

恭喜你!终于看完了这篇超详细的 JavaScript 基础语法博客!我们来总结一下这篇博客讲了哪些核心内容:

变量

  • 变量的本质:带名字的收纳盒;
  • 变量的创建:声明 + 初始化;
  • 变量的语法规则:英文标点、分号、命名规则、小驼峰;
  • var 和 let 的使用;
  • 动态类型:运行时确定类型,类型可以改变;
  • typeof 运算符:检测类型。

基本数据类型

  • number:所有数字,特殊值 Infinity、-Infinity、NaN,0.1+0.2 的坑;
  • string:单双引号、转义字符、length、拼接;
  • boolean:true 和 false;
  • undefined:只声明不赋值;
  • null:主动赋值的空值;
  • falsy 值和 truthy 值。

运算符

  • 算术运算符:+、-、*、/、%;
  • 赋值运算符:=、+=、-= 等;
  • 自增自减:++、--,前置和后置的区别;
  • 比较运算符:<、>、<=、>=,== 和 === 的核心区别(推荐用 ===);
  • 逻辑运算符:&&、||、!,短路求值;
  • 位运算和移位运算:了解即可。

条件语句

  • if 语句:单分支、双分支、多分支;
  • falsy 和 truthy 在条件里的应用;
  • 三元表达式:if...else 的简化写法;
  • switch 语句:等值判断,break 的作用,穿透的利用。

循环语句

  • 循环的三要素:初始化、条件、更新;
  • while 循环:先判断,后执行;
  • do...while 循环:先执行,后判断,至少执行一次;
  • for 循环:最常用,结构清晰;
  • break:结束整个循环;
  • continue:跳过本次循环;
  • 循环的嵌套:外层控制行,内层控制列。

数组

  • 数组的概念:带编号的收纳柜;
  • 创建数组:推荐用字面量 [];
  • 数组的特点:类型可以不同,长度可以动态改变;
  • 数组的下标:从 0 开始!
  • 获取和修改元素:数组名[下标];
  • length 属性:获取长度,获取最后一个元素,截断数组;
  • 遍历数组:用 for 循环,i 从 0 开始;
  • 不要给数组名直接赋值!

这篇博客的内容非常多,每一个知识点都讲得非常详细,建议你多看几遍,并且把每一个例子都自己动手敲一遍,运行一下,修改一下,只有自己动手,才能真正理解!

学习编程没有捷径,多写代码、多调试、多思考,是最好的方法!祝你在 JavaScript 的学习道路上越走越远!

相关推荐
月夏1 小时前
Vue3 转 React:18 项对照表 + 5 个必踩的坑(附 7 天上手路线)
前端
广州华水科技1 小时前
单北斗GNSS位移监测系统的应用与维护解析
前端
救救孩子把2 小时前
AI面试八股文-13_12-AI 框架与运维面试题
运维·人工智能·面试
普通人62 小时前
php+html+canvas实现树状打开的结构
开发语言·javascript·html5·1024程序员节
梦曦i2 小时前
uni-storage 0.1.0 发布:TTL过期管理+多实例隔离
前端·storage·uni-app x
电商API_180079052472 小时前
反向海淘多币种结算:汇率该在哪个时点被冻结
java·开发语言·数据库·跨境电商·1688 api
救救孩子把2 小时前
AI面试八股文-14_14-MCP-Skill-Function Calling
人工智能·安全·面试·职场和发展·推理·mcp·functioncall
开开心心就好3 小时前
多份文档怎么批量打印?支持多种格式
java·服务器·开发语言·pdf·ocr·散列表·启发式算法
Want5953 小时前
Python炫酷烟花①
开发语言·python