this 不是你想的 this:从作用域迷失到调用栈掌控

一、自由变量 vs 对象属性:

一段代码揭示 JavaScript 作用域的本质

我们从一段看似简单、却常被用作教学陷阱的 JavaScript 代码开始:

Js 复制代码
var bar = { 
  myName: "time.geekbang.com",
  printName: function() {
    console.log(myName); // 注意:这里没有 this,也没有声明 myName
  }
}

function foo() {
  let myName = '极客时间';
  return bar.printName;
}

let _printName = foo();
_printName(); // ❌ ReferenceError: myName is not defined

初看之下,这段代码似乎只是想"打印某个名字"。但运行后,控制台会抛出一个错误:

txt 复制代码
ReferenceError: myName is not defined

为什么会这样?

这背后涉及 JavaScript 中两个极易混淆的核心概念:词法作用域中的自由变量查找对象属性访问。它们看似相似,实则天差地别。


🔍 myName 到底是谁?

关键问题在于:printName 函数体中的 myName 指的是什么?

  • 不是 this.myName ------ 代码中压根没写 this
  • 它也不是 局部变量 ------ 函数内部没有用 varletconst 声明 myName

于是,JavaScript 引擎启动标识符解析(Identifier Resolution) 机制:沿着词法作用域链(Lexical Scope Chain) 向上查找这个变量。

📌 词法作用域由函数"定义的位置"决定,而非"调用的位置"

查找路径如下:

  1. printName 自身的作用域中查找 → 未找到
  2. 跳转到该函数定义时的外层作用域 → 这里是全局作用域
  3. 在全局作用域中查找名为 myName 的绑定。

然而,在初始版本中,全局作用域确实没有 myName 变量

⚠️ 重要区分:对象属性 ≠ 变量

尽管 bar 对象有一个 myName 属性,但 bar.myName属性访问表达式 ,而 myName 是一个自由变量(Free Variable)

JavaScript 不会自动将对象属性当作同名变量来解析。

因此,引擎找不到 myName,抛出 ReferenceError


✅ 添加全局变量后:为什么输出 "极客邦"?

现在我们在全局作用域添加一行:

Js 复制代码
let myName = '极客邦';

完整代码变为:

Html 复制代码
<script>
var bar = { 
  myName: "time.geekbang.com",
  printName: function() {
    console.log(myName); // 自由变量查找
  }
}

function foo() {
  let myName = '极客时间';
  return bar.printName;
}

let myName = '极客邦'; // ← 全局词法环境中的绑定
let _printName = foo();
_printName(); // 输出:'极客邦'
</script>

此时,printName 执行时:

  • 自身作用域无 myName
  • 沿词法作用域链向上 → 找到全局作用域中的 myName 绑定
  • 成功解析为 '极客邦'

💡 补充说明:

虽然 let 声明的全局变量不会挂载到 window 对象上 (即 window.myName === undefined),但它仍然存在于全局词法环境(Global Lexical Environment)中 ,对自由变量查找完全可见。

这正是 ES6 引入块级作用域后的设计:作用域查找 ≠ 全局对象属性查找


🎯 如何真正访问 "time.geekbang.com"

我们的目标其实是 bar 对象上的属性值 "time.geekbang.com"。既然 console.log(myName) 打印的是全局变量 '极客邦',那该如何正确访问对象属性?

答案很简单:显式通过对象引用访问

Js 复制代码
console.log(bar.myName); // ✅ 输出 "time.geekbang.com"

这行代码之所以成功,是因为它直接执行了属性访问操作 。只要 bar 在当前作用域可见(这里是全局),就能稳定、可靠地获取其属性。

🗣️ 虽说"直呼其名有点不礼貌"?

其实在编程中,清晰比委婉更重要

显式写出 bar.myName,是对代码可读性和可维护性的最大尊重。


⚠️ 那 this.myName 为什么不行?

第三行代码试图用 this 来访问:

Js 复制代码
console.log(this.myName); // ❌ 输出 undefined

问题出在 this 的绑定方式 上。

虽然 printNamebar 的方法,但我们是这样调用它的:

Js 复制代码
let _printName = foo();
_printName(); // 直接调用函数,没有通过 bar

这种调用方式下,this 指向全局对象 (如浏览器中的 window)。

但全局变量是用 let 声明的:

Js 复制代码
let myName = '极客邦';

let 声明的变量不会成为全局对象的属性,所以:

Js 复制代码
this.myName   // 等价于 window.myName → undefined

🔁 对比实验:

如果改成 var myName = '极客邦',那么 this.myName 会输出 '极客邦' ------

但请注意,这仍然是全局变量不是 bar.myName

因此,this.myName 在这里既不可靠,也不是你真正想访问的值。


🧩 小结:三种访问方式的本质区别

写法 机制 是否依赖作用域 能否访问 bar.myName
myName 自由变量(词法作用域) ❌(除非全局巧合)
bar.myName 对象属性访问 ❌(只需 bar 可见)
this.myName 动态上下文绑定 ❌(除非通过 bar.printName() 调用)

