【javascript】this关键字
this 指向总结
this
的指向取决于函数的调用方式:
- 全局上下文:指向全局对象(
window
或global
) - 普通函数调用:指向全局对象或
undefined
(严格模式) - 对象方法调用:指向该对象
- 构造函数调用:指向新创建的对象
call
、apply
、bind
:指向指定的对象- 箭头函数:捕获其所在上下文的
this
值 - 事件处理程序:指向触发事件的 DOM 元素
setTimeout
和setInterval
:指向全局对象
this 介绍
在绝大多数情况下,函数的调用方式决定了 this
的值(运行时绑定)。this
不能在执行期间被赋值,并且在每次函数被调用时 this
的值也可能会不同。
ES5 引入了 bind
方法来设置函数的 this
值,而不用考虑函数如何被调用的。
ES2015(es6) 引入了箭头函数,箭头函数不提供自身的 this 绑定(this 的值将保持为闭合词法上下文的值)。
this 指向:
在 JavaScript 中,this
关键字的指向在不同上下文中会有不同的结果。
以下是几种常见的场景:
1. 全局上下文
在全局执行环境中,this
指向全局对象。
- 在浏览器中,全局对象是
window
- 在 Node.js 中,全局对象是
global
console.log(this); // 在浏览器中输出 window 对象,在 Node.js 中输出 global 对象
2. 函数上下文
当 this
出现在函数内部时,它的指向取决于函数的调用方式。
普通函数调用
在 JavaScript 中,严格模式和非严格模式下 this
的指向区别主要体现在普通函数调用时的行为上。在非严格模式下,普通函数调用时 this
指向全局对象;而在严格模式下,普通函数调用时 this
指向 undefined
。在其他调用场景(如对象方法调用、构造函数调用等)中,严格模式和非严格模式下 this 的指向是一致的。
当函数作为一个独立的函数被调用时,this
的指向取决于运行环境:
-
在严格模式下:
- 如果函数在没有被任何东西访问的情况下被调用,
this
将是undefined
function logThis() {"use strict";console.log(this);}[1, 2, 3].forEach(logThis); // undefined、undefined、undefined```
- 如果方法被访问的值是一个原始值,
this
也将是一个原始值function getThisStrict() {"use strict"; // 进入严格模式return this; } // 仅用于演示——你不应该改变内置的原型对象 Number.prototype.getThisStrict = getThisStrict; console.log(typeof (1).getThisStrict()); // "number"
- 如果函数在没有被任何东西访问的情况下被调用,
-
在非严格模式下,一个特殊的过程称为
this
替换以确保this
的值总是一个对象。这意味着:- 如果一个函数被调用时
this
被设置为undefined
或null
,this
会被替换为 globalThis。 - 如果函数被调用时
this
被设置为一个原始值,this 会被替换为原始值的包装对象。function getThis() {return this; } // 仅用于演示——你不应该修改内置的原型对象 Number.prototype.getThis = getThis; console.log(typeof (1).getThis()); // "object" console.log(getThis() === globalThis); // true
- 当一个函数作为回调函数传递时,
this
的值取决于如何调用回调,这由 API 的实现者决定。回调函数通常以undefined
作为this
的值被调用(直接调用,而不附加到任何对象上),这意味着如果函数是在非严格模式,this
的值会是全局对象(globalThis)。这在迭代数组方法、Promise() 构造函数等例子中都是适用的。
一些 API 允许你为回调函数的调用设置一个 this 值。例如,所有的迭代数组方法和相关的方法,如Set.prototype.forEach(),都接受一个可选的 thisArg 参数。function logThis() {console.log(this); } [1, 2, 3].forEach(logThis); // Window {...}、Window {...}、Window {...}
[1, 2, 3].forEach(logThis, { name: "obj" }); // { name: 'obj' }, { name: 'obj' }, { name: 'obj' }
- 如果一个函数被调用时
作为对象方法调用
当函数作为对象的方法被调用时,this
指向该对象。
const obj = {name: 'test',test: function() {console.log(this); }
};
obj.test();
// 输出 { name: 'test', test: f() }
构造函数调用
当函数作为构造函数被调用(使用 new
关键字),this
指向新创建的对象。
function Person(name) {this.name = name;
}
const person = new Person('test');
console.log(person.name);
// 输出 'test'
使用 call
、apply
或 bind
调用
call
、apply
或 bind
方法可以明确指定函数内部 this
的指向。
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };function test() {console.log(this.name);
}test.call(obj1); // 输出 'obj1'
test.apply(obj2); // 输出 'obj2'const boundFunc = test.bind(obj1);
boundFunc(); // 输出 'obj1'
3. super
在 JavaScript 的类继承中,super
关键字用于调用父类的构造函数或方法。当使用 super.method()
调用父类的方法时,该方法内部的 this
指向的是当前子类的实例,而不是父类的实例。这是因为 super.method() 的调用上下文是子类的实例,而不是父类的实例。
class Parent {constructor() {this.parentValue = "Parent";}getParentValue() {console.log(this.parentValue);return this.parentValue;}
}class Child extends Parent {constructor() {super(); // 调用父类的构造函数this.childValue = "Child";}getChildValue() {// 调用父类的 getParentValue 方法super.getParentValue(); // 输出 "Parent"console.log(this.childValue); // 输出 "Child"return this.childValue;}
}const child = new Child();
child.getChildValue();
// 输出:
// Parent
// Child
4. 类上下文
JavaScript 的类存在两种上下文:静态上下文和实例上下文。
-
实例上下文:包括构造函数、实例方法和实例字段初始化器。在这些地方,this 指向类的实例。
- 构造函数: 通过 new 调用,this 指向新创建的实例。
class Person2 {constructor(name) {this.name = name; // this = 新实例} } const alice = new Person2("Alice"); console.log(alice.name); // 输出: "Alice"
- 实例方法:this 是调用它的对象(通常是实例)。
window.name = "Global";class Person2 {constructor(name) {this.name = name; }greet() { //实例方法console.log(`Hello, I'm ${this.name}`);}}const alice = new Person2("Alice");alice.greet(); // 输出: "Hello, I'm Alice"// 如果方法被提取到其他对象--如下所示--this 将不再指向实例,this 将指向全局对const greetFn = alice.greet;greetFn();// 报错: Cannot read property 'name' of undefined// 报错原因:ES6 类语法设计上强制启用严格模式,确保代码更安全
- 实例字段初始化器:this 指向正在构造的实例
class Person3 {age = 30; // 实例字段getAge = () => {console.log(this.age); // 箭头函数绑定实例}; } const bob = new Person3(); bob.getAge(); // 输出: 30
- 构造函数: 通过 new 调用,this 指向新创建的实例。
-
静态上下文:包括静态方法、静态字段初始化器和静态初始化块。在这些地方,this 指向类本身(或子类)。
- 静态方法: this 指向类本身
class Person4 {static species = "Human";static describe() {console.log(`Species: ${this.species},this:${this}`); // this = Person 类} } Person4.describe();// 子类继承静态方法 class Student extends Person4 { } Student.describe(); //输出: // Species: Human, this: class Person4 { // static species = "Human"; // static describe() { // console.log(`Species: ${this.species},this:${this}`); // this = Person 类 // } // } // Species: Human, this: class Student extends Person4 { }
- 静态字段初始化器: this 指向类本身
class MyClass {static staticValue = 42;static staticArrow = () => {console.log(this.staticValue); // 箭头函数绑定类}; } MyClass.staticArrow(); // 输出: 42
- 静态初始化块: 初始化块中的 this 指向当前类
class MyClass2 {static x;static {this.x = 100; // this = MyClass} } console.log(MyClass2.x); // 输出: 100
- 静态方法: this 指向类本身
5. 箭头函数
箭头函数没有自己的 this
,它会捕获其所在上下文的 this
值。
const obj = {name: 'test',test: () => {console.log(this); // 输出全局对象(window 或 global)}
};
obj.test();
在全局代码中,无论是否在严格模式下,由于全局上下文绑定,this
值总是 globalThis
。
const globalObject = this;
const foo = () => this;
console.log(foo() === globalObject);
// 输出:true
箭头函数在其周围的作用域上创建一个 this
值的闭包,这意味着箭头函数的行为就像它们是“自动绑定”的——无论如何调用,this
都绑定到函数创建时的值(在上面的例子中,是全局对象)。在其他函数内部创建的箭头函数也是如此:**它们的 this
值保持为闭合词法上下文的 this
**。
const obj4 = {getThisGetter() {const getter = () => this;return getter;},
};
const fn = obj4.getThisGetter();
console.log(fn() === obj4);
// 输出:true
const fn2 = obj4.getThisGetter;
//调用 fn2()() 将返回 globalThis,因为它遵循 fn2() 的 this,由于它没有附加到任何对象上进行调用,所以是 globalThis。
console.log(fn2()() === globalThis);
// 输出:在非严格模式下为 true
当使用 call()、bind() 或 apply() 调用箭头函数时,thisArg 参数会被忽略。
const foo = () => this;
const obj3 = { name: "obj" };
// 尝试使用 call 设置 this
console.log(foo.call(obj3) === globalObject); // true
// 尝试使用 apply 设置 this
console.log(foo.apply(obj3) === globalObject); // true
// 尝试使用 bind 设置 this
const boundFoo = foo.bind(obj3);
console.log(boundFoo() === globalObject); // true
6. 事件处理程序
当函数作为事件处理程序时,this
指向触发该事件的 DOM 元素。
const button = document.createElement('button');
button.addEventListener('click', function() {console.log(this); // 输出按钮元素
});
7. setTimeout 和 setInterval
在 setTimeout
和 setInterval
中,this
指向全局对象。
setTimeout(function() {console.log(this); // 在浏览器中输出 window,在 Node.js 中输出 global
}, 1000);
拓展
补充一些概念。
JavaScript 字段初始化器(Field Initializers)
一、字段初始化器的定义
字段初始化器是 ES6+ 中直接在类中声明属性值的语法,用于 自动初始化属性,分为两类:
- 实例字段初始化器:为每个实例初始化属性。它们会在 构造函数执行前 运行,并可以访问实例的
this
- 静态字段初始化器:为类本身初始化属性。
class MyClass {instanceField = "实例属性"; // 实例字段初始化器static staticField = "静态属性"; // 静态字段初始化器
}
二、核心特性对比
特性 | 实例字段初始化器 | 静态字段初始化器 |
---|---|---|
所属对象 | 实例属性(每个实例独有) | 静态属性(类自身拥有) |
初始化时机 | 在构造函数执行 前 运行 | 在类定义加载时运行 |
this 指向 | 当前正在构造的实例 | 类本身(构造函数) |
访问其他字段 | 可访问已声明的实例字段(注意顺序) | 可访问已声明的静态字段 |
应用场景 | 实例默认值、方法绑定 | 类级别的配置、共享数据 |
三、实例字段初始化器详解
1. 初始化时机
- 在构造函数之前执行:字段初始化器的赋值操作发生在构造函数代码之前。
- 执行顺序:父类字段 → 子类字段 → 父类构造函数 → 子类构造函数。
class Test {a = console.log("字段初始化器执行");constructor() {console.log("构造函数执行");} } new Test(); // 输出顺序: // 字段初始化器执行 // 构造函数执行
2. 基础用法
class Person {name = "Anonymous"; // 实例字段初始化器age = 0; // 初始化为 0constructor(name) {if (name) this.name = name; // 可覆盖默认值}
}const alice = new Person("Alice");
console.log(alice.name); // "Alice"
console.log(alice.age); // 0
3. 箭头函数绑定 this
class Button {handleClick = () => { // 箭头函数绑定实例的 thisconsole.log("Clicked", this);};
}const btn = new Button();
document.addEventListener("click", btn.handleClick); // 无论何时调用,this 正确指向实例
4. 初始化顺序问题
class Problematic {a = this.b + 1; // ❌ 错误:b 尚未初始化b = 2;
}class Correct {b = 2;a = this.b + 1; // ✅ 正确:b 已初始化
}
5. 与传统构造函数初始化的对比
1. 构造函数中初始化(传统方式)
class Person {constructor() {this.name = "Anonymous";this.age = 0;}
}
2. 使用字段初始化器(ES6+)
class Person {name = "Anonymous";age = 0;constructor() {} // 可省略
主要区别:
特性 | 字段初始化器 | 构造函数初始化 |
---|---|---|
代码可读性 | 属性声明集中,更直观 | 分散在构造函数中 |
多构造函数支持 | 需在每个构造函数中重复初始化代码 | 只需在基类声明一次 |
执行顺序 | 在构造函数之前运行 | 在构造函数内部运行 |
访问其他字段 | 需注意声明顺序 | 可自由访问已初始化的字段 |
四、静态字段初始化器详解
1. 基础用法
class Config {static apiUrl = "https://api.example.com"; // 静态字段static maxRetry = 3;
}
console.log(Config.apiUrl); // "https://api.example.com"
2. 访问其他静态成员
class MathUtils {static PI = 3.14159;static radius = 10;static area = this.PI * this.radius ** 2; // ✅ 正确:访问静态字段
}
console.log(MathUtils.area); // 314.159
3. 静态初始化块(Static Block)
class Database {static connection;static {// 复杂初始化逻辑this.connection = connectToDB();}
}
五、执行顺序规则
- 父类静态字段 → 子类静态字段
- 父类实例字段 → 父类构造函数
- 子类实例字段 → 子类构造函数
class Parent {parentField = console.log("父类实例字段");static parentStatic = console.log("父类静态字段");constructor() { console.log("父类构造函数"); }
}class Child extends Parent {static childStatic = console.log("子类静态字段");childField = console.log("子类实例字段");constructor() { super();console.log("子类构造函数"); }
}new Child();
/* 输出顺序:
父类静态字段
子类静态字段
父类实例字段
父类构造函数
子类实例字段
子类构造函数
*/
六、注意事项
- 避免循环依赖
字段之间不要相互依赖未初始化的属性。 - 性能影响
避免在字段初始化器中执行复杂操作(每次实例化都会运行)。 - 动态值限制
字段初始化器无法接收参数,动态值需在构造函数中处理。
七、总结
- 用途
- 实例字段:定义实例的默认属性,解决
this
绑定问题。 - 静态字段:定义类级别的配置或共享数据。
- 实例字段:定义实例的默认属性,解决
- 优势
- 代码更简洁,属性声明集中化。
- 箭头函数自动绑定
this
,避免方法丢失上下文。
- 适用场景
- 简单默认值、事件处理器绑定、类级别常量。
静态初始化块(Static Initialization Blocks)
一、静态初始化块的定义
静态初始化块是 ES2022(ES13) 引入的特性,用于在类定义时执行复杂的静态成员初始化逻辑。它通过 static {}
语法声明,在类加载阶段运行一次,适用于需要多步骤计算、异常处理或依赖其他静态字段的场景。
基本语法:
class MyClass {static staticProperty;static { // 静态初始化块this.staticProperty = 'value';}
}
二、核心特性
1. 执行时机与顺序
- 触发条件:当类被首次加载时(如首次访问静态成员、创建实例或继承时)。
- 执行顺序:
- 父类静态块 → 子类静态块(遵循继承链)。
- 同一类中按代码顺序执行(静态块与静态字段初始化交织)。
- 所有静态块在 实例化对象前 完成。
示例:
class Parent {static parentProp = 'Parent';static { console.log('Parent 静态块'); }
}class Child extends Parent {static childProp = 'Child';static { console.log('Child 静态块'); }
}// 输出顺序:
// Parent 静态块
// Child 静态块
2. 作用域与 this
指向
- 作用域:静态块内可访问类的静态成员,无法访问实例成员。
this
指向:this
指向当前类(构造函数),而非实例。- 变量隔离:静态块内声明的变量为块级作用域,外部不可访问。
示例:
class Example {static x = 1;static {this.x = 2; // 修改静态属性const y = 3; // 块内局部变量console.log(this.x); // 2}
}
三、典型应用场景
1. 复杂静态属性初始化
当静态属性的初始化需要多步计算或依赖其他静态字段时:
class Circle {static radius = 10;static area;static circumference;static {this.area = Math.PI * this.radius ** 2;this.circumference = 2 * Math.PI * this.radius;}
}
2. 异常处理
在初始化过程中捕获错误:
class Config {static apiKey;static {try {this.apiKey = localStorage.getItem('apiKey') || 'default';} catch (e) {this.apiKey = 'default';}}
}
3. 初始化私有静态字段
配合 #
语法初始化私有静态字段:
class SecureData {static #secretKey;static {this.#secretKey = generateCryptoKey(); // 假设 generateCryptoKey 是外部函数}
}
四、与静态字段初始化器的对比
特性 | 静态字段初始化器(static x = 1; ) | 静态初始化块(static { ... } ) |
---|---|---|
适用场景 | 简单赋值(单行表达式) | 多步逻辑、异常处理、依赖其他字段 |
执行顺序 | 按代码顺序与静态块交织执行 | 按代码顺序执行(可包含多个块) |
代码灵活性 | 低(仅限表达式) | 高(支持语句、循环、条件判断) |
异常处理 | 无法直接捕获异常 | 支持 try/catch |
五、注意事项
-
避免循环依赖
静态块中不要引用尚未初始化的静态字段。class Problem {static a = this.b; // ❌ 错误:b 未初始化static b = 2; }
-
性能影响
避免在静态块中执行耗时操作,否则会延迟类加载速度。 -
不可访问实例成员
静态块无法通过this
访问实例属性或方法。class MyClass {instanceProp = 42;static {console.log(this.instanceProp); // undefined} }
六、总结
- 核心价值:提供类级别复杂初始化的解决方案,弥补静态字段初始化器的不足。
- 最佳实践:
- 简单赋值用静态字段,复杂逻辑用静态块。
- 优先处理静态成员间的依赖关系。
- 避免在静态块中引入副作用或全局污染。
- 兼容性:ES2022+ 支持,现代浏览器及 Node.js 16+ 已实现。
参考
this