JavaScript内核与高级编程之:`JavaScript`的`this`指向:从`Call Stack`看其动态绑定。

各位观众老爷,晚上好!我是你们的老朋友,代码界的段子手。今天咱们聊点刺激的——JavaScript 的 this 指向!

相信不少小伙伴都曾被 this 虐得死去活来,一会儿指向 window,一会儿指向按钮,一会儿又 undefined 了,简直比渣男还善变!今天,我就要带着大家从 Call Stack 的角度,扒一扒 this 动态绑定的底裤,保证让大家以后再也不怕 this 了。

开胃小菜:this 是个啥?

在正式开始之前,咱们先简单回顾一下 this 到底是个什么玩意儿。

简单来说,this 就是一个指针,指向函数执行时的执行上下文(Execution Context)。而执行上下文又包含了变量环境、词法环境、以及最重要的 this 绑定。

记住一句话:this 的指向,取决于函数是如何被调用的,而不是函数如何被定义的! 这就是 this 动态绑定的核心思想。

正餐:从 Call Stack 看 this 的动态绑定

好,开胃小菜吃完了,咱们上正餐!要理解 this 的动态绑定,就必须先了解 Call Stack。

1. 什么是 Call Stack?

Call Stack(调用栈)是 JavaScript 引擎追踪函数执行流程的一种数据结构。想象一下,它就像一摞盘子,每调用一个函数,就往上面放一个盘子(叫做 Stack Frame),函数执行完毕,就从上面拿掉一个盘子。

每个 Stack Frame 都包含了函数的信息,比如函数名、参数、以及最重要的 执行上下文。而 this 就在这个执行上下文中。

2. Call Stack 如何影响 this 的指向?

关键就在于:当函数被调用时,JavaScript 引擎会根据调用方式,来确定当前 Stack Frame 的 this 绑定。

下面我们来分情况讨论:

(1) 默认绑定(Default Binding):全局环境 or undefined

这是最简单的情况,当函数独立调用(也就是直接 func() 这样调用)时,this 的指向取决于是否是严格模式。

  • 非严格模式: this 指向全局对象,在浏览器中通常是 window
  • 严格模式: this 指向 undefined
function foo() {
  console.log(this);
}

foo(); // 非严格模式: window, 严格模式: undefined

"use strict";
function bar() {
  console.log(this);
}

bar(); // undefined

Call Stack 分析:

  • 非严格模式: 当调用 foo() 时,JavaScript 引擎创建一个 Stack Frame,由于是独立调用,引擎会将 this 绑定到全局对象 window
  • 严格模式: 当调用 bar() 时,JavaScript 引擎创建一个 Stack Frame,由于是严格模式下的独立调用,引擎会将 this 绑定到 undefined

(2) 隐式绑定(Implicit Binding):谁调用我,我就指向谁

当函数作为对象的方法被调用时,this 指向调用该方法的对象。

