JavaScript this 绑定规则全解析:为什么它总让你掉坑?(彻底搞懂这四种规则,面试再也不慌)

67 阅读5分钟

在 JavaScript 这门语言里,如果说作用域和闭包是“最难理解”的概念,那 this 绝对是“最让人抓狂”的设计。

它不遵守词法作用域(Lexical Scope),而是完全由函数的调用方式决定。这导致无数人写着写着 this 就“丢了”,打印出来是 window 或 undefined,线上 bug 层出不穷。

今天我们就把 this 的底层机制彻底扒得一干二净,用最经典的例子(包括你自己踩过的坑),让你彻底明白:this 到底是怎么绑定的。

先说一个最容易让人崩溃的例子(99% 的人都会错)

HTML

<script>
'use strict';
var bar = {
    myName: 'bar',
    printName: function() {
        console.log("bar1", myName);     // global
        console.log("bar2:", this);      // window
        console.log("bar3:", this.myName); // undefined
    }
}
function foo() {
    let myName = 'foo';
    return bar.printName;
}
var myName = 'global';
let _printName = foo();
_printName();       // 普通函数调用 → 默认绑定
bar.printName();    // 对象方法调用 → 隐式绑定
</script>

运行结果:

  • _printName() 输出:global / window / undefined
  • bar.printName() 输出:global / bar 对象 / bar

为什么?

因为 printName 这个函数在定义时是全局函数,即使它写在 bar 对象里,本质上还是全局作用域的函数。

当你把它赋值给 _printName 再调用时,它就是普通函数调用,this 默认绑定到全局对象(严格模式下是 undefined)。

这就是经典的“隐式绑定丢失”。

很多人以为“函数写在对象里,this 就一定是这个对象”,错!this 永远只看“谁在调用它”。

this 的本质:执行上下文中的 thisBinding

V8 在创建执行上下文(Execution Context)时,会同时创建三个部分:

  • LexicalEnvironment(词法环境):管 let/const/block
  • VariableEnvironment(变量环境):管 var/function
  • thisBinding:就是我们要讲的 this 值

前两者在编译阶段就确定了(词法作用域),但 thisBinding 是执行阶段动态确定的,完全取决于你怎么调用这个函数。

《ECMAScript 规范》把 this 绑定分为四种规则,按优先级从高到低排列:

  1. new 绑定(最高优先级)
  2. 显式绑定(call/apply/bind)
  3. 隐式绑定(obj.foo())
  4. 默认绑定(普通函数调用)

记住这个优先级,90% 的 this 问题都能秒解。

规则1:默认绑定(最容易踩坑)

独立函数调用,this指向全局对象(非严格模式下),严格模式下是 undefined。

JavaScript

'use strict';
function foo() {
    console.log(this); // undefined
}
foo(); // 普通调用 → 默认绑定

为什么设计成这样?

Brendan Eich 10 天设计出 JavaScript,当时想让它像 Java 一样支持面向对象,所以加了 this。

但又想让函数超级灵活(第一等公民),于是偷懒了:没调用者就指向全局。

结果就是 var 声明的变量会挂到 window 上,污染全局,let/const 不会。

这就是历史遗留问题,后人只能背锅。

规则2:隐式绑定(最常见的“this 丢失”场景)

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

JavaScript

var bar = {
    myName: 'bar',
    printName: function() {
        console.log(this.myName);
    }
};
bar.printName(); // 'bar'   ← 隐式绑定,this = bar

但一旦把方法取出来单独调用,隐式绑定就没了:

JavaScript

let fn = bar.printName;
fn(); // undefined(严格模式)或 global(非严格)

这就是上面第一个例子的根本原因。

真实项目中常见的坑:

JavaScript

var obj = {
    name: '阿宝哥',
    getName: function() {
        console.log(this.name);
    }
};
setTimeout(obj.getName, 1000); // undefined 或 window

因为 setTimeout 内部是 window.setTimeout,相当于 fn() 调用,this 丢失。

正确写法:

JavaScript

setTimeout(() => obj.getName(), 1000); // 箭头函数无this,沿用外层this
// 或
setTimeout(obj.getName.bind(obj), 1000); // 硬绑定

Vue/React 里 this 丢失的根本原因也是这个。

