引子: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关键字调用函数时,魔法发生了:
- JavaScript会创建一个新对象
{} - 将新对象的原型指向构造函数的
prototype - 将
this绑定到这个新对象 - 执行构造函数中的代码
- 返回这个新对象(除非构造函数返回一个对象)
所以,在构造函数中,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提供了三大神器来驯服它:call、apply和bind。
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
call和apply都能立即执行函数,并指定函数内部的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如此灵活多变?这其实与它的设计哲学有关:
- 动态绑定:JavaScript的函数是"一等公民",可以被任意传递和调用
- 执行上下文:每次函数调用都会创建一个新的执行上下文
- 调用方式决定this:this的值在函数被调用时确定,而不是定义时
理解这些概念,才能真正掌握this的精髓!
结语:与this和解
经过这一趟奇幻之旅,相信你已经对JavaScript中的this有了全新的认识。记住:
this不是敌人,而是需要理解的朋友。当你掌握了它的规律,它将成为你编程路上的得力助手!
最后,让我们用一个幽默的比喻结束今天的旅程:
JavaScript中的this就像你的童年玩伴,有时候跟你形影不离(对象方法),有时候又不知跑哪去了(回调函数),但只要掌握好沟通技巧(call/apply/bind),你们就能成为最佳搭档!
希望本文能帮助你在JavaScript的海洋中乘风破浪!下次再见!