玩转 JS 中 this 指向

584 阅读11分钟

前言

JavaScript 中 this 的指向问题,对于刚进入 web 前端职场的人来说是个较为复杂且具有迷惑性的问题。它的指向并不是固定不变的,而是会根据函数的调用方式、上下文环境等因素而变化。理解和掌握 this的指向规则需要一定的实践和经验积累。这篇文章将详细解析this的指向问题。

正文

什么是this?

古话说:知己知彼方能百战百胜。要掌握this先要知道this是什么。

首先我们把对象a看作两个部分。一个是对象的名字,一个是该对象的数据。

let xm = {
    name : 小明,
}
let lm ={
    name : 李明,
}

对象名字存储在栈里,对象的数据存储在堆中。

屏幕截图 2024-04-28 102732.png

this是函数的自有变量,指向了保存在堆中的某个对象的数据。

屏幕截图 2024-04-28 103455.png

this不在编译时绑定,而是在运行时绑定,取决于函数的调用位置。

对this的错误认知

  • this是指向函数自身
  • this是指向函数的作用域

this指向的规则

默认绑定

当函数被直接调用而不是使用任何显式绑定方法(如 call()apply()bind())或者作为构造函数使用时,JavaScript 中的 this 将默认绑定到全局对象(在浏览器环境中通常是 window 对象),或者在严格模式下是 undefined

eg1:

console.log(this == window)  //输出:true

此时this指向全局对象。

eg2:自执行函数

var name = '小明';

(function () {
    var name = '李明'
    console.log(this.name)
})()//输出:小明

在自执行函数表达式中,this 的值在非严格模式下也是指向全局对象(在浏览器中是window对象)。

eg3:

function greet() {
    var name = '李明'
  console.log('我是' + this.name)
}

var name = '小明'

greet(); // 输出:我是小明

greet() 函数被独立调用,因此 this 默认绑定到全局对象上,所以输出的结果是 '我是小明'而不是'我是李明'

严格模式是 JavaScript 的一种工作模式,它使得 JavaScript 引擎在更严格的条件下运行代码,从而减少一些不确定行为,提高代码的安全性和可靠性。只需在代码文件或函数的顶部添加"use strict"即可启用严格模式。

eg4:在严格模式下

function greet() {
    "use strict"
    var name = '李明'
  console.log('我是' + this.name)
}

var name = '小明'

greet(); // 执行结果报错

结果:

屏幕截图 2024-04-28 121559.png

隐式绑定

隐式绑定是指在函数调用时,函数内部的 this 关键字会隐式地绑定到调用该函数的对象上。这种绑定方式常见于对象方法的调用。具体来说,当一个函数作为对象的方法被调用时,该函数内部的 this 将自动指向调用该方法的对象。

eg1:

var name = '李明'
var person ={
    name : '小明',
    func: function(){
        console.log(this.name)//输出:小明
    }
}
person.func()

func方法作为person 对象的属性被调用,因此 thisfunc 方法内部指向了 person 对象,使得可以访问 person 对象的 name 属性。

eg2:

var name = '李明'
var person ={
    name : '小明',
    func: function(){
        console.log(this.name)//输出:小明
        function a(){
            console.log(this.name)//输出:李明
        }
        a()
    }
}
person.func()

a函数被独立调用,因此 this 默认绑定到全局对象上。结果输出'李明'func方法作为person 对象的属性被调用,因此 thisfunc 方法内部指向了 person 对象,使得可以访问 person 对象的 name 属性。

eg3:开始套娃

var name = '李明'
var Person = {
    name: '小李',
    person: {
        name: '小明',
        func: function () {
            console.log(this.name)//输出:小明
            function a() {
                console.log(this.name)//输出:李明
            }
            a()
        }
    }
}
Person.person.func()

func方法作为Person.person 对象的属性被调用,因此 thisfunc 方法内部指向了 Person.person 对象,使得可以访问 Person.person 对象的 name 属性。

隐式绑定使得方法可以更方便地访问和操作它所属的对象的属性和方法。

隐式丢失

隐式丢失是指在对象方法调用中,函数失去了原本的隐式绑定,导致 this 不再指向该对象,而是指向了其他对象或全局对象。

eg4:隐式丢失————变量赋值的情况

var name = '李明'
function func() {
    console.log(this.name)
}
var person = {
    name: '小明',
    func: func
}
var a = person.func;
person.func()//输出:小明
a()//输出:李明
  1. person.func() 被调用时,func 函数是作为 person 对象的方法调用的,因此函数内部的 this 将指向 person 对象。所以,this.name 将输出 Alice
  2. a() 被调用时,a 是在全局作用域中调用的,因此函数内部的 this 将指向全局对象。所以,this.name 将输出全局作用域下的 name,其值为 李明

