与 this 的甜蜜约会:JavaScript 中的浪漫探索

90 阅读5分钟

前言

在JavaScript这个充满魔法的世界里,有一个特别的角色——this。它是一个指向函数运行环境的神秘指针,一个可以让你的代码与众不同的魔法棒。今天,让我们一起享受一场甜蜜的约会,深入了解 this 的秘密,看看它是如何在 JavaScript 中施展它的魅力。

一、this的诞生之地:调用栈与执行上下文

每当一个函数被调用时,它就像是一场新的冒险开始。在这场冒险中,this扮演着非常重要的角色。它是一个指针,指向当前函数的执行上下文(即调用环境)。调用栈就像是一个舞台,每一个函数调用都是一个新的场景,在这个场景中,this会根据不同的情况选择自己要代表的对象。

二、this的身份转变:不同调用方式下的变身法则

  • 代码示例
'use strict';
var x = 2;
var obj = {
   x: 1,
   foo: function() {
       console.log(this);
       console.log(this.x);
   }
 }
 // 函数体
 var foo = obj.foo
 var obj2 = {
   x: 5,
   foo: foo
 }
 // 对象的方法被调用
 obj2.foo(); // this 指向 obj2, 输出 {x: 5, foo: [Function: foo]} 和 5
 obj.foo();  // this 指向 obj, 输出 {x: 1, foo: [Function: foo]} 和 1
 // 普通函数被调用
 foo(); // 2 

  • 对象方法的召唤: 当this在一个对象的方法中被唤醒时,它就会成为这个对象的代言人。例如:
  // 对象的方法被调用
  obj2.foo(); // this 指向 obj2, 输出 {x: 5, foo: [Function: foo]} 和 5
  obj.foo();  // this 指向 obj, 输出 {x: 1, foo: [Function: foo]} 和 1
  • 普通函数的独白: 如果this是在全局环境中被唤醒的,那么在非严格模式下它会化身成为整个世界的守护者——全局对象(在浏览器环境中即为window)。但在严格模式下,this会变得害羞,不愿意展示自己,这时它就是undefined

    'use strict';
    foo(); // 在严格模式下,this 是 undefined
    
  • 新生力量的崛起: 当this伴随着new关键字出现时,它便化身为新生事物的引领者,指向新创建的对象实例。

  • 指定召唤者的秘密: 有时候,开发者们会通过.call().apply().bind()等方法明确地告诉this谁是它的召唤者,从而确保它不会迷失方向。

  • 注意 调用.call().apply()方法会立即执行指定的函数,.bind() 并不会立即调用函数,而是返回一个新函数,这个新函数可以稍后被调用。 d338aba126581a4b7c4cbbe728a5d0f.png

  • .call() 接受的是一个参数列表。

  • .apply() 接受的是一个参数数组。

var a = {
    name: 'Cherry',
    // apply 一次性给,以数组的形式
    // call 一个个给 call(thisBinder, a,b,c, ...参数)
    fn: function(a, b) {
      console.log(this.name)
      console.log(a + b)
    }
  }
  
  var b = a.fn; // 普通函数
  console.log(b.apply(a, [1, 2]))
  console.log(b.call(a, [1, 2])) // 输出:1,2undefined

代码最后一步输出 1,2undefined 是因为将 [1,2] 赋值给了a,而b并没有被赋值,因此b输出是 undefined ,而中间的+是作为运算连接符,将两个字符串连接。输出结果不是数组而是字符串的原因是 [1,2].toString 是隐式类型转换。JavaScript 在进行加法操作时,如果其中一个操作数是字符串,它会将另一个操作数转换为字符串并执行字符串拼接。具体来说,数组 [1, 2] 被隐式转换成了它的字符串表示形式 "1,2"

可以将最后一步改为

console.log(b.call(a, 1, 2)); // 输出: Cherry 3
  • .bind()
var name = "刀郎"
var a = {
    name: "薛之谦",
    func1: function() {
        console.log(this.name);
    },
    func2: function() {
        setTimeout((function() {
            // this被指定了
            this.func1();
        }).bind(a),1000)
    }
}

a.func2(); // 输出薛之谦

三、异步世界的挑战:保持this的一致性

在异步操作如setTimeout中,this很容易失去自己的方向,因为它可能会脱离原来的作用域。但是,聪明的开发者总能找到办法解决这个问题,比如使用变量保存当前的this(如let _this = this;),或者利用箭头函数来保持this的绑定。

var a = {
    name: "杜",
    func1: function() {
        console.log(this.name);
    },
    func2: function() {
        console.log("func2",this);
        let _this = this;
        setTimeout((function() {
        // this 丢失
            _this.func1()
        }), 1000);
    }
}
a.func2();

四、箭头函数的小秘密:继承父级作用域的this

箭头函数就像是来自未来的使者,它并不遵循传统的this规则。相反,它总是继承外部作用域的this值,仿佛带着前世的记忆穿越而来。箭头函数在定义时(可以理解为编译阶段)就决定了 this 的值,而不是在调用时动态绑定。因此,在需要保持this一致性的地方,箭头函数是一个非常可靠的选择。

var a = {
    name: "杜",
    func1: function() {
        console.log(this.name);
    },
    func2: function() {
        console.log("func2",this); // 输出"杜"
        setTimeout((() => { // 箭头函数内部没有this
            this.func1(); // 箭头函数内的 this 继承自 func2 的 this
        }), 1000);
    }
}
a.func2();

结语:与this和谐共处

在JavaScript的世界里,this虽然看似变幻莫测,但只要我们理解了它背后的工作原理,就能轻松驾驭它,让它成为构建强大应用的好帮手。希望这篇小文能够帮助你更好地理解this这位神秘的朋友,并在编写代码的过程中更加得心应手!


通过上述内容,我们可以看到this的行为是由其调用上下文决定的,并且在不同的情况下有不同的表现形式。理解这些规则有助于写出更清晰、更可靠的JavaScript代码。