JavaScript 中 this 的行为与作用域链的深度解析
在 JavaScript 的世界中,this 关键字一直是一个让人又爱又恨的存在。它灵活多变,却又常常令人困惑。尤其是在函数被赋值、传递或作为回调使用时,this 的指向可能与我们的直觉大相径庭。本文将结合一段典型代码,深入剖析 this 的工作机制,并澄清它与**词法作用域(Lexical Scope)和执行上下文(Execution Context)**之间的关系。
示例代码回顾
<script>
'use strict';
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(myName); // 自由变量查找
console.log(bar.myName); // 显式访问对象属性
console.log(this.myName); // this 绑定问题
}
};
function foo() {
let myName = '极客时间';
return bar.printName;
}
let myName = '极客邦';
let _printName = foo();
_printName(); // 普通函数调用
</script>
这段代码看似简单,却蕴含了 JavaScript 中几个核心概念:自由变量、作用域链、this 绑定规则、严格模式的影响。
一、变量查找:词法作用域 vs 自由变量
1. 什么是自由变量?
在 printName 函数内部:
console.log(myName);
这里的 myName 并不是该函数的参数,也不是用 var/let/const 在函数内部声明的局部变量,因此它是一个自由变量(free variable) 。
JavaScript 引擎会沿着词法作用域链(Lexical Scope Chain)向上查找这个变量。由于 printName 是在全局作用域中定义的(尽管它是 bar 对象的一个方法),它的外层作用域就是全局作用域。
✅ 关键点:函数的作用域由声明位置决定,而不是调用位置。
所以:
console.log(myName)会输出全局的myName,即'极客邦'。- 即使
foo()内部有let myName = '极客时间',也不会影响printName,因为printName并不是在foo内部定义的!
二、this 的绑定机制:动态绑定与执行上下文
1. this 是执行上下文的一部分
在 JavaScript 中,每当一个函数被调用,都会创建一个新的执行上下文(Execution Context) 。执行上下文包含四个核心组成部分:
- 变量环境(Variable Environment)
- 词法环境(Lexical Environment)
- outer(外部作用域引用)
- this 值
🧠 重点强调:
this不是变量,而是执行上下文的一个属性,其值在函数调用时确定,属于“动态绑定”范畴。
三、this 指向的五种常见情况
✅ 1. 作为对象的方法被调用 → this 指向该对象
当函数是某个对象的属性,并通过对象调用时,this 指向该对象。
var obj = {
name: "Alice",
sayHello: function() {
console.log(this.name); // 输出 "Alice"
}
};
obj.sayHello(); // 此时 this === obj
💡 原理:调用表达式
obj.sayHello(),JS 引擎将obj作为this绑定到函数执行上下文中。
✅ 2. 作为普通函数被调用 → this 指向全局对象(非严格模式)或 undefined(严格模式)
如果函数没有通过对象调用,而是直接执行,this 的行为取决于是否开启严格模式。
function greet() {
console.log(this);
}
greet(); // 非严格模式:window(浏览器)或 global(Node.js)
// 严格模式:undefined
⚠️ 注意:即使函数是对象的方法,但若被提取后调用(如本例中的
_printName()),也会失去原始绑定。
✅ 3. 使用 call() / apply() 显式绑定 this
这两个方法允许你手动指定 this 的值。
var obj1 = { name: "Bob" };
var obj2 = { name: "Charlie" };
function sayName() {
console.log(this.name);
}
sayName.call(obj1); // 输出 "Bob"
sayName.apply(obj2); // 输出 "Charlie"
call()接受参数列表:func.call(context, arg1, arg2)apply()接受参数数组:func.apply(context, [arg1, arg2])
✅ 这是解决
this指向问题的经典方式,尤其适用于回调函数。
✅ 4. 构造函数调用 → this 指向新创建的实例
当函数用 new 关键字调用时,this 指向新创建的对象实例。
function Person(name) {
this.name = name; // this 指向新实例
}
var p = new Person("David");
console.log(p.name); // "David"
🔁 流程:
- 创建一个空对象
{};- 将该对象的原型指向构造函数的
prototype;- 执行构造函数,
this指向该对象;- 返回该对象(除非显式返回其他对象)。
✅ 5. 事件处理函数中 → this 指向事件绑定的元素
在 DOM 事件中,this 默认指向触发事件的元素。
<button id="btn">点击我</button>
<script>
document.getElementById('btn').onclick = function() {
console.log(this); // 指向 <button> 元素
};
</script>
📌 说明:这是浏览器环境下的特殊行为,底层是通过
addEventListener或直接赋值实现的,this被隐式绑定到目标元素。
四、this 的绑定优先级(从高到低)
| 优先级 | 绑定方式 | 示例 |
|---|---|---|
| 1 | new 构造调用 | new MyConstructor() |
| 2 | call() / apply() | func.call(obj) |
| 3 | 对象方法调用 | obj.method() |
| 4 | 普通函数调用(严格模式) | func() → undefined |
| 5 | 普通函数调用(非严格模式) | func() → global/window |
✅ 记忆口诀:
new > call/apply > 对象方法 > 普通调用
五、this 是执行上下文的属性,而非变量
再次强调:this 不是一个变量,它是执行上下文的一个属性,在函数执行前就确定了。
function testThis() {
console.log(this);
}
// 无论怎么调用,this 的值都是在调用时才确定
testThis(); // undefined (strict)
(new testThis)(); // testThis 实例
testThis.call({}); // {}
🔄 每次函数调用都会创建新的执行上下文,其中包含独立的
this值。
六、避免 this 陷阱的最佳实践
-
使用箭头函数:箭头函数没有自己的
this,继承外层作用域的this。var obj = { name: "Eve", method: () => { console.log(this.name); // this 指向全局,不是 obj } }; -
使用
.bind()预绑定thisvar obj = { name: "Frank" }; var func = function() { console.log(this.name); }.bind(obj); func(); // 输出 "Frank" -
避免在循环中使用
this未绑定的函数var buttons = document.querySelectorAll('button'); buttons.forEach(function(btn) { btn.onclick = function() { console.log(this); // 可能不是预期的 btn }; });改进方式:
buttons.forEach(function(btn) { btn.onclick = function() { console.log(btn); // 使用闭包保存 btn }.bind(btn); // 或者 bind });
七、总结:this 的本质与设计哲学
| 类别 | 特性 |
|---|---|
| 变量查找 | 词法作用域决定,静态绑定 |
| this 绑定 | 动态绑定,由调用方式决定 |
| 执行上下文 | 包含 this、变量环境、词法环境等 |
| 最佳实践 | 使用 bind、箭头函数、new 控制 this |
💬 总结一句话:
this是执行上下文的一部分,它的值由调用方式决定,而非函数定义位置。
结语
JavaScript 的 this 和作用域机制看似混乱,实则有其内在逻辑。理解“词法作用域管变量,调用方式管 this”这一原则,就能在复杂场景中游刃有余。正如那句老话所说:“知道坑在哪,才能绕着走。 ”
希望本文能帮你彻底理清 this 与作用域的关系。欢迎在评论区分享你的踩坑经历!
- 变量环境:存储变量和函数声明(如
var,function) - 词法环境:存储块级作用域变量(如
let,const) - outer:指向外部词法环境,形成作用域链
- this:当前执行上下文的
this值,由调用方式决定
🌟 理解执行上下文,是掌握 JS 执行机制的关键一步。