eg5:隐式丢失————参数赋值的情况

var name = '李明'
function func() {
    console.log(this.name)//输出:李明
}
function b(c) {
    console.log(this.name)//输出:李明
    c()
}
var person = {
    name: '小明',
    func: func
}

b(person.func)
  1. b(person.func) 被调用时,b 函数内部的 this 指向全局对象,因为它是在全局作用域中被调用的。因此,第一个 console.log(this.name) 输出的是全局作用域下的 name,其值为 '李明'
  2. 在函数 b 内部,又调用了参数 c,即 person.func。因为 person.func 是作为函数参数传递进来的,调用时并没有指定上下文,所以 this 指向了全局对象,而不是 person 对象。因此,第二个 console.log(this.name) 输出的也是全局作用域下的 name,其值仍然是 '李明'

不管对象的方法是变量赋值后还是参数赋值后,只要是被独立调用,其this便指向全局对象。

显性绑定

显性绑定是指通过 call()apply()bind() 方法显式地指定函数内部的 this 值。这些方法允许我们明确地设置函数在执行时的上下文,而不依赖于函数被调用的位置或方式。

  • call是一个方法,是函数的方法。call可以调用函数,call也可以改变函数中this指向。call 方法可以接受多个参数。它的第一个参数是一个对象,用于指定函数执行时的this指向,后面的参数则是函数的实际参数。

    eg1:call可以调用函数。

    function func(){
        console.log(this)
    }
    func.call()//输出:window
    //说明call调用了func函数
    

    eg2:call也可以改变函数中this指向。

    var name = '小明'
    function func(){
        console.log(this.name)
    }
    var person={
        name : '李明'
    }
    
    func()//输出:小明
    func.call(person)//输出:李明
    //说明call改变了func函数中this指向,使this指向了person对象
    

    eg3:call 方法可以接受多个参数。它的第一个参数是一个对象,用于指定函数执行时的this指向,后面的参数则是函数的实际参数。

    var person={
        name : '小李',
        func : function (b){
            console.log(this.name+'和'+b+'是朋友')
        }
    }
    
    var person1={
        name : '李明'
    }
    
    person.func.call(person1,'小明')//输出:李明和小明是朋友
    
  • apply()方法与call()方法类似,只是它接受一个数组作为参数,数组中的元素将作为函数的参数传递给被调用的函数。

    eg:

    var person={
        name : '小李',
        func : function (a,b){
            console.log(this.name+'和'+a+'和'+b+'是朋友')
        }
    }
    
    var person1={
        name : '李明'
    }
    
    person.func.apply(person1,['小明','小李'])//输出:李明和小明和小李是朋友
    
  • bind()方法创建一个新的函数,该函数的this指针被绑定到指定的对象上。与call()apply()不同的是,bind()方法不会立即执行函数,而是返回一个新的函数,需要手动调用。

    var person={
        name : '小李',
        func : function (b){
            console.log(this.name+'和'+b+'是朋友')
        }
    }
    
    var person1={
        name : '李明'
    }
    
    var a= person.func.bind(person1,'小明')//输出:李明和小明是朋友
    a()
    

注意事项

当使用 call()apply()bind() 方法将函数内部的 this 值显式地指定为 nullundefined 时,this的绑定方式将会变为默认绑定方式(this指向全局对象)。

var name = '小美'
var person = {
    name: '小李',
    func: function (b) {
        console.log(this.name + '和' + b + '是朋友')
    }
}

var person1 = {
    name: '李明'
}

person.func.call(null, '小明')//输出:小美和小明是朋友
var name = '小美'
var person = {
    name: '小李',
    func: function (b) {
        console.log(this.name + '和' + b + '是朋友')
    }
}

var person1 = {
    name: '李明'
}

person.func.call(undefined, '小明')//输出:小美和小明是朋友

小插曲

父函数可以通过一些技巧或方法来影响子函数或回调函数的 this 指向。因为this 的指向是在函数被调用时确定的,而不是在函数定义时确定的。但是,父函数本身并不能直接决定子函数或回调函数的 this 指向。

eg:forEach方法的语法

array.forEach(callbackFn(currentValue, index, arr), thisValue)

在数组的forEach方法中,可以通过修改thisValue的值,改变回调函数的this指向。

const person = {
    name: '小明',
    greet: function () {
        console.log('我是' + this.name);
    }
};

const numbers = [1, 2, 3];

numbers.forEach(function (item) {
    // 在这里,this 指向的是 person 对象
    this.greet();
}, person);
//输出:我是小明	
//     我是小明
//     我是小明

