真值(Truthy)和假值(Falsy)
以下8个值会被强制转换为 false:
// 1. false 布尔值
false
// 2. 0 数字零
0
-0 // 负零也是假值
// 3. 空字符串
""
''
`` // 模板字符串
// 4. null
null
// 5. undefined
undefined
// 6. NaN
NaN
// 7. 0n BigInt零
0n
// 8. document.all (只有浏览器环境,历史遗留问题)
document.all
除了上述8个假值外,所有其他值都是真值
// 1. 数字(非零)
1, -1, 3.14, Infinity, -Infinity
// 2. 字符串(非空)
"0" // 注意:字符串"0"是真值
"false" // 字符串"false"是真值
" " // 空格字符串是真值
"null" // 字符串"null"是真值
"undefined" // 字符串"undefined"是真值
// 3. 对象(任何对象,包括空对象)
{} // 空对象
[] // 空数组
new Date() // 日期对象
/正则/ // 正则表达式
// 4. 函数
function() {}
() => {}
// 5. Symbol
Symbol()
Symbol("description")
// 6. BigInt(非零)
1n, -1n, 100n
容易混淆的例子
// 这些是假值
Boolean("") // false
Boolean(0) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
// 这些是真值(常见误区)
Boolean("0") // true ⭐ 字符串"0"是真值
Boolean("false") // true ⭐ 字符串"false"是真值
Boolean([]) // true ⭐ 空数组是真值
Boolean({}) // true ⭐ 空对象是真值
Boolean(" ") // true ⭐ 空格字符串是真值
Boolean(-1) // true
|| 返回真值的值
|| 运算符在JavaScript中的行为是:
- 返回第一个真值(truthy)的表达式的值,也就是返回的是原始值
- 不会把结果转换成布尔值
// 对比
"hello" || "world" // 返回 "hello"(字符串)
0 || "world" // 返回 "world"(字符串)
"" || "world" // 返回 "world"(字符串)
null || "world" // 返回 "world"(字符串)
undefined || "world" // 返回 "world"(字符串)
// 不会返回 true/false
&& 返回假值的值
&& :从左到右计算,返回第一个假值,如果所有都是真值,返回最后一个值
// 场景1:遇到假值,返回该假值
"hello" && "" && "world" // 返回 ""(空字符串)
"hello" && 0 && "world" // 返回 0
"hello" && null && "world" // 返回 null
// 场景2:全部是真值,返回最后一个
"hello" && "world" && "!" // 返回 "!"
1 && 2 && 3 // 返回 3
true && "yes" && 100 // 返回 100
安全取值(可选链操作符出现前的写法)
// 旧写法
const name = user && user.profile && user.profile.name;
// 如果user是null/undefined,返回null
// 如果user.profile是null/undefined,返回null
// 否则返回user.profile.name
// 现代写法(可选链)
const name = user?.profile?.name;
可选链操作符 ?.
**?. 的作用:**安全地访问嵌套对象的属性,如果中间的某个属性不存在,不会报错,而是返回 undefined。
// 传统写法(容易报错)
props.info.goods_banner_img
// 如果 props.info 是 null 或 undefined,会报错:
// TypeError: Cannot read property 'goods_banner_img' of null
// 传统安全写法(繁琐)
props.info && props.info.goods_banner_img
// 需要手动检查每一层
// 可选链写法(简洁安全)
props.info?.goods_banner_img
// 如果 props.info 是 null/undefined,直接返回 undefined,不会报错
多层嵌套访问
// 假设有这样的数据
const props = {
info: {
goods_banner_img: "image.jpg"
}
}
// 正常情况
props.info?.goods_banner_img // 返回 "image.jpg"
// 异常情况
const props2 = {}
props2.info?.goods_banner_img // 返回 undefined(不会报错)
const props3 = { info: null }
props3.info?.goods_banner_img // 返回 undefined(不会报错)
与数组结合
// 访问数组元素
props.info?.goods_banner_imgs?.[0]
// 相当于:
// 1. 先检查 props.info 是否存在
// 2. 再检查 props.info.goods_banner_imgs 是否存在
// 3. 最后尝试取第一个元素
// 任何一步不存在都返回 undefined
对象解构
const user = {
name: "Tom",
age: 20
}
const {name, age} = user
console.log(name)
等价于:
const name = user.name
const age = user.age
扩展运算符...
扩展运算符(Spread Operator)...的作用:将一个对象或数组的所有可枚举属性”展开”到新的对象或数组中。
对象的展开
// 原始对象(打包的状态)
const person = {
name: "张三",
age: 25,
city: "北京"
}
// 把这个对象"展开"
console.log(...person) // 这不能直接运行,但想象一下:
// 展开后就是:
name: "张三",
age: 25,
city: "北京"
// 变成了三个独立的键值对
// 实际使用:放入新对象
const newPerson = {
job: "程序员",
...person // 这里就是展开操作
}
// 等价于手动写:
const newPerson = {
job: "程序员",
name: "张三",
age: 25,
city: "北京"
}
数组的展开
// 原始数组
const arr = [1, 2, 3, 4, 5]
// 把这个数组"展开"
console.log(...arr) // 输出: 1 2 3 4 5
// 数组的方括号被去掉了,元素被一个个拿出来
// 实际使用:放入新数组
const newArr = [0, ...arr, 6]
// 等价于:
const newArr = [0, 1, 2, 3, 4, 5, 6]
使用场景
合并对象
const obj1 = { a: 1, b: 2 }
const obj2 = { c: 3, d: 4 }
const merged = { ...obj1, ...obj2 } // { a: 1, b: 2, c: 3, d: 4 }
复制对象
const original = { a: 1, b: 2 }
const copy = { ...original } // { a: 1, b: 2 },浅拷贝
覆盖属性
const user = { name: "张三", age: 20 }
const updated = { ...user, age: 21 } // { name: "张三", age: 21 }
// 后面的属性会覆盖前面的
注意事项
属性覆盖顺序
return {
_bannerImg: "a.jpg",
...props.info,
_skuInfo: skuData
}
// 如果props.info中也有_bannerImg,会被前面的_bannerImg覆盖吗?
// 不会!后面的会覆盖前面的
// 实际顺序决定覆盖关系
{
...props.info, // 先展开
_bannerImg: "a.jpg" // 后定义的会覆盖先定义的
}
浅拷贝
const obj = {
a: 1,
b: { c: 2 }
}
const copy = { ...obj }
copy.b.c = 3 // 会影响原对象的b.c,因为只浅拷贝了一层
性能考虑
// 如果props.info很大,每次计算属性都会创建新对象
return {
_bannerImg,
_skuInfo,
...props.info // 会复制所有属性
}
剩余运算符 ...
剩余运算符用三个点 ... 表示,它的核心作用是将剩余的元素收集到一个数组或对象中。
数组中的剩余运算符
// 收集剩余的元素到数组中
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5] ← 剩余的元素被收集到数组中
对象中的剩余运算符
// 收集剩余的属性到对象中
const person = {
name: "张三",
age: 25,
city: "北京",
job: "工程师",
hobby: "读书"
};
const {name, age, ...otherInfo} = person;
console.log(name); // "张三"
console.log(age); // 25
console.log(otherInfo); // {city: "北京", job: "工程师", hobby: "读书"}
作用域
三种作用域
全局作用域(Global Scope):在程序最外层声明的变量就是 全局变量。
- 在任何地方都可以访问
- 生命周期 = 页面存在期间
- 浏览器环境会挂在
window上(var)
函数作用域(Function Scope):函数内部形成一个独立作用域。
- 外部不能访问
- 内部可以访问外部变量
块级作用域(Block Scope):ES6 引入,由 {} 形成:
- if
- for
- while
- switch
- 单独代码块
let globalVar = "我是全局变量"; // 全局作用域
function test() {
var functionVar = "我是函数变量"; // 函数作用域
if (true) {
let blockVar = "我是块级变量"; // 块级作用域
console.log(globalVar); // 可以访问
console.log(functionVar); // 可以访问
console.log(blockVar); // 可以访问
}
console.log(globalVar); // 可以访问
console.log(functionVar); // 可以访问
console.log(blockVar); // ❌ 报错
}
test();
console.log(globalVar); // 可以访问
console.log(functionVar); // ❌ 报错
console.log(blockVar); // ❌ 报错
全局作用域
│
├── globalVar
│
└── test() 函数作用域
│
├── functionVar
│
└── if 块级作用域
│
└── blockVar
闭包(Closure)
什么是闭包
闭包 = 函数 + 它创建时所在的作用域环境
函数离开了原来的作用域,但仍然“记住”那个作用域里的变量。
普通函数
function outer() {
let count = 0
console.log(count)
}
outer()
执行流程:
调用 outer()
↓
创建变量 count
↓
打印
↓
函数结束
↓
count 被销毁
函数结束后:count 不存在了
因为它只是函数内部变量。
闭包出现的关键:函数被返回
function outer(){
let count = 0
return function(){
count++
console.log(count)
}
}
const fn = outer()
fn()
fn()
第一步:const fn = outer()
outer 执行
↓
创建变量 count = 0
↓
创建一个内部函数
↓
把这个函数返回
此时内存可以理解为:
fn ─────► function(){
count++
console.log(count)
}
count = 0
关键点:内部函数仍然引用 count。
第二步:fn()
发生:
count++
count = 1
输出:1
第三步: fn()
count 原来是 1:count++ —> count = 2
为什么 count 没消失
正常情况下:outer 执行完,count 应该被销毁
但现在不会,因为内部函数还在使用 count
所以 JavaScript 引擎会保留 outer 的作用域
这个被保留下来的作用域就叫:闭包
闭包的影响
正面影响(优点)
1️⃣ 实现“私有变量”。闭包最常见用途是 隐藏变量。
2️⃣ 让变量长期存在。正常函数执行完变量会销毁,但闭包可以让变量 持续存在。
3️⃣ 保存状态(State)。闭包可以保存函数状态。
function counter(){
let num = 0
return function(){
num++
console.log(num)
}
}
const add = counter()
add() //1
add() //2
add() //3
就是一个 持久状态。这在事件处理、缓存、防抖、节流中非常常见。
负面影响(缺点)
1️⃣ 可能造成内存泄漏。因为闭包会 保留变量引用。如果不释放,就会一直占用内存。
2️⃣ 不容易调试。闭包会形成 复杂作用域链。
3️⃣ 容易写出 bug
for(var i=0;i<3;i++){
setTimeout(function(){
console.log(i)
},100)
}
结果:3 3 3
因为闭包共享同一个 i
解决:
for(let i=0;i<3;i++){
setTimeout(function(){
console.log(i)
},100)
}
变量声明
var :早期的变量声明方式
基本用法
var name = "Tom";
作用域:函数作用域(function scope)
var 不支持块级作用域。
if (true) {
var a = 10;
}
console.log(a); // 10
因为 var 声明的变量 会穿透代码块。
只有在函数里才会形成作用域:
function test() {
var b = 20;
}
console.log(b); // 报错
变量提升(Hoisting)
var 会被提升到当前作用域顶部。
console.log(x); // undefined
var x = 5;
实际执行类似:
var x;
console.log(x);
x = 5;
可以重复声明
var a = 1;
var a = 2;
console.log(a); // 2
全局变量会挂在 window 上
浏览器环境:
var x = 10;
console.log(window.x); // 10
缺点
var 容易造成:
- 变量污染
- 作用域混乱
- 隐式覆盖
因此 现代 JavaScript 基本不推荐使用 var。
let :ES6 引入的变量声明
基本用法
let age = 20;
作用域:块级作用域(block scope)
if (true) {
let a = 10;
}
console.log(a); // 报错
只在 {} 内有效。
不允许重复声明
let a = 1;
let a = 2; // 报错
变量提升但不可访问(TDZ)
let 也会提升,但在声明前 不能访问。
console.log(a); // 报错
let a = 10;
这段时间叫:
Temporal Dead Zone(暂时性死区)
可以修改值
let score = 80;
score = 90;
常见应用
循环变量:
for (let i = 0; i < 3; i++) {
console.log(i);
}
let 每次循环会创建新的作用域。
const :常量声明
基本用法
const PI = 3.14;
必须初始化
const a; // 报错
必须:
const a = 10;
不能重新赋值
const a = 10;
a = 20; // 报错
也是块级作用域
if (true) {
const a = 5;
}
console.log(a); // 报错
对象/数组可以修改内容
注意:const 只是不能重新赋值变量地址。
const obj = { name: "Tom" };
obj.name = "Jerry"; // 可以
但不能:
obj = {}; // 报错
数组同理:
const arr = [1,2,3];
arr.push(4); // 可以
三种方式核心区别总结
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 有 | 有(TDZ) | 有(TDZ) |
| 可重复声明 | ✅ | ❌ | ❌ |
| 可修改值 | ✅ | ✅ | ❌ |
| 必须初始化 | ❌ | ❌ | ✅ |
| 挂载 window | ✅ | ❌ | ❌ |
现代 JavaScript 推荐使用方式:
- 默认使用
const - 需要修改时用
let - 避免使用
var