我们来深入地梳理一下函数、方法、对象,以及它们之间纠缠的根源------this。这不仅仅是记住几个概念,而是理解JavaScript(以及很多面向对象语言)在运行时,代码是如何被组织、调用和执行的。
首先,我们需要建立一个核心的、动态的世界观:代码是静态的文本,而对象是存在于内存中的、活的"事物"。 函数则是一段可以执行的、静态的代码和动态的执行环境的结合体。
1. 对象:数据的容器与行为的宿主
对象,从本质上讲,是在内存中开辟的一块区域,用来存放一组相关联的数据 (我们称之为属性)和操作这些数据的行为(我们称之为方法)。
可以把对象想象成一个"小王国"。这个王国里有很多"资源"(属性),比如 name: 'Alice', age: 30。同时,这个王国也有一些"法令"或"工作流程"(方法),比如 sayHello()。当这个王国要执行 sayHello 这个工作流程时,它需要知道"我"是谁?它需要访问自己的资源 name,才能说出"你好,我是Alice"。
所以,对象存在的核心意义之一,就是为行为(函数)提供一个执行的上下文(Context) 。这个"上下文"就是 this 最终指向的东西------即当前这个活跃的"小王国"本身。
2. 函数:独立的执行代码单元
函数,本质上也是一段可执行的代码。但它不像方法那样,生来就"属于"某个对象。函数是独立的。你可以把它想象成一个"万能工具"或一份"公开的蓝图"。
这份蓝图(函数体)本身描述了要做什么:function greet() { console.log('Hello') }。但在它真正被执行(被调用)之前,它不知道自己属于哪个"王国"。它是自由的、无上下文的。
当你在全局环境中调用它,它就暂时地在全局"王国"里执行。你可以把这个自由的函数,通过赋值的方式,"借给"一个对象,让它成为那个对象的一个属性。从这一刻起,它在这个对象上,就扮演了"方法"的角色。
3. 方法:函数在对象中的角色
所以,方法并不是一种特殊的函数,而是函数的一种"角色"或"状态"。
当你通过一个对象去调用它拥有的函数属性时,比如 alice.sayHello(),这个函数就临时获得了 alice 这个对象作为它的执行上下文。此刻,它就不再是一个孤立的"万能工具",而是为 alice 王国服务的、有归属的"工作流程"。
关键点来了:函数本身并没有变,变的是它被调用的方式。 同一个函数,完全可以一会儿作为独立函数被调用,一会儿作为某个对象的方法被调用。它的行为会因为调用方式的不同而产生巨大的差异,这个差异的核心,就是 this。
4. this:动态的执行上下文指针
this 就是连接函数、方法和对象的那根"命脉"。它是一个在函数被调用时才确定下来的指针,指向调用该函数的那个对象(即当前执行的上下文)。
-
当函数作为独立函数被调用时:
greet()。在非严格模式下,this会指向全局对象(浏览器里的window)。这就像这个函数在全局王国里临时客串了一下,但它找不到自己的属性,很容易出错。在严格模式下,this则是undefined,明确告诉你,它没有合法的上下文。 -
当函数作为对象的方法被调用时:
alice.sayHello()。this就会指向alice这个对象。sayHello函数体里的this.name,就自然而然地取到了alice.name的值。这就是"方法"能够操作"所属对象"数据的根本原理。 -
当函数作为构造函数被调用时:
new Person('Bob')。一个全新的、空的对象会在内存中被创建出来,然后这个函数被调用,并且函数体内的this会指向这个即将被创建出来的新对象 。函数通过this.name = name这样的方式,给这个新对象初始化属性。最后,这个新对象被返回。这个过程清晰地展示了函数如何作为"蓝图"来创造新的对象"王国"。 -
通过
.call、.apply或.bind强制指定this: 这是最灵活的方式。你可以强行把一个函数"塞"进任何一个对象里去执行。比如greet.call(alice),就是把greet这个独立函数,临时借给alice对象,让它以alice为上下文去执行。这完美地证明了函数和方法的本质关系------函数只是一个代码块,而通过控制this,我们可以动态地决定它"属于"谁。
5. 内在关联:
把这些点串联起来,我们就能看到一个完整的逻辑链条:
- 存储与组织: 我们用对象来组织数据和逻辑。对象的属性存储数据,而它的方法则定义了对这些数据的操作。
- 定义与复用: 这些"方法"本质上就是函数。我们在对象外部或内部定义函数,目的是为了复用代码逻辑。函数本身不依赖于任何对象。
- 绑定与执行: 当程序运行时,我们需要把函数和对象动态地结合起来。这个结合点就是函数调用 。调用方式(独立调用、方法调用、构造调用、间接调用)决定了函数体内的
this指针指向哪个对象。 - 上下文的意义:
this指向的对象,为函数的执行提供了"上下文"数据。方法之所以能操作其所属对象的属性,就是因为this正确地指向了那个对象。
6. bind、apply、call:手动控制this
理解了this是动态绑定的之后,我们就掌握了一个强大的能力------我们可以主动干预这个绑定过程 。JavaScript提供了三个方法:call、apply和bind,它们都存在于函数对象的原型上(Function.prototype),这意味着每一个函数都天生拥有这三个方法。
这三个方法的核心作用完全一致:让你手动指定函数执行时的this指向。但它们在使用方式和执行时机上有细微的差别,理解这些差别能让你在不同的场景下灵活运用。
6.1 执行并传参:call 与 apply
call和apply都是立即执行 函数的方法。它们的作用完全一样,唯一的区别在于传参方式不同。
call:参数列表形式
javascript
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Alice' };
// call 接收参数列表,第一个参数是 this 的指向,后面的参数依次传给函数
greet.call(person, 'Hello', '!');
// 输出:Hello, Alice!
apply:参数数组形式
javascript
// apply 接收两个参数:第一个是 this 的指向,第二个是参数数组
greet.apply(person, ['Hi', '!!']);
// 输出:Hi, Alice!!
为什么需要两种形式?
这源于JavaScript的灵活性。call适用于你明确知道函数需要几个参数的情况,写起来更直观。apply的强大之处在于,它可以配合数组使用,特别是当参数数量不确定时,或者你有一个现成的参数数组时。
一个经典的例子:求数组中的最大值
javascript
const numbers = [5, 6, 2, 3, 7];
// Math.max 接收参数列表,不接收数组
const max1 = Math.max(5, 6, 2, 3, 7); // 正常用法
// 但如果我们有一个数组呢?用 apply 可以完美解决
const max2 = Math.max.apply(null, numbers);
// 这里第一个参数传 null,是因为 Math.max 不依赖 this
console.log(max2); // 7
// 现在也可以用扩展运算符实现类似效果
const max3 = Math.max(...numbers);
6.2 绑定并等待:bind
bind和前两者有本质区别。bind不是立即执行函数,而是返回一个新的函数 ,这个新函数的this被永久地绑定到了你指定的对象上。
javascript
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Alice' };
// bind 返回一个新函数,this 被绑定到 person
const greetAlice = greet.bind(person, 'Hello');
// 现在可以在任何时候调用这个新函数
greetAlice('!'); // 输出:Hello, Alice!
greetAlice('!!'); // 输出:Hello, Alice!!
注意上面的例子中,我们不仅绑定了this,还预置了第一个参数'Hello'。这叫做**柯里化(Currying)**的一种形式------固定某些参数,生成一个参数更少的新函数。
bind的核心特征:
- 永久绑定 :一旦用
bind绑定了this,即使对这个新函数再次使用call、apply或bind,也无法改变它的this指向。这被称为"硬绑定"。
javascript
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };
const greetPerson1 = greet.bind(person1);
// 尝试用 call 改变 this,不会生效
greetPerson1.call(person2, 'Hi', '!'); // 仍然输出:Hi, Alice!
- 延迟执行 :
bind生成的函数可以留到需要的时候再调用,这在实际开发中非常有用,特别是在事件处理、定时器、回调函数等场景中。
6.3 实际应用场景:为什么要手动控制this?
理解了这三个工具怎么用,更重要的是理解什么时候需要用它们。
场景一:丢失的this(最经典的场景)
javascript
const person = {
name: 'Alice',
greet: function() {
console.log('Hello, ' + this.name);
}
};
// 正常调用
person.greet(); // Hello, Alice
// 把方法赋值给一个变量
const greetFunction = person.greet;
greetFunction(); // Hello, undefined (或者报错)
为什么会这样?因为greetFunction现在是独立函数调用,this指向了全局对象。解决方案就是用bind固定this:
javascript
const boundGreet = person.greet.bind(person);
boundGreet(); // Hello, Alice
在React类组件中,你经常看到this.handleClick = this.handleClick.bind(this),正是为了解决这个问题。
场景二:借用方法
有时候一个对象没有某个方法,但另一个对象有。我们可以借用过来用。
javascript
const alice = {
name: 'Alice',
friends: ['Bob', 'Charlie']
};
const bob = {
name: 'Bob',
friends: ['Alice', 'David']
};
// 定义一个通用的打印方法(当然也可以定义在原型上)
function printFriends() {
this.friends.forEach(friend => {
console.log(this.name + ' knows ' + friend);
});
}
// 让 alice 借用这个方法
printFriends.call(alice);
// Alice knows Bob
// Alice knows Charlie
// 让 bob 借用
printFriends.call(bob);
// Bob knows Alice
// Bob knows David
场景三:类数组对象转数组
DOM操作返回的NodeList、函数内部的arguments都是类数组对象,它们有length属性和索引,但没有数组的方法。我们可以借用数组的方法。
javascript
function listArguments() {
// arguments 是类数组,没有 forEach 方法
// 但我们可以借用数组的 forEach
Array.prototype.forEach.call(arguments, arg => {
console.log(arg);
});
// 或者转成真正的数组
const argsArray = Array.prototype.slice.call(arguments);
// 现代写法:const argsArray = Array.from(arguments);
}
listArguments(1, 2, 3); // 输出 1, 2, 3
场景四:保存上下文(配合箭头函数)
在异步回调中,我们常常需要保存外层的this。在箭头函数出现之前,常用的手法是var self = this,然后用bind。
javascript
function Timer() {
this.seconds = 0;
// 传统写法:用 bind 绑定
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000);
// 箭头函数写法(箭头函数没有自己的 this,会继承外层)
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
6.4 总结三者的核心区别
| 方法 | 执行时机 | 返回值 | 参数传递 | 典型用途 |
|---|---|---|---|---|
call |
立即执行 | 函数执行结果 | 参数列表 | 需要立即调用,参数数量明确 |
apply |
立即执行 | 函数执行结果 | 参数数组 | 需要立即调用,参数是数组形式 |
bind |
延迟执行 | 新函数 | 参数列表(可分批) | 需要固定this以便后续调用 |
所以 : 想立即调用 并指定this,用call或apply(区别在于传参方式),想创建一个新函数 ,永久绑定this到某个对象,以便以后调用,用bind