JavaScript中的this指向:从懵圈到豁然开朗的奇幻之旅

162 阅读6分钟

引子:this的神秘面纱

在JavaScript的世界里,this关键字就像是一个善变的精灵,它时而指向这里,时而指向那里,让无数开发者头疼不已。今天,就让我们一起揭开this的神秘面纱,看看它究竟是何方神圣!

第一部分:this的七十二变

1.1 全局环境中的this

在下述代码中,我们看到一个有趣的例子:

var name = '小卢'
function fn() {
    var name = '小李'
    console.log(this.name)
}
fn() // 输出什么?

this会指向最后调用它的对象

答案是——"小卢"!为什么呢?因为当我们直接调用函数时,this会指向全局对象(在浏览器中就是window)。所以这里的this.name实际上是window.name,也就是全局变量小卢。这里调用fn()其实相当于window.fn(),所以this指向window

小贴士:在严格模式下('use strict'),这里的this会是undefined,避免污染全局对象哦!

1.2 对象方法中的this

我们还有另一个例子:

let obj = {
    name: '小王',
    fn: function() {
        console.log(this.name)
    }
}
obj.fn() // 输出"小王"

当函数作为对象的方法被调用时,this会指向调用它的对象。所以这里this.name就是obj.name,也就是"小王"。

1.3 this的"变心"时刻

this的忠诚度有时候令人担忧:

const fn2 = obj.fn
fn2() // 输出"小卢"而不是"小王"

为什么?因为当我们把方法赋值给变量再调用时,它变成了普通函数调用,this又指向了全局对象!

思考题:为什么JavaScript要这样设计this的行为呢?答案在文章末尾揭晓!

第二部分:构造函数中的this

在下述代码中,我们看到了构造函数的神奇之处:

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

const haha = new Person('haha', 18)
console.log(haha.name) // "haha"

当我们使用new关键字调用函数时,魔法发生了:

  1. JavaScript会创建一个新对象{}
  2. 将新对象的原型指向构造函数的prototype
  3. this绑定到这个新对象
  4. 执行构造函数中的代码
  5. 返回这个新对象(除非构造函数返回一个对象)

所以,在构造函数中,this指向的就是即将诞生的新对象!

函数是如何区分 构造 还是 执行的?

一个函数是一个对象,它有两个特殊的属性:[[Call]][[Construct]]。 函数在运行的时候有两个个分支选择,当是函数执行就走[[Call]] 当是构造函数走[[Construct]] 生成一个新的对象 {} this 指向这个对象 [[Construct]] 帮我们完成了对象的构造,并把 this 指向这个对象

第三部分:事件处理中的this

在下述代码中,我们看到了事件处理函数中的this

<button id="btn">点击</button>
<script>
    const btn = document.getElementById('btn')
    btn.addEventListener('click', function() {
        console.log(this); // <button id="btn">点击</button>
    })
</script>

在DOM事件处理函数中,this默认指向触发事件的DOM元素。这让我们可以方便地操作当前元素:

btn.addEventListener('click', function() {
    this.style.backgroundColor = 'red' // 直接修改按钮颜色
})

第四部分:驯服this的三大神器

当我们无法控制this的指向时,JavaScript提供了三大神器来驯服它:callapplybind

4.1 call和apply:立即执行

在下述代码中,我们看到它们的用法:

var a = {
    name: '小公主',
    fn: function(a, b) {
        console.log(this.name, a, b)
    }
}

const b = a.fn
b.call(a, 1, 2) // "小公主" 1 2
b.apply(a, [1, 2]) // "小公主" 1 2

callapply都能立即执行函数,并指定函数内部的this值。区别在于参数传递方式:

  • call:逐个传递参数
  • apply:以数组形式传递参数

4.3 解决回调函数中的this丢失

我们遇到了一个经典问题:

var a = {
    name: '小武',
    func1: function() {
        console.log(this.name);
    },
    func2: function() {
        setTimeout(function() {
            this.func1(); // 报错:this.func1 is not a function
        }, 1000)
    }
}
a.func2() 

为什么报错?因为setTimeout中的函数是普通函数调用,this指向全局对象(window),而全局对象上没有func1方法。

解决方案1:闭包保存this

func2: function() {
    var _this = this
    setTimeout(function() {
        _this.func1() 
    }, 1000)
}

解决方案2:使用bind

func2: function() {
    setTimeout(function() {
        this.func1();
    }.bind(this), 1000)
}

解决方案3:箭头函数(最优雅的方式)

func2: function() {
    setTimeout(() => {
        this.func1();
    }, 1000)
}

箭头函数没有自己的this,它会继承外层作用域的this值,完美解决回调中的this问题! 简单来说,就是通过作用域链,找到this

第五部分:实战案例 - 按钮组件

在下述代码中,我们有一个实际的组件案例:

// button.js
function Button(id) {
    this.element = document.querySelector(`#${id}`)
    this.bindEvent()
}

Button.prototype.bindEvent = function() {
    this.element.addEventListener('click', this.setBgColor.bind(this))
}

Button.prototype.setBgColor = function() {
    this.element.style.backgroundColor = '#1abc9c'
}

这里的关键点是:this.setBgColor.bind(this)。为什么要这样?

因为事件监听器中的回调函数默认this指向DOM元素,但我们希望它指向Button实例,这样才能访问实例的属性和方法。

如果不绑定:

// 错误写法
Button.prototype.bindEvent = function() {
    this.element.addEventListener('click', this.setBgColor)
}

// 点击时相当于:
// this.setBgColor() -> 里面的this就会指向我们点击的元素,并不是我们的实例化对象,而我们的元素是没有element这个属性的,就会报错,所以我们要用bind改变this的绑定

第六部分:箭头函数的特殊力量

相信大家知道箭头函数的特殊性质:

箭头函数内部没有this,argument 也没有

这意味着箭头函数会继承定义时所在作用域的this值,也就是会通过作用域链访问到this,而不是调用时的this值。这个特性让它在某些场景下非常有用:

// 使用箭头函数解决回调问题
Button.prototype.bindEvent = function() {
    this.element.addEventListener('click', () => {
        this.setBgColor()
    })
}

但要注意:箭头函数不能用作构造函数,也没有prototype属性。

第七部分:this指向的终极决策树

为了帮助大家记住this的指向规则,我总结了这张决策树:

函数被调用时:
│
├── 使用 new 调用? → this = 新创建的对象
│
├── 使用 call/apply/bind? → this = 指定的对象
│
├── 作为对象方法调用? → this = 调用该方法的对象
│
├── 箭头函数? → this = 定义时的外层this
│
└── 其他情况 → 
    ├── 严格模式? → this = undefined
    └── 非严格模式? → this = 全局对象

总结一句话,就是最后调用函数的对象

第八部分:深入理解this的本质

为什么JavaScript的this如此灵活多变?这其实与它的设计哲学有关:

  1. 动态绑定:JavaScript的函数是"一等公民",可以被任意传递和调用
  2. 执行上下文:每次函数调用都会创建一个新的执行上下文
  3. 调用方式决定this:this的值在函数被调用时确定,而不是定义时

理解这些概念,才能真正掌握this的精髓!

结语:与this和解

经过这一趟奇幻之旅,相信你已经对JavaScript中的this有了全新的认识。记住:

this不是敌人,而是需要理解的朋友。当你掌握了它的规律,它将成为你编程路上的得力助手!

最后,让我们用一个幽默的比喻结束今天的旅程:

JavaScript中的this就像你的童年玩伴,有时候跟你形影不离(对象方法),有时候又不知跑哪去了(回调函数),但只要掌握好沟通技巧(call/apply/bind),你们就能成为最佳搭档!

希望本文能帮助你在JavaScript的海洋中乘风破浪!下次再见!