🛸🛸谁在调用我?深入 JavaScript中 this的指向之谜

148 阅读4分钟

前言

在JavaScript中,this关键字的行为常让开发者困惑。本文将通过示例解析this的绑定规则、箭头函数特性及call/apply/bind的使用区别。

一、this的四种调用方式

1. 普通函数调用(默认绑定)

var name = '全局名称';
function fn() {
    console.log(this.name); 
}
fn(); // 输出"全局名称"(非严格模式)
  • 此时this指向全局对象(浏览器中为windowNode.js global
  • 严格模式("use strict")下thisundefined

2. 对象方法调用(隐式绑定)

const obj = {
    name: '对象名称',
    fn: function() {
        console.log(this.name);
    }
}
obj.fn(); // 输出"对象名称"
  • this指向调用该方法的对象(即点符号前的对象,这里是obj对象)

3. 构造函数调用(new绑定)

function Person(name) {
    this.name = name;
}
const p = new Person('构造的名称');
console.log(p.name); // 输出"构造的名称"
  • new操作符会:

    1. 创建新对象
    2. this绑定到新对象
    3. 执行构造函数代码
    4. 返回新对象
  • 通过 new 关键字调用构造函数时, this 指向 新创建的实例对象

4. 事件处理函数调用

<button id="btn">点击</button>
<script>
    btn.onclick = function() {
        console.log(this); // 输出<button>元素
    }
</script>
  • 事件处理函数中,this指向触发事件的DOM元素

优先级顺序为

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

箭头函数不遵循上述规则,它的 this 在定义时确定,且无法通过 call / apply / bind 修改。

二、箭头函数的this特性

箭头函数没有自己的this,而是继承外层作用域的this值:

const obj = {
    fn: function() {
        setTimeout(() => {
            console.log(this); // 继承fn的this(指向obj)
        }, 100);
    }
};
obj.fn();

关键区别

特性普通函数箭头函数
this绑定动态绑定静态继承
可用call/bind
构造函数不可

三、call/apply/bind:显式绑定this

1. call & apply

const obj = { name: '指定对象' };

function demo(a, b) {
    console.log(this.name, a, b);
}

// call:参数逐个传递
demo.call(obj, 1, 2); // 输出"指定对象 1 2"

// apply:参数数组传递
demo.apply(obj, [1, 2]); // 输出"指定对象 1 2"

2. bind:永久绑定

const boundFn = demo.bind(obj, 1); // bind返回的是一个函数,需要手动执行
boundFn(2); // 输出"指定对象 1 2"
  • 返回新函数,this永久绑定
  • 支持参数预填充(柯里化,如果不了解柯里化的同学可以看柯里化

三者的核心区别:

方法执行时机返回值参数形式
call()立即执行函数结果逗号分隔列表
apply()立即执行函数结果数组
bind()延迟执行绑定后的函数逗号分隔列表

四、函数的双重身份:callableconstructor

JavaScript函数具有双重能力:

function MyFunc() {
    // 作为普通函数
    if (!new.target) {
        console.log('Called as function');
        return;
    }
    // 作为构造函数
    this.prop = 'value';
}

// 1. 函数调用([[call]])
MyFunc(); // 输出"Called as function"

// 2. 构造函数调用([[construct]])
const instance = new MyFunc();
console.log(instance.prop); // 输出"value"

关键机制

  • 函数内部通过new.target检测调用方式

  • 构造函数中:

    • 自动创建新对象,绑定到this
    • 隐式返回this(除非显式返回对象)

五、实战应用场景

1. 解决回调函数this丢失

// 传统方案:闭包保存this
const obj = {
    value: 42,
    init() {
        const that = this;
        button.addEventListener('click', function() {
            console.log(that.value);
        });
    }
}

// 现代方案:箭头函数
init() {
    button.addEventListener('click', () => {
        console.log(this.value);
    });
}

// 绑定方案:bind
init() {
    button.addEventListener('click', this.handler.bind(this));
}

2. 类数组转为真实数组

function listArgs() {
    // arguments是类数组对象
    const arr = Array.prototype.slice.call(arguments);
    arr.push('new');
    return arr;
}
listArgs(1, 2); // [1, 2, 'new']

六、练习题

题目 1:默认绑定

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

答案window(严格模式为 undefined
解析:函数直接调用时,非严格模式下 this 默认指向全局对象(浏览器中为 window)。


题目 2:隐式绑定(方法调用)

const user = {
  name: "Alice",
  greet() {
    console.log(this.name);
  }
};
user.greet();

答案"Alice"
解析:通过对象调用方法时,this 指向调用该方法的对象(即 user)。


题目 3:隐式绑定丢失(回调函数)

const user = {
  name: "Bob",
  greet() {
    console.log(this.name);
  }
};
setTimeout(user.greet, 100);

答案""(输出空字符串,实际指向 window.name
解析:将方法作为回调传递时,this 会丢失并回退到默认绑定(window)。
修复setTimeout(() => user.greet(), 100) 或 setTimeout(user.greet.bind(user), 100)


题目 4:显式绑定(call)

function showName() {
  console.log(this.name);
}
const obj = { name: "Charlie" };
showName.call(obj);

答案"Charlie"
解析call 强制将 this 绑定到第一个参数(此处为 obj)。


题目 5:箭头函数(词法作用域)

const person = {
  name: "David",
  greet: () => {
    console.log(this.name);
  }
};
person.greet();

答案""(指向 window.name
解析:箭头函数的 this 继承自外层作用域(此处是全局作用域),与调用方式无关。


题目 6:构造函数(new 绑定)

function Animal(name) {
  this.name = name;
}
const cat = new Animal("Tom");
console.log(cat.name);

答案"Tom"
解析:使用 new 调用构造函数时,this 指向新创建的对象实例(即 cat)。


总结规律:

  1. 独立调用 → 默认绑定(window/undefined
  2. 对象方法调用 → 隐式绑定(指向对象)
  3. call/apply/bind → 显式绑定(指向参数)
  4. 箭头函数 → 继承外层 this(定义时确定)
  5. new 调用 → 绑定到新实例

总结要点

  1. this绑定优先级:new > call/apply/bind > 方法调用 > 默认绑定

  2. 箭头函数解决this穿透问题,但不可作为构造函数

  3. 根据场景选择绑定方法:

    • 立即调用 → call/apply
    • 延迟执行 → bind
  4. 函数的双重角色通过new.target区分

理解this的绑定规则和函数执行机制,能有效避免常见的作用域陷阱,写出更健壮的JavaScript代码。