JS进阶-this指向

通常,对象方法需要访问对象中存储的信息才能完成其工作。例如,user.sayHi() 中的代码可能需要用到 user 的 name 属性。为了访问该对象,方法中可以使用 this 关键字。this 的值就是在点之前的这个对象,即调用该方法的对象。

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" 指的是“当前的对象”
    alert(this.name);
  }

};

user.sayHi(); // John

四种绑定规则

JavaScript 中的 this 可以用于任何函数,即使它不是对象的方法。判断普通函数中的this指向,可以通过这4个问题:

  1. 函数是否是通过 new 调用的?

    场景: new Func()
    规则: this指向新创建的实例对象。这是优先级最高的规则。

    function Person(name) {
        this.name = name; 
    }
    const p = new Person('Bob');
    console.log(p.name); // Bob
    
  2. 函数是否通过 call / apply / bind 显式指定了上下文?

    场景: callapplybind
    规则: 传入什么对象,this就是什么对象(若传null/undefined则转为默认绑定)。

    function greet() {
        console.log(this.name);
    }
    const person = { name: 'Alice' };
    
    greet.call(person); // Alice
    greet.apply(person); // Alice
    const boundGreet = greet.bind(person);
    boundGreet(); // Alice
    
  3. 函数是否是通过对象打点调用的 (e.g. obj.fn() )?

    场景: obj.func()
    规则: 指向调用时最后一级的那个对象。

    const obj = {
        name: 'obj',
        inner: {
            name: 'inner',
            fn: function() { console.log(this.name); }
        }
    };
    obj.inner.fn(); // 输出 'inner' —— 最近一级调用者是 inner
    

    ⚠️ 进阶陷阱:隐式丢失

    const obj = {
        name: 'obj',
        fn: function(){ console.log(this); }
    };
    
    const reference = obj.fn; // 仅仅是函数引用的传递
    reference(); // 输出 window —— 回归到规则1:默认绑定
    
  4. 是否独立调用?

    场景:func()
    规则: 非严格模式下指向window(浏览器)或global(Node);严格模式下指向undefined

    function show(){
        console.log(this);
    }
    show(); // window 或 undefined (严格模式)
    

上述规则针对普通函数,箭头函数不遵守上述四种规则,它没有自己的this

箭头函数(没有自己的this)

箭头函数的this定义时就确定了,永远指向外层作用域this(静态作用域/词法作用域)。

const obj = {
    name: 'obj',
    // 普通函数:this 取决于调用者
    normalFn: function() {
        console.log('Normal:', this.name);
    },
    // 箭头函数:this 取决于定义时的外层(此处外层是全局/模块作用域)
    arrowFn: () => {
        console.log('Arrow:', this.name);
    }
};

obj.normalFn(); // Normal: obj
obj.arrowFn();  // Arrow: undefined (因为定义时外层this是window/module)


经典用法:解决 setTimeout 回调丢失 this 的问题

const timer = {
    count: 0,
    start: function() {
        // 箭头函数让 this 锁定为 start 函数中的 this(即 timer 对象)
        setTimeout(() => {
            console.log(this.count++); // 正确指向 timer
        }, 1000);
    }
};

