别再猜this指向!JS动态绑定的底层逻辑与实战
this 是JavaScript中最容易让人困惑的概念之一------它既不是静态绑定,也不遵循词法作用域规则,而是由函数的调用方式决定 。本文将结合实战代码案例,从自由变量、执行上下文、调用方式三个维度,彻底拆解this的设计逻辑、指向规则和常见陷阱,让你从根源理解this的本质。
一、先理清:this vs 自由变量(词法作用域)
在讲this之前,必须先区分自由变量和this的核心差异------这是理解this的关键前提。
1. 自由变量:遵循词法作用域(编译阶段确定)
自由变量指"函数内部使用,但既不是参数也不是局部变量"的变量,其查找规则是沿着词法作用域链(Outer)向上找 ,由函数声明的位置决定,与调用方式无关。
实战代码解析:自由变量的查找
javascript
'use strict'
// 全局作用域定义对象bar
var bar = {
myName: "time.geekbang.com",
printName: function() {
// 自由变量:myName既不是参数,也不是局部变量
console.log(myName); // 输出:极客邦(全局作用域的myName)
console.log(bar.myName); // 输出:time.geekbang.com(对象属性)
// this:与自由变量无关,由调用方式决定
console.log(this);
console.log(this.myName);
}
}
function foo() {
let myName = '极客时间' // 函数内局部变量
return bar.printName // 返回函数引用
}
// 全局作用域定义自由变量myName
var myName = '极客邦'
var _printName = foo();
// 调用场景1:普通函数调用
_printName();
// 自由变量myName:找全局的「极客邦」(词法作用域决定)
// this:指向window(普通函数调用规则)
// this.myName:window.myName → 极客邦
// 调用场景2:对象方法调用
bar.printName();
// 自由变量myName:依然是全局的「极客邦」(词法作用域不变)
// this:指向bar对象(方法调用规则)
// this.myName:bar.myName → time.geekbang.com
核心结论:
- 自由变量的查找路径在编译阶段就已确定(词法作用域),无论函数怎么调用,查找规则不变;
this的指向在执行阶段确定,由函数的调用方式决定------这是JS设计上的"特殊点"。
2. this的设计背景:为什么需要this?
JS早期没有class语法,要实现面向对象(OOP),需要一种机制让"对象的方法能访问对象自身的属性"。this就是这个机制:在对象方法内部,通过this指向对象本身,从而访问对象属性。
但JS的设计有个"缺陷":this的指向不是固定的------它不绑定到函数本身,而是绑定到调用上下文,这也是this容易混乱的根源。
二、this的核心规则:调用方式决定指向
this的指向只有一个核心原则:谁调用函数,this就指向谁。以下是5种常见调用场景,结合代码逐一解析。
场景1:普通函数调用 → this指向全局对象(非严格模式)
普通函数调用指"直接调用函数名",非严格模式下this指向全局对象(浏览器中是window),严格模式下this为undefined。
代码解析:
javascript
function foo() {
console.log(this); // 输出:window(非严格模式)
}
foo() // 等价于 window.foo() → 全局对象调用
// 严格模式下
'use strict'
function bar() {
console.log(this); // 输出:undefined
}
bar()
关键细节:
var声明的全局变量会挂载到window上(如var myName = '极客邦'→window.myName = '极客邦'),容易造成全局污染;let/const声明的全局变量不会挂载到window上,这是ES6的优化;- 严格模式下禁止"无意义的this指向全局",直接设为
undefined,规避全局污染问题。
场景2:对象方法调用 → this指向调用对象
当函数作为对象的方法被调用时,this指向该对象(即"调用者")。
代码解析:
javascript
var myObj = {
name: "极客时间",
showThis:function() {
console.log(this); // 输出:myObj对象
}
}
myObj.showThis(); // myObj调用方法 → this指向myObj
// 陷阱:方法被赋值给变量后,变为普通函数调用
var foo = myObj.showThis;
foo(); // 普通函数调用 → this指向window(非严格模式)
核心陷阱:
- 函数的"方法身份"只在调用时生效,一旦将方法赋值给变量,函数就变回"普通函数",
this指向全局; - 例:
myObj.showThis()是方法调用(this→myObj),foo()是普通调用(this→window)。
场景3:call/apply调用 → this指向第一个参数
call和apply是显式绑定this的方法,第一个参数就是this的指向(若传null/undefined,非严格模式下指向window)。
代码解析:
javascript
let bar = {
myName: "极客邦",
text1: 1
}
function foo() {
this.myName = "极客时间" // this指向call/apply的第一个参数
}
// 显式绑定this为bar
foo.call(bar);
// 等价写法:foo.apply(bar);
console.log(bar); // 输出:{myName: '极客时间', text1: 1}
call vs apply:
- 相同点:第一个参数都是
this指向,都立即执行函数; - 不同点:参数传递方式------
call传多个参数(foo.call(bar, 1, 2)),apply传数组(foo.apply(bar, [1, 2]))。
场景4:构造函数调用(new)→ this指向实例对象
new运算符会创建一个新对象,构造函数内的this指向这个新实例(模拟new的代码能更直观理解)。
代码解析:模拟new的this指向
javascript
function CreateObj() {
// 手动模拟new的核心逻辑
// 1. 创建空对象
var temObj = {}
// 2. 绑定this为temObj
CreateObj.call(temObj)
// 3. 关联原型链
temObj.__proto__ = CreateObj.prototype
// 4. 返回实例
return temObj
// 原生new中,构造函数内的this指向新实例
console.log(this); // new调用时,this→myObj
this.name = '极客时间'
}
var myObj = new CreateObj();
console.log(myObj.name); // 输出:极客时间
原生new的this规则:
new Constructor()会创建新对象,构造函数内的this指向该对象;- 构造函数无需手动返回对象,
new会自动返回这个绑定了this的实例。
场景5:事件处理函数 → this指向绑定元素
DOM事件处理函数中,this指向触发事件的元素(即事件绑定的DOM节点)。
代码解析:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<a href="#" id="link">点击我</a>
<script>
document.getElementById('link')
.addEventListener('click',function() {
console.log(this);
})
</script>
</body>
</html>
核心逻辑:
- 事件处理函数由DOM元素触发调用,因此
this指向该元素; - 若事件处理函数是箭头函数,
this会继承外层作用域的this(箭头函数无自身this)。
三、从执行上下文视角理解this
JS执行代码时会创建「执行上下文」,每个执行上下文包含:
- 变量环境:存储变量、函数声明;
- 词法环境:存储let/const声明;
this绑定:存储this的指向。
执行上下文的this绑定规则:
-
全局执行上下文:
this指向全局对象(window); -
函数执行上下文:
this的绑定在函数调用时确定,而非创建时;- 普通调用:this→window(非严格模式);
- 方法调用:this→调用对象;
- call/apply调用:this→第一个参数;
- new调用:this→新实例。
关键对比:
| 概念 | 确定阶段 | 决定因素 |
|---|---|---|
| 词法作用域 | 编译阶段 | 函数声明的位置 |
| this绑定 | 执行阶段 | 函数的调用方式 |
四、this的常见陷阱与避坑技巧
陷阱1:方法赋值后this指向全局
javascript
var obj = {
name: '极客时间',
fn: function() {
console.log(this.name);
}
}
var fn = obj.fn;
fn(); // 输出:undefined(this→window,window.name为空)
避坑 :用bind永久绑定this → var fn = obj.fn.bind(obj)。
陷阱2:嵌套函数的this指向
javascript
var obj = {
name: '极客时间',
fn: function() {
function inner() {
console.log(this.name); // this→window
}
inner();
}
}
obj.fn(); // 输出:undefined
避坑:
- 用变量保存外部this →
var that = this; inner中用that.name; - 用箭头函数(继承外层this)→
const inner = () => { console.log(this.name) }。
陷阱3:严格模式下的this
javascript
'use strict'
function fn() {
console.log(this); // undefined
}
fn(); // 普通调用 → this≠window
避坑:严格模式下避免依赖"this指向全局"的逻辑,显式绑定this。
五、总结:this的核心记忆法则
-
核心原则:谁调用,this指向谁;
-
特殊场景:
- 普通函数调用:非严格模式→window,严格模式→undefined;
- call/apply/bind:显式绑定this,优先级最高;
- new调用:this指向新实例;
- 箭头函数:无自身this,继承外层作用域的this;
-
与词法作用域的区别:
- 自由变量:编译阶段确定,找声明位置的外层作用域;
- this:执行阶段确定,看调用方式。
理解this的关键是"放弃静态绑定的思维"------不要试图在函数声明时确定this,而是看函数被调用的那一刻 :谁是调用者,this就指向谁。掌握这一点,就能轻松应对this场景了。