JavaScript中的"this":一个让代码更优雅的"代词"

108 阅读7分钟

一 、为什么需要this

想象一下,如果你每次说话提到自己都要说全名:"王小明今天很开心,因为王小明中彩票了!"——是不是很啰嗦?this的存在就是为了解决这个问题,它让我们可以优雅地隐式引用对象,避免重复传递上下文。

当没有this时,我javas们的代码会是什么样呢?

function identify(context) {
    return context.name.toUpperCase()
}

function speek(context) {
    var greeting = 'hello, I am ' + identify(context)  
    console.log(greeting);
}

var me = {
    name: 'Tom'
}
speek(me)

以这段代码为例,无论是speek还是identify函数都需要显示传入context参数,如果需要对其他对象使用这些函数,必须每次都传入上下文对象,虽然语言依然可以运行,但代码会变得臃肿、不优雅。但是当我们使用到了this时就可以很好的解决这个问题,它让我们可以优雅地隐式引用对象,避免重复传递上下文,以下面代码为例。

function identify() {
    return this.name.toUpperCase()
}

function speek() {
    var greeting = 'hello, I am ' + identify.call(this)  
    console.log(greeting);
}

var me = {
    name: 'Tom'
}
speek.call(me)

在这个例子中我们直接用this关键字代替了context,这样做可以将这些函数作为对象的方法,隐式地访问调用它们的对象属性,从而简化代码结构并提高可读性。避免了显式传递上下文对象的麻烦。

二、 this的用武之地:它能在哪里出现?

this就像JavaScript世界的"变色龙",它的出现位置决定了它的"颜色"(指向)。让我们看看这个"代词"都能在哪些场合大显身手。

1. 全局舞台:this的默认主场

在全局作用域中,this会毫不犹豫地指向全局对象。在浏览器里,它就是那个大名鼎鼎的window;在Node.js环境中,它则是global

console.log(this === window); // 浏览器中输出true
console.log(this); // 浏览器中输出Window对象

// 在Node.js环境中
console.log(this === global); // 输出false(模块作用域)
console.log(globalThis === global); // 输出true

有趣的是,在浏览器控制台直接输入this,你会看到它指向window,就像皇帝坐在龙椅上一样理所当然。

2. 函数剧场:this的百变秀场

在函数体内,this的指向就变得丰富多彩了,它会根据函数的调用方式"变脸":

function showThis() {
    console.log(this);
}

// 直接调用时(默认绑定)
showThis(); // 浏览器中输出window

// 作为对象方法调用时(隐式绑定)
const actor = {
    name: '周星驰',
    showThis: showThis
}
actor.showThis(); // 输出actor对象

// 使用call/apply调用时(显式绑定)
showThis.call({name: '刘德华'}); // 输出{name: '刘德华'}

// 作为构造函数调用时(new绑定)
new showThis(); // 输出新创建的实例对象

三、this的五大"交友法则"

1. 默认绑定:单身狗的归宿

当函数独自一人(独立调用)时,this就会指向全局对象(浏览器中是window,Node中是global)。就像单身狗最后都会回到自己的小窝。

var a = 1
function foo() {
    console.log(this.a);
}
function bar() {
    var a = 2
    foo()
}
bar()

当这段代码我们用node运行和用浏览器运行结果是不一样的这是因为此时的this触发了默认绑定规则,当函数被独立调用时,函数的 this 指向 window,而在node当中没有window所以最后的输出结果为undefined,但是在浏览器当中输出则为1。

2. 隐式绑定:谁调用就认谁

当函数被某个对象"领养"并调用时,this就会指向这个"养父母"对象。遵循"就近原则"——谁离得近就认谁。

var a = 1
function foo() {
    console.log(this.a); // obj.a
}
var obj = {
    a: 2,
    foo: foo
}
obj.foo()

当函数被某个对象引用并调用时,this会绑定到该对象。例如,若obj.foo()调用foo函数,则this指向obj。即当函数引用有上下文对象 且被对象调用时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。所以此时obj.foo()输出的就是2.

3、隐式丢失:this的"健忘症"时刻

this有时候会突然"失忆",忘记自己应该指向谁——这种现象我们亲切地称为"隐式丢失"。就像你突然忘记把钥匙放在哪了一样让人抓狂。最常见的隐式丢失场景:当我们把对象方法赋值给变量时,方法就会忘记自己原来的主人。

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

