一、函数的length 属性:它在数什么?

在 JavaScript 中,每个函数都有一个 length 属性,告诉你这个函数在定义时“期望”接收的参数数量。听起来有点抽象?我们用例子来说明。

1. 基本用法

先看一个简单的函数:

function sayHello() {
  console.log('Hello!')
}
console.log(sayHello.length) // 输出 0

这个函数没有参数,所以 length 是 0。很简单吧?再看一个有参数的:

function greet(name, message) {
  console.log(`${name}, ${message}`)
}
console.log(greet.length) // 输出 2

greet 定义了 namemessage 两个参数,所以 length 是 2。直观明了。

2. 小心默认值和剩余参数

事情在遇到默认值或剩余参数时会变得有趣:

function calculate(a, b = 10, c) {
  return a + b + c
}
console.log(calculate.length) // 输出 1

为什么是 1?因为 length 只计算第一个带默认值的参数之前的参数数量。b 有了默认值,计数就停了,只算了 a

再看剩余参数:

function sum(a, ...numbers) {
  let total = a
  for (const num of numbers) {
    total += num
  }
  return total
}
console.log(sum.length) // 输出 1

类似地,...numbers 是剩余参数,length 只算到它之前,也就是只有 a

3.length 的实际用途

length 属性在日常编码中可能用得不多,但在框架或库中却很常见。比如:

  • 判断函数需要多少参数,方便动态调用。
  • 在函数式编程(如柯里化)中,检查函数的“必需”参数数量。
  • 优化参数处理的逻辑。

小结length 反映的是函数定义时“必需”参数的数量(不包括默认值和剩余参数)。它是个小工具,帮你快速了解函数的签名。


二、普通函数 vs 箭头函数:选哪个?

JavaScript 中的函数有两种常见形式:普通函数和箭头函数(ES6 引入)。它们语法和行为上都有差异,选对适合的场景能让代码更清晰。

1. 语法差异

箭头函数的语法更简洁:

// 普通函数
function add(a, b) {
  return a + b
}

// 箭头函数
const add = (a, b) => a + b

如果箭头函数只有一个参数,可以省略括号;如果只有一行代码,可以省略 {}return,非常适合写简短的回调函数。

2.this 绑定的不同

这是两者最大的区别。普通函数的 this 取决于调用方式,而箭头函数的 this定义时捕获的外层作用域的 this

const person = {
  name: '小明',
  sayHiNormal: function() {
    console.log(`你好,我是 ${this.name}`)
  },
  sayHiArrow: () => {
    console.log(`你好,我是 ${this.name}`)
  }
}

person.sayHiNormal() // 输出:你好,我是 小明
person.sayHiArrow()  // 输出:你好,我是 undefined

为什么?sayHiNormalthis 指向调用它的对象 person,而 sayHiArrowthis 指向定义时的外层作用域(通常是全局对象,name 未定义,所以是 undefined)。

3. 其他差异

  • 没有 arguments 对象:箭头函数不绑定 arguments,如果需要收集参数,可以用剩余参数 ...args
  • 不能作为构造函数:箭头函数不能用 new 调用,因为它没有 prototype 属性。
  • **无法改变 **this:普通函数可以用 callapplybind 改变 this,箭头函数不行。
const obj = { value: '对象' }
function regularFunc() {
  return this.value
}
const arrowFunc = () => this.value

console.log(regularFunc.call(obj)) // 对象
console.log(arrowFunc.call(obj))  // undefined

4. 适用场景

  • 箭头函数:适合需要捕获外层 this 的场景,比如回调函数或事件监听。
const app = {
  init() {
    document.querySelector('button').addEventListener('click', () => {
      this.doSomething() // this 指向 app
    })
  },
  doSomething() { console.log('做事啦') }
}
  • 普通函数:适合需要动态 this 的场景,比如对象方法或构造函数。
const counter = {
  count: 0,
  increment() {
    this.count++
  }
}

小结:箭头函数简洁且绑定外层 this,适合回调和函数式编程;普通函数灵活,适合对象方法和构造函数。根据 this 和功能需求选择合适的函数类型。


三、构造函数:批量生产对象的蓝图

构造函数是 JavaScript 面向对象编程的基础,用来创建结构一致的对象。

1. 什么是构造函数?

构造函数是一个普通函数,但通过 new 调用,专门用来初始化对象。约定俗成,构造函数首字母大写。

function Person(name, age) {
  this.name = name
  this.age = age
  this.sayHello = function() {
    console.log(`嗨,我是 ${this.name},今年 ${this.age}`)
  }
}

const xiaoming = new Person('小明', 18)
xiaoming.sayHello() // 嗨,我是 小明,今年 18 岁

2.new 做了什么?

使用 new 调用构造函数时,JavaScript 会:

  1. 创建一个空对象 {}
  2. 将构造函数的 this 指向这个新对象。
  3. 执行构造函数代码,添加属性和方法。
  4. 返回这个新对象。

3. 原型的力量

每个构造函数都有一个 prototype 属性,指向原型对象。原型上的方法和属性被所有实例共享,节省内存。

function Dog(name) {
  this.name = name
}
Dog.prototype.bark = function() {
  console.log(`${this.name}:汪汪汪!`)
}

const dog1 = new Dog('小黑')
dog1.bark() // 小黑:汪汪汪!

如果在构造函数内定义方法,每个实例都会有一份方法副本,浪费内存:

function Student(name) {
  this.name = name
  this.study = function() { // 每个实例都有独立的副本
    console.log(`${this.name} 正在学习`)
  }
}

推荐做法是将方法定义在原型上:

function Student(name) {
  this.name = name
}
Student.prototype.study = function() {
  console.log(`${this.name} 正在学习`)
}

4. 构造函数 vs ES6 Class

ES6 的 class 语法是构造函数的“语法糖”,写法更现代:

class User {
  constructor(name, email) {
    this.name = name
    this.email = email
  }
  sendEmail(content) {
    console.log(`${this.email} 发送邮件:${content}`)
  }
}

功能上等价于构造函数,但更清晰,更适合现代项目。

5. 实际应用

构造函数常用于:

  • 创建结构相似的对象(如购物车项、用户对象)。
  • 实现面向对象编程,封装逻辑。
  • 定义自定义类型。

例如,购物车示例:

function CartItem(product, quantity) {
  this.id = Date.now() + Math.random()
  this.product = product
  this.quantity = quantity
  this.price = product.price
}
CartItem.prototype.getTotalPrice = function() {
  return this.price * this.quantity
}

const apple = { name: '苹果', price: 5 }
const item = new CartItem(apple, 3)
console.log(item.getTotalPrice()) // 15

小结:构造函数是创建对象的模板,通过 new 和原型机制实现高效的对象管理。ES6 的 class 让代码更现代,但本质相同。


四、执行上下文:JavaScript 的运行秘密

执行上下文是 JavaScript 运行代码的“环境”,决定了变量、函数和 this 的行为。理解它能帮你搞清楚代码为何那样运行。

1. 执行上下文的类型

JavaScript 有三种执行上下文:

  • 全局执行上下文:代码运行时创建,管理全局变量和函数。
  • 函数执行上下文:每次调用函数时创建,管理函数内的变量和 this
  • Eval 执行上下文eval 函数的上下文(不推荐使用)。

2. 执行上下文栈

JavaScript 用“调用栈”管理多个上下文:

function first() {
  console.log('我是 first 函数')
  second()
}
function second() {
  console.log('我是 second 函数')
}
first()

执行过程:

  1. 全局上下文入栈。
  2. first() 调用,first 上下文入栈。
  3. second() 调用,second 上下文入栈。
  4. second 执行完出栈,回到 first,最后回到全局。

3. 上下文的创建

上下文创建分两阶段:

  • 创建阶段:初始化变量对象、作用域链和 this
  • 执行阶段:执行代码,赋值等。

这导致了变量提升现象:

console.log(name) // undefined
var name = '张三'

sayHello() // 你好
function sayHello() {
  console.log('你好')
}

变量和函数声明在创建阶段被“提升”,但赋值要等到执行阶段。

4. 块级作用域

ES6 的 letconst 引入了块级作用域,变量不会提升:

console.log(age) // 报错
let age = 25

5. 闭包与执行上下文

闭包是执行上下文的经典应用:

function createCounter() {
  let count = 0
  return function() {
    count++
    return count
  }
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2

返回的函数保留了对 count 的引用,因为它的作用域链指向了 createCounter 的上下文。

6.this 绑定

this 的值由上下文决定:

const user = {
  name: '李四',
  greet() {
    console.log(`你好,我是 ${this.name}`)
  }
}
user.greet() // 你好,我是 李四
const sayHi = user.greet
sayHi() // 你好,我是 undefined

箭头函数的 this 继承外层上下文,不受调用方式影响。

小结:执行上下文是 JavaScript 运行代码的“幕后英雄”,管理变量、作用域和 this。理解它能帮你写出更可靠的代码。


五、综合应用:从理论到实践

假设我们要实现一个简单的任务管理器,结合以上知识:

class Task {
  constructor(title, priority = 'low') {
    this.id = Date.now()
    this.title = title
    this.priority = priority
    this.completed = false
  }
  toggleComplete() {
    this.completed = !this.completed
    console.log(`${this.title} 现在${this.completed ? '已完成' : '未完成'}`)
  }
}

const TaskManager = {
  tasks: [],
  addTask(title, priority) {
    const task = new Task(title, priority)
    this.tasks.push(task)
    return task
  },
  getSummary: () => {
    console.log(`总计 ${TaskManager.tasks.length} 个任务`)
  }
}

TaskManager.addTask('学习 JavaScript', 'high')
TaskManager.getSummary() // 总计 1 个任务
TaskManager.tasks[0].toggleComplete() // 学习 JavaScript 现在已完成

console.log(Task.prototype.toggleComplete.length) // 0(无参数)
console.log(TaskManager.addTask.length) // 2(title 和 priority)

这里用到了:

  • class 语法(构造函数的现代形式)。
  • 箭头函数(getSummary 绑定 TaskManagerthis)。
  • length 属性检查方法参数。
  • 原型方法(toggleComplete 共享)。

六、总结与建议

  1. **函数的 **length:快速了解函数的“必需”参数数量,适合元编程和参数校验。
  2. 普通函数 vs 箭头函数:根据 this 和功能需求选择,回调用箭头函数,对象方法用普通函数。
  3. 构造函数:通过 new 和原型创建对象,适合批量生成结构一致的对象。
  4. 执行上下文:理解变量提升、作用域和 this,避免代码中的“奇怪”行为。

建议

  • 多实践闭包、原型和 this,它们是 JavaScript 的核心。
  • 使用现代 class 语法,但理解其背后的构造函数原理。
  • 调试时关注调用栈,搞清楚上下文切换的逻辑。