【JS基础-Day4】函数与作用域

3 阅读3分钟

【JS基础-Day4】函数与作用域

📺 对应视频:P50-P63 | 🎯 核心目标:理解函数的本质、掌握各种函数定义方式、作用域规则以及IIFE


一、函数基础

1.1 为什么需要函数?

函数是代码复用的基本单元。把重复逻辑封装成函数,减少冗余,提高可维护性。

// 没有函数:重复代码
console.log(1 + 2)
console.log(3 + 4)
console.log(5 + 6)

// 有函数:一次定义,多次使用
function add(a, b) {
  return a + b
}
console.log(add(1, 2))  // 3
console.log(add(3, 4))  // 7
console.log(add(5, 6))  // 11

1.2 函数声明(Function Declaration)

// 语法:function 函数名(参数列表) { 函数体 }
function greet(name) {
  console.log(`你好,${name}!`)
}

// 调用函数
greet('张三')  // 你好,张三!
greet('李四')  // 你好,李四!

// ✅ 函数声明会被提升,可以在声明前调用
sayHi()  // 正常执行
function sayHi() { console.log('Hi!') }

1.3 参数详解

function func(形参1, 形参2) { ... }
func(实参1, 实参2)

// 参数数量不匹配时
function sum(a, b) {
  console.log(a, b)
  return a + b
}
sum(1, 2, 3)  // a=1, b=2,多余的参数被忽略
sum(1)        // a=1, b=undefined,返回 NaN

// ES6 默认参数(解决参数缺失问题)
function sum(a, b = 0) {
  return a + b
}
sum(1)     // 1(b默认为0)
sum(1, 2)  // 3(传了实参,覆盖默认值)

arguments 对象(函数内隐式存在):

function test() {
  console.log(arguments)        // Arguments [1, 2, 3](类数组)
  console.log(arguments.length) // 3
  console.log(arguments[0])     // 1
}
test(1, 2, 3)

// 注意:箭头函数没有 arguments!

1.4 返回值

function max(a, b) {
  return a > b ? a : b  // 用 return 返回结果
}
let result = max(3, 7)  // 7

// return 的特点:
// 1. return 后面的代码不执行
// 2. return 不写(或只写 return),函数返回 undefined
// 3. 函数只能 return 一个值(可以 return 对象/数组来绕过)

function getInfo() {
  return { name: '张三', age: 18 }  // 返回对象
}
function getCoords() {
  return [x, y]  // 返回数组
}

二、函数的多种写法

2.1 函数表达式(Function Expression)

// 将函数赋值给变量
const greet = function(name) {
  return `你好,${name}`
}
greet('张三')

// ⚠️ 函数表达式不会提升!
sayBye()  // ❌ TypeError,sayBye 此时是 undefined
const sayBye = function() { console.log('Bye') }

2.2 匿名函数

// 函数没有名字,常用于:
// 1. 赋值给变量(函数表达式)
const fn = function() { ... }

// 2. 作为回调函数传入
setTimeout(function() {
  console.log('1秒后执行')
}, 1000)

[1, 2, 3].forEach(function(item) {
  console.log(item)
})

2.3 箭头函数(Arrow Function,ES6)

// 基本语法
const add = (a, b) => a + b

// 等价于:
const add = function(a, b) { return a + b }

// 语法简化规则:
// 1. 只有一个参数,可省略括号
const double = x => x * 2

// 2. 函数体只有一行 return,可省略大括号和 return
const add = (a, b) => a + b

// 3. 返回对象字面量,需加括号(避免{}歧义)
const getObj = () => ({ name: '张三', age: 18 })

// 4. 多行函数体,正常写法
const greet = (name) => {
  const msg = `你好,${name}`
  return msg
}

箭头函数的重要特性(Day1进阶会详解):

  • 没有自己的 this,继承外层 this
  • 没有 arguments 对象
  • 不能作为构造函数(不能 new)

2.4 IIFE(立即执行函数)

// Immediately Invoked Function Expression
// 定义后立即执行,创建独立作用域

;(function() {
  let name = '张三'  // 局部变量,外部无法访问
  console.log('立即执行!')
})()

// 箭头函数版 IIFE
;(() => {
  console.log('也是立即执行!')
})()

// 带参数的 IIFE
;(function(a, b) {
  console.log(a + b)
})(1, 2)  // 3

IIFE 的用途:

// 1. 避免全局变量污染
;(function() {
  let count = 0  // 只在 IIFE 内部存在
  // 做一些初始化
})()

// 2. 模块模式(现代开发已用 ES Module 替代)
const counter = (function() {
  let _count = 0
  return {
    increment() { _count++ },
    getCount() { return _count }
  }
})()

三、作用域

3.1 什么是作用域?

作用域是变量可以被访问的范围,控制变量的可见性和生命周期。

JS 作用域类型
├── 全局作用域(Global Scope)
├── 函数作用域(Function Scope)
└── 块级作用域(Block Scope)← ES6新增(let/const)

