【你不知道的JavaScript】一:JavaScript中this到底指向谁?

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


1.1 this到底是什么

当一个函数被调用时,会创建一个执行上下文,这个记录会包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到;
this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用;

1.2 绑定规则

1.2.1 默认绑定

无法应用其他规则时的默认规则

虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定;

"use strict"
function foo1() {
    console.log(this.a)
}
function foo2() {
    "use strict"
    console.log(this.a)
}

var a = 2
foo1() // TypeError: Cannot read properties of undefined 
foo2() // TypeError: Cannot read properties of undefined (reading 'a')
1.2.2 隐式绑定

如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,如果函数调用前面存在多个对象,this就会指向距离调用自己最近的对象;

当函数引用有上下文对象时,隐式绑定 规则会把函数调用中的this绑定到这个上下文对象,因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的;

function foo() {
    console.log(this.a)
}

var obj2 = {
    a: 42,
    foo3
}
var obj1 = {
    a: 2,
    obj2
}

obj1.obj2.foo()

隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上没取决于是否是严格模式;

  • 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定;
function foo() {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo
}
var bar = obj.foo
var a = 'oops, global'
bar() // oops, global
  • 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值;
function foo() {
    console.log(this.a)
}
function doFoo(fn) {
    fn()
}
var obj = {
    a: 2,
    foo
}
var a = 'oops, global'
doFoo(obj.foo)  // oops, global
  • 如果把函数传入语言内置的函数中,结果也是一样的,本质上与参数传递相同;
function foo() {
    console.log(this.a)
}
var obj = {
    a: 2,
    foo
}
var a = 'oops, global'
setTimeout(obj.foo, 2000) // oops, global

个人认为参数传递与变量赋值是同一类,参数传递是隐式赋值;

1.2.3 显示绑定

直接指定this的绑定对象, 我们称之为显示绑定;

硬绑定

无论之后如何调用函数,它总会手动在我们指定对象上调用,这种绑定是一种显示的强制绑定,我们称之为硬绑定;

function foo(something) {
    console.log(this.a, something)
    return this.a + something
}
var obj = {
    a: 2,
}
var bar = function() {
    return foo.apply(obj, arguments)
}
var b = bar(3) // 2 3
console.log(b) // 5

API调用的“上下文”

第三方库的许多函数,以及js中的许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind()一样,确保你的回调函数使用指定的this;

function foo(something) {
    console.log(something, this.id)
}
var obj = {
    id: 'awesome',
}
var id = '全局id'
let arr = [1, 2, 3]
arr.forEach(foo, obj)
// 1 'awesome' 2 'awesome' 3 'awesome'
1.2.4 new绑定

我们需要重新定义一下js中的构造函数,在js中,构造函数只是一个使用new操作符时被调用的函数,他们并不会属于某个类,也不会实例化一个类,实际上,它们甚至都补鞥呢说是一种特殊的函数类型,他们只是被new操作符调用的普通函数而已;所以,包含内置对象函数在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上并不存在所谓的构造函数,只有对于函数的构造调用

使用new操作符会自动执行下面操作:

  1. 创建一个全新的对象;
  2. 这个新对象会继承原型上的方法;
  3. 这个新对象会绑定到函数调用的this,获取私有属性;
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象;
function foo(a) {
    this.a = a // this被绑定到bar对象上
}
var bar = new foo(2)
console.log(bar.a) // 2 

使用new来调用foo时,我们会构造一个对象并把它绑定到foo调用中的this上。

1.3 优先级

现在我们已经知道了this绑定的四条规则,接下来我们需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则那么应该怎么办?毫无疑问默认绑定的优先级是最低的,那么我们来讨论剩下的规则的优先级问题;

我们先来判断显示绑定和隐式绑定的优先级

function foo() {
    console.log(this.a)
}
var obj1 = {
    a: 2,
    foo
}
var obj2 = {
    a: 3,
    foo
}
obj1.foo() // 2
obj2.foo() // 3

obj1.foo.call(obj2) // 3
obj2.foo.call(obj1) // 2

通过测试我们可以知道:显示绑定的优先级高于隐式绑定

再来判断new绑定和隐式绑定的优先级

function foo(a) {
    this.a = a
}
var obj1 = {
    foo
}
var obj2 = {}
obj1.foo(2)
console.log(obj1.a) // 2 隐式绑定

obj1.foo.call(obj2, 3)
console.log(obj2.a) // 3 显示绑定

var bar = new obj1.foo(4)
console.log(obj1.a) // 2 还是上面的隐式绑定
console.log(bar.a) // 4 new绑定

通过测试我们可以知道:new绑定的优先级高于隐式绑定

那么,new绑定和显示绑定谁的有优先级更高呢?

new和call/apply无法一起使用,因此无法通过new foo.call(obj1)来直接进行测试,但是我们可以使用硬绑定来测试他们的优先级;

function foo(a) {
    this.a = a
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2 硬绑定

var baz = new bar(3)
console.log(obj1.a) // 2 居然不是3
console.log(baz.a) // 3 new绑定

测试有点出乎意料,bar被硬绑定到了obj1上面,但是new bar(3)并没有像我们预计的哪有把obj1.a修改为3.相反,new修改了硬绑定(到obj1的)调用bar中的this。因为使用了new绑定,我们得到了一个名字为baz的新对象,并且baz.a的值是3;

1.4 绑定例外

1.4.1 被忽略的this

如果你把null或者undefined作为this的绑定对象传入call,applu或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则

function foo () {
    console.log(this.a)
}
var a = 2
foo.call(null) // 2
1.4.2 间接引用

另一个需要注意的是,你又可能创建一个函数的间接引用,在这种情况下,调用这个函数会应用默认绑定规则

function foo () {
    console.log(this.a)
}
var a = 2
var o = {
    a: 3,
    foo: foo
}
var p = {
    a: 4
}
o.foo() // 3
(p.foo = o.foo)() // 2

1.5 this词法

前面介绍的规则可以包含所有正常的函数,但是ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。

具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么);

function foo () {
    return (a) => {
        console.log(this.a)
    }
}
var obj1 = {
    a: 2
}
var obj2 = {
    a: 3
}
var bar = foo.call(obj1) // 2
bar.call(obj2) // 2 由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new 也不行!)

1.6 小结

如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置,找到之后就可以顺序应用下面这四条规则来判断this的绑定对象。

  1. 由new调用?绑定到新创建的对象
  2. 有call或者apply(或者bind)调用?绑定到指定的对象
  3. 由上下文对象调用?绑定到哪个上下文对象。
  4. 默认:在严格模式(函数运行在严格模式中,不是在严格模式环境中调用)下绑定到undefined,否则绑定到全局对象。

不管你如何修改箭头函数的this,它永远会继承外层函数调用的this绑定,除非你修改外层函数的this绑定;


  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Whoopsina

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值