实践应用

  • 数组方法中的thisArg

    forEachmapfilter 等方法的第二个参数可以显式绑定this

    const obj = { multiplier: 10 };
    
    // ❌ 箭头函数:thisArg 被忽略
    [1, 2, 3].map((item) => {
        return item * this.multiplier;  // this 指向外层作用域,不是 obj
    }, obj); // 报错或 NaN
    
    // ✅ 普通函数:thisArg 生效
    [1, 2, 3].map(function(item) {
        return item * this.multiplier;
    }, obj); // [10, 20, 30]
    
  • 事件处理函数

    在原生 DOM 规范中,事件处理函数内部的 this强制绑定为当前触发事件的元素(即 event.currentTarget)。

    浏览器在调用事件回调时,本质上执行了类似这样的操作(伪代码):

    // 浏览器事件中心内部逻辑
    element.addEventListener('click', handler);
    
    // 触发时,浏览器底层做了一次 call 调用:
    handler.call(element, event); 
    // 所以 this === element
    

    可以通过代码验证:

    const btn = document.createElement('button');
    btn.textContent = 'Click';
    
    btn.addEventListener('click', function(e) {
        console.log(this === btn);             // true
        console.log(this === e.currentTarget); // true
        console.log(this === e.target);        // 不一定,取决于点击的具体子元素
    });
    

    如果监听器使用箭头函数,浏览器的 call(element) 将无效,this 回归外层词法作用域。

    btn.addEventListener('click', (e) => {
        console.log(this); // 指向定义时外层的作用域(可能是 window 或 类实例)
        // 此时想获取元素必须用 e.currentTarget
    });
    
  • 优先级冲突测试

    问题:newbind 谁更优先?

    function foo() {
        console.log(this.a);
    }
    const obj = { a: 2 };
    const Bar = foo.bind(obj); // 强行把 foo 的 this 绑定为 obj
    const b = new Bar();       // 使用 new 调用
    // 输出 undefined,因为 new 创建的新对象 { a: undefined } 覆盖了 bind 的 obj
    // 结论:new > 显式绑定 > 隐式绑定 > 默认绑定
    

为什么提倡使用箭头函数

提倡使用箭头函数,本质上是为了锁定 this 的指向,彻底终结传统函数中 this 动态变化带来的不确定性。

简单来说:普通函数关注"谁调用了它",箭头函数关注"谁定义了它"。

当把一个普通函数作为回调传递时,只传递了函数体,并没有传递调用者。到了回调真正执行的时候,调用者已经变成了全局环境(window/undefined)或别的对象。

const obj = {
    name: 'Alice',
    greet: function() {
        console.log(`Hello, ${this.name}`);
    }
};

// 我们希望 1 秒后打招呼
setTimeout(obj.greet, 1000); 
// 输出:Hello, undefined (或报错)
// 因为 setTimeout 内部是独立调用 fn(),this 指向了 window

如果不用箭头函数,传统解法非常繁琐:

  1. setTimeout(function() { obj.greet(); }, 1000); (多一层包装,不够优雅)
  2. setTimeout(obj.greet.bind(obj), 1000); (显式绑定,心智负担重)

使用箭头函数重构:

箭头函数没有自己的 this。它在定义时就把外层的 this "快照"进了闭包。无论之后函数被如何调用、被传递给谁、被哪个第三方库调用,里面的 this 永远指向当初定义它的那个对象。

const obj = {
    name: 'Alice',
    // 使用箭头函数定义方法 (如果是对象字面量,注意外层的 this)
    // 或者更常见的场景:
    init: function() {
        // 这里的 this 是 obj
        setTimeout(() => {
            // 箭头函数没有 this,它直接从 init 作用域借用 this
            console.log(`Hello, ${this.name}`);
        }, 1000);
    }
};
obj.init(); // 1秒后正确输出 "Hello, Alice"

除了定时器外,异步请求也是箭头函数最经典的阵地。异步回调脱离了原本的调用栈,普通函数的 this 必然丢失。

class Loader {
    constructor() {
        this.data = null;
    }
    
    // ✅ 推荐:箭头函数
    fetchDataArrow() {
        fetch('/api').then(res => res.json()).then(data => {
            this.data = data; // this 指向 Loader 实例
        });
    }
    
    // ❌ 反模式:普通函数 (除非你 bind 了它)
    fetchDataNormal() {
        fetch('/api').then(function(res) {
            return res.json();
        }).then(function(data) {
            this.data = data; // TypeError: Cannot set property 'data' of undefined
        });
    }
}

⚠️唯一的"不建议"场景:不要用箭头函数定义对象方法

const person = {
    name: 'Bob',
    // ❌ 这里的箭头函数定义在了全局作用域,this 指向 window
    sayHi: () => {
        console.log(`Hi ${this.name}`); // Hi undefined
    }
};

原因:对象字面量 {} 本身不构成作用域。箭头函数在这里会捕获更外层的 this(通常是 window),导致无法访问对象属性。

只要函数离开了当前对象去执行(即:不是 obj.fn() 这种即时调用),可以使用箭头函数来保持 this 的正确性。

posted @ 2026-04-08 10:48  小风车吱呀转  阅读(5)  评论(0)    收藏  举报