一、函数的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
定义了 name
和 message
两个参数,所以 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
为什么?sayHiNormal
的 this
指向调用它的对象 person
,而 sayHiArrow
的 this
指向定义时的外层作用域(通常是全局对象,name
未定义,所以是 undefined
)。
3. 其他差异
- 没有
arguments
对象:箭头函数不绑定 arguments
,如果需要收集参数,可以用剩余参数 ...args
。
- 不能作为构造函数:箭头函数不能用
new
调用,因为它没有 prototype
属性。
- **无法改变 **
this
:普通函数可以用 call
、apply
或 bind
改变 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 会:
- 创建一个空对象
{}
。
- 将构造函数的
this
指向这个新对象。
- 执行构造函数代码,添加属性和方法。
- 返回这个新对象。
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()
执行过程:
- 全局上下文入栈。
first()
调用,first
上下文入栈。
second()
调用,second
上下文入栈。
second
执行完出栈,回到 first
,最后回到全局。
3. 上下文的创建
上下文创建分两阶段:
- 创建阶段:初始化变量对象、作用域链和
this
。
- 执行阶段:执行代码,赋值等。
这导致了变量提升现象:
console.log(name) // undefined
var name = '张三'
sayHello() // 你好
function sayHello() {
console.log('你好')
}
变量和函数声明在创建阶段被“提升”,但赋值要等到执行阶段。
4. 块级作用域
ES6 的 let
和 const
引入了块级作用域,变量不会提升:
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
绑定 TaskManager
的 this
)。
length
属性检查方法参数。
- 原型方法(
toggleComplete
共享)。
六、总结与建议
- **函数的 **
length
:快速了解函数的“必需”参数数量,适合元编程和参数校验。
- 普通函数 vs 箭头函数:根据
this
和功能需求选择,回调用箭头函数,对象方法用普通函数。
- 构造函数:通过
new
和原型创建对象,适合批量生成结构一致的对象。
- 执行上下文:理解变量提升、作用域和
this
,避免代码中的“奇怪”行为。
建议:
- 多实践闭包、原型和
this
,它们是 JavaScript 的核心。
- 使用现代
class
语法,但理解其背后的构造函数原理。
- 调试时关注调用栈,搞清楚上下文切换的逻辑。