JavaScript 中的 `this` 与变量查找:一场关于“身份”与“作用域”的深度博弈

JavaScript 中的 this 与变量查找:一场关于"身份"与"作用域"的深度博弈

在 JavaScript 的浩瀚宇宙中,有两个概念让无数开发者爱恨交织:一个是像变色龙一样的 this ,另一个是像迷宫一样的 作用域链(Scope Chain)

很多初学者容易混淆这两者:以为 this 也是沿着作用域链查找的,或者以为变量查找会受 this 影响。事实恰恰相反

  • 变量查找 :遵循词法作用域(Lexical Scope) ,由代码写在哪里决定(静态的)。
  • this 指向 :遵循动态绑定(Dynamic Binding) ,由代码怎么被调用决定(动态的)。

就像一个人的社会身份(this)取决于他此刻站在哪个舞台上,而他的记忆(变量查找)取决于他出生和成长的地方(代码声明的位置)。

本文将基于深度对话中的四个经典场景,从变量查找陷阱到构造函数迷局,再到 DOM 事件与调用方式的终极对比,带你彻底看透 JavaScript 的核心机制。


第一幕:错位的记忆 ------ 变量查找 vs this 指向

让我们从一个极具迷惑性的代码片段开始。这段代码完美展示了**"变量去哪找" this 指向谁**是完全平行的两条线。

javascript 复制代码
var bar = { 
  myName: "time.geekbang.com",
  printName: function() {
    // 【变量查找】:沿着作用域链向上找
    // 1. 函数内部有没有 myName? 没有。
    // 2. 外层作用域(全局)有没有 myName? 有!值是 '极客邦'
    console.log(myName); // 输出:极客邦
    
    // 【对象属性访问】:直接访问 bar 对象的属性
    console.log(bar.myName); // 输出:time.geekbang.com
    
    // 【this 指向】:取决于调用方式
    console.log(this); 
    console.log(this.myName);
  }
}

function foo() {
  let myName = '极客时间'; // 注意:这是 foo 内部的局部变量
  return bar.printName;    // 返回的是函数引用,带走了吗?没有!
}

// 全局变量
var myName = '极客邦';

// 获取函数引用
var _printName = foo();

// 【关键调用】:独立函数调用
_printName(); 

🕵️‍♂️ 深度剖析:当 _printName() 执行时

假设我们在浏览器环境(非严格模式)下运行 _printName(),结果如下:

  1. console.log(myName) -> 输出 '极客邦'

    • 原因 :这是自由变量查找。
    • 路径 :函数内部找不到 -> 沿着词法作用域链 向外找 -> 找到全局作用域下的 var myName = '极客邦'
    • 误区 :很多人以为它会找到 foo 里的 '极客时间'错! printName 函数是在 bar 对象里定义的(全局作用域),它的"出生地"决定了它只能看到全局变量,根本看不见 foo 内部的 let myName。哪怕它是通过 foo 返回的,它的作用域链依然在定义时就固定了。
  2. console.log(bar.myName) -> 输出 'time.geekbang.com'

    • 原因 :这是显式的对象属性访问,与 this 无关,直接读取 bar 对象上的值。
  3. console.log(this) & this.myName -> 输出 Windowundefined (或全局 myName)

    • 原因_printName()独立函数调用(前面没有点号)。
    • 规则 :在非严格模式下,独立调用的 this 指向全局对象 window
    • 结果thiswindowwindow.myName 的值正是全局变量 '极客邦'(因为 var 声明的全局变量会自动挂载到 window 上)。

⚖️ 变量修改实验:let vs var 的蝴蝶效应

现在,我们来玩两个"如果",看看世界如何改变。

实验 A:把 foo() 里的 let 换成 var
javascript 复制代码
function foo() {
  var myName = '极客时间'; // 换成 var
  return bar.printName;
}
  • 结果毫无变化
  • 解析 :无论 foo 内部用 let 还是 varmyName 依然是 foo局部变量printName 函数的作用域链依然只包含它自己、全局作用域,不包含 foo 的执行上下文。变量查找依然跳过 foo,直接找到全局的 '极客邦'
实验 B:把全局的 var myName 改为 let myName
javascript 复制代码
// 全局
let myName = '极客邦'; // 换成 let
  • 结果
    • console.log(myName) -> 报错!ReferenceError: myName is not defined (如果在某些模块环境) 或者依然能访问到?
    • 修正解析 :在全局作用域用 let 声明的变量不会 挂载到 window 对象上,但它依然在全局词法环境中。
    • console.log(myName) (第一行) -> 依然输出 '极客邦'。因为变量查找是沿着词法作用域链,能找到全局 let 变量。
    • console.log(this.myName) (最后一行) -> 输出 undefined
    • 核心差异this 指向 window,而 window 对象上没有 myName 属性(因为 let 不挂载到 window)。
    • 结论 :变量查找找到了值,但 this 查找失败了。这再次证明了变量查找路径this 指向是两套完全独立的系统。

💡 核心洞察函数带走的是"代码",不是"环境"printName 被返回后,它依然坚守着它出生时的作用域链(全局),对 foo 内部的秘密(局部变量)一无所知。而 this 则像个墙头草,谁调用它,它就指向谁。


第二幕:身份的切换 ------ 两种调用方式的终极对决

紧接着上面的代码,如果我们换一种调用方式,世界瞬间反转:

javascript 复制代码
// 方式一:独立调用
_printName(); 

// 方式二:对象方法调用
bar.printName();

🥊 巅峰对决

特性 独立调用 (_printName()) 对象方法调用 (bar.printName())
语法形式 函数名直接加括号,前面无归属 对象.函数名(),前面有点号
this 指向 window (非严格模式) bar 对象
this.myName window.myName ('极客邦') bar.myName ('time.geekbang.com')
变量 myName 依然找全局 ('极客邦') 依然找全局 ('极客邦')
本质逻辑 函数失去了上下文,回归默认 函数明确了所有者,指向调用者
  • _printName() :就像把一个员工从公司(bar)开除,让他去大街上(全局)流浪。此时他代表的是"路人甲"(window)。
  • bar.printName() :员工在公司打卡上班。此时他明确代表"极客时间官网"(bar)。

💡 核心洞察点号(.)是 this 的开关 。只要有 obj.func() 的形式,this 就是 obj。一旦把函数赋值给变量再调用(var f = obj.func; f()),点号消失,this 也就迷失了。


第三幕:错位的时空 ------ 构造函数中的递归迷局

除了对象方法,new 操作符是 this 的另一个重要舞台。但这里同样藏着陷阱。

javascript 复制代码
function CreateObj() {
    var temObj = {};             
    CreateObj.call(temObj);      // ⚠️ 致命递归
    temObj.__proto__ = CreateObj.prototype;
    return temObj;               
    console.log(this);           // 死代码
    this.name = '极客时间';      
}

var myObj = new CreateObj();

🚨 崩溃现场

这段代码试图在构造函数内部手动模拟 new,却导致了 栈溢出(RangeError)

  1. new 的隐式魔法 :执行 new CreateObj() 时,引擎已经创建了实例 instance 并绑定了 this
  2. 致命的递归CreateObj.call(temObj) 并不是改变当前的 this,而是开启了一次全新的函数调用
    • 新调用 -> 创建新 temObj -> 再次 call -> 无限循环。
  3. 死代码return temObj 导致后面的 this.name 永远无法执行。且因为显式返回了对象,new 原本创建的 instance 被丢弃。

✅ 正确的"手动 New"姿势

要在外部模拟 new,必须在函数外控制:

javascript 复制代码
function CreateObj() {
    this.name = '极客时间'; // 这里的 this 由外部 call 决定
}

var temObj = {};
temObj.__proto__ = CreateObj.prototype;
CreateObj.call(temObj); // 只调用一次,绑定 temObj
var myObj = temObj;

💡 核心洞察this 在函数执行瞬间即被定格。你无法在函数内部通过 call 篡改当前执行的 this,那只会开启新的轮回。


第四幕:舞台的主角 ------ DOM 事件中的本能反应

最后,来到浏览器前端。

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

🎭 舞台规则

addEventListener 的普通函数回调中:

this 自动指向触发事件的 DOM 元素。

  • 谁被点了? <a> 标签。
  • this 是谁? <a> 标签。

⚠️ 陷阱 :若改用箭头函数 () => {}this 将不再指向 <a>,而是继承外层(通常是 window)。所以在处理 DOM 事件时,普通函数是首选


🏁 终极总结:掌握 JavaScript 的双核驱动

通过这四幕大戏,我们理清了 JavaScript 中最容易混淆的两个核心机制:

1. 变量查找(静态的·出身的烙印)

  • 规则 :沿着词法作用域链向上查找。
  • 决定因素 :函数写在哪里(声明位置)。
  • 特点 :一旦函数定义完成,它能访问哪些变量就永久固定 了,不受调用方式影响。
    • 案例printName 无论在哪儿调用,它永远只能找到全局的 myName,找不到 foo 内部的 myName

2. this 指向(动态的·舞台的身份)

  • 规则 :看调用方式(Call Site)。
  • 决定因素 :函数怎么被调用
  • 四大场景
    1. 独立调用 (func()) -> window (非严格模式)。
    2. 方法调用 (obj.func()) -> obj
    3. 构造调用 (new Func()) -> 新实例。
    4. 事件回调 (element.addEventListener(..., function)) -> DOM 元素。
    5. 显式绑定 (call/apply/bind) -> 指定的对象(开启新调用)。

🗝️ 钥匙在手

  • 如果你想访问外层变量 ,请关心作用域链(代码写在哪)。
  • 如果你想操作当前对象 ,请关心 this(代码怎么调)。
  • 切记 :不要试图在函数内部用 call 改变当前的 this,那是徒劳的;也不要以为函数被传递后能带走它的局部变量环境,那也是错觉。

JavaScript 的灵活性赋予了它强大的能力,也带来了复杂性。但只要分清**"静态的作用域""动态的 this"**,你就能在代码的迷宫中游刃有余,写出既精准又优雅的逻辑!

相关推荐
Kakarotto21 小时前
Canvas 直线点击事件处理优化
javascript·vue.js·canvas
顺遂21 小时前
基于Rokid CXR-M SDK的引导式作业辅导系统设计与实现
前端
代码搬运媛21 小时前
Generator 迭代器协议 & co 库底层原理+实战
前端
前端拿破轮21 小时前
从0到1搭建个人网站(三):用 Cloudflare R2 + PicGo 搭建高速图床
前端·后端·面试
功能啥都不会21 小时前
PM2 使用指南 - 踩坑记录
前端
HelloReader21 小时前
React 中 useState、useEffect、useRef 的区别与使用场景详解,终于有人讲明白了
前端
兆子龙21 小时前
CSS 里的「if」:@media、@supports 与即将到来的 @when/@else
前端
踩着两条虫21 小时前
AI 智能体如何重构开发工作流
前端·人工智能·低代码
代码老中医21 小时前
逃离"Div汤":2026年,当AI写了75%的代码,前端开发者还剩什么?
前端