在 JavaScript 编程世界里,
this
是一个极为关键却又常常令人困惑的概念。深入掌握this
能极大地提升对这门语言的驾驭能力,为构建更复杂、高效的代码奠定坚实基础。
一、this 是什么
this
是 JavaScript 中的一个关键字,它代表函数执行时的上下文对象。简单来说,当一个函数被调用时,this
指向的是与该函数调用相关联的特定对象。其作用在于为函数提供了一种在不同对象环境下复用的机制,使得函数可以根据调用它的对象来执行不同的逻辑,访问不同对象的属性和方法。
二、this 的指向判断
1. 默认绑定:
当函数以独立函数形式调用时,即没有任何对象调用它,在非严格模式下,this
会指向全局对象。在浏览器环境中,这个全局对象就是window
。但在严格模式下,独立函数调用时this
的值为undefined
,这一设计有效避免了意外的全局变量污染等问题。
- 非严格模式 :也称为松散模式,是 JavaScript 的默认模式。在这种模式下,对一些语法和行为的限制相对宽松,例如可以随意使用未声明的变量,会自动创建全局变量;函数中的
this
在独立调用时默认指向全局对象等。这种模式在早期 JavaScript 开发中较为常用,但也容易导致一些潜在的问题,如变量命名冲突、全局变量污染等。 - 严格模式 :是一种更严格的 JavaScript 运行模式,可以通过在脚本或函数的开头添加
"use strict";
来启用。- 在严格模式下,有以下一些特点:不允许使用未声明的变量,否则会报错;
- 禁止删除不可删除的属性;
- 函数中的
this
在独立调用时不再默认指向全局对象,而是undefined
。
javascript
// 非严格模式
function foo() {
console.log(this);
}
foo();
// 严格模式
function bar() {
'use strict';
console.log(this);
}
bar();
foo
函数是在非严格模式下定义的独立函数,调用foo()
时,this
指向全局对象(浏览器中是window
),在控制台会输出window
对象相关信息。bar
函数使用了'use strict';
声明进入严格模式,调用bar()
时,this
的值为undefined
,在控制台会输出undefined
。
2. 隐式绑定:
当函数作为对象的方法被调用,采用obj.method()
这种形式时,this
会指向调用该方法的对象。这是因为函数在调用时会隐式地与该对象建立关联,函数的调用通过对象触发,所以this
绑定到了这个调用者对象上。
javascript
const person = {
name: 'Alice',
age: 25,
introduce: function() {
console.log(this);
console.log(`My name is ${this.name}, and I'm ${this.age} years old.`);
}
};
person.introduce();
3. new 绑定:
当使用new
关键字调用构造函数时,this
会指向新创建的实例对象。构造函数利用this
来初始化新对象的属性和方法,this
在其中就代表正在创建的这个对象。
javascript
function Person(name, age) {
// 打印 this 的指向
console.log('构造函数内 this 的指向:', this);
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`你好,我叫 ${this.name},今年 ${this.age} 岁。`);
};
}
// 使用 new 关键字调用构造函数创建对象实例
const person1 = new Person('张三', 25);
// 打印创建的对象实例
console.log('创建的对象实例:', person1);
-
在
Person
构造函数里,console.log('构造函数内 this 的指向:', this);
这行代码会输出this
的指向。由于是通过new
关键字调用Person
构造函数来创建对象,所以此时this
指向新创建的对象实例。 -
const person1 = new Person('张三', 25);
使用new
关键字调用Person
构造函数,创建了一个名为person1
的对象实例。 -
运行代码后,你会发现构造函数内部打印的
this
指向和后续打印的person1
对象实例是一样的,这就证明了在构造函数中this
确实指向新创建的对象实例。
4. 箭头函数:
箭头函数与普通函数不同,它没有自己独立的this
绑定。箭头函数中的this
继承自外层作用域的this
指向,在定义时就已确定,不会随着调用方式的改变而改变,也无法通过call
、apply
、bind
等方法进行修改。
javascript
const obj = {
name: 'Bob',
sayHello: function() {
const arrowFunc = () => {
console.log(this.name);
};
arrowFunc();
}
};
obj.sayHello();
-
在
sayHello
函数内部,定义了箭头函数arrowFunc
。 -
在这个例子中,
arrowFunc
的外层作用域是sayHello
函数,而sayHello
函数中的this
指向obj
,所以arrowFunc
中的this
也指向obj
。 -
当执行
arrowFunc()
时,console.log(this.name);
会输出obj
对象的name
属性的值,也就是'Bob'
。
三、this 的设计与缺点
(一)this 的设计
this
的设计基于函数调用方式来动态确定其指向的上下文对象,这种动态绑定机制是 JavaScript 灵活性的重要来源。
在面向对象编程中,它扮演着核心角色。比如在构造函数里,this
指代正在创建的对象实例。以创建Person
对象为例:
javascript
function Person(name) {
this.name = name;
this.sayName = function() {
console.log(`My name is ${this.name}`);
};
}
const person = new Person('Alice');
person.sayName();
这里this
让构造函数能够为每个新创建的实例对象初始化属性(如name
)和方法(如sayName
),实现了数据和行为的封装。
在原型链继承中,this
也发挥着重要作用。当通过原型链查找属性和方法时,this
能在不同的对象层次中正确定位。例如:
javascript
function Animal() {
this.type = 'Animal';
}
Animal.prototype.speak = function() {
console.log('I am an animal');
};
function Dog() {
Animal.call(this);
this.breed = 'Labrador';
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
console.log('Woof! I am a dog');
};
const dog = new Dog();
dog.speak();
在这个过程中,this
保证了在调用speak
等方法时,能在正确的对象实例及其原型链上找到对应的实现,实现了代码的复用。
(二)缺点
1. 规则复杂,极易出错
JavaScript 中 this
的指向规则极为复杂,同一函数因调用方式的不同,this
指向会截然不同。
独立调用时,非严格模式下函数中的 this
指向全局对象(浏览器环境下为 window
),严格模式下则为 undefined
。当函数作为对象的方法调用时,this
指向调用该方法的对象。使用 new
关键字调用构造函数,this
指向新创建的实例对象。
箭头函数较为特殊,没有自身独立的 this
绑定,其 this
继承自外层作用域的 this
指向。例如:
javascript
function logThis() {
console.log(this);
}
logThis(); // 非严格模式下为window(独立调用)
const obj = {
method: logThis
};
obj.method(); // 指向obj对象
new logThis(); // 指向新创建的空对象
const arrowLog = () => console.log(this);
arrowLog(); // 取决于外层作用域的this指向
开发者需时刻牢记这些复杂规则,在不同调用场景中精准判断 this
的指向,稍有疏忽就可能导致程序出错。
2. 调用场景引发的问题
在非严格模式下,函数以独立函数形式调用时,this
默认指向全局对象,这易引发全局变量污染问题。例如:
javascript
function updateCounter() {
this.count = 1; // 意外创建window.count
}
updateCounter();
console.log(window.count); // 输出1
若未意外创建 window.count
,正常情况下 console.log(window.count)
应输出 undefined
。在严格模式下,独立函数调用时 this
绑定为 undefined
,执行 this.count = 1
会报错,可避免意外的全局变量污染。
函数作为参数传递,尤其在回调函数场景中,常出现上下文丢失情况。以 setTimeout
和 addEventListener
为例:
javascript
const user = {
name: "小明",
sayHi() { console.log(this.name) }
};
// 正确调用
user.sayHi(); // 小明
// 错误案例
setTimeout(user.sayHi, 100); // 输出undefined(this指向window)
const button = document.getElementById('button');
button.addEventListener('click', user.sayHi); // 点击时this指向按钮元素
setTimeout
接收回调函数作为参数,user.sayHi
作为回调函数传递时,不再作为 user
对象的方法调用,而是独立函数被 setTimeout
调用,非严格模式下 this
指向全局对象,导致输出 undefined
。
addEventListener
中,user.sayHi
作为回调函数,点击按钮时 this
指向触发事件的 DOM 元素,即按钮,同样导致输出 undefined
,与期望的 this
指向 user
对象不符。
3. 特殊函数类型问题
箭头函数具有独特的 this
绑定特性,没有自身独立的 this
,而是继承外层作用域的 this
指向。若在不恰当场景使用箭头函数,可能导致 this
指向与预期不符。例如:
javascript
const button = document.getElementById('button');
const obj = {
message: 'Hello',
clickHandler: function() {
button.addEventListener('click', () => {
console.log(this.message);
});
}
};
obj.clickHandler();
期望点击按钮时输出 Hello
,但箭头函数继承了外层 clickHandler
函数的 this
,而 addEventListener
的回调函数中 this
并非 obj
,导致输出 undefined
。
若将箭头函数改为普通函数,或正确绑定 this
,可避免此类问题。
4. 新手学习门槛高
与多数编程语言中 this
或 self
通常指向当前对象不同,JavaScript 中 this
的指向规则对新手而言具有很强的反直觉性。例如:
javascript
const obj = {
outer() {
function inner() {
console.log(this); // 指向window!
}
inner();
}
};
obj.outer();
outer
函数作为 obj
对象的方法被调用,outer
函数内部 this
指向 obj
。但内部函数 inner
以独立函数调用,在非严格模式下,this
指向全局对象 window
,而非 obj
,与新手对面向对象编程中 this
指向的常规认知相悖,增加了学习成本。
若想让内部函数的 this
指向外部的 obj
对象,可使用 bind
方法或箭头函数。如改为使用箭头函数:
javascript
const obj = {
outer() {
const inner = () => {
console.log(this); // 指向 obj
};
inner();
}
};
obj.outer();
此时,inner
函数的 this
继承自外层作用域(outer
函数)的 this
,所以指向 obj
。
四、this 指向混乱的原因
(一)动态绑定机制的复杂性
JavaScript 中 this
的指向并非在函数定义时就固定,而是依据函数的调用方式在运行时动态确定,这是导致 this
指向混乱的核心原因。
-
独立函数调用 :独立形式调用时,非严格模式下
this
指向全局对象,浏览器环境中为window
;严格模式下this
值变为undefined
。 -
作为对象方法调用 :当函数作为对象的方法被调用,如
obj.method()
形式,this
指向调用该方法的对象,因为函数调用时会隐式地与该对象关联。 -
构造函数调用 :使用
new
关键字调用构造函数时,this
指向新创建的实例对象,构造函数通过this
初始化新对象的属性和方法。如此繁多且依赖运行时调用方式的指向规则,开发者在编写和阅读代码时,稍有疏忽就容易对
this
的实际指向产生误解,进而导致 "规则复杂,极易出错" 的问题。
(二)箭头函数特性引发的混淆
箭头函数独特的 this
绑定特性在带来便利的同时,也容易引发 this
指向的混乱,对应 "特殊函数类型问题"。
如果开发者在不恰当的场景使用箭头函数,比如在事件回调函数或者对象方法中,就可能导致 this
指向与预期不符,就像前文 button.addEventListener
示例中,箭头函数继承外层 this
导致与预期输出不一致的情况。
(三)全局作用域污染带来的不确定性
在非严格模式的全局作用域中,独立调用的函数默认情况下 this
会指向全局对象。在复杂项目中,开发者可能因疏忽,导致 this
意外地指向全局,尤其在使用回调函数时未进行正确的绑定,从而引发全局变量污染,这与 "调用场景引发的问题" 相关。
(四)异步 / 回调场景的隐式丢失
在异步操作和回调函数场景中,this
很容易丢失原有的上下文,属于 "调用场景引发的问题" 范畴。
-
setTimeout 示例 :当函数作为
setTimeout
的回调函数传递时,this
指向会发生变化。如前文user
对象的sayHello
方法中使用setTimeout
回调函数,期望输出Hello, Bob
,但实际输出Hello, undefined
,原因是setTimeout
回调函数中的this
指向了全局对象(非严格模式下),原有的user
对象上下文丢失。 -
事件处理器示例 :在事件处理中,
this
默认指向触发事件的 DOM 元素。若回调函数使用箭头函数或者没有手动进行绑定,this
的指向也会发生改变。如前文中app
对象的handleClick
方法中,btn
的click
事件回调函数使用箭头函数,预期点击按钮输出Button clicked
,但由于箭头函数继承外层this
,输出undefined
,因为addEventListener
回调函数的this
并非app
对象。
(五)严格模式切换导致的不一致性
JavaScript 存在严格模式与非严格模式,这两种模式下全局作用域中的 this
指向不同。
如果代码中混合使用这两种模式,很可能会引发一些意外行为,导致 this
指向出现混乱,与 "规则复杂,极易出错" 问题相关。
在大型项目中,不同模块可能采用不同的模式,这种模式的不一致性会进一步加剧 this
指向混乱的问题。
五、改变 this 指向的 3 种方式及区别(call、apply、bind)
call
、apply
和bind
这三种方法为开发者提供了灵活改变函数this
指向的途径,使函数能够在特定的上下文环境中执行。
(一)call 方法
-
执行时机 :
call
方法会立即执行被调用的函数。 -
参数传递 :它的第一个参数是要绑定的
this
值,后续参数则是要传递给函数的参数,需逐个列出。- 例如,有一个函数
function add(a, b) { return this.x + a + b; }
,我们可以通过add.call({x: 5}, 3, 2)
来调用。在此例中,{x: 5}
被绑定为函数执行时的this
,3
和2
则作为参数传递给add
函数。
- 例如,有一个函数
-
应用场景 :
call
方法适用于需要明确指定this
指向并立即执行函数,且参数个数和顺序明确的情况。-
在实现继承时,它有着广泛的应用,比如在子类构造函数中调用父类构造函数并传递参数,同时指定
this
指向子类实例,从而实现属性和方法的继承。 -
在
Parent.call(this, name)
中,this
表示将Parent
构造函数执行时的this
指向当前正在创建的Child
对象
-
javascript
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
const child = new Child('小明', 10);
console.log(child.name); // 小明
(二)apply 方法
- 执行时机 :与
call
方法一样,apply
方法也是立即执行函数。 - 参数传递 :
apply
方法的第一个参数同样是要绑定的this
值,但其第二个参数是一个数组,数组中的元素即为要传递给函数的参数。- 以之前的
add
函数为例,可使用add.apply({x: 5}, [3, 2])
进行调用,这里[3, 2]
作为参数数组传递给add
函数。
- 以之前的
- 应用场景 :当需要传递一个数组作为函数参数,并且希望立即执行函数时,
apply
方法尤为方便。- 例如,在使用
Math.max
函数求数组中的最大值时,由于Math.max
本身不直接支持数组作为参数,此时就可以借助apply
方法,如Math.max.apply(null, [1, 5, 3])
,将数组元素作为参数传递给Math.max
函数。
- 例如,在使用
另外,假设我们有一个函数用于计算多个数的平均值:
javascript
function average() {
let sum = 0;
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum / arguments.length;
}
const numbers = [2, 4, 6, 8];
// 使用apply将数组作为参数传递给average函数
const result = average.apply(null, numbers);
console.log(result);
在这个例子中,average
函数原本期望接收多个独立参数,但我们有一个包含数据的数组numbers
。通过apply
方法,我们将数组numbers
作为参数传递给average
函数,从而顺利计算出数组元素的平均值。
此外,在一些需要将数组元素逐个作为参数传递给其他函数的场景中,apply
方法也能派上用场。
(三)bind 方法
- 执行时机 :
bind
方法不会立即执行函数,而是返回一个新函数,只有当调用这个新函数时,原函数才会执行。 - 参数传递 :
bind
方法的第一个参数是要绑定的this
值,后续参数可以在绑定this
时传递,也可以在调用返回的新函数时传递。- 例如,
const newAdd = add.bind({x: 5}, 3)
,这里将this
绑定为{x: 5}
,并传递了一个参数3
。之后调用newAdd(2)
时,效果等同于执行原add
函数并传入3
和2
作为参数。
- 例如,
- 应用场景 :
bind
方法常用于需要将函数的this
绑定为特定值,然后在后续某个时刻再执行函数的情况。在事件绑定中,由于事件触发时this
指向可能不符合预期,使用bind
可以预先绑定正确的this
指向。比如:
javascript
const button = document.getElementById('button');
const obj = {
message: 'Button clicked',
handleClick: function() {
console.log(this.message);
}
};
button.addEventListener('click', obj.handleClick.bind(obj));
在异步操作中,当需要在某个特定时刻执行函数时,也可以使用bind
提前绑定this
和部分参数,方便后续调用。
(四)三种方法的共同点
call
、apply
和bind
这三种方式的核心共同点在于都能够强制指定函数执行时的this
指向,让函数在开发者期望的特定上下文环境中执行,从而实现更灵活的编程逻辑和代码复用。
(五)应用场景总结
- call 和 apply :适合需要一次性调用函数并改变
this
指向的场景。在实现继承时,通过call
或apply
让子类调用父类的构造函数,是一种常见且有效的方式。在处理数组相关操作时,若某个函数原本不支持数组参数,apply
可以巧妙地传入数组参数,满足特定需求。 - bind :适用于需要延迟执行函数或者作为事件回调的场景。在事件处理中,使用
bind
预先绑定正确的this
指向,能确保事件处理函数在触发时this
指向符合预期。在异步操作中,提前使用bind
绑定this
和部分参数,为后续特定时刻执行函数做好准备,增强了代码的可控性。