一、前言
最近在学习react和Js,开局就被this摆了一道。由于我是Java出身,在Java中this指向非常清晰,所以当面对ES6中的this动态绑定时总觉得不明所以,大为震撼,因此准备在学习时候总结一番,争取弄清楚它。
二、this的背景
2.1、this 是什么
this 是一个特殊的关键字,它指向函数执行时的上下文对象 。简单来说,它告诉我们在当前函数被调用时,这个函数是“属于”哪个对象的。
2.2、为什么需要this
通过this,我们可以获取当前的对象,从而访问当前对象的属性和行为。比如:
1.访问对象的属性和方法
const person = {
name: '张三',
sayHello: function() {
console.log(`你好,我叫 ${this.name}`); // this.name 访问 person 对象的 name 属性
}
};
person.sayHello(); // 输出: 你好,我叫 张三
2.在构造函数中初始化
function Person(name, age) {
this.name = name; // 为新实例添加 name 属性
this.age = age; // 为新实例添加 age 属性
this.greet = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
}
const p1 = new Person('李四', 25);
3.事件处理中引用
<button id="myButton">点击我</button>
<script>
document.getElementById('myButton').addEventListener('click', function() {
console.log(this.textContent); // this 指向 <button> 元素,输出: 点击我
this.style.backgroundColor = 'lightblue'; // 改变按钮背景色
});
</script>
三、为什么JS中this语法这么复杂
Js的thi 复杂性是其动态性、函数式编程特性以及历史演进的产物。它提供了极大的灵活性,允许函数在不同上下文中复用。
- 动态上下文: JavaScript 的 this是动态绑定 的,它的值在函数被调用时才确定,而不是在函数定义时确定。
- 函数是一等公民:在 Js 中,函数可以像普通变量一样被传递、赋值和作为参数。这导致函数可以在各种不同的上下文中被调用,而 this需要适应这些不同的调用场景。
- 缺乏传统类的概念:在 ES6 引入class语法糖之前,Js是基于原型的面向对象语言。对象是通过函数和原型链创建的,这种基于原型的继承和对象创建方式,使得 this 的行为更加灵活但也更难以捉摸。
- 回调函数中的this丢失:在异步编程和事件处理中,函数经常作为回调函数被传递。当回调函数被调用时,它通常会失去原始上下文的 this 绑定,这导致了常见的this丢失问题,需要使用 bind() 或箭头函数来解决。
四、JS中都有哪些this
4.1、全局上下文中的this
代码
//1、非严格模式下
console.log('全局作用域中的this:', this); // this指向window对象
//2、严格模式下
'use strict';
console.log('严格模式下全局作用域中的this:', this); // this指向window对象
总结
在全局上下文中,无论是否开启严格模式,this均指向全局对象,在浏览器中为window对象(以下默认均为浏览器中对象)。
扩展
什么是全局上下文?
全局上下文是 JavaScript 运行时最先创建的环境,存放着整个程序都能访问的内容。 在浏览器环境中,全局上下文是window对象;
什么是严格模式?
严格模式是 JavaScript 的一种运行模式,它会执行更严格的语法检查,让错误更容易被发现。
4.2、普通函数中的this
代码
function fn1() {
console.log('普通函数中的this:', this);
}
fn1(); // 普通函数中的this: window对象
function fn2() {
'use strict';
console.log('严格模式下的this:', this);
}
fn2(); // 严格模式下的this:undefined
总结
在普通函数调用中,非严格模式下this指向window对象,严格模式下this指向undefined。
(use strict除了可以放在函数内部外,还可以放在脚本文件顶部,如果放在全局作用域顶部,标志整个脚本均为严格模式。)
扩展
为什么严格模式下this指向undefined?
在严格模式下,如果一个函数在被调用时,其 this 值没有被显式设置(例如通过对象方法调用、 call / apply / bind ),那么 this 的值将是 undefined ,而不是全局对象。这是为了避免意外地修改全局对象,并使代码行为更可预测。
4.3、对象方法中的this
代码
const obj = {
name: '测试对象',
fn1: function() {
console.log('对象方法中的this:', this);
},
fn2: function() {
'use strict';
console.log('严格模式下对象方法中的this:', this);
}
};
obj.fn1(); // 对象方法中的this: obj对象
obj.fn2(); // 严格模式下对象方法中的this: obj对象
总结
在对象方法中,无论是否开启严格模式,this均指向调用该方法的对象。
扩展
对象字面量是什么?
这种无需根据类就创建的对象的方式,被称为对象字面量。
对象字面量允许直接创建对象实例 ,而无需预先定义一个类。可以直接在代码中“写出”一个对象,就像写一个数字或字符串一样。这种方式非常适合创建一次性使用的、结构简单的数据对象,或者作为函数参数传递配置对象等。
4.4、箭头函数中的this
代码
// 普通函数
function normalFunction() {
console.log('普通函数中的this:', this); // window或undefined(严格模式)
}
// 箭头函数
const arrowFunction = () => {
console.log('箭头函数中的this:', this); // 继承外层作用域的this,外层为全局上下文无论是否是严格模式都是window
}
// 对象中的方法
const obj = {
name: '测试对象',
normalMethod: function() {
console.log('对象普通方法中的this:', this); // 指向obj对象
},
arrowMethod: () => {
console.log('对象箭头方法中的this:', this); // 继承外层作用域(定义时所处的作用域)的this
}
};
// 调用测试
normalFunction();
arrowFunction();
obj.normalMethod();
obj.arrowMethod();
// 改变this指向测试
const newObj = { name: '新对象' };
normalFunction.call(newObj); // this指向newObj
arrowFunction.call(newObj); // this仍然继承外层作用域的this
总结
- 箭头函数没有自己的 this 绑定,它会捕获其定义时所处的外层作用域的 this 值。
- this 的指向在箭头函数定义时就已经确定,并且不会因为调用方式包括 call 、 apply 、 bind 而改变。
- 在对象方法中,箭头函数也没有自己的this绑定,同样使用外层作用域的this值,该对象字面量中,外层作用域仍然是全局作用域。
扩展
什么是箭头函数
箭头函数是 ES6中引入的一种新的函数定义方式,它使用 => 符号来定义函数,语法上比传统函数表达式更简洁。
为什么要有箭头函数
- 箭头函数语法简洁,代码紧凑,可读性高。
- 解决 this 绑定问题 :在传统函数中,this 的指向是动态的,取决于函数被调用的方式,这经常导致在回调函数或嵌套函数中 this 指向不符合预期的问题。箭头函数通过词法作用域绑定 this ,解决了这一痛点。
normalFunction.call(newObj)这种用法是什么
在 JavaScript 中,所有的函数都是 Function 对象的实例,因此它们都继承了 Function.prototype 上的方法,包括 call() , apply() , 和 bind() 。
在传统函数中,this 的指向是动态的,取决于函数被调用的方式,这经常导致在回调函数或嵌套函数中 this 指向不符合预期的问题。
call() , apply() , bind() 这些显示绑定方法,就是为了解决这种动态性带来的问题,允许开发者强制指定函数执行时的 this 值,从而更好地控制函数的上下文。
4.5、类中的this
代码
class TestClass {
constructor(name) {
this.name = name;
console.log('类构造函数中的this:', this); // 类构造函数中的this: TestClass实例
}
// 普通方法
normalMethod() {
console.log('类普通方法中的this:', this); // 类普通方法中的this: TestClass实例
}
//箭头函数
arrowMethod = () => {
console.log('类箭头函数方法中的this:', this); // 类箭头函数方法中的this: TestClass实例
}
// 静态方法
static staticMethod() {
console.log('类静态方法中的this:', this); // 类静态方法中的this: TestClass类本身
}
}
const instance = new TestClass('测试类');
instance.arrowMethod();
instance.normalMethod();
TestClass.staticMethod();
总结
在类中this 的指向取决于函数被调用的方式:在实例方法中指向实例,在静态方法中指向类本身,在构造函数中指向新创建的实例,箭头函数中也指向当前实例。
严格模式下也相同,JavaScript 的类内部的代码默认就处于严格模式下,因此无需特别指明。
扩展
为什么类中的箭头函数,this不是全局上下文对象 (对象方法中指向的是全局上下文) ,而是类的当前实例对象?
当我们在类中定义箭头函数的时候,实际上是定义了一个类字段
- 这些类字段是在 实例被创建时 (即调用 new TestClass() 时),在 构造函数执行之后、返回实例之前 ,被添加到每个实例上的。
- 当 arrowMethod 这个箭头函数被定义并赋值给实例的 arrowMethod 属性时,它所处的“外层作用域”就是构造函数执行时的上下文 。因此指向的是当前类的实例对象。
而对象字面量创建的方法本身不创建作用域,所以该对象中的箭头函数中this指向的是全局上下文。
4.6、事件处理器中的this
代码
// 创建一个按钮元素
const button = document.createElement('button');
button.textContent = '点击测试this';
document.body.appendChild(button);
class EventTest {
constructor(name) {
this.name = name;
// 在构造函数中绑定方法,确保this指向实例
this.handleClick = this.handleClick.bind(this);
}
// 普通方法作为事件处理器
handleClick() {
console.log('普通方法事件处理器中的this:', this);
}
// 箭头函数作为事件处理器
handleClickArrow = (event) => {
console.log('箭头函数事件处理器中的this:', this);
}
// 直接绑定事件处理器方法
addEventListeners() {
// 使用普通函数(已绑定)
button.addEventListener('click', this.handleClick);
// 使用箭头函数
button.addEventListener('click', this.handleClickArrow);
// 未绑定的普通函数(this将指向button元素)
button.addEventListener('click', function() {
console.log('未绑定的事件处理器中的this:', this);
});
// 内联箭头函数(保持外部this指向)
button.addEventListener('click', () => {
console.log('内联箭头函数事件处理器中的this:', this);
});
}
}
const eventTest = new EventTest('事件测试');
eventTest.addEventListeners();
总结
在事件处理中,除箭头函数外,其余函数均指向dom元素,除非在构造函数中使用.bind()绑定,指明绑定的this为当前实例对象。
由于类中默认开启严格模式,因此开启后效果相同。
扩展
事件处理器中的this丢失
- 当一个函数被用作 DOM 元素的事件监听器时,无论这个函数最初是如何定义的(普通函数、类方法、静态方法),只要它是一个 普通函数 (非箭头函数),并且没有通过 bind() 等方式显式绑定 this ,那么在事件触发时,浏览器会 自动将这个函数的 this 上下文设置为触发事件的 DOM 元素 。
- 这是 DOM 规范中对事件处理函数 this 的一个特殊约定,目的是为了方便开发者在事件处理函数中直接操作触发事件的元素。
4.7、回调函数中的this
代码
// 定义一个对象
const person = {
name: '张三',
age: 20,
sayHi() {
console.log('普通方法中的this:', this); // this指向对象实例
},
// 普通箭头函数
sayHiArrow: () => {
console.log('普通箭头函数中的this:', this); // this指向window对象
},
sayHiAsync() {
// 普通回调函数中的this指向全局对象(window)或undefined
setTimeout(function () {
console.log('1秒后: 普通函数中的this:', this);
}, 1000);
// 箭头函数中的this继承自外层作用域(sayHiAsync方法的作用域),person对象
setTimeout(() => {
console.log('2秒后: 箭头函数中的this:', this);
}, 2000);
},
sayHiAsyncStrict() {
'use strict';
// 普通回调函数中的this指向全局对象(window)或undefined
setTimeout(function () {
console.log('1秒后: 严格模式下,普通函数中的this:', this);
}, 1000);
// 回调函数中箭头函数中的this继承自外层作用域(sayHiAsyncStrict方法的的作用域),也就是persond对象
setTimeout(() => {
console.log('2秒后: 严格模式下,回调函数中箭头函数中的this:', this);
}, 2000);
}
};
// 调用方法
person.sayHi();
person.sayHiArrow();
person.sayHiAsync();
person.sayHiAsyncStrict();
总结
在回调函数中
- 普通函数的this指向丢失,this指向window对象。
- 始终继承定义时的外层作用域(此处为
sayHiAsync或sayHiAsyncStrict的this,即person对象) - 严格模式下效果相同。
总结
以下对以上内容进行总结,方便回顾。
| 场景 | 严格模式 | 非严格模式 | 关键特征 |
|---|---|---|---|
| 全局上下文 | Window | Window | 恒指向全局对象 |
| 普通函数 | undefined | Window | 严格模式丢失上下文 |
| 对象方法 | 调用对象 | 调用对象 | 由调用者决定 |
| 箭头函数 | 词法作用域this | 词法作用域this | 定义时即固定,不受 call/bind 影响 |
| 类构造函数/方法 | 当前实例 | 当前实例 | 类中默认严格模式 |
| 类静态方法 | 类本身 | 类本身 | 指向 class 而非实例 |
| 事件处理器 | DOM元素触发 | DOM元素触发 | 浏览器隐式绑定 this 到 DOM 元素 |
| 回调函数(异步) | Window | Window | 动态丢失上下文 |