【JS系列】:this指向

48 阅读11分钟

一、前言

最近在学习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 复杂性是其动态性函数式编程特性以及历史演进的产物。它提供了极大的灵活性,允许函数在不同上下文中复用。

  1. 动态上下文: JavaScript 的 this是动态绑定 的,它的值在函数被调用时才确定,而不是在函数定义时确定。
  2. 函数是一等公民:在 Js 中,函数可以像普通变量一样被传递、赋值和作为参数。这导致函数可以在各种不同的上下文中被调用,而 this需要适应这些不同的调用场景。
  3. 缺乏传统类的概念:在 ES6 引入class语法糖之前,Js是基于原型的面向对象语言。对象是通过函数和原型链创建的,这种基于原型的继承和对象创建方式,使得 this 的行为更加灵活但也更难以捉摸。
  4. 回调函数中的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

总结

  1. 箭头函数没有自己的 this 绑定,它会捕获其定义时所处的外层作用域的 this 值。
  2. this 的指向在箭头函数定义时就已经确定,并且不会因为调用方式包括 call 、 apply 、 bind 而改变。
  3. 在对象方法中,箭头函数也没有自己的this绑定,同样使用外层作用域的this值,该对象字面量中,外层作用域仍然是全局作用域。

扩展

什么是箭头函数

箭头函数是 ES6中引入的一种新的函数定义方式,它使用 => 符号来定义函数,语法上比传统函数表达式更简洁。

为什么要有箭头函数

  1. 箭头函数语法简洁,代码紧凑,可读性高。
  2. 解决 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(); 

总结

在回调函数中

  1. 普通函数的this指向丢失,this指向window对象。
  2. 始终继承定义时的外层作用域(此处为 sayHiAsyncsayHiAsyncStrictthis,即 person 对象)
  3. 严格模式下效果相同。

总结

以下对以上内容进行总结,方便回顾。

场景严格模式非严格模式关键特征
全局上下文WindowWindow恒指向全局对象
普通函数undefinedWindow严格模式丢失上下文
对象方法调用对象调用对象由调用者决定
箭头函数词法作用域this词法作用域this定义时即固定,不受 call/bind 影响
类构造函数/方法当前实例当前实例类中默认严格模式
类静态方法类本身类本身指向 class 而非实例
事件处理器DOM元素触发DOM元素触发浏览器隐式绑定 this 到 DOM 元素
回调函数(异步)WindowWindow动态丢失上下文