this关键字和上下文
this关键字是JavaScript中的一个特殊变量,它指向当前执行代码的上下文对象。理解this的指向规则是掌握JavaScript的关键之一,因为它在不同的执行环境中会有不同的指向。本文将详细介绍JavaScript中this关键字的概念、指向规则以及如何改变this的指向。
1. this的概念
this是一个在函数执行时自动生成的变量,它指向当前函数的执行上下文。执行上下文是指函数执行时的环境,包括函数的调用者、调用方式、传入的参数等。
特点:
this的值在函数定义时是不确定的,只有在函数执行时才能确定。this的指向与函数的调用方式有关,与函数的定义位置无关。- 在严格模式和非严格模式下,
this的指向可能会有所不同。
2. this的指向规则
在JavaScript中,this的指向主要取决于函数的调用方式,以下是几种常见的情况:
2.1 全局上下文
在全局上下文中,this指向全局对象(在浏览器中是window,在Node.js中是global)。
使用方式:
// 全局上下文
console.log(this); // window(浏览器中)
// 全局函数中的this
function globalFunction() {
console.log(this); // window(浏览器中)
}
globalFunction();
// 严格模式下的全局函数
'use strict';
function strictGlobalFunction() {
console.log(this); // undefined
}
strictGlobalFunction();2.2 函数作为对象方法调用
当函数作为对象的方法调用时,this指向调用该方法的对象。
使用方式:
const obj = {
name: 'Object',
sayHello: function() {
console.log(`Hello, ${this.name}!`);
console.log(this); // obj
}
};
obj.sayHello(); // Hello, Object!
// 方法赋值给变量后调用
const sayHello = obj.sayHello;
sayHello(); // Hello, undefined!(this指向全局对象)2.3 构造函数调用
当函数通过new关键字作为构造函数调用时,this指向新创建的实例对象。
使用方式:
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this); // Person { name: 'John', age: 30 }
}
const person = new Person('John', 30);
console.log(person.name); // John
console.log(person.age); // 30
// 不使用new关键字调用构造函数
const notPerson = Person('John', 30);
console.log(notPerson); // undefined(构造函数没有返回值)
console.log(name); // John(this指向全局对象,污染全局变量)2.4 箭头函数
箭头函数没有自己的this,它会捕获定义时所在上下文的this值,作为自己的this值。
使用方式:
const obj = {
name: 'Object',
regularFunction: function() {
console.log(this); // obj
const arrowFunction = () => {
console.log(this); // obj(捕获外部函数的this)
};
arrowFunction();
},
arrowMethod: () => {
console.log(this); // window(箭头函数没有自己的this,捕获全局上下文的this)
}
};
obj.regularFunction();
obj.arrowMethod();2.5 事件处理函数
在DOM事件处理函数中,this指向触发事件的元素。
使用方式:
// HTML: <button id="btn">Click me</button>
document.getElementById('btn').addEventListener('click', function() {
console.log(this); // <button id="btn">Click me</button>
});
// 箭头函数作为事件处理函数
document.getElementById('btn').addEventListener('click', () => {
console.log(this); // window(箭头函数捕获全局上下文的this)
});2.6 定时器函数
在setTimeout和setInterval的回调函数中,this指向全局对象(在浏览器中是window)。
使用方式:
const obj = {
name: 'Object',
timeoutFunction: function() {
setTimeout(function() {
console.log(this); // window(定时器回调函数中的this指向全局对象)
console.log(`Hello, ${this.name}!`); // Hello, undefined!
}, 1000);
// 使用箭头函数
setTimeout(() => {
console.log(this); // obj(箭头函数捕获外部函数的this)
console.log(`Hello, ${this.name}!`); // Hello, Object!
}, 1000);
}
};
obj.timeoutFunction();3. 改变this的指向
在JavaScript中,我们可以通过以下方法改变函数中this的指向:
3.1 call方法
call方法调用一个函数,并用指定的this值和单独的参数列表。
使用方式:
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person1 = { name: 'John' };
const person2 = { name: 'Jane' };
// 使用call方法改变this指向
greet.call(person1, 'Hello', '!'); // Hello, John!
greet.call(person2, 'Hi', '.'); // Hi, Jane.
// 没有参数时
greet.call(person1); // undefined, Johnundefined特点:
call方法的第一个参数是this的指向对象。- 从第二个参数开始,是传递给函数的参数列表。
- 函数会立即执行。
3.2 apply方法
apply方法调用一个函数,并用指定的this值和参数数组(或类数组对象)。
使用方式:
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person1 = { name: 'John' };
const person2 = { name: 'Jane' };
// 使用apply方法改变this指向
greet.apply(person1, ['Hello', '!']); // Hello, John!
greet.apply(person2, ['Hi', '.']); // Hi, Jane.
// 没有参数时
greet.apply(person1); // undefined, Johnundefined
// 使用apply传递数组参数
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum.apply(null, numbers)); // 6(this指向全局对象)特点:
apply方法的第一个参数是this的指向对象。- 第二个参数是一个数组或类数组对象,包含传递给函数的参数。
- 函数会立即执行。
3.3 bind方法
bind方法创建一个新函数,当调用时,将其this关键字设置为提供的值,并在调用新函数时,将给定的参数列表作为原函数的参数序列的前几个参数。
使用方式:
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person1 = { name: 'John' };
const person2 = { name: 'Jane' };
// 使用bind方法创建新函数
const greetPerson1 = greet.bind(person1);
const greetPerson2 = greet.bind(person2, 'Hi'); // 预先设置第一个参数
// 调用新函数
greetPerson1('Hello', '!'); // Hello, John!
greetPerson2('.'); // Hi, Jane.
// 绑定this后原函数不受影响
greet('Hello', '!'); // Hello, undefined!(this指向全局对象)特点:
bind方法的第一个参数是this的指向对象。- 从第二个参数开始,是预先设置的参数,会在调用新函数时传递给原函数。
bind方法返回一个新函数,不会立即执行。
3.4 三种方法的比较
| 方法 | 特点 | 是否立即执行 | 参数传递方式 |
|---|---|---|---|
| call | 改变this指向 | 是 | 单独的参数列表 |
| apply | 改变this指向 | 是 | 参数数组或类数组对象 |
| bind | 改变this指向并创建新函数 | 否 | 预先设置的参数列表 |
4. 箭头函数中的this
箭头函数是ES6引入的新特性,它与普通函数的一个重要区别是箭头函数没有自己的this值,而是捕获定义时所在上下文的this值。
4.1 箭头函数的this绑定
箭头函数的this绑定是词法的,它会捕获定义时所在上下文的this值,作为自己的this值,并且在箭头函数的整个生命周期中保持不变。
使用方式:
const obj = {
name: 'Object',
regularFunction: function() {
console.log(this); // obj
// 普通函数
const regularInnerFunction = function() {
console.log(this); // window(普通函数的this指向调用者)
};
// 箭头函数
const arrowInnerFunction = () => {
console.log(this); // obj(捕获外部函数的this)
};
regularInnerFunction();
arrowInnerFunction();
},
arrowMethod: () => {
console.log(this); // window(箭头函数捕获全局上下文的this)
}
};
obj.regularFunction();
obj.arrowMethod();4.2 箭头函数的优点
- 简化代码:箭头函数的语法更简洁,减少了代码量。
- 避免this指向问题:箭头函数捕获外部上下文的
this,避免了普通函数中this指向不明确的问题。 - 适合回调函数:箭头函数特别适合作为回调函数,因为它可以保持外部上下文的
this指向。
4.3 箭头函数的注意事项
- 不能作为构造函数:箭头函数没有自己的
this,不能使用new关键字调用。 - 没有arguments对象:箭头函数没有自己的
arguments对象,但可以访问外部函数的arguments对象。 - 不能使用yield关键字:箭头函数不能作为生成器函数。
- 不适合作为对象方法:箭头函数作为对象方法时,
this不会指向该对象,而是捕获外部上下文的this。
5. 严格模式下的this
在严格模式('use strict')下,this的指向与非严格模式有所不同:
使用方式:
// 非严格模式
function nonStrictFunction() {
console.log(this); // window
}
nonStrictFunction();
// 严格模式
'use strict';
function strictFunction() {
console.log(this); // undefined
}
strictFunction();
// 严格模式下作为对象方法调用
const obj = {
name: 'Object',
strictMethod: function() {
'use strict';
console.log(this); // obj
}
};
obj.strictMethod();
// 严格模式下使用call/apply/bind
function greet() {
'use strict';
console.log(this);
}
greet.call(null); // null
greet.call(undefined); // undefined
greet.call(123); // 123特点:
- 在严格模式下,全局函数中的
this指向undefined,而不是全局对象。 - 在严格模式下,使用
call、apply或bind方法时,this的指向会严格按照传入的值,即使传入的是null或undefined。 - 在严格模式下,作为对象方法调用的函数,
this仍然指向调用该方法的对象。
6. this的优先级
当多种this绑定规则同时存在时,它们的优先级从高到低依次是:
- new绑定:使用
new关键字调用构造函数时,this指向新创建的实例。 - 显式绑定:使用
call、apply或bind方法改变this的指向。 - 隐式绑定:函数作为对象的方法调用时,
this指向调用该方法的对象。 - 默认绑定:在全局上下文中,
this指向全局对象;在严格模式下,this指向undefined。
使用方式:
// 1. new绑定优先级高于显式绑定
function Person(name) {
this.name = name;
console.log(this);
}
const obj = { name: 'Object' };
const boundPerson = Person.bind(obj);
const person = new boundPerson('John'); // Person { name: 'John' }(new绑定优先级更高)
console.log(obj.name); // Object(obj的name没有被改变)
// 2. 显式绑定优先级高于隐式绑定
const obj1 = {
name: 'Obj1',
greet: function() {
console.log(this.name);
}
};
const obj2 = { name: 'Obj2' };
obj1.greet(); // Obj1(隐式绑定)
obj1.greet.call(obj2); // Obj2(显式绑定优先级更高)
// 3. 隐式绑定优先级高于默认绑定
const obj3 = {
name: 'Obj3',
greet: function() {
console.log(this.name);
}
};
const greet = obj3.greet;
greet(); // undefined(默认绑定)
obj3.greet(); // Obj3(隐式绑定优先级更高)面试常见问题
1. 什么是this?this的指向取决于什么?
this是JavaScript中的一个特殊变量,它指向当前执行代码的上下文对象。
this的指向取决于函数的调用方式,与函数的定义位置无关。具体来说:
- 在全局上下文中,
this指向全局对象。 - 当函数作为对象的方法调用时,
this指向调用该方法的对象。 - 当函数通过
new关键字作为构造函数调用时,this指向新创建的实例对象。 - 箭头函数没有自己的
this,它会捕获定义时所在上下文的this值。 - 在DOM事件处理函数中,
this指向触发事件的元素。 - 在定时器函数中,
this指向全局对象。
2. 如何改变this的指向?call、apply和bind有什么区别?
改变this指向的方法:
- call:调用函数,并用指定的
this值和单独的参数列表。 - apply:调用函数,并用指定的
this值和参数数组(或类数组对象)。 - bind:创建一个新函数,当调用时,将其
this关键字设置为提供的值,并在调用新函数时,将给定的参数列表作为原函数的参数序列的前几个参数。
区别:
- 执行方式:
call和apply会立即执行函数,bind会返回一个新函数,不会立即执行。 - 参数传递:
call接收单独的参数列表,apply接收参数数组或类数组对象,bind接收预先设置的参数列表。
3. 箭头函数中的this与普通函数有什么不同?
箭头函数中的this:
- 箭头函数没有自己的
this,它会捕获定义时所在上下文的this值,作为自己的this值。 - 箭头函数的
this绑定是词法的,在箭头函数的整个生命周期中保持不变。 - 箭头函数不能作为构造函数使用,因为它没有自己的
this。
普通函数中的this:
- 普通函数的
this值在函数执行时确定,取决于函数的调用方式。 - 普通函数可以作为构造函数使用,使用
new关键字调用时,this指向新创建的实例对象。
4. 下面的代码输出什么?为什么?
const obj = {
name: 'Object',
sayHello: function() {
console.log(`Hello, ${this.name}!`);
}
};
const sayHello = obj.sayHello;
sayHello();输出:
Hello, undefined!原因:当obj.sayHello被赋值给变量sayHello后,函数的调用方式变成了全局调用,此时this指向全局对象(在浏览器中是window),而全局对象中没有name属性,所以输出Hello, undefined!。
5. 如何修改上面的代码,使其输出Hello, Object!?
方法1:使用bind方法
const obj = {
name: 'Object',
sayHello: function() {
console.log(`Hello, ${this.name}!`);
}
};
const sayHello = obj.sayHello.bind(obj);
sayHello(); // Hello, Object!方法2:使用箭头函数
const obj = {
name: 'Object',
sayHello: function() {
const innerSayHello = () => {
console.log(`Hello, ${this.name}!`);
};
return innerSayHello;
}
};
const sayHello = obj.sayHello();
sayHello(); // Hello, Object!方法3:使用call/apply方法
const obj = {
name: 'Object',
sayHello: function() {
console.log(`Hello, ${this.name}!`);
}
};
const sayHello = obj.sayHello;
sayHello.call(obj); // Hello, Object!
sayHello.apply(obj); // Hello, Object!6. 下面的代码输出什么?为什么?
function Person(name) {
this.name = name;
this.sayHello = function() {
setTimeout(function() {
console.log(`Hello, ${this.name}!`);
}, 1000);
};
}
const person = new Person('John');
person.sayHello();输出:
Hello, undefined!原因:setTimeout的回调函数是一个普通函数,它的this指向全局对象(在浏览器中是window),而全局对象中没有name属性,所以输出Hello, undefined!。
7. 如何修改上面的代码,使其输出Hello, John!?
方法1:使用箭头函数
function Person(name) {
this.name = name;
this.sayHello = function() {
setTimeout(() => {
console.log(`Hello, ${this.name}!`);
}, 1000);
};
}
const person = new Person('John');
person.sayHello(); // Hello, John!方法2:使用bind方法
function Person(name) {
this.name = name;
this.sayHello = function() {
setTimeout(function() {
console.log(`Hello, ${this.name}!`);
}.bind(this), 1000);
};
}
const person = new Person('John');
person.sayHello(); // Hello, John!方法3:使用变量保存this
function Person(name) {
this.name = name;
this.sayHello = function() {
const self = this;
setTimeout(function() {
console.log(`Hello, ${self.name}!`);
}, 1000);
};
}
const person = new Person('John');
person.sayHello(); // Hello, John!8. 什么是词法作用域?它与this的指向有什么关系?
词法作用域是指变量的作用域由其在代码中的定义位置决定,而不是由其执行位置决定。
与this的指向的关系:
- 普通函数的
this指向由其执行方式决定,与词法作用域无关。 - 箭头函数的
this绑定是词法的,它会捕获定义时所在上下文的this值,作为自己的this值,与词法作用域类似。
9. 在严格模式下,this的指向有什么不同?
严格模式下的this:
- 在严格模式下,全局函数中的
this指向undefined,而不是全局对象。 - 在严格模式下,使用
call、apply或bind方法时,this的指向会严格按照传入的值,即使传入的是null或undefined。 - 在严格模式下,作为对象方法调用的函数,
this仍然指向调用该方法的对象。
10. 什么是this的优先级?请按从高到低的顺序排列。
this的优先级从高到低依次是:
- new绑定:使用
new关键字调用构造函数时,this指向新创建的实例。 - 显式绑定:使用
call、apply或bind方法改变this的指向。 - 隐式绑定:函数作为对象的方法调用时,
this指向调用该方法的对象。 - 默认绑定:在全局上下文中,
this指向全局对象;在严格模式下,this指向undefined。
11. 箭头函数为什么不能作为构造函数?
箭头函数不能作为构造函数的原因:
- 箭头函数没有自己的
this值,它会捕获定义时所在上下文的this值。 - 箭头函数没有
prototype属性,而构造函数需要使用prototype来创建实例对象。 - 使用
new关键字调用函数时,会创建一个新对象,并将函数的this指向该新对象,但箭头函数没有自己的this,所以不能作为构造函数使用。
12. 如何在箭头函数中访问arguments对象?
箭头函数没有自己的arguments对象,但可以通过以下方法访问外部函数的arguments对象:
方法1:使用剩余参数
function outer(...args) {
const inner = () => {
console.log(args); // 访问外部函数的参数
};
inner();
}
outer(1, 2, 3); // [1, 2, 3]方法2:在外部函数中保存arguments对象
function outer() {
const args = arguments;
const inner = () => {
console.log(args); // 访问外部函数的arguments对象
};
inner();
}
outer(1, 2, 3); // Arguments(3) [1, 2, 3]总结
this关键字是JavaScript中的一个特殊变量,它指向当前执行代码的上下文对象。理解this的指向规则对于掌握JavaScript至关重要。
this的指向规则:
- 全局上下文:指向全局对象。
- 函数作为对象方法调用:指向调用该方法的对象。
- 构造函数调用:指向新创建的实例对象。
- 箭头函数:捕获定义时所在上下文的
this值。 - 事件处理函数:指向触发事件的元素。
- 定时器函数:指向全局对象。
改变this指向的方法:
call:调用函数,并用指定的this值和单独的参数列表。apply:调用函数,并用指定的this值和参数数组。bind:创建一个新函数,将其this关键字设置为提供的值。
箭头函数的this:
- 箭头函数没有自己的
this,它会捕获定义时所在上下文的this值。 - 箭头函数的
this绑定是词法的,在整个生命周期中保持不变。 - 箭头函数不能作为构造函数使用。
- 箭头函数没有自己的
严格模式下的this:
- 严格模式下,全局函数中的
this指向undefined。 - 严格模式下,使用
call、apply或bind方法时,this的指向会严格按照传入的值。
- 严格模式下,全局函数中的
this的优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
通过理解和掌握this关键字的指向规则,你可以编写更加清晰、可维护的JavaScript代码,避免常见的this指向问题。