为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

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

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

  • 变量查找:遵循词法作用域(Lexical Scope),由代码写在哪里决定(静态的)。

  • this 指向:遵循动态绑定(Dynamic Binding),由代码怎么被调用决定(动态的)。

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

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


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

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

javascript体验AI代码助手代码解读复制代码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 返回的,它的作用域链依然在定义时就固定了。

console.log(bar.myName) -> 输出 'time.geekbang.com'

  • 原因:这是显式的对象属性访问,与 this 无关,直接读取 bar 对象上的值。

console.log(this) & this.myName -> 输出 Windowundefined (或全局 myName)

  • 原因_printName()独立函数调用(前面没有点号)。

  • 规则:在非严格模式下,独立调用的 this 指向全局对象 window

  • 结果thiswindowwindow.myName 的值正是全局变量 '极客邦'(因为 var 声明的全局变量会自动挂载到 window 上)。

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

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

实验 A:把 foo() 里的 let 换成 var
javascript体验AI代码助手代码解读复制代码function foo() {   var myName = '极客时间'; // 换成 var   return bar.printName; }

  • 结果毫无变化

  • 解析:无论 foo 内部用 let 还是 varmyName 依然是 foo局部变量printName 函数的作用域链依然只包含它自己、全局作用域,不包含 foo 的执行上下文。变量查找依然跳过 foo,直接找到全局的 '极客邦'

实验 B:把全局的 var myName 改为 let myName
javascript体验AI代码助手代码解读复制代码// 全局 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体验AI代码助手代码解读复制代码// 方式一:独立调用 _printName();   // 方式二:对象方法调用 bar.printName();

🥊 巅峰对决

特性独立调用 (_printName())对象方法调用 (bar.printName())
语法形式函数名直接加括号,前面无归属对象.函数名(),前面有点号
this 指向window (非严格模式)bar 对象
this.myNamewindow.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体验AI代码助手代码解读复制代码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 -> 无限循环。

死代码return temObj 导致后面的 this.name 永远无法执行。且因为显式返回了对象,new 原本创建的 instance 被丢弃。

✅ 正确的“手动 New”姿势

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

javascript体验AI代码助手代码解读复制代码function CreateObj() {     this.name = '极客时间'; // 这里的 this 由外部 call 决定 }  var temObj = {}; temObj.__proto__ = CreateObj.prototype; CreateObj.call(temObj); // 只调用一次,绑定 temObj var myObj = temObj;

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


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

最后,来到浏览器前端。

html体验AI代码助手代码解读复制代码<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”**,你就能在代码的迷宫中游刃有余,写出既精准又优雅的逻辑!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
数据库工程师
手记
粉丝
1
获赞与收藏
3

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消