流星123
流星123
  • 发布:18 分钟前
  • 更新:18 分钟前
  • 阅读:6

JavaScript `this` 指南

分类:ASK社区

JavaScript this 指南

一、核心心法

this 不是在函数定义时确定的,而是在函数被调用时确定的。
判断 this,只需要看一件事:这个函数是怎么被调用的(调用点)

唯一的例外是箭头函数——它根本没有自己的 this


二、五种绑定规则

规则 1:默认绑定(独立函数调用)

function foo() {  
  console.log(this);  
}  

foo();  // 非严格模式: globalThis (浏览器 window / Node global)  
        // 严格模式:   undefined
'use strict';  
function foo() { console.log(this); }  
foo();  // undefined

顶层 this 的值取决于环境:

环境 顶层 this
浏览器 <script> window
ES Module (.mjs / type="module") undefined
CommonJS 模块 (Node .js) module.exports(即 {})
函数内(非严格) globalThis
函数内(严格) undefined
class 内部 永远严格模式

规则 2:隐式绑定(方法调用)

看调用时最后一个点前面是谁。

const obj = {  
  name: 'obj',  
  foo() { console.log(this.name); }  
};  

obj.foo();  // 'obj'

多层嵌套只看最后一层:

const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };  
a.b.foo();  // 'b'  ← 不是 'a'

方括号访问同理:

a.b['foo']();  // 'b'

规则 3:显式绑定(call / apply / bind)

function greet(greeting, punct) {  
  console.log(greeting + ', ' + this.name + punct);  
}  
const me = { name: 'Alice' };  

greet.call(me, 'Hi', '!');       // 逐个传参  
greet.apply(me, ['Hi', '!']);    // 数组传参 (Apply → Array)  
const bound = greet.bind(me, 'Hi');  // 返回新函数 + 支持柯里化  
bound('!');

bind 的三个特性:

  1. 返回一个新函数,不立即执行
  2. 支持参数预置(柯里化)
  3. 硬绑定:绑定后无法再被 call/apply 改变
const hard = greet.bind(me);  
hard.call({ name: 'Bob' }, 'Yo', '!');  // 依然是 Alice

null / undefined 的坑:

function foo() { console.log(this); }  
foo.call(null);  // 非严格: globalThis(被替换了!)  严格: null

安全做法——DMZ 空对象:

const DMZ = Object.create(null);   // 比 {} 更干净,无原型  
foo.apply(DMZ, [1, 2]);

原始值会被装箱(仅非严格模式):

function f() { console.log(typeof this); }  
f.call(1);              // 'object'  (Number 包装对象)  
// 严格模式下: 'number'

规则 4:new 绑定

new Foo() 做了四件事:

  1. 创建一个新的空对象
  2. 将该对象的 [[Prototype]] 指向 Foo.prototype
  3. 以该对象为 this 执行 Foo
  4. 如果 Foo 没有返回一个对象,则返回这个新对象
function Person(name) {  
  this.name = name;  
  // return { name: 'hacked' };  // 若返回对象,则 new 的结果是它  
  // return 42;                  // 返回原始值 → 被忽略,仍返回 this  
}  
const p = new Person('Alice');  // p.name === 'Alice'

规则 5:箭头函数(词法绑定)

箭头函数没有自己的 this,它继承定义时所在外层作用域this,且永久不可改变

const obj = {  
  name: 'obj',  
  regular() {  
    const arrow = () => console.log(this.name);  
    arrow();  // 'obj'  ← 继承 regular 的 this  
  },  
  arrowMethod: () => console.log(this?.name)  // ⚠️ 继承的是模块/全局的 this  
};  

obj.regular();      // 'obj'  
obj.arrowMethod();  // undefined  ← 经典陷阱!

箭头函数无法被改变:

const arrow = () => console.log(this);  
arrow.call({ a: 1 });   // call 无效  
arrow.bind({ a: 1 })(); // bind 无效  
new arrow();            // TypeError: arrow is not a constructor

箭头函数也没有 argumentssupernew.target


三、优先级

new 绑定  >  显式绑定(bind/call/apply)  >  隐式绑定(obj.fn)  >  默认绑定

箭头函数不在此列——它压根不参与,直接看词法作用域。

验证 new > bind:

function Foo(a) { this.a = a; }  
const obj = {};  
const B = Foo.bind(obj);  

B(2);              // obj.a === 2      (bind 生效)  
const b = new B(3);// b.a === 3, obj.a 仍是 2   ← new 赢了

这正是 bind polyfill 里必须判断 new.target 的原因。


四、隐式丢失:最常见的 Bug 来源

1. 赋值给变量

const obj = { name: 'obj', foo() { console.log(this?.name); } };  
const fn = obj.foo;   // 只拿到了函数引用,丢掉了 obj  
fn();                 // undefined

2. 作为回调传递

setTimeout(obj.foo, 100);        // 丢失  
[1].forEach(obj.foo);            // 丢失  
btn.addEventListener('click', obj.foo);  // this 变成 btn

3. 解构

const { foo } = obj;  
foo();  // 丢失

这就是 React 中 const { setState } = this 会出问题、
以及 Vue3 用 ref 而非 this 的部分原因。

4. 间接引用

const o1 = { name: 'o1', foo() { console.log(this.name); } };  
const o2 = { name: 'o2' };  

o2.foo = o1.foo;  
o2.foo();          // 'o2'  (正常隐式绑定)  

(o2.foo = o1.foo)();  // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错

5. 解决方案汇总

// ① bind  
setTimeout(obj.foo.bind(obj), 100);  

// ② 箭头函数包裹(推荐,可读性好)  
setTimeout(() => obj.foo(), 100);  

// ③ 数组方法的 thisArg 参数  
[1, 2].forEach(obj.foo, obj);  

// ④ class field 箭头函数(见下文)

支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap

不支持的: reduce reduceRight sort(它们的第二参数另有用途)


五、Class 中的 this

类体内永远是严格模式

class Counter {  
  count = 0;  

  inc() { this.count++; }               // 原型方法 → this 会丢失  
  dec = () => { this.count--; };        // 类字段箭头函数 → this 永久绑定实例  
}  

const c = new Counter();  
const { inc, dec } = c;  
dec();   // ✅ 正常  
inc();   // ❌ TypeError: Cannot read properties of undefined

两者的本质区别:

原型方法 inc() 类字段 dec = () => {}
存放位置 Counter.prototype 每个实例上
内存 共享一份 每个实例一份(内存开销大)
this 调用时决定 定义时锁死为实例
可被 call 改变
可被子类 super 调用

推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:

class Counter {  
  constructor() {  
    this.inc = this.inc.bind(this);  // 等价效果  
  }  
}

静态方法中的 this

class A {  
  static create() { return new this(); }  // this === 类本身,支持子类继承  
}  
class B extends A {}  
B.create();  // 返回 B 的实例,而非 A

继承与 super

class Base {  
  constructor() { this.type = 'base'; }  
  hello() { console.log('base hello'); }  
}  
class Child extends Base {  
  constructor() {  
    // console.log(this);  // ❌ ReferenceError: 必须先调用 super()  
    super();               // 派生类中,super() 之前 this 处于 TDZ  
    console.log(this.type);// 'base'  
  }  
  hello() {  
    super.hello();         // super 方法调用时,this 仍是当前实例  
  }  
}

私有字段的坑

class A {  
  #secret = 1;  
  get() { return this.#secret; }  
}  
const a = new A();  
a.get.call({});  // TypeError: Cannot read private member #secret  
                 // 私有字段是"品牌检查",this 必须是真实实例

六、各类场景速查

DOM 事件

btn.addEventListener('click', function () {  
  console.log(this);  // === event.currentTarget (btn)  
});  

btn.addEventListener('click', () => {  
  console.log(this);  // 外层作用域的 this,不是 btn  
});

handleEvent 特殊接口(传对象而非函数):

const handler = {  
  count: 0,  
  handleEvent(e) { console.log(this.count++, e.type); }  // this === handler  
};  
btn.addEventListener('click', handler);

内联 onclick="..." 属性中的 this 也是元素本身。

定时器

setTimeout(function () {  
  console.log(this);  
  // 浏览器非严格: window  
  // Node:         Timeout 对象  
}, 0);

getter / setter

const obj = {  
  _v: 1,  
  get v() { return this._v; }   // this 是访问该属性的对象  
};

Proxy 中的 this 泄漏

const target = { greet() { return this; } };  
const proxy = new Proxy(target, {});  
proxy.greet() === proxy;  // true —— this 是 proxy,不是 target  
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错

标签模板 / 生成器 / async

async function f() { return this; }   // 与普通函数规则一致  
function* g() { yield this; }         // 同上

七、手写实现(面试高频)

Function.prototype.myCall = function (context, ...args) {  
  if (typeof this !== 'function') throw new TypeError('not a function');  
  context = context ?? globalThis;  
  if (Object(context) !== context) context = Object(context);  // 原始值装箱  

  const key = Symbol('fn');       // Symbol 避免属性名冲突  
  context[key] = this;  
  try {  
    return context[key](...args);  
  } finally {  
    delete context[key];  
  }  
};  

Function.prototype.myApply = function (context, args = []) {  
  return this.myCall(context, ...args);  
};  

Function.prototype.myBind = function (context, ...preArgs) {  
  const fn = this;  
  if (typeof fn !== 'function') throw new TypeError('not a function');  

  function bound(...args) {  
    // 关键:new 调用时忽略 context,使用新建的 this  
    return fn.apply(  
      new.target ? this : context,  
      [...preArgs, ...args]  
    );  
  }  
  // 维持原型链,使 instanceof 正确  
  bound.prototype = Object.create(fn.prototype || null);  
  return bound;  
};
// 手写 new  
function myNew(Ctor, ...args) {  
  const obj = Object.create(Ctor.prototype);  
  const ret = Ctor.apply(obj, args);  
  return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))  
    ? ret  
    : obj;  
}

八、综合练习(先自己作答)

var name = 'global';   // 注意:必须是 var,let/const 不挂载到 window  

const obj = {  
  name: 'obj',  
  f1() { console.log(this.name); },  
  f2: () => console.log(this?.name),  
  f3() {  
    return function () { console.log(this?.name); };  
  },  
  f4() {  
    return () => console.log(this.name);  
  }  
};  

obj.f1();          // ?  
obj.f2();          // ?  
obj.f3()();        // ?  
obj.f4()();        // ?  
const g = obj.f1;  
g();               // ?  
obj.f1.call({ name: 'x' });  // ?  
new obj.f1();      // ?

<details>
<summary>答案</summary>

obj.f1()          → 'obj'        隐式绑定  
obj.f2()          → 'global'/undefined  箭头函数继承顶层 this  
obj.f3()()        → 'global'/undefined  返回的函数独立调用,默认绑定  
obj.f4()()        → 'obj'        箭头函数继承 f4 的 this  
g()               → 'global'/undefined  隐式丢失  
obj.f1.call({...})→ 'x'          显式绑定  
new obj.f1()      → undefined    new 绑定,新对象上没有 name

(浏览器非严格模式下顶层 thiswindow,故为 'global';ESM/严格模式下为 undefined 或报错)

</details>


九、终极判断流程图

函数是箭头函数?  
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)  
└─ 否 ↓  
   用 new 调用? ──→ 是 → this = 新创建的对象  
   ↓ 否  
   用 call/apply/bind 调用? ──→ 是 → this = 指定的对象  
   ↓ 否                              (非严格下 null/undefined 会替换为 globalThis)  
   通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)  
   ↓ 否  
   严格模式? ──→ 是 → this = undefined  
              └→ 否 → this = globalThis

十、最佳实践

  1. 优先用箭头函数写回调,避免 this 丢失
  2. 对象方法用简写语法 foo() {},不要用箭头函数(除非确实想要外层 this)
  3. class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器 bind
  4. 全项目开启严格模式 / 使用 ESM,让 this 错误尽早暴露为 undefined 报错
  5. TypeScript 用户:开启 noImplicitThis,并善用显式 this 参数:
    function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ }  
  6. 不确定时,打印 console.log(this) —— 比推理快十倍

需要我把这份指南写入文件(比如 docs/js-this-guide.md),或者补充某个章节的深入内容(如 Proxy receiver、React/Vue 中的实战案例)吗?

0 关注 分享

要回复文章请先登录注册