首页 > 编程笔记

JS this到底指向什么?

在 JavaScript 中,this 的取值与它所处的上下文(Context)有关,并且在普通和严格模式下也有所不同。

在全局作用域上下文中,this 指向的是全局对象,全局对象在浏览器中是 window 对象,在 Node.js 中是当前模块,下方示例展示了全局作用域下的 this 的取值,代码如下:
this===window;         //true,浏览器环境下
this===module.exports  //true,Node.js环境下
关于不同环境下的全局对象,也可以使用 globalThis 来统一获取,但要注意 Node.js 中的 globalThis 指向的是 global 对象,与上例中全局作用域的 this 指向的 module.exports 不同,在浏览器下 globalThis 指向的对象为 window,与全局作用域 this 所指向的对象相同。

对于在函数上下文中的 this,它的值需要根据函数的调用方式决定。如果是普通的函数,且使用一般的方式进行调用(不使用 new 以构造函数进行调用),则函数体里的 this 指向的都是 globalThis,即 window(浏览器)或 global(Node.js),可以通过下方示例进行验证,代码如下:
function func(){
     console.log(this===globalThis);
}
func();//true
而在严格模式下,普通函数中 this 的值为 undefined。

如果以构造函数的方式调用函数,则 this 指向的是新创建的对象,代码如下:
function Func(){
     this.a=5;
}
const obj=new Func();
obj.a;  //5,obj即为Func()中this的指向

在对象的方法中,如果方法是使用普通函数定义的,则 this 指向的是当前对象,代码如下:
const obj={
     a:1,
     f(){
          console.log(obj===this);
          console.log(this.a);
     },
};
obj.f();//true
        //1
要判断普通函数中 this 的指向有一个简单直观的方法,即看它调用时左侧的代码:
如果对象方法是使用箭头函数定义的,则 this 的指向会有所不同。箭头函数中 this 的指向是根据定义时它所在的代码位置决定的,即词法上下文(LexicalContext),this 的取值为包裹箭头函数的作用域中 this 的值。

在上方示例中,如果把 f() 函数改为箭头函数,则它里边的 this 的指向与全局作用域中的 this 的指向一样,即浏览器下为 window,Node.js 下为 module.exports,因为字面值的 obj 是在全局作用域中定义的(定义对象的大括号为对象字面值的语法,并未形成新的块级作用域),包裹 f() 函数的作用域就是全局作用域,代码如下:
const obj={
     f:()=>{console.log(this)}
}
obj.f();  //Window

如果在构造函数中使用箭头函数,则箭头函数的 this 就是构造函数中的 this,即指向创建的对象,代码如下:
function Func(){
     const init=()=>{
          this.a=5;
     };
     init();
}
const obj=new Func();
obj.a;  //5

一般在对象中使用普通函数作为对象的方法,这样可以保留 this 的指向,但是有些特殊情况使用箭头函数会更合适。

先来看一个例子,这个例子并不是真实的事件处理方式,不过可以解释 this 在回调函数中的问题,代码如下:
function Button(label){
     this.label=label;
     this.handleClick=function(){
          console.log(this.label);
     };
}
//模拟触发单击事件
function emitClick(callback){
     callback();
}
const btn=new Button("按钮");
emitClick(btn.handleClick); //undefined
代码中首先定义了 Button 构造函数,代表一个按钮组件,它有 label 属性和处理单击事件的方法 handleClick(),方法里边简单地打印出来了按钮的 label 属性值。

emitClick() 函数简单地模拟了单击事件的触发,它接收一个回调函数,用于在单击事件触发后要执行的业务逻辑。

接下来创建了按钮组件的实例,并触发了单击事件,把按钮中的 handleClick() 传递给了 emitClick,这样就会执行它里边的代码。

看起来应该是打印出 label 属性的值:"按钮",但是结果却是 undefined。这是因为 handleClick() 在传递给 emitClick() 的时候,this 的指向已经发生了变化。

可以看到在 emitClick() 中调用 callback() 时,也就是 Button 中的 handleClick(),左边没有任何东西,那么此时 this 指向的是全局对象,它里边没有 label 属性,所以打印出了 undefined。

要解决这个问题有3种方法,第1种解决方法是在 Button 构造函数中,把 this 的值保存到一个变量中,通常使用 self 作为变量名表示对象本身,然后在 handleClick() 中引用,代码如下:
function Button(label){
     this.label=label;
     var self=this;
     this.handleClick=function(){
          console.log(self.label);
     };
}
这时,Button 构造函数和 handleClick() 形成了一个闭包,handleClick() 可以捕获 self 变量的值,后边无论在哪里调用,都可以访问它所指向的对象中的属性了。

第2种解决方法是使用箭头函数,代码如下:
this.handleClick=()=>{
     console.log(this.label);
};
因为箭头函数中的 this 是根据箭头函数定义时的位置决定的,所以使用箭头函数定义 handleClick() 时,this 已经确定为构造函数 Button 的 this,所以最后成功地访问了 label 属性。

第3种解决方法是使用函数对象中的 bind() 方法。使用 bind() 可以给函数绑定运行时的 this,并返回新的函数,这样在后边调用这个新函数时,它的 this 就是使用 bind() 所绑定的 this。

例如将 handleClick() 修改为使用 bind(),代码如下:
this.handleClick=function(){console.log(this.label)}.bind(this);
//或者这样更清楚一些
//this.handleClick=function(){console.log(this.label)}
//this.handleClick=this.handleClick.bind(this)
bind() 参数中的 this 就是给 handleClick() 绑定的 this,由于是在 Button 构造函数中,所以 this 指向的是 Button 构造函数中的 this,这样也能打印出 label 属性的值。

这3种解决方法可以任选其一,不过使用箭头函数的方式更为简洁清晰。

推荐阅读