我们要明确this的指向在obj2.obj.foo()中,虽然foo通过obj2.obj间接访问,但实际调用时的形式是obj.foo()。JavaScript 的this绑定规则是:最终调用函数的对象决定this的指向。因此,这里的this指向obj,而非obj2。用一句话来解释就是当一个函数被多层对象调用时,函数的 this 指向最近的那一层对象,也就是就近原则。

4. 显式绑定:强扭的瓜也甜

callapplybind可以强行改变this的指向,就像拿着"虎符"调兵遣将:

function foo(x,y) {
    console.log(this.a, x + y);
}
var obj = {
    a: 1
}
// foo.call(obj, 1, 2)
// foo.apply(obj, [1, 2])
const bar = foo.bind(obj, 2, 3)
bar()

在 JavaScript 中,this的显示绑定允许你明确指定函数执行时this的指向,而不依赖于函数的调用方式。这种绑定方式通过函数的三个方法实现:call()apply()bind()。其三种绑定方式如下所示:

  1. fn.call(obj,x,y...) 显示的将 fn 里面的 this 绑定到 obj 上, call 负责将 fn 接收参数
  2. fn.apply(obj, [x,y ...]) 显示的将 fn 里面的 this 绑定到 obj 上, apply 负责帮 fn 接收参数,参数必须以数组盛放
  3. fn.bind(obj,x,y...)(x,y, ...) 显示的将 fn 里面的 this 绑定到 obj 上, bind 会返回一个新的函数, bind 和新函数都可以负责帮 fn 接收参数,参数零散的传入。

5. new绑定:造物主的特权

new调用函数时,this会指向新创建的实例对象,就像造物主创造新生命:

function Person() {
    this.name = 'chaochao'
    this.age = 18
    console.log(this);
}
const p1 = new Person()

当使用new调用构造函数(如Person)时,JavaScript 引擎会执行以下操作:

1.先隐式创建一个空对象,这个对象继承自构造函数的prototype属性。

2.将构造函数内的this指向新创建的对象。

3.执行构造函数代码,为新对象添加属性和方法。

4.如果构造函数没有显式返回其他对象,则自动返回这个新创建的对象。

四、 箭头函数:佛系的this

箭头函数没有自己的this,它会淡定地继承外层函数的this,就像佛系青年随遇而安:

function a() {
    let b = function() {
        let c = () => {
            let d = () => {
                console.log(this); // this 不是函数 d 的,层层往上是函数 b 的
            }
            d()
        }
        c()
    }
    b()
}
a()

箭头函数中没有 this 这个概念,写在了箭头函数中的 this ,也是它外层那个非箭头函数的 this,至于这个 this 指向谁要看这个外层的非箭头函数它是怎么触发的 独立触发就是默认绑定规则。以上面为例,首先要判断console.log(this)中的this是谁的,根据箭头函数中的规则,this应层层往上直到找到外层的非箭头函数也就是函数b,所以这个this就是函数b的再看函数b怎么调用其第十一行代码显示其为独立调用,所以this的绑定规则第一条默认绑定 --- 当函数被独立调用时,函数的 this 指向 window,所以我们得知这个this也就是指向全局window。

再看第二个例子:

var a = 1
var obj = {
    a: 2,
    bar: function() {
        const baz = () => {
            console.log(this.a);
        }
        baz()
    }
}
obj.bar()

要判断console.log(this.a)输出结果首先要判断其this指向谁,根据其绑定规则this应层层往上直到找到外层的非箭头函数也就是函数bar得知this绑定在其身上,其次再看bar如何被调用,也就是第十一行代码obj.bar(),所以根据其绑定规则this触发隐式绑定 --- 当函数引用有上下文对象 且被该对象调用时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象,所以最终得知this.a相当于obj.a所以得知console.log(this.a)输出结果为2。

总结:如何判断"this"指向?

  1. 函数是箭头函数?→ 看外层函数this
  2. new调用了?→ this指向新实例
  3. call/apply/bind了?→ this指向第一个参数
  4. 被对象调用了?→ this指向该对象
  5. 以上都不是?→ this指向全局对象

记住这些规则,你就能像侦探一样准确找出this的指向了!下次面试官问你this的问题,你就可以自信地说:"这个this啊,它指向..."