3.2 全局作用域

// 在最外层声明的变量,任何地方都能访问
let globalVar = '我是全局变量'

function test() {
  console.log(globalVar)  // ✅ 可以访问
}

// ⚠️ 不用 var/let/const 声明,会直接成为全局变量(危险!)
function bad() {
  oops = '意外的全局变量'  // 严格模式下报错
}

3.3 函数作用域(局部作用域)

function outer() {
  let localVar = '局部变量'
  console.log(localVar)  // ✅ 函数内可访问
}
console.log(localVar)  // ❌ ReferenceError,外部无法访问

3.4 块级作用域(let/const)

{
  let blockVar = '块级变量'
  const PI = 3.14
  var oldVar = '老方式'  // var 无块级作用域!
}
console.log(blockVar)  // ❌ ReferenceError
console.log(PI)        // ❌ ReferenceError
console.log(oldVar)    // ✅ '老方式'(var 泄露出来了)

3.5 作用域链

当访问一个变量时,JS 会从当前作用域开始,逐层向外查找,直到全局作用域。

let x = 'global'

function outer() {
  let x = 'outer'
  
  function inner() {
    let x = 'inner'
    console.log(x)  // 'inner'(找到了,停止查找)
  }
  
  function inner2() {
    // 没有 x 声明
    console.log(x)  // 'outer'(向外找,找到 outer 的 x)
  }
  
  inner()
  inner2()
}

outer()

作用域链图示:

inner 作用域 → outer 作用域 → 全局作用域 → 找不到 → ReferenceError

四、变量提升(Hoisting)

// var 声明提升(只提升声明,不提升赋值)
console.log(a)  // undefined(不报错!)
var a = 10
// 等价于:
var a
console.log(a)  // undefined
a = 10

// let/const 存在"暂时性死区(TDZ)"
console.log(b)  // ❌ ReferenceError(死区内不可访问)
let b = 20

// 函数声明整体提升
greet()  // ✅ 正常执行
function greet() { console.log('Hi') }

// 函数表达式不提升
sayBye()  // ❌ TypeError
var sayBye = function() { console.log('Bye') }

五、逻辑中断与短路在函数中的应用

// 参数校验
function greet(name) {
  name = name || '游客'          // 旧写法:||
  name = name ?? '游客'          // 新写法:??(只处理 null/undefined)
  console.log(`你好,${name}`)
}

// 更现代的写法
function greet(name = '游客') {
  console.log(`你好,${name}`)
}

// 防止错误的链式调用
const user = null
// const name = user.profile.name  // ❌ TypeError
const name = user?.profile?.name   // ✅ undefined(可选链,ES2020)

六、函数的高级概念(预览)

6.1 纯函数(Pure Function)

// 纯函数:相同输入永远得到相同输出,无副作用
function add(a, b) { return a + b }  // ✅ 纯函数

// 非纯函数:有副作用(修改外部状态)
let count = 0
function increment() { count++ }  // ❌ 有副作用

6.2 函数作为一等公民

// JS 中函数可以:
// 1. 赋值给变量
const fn = function() {}

// 2. 作为参数传递(回调函数)
setTimeout(fn, 1000)

// 3. 作为返回值(高阶函数)
function makeAdder(x) {
  return function(y) { return x + y }
}
const add5 = makeAdder(5)
console.log(add5(3))  // 8

七、知识图谱

函数与作用域
├── 函数
│   ├── 声明:function 关键字(会提升)
│   ├── 表达式:赋值给变量(不会提升)
│   ├── 匿名函数
│   ├── 箭头函数(ES6,无 this/arguments)
│   ├── IIFE(立即执行,独立作用域)
│   ├── 参数:形参、实参、默认参数、arguments
│   └── 返回值:return(只能返回一个值)
└── 作用域
    ├── 全局作用域
    ├── 函数作用域(局部)
    ├── 块级作用域(let/const)
    ├── 作用域链(从内到外查找变量)
    └── 变量提升(var提升声明,let/const暂时性死区)

八、高频面试题

Q1:函数声明和函数表达式有什么区别?

函数声明会被整体提升到作用域顶部,可以在声明前调用;函数表达式不会提升,且变量在赋值前是 undefined(var)或暂时性死区(let/const)。

Q2:箭头函数和普通函数的主要区别?

① 没有自己的 this,继承外层上下文的 this;② 没有 arguments 对象;③ 不能作为构造函数(不能 new);④ 没有 prototype 属性;⑤ 语法更简洁。

Q3:什么是作用域链?

当访问变量时,JS 引擎先在当前作用域查找,找不到就往外层作用域找,一直到全局作用域。这种层层嵌套的查找链路叫作用域链。

Q4:IIFE 有什么用?

① 立即执行并创建独立作用域,防止变量污染全局;② 实现模块模式;③ 历史上用于模拟块级作用域(ES6 之前)。


⬅️ 上一篇Day3 - 循环与数组 ➡️ 下一篇Day5 - 对象与内置对象