深入 this 关键字:一场函数间的“派对”冒险!

175 阅读4分钟

引言

在JavaScript的世界里,this关键字就像是一场神秘的派对中的主持人,它总是根据谁邀请了它、在哪种场合下被邀请而改变它的行为。为了让这场派对更加有趣和易于理解,我们不妨将执行上下文与调用栈想象成一系列精心策划的活动安排。

执行上下文:为每个活动量身定制的房间

每当有新的活动开始(即函数被调用),就会创建一个新的“房间”,这个房间就是执行上下文。每个房间里都有它自己的规则、音乐列表(变量)、食物菜单(参数)以及装饰风格(作用域)。而且,每个房间都有一个特别的DJ——this,负责根据当前的场合调整气氛。

  • 局部变量是房间内的临时访客名单。
  • 参数则是事先准备好的VIP嘉宾。
  • 内部作用域链就像是通往其他房间的地图,当找不到特定的嘉宾时,可以顺着地图找到他们。

调用栈:排着队的派对邀请函

调用栈就像是一个长长的队伍,所有等待入场的活动都必须按顺序排队。每当一个新的活动开始,它就加入了这条队伍,成为最前面的一个。一旦活动结束,对应的房间就会被清理干净,为下一个活动腾出空间。这确保了所有的活动都能有序地进行,不会发生混乱。

function dance() {
    console.log("正在跳舞!");
}

function sing() {
    console.log("正在唱歌!");
    dance(); // 等待中的下一项活动
}

sing(); // 开始唱歌,并准备加入跳舞活动

在这个例子中,sing先开始了它的表演,然后决定邀请dance加入。因此,dance排在sing之后,形成了一个小小的派对链。当dance结束时,它离开了房间,接着sing也完成了它的表演。最后,整个派对回到了初始状态,所有的东西都被清理干净,准备迎接下一轮活动。

this 的几种形式:不同场合的不同主持人

  • 对象方法调用:当活动是由某个特定的对象发起时,this自然是指向那个对象。例如:

    const partyHost = {
        name: "Mr. Host",
        welcomeGuests: function() {
            console.log(`${this.name} 欢迎您来到派对!`);
        }
    };
    
    partyHost.welcomeGuests(); // Mr. Host 欢迎您来到派对!
    
  • 普通函数调用:如果活动没有明确的发起者,在非严格模式下,this会指向全局对象(如浏览器中的window或Node.js中的global)。而在严格模式下,this则变成了undefined,仿佛是在说:“嘿,这里没有指定的主持人!”

    "use strict";
    function casualParty() {
        console.log(this); // undefined - 没有明确的主持人
    }
    casualParty();
    
  • 构造函数调用:使用new关键字调用函数时,this指向新创建的实例,就好像是给每个参加派对的人发放了一个专属的身份牌。

    function Person(name) {
        this.name = name;
    }
    var person = new Person('Alice');
    console.log(person.name); // 输出 "Alice"
    
  • 指定this调用:有时候你想要指定谁来主持这个活动,这时就可以使用callapplybind方法,它们允许你显式地指定this的值。

    var objA = {value: 'A'};
    var objB = {value: 'B'};
    function printValue() {
        console.log(this.value);
    }
    printValue.call(objA); // 输出 "A"
    
  • 箭头函数:这些特殊的活动没有自己的主持人,而是继承了上一级活动的主持人。换句话说,箭头函数不拥有自己的this,而是捕获定义时所在的作用域的this值。

    var objC = {
        value: 'C',
        arrowFunc: () => console.log(this.value)
    };
    objC.arrowFunc(); // 如果在全局作用域中定义,则输出全局的value或者undefined(严格模式)
    

实例分析:让派对更精彩

22.jpg

让我们通过一个具体的例子来深入理解如何利用 this 来举办一场精彩的派对。想象一下,我们有一个艺术家对象 artist,它有两个方法:performschedulePerformanceperform 方法会输出正在演出的艺术家的名字,而 schedulePerformance 方法则会在一秒钟后安排一次演出。

"use strict";
var globalName = "刀郎";

var artist = {
    name: '薛之谦',
    perform: function() {
        console.log(`正在演出的是 ${this.name}`);
    },
    schedulePerformance: function() {
        setTimeout(() => {
            this.perform(); // 这里的 this 正确指向 artist 对象
        }, 1000);
    }
};

artist.schedulePerformance(); // 一秒后输出 "正在演出的是 薛之谦"
理解

在这个例子中,schedulePerformance 使用了箭头函数作为 setTimeout 的回调。这里的关键点在于箭头函数没有自己的 this 绑定,而是继承了定义时所在上下文的 this 值。因此,在这个例子中,箭头函数内的 this 继承自 schedulePerformance 方法中的 this,也就是 artist 对象。这意味着即使在异步操作(如 setTimeout)中,this.perform() 中的 this 仍然正确地指向 artist,从而可以调用 perform 方法并正确显示艺术家的名字。

如果我们使用普通函数而不是箭头函数作为 setTimeout 的回调,情况就会有所不同:

schedulePerformance: function() {
    setTimeout(function() {
        this.perform(); // 这里的 this 不再指向 artist 对象
    }, 1000);
}

在这种情况下,this 将指向全局对象(在浏览器中是 window,在 Node.js 中是 global),因为普通函数创建了一个新的执行上下文,并且在非严格模式下默认绑定到全局对象。这会导致 this.perform 抛出错误,因为在全局作用域中并没有定义 perform 方法。

为了修复这个问题,我们可以显式地将 this 绑定到回调函数上,例如使用 .call(this) 方法:

schedulePerformance: function() {
    setTimeout(function() {
        this.perform(); // 现在 this 正确指向 artist 对象
    }.call(this), 1000);
}

或者,我们可以选择在 schedulePerformance 方法内部保存 this 的引用,然后在回调中使用该引用:

schedulePerformance: function() {
    var self = this; // 保存 this 引用
    setTimeout(function() {
        self.perform(); // 使用保存的 this 引用
    }, 1000);
}

这两种方法都可以确保 this 在异步回调中正确地指向 artist 对象。但是,使用箭头函数是最简洁和推荐的方式,因为它自然地继承了外部作用域的 this,减少了代码的复杂性,并避免了手动绑定 this 的需要。

综上所述,通过合理运用箭头函数、.call() 或者保存 this 的引用,我们可以确保在异步环境中正确处理 this 的绑定问题,从而实现更加可靠和直观的功能逻辑。

结语

掌握this的关键在于理解其背后的工作原理和不同调用模式下的行为变化。通过实践和深入思考,你可以更有效地利用this来构建复杂且可靠的JavaScript应用。希望这次的“派对”解释能让你对JavaScript的工作原理有了更深的理解和更多的乐趣!

15.jpg