Appearance
函数调用
一、什么是函数调用?
函数调用是执行函数体的过程。当一个函数被定义后,它不会自动执行,只有通过调用才能运行函数内部的代码。
函数调用的本质
js
function greet(name) {
return `Hello, ${name}!`
}
greet('World')当调用 greet('World') 时,发生了以下步骤:
- 创建执行上下文 - 为函数调用创建一个新的执行环境
- 绑定 this - 根据调用方式确定
this的值 - 绑定参数 - 将实参传递给形参
- 执行函数体 - 运行函数内部的代码
- 返回值 - 返回执行结果(如果没有显式返回,则返回
undefined)
函数调用的核心要素
每次函数调用都会涉及两个重要概念:
| 概念 | 说明 |
|---|---|
this | 函数执行时的上下文对象,取决于调用方式 |
arguments | 函数调用时传入的实参列表(类数组对象) |
二、函数调用方式及其优缺点
JavaScript 中有多种函数调用方式,不同的调用方式会决定 this 的绑定规则。
1. 普通函数调用(直接调用)
最基础的调用方式,函数名后直接加括号。
js
function sayHi() {
console.log(this)
}
sayHi()this 绑定规则
- 非严格模式:
this指向全局对象(浏览器中是window,Node.js 中是global) - 严格模式:
this为undefined
js
function strictMode() {
'use strict'
console.log(this)
}
strictMode()优点
| 优点 | 说明 |
|---|---|
| 语法简洁 | 直接使用函数名调用,代码清晰 |
| 适合工具函数 | 不依赖上下文的纯函数,如数学计算、数据处理 |
缺点
| 缺点 | 说明 |
|---|---|
| this 不确定 | 容易丢失上下文,导致意外行为 |
| 难以控制上下文 | 无法指定函数内部的 this 指向 |
典型问题示例
js
const obj = {
name: 'Alice',
greet: function () {
console.log(`Hello, ${this.name}`)
}
}
const fn = obj.greet
fn()2. 方法调用
函数作为对象的方法被调用,通过对象来访问函数。
js
const person = {
name: 'Bob',
greet: function () {
console.log(`Hello, ${this.name}`)
}
}
person.greet()this 绑定规则
this 指向调用该方法的对象(点号前面的对象)。
js
const obj1 = {
name: 'Object1',
greet: function () {
console.log(this.name)
}
}
const obj2 = {
name: 'Object2'
}
obj2.greet = obj1.greet
obj2.greet()优点
| 优点 | 说明 |
|---|---|
| this 明确 | this 指向调用对象,语义清晰 |
| 面向对象编程 | 符合 OOP 思想,便于封装数据和行为 |
| 代码组织性好 | 相关数据和方法组织在同一对象中 |
缺点
| 缺点 | 说明 |
|---|---|
| 容易丢失 this | 回调函数中容易丢失上下文 |
| 方法不能独立使用 | 脱离对象调用会丢失绑定 |
典型问题示例
js
const button = {
text: 'Click me',
click: function () {
console.log(this.text)
}
}
document.querySelector('button').addEventListener('click', button.click)解决方案
js
document
.querySelector('button')
.addEventListener('click', button.click.bind(button))
document.querySelector('button').addEventListener('click', () => button.click())3. 构造函数调用(new 调用)
使用 new 关键字调用函数,创建一个新的对象实例。
js
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.introduce = function () {
console.log(`I'm ${this.name}, ${this.age} years old.`)
}
const alice = new Person('Alice', 25)
alice.introduce()new 调用的执行过程
js
function _new(Constructor, ...args) {
const obj = Object.create(Constructor.prototype)
const result = Constructor.apply(obj, args)
return result instanceof Object ? result : obj
}- 创建空对象 - 创建一个新的空对象
- 设置原型链 - 将新对象的原型指向构造函数的
prototype - 绑定 this - 将构造函数的
this绑定到新对象 - 执行构造函数 - 运行构造函数内部代码
- 返回对象 - 如果构造函数返回对象则返回该对象,否则返回新创建的对象
this 绑定规则
this 指向新创建的实例对象。
优点
| 优点 | 说明 |
|---|---|
| 创建对象实例 | 可以批量创建结构相同的对象 |
| 原型继承 | 通过 prototype 实现属性和方法共享 |
| 类型识别 | 可以通过 instanceof 判断对象类型 |
缺点
| 缺点 | 说明 |
|---|---|
| 忘记使用 new | 如果忘记 new,this 会指向全局对象,造成污染 |
| 不够直观 | 与普通函数在语法上没有区别,容易误用 |
防止忘记 new 的技巧
js
function Person(name) {
if (!(this instanceof Person)) {
return new Person(name)
}
this.name = name
}
Person('Alice')4. call() 调用
使用 Function.prototype.call() 方法调用函数,可以显式指定 this。
js
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`)
}
const person = { name: 'Alice' }
greet.call(person, 'Hello', '!')语法
js
func.call(thisArg, arg1, arg2, ...)| 参数 | 说明 |
|---|---|
thisArg | 函数执行时的 this 值(传 null 或 undefined 时指向全局对象) |
arg1, arg2, ... | 逐个传递的参数 |
this 绑定规则
this 指向 call 的第一个参数。
优点
| 优点 | 说明 |
|---|---|
| 灵活控制 this | 可以显式指定函数执行的上下文 |
| 参数逐个传递 | 适合参数较少的情况 |
| 借用方法 | 可以借用其他对象的方法 |
缺点
| 缺点 | 说明 |
|---|---|
| 参数传递繁琐 | 参数多时需要逐个列出 |
| 代码可读性一般 | 相比直接调用,语法稍显复杂 |
典型应用场景
js
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
}
const arr = Array.prototype.slice.call(arrayLike)
console.log(arr)
function getMax() {
return Math.max.call(null, ...arguments)
}
console.log(getMax(1, 5, 3, 9, 2))5. apply() 调用
使用 Function.prototype.apply() 方法调用函数,与 call 类似,但参数以数组形式传递。
js
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`)
}
const person = { name: 'Bob' }
greet.apply(person, ['Hi', '?'])语法
js
func.apply(thisArg, argsArray)| 参数 | 说明 |
|---|---|
thisArg | 函数执行时的 this 值 |
argsArray | 参数数组或类数组对象 |
this 绑定规则
与 call 相同,this 指向第一个参数。
优点
| 优点 | 说明 |
|---|---|
| 适合数组参数 | 当参数已经是数组时,使用更方便 |
| 与数组方法配合 | 可以与 arguments、NodeList 等配合使用 |
| 动态参数数量 | 参数数量不确定时更灵活 |
缺点
| 缺点 | 说明 |
|---|---|
| 需要构造数组 | 参数少时反而不如 call 方便 |
| 可读性一般 | 语法不如直接调用直观 |
call vs apply 对比
js
function introduce(name, age, city) {
console.log(`${name}, ${age}岁, 来自${city}`)
}
introduce.call(null, 'Alice', 25, '北京')
introduce.apply(null, ['Bob', 30, '上海'])
const args = ['Charlie', 28, '广州']
introduce.apply(null, args)典型应用场景
js
const numbers = [5, 6, 2, 3, 7]
const max = Math.max.apply(null, numbers)
console.log(max)
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
Array.prototype.push.apply(arr1, arr2)
console.log(arr1)6. bind() 调用
bind() 方法不会立即执行函数,而是返回一个新函数,新函数的 this 被永久绑定。
js
function greet() {
console.log(`Hello, ${this.name}`)
}
const person = { name: 'Alice' }
const boundGreet = greet.bind(person)
boundGreet()语法
js
func.bind(thisArg, arg1, arg2, ...)| 参数 | 说明 |
|---|---|
thisArg | 绑定的 this 值 |
arg1, arg2, ... | 预设的参数(柯里化) |
this 绑定规则
返回的新函数无论以何种方式调用,this 都指向绑定的值。
js
function greet() {
console.log(this.name)
}
const person = { name: 'Alice' }
const boundGreet = greet.bind(person)
boundGreet()
boundGreet.call({ name: 'Bob' })
boundGreet.apply({ name: 'Charlie' })
const obj = { name: 'David', fn: boundGreet }
obj.fn()优点
| 优点 | 说明 |
|---|---|
| 永久绑定 this | 不会丢失上下文,非常可靠 |
| 延迟执行 | 返回新函数,可以在需要时调用 |
| 支持柯里化 | 可以预设部分参数 |
| 适合回调函数 | 解决回调函数丢失 this 的问题 |
缺点
| 缺点 | 说明 |
|---|---|
| 创建新函数 | 每次调用都创建新函数,可能影响性能 |
| 不能再改变 this | 绑定后无法再次绑定或使用 call/apply 改变 |
| 不适合构造函数 | bind 返回的函数不能作为构造函数使用 |
典型应用场景
js
const module = {
x: 42,
getX: function () {
return this.x
}
}
const unboundGetX = module.getX
console.log(unboundGetX())
const boundGetX = unboundGetX.bind(module)
console.log(boundGetX())
function list() {
return Array.prototype.slice.call(arguments)
}
const list1 = list(1, 2, 3)
const leadingThirtysevenList = list.bind(null, 37)
const list2 = leadingThirtysevenList()
const list3 = leadingThirtysevenList(1, 2, 3)
console.log(list1)
console.log(list2)
console.log(list3)
class Button {
constructor(text) {
this.text = text
}
click() {
console.log(`Clicked: ${this.text}`)
}
}
const btn = new Button('Submit')
document.addEventListener('click', btn.click.bind(btn))7. 箭头函数调用
箭头函数是 ES6 引入的特殊函数,它没有自己的 this,而是继承外层作用域的 this。
js
const person = {
name: 'Alice',
greet: function () {
const arrowGreet = () => {
console.log(`Hello, ${this.name}`)
}
arrowGreet()
}
}
person.greet()this 绑定规则
箭头函数的 this 在定义时确定,继承自外层作用域,无法被改变。
js
const person = {
name: 'Alice',
greet: () => {
console.log(this.name)
}
}
person.greet()
const obj = {
name: 'Bob',
fn: function () {
const arrow = () => console.log(this.name)
arrow()
}
}
obj.fn()优点
| 优点 | 说明 |
|---|---|
| this 继承外层 | 不需要担心 this 丢失问题 |
| 语法简洁 | 适合简单的回调函数 |
| 适合回调场景 | 在数组方法、定时器等回调中非常方便 |
缺点
| 缺点 | 说明 |
|---|---|
| 不能绑定 this | 无法使用 call/apply/bind 改变 this |
| 不能作为构造函数 | 不能使用 new 调用 |
| 没有 arguments 对象 | 需要使用 rest 参数代替 |
| 不适合对象方法 | 作为对象方法时 this 可能不是预期值 |
典型应用场景
js
const obj = {
name: 'Alice',
friends: ['Bob', 'Charlie'],
showFriends: function () {
this.friends.forEach(friend => {
console.log(`${this.name} knows ${friend}`)
})
}
}
obj.showFriends()
function Timer() {
this.seconds = 0
setInterval(() => {
this.seconds++
console.log(this.seconds)
}, 1000)
}
const timer = new Timer()
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(n => n * 2)
console.log(doubled)三、调用方式对比总结
| 调用方式 | this 绑定 | 是否立即执行 | 能否改变 this | 典型场景 |
|---|---|---|---|---|
| 普通调用 | 全局对象/undefined | 是 | 否 | 工具函数 |
| 方法调用 | 调用对象 | 是 | 否 | 对象方法 |
| new 调用 | 新实例对象 | 是 | 否 | 创建对象 |
| call() | 第一个参数 | 是 | 是 | 借用方法 |
| apply() | 第一个参数 | 是 | 是 | 数组参数 |
| bind() | 第一个参数 | 否(返回新函数) | 否(永久绑定) | 回调函数 |
| 箭头函数 | 外层作用域 | 是 | 否 | 回调、数组方法 |
四、this 绑定优先级
当多种绑定方式同时出现时,优先级从高到低为:
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定js
function foo() {
console.log(this)
}
const obj = { name: 'obj' }
const boundFoo = foo.bind(obj)
new boundFoo()优先级验证
js
function foo() {
console.log(this.a)
}
const obj1 = { a: 1, foo }
const obj2 = { a: 2 }
obj1.foo()
obj1.foo.call(obj2)
const boundFoo = obj1.foo.bind(obj2)
boundFoo()
new boundFoo()五、最佳实践
1. 根据场景选择合适的调用方式
js
// 工具函数 - 普通调用
function add(a, b) {
return a + b
}
// 对象方法 - 方法调用
const calculator = {
value: 0,
add(n) {
this.value += n
return this
}
}
// 创建对象 - new 调用
function User(name) {
this.name = name
}
// 回调函数 - bind 或箭头函数
class Component {
constructor() {
this.state = {}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this.state)
}
}2. 避免在回调中丢失 this
js
// 错误示例
const obj = {
name: 'Alice',
greet() {
setTimeout(function () {
console.log(this.name)
}, 1000)
}
}
// 解决方案1:使用箭头函数
const obj1 = {
name: 'Alice',
greet() {
setTimeout(() => {
console.log(this.name)
}, 1000)
}
}
// 解决方案2:使用 bind
const obj2 = {
name: 'Alice',
greet() {
setTimeout(
function () {
console.log(this.name)
}.bind(this),
1000
)
}
}
// 解决方案3:保存 this 引用
const obj3 = {
name: 'Alice',
greet() {
const self = this
setTimeout(function () {
console.log(self.name)
}, 1000)
}
}3. 使用严格模式避免意外
js
'use strict'
function foo() {
console.log(this)
}
foo()六、总结
函数调用是 JavaScript 中最基础也最重要的概念之一。理解不同调用方式下 this 的绑定规则,是掌握 JavaScript 的关键。
核心要点:
- 普通调用:
this指向全局对象或undefined(严格模式) - 方法调用:
this指向调用该方法的对象 - new 调用:
this指向新创建的实例 - call/apply:显式指定
this,立即执行 - bind:返回新函数,永久绑定
this - 箭头函数:继承外层作用域的
this
选择合适的调用方式,可以让代码更加清晰、可维护,避免 this 相关的 bug。