我们通过将 person 对象作为 thisValue 参数传递给 forEach() 方法,使this 指向的是 person 对象,从而可以在回调函数中访问 person 对象的属性和方法。

new绑定

new绑定指的是通过使用new关键字来调用构造函数时,将一个新创建的空对象绑定到该构造函数的this关键字上,然后执行构造函数内部的代码,并最终返回这个新创建的对象。

function Person(name,sex) {
    this.name = name;
    this.sex =sex;
}

var person1 = new Person('小明', '男');
var person2 = new Person('李明', '男');

console.log(person1); // 输出: { name: '小明',sex: '男'}
console.log(person2); // 输出: { name: '李明',sex: '男'}

箭头函数绑定

箭头函数没有自己的this,它的this值是在定义时所在的作用域环境中确定的。在箭头函数中,this的绑定是静态的,不会被call()apply()new等操作改变。

箭头函数的语法:

() => {}
//()中定义参数,如果只有一个参数,可以不写括号;
//{}中写函数体,如果函数体中只有返回值,可以不写return和{}。
const func =function(){
    return 100
}
console.log(func())//输出100

//用箭头函数之后

const func = () => 100
console.log(func())//输出100

箭头函数和普通函数的区别

  • 普通函数:谁调用这个函数,this就指向谁。
  • 箭头函数:在哪里定义函数,this就指向谁。
//普通函数
var name = '小李'
var person = {
    name : '小明',
    sayName : function(){
       setTimeout(function(){
            console.log(`我是${this.name}`)
       },500)
    }
}
person.sayName()//输出:我是小李

//箭头函数
var name = '小李'
var person = {
    name : '小明',
    sayName : function(){
       setTimeout(() => {
            console.log(`我是${this.name}`)
       },500)
    }
}
person.sayName()//输出:我是小明

在这个例子中,普通函数计算器中的函数是独立调用的,this指向全局对象。而箭头函数所在的词法作用域是 person.sayName() 方法,,所以箭头函数内部的 this 就指向了person对象,并且this的值不会改变。

绑定方式的优先级

因为箭头函数的 this 绑定是固定的,它是根据词法作用域决定的,而不是动态绑定。相比之下,其他函数调用的方式具有更高的优先级,因为它们的 this 绑定是动态确定的,可以根据函数被调用的方式来改变。

默认绑定是在没有明确对象调用的情况下发生的,通常是在函数独立调用时。相比之下,其他动态绑定发生在明确指定对象的调用上,this 的指向是根据函数被调用的上下文动态确定的。所以默认绑定的优先级低于其他动态绑定。

eg1:

function func (){
    console.log(this.name)
}
var name = '小美'
var person1 = {
    name : '小明',
    func : func 
}
var person2 = {
    name : '李明',
    func : func 
}

person1.func()//输出:小明
person2.func()//输出:李明

person1.func.call(person2)//输出:李明
person2.func.call(person1)//输出:小明
//改变了隐式绑定的输出结果

从这个例子中可以看出在显性绑定的优先级大于隐式绑定的优先级。

eg2:

function func(name) {
    this.name = name
}

var person1 = {}
var person = func.bind(person1)
person('小明')

console.log(person1.name);//输出:小明
var person2 = new person('李明')
console.log(person1.name);//输出:小明

console.log(person2.name);//输出:李明

我们来分析一下这段代码。首先我们创建了一个构造函数func和一个空对象person1

var person = func.bind(person1)
person('小明')
//显性绑定,this指向person1。效果也就是在person1对象里面添加一个键值对name:'小明'。
//person1{
//    name : '小明'
//}


var person2 = new person('李明')
//new绑定,this指向person2。创建了一个新的实例 person2,this指向person2,相当于执行了person2.name = '李明'。

我们可以看出,在var person2 = new person('李明')的时候this的指向被改变了,由person1变为person2 。所以我们可以得出new绑定的优先级大于显性绑定。

小结

绑定方式的优先级:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定 > 箭头函数绑定。

结尾

默认绑定this默认指向window;在函数独立调用时,this 通常指向全局对象(在浏览器中是window)。

隐式绑定:通过对象调用方法时,this 指向该对象。谁调用就指谁。(存在隐式丢失情况)

显式绑定:使用 call()apply()bind() 方法可以显式指定 this 的指向。

new绑定:当函数被new关键字调用时,this指向一个新创建的空对象,该对象由构造函数创建并返回。

箭头函数绑定:箭头函数的 this 是词法作用域的,由外层作用域决定。在哪里定义函数,this就指向谁。

this 的指向优先级顺序:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定 > 箭头函数绑定。

掌握 this 的指向规则对于理解和正确使用 JavaScript中的函数至关重要,希望本文能帮助刚进入职场的前端开发者更好地应对 this 指向问题带来的挑战。