const obj = {
  name: "小明",
  sayHello: function() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

obj.sayHello(); // Hello, I'm 小明

Call Stack 分析:

  • 当调用 obj.sayHello() 时,JavaScript 引擎创建一个 Stack Frame。
  • 引擎发现 sayHello 是作为 obj 的方法被调用的,因此将 this 绑定到 obj

注意:隐式绑定可能会丢失!

const obj = {
  name: "小明",
  sayHello: function() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

const greet = obj.sayHello; // 将方法赋值给一个变量

greet(); // Hello, I'm undefined (非严格模式) 或 报错 (严格模式)

Call Stack 分析:

  • greet() 被调用时,JavaScript 引擎创建一个 Stack Frame。
  • 虽然 greet 之前引用了 obj.sayHello,但现在 greet 是被独立调用的,所以 this 遵循默认绑定规则,指向全局对象或 undefined

(3) 显式绑定(Explicit Binding):call、apply、bind

显式绑定允许我们手动指定 this 的指向。

  • callapply: 立即执行函数,并指定 this 的指向。两者的区别在于参数传递方式不同。call 接收参数列表,apply 接收参数数组。
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const obj = { name: "小红" };

greet.call(obj, "你好"); // 你好, 小红
greet.apply(obj, ["Hello"]); // Hello, 小红

Call Stack 分析:

  • greet.call(obj, "你好")greet.apply(obj, ["Hello"]) 被调用时,JavaScript 引擎创建一个 Stack Frame。

  • callapply 强制将 this 绑定到 obj

  • bind: 创建一个新的函数,并将 this 永久绑定到指定对象。

function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const obj = { name: "小刚" };

const boundGreet = greet.bind(obj); // 创建一个新函数, this 绑定到 obj
boundGreet("Hi"); // Hi, 小刚

Call Stack 分析:

  • greet.bind(obj) 被调用时,bind 方法创建一个新的函数 boundGreet
  • boundGreet 内部的 this 已经被永久绑定到 obj,即使以后 boundGreet 被独立调用,this 仍然指向 obj

(4) new 绑定(New Binding):构造函数

当使用 new 关键字调用函数时,会发生以下几个步骤:

  1. 创建一个新的空对象。
  2. 将新对象的原型指向构造函数的 prototype 属性。
  3. this 绑定到新对象。
  4. 执行构造函数中的代码。
  5. 如果构造函数没有显式返回一个对象,则返回新对象。
function Person(name) {
  this.name = name;
  console.log(this); // 指向新创建的 Person 实例
}

const person = new Person("小强"); // Person {name: "小强"}
console.log(person.name); // 小强

Call Stack 分析:

  • new Person("小强") 被调用时,JavaScript 引擎创建一个 Stack Frame。
  • new 关键字强制将 this 绑定到新创建的 Person 实例。

优先级:显式绑定 > 隐式绑定 > 默认绑定

当多种绑定规则同时存在时,优先级如下:

  • new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
function foo(something) {
  this.a = something;
  return this;
}

var obj1 = {};
var obj2 = {};

// 隐式绑定
foo.call( obj1, "a" ); // 显式绑定
obj2 = foo.bind( obj1, "b" );
obj2("c");
console.log( obj1.a ); // b

表格总结:this 的指向

调用方式 this 指向 说明
独立调用 全局对象 (非严格模式) 或 undefined (严格模式) 遵循默认绑定规则。
作为对象的方法调用 调用该方法的对象 遵循隐式绑定规则。
call / apply 指定的对象 遵循显式绑定规则。
bind 指定的对象 创建一个新的函数,并将 this 永久绑定到指定对象。
new 新创建的对象 遵循 new 绑定规则。
箭头函数 继承自外层作用域的 this 箭头函数没有自己的 this,它会捕获其所在上下文的 this 值,作为自己的 this 值。这意味着箭头函数中的 this 值在定义时就已经确定,并且永远不会改变。这使得箭头函数非常适合用于回调函数,因为它们可以避免 this 指向问题。但是,这也意味着箭头函数不能用作构造函数,因为它们没有自己的 this 值。

彩蛋:箭头函数的 this

箭头函数是个特殊的存在,它没有自己的 this,它的 this 是继承自外层作用域的 this

const obj = {
  name: "小李",
  sayHello: function() {
    setTimeout(() => {
      console.log(`Hello, I'm ${this.name}`);
    }, 1000);
  }
};

obj.sayHello(); // Hello, I'm 小李

Call Stack 分析:

  • obj.sayHello() 被调用时,this 指向 obj
  • setTimeout 接收一个箭头函数作为回调函数。
  • 箭头函数没有自己的 this,它会捕获 sayHello 函数的 this,也就是 obj

总结:this 的终极奥义

理解 this 的关键在于:

  1. 明确函数的调用方式 (独立调用、作为对象方法调用、call/apply/bindnew)。
  2. 理解 Call Stack 的作用,知道 JavaScript 引擎是如何根据调用方式来确定 this 绑定的。
  3. 记住优先级规则

只要掌握了这三点,this 就不再是你的噩梦,而是你手中的利器!

练习题:

var name = "Global";

var obj = {
    name: "Obj",
    getName: function () {
        return function () {
            return this.name;
        };
    }
};

console.log(obj.getName()()); // 输出什么?

答案是 "Global"。

解释:

  1. obj.getName() 返回一个匿名函数。
  2. 这个匿名函数被立即调用 (),但它是独立调用,不是作为 obj.getName() 的方法调用,因此 this 指向全局对象 (window)。
  3. 全局对象的 name 属性是 "Global"。

希望今天的分享能帮助大家彻底理解 JavaScript 的 this 指向。记住,代码虐我千百遍,我待代码如初恋!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注