this指向的四种绑定规则深度解析
一句话概括
this 不是”函数属于谁就指向谁”,而是看函数怎么被调用——四种绑定规则(默认/隐式/显式/new)决定了 this 值,优先级从低到高。箭头函数不参与这套规则,它直接从外层抓。
1
2
3
4
5
6
7
8
function show() { console.log(this); }
show(); // 默认绑定 → undefined(严格模式)
const obj = { show };
obj.show(); // 隐式绑定 → obj
show.call({ x: 1 }); // 显式绑定 → { x: 1 }
new show(); // new 绑定 → 新对象
// 箭头函数无视以上所有:() => {} 的 this 在定义时就锁死了
核心知识点
1. 默认绑定 —— 裸调函数,this 是 undefined
没有任何 . / call / new 的函数调用,this 在严格模式下就是 undefined,非严格下是全局对象。这是最容易写出来但最难发现的 bug 来源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 严格模式(ES6 模块、class 内部默认严格)
function foo() {
console.log(this); // undefined
}
foo();
// 非严格模式(普通的 <script> 中)
function bar() {
console.log(this); // window
}
bar();
// 哪怕是嵌套调用,只要是独立调用就算默认绑定
const obj = {
fn() {
function inner() { console.log(this); }
inner(); // undefined —— 不是 obj!inner 是独立调用的
}
};
obj.fn();
2. 隐式绑定 —— 谁调用,this 就是谁
obj.method() 这种形式,. 前面的那个对象就是 this。记住:只看最后一层,中间隔了几层不重要。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const person = {
name: 'Alice',
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
person.greet(); // this → person
// 链式调用:只看最后一个点前面的对象
const parent = { person, name: 'parent' };
parent.person.greet(); // this → person(不是 parent!)
// 绑定丢失:把方法值取出来再调,就不是隐式绑定了
const g = person.greet;
g(); // this → undefined,不再是 person
3. 显式绑定 —— call / apply / bind 强行指定 this
fn.call(obj, ...)、fn.apply(obj, [...]) 立即调用,fn.bind(obj) 返回一个绑定了 this 的新函数。call 和 apply 唯一区别是传参格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function introduce(greeting, symbol) {
console.log(`${greeting}, I'm ${this.name}${symbol}`);
}
const user = { name: 'Bob' };
introduce.call(user, 'Hi', '!'); // "Hi, I'm Bob!"
introduce.apply(user, ['Hello', '~']); // "Hello, I'm Bob~"
const bound = introduce.bind(user, 'Hey');
bound('?'); // "Hey, I'm Bob?"
// bind 返回的函数如果被 new 调用,bind 的 this 会被忽略
function Person(name) { this.name = name; }
const BoundPerson = Person.bind({ x: 999 }, 'defaultName');
const p = new BoundPerson(); // this → 新对象,不是 { x: 999 }
console.log(p.name); // 'defaultName'
4. new 绑定 —— 最高优先级,创造新对象
new 做了四件事:创建空对象 → 设原型链 → 把 this 绑到新对象上 → 返回新对象。优先级碾压所有其他规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Cat(name) {
this.name = name;
}
const kitty = new Cat('喵喵');
// 等价于引擎做了:
// const obj = Object.create(Cat.prototype);
// Cat.call(obj, '喵喵');
// 返回 obj
// new 绑定 > 显式绑定(bind 也不行)
function Demo() { console.log(this); }
const BoundDemo = Demo.bind({ a: 1 });
new BoundDemo(); // this → 新对象,不是 { a: 1 }
5. 箭头函数 —— 没有自己的 this,从外层抓
箭头函数不参与四种绑定规则。它的 this 在定义时就确定了,等于外层作用域的 this。call/apply/bind 对它无效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const outer = {
name: 'outer',
// 普通方法:this = 调用者
normal() { return () => console.log(this.name); },
// 箭头方法:this = 定义时外层的 this(这里是 window/undefined)
arrow: () => console.log(this),
};
const fn1 = outer.normal();
fn1(); // "outer" —— 箭头函数继承了 normal 的 this(outer)
fn1.call({}); // "outer" —— call 对箭头函数无效
outer.arrow(); // undefined —— 箭头函数定义时的外层是模块作用域,this = undefined
// 经典坑:对象字面量里写箭头函数
const counter = {
count: 0,
add: () => { this.count++; } // ❌ this 不是 counter!
};
counter.add();
console.log(counter.count); // 0 —— 根本没加到 counter 上
其实你每天都在用
写完上面的规则,你可能觉得这是”专门背来面试的”。其实你每天都在靠这套规则干活:
React 事件处理中的
this.handleClick.bind(this):你把方法传给onClick时,隐式绑定丢了,bind把它找回来。后来有了箭头函数的 class field 写法handleClick = () => {},本质上就是用箭头函数从构造函数里直接抓this,省掉 bind。Vue 的
methods里用普通函数而不是箭头函数:methods: { greet() { console.log(this.name) } }如果写成箭头函数,this就不是组件实例了。Vue 内部帮你把 methods 挂到组件代理上,靠的就是隐式绑定——用普通函数,调用this.greet()时this自然就是组件。Array.prototype.slice.call(arguments):这是 ES6 之前把arguments转成数组的标准写法。arguments是类数组对象,没有slice方法,但你可以用call把Array.prototype.slice的this绑到arguments上。工具函数里的上下文传参:
document.querySelector('.btn').forEach.call(nodeList, btn => ...)同理。有了Array.from()和展开运算符之后这种写法少了,但老代码里到处都是。setTimeout回调里的this丢了:setTimeout(obj.method, 1000)看起来像隐式绑定,但定时器是独立调用这个回调的,this变成undefined。解法就是.bind(obj)或者用箭头函数包一层() => obj.method()。
常见误解 FAQ
❌ 误区一:”函数定义在对象里,this 就指向那个对象”
这是最常见的错觉。this 跟函数写在哪没关系,只看怎么被调。上面 const g = person.greet; g() 的例子就是反例——定义在 person 里,调的时候 this 却是 undefined。
❌ 误区二:”箭头函数的 this 指向’定义时所在的对象’“
这个表述很危险。箭头函数的 this 指向外层函数作用域的 this,不是”所在对象”。在对象字面量里直接写箭头函数,外层没有函数包裹,this 就是模块/全局的 this:
1
2
3
4
5
const obj = {
name: 'obj',
fn: () => console.log(this.name) // this 不是 obj!
};
obj.fn(); // undefined
❌ 误区三:”隐式绑定 > 显式绑定,因为 obj.method.call(other) 也是 obj 的方法”
反过来。obj.method.call(other) 里 call 是调用方式的一部分,它会把 this 强行改成 other。优先级是显式 > 隐式。
❌ 误区四:”bind 返回的函数不能再被 new,因为是绑定了 this 的函数”
能 new。bind 返回的函数如果作为构造函数用 new 调用,bind 指定的 this 会被忽略,新对象接管。这是 bind polyfill 实现里专门要处理的一个分支(this instanceof fn 判断)。
一句话总结
this的值不看定义看调用。四种规则优先级从低到高:裸调→默认绑定(undefined),点调→隐式绑定(点前的对象),call/apply/bind→显式绑定(第一个参数),new→new绑定(新对象)。箭头函数不参与,直接从外层抓。面试记住三个词:调用方式 + 优先级 + 箭头函数例外。