努力让学习成为一种习惯,自信来源于充分的准备
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
前言
this的学习非常重要,无论是在我们日常开发还是面试都经常会遇到。理解this能够让你在日常开发中更加自信。但是 this的执行机制让人不太容易理解,甚至会有人把this执行机制和作用域的执行机制混淆,接下来让我们来一探究竟
奇怪的 this
我们先来看一段简单的代码
var a = 1
function outer() {
var a = 2
function inner() {
console.log(a) // 2
console.log(this.a) // 1
}
inner()
}
outer()
通过输出结果我们可以发现this查询变量的规则不同于作用域。在深入理解作用域与作用域链中讲到过。词法作用域在代码编译的时候就确定了的,取决于函数的声明位置。但 this不同,this是在执行上下文创建过程中生成,而执行上下文是在函数调用的时候创建的。因此this在不同的执行上下文表现形式会有所区别
全局上下文中的 this
let a = 1
var b = 2
console.log(this === window) // true
console.log(this.a) // undefined
console.log(this.b) // 2
全局上下文中,this指向全局对象window,其中通过let声明的变量不会挂载到全局对象中,因此this.a为 undefined。这种情况比较简单
函数执行上下文中的 this
this真正让人头疼的、并且实际运用较多的场景是在函数内
引用《js高级程序设计》中对其的定义:
在标准函数中,this引用的是把函数当成方法调用的上下文对象
换言之:函数是谁调用的,this 就指向它
在ECMAScript5中会给函数增加一个属性:caller,这个属性引用的是调用当前函数的函数。如果在全局调用则为null,为了降低耦合度,也可以通过 arguments.callee.caller来引用同样的值,如下代码
function outer() {
console.log('outer.caller :>> ', outer.caller); // null
console.log(arguments.callee.caller) // null
inner()
}
function inner() {
console.log('inner.caller :>> ', inner.caller); // f outer() {xxxx}
console.log(arguments.callee.caller) // f outer() {xxx}
}
outer()
接下来我们从绑定规则的角度来分析函数上下文中的this
默认绑定
当函数独立调用的时候采用默认绑定规则
var a = 1
function func() {
var a = 2
console.log(this.a) // 1
func2()
}
function func2() {
console.log(this.a) // 1
}
func()
在非严格模式下,会将全局对象用于默认绑定。在浏览器环境中,全局对象为window,因此,上面代码中
func()相当于window.func(),this绑定的是window
var a = 1
function func() {
"use strict"
console.log(this.a) // Uncaught TypeError: Cannot read properties of undefined (reading 'a')
}
func()
在严格模式下,则不能将全局对象用于默认绑定,this指向为undefined
隐式绑定
调用的位置是否有上下文对象
简单来说:该函数是否是作为对象的方法在调用
const obj = {
a:1,
func
}
function func() {
console.log(this.a);
}
obj.func() // 1
这里需要注意一点,func函数无论是作为外部声明后引用,还是直接定义为obj的属性,func函数都不属于 obj对象, 对象只是包含该函数的引用,即下面这种情况是一样的
const obj = {
a:1,
func: function() {
console.log(this.a)
}
}
obj.func() // 1
隐式丢失
被隐式绑定的函数容易丢失绑定对象,从而导致this的指向不符合预期
const obj = {
a:1,
func: function() {
console.log(this.a)
}
}
const func = obj.func
func() // undefined, 严格模式下会报错
本质上,obj.func与func都是对函数的引用,在这个例子中。函数是独立调用。因此this采用的是默认绑定的规则
另一种更加隐蔽且常见的场景发生在回调函数中
const obj = {
a:1,
func: function() {
// 业务逻辑
// XXXX
setTimeout(function() {
console.log(this.a);
}, 0);
}
}
obj.func() // undefined
内部函数并不会继承外层函数的this, 上面的例子中计时器中回调函数里的 this采用的是默认绑定规则,绑定的是全局对象,想让回调函数中的 this如我们预期那样,有许多种方法
- 用一个变量保存外部函数的
this:
const obj = {
a:1,
func: function() {
// 业务逻辑
// XXXX
const _this = this
setTimeout(function() {
console.log(_this.a); // 1
}, 0);
}
}
obj.func()
- 使用箭头函数
const obj = {
a:1,
func: function() {
// 业务逻辑
// XXXX
setTimeout(() => {
console.log(this.a); // 1
}, 0);
}
}
obj.func()
关于箭头函数不详细介绍。在这里,我们需要知道箭头函数中的this引用的是定义箭头函数的上下文,它没有自己的this
另一种常见的场景是给某个DOM元素绑定事件
<div>
<button id="btn">button</button>
</div>
<script>
var a = 1
const button = document.querySelector('#btn')
button.addEventListener('click', function () {
console.log(this); // <button>
console.log(this.a); // undefined
})
button.addEventListener('click', () => {
console.log(this); // window
console.log(this.a); // 1
})
</script>
这里监听 click事件的回调函数中的this会被强制绑定到触发事件的DOM元素中
我们可以使用箭头函数让this重新指向全局
显示绑定
如果每次都需要通过对象方法的方式调用来隐式绑定this未免太不方便了。那么有没有其他的什么方式可以在函数运行时候指定其内部的this指向呢,答案是肯定的
JS函数中分别提供了bind、apply、call三种函数方法来手动设置函数执行上下文中的this
bind
const obj = {
a: 1
}
function func() {
console.log(this.a);
}
const _func = func.bind(obj)
_func() // 1
apply、call
const obj = {
a: 1
}
function func(...args) {
console.log(this.a);
console.log('args :>> ', args);
}
func.apply(obj, [1,2,3]) // 1 [1,2,3]
func.call(obj, 1,2,3) // 1 [1,2,3]
这里只是通过简单的例子说明bind、apply、call的基本用法。本文中不具体展开介绍
额外需要注意
- 是如果传入的是基本类型,则this指向为其装箱类型
- 对箭头函数没有作用
- 如果传入的是
null、undefined,在调用的时候会被忽略,实际采用的是默认绑定规则
var a = 3
const obj = {a: 1}
const func = () => {
console.log(this.a); // 3
}
func.apply(obj)
new绑定
另一种常见的this绑定是通过构造函数
function Func() {
this.a = 1
}
const f = new Func()
console.log(f.a) // 1
有关构造函数以及new操作符的细节在这里不深入展开
我们需要知道的是:
通过 new规则方式调用的函数,会在内部创建一个新对象,并将this绑定到该对象。如果函数没有返回其他对象,则函数会默认返回这个新建的对象
优先级
隐式绑定与显式绑定
const obj1 = { a:1, func }
const obj2 = {a:2, func }
function func() {
console.log(this.a)
}
obj1.func() // 1
obj2.func() // 2
obj1.func.apply(obj2) // 2
obj2.func.apply(obj1) // 1
通过上面的例子可以看出:显示绑定的优先级大于隐式绑定
隐式绑定与new绑定
const obj1 = {
func: function (num) {
this.a = num
}
}
obj1.func(2)
console.log(obj1.a); // 2
const bar = new obj1.func(3)
console.log(obj1.a); // 2
console.log(bar.a); // 3
通过上面的例子可以看出:new绑定的优先级大于隐式绑定
new绑定与显示绑定
function func (num) {
this.a = num
}
const obj = {}
const _func = func.bind(obj)
_func(1)
console.log(obj.a) // 1
const bar = new _func(2)
console.log(obj.a) // 1
console.log(bar.a) // 2
通过上面的例子可以看出:new绑定的优先级大于显示绑定,new操作符直接改变了func函数里面的 this
综合下来, this绑定的优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
结语
通过上面的内容,相信大家对this机制有了更清晰的认识
最后我们可以总结下函数执行上下文中的this绑定规则(优先级):
- 函数通过
new调用。此时this为new绑定规则,绑定的是新创建的对象
const f = new func()
- 函数通过
bind、apply、call方法调用,此时this为显示绑定规则,绑定的是指定对象
func.apply(obj)
- 函数通过
对象方法调用,此时this为隐式绑定规则,绑定的是上下文对象
obj.func()
- 函数独立调用,此时this为默认绑定规则,严格模式下为
undefined,非严格模式下绑定的是全局对象
到这里,就是本篇文章的全部内容了
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
如果你有疑问或者出入,评论区告诉我,我们一起讨论