【每日一面】this 指向与绑定规则
this 在函数被调用的那一刻才定下来,看调用方式就行:new > 显式 bind > 对象方法 > 默认绑定,而箭头函数不绑定 this,只一路向上继承词法作用域的 this。
基础问答
Q1:JavaScript 中的 this 是什么?它和静态作用域是什么关系?
this 是 JavaScript 函数执行时自动创建的一个内部标识符,指向调用该函数的上下文对象。this 的值在函数定义时无法确定,而是在函数调用时动态绑定的。
this 和静态作用域(词法作用域)是两套不同的机制:
-
静态作用域:变量的查找范围在函数定义时由代码嵌套位置决定,与调用无关
-
this 绑定:this 的值在函数调用时根据调用方式决定,与定义位置无关
javascriptconst obj = { name: 'Alice', sayName() { console.log(this.name); // this 指向调用者,不是定义者 } }; const fn = obj.sayName; fn(); // undefined(非严格模式指向 window,window.name 不存在) obj.sayName(); // "Alice"
Q2:this 常见调用场景有哪些?
| 调用方式 | 绑定规则 | this 指向 |
|---|---|---|
| 普通函数调用 | 默认绑定 | 全局对象(window / globalThis,严格模式为 undefined) |
| 对象方法调用 | 隐式绑定 | 调用该方法的对象 |
| call / apply / bind | 显式绑定 | 传入的第一个参数 |
| new 调用 | new 绑定 | 新创建的对象实例 |
| 箭头函数 | 词法绑定 | 继承外层作用域的 this |
| DOM 事件 | 事件绑定 | 触发事件的 DOM 元素 |
| 定时器回调 | 默认绑定 | 全局对象(严格模式 undefined) |
扩展延伸
四种绑定规则
-
默认绑定
函数独立调用时,this 指向全局对象。在浏览器中为
window,Node.js 中为global。javascriptfunction greet() { console.log(this); // window(非严格模式) } greet(); // 严格模式下 function strictGreet() { 'use strict'; console.log(this); // undefined } strictGreet();注意:函数内部的嵌套函数也存在默认绑定,这是常见的易忽略的点。
javascriptconst obj = { name: 'Alice', friends: ['Bob', 'Charlie'], showFriends() { this.friends.forEach(function(friend) { // 此处的 this 不指向 obj,指向 window console.log(`${this.name} knows ${friend}`); }); } }; obj.showFriends(); // undefined knows Bob // undefined knows Charlie -
隐式绑定
当函数被作为对象的方法调用时,
this指向该对象。javascriptfunction sayName() { console.log(this.name); } const person1 = { name: 'Alice', sayName }; const person2 = { name: 'Bob', sayName }; person1.sayName(); // "Alice" person2.sayName(); // "Bob"隐式丢失:这是最常见的 this 指向陷阱
javascriptconst obj = { name: 'Alice', greet() { console.log(`Hello, ${this.name}`); } }; // 情况 1:赋值给变量 const fn = obj.greet; fn(); // Hello, undefined(隐式丢失,变回默认绑定) // 情况 2:作为参数传递 setTimeout(obj.greet, 100); // Hello, undefined(引用丢失) // 情况 3:回调函数中的 this const button = { name: 'submit', events: [], addEvent(callback) { this.events.push(callback); }, fire() { this.events.forEach(fn => fn()); } }; button.addEvent(obj.greet); button.fire(); // Hello, undefined(greet 执行时 this 指向 button.events) -
显式绑定
通过
call、apply、bind手动指定 this 的值。javascriptfunction introduce(age, city) { console.log(`${this.name} is ${age} years old, from ${city}`); } const person = { name: 'Alice' }; // call:参数逐个传递 introduce.call(person, 25, 'Beijing'); // apply:参数以数组形式传递 introduce.apply(person, [25, 'Beijing']); // bind:返回一个新函数,this 永久绑定 const boundIntroduce = introduce.bind(person, 25, 'Beijing'); boundIntroduce();bind 的优先级:bind 会锁定 this,即使再通过 call/apply 也无法改变(除非用 new 调用)
bind - 绑定,顾名思义。
javascriptconst obj1 = { name: 'obj1' }; const obj2 = { name: 'obj2' }; function greet() { console.log(this.name); } const bound = greet.bind(obj1); bound.call(obj2); // "obj1"(bind 的绑定优先级高于 call/apply) -
new 绑定
当函数通过
new关键字调用时,this 指向新创建的实例对象。javascriptfunction Person(name) { // new 操作符自动创建一个新对象 // this 指向这个新对象 this.name = name; this.age = 30; // 如果没有显式返回对象,则返回 this } const alice = new Person('Alice'); console.log(alice.name); // "Alice"new 绑定的优先级是最高的
javascriptfunction greet() { console.log(this.name); } const obj = { name: 'obj' }; const bound = greet.bind(obj); const instance = new bound(); // undefined(new 绑定覆盖了 bind 的 this)
优先级
new 绑定 > 显式绑定(bind) > 隐式绑定 > 默认绑定flowchart TD
A[函数被调用] --> B{调用方式?}
B -->|new 关键字| C[new 绑定]
B -->|call/apply/bind| D[显式绑定]
B -->|对象方法调用| E[隐式绑定]
B -->|普通函数调用| F[默认绑定]
C --> G[① 最高优先级<br/>指向新创建实例]
D --> H[② 第二优先级<br/>指向传入的第一个参数]
E --> I[③ 第三优先级<br/>指向调用该方法的对象]
F --> J[④ 最低优先级<br/>指向全局对象/undefined]
style C fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
style D fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
style E fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
style F fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
style G fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
style H fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
style I fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
style J fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px
特殊的箭头函数
箭头函数不绑定 this,它直接继承外层作用域(词法作用域)中的 this 值,并且 this 一旦确定就无法被改变。
// 箭头函数解决嵌套函数 this 问题
const obj = {
name: 'Alice',
friends: ['Bob', 'Charlie'],
showFriends() {
this.friends.forEach(friend => {
console.log(`${this.name} knows ${friend}`); // this 指向 obj
});
}
};
obj.showFriends();
// Alice knows Bob
// Alice knows Charlie箭头函数的 this 不可被重写
const arrow = () => {
console.log(this);
};
arrow.call({ name: 'obj' }); // window(箭头函数不响应显式绑定)
arrow.bind({ name: 'obj' })(); // window(bind 无效)
const obj = {
arrow: () => {
console.log(this); // 此处的 this 继承自 obj 所在的上下文
}
};
obj.arrow(); // window(不是 obj!)常见场景
flowchart LR
subgraph 场景1:对象方法
A1["obj.method"] --> B1["this = obj"]
end
subgraph 场景2:回调函数
A2["arr.forEach(cb)"] --> B2["this = window<br/>回调函数是独立调用"]
end
subgraph 场景3:事件监听
A3["el.addEventListener"] --> B3["this = el"]
end
subgraph 场景4:箭头函数
A4["() => {}"] --> B4["this = 外层作用域"]
end
subgraph 场景5:定时器
A5["setTimeout"] --> B5["this = window<br/>严格模式 undefined"]
end
subgraph 场景6:类方法
A6["class.method"] --> B6["this = 实例<br/>注意类中方法默认是严格模式"]
end
判断 this 指向的方法
flowchart TD
A[判断函数类型] --> B{是箭头函数?}
B -->|是| C[找到外层最近的非箭头函数的 this]
B -->|否| D{调用方式?}
D -->|new 调用| E[this = 新创建的实例]
D -->|call/apply/bind| F[this = 传入的第一个参数]
D -->|"对象.method()"| G[this = 点号前的对象]
D -->|独立调用| H{严格模式?}
H -->|是| I[this = undefined]
H -->|否| J[this = 全局对象]
C --> K[✅ 确定 this]
E --> K
F --> K
G --> K
I --> K
J --> K
style K fill:#e8f5e9,stroke:#1a7a3a,stroke-width:2px
经典代码问题
-
隐式丢失
javascriptvar name = 'window'; const obj = { name: 'obj', greet() { console.log(this.name); } }; obj.greet(); // ① 输出什么? const fn = obj.greet; fn(); // ② 输出什么?
-
①
obj.greet()— 隐式绑定,this = obj,输出"obj" -
②
fn()— 赋值给变量后独立调用,默认绑定,输出"window"
-
多层嵌套 + 箭头函数
javascriptvar name = 'window'; const obj = { name: 'obj', outer() { function inner() { console.log('inner:', this.name); // ① } const arrowInner = () => { console.log('arrow:', this.name); // ② }; inner(); arrowInner(); } }; obj.outer();
-
①
inner()— 独立调用,默认绑定,this = window,输出"window" -
②
arrowInner()— 箭头函数,继承外层outer的 this(outer的 this 指向 obj),输出"obj"
-
bind + new 混合
javascriptfunction User(name) { this.name = name; console.log(this); } const BoundUser = User.bind({ name: 'fake' }); new BoundUser('Alice'); // 输出什么?
-
new优先级高于bind -
即使
bind绑定了{ name: 'fake' },new调用时会创建一个新对象 -
this指向新创建的实例,输出User { name: "Alice" }
面试追问
追问 1:说说下面代码的输出
const obj = {
name: 'obj',
greet: () => {
console.log(this.name);
}
};
obj.greet();输出 undefined(严格模式)或 ""(非严格模式,取决于全局对象的 name 属性)。
因为 obj.greet 是箭头函数,它不绑定 this,而是继承定义时外层作用域的 this。obj 字面量所在作用域是全局作用域,所以 this 指向全局对象。
追问 2:如何让下面的代码输出正确的 name?
const obj = {
name: 'obj',
friends: ['a', 'b', 'c'],
show() {
this.friends.forEach(function(friend) {
console.log(`${this.name} knows ${friend}`);
});
}
};
obj.show(); // 输出 undefined knows a / b / c三种解决方式:
-
箭头函数:
this.friends.forEach(friend => ...) -
缓存 this:
const self = this;然后用self.name -
显式绑定:
this.friends.forEach(function(friend) {...}.bind(this)) -
forEach 的第二个参数:
this.friends.forEach(function(friend) {...}, this)
追问 3:说说 class 中 this 的问题
class Button {
constructor(text) {
this.text = text;
}
click() {
console.log(this.text);
}
}
const btn = new Button('Submit');
const clickHandler = btn.click;
clickHandler(); // 输出什么?输出 undefined(或报错,取决于严格模式)。
class 中的方法默认处于严格模式,clickHandler() 作为独立调用时 this 为 undefined。解决方案:在 constructor 中 bind:this.click = this.click.bind(this),或使用箭头函数类属性 click = () => {...}。
追问 4:手写一个 call 函数
核心思路:将函数作为目标对象的临时属性调用,然后删除该属性。
Function.prototype.myCall = function(context, ...args) {
context = context ?? globalThis; // null/undefined 时指向全局对象
const key = Symbol('temp'); // 唯一 key,避免属性冲突
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};追问 5:const 声明的变量不会挂到 window 上,那为什么下面的代码可以访问到?
const name = 'const';
const obj = {
name: 'obj',
greet() {
console.log(this.name);
}
};
obj.greet(); // "obj"
const fn = obj.greet;
fn(); // 非严格模式下输出 "" 而不是 "const",为什么?因为 const name = 'const' 声明的是块级作用域变量,不在 window 上。fn() 默认绑定时 this 指向 window,window.name 是一个特殊属性(与窗口名称相关,默认为空字符串),与 const 声明的 name 无关。这也是为什么避免用 name 作为变量名的原因之一,window.name 是浏览器保留属性。
下一篇预告:深浅拷贝——赋值、浅拷贝、深拷贝有什么区别?如何实现一个完整的深拷贝函数?