规则3:显式绑定(call、apply、bind)—— 硬绑定,永不丢失

JavaScript

function foo() {
    console.log(this.myName);
}
let bar = { myName: 'bar' };

foo.call(bar);   // 'bar'
foo.apply(bar);  // 'bar'
let bindFoo = foo.bind(bar);
bindFoo();       // 'bar',永远是 bar

bind 是最强的,一旦 bind 了,后面的 call/apply/new 都改不了(new 优先级更高,后面讲)。

经典面试题:

JavaScript

var name = 'global';
let obj = { name: 'obj' };

function foo() {
    console.log(this.name);
}
let bindFoo = foo.bind(obj);
bindFoo();        // obj
bindFoo.call(window); // 还是 obj!bind 最硬

规则4:new 绑定(优先级最高)

用 new 调用构造函数,this 指向新创建的实例。

JavaScript

function CreateObj() {
    this.name = 'CreateObj';
    console.log(this); // CreateObj {}
}
var obj = new CreateObj(); // this 指向新对象

new 到底干了什么?(手动实现一个 new)

JavaScript

function myNew(Constructor, ...args) {
    // 1. 创建空对象
    const obj = {};
    // 2. 链接原型链
    obj.__proto__] = Constructor.prototype;
    // 3. 绑定 this 执行构造函数
    const result = Constructor.apply(obj, args);
    // 4. 返回对象(如果构造函数返回对象则返回那个,否则返回 obj)
    return typeof result === 'object' && result !== null ? result : obj;
}

你提供的最后一个代码其实想表达这个意思,但写错了位置,我帮你纠正一下:

JavaScript

function CreateObj() {
    this.name = 'CreateObj';
}
CreateObj.prototype.say = function() {
    console.log('hello');
};
var obj = myNew(CreateObj);
console.log(obj.name); // { name: 'CreateObj' }
obj.say(); // hello

new 绑定的优先级最高,所以:

JavaScript

function foo() {
    console.log(this);
}
let bindFoo = foo.bind({ name: '被bind的' });
new bindFoo(); // foo {}   ← new 赢了!

箭头函数:彻底打破规则的“异类”

箭头函数没有自己的 this,它会捕获定义时所在的外层执行上下文的 this。

JavaScript

let obj = {
    name: 'obj',
    foo: function() {
        setTimeout(() => {
            console.log(this); // obj 对象!不是 window
        }, 1000);
    }
};
obj.foo();

这是解决 this 丢失最优雅的方式,Vue/React 推荐使用箭头函数绑定事件就是因为这个。

面试高频题

题1:

JavaScript

var name = 'window';
var A = {
    name: 'A',
    say: function() {
        console.log(this.name);
    }
};
var B = {
    name: 'B',
    speak: A.say                // speak 只是对同一个函数的引用
};

B.speak();         // → "B"   (隐式绑定)
A.say.call(B);     // → "B"   (显式绑定)

答案:B(隐式绑定)/ B(显式绑定覆盖)

题2:

JavaScript

function foo() {
    console.log(this.a);
}
var a = 2;
var o = { a: 3, foo };
o.foo();           // 3(隐式)
foo.call(null);    // 2(call(null) 相当于默认绑定,非严格模式)

call(null) 在非严格模式下会退化为默认绑定,这也是坑。

题3:

JavaScript

let obj = {
    a: 1,
    foo: function() {
        console.log(this.a);
    }.bind({ a: 2 })
};
obj.foo(); // 2!bind 后的函数,隐式绑定无效

总结:一句话记住 this 绑定规则

谁调用我,我就是谁(除了箭头函数)

  • obj.foo() → this = obj(隐式)
  • foo() → this = window/undefined(默认)
  • foo.call(obj) / bind(obj) → this = obj(显式)
  • new foo() → this = 新实例(最高优先级)

记住这四条 + 优先级 + 箭头函数无 this,你就已经超越 95% 的前端开发者了。

this 的设计确实是个历史包袱,但理解了它的规则,你就能在 Vue、React、Node、小程序各种环境上游刃有余地驾驭它,而不是被它驾驭。

下次再遇到 this 指向问题,直接问自己一句话:

“这个函数到底是被谁调用的?”

答案就出来了。