JS:特殊语法

真值(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

×

喜欢就点赞,疼爱就打赏