对象属性是数据,变量是绑定;前者靠引用访问,后者靠作用域查找。二者在 JavaScript 中属于完全不同的命名空间。

理解这一点,是避免"我以为它能找到"的关键。下一部分,我们将深入探讨 this 的动态绑定规则------为什么它如此"善变",又该如何掌控它。

二、this 的真相:

动态上下文如何由调用方式决定?

在第一节中,我们厘清了:

  • 变量查找是静态的(词法作用域)
  • 对象属性 ≠ 变量
  • 自由变量沿定义时的作用域链向上查找

而本节要揭示的是另一个平行但常被混淆的机制:

this 的值与作用域无关,它完全由函数的"调用方式"决定------它是动态的、运行时的上下文引用。

这正是初学者甚至中级开发者频繁踩坑的根源:this 当作"当前对象"或"作用域"的同义词

🎯 this 到底是谁?

在 JavaScript 中,this 是一个既基础又令人困惑的概念。很多开发者误以为 this 和"函数定义的位置"或"当前作用域"有关,但事实恰恰相反:this 的值完全由函数的调用方式决定,与词法作用域无关

本文将通过三个典型场景,带你一步步揭开 this 的真实面目,并建立一套可靠的判断逻辑。


第一部分:普通函数调用 → 默认绑定

Js 复制代码
function foo() {
    console.log(this);  // 输出 window(在浏览器中)
}
foo();  // 普通函数调用

非严格模式 下,当你直接调用一个函数(如 foo()),JavaScript 引擎会将该函数的 this 默认绑定到全局对象

  • 在浏览器环境中,全局对象是 window
  • 在 Node.js 中,则是 global

因此,上述代码会输出 window

❓ 为什么这看起来"不合理"?

因为我们本能地认为:"这个函数写在全局,那 this 应该代表'当前上下文'。"

但 JavaScript 的设计哲学是:this 不是静态的,而是动态的------它取决于"怎么调用",而不是"在哪定义"。

小结

普通函数调用(foo()) → this 指向全局对象

(非严格模式下是 window;严格模式下是 undefined


第二部分:call / apply → 显式绑定

Js 复制代码
let bar = {
    myName: "极客邦",
    test1: "1"
};

function foo() {
    this.myName = "极客时间";
}

foo.apply(bar);
console.log(bar); // { myName: "极客时间", test1: "1" }

这里我们重新定义了 foo,它的作用是给 this 对象设置 myName 属性。

关键在于这一行:

Js 复制代码
foo.apply(bar);

Function.prototype.apply(以及 callbind)允许我们显式指定函数执行时的 this
foo.apply(bar) 的含义是:

"调用 foo 函数,并强制让函数内部的 this 指向 bar 对象。"

于是,this.myName = "极客时间" 实际上等价于 bar.myName = "极客时间",成功修改了 bar 的属性。

小结

foo.call(obj)foo.apply(obj)this 被显式绑定为 obj

这是控制 this 最直接、最可靠的方式之一。


第三部分:对象方法调用 → 隐式绑定

Js 复制代码
function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo
};

obj.foo(); // 输出 2
  • 关键:通过 obj.foo() 调用函数

  • 在 JavaScript 中,当一个函数作为对象的方法被调用 时,函数内部的 this 会自动绑定到该对象 (即 obj)。

  • 所以在 foo 执行时:

    • this 指向 obj
    • this.a 就是 obj.a,即 2
  • 因此输出:2

小结

作为对象的方法调用(obj.method()) → this 指向 obj

但仅限于"点调用"形式,解构或赋值后调用会失效。


理解 this,是迈向 JavaScript 运行时高手的关键一步。下一次当你看到 this 时,别再问"它应该是什么",而是问: "它是怎么被调用的?" ------答案就在调用栈中。

第四部分:隐式绑定丢失

第三部分讲到了隐式绑定的概念,现在我们来讲讲隐式丢失

js 复制代码
var myObj = {
            name : "极客时间",
            showThis: function(){
                this.name = '极客邦';
                console.log(this);
            }
        }
        var foo = myObj.showThis;   // window.foo

⚠️ 隐式绑定为什么会"丢失"?

现在看你的代码:

Js 复制代码
var foo = myObj.showThis; // 仅获取函数引用,未调用
foo(); // ❌ this 不再是 myObj!

这一步发生了什么?

  1. myObj.showThis 是一个函数引用,它本身只是一个普通的函数值;
  2. 将其赋值给变量 foo 后,foomyObj 彻底断开联系
  3. 当你执行 foo() 时,这是一个普通函数调用 (没有通过对象),因此触发的是 默认绑定规则

在非严格模式下:

Js 复制代码
foo(); // 相当于 window.foo() → this = window

所以,在 showThis 内部:

Js 复制代码
this.name = '极客邦'; // 实际上是 window.name = '极客邦'
console.log(this);    // 输出 window 对象

🔥 这就是"隐式绑定丢失":
原本属于对象的方法,一旦被当作普通函数调用,就失去了与原对象的上下文关联,this 回退到全局对象(或 undefined)。

第五部分:new 调用 → new 绑定(最高优先级)

你可能写过这样的代码:

Js 复制代码
function CreateObj() {
    this.name = "极客时间";
}

var myObj = new CreateObj();
console.log(myObj.name); // "极客时间"

但你是否想过:为什么 this 在这里指向新创建的对象?

这背后是 new 操作符在运行时完成的一系列精密步骤。我们可以通过"手动实现 new"来还原其本质:

✅ 正确的手写 new 实现(教学版)
Js 复制代码
function myNew(Constructor, ...args) {
    // 1. 创建一个新对象
    const obj = {};

    // 2. 将新对象的 [[Prototype]] 链接到构造函数的 prototype
    obj.__proto__ = Constructor.prototype;

    // 3. 将构造函数内的 this 绑定到这个新对象,并执行构造函数
    const result = Constructor.apply(obj, args);

    // 4. 如果构造函数返回的是引用类型,则返回该值;否则返回新对象
    return (typeof result === 'object' && result !== null) ? result : obj;
}

🧠 new 绑定的核心规则:

当使用 new CreateObj() 时,JavaScript 引擎会按以下顺序执行

  1. 创建一个全新的 空对象
  2. 将该对象的内部 [[Prototype]] 链接到 CreateObj.prototype
  3. CreateObj 函数体内的 this 绑定到这个新对象;
  4. 执行构造函数体**(即 this.name = "极客时间");
  5. 如果构造函数没有显式返回一个对象,则自动返回新创建的对象。

第六部分:DOM 事件监听器中的 this

考虑这段常见代码:

Html 复制代码
<a href="#" id="link">点击我</a>
<script>
document.getElementById('link')
  .addEventListener('click', function() {
    console.log(this); // 👉 输出 <a id="link">点击我</a>
  });
</script>

尽管这是一个普通函数 作为回调传入,且以普通方式被调用 (我们并没有写 handler.call(element)

但它的 this 却神奇地指向了触发事件的 DOM 元素。

🔍 这是怎么发生的?

答案是:浏览器在内部调用你的回调函数时,显式绑定了 this

也就是说,浏览器大致做了这样的事:

Js 复制代码
// 浏览器内部伪代码
const element = document.getElementById('link');
const handler = function(event) { console.log(this); };

// 当点击发生时:
handler.call(element, event); // ← 显式将 this 设为 element!

因此,虽然你写的是一个"普通函数",但调用者(浏览器)使用了 .call() ,使得 this 指向事件目标(event target)。

✅ 这不是 JavaScript 引擎的默认行为,而是 DOM API 的设计约定


⚠️ 注意:箭头函数会破坏这一行为!

如果你改用箭头函数:

Js 复制代码
document.getElementById('link')
  .addEventListener('click', () => {
    console.log(this); // 👉 输出 window(非严格模式)
  });

你会发现 this 变成了全局对象。为什么?

因为:

  • 箭头函数没有自己的 this
  • 它的 this 继承自外层词法作用域(这里是全局作用域);
  • 浏览器即使想通过 .call() 绑定 this,也对箭头函数无效(规范规定箭头函数忽略 this 参数)。

📌 所以:如果你想在事件回调中使用 this 指向元素,必须使用普通函数。
💡 记住:this 的值永远取决于"谁在调用"以及"怎么调用"------即使是浏览器,也在遵守这一原则。

🧩 总结:this 绑定规则优先级(从高到低)

绑定类型 调用形式 this 指向
new 绑定 new Fn() 新创建的实例对象
显式绑定 fn.call(obj), fn.bind(obj) 指定的对象 obj
隐式绑定 obj.method() obj
默认绑定 fn() 全局对象 / undefined
箭头函数 任意 继承外层词法作用域的 this

💡 终极心法
不要问"this 应该是什么",而要问"它是怎么被调用的?"

答案,永远藏在调用栈中。

相关推荐
风止何安啊1 小时前
别被 JS 骗了!终极指南:JS 类型转换真相大揭秘
前端·javascript·面试
拉不动的猪1 小时前
深入理解 Vue keep-alive:缓存本质、触发条件与生命周期对比
前端·javascript·vue.js
over6971 小时前
深入理解 JavaScript 原型链与继承机制:从 instanceof 到多种继承模式
前端·javascript·面试
烂不烂问厨房1 小时前
前端实现docx与pdf预览
前端·javascript·pdf
GDAL2 小时前
Vue3 Computed 深入讲解(聚焦 Vue3 特性)
前端·javascript·vue.js
Moment2 小时前
半年时间使用 Tiptap 开发一个和飞书差不多效果的协同文档 😍😍😍
前端·javascript·后端
前端加油站2 小时前
记一个前端导出excel受限问题
前端·javascript
坐吃山猪2 小时前
Electron02-Hello
开发语言·javascript·ecmascript
尘世中一位迷途小书童2 小时前
JavaScript 一些小特性:让你的代码更优雅高效
前端·javascript·架构