一、自由变量 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; - 它也不是 局部变量 ------ 函数内部没有用
var、let或const声明myName。
于是,JavaScript 引擎启动标识符解析(Identifier Resolution) 机制:沿着词法作用域链(Lexical Scope Chain) 向上查找这个变量。
📌 词法作用域由函数"定义的位置"决定,而非"调用的位置" 。
查找路径如下:
- 在
printName自身的作用域中查找 → 未找到; - 跳转到该函数定义时的外层作用域 → 这里是全局作用域;
- 在全局作用域中查找名为
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 的绑定方式 上。
虽然 printName 是 bar 的方法,但我们是这样调用它的:
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(以及 call、bind)允许我们显式指定函数执行时的 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指向objthis.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!
这一步发生了什么?
myObj.showThis是一个函数引用,它本身只是一个普通的函数值;- 将其赋值给变量
foo后,foo和myObj彻底断开联系; - 当你执行
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 引擎会按以下顺序执行
- 创建一个全新的 空对象;
- 将该对象的内部
[[Prototype]]链接到CreateObj.prototype; - 将
CreateObj函数体内的this绑定到这个新对象; - 执行构造函数体**(即
this.name = "极客时间"); - 如果构造函数没有显式返回一个对象,则自动返回新创建的对象。
第六部分: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应该是什么",而要问"它是怎么被调用的?"答案,永远藏在调用栈中。