【每日一面】this 指向与绑定规则

2026-09-10 21 min 7496 字 -- 次阅读
摘要

this 在函数被调用的那一刻才定下来,看调用方式就行:new > 显式 bind > 对象方法 > 默认绑定,而箭头函数不绑定 this,只一路向上继承词法作用域的 this。

基础问答

Q1:JavaScript 中的 this 是什么?它和静态作用域是什么关系?

this 是 JavaScript 函数执行时自动创建的一个内部标识符,指向调用该函数的上下文对象。this 的值在函数定义时无法确定,而是在函数调用时动态绑定的

this 和静态作用域(词法作用域)是两套不同的机制:

  • 静态作用域:变量的查找范围在函数定义时由代码嵌套位置决定,与调用无关

  • this 绑定:this 的值在函数调用时根据调用方式决定,与定义位置无关

    javascript
    const 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)

扩展延伸

四种绑定规则

  1. 默认绑定

    函数独立调用时,this 指向全局对象。在浏览器中为 window,Node.js 中为 global

    javascript
    function greet() {
      console.log(this); // window(非严格模式)
    }
    
    greet();
    
    // 严格模式下
    function strictGreet() {
      'use strict';
      console.log(this); // undefined
    }
    strictGreet();

    注意:函数内部的嵌套函数也存在默认绑定,这是常见的易忽略的点。

    javascript
    const 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
  2. 隐式绑定

    当函数被作为对象的方法调用时,this 指向该对象。

    javascript
    function sayName() {
      console.log(this.name);
    }
    
    const person1 = { name: 'Alice', sayName };
    const person2 = { name: 'Bob', sayName };
    
    person1.sayName(); // "Alice"
    person2.sayName(); // "Bob"

    隐式丢失:这是最常见的 this 指向陷阱

    javascript
    const 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)
  3. 显式绑定

    通过 callapplybind 手动指定 this 的值。

    javascript
    function 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 - 绑定,顾名思义。

    javascript
    const 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)
  4. new 绑定

    当函数通过 new 关键字调用时,this 指向新创建的实例对象。

    javascript
    function Person(name) {
      // new 操作符自动创建一个新对象
      // this 指向这个新对象
      this.name = name;
      this.age = 30;
      // 如果没有显式返回对象,则返回 this
    }
    
    const alice = new Person('Alice');
    console.log(alice.name); // "Alice"

    new 绑定的优先级是最高的

    javascript
    function greet() {
      console.log(this.name);
    }
    
    const obj = { name: 'obj' };
    const bound = greet.bind(obj);
    
    const instance = new bound(); // undefined(new 绑定覆盖了 bind 的 this)

优先级

plaintext
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 一旦确定就无法被改变。

javascript
// 箭头函数解决嵌套函数 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 不可被重写

javascript
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

经典代码问题

  1. 隐式丢失

    javascript
    var 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"

  1. 多层嵌套 + 箭头函数

    javascript
    var 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"

  1. bind + new 混合

    javascript
    function 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:说说下面代码的输出

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

obj.greet();

输出 undefined(严格模式)或 ""(非严格模式,取决于全局对象的 name 属性)。

因为 obj.greet 是箭头函数,它不绑定 this,而是继承定义时外层作用域的 thisobj 字面量所在作用域是全局作用域,所以 this 指向全局对象。

追问 2:如何让下面的代码输出正确的 name?

javascript
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

三种解决方式:

  1. 箭头函数:this.friends.forEach(friend => ...)

  2. 缓存 this:const self = this; 然后用 self.name

  3. 显式绑定:this.friends.forEach(function(friend) {...}.bind(this))

  4. forEach 的第二个参数:this.friends.forEach(function(friend) {...}, this)

追问 3:说说 class 中 this 的问题

javascript
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() 作为独立调用时 thisundefined。解决方案:在 constructor 中 bind:this.click = this.click.bind(this),或使用箭头函数类属性 click = () => {...}

追问 4:手写一个 call 函数

核心思路:将函数作为目标对象的临时属性调用,然后删除该属性。

typescript
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 上,那为什么下面的代码可以访问到?

javascript
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 指向 windowwindow.name 是一个特殊属性(与窗口名称相关,默认为空字符串),与 const 声明的 name 无关。这也是为什么避免用 name 作为变量名的原因之一,window.name 是浏览器保留属性。

下一篇预告:深浅拷贝——赋值、浅拷贝、深拷贝有什么区别?如何实现一个完整的深拷贝函数?

评论