文章

this指向的四种绑定规则深度解析

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定义时就确定了,等于外层作用域的 thiscall/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 方法,但你可以用 callArray.prototype.slicethis 绑到 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绑定(新对象)。箭头函数不参与,直接从外层抓。面试记住三个词:调用方式 + 优先级 + 箭头函数例外。

本文由作者按照 CC BY 4.0 进行授权