this 绑定与隐式丢失
发表于:2026-07-29
字数统计:4500 字
预计阅读16分钟
涵盖 this 四种绑定规则、隐式丢失的本质、bind/箭头函数/保存 self 三种解决方案。
一、this 绑定四种规则
JavaScript
var name = '全局';
var obj = {
name: '对象',
sayName: function() {
console.log(this.name)
}
};
// 隐式绑定(对象方法调用)
obj.sayName(); // '对象'
// 隐式丢失(独立调用)
var bar = obj.sayName
bar(); // '全局'this 绑定规则:
| 调用方式 | this |
|---|---|
obj.foo()(对象前缀) | obj |
bar()(无前缀) | 全局(非严格模式)/ undefined(严格模式) |
new Foo() | 新创建的实例 |
foo.call(obj) / apply / bind | obj(显式指定) |
二、隐式丢失的本质
JavaScript
var a = 1
function foo() {
var a = 3
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
// 关键步骤分析
var bar = obj.foo // bar 只是 foo 函数的另一个引用
bar() // 独立调用,this 指向全局bar 不是一个对象,它只是一个变量,存储的是函数的引用。调用 bar() 时,实际上是独立函数调用,而不是作为对象的方法调用。
等价于
JavaScript
// 1. obj.foo 获取到 foo 函数的引用
// 2. 把这个引用赋值给 bar
// 3. bar() 相当于直接调用 foo()
// 4. 独立函数调用,this 指向全局内存中的关系
JavaScript
function foo() { ... } // 函数对象存储在内存中
// 多个引用指向同一个函数
obj.foo = foo // obj 的 foo 属性指向函数
var bar = foo // bar 变量也指向同一个函数
// 调用方式决定 this
obj.foo() // 隐式绑定,this = obj
bar() // 独立调用,this = 全局
foo() // 独立调用,this = 全局三、详细的执行过程
JavaScript
var a = 1
function foo() {
var a = 3
console.log(this.a)
}
var obj = {
a: 2,
foo: foo // obj.foo 引用 foo 函数
}
// 步骤1:获取 obj.foo 的值(这是一个函数引用)
var bar = obj.foo // bar 现在持有这个函数引用
// 步骤2:调用 bar()
bar() // 相当于直接调用函数,没有任何对象前缀
// 这是独立函数调用,this 指向全局对象
// 所以 this.a = window.a = 1四、为什么 bar 不是新对象?
JavaScript
var bar = obj.foo
console.log(typeof bar) // "function",不是 "object"
// 关键:调用时没有对象前缀
bar(); // 没有使用 . 或 [] 调用,所以是独立调用五、各种调用方式对比
JavaScript
var a = 1
var obj = {
a: 2,
foo: function() {
console.log(this.a)
}
};
// 情况1:隐式绑定(对象方法调用)
obj.foo() // 2 - 有对象前缀,this = obj
// 情况2:独立调用(函数引用)
var bar = obj.foo
bar() // 1 - 没有对象前缀,this = 全局
// 情况3:独立调用(直接调用)
var func = obj.foo
func() // 1
// 情况4:立即调用(仍然是独立调用)
(obj.foo)() // 2 - 括号不影响,仍然是 obj.foo()
(obj.foo = obj.foo)() // 1 - 赋值表达式返回函数本身,独立调用六、更深入的例子
JavaScript
var name = '全局'
var obj = {
name: '对象',
sayName: function() {
console.log(this.name)
}
}
// 示例1:作为参数传递
function run(fn) {
fn() // 独立调用,this = 全局
}
run(obj.sayName) // 输出: 全局
// 示例2:作为返回值
function getFn() {
return obj.sayName
}
var fn = getFn()
fn() // 输出: 全局
// 示例3:数组中的函数
var arr = [obj.sayName]
arr[0]() // 输出: 全局 - 通过数组索引调用也是独立调用
// 示例4:setTimeout 中的函数
setTimeout(obj.sayName, 100) // 输出: 全局 - 回调是独立调用七、参数传递的本质
JavaScript
// 在 JavaScript 中,参数传递是值传递(对函数来说传递的是引用值)
var obj = { value: 1 }
function test(param) {
// param 接收的是 obj 引用的副本
// 但函数引用也是值
param()
}
// 等价于:
var temp = obj.foo // 获取函数引用
test(temp) // 传递引用副本
// 在 test 内部
function test(param) {
// param 指向同一个函数
// 调用时没有对象上下文
param()
}八、避免隐式丢失的三种方法
方法1:使用 bind 绑定
JavaScript
var obj = {
a: 2,
foo: function() {
console.log(this.a)
}
}
var bar = obj.foo.bind(obj)
bar() // 2方法2:使用箭头函数
JavaScript
var obj2 = {
a: 2,
foo: function() {
setTimeout(() => {
console.log(this.a) // 箭头函数捕获外层 this
}, 100)
}
}
obj2.foo() // 2方法3:保存 this 引用
JavaScript
var obj3 = {
a: 2,
foo: function() {
var self = this
setTimeout(function() {
console.log(self.a) // 使用保存的引用
}, 100)
}
}
obj3.foo() // 2九、判断 this 的规则
- 有对象前缀(
obj.foo())→ this = 该对象 - 无对象前缀(
bar())→ this = 全局对象(非严格模式)或 undefined(严格模式)
这就是为什么 bar() 输出全局的 1,而不是对象的 2。
十一、变量提升 vs 函数声明提升
核心规则
- 函数声明优先于变量声明提升:函数声明会被提升到最顶端,变量声明次之。
var只提升声明,不提升赋值:提升后初始值为undefined。- 同名函数声明,后面的会覆盖前面的。
经典案例分析
JavaScript
function foo() {
console.log('foo1')
}
foo() // foo2
var foo = function() {
console.log('foo2')
}
foo() // foo2
function foo() {
console.log('foo3')
}
foo() // foo3(最终 foo 函数体)预编译阶段:
- 函数声明
function foo()提升(两个都会被提升,后面的覆盖前面的),最终foo函数体为console.log('foo3')。 var foo提升(但函数声明优先),但因为foo已经被赋值为函数,重复声明会被忽略。var foo = function() { console.log('foo2') }这行赋值在执行阶段才发生。
执行阶段:
- 第 1 行:
foo()输出'foo2'(覆盖前最终函数体)。 - 第 7 行:执行
var foo = function() {...},把foo重新赋值为函数表达式。 - 第 12 行:
foo()输出'foo3'(?? 因为函数声明function foo()已经被上面的赋值覆盖了)
这道题的争议点:实际中 V8 引擎对函数提升和赋值提升的处理有优化行为,不同环境可能略有差异。核心考点是:函数声明 + 同名 var 会被覆盖。
关键纠错点
- 不要把
var foo = function() {}和function foo() {}等同——前者是"声明+赋值",后者只是"声明"。 - 多个同名
function声明,提升时是后面的覆盖前面的。 var声明的变量如果和函数同名,函数优先。
十二、作用域 + this + 运算符优先级 + new 绑定规则
案例
JavaScript
var name = 'window'
function Foo(name) {
this.name = name
}
Foo.prototype.getName = function() {
return this.name
}
function getName() {
console.log(this.name)
}
getName() // ?
Foo.prototype.getName() // ?
var getName = Foo.prototype.getName
getName() // ?
new Foo('foo').getName() // ?答案
getName()→'window'(全局调用,this是 window,name是全局var name)Foo.prototype.getName()→undefined(this 是原型对象,原型没有name)var getName = Foo.prototype.getName; getName()→'window'(赋值后再调用,this 是 window)new Foo('foo').getName()→'foo'(new 出来的实例name = 'foo',this 指向实例)
核心考点
- 变量污染:
var name = 'window'声明在全局,全局getName调用时会读取它。 - this 指向:决定
this.name读哪个name。 - 运算符优先级:
new Foo('foo').getName()是new Foo('foo')先执行,然后.getName()访问实例方法。 - 原型链查找:
Foo.prototype.getName实际上是通过原型链给实例添加方法。
一句话总结
this 指向取决于调用方式(对象方法 / 全局 / call / apply / bind / new),而变量提升和函数声明提升决定函数 / 变量在哪个作用域里能被访问到。
十三、关联文档
- 01-闭包与作用域
- 04-函数大全(call/apply/bind)
- 05-异步与事件循环(setTimeout 中的 this 丢失)