JavaScript中this指向规则:默认、隐式、显式及new绑定的优先级
全面解析JavaScript中this关键字的确定机制与四种核心绑定规则,通过调用位置分析、代码示例、执行原理与优先级推导,系统解决this指向混乱问题。
JavaScript 中的 this 机制常常给开发者带来困扰,其核心原因在于它不同于词法作用域。除了箭头函数之外,this 既不指向函数本身,也不取决于函数声明定义的位置,而是在函数被实际调用时动态绑定的。要准确判断 this 的指向,只需把握两个步骤:第一步,找到该函数执行的实际调用位置(Call-site);第二步,按照 new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定 的优先级顺序套用判断规则。本文将系统拆解这四种绑定规则的运作机制、常见失误场景以及它们相互重叠时的优先级判定逻辑。
一、寻找调用的源头:调用位置与调用栈
分析 this 的第一步绝非看函数在何处写出,而是观察它在运行瞬间是“如何被调用的”。这需要理清调用位置(Call-site)与调用栈(Call-stack)的关系。
调用位置指的是函数在代码中被调用的具体行。调用栈则是为了到达当前执行位置所调用的所有函数形成的执行环境栈。真正的调用位置恰恰就在当前执行函数的前一个调用帧中。
function topLevel() {
// 当前调用栈:topLevel
// 调用位置是全局环境
console.log("topLevel执行");
secondLevel(); // <-- secondLevel 的调用位置
}
function secondLevel() {
// 当前调用栈:topLevel -> secondLevel
// secondLevel 的调用位置在 topLevel 中
console.log("secondLevel执行");
targetFunc(); // <-- targetFunc 的调用位置
}
function targetFunc() {
// 当前调用栈:topLevel -> secondLevel -> targetFunc
// targetFunc 的调用位置在 secondLevel 中
console.log(this);
}
topLevel(); // <-- topLevel 的调用位置
二、四种核心绑定规则详解
在定位到调用位置之后,需根据调用的语法结构将其归入四种标准绑定规则之一。
1. 默认绑定(Default Binding)
最常见也最容易发生疏漏的调用类型是独立函数调用(Standalone Function Invocation)。当函数不带任何修饰、直接以标识符加圆括号调用时,适用的就是默认绑定。
- 非严格模式:
this默认指向全局对象(在浏览器端为window,在 Node.js 环境中为global)。 - 严格模式('use strict'):全局对象不能作为默认绑定的载体,
this将被绑定到undefined。
function showValue() {
console.log(this.val);
}
var val = "全局变量";
showValue(); // 输出: "全局变量"(非严格模式)
function strictShow() {
"use strict";
console.log(this);
}
strictShow(); // 输出: undefined
注意调用环境与定义位置的严格模式差异:决定 this 是否绑定为 undefined 的,取决于函数体内部是否声明了严格模式,而不是调用位置所在的函数是否处于严格模式下。
2. 隐式绑定(Implicit Binding)
当调用位置具有上下文对象(Context Object)或者说该函数作为某个对象的属性被引用调用时,隐式绑定规则会起作用。此时,该上下文对象会被绑定到函数调用的 this 上。
function introduce() {
console.log(this.name);
}
const user = {
name: "张三",
introduce: introduce
};
user.introduce(); // 输出: "张三"
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。例如 company.department.user.introduce(),最终绑定到 this 上的对象仅是紧邻函数的 user。
隐式丢失现象:隐式绑定最容易出现的问题是“丢失绑定对象”,从而退化为默认绑定。最典型的场景包括引用赋值和参数传递:
const bar = user.introduce; // 仅仅是把函数引用赋给 bar
bar(); // 实际上是独立函数调用!输出: undefined(或报错)
// 传递回调函数时同样会隐式丢失
setTimeout(user.introduce, 100); // 内部执行独立调用,this 丢失
3. 显式绑定(Explicit Binding)
隐式绑定强制要求函数成为对象的属性。如果不想在对象内部增加额外函数属性,而希望直接在某个指定对象上强制调用函数,可以使用 Function.prototype 提供的 call()、apply() 以及 bind() 方法。
call(thisArg, arg1, arg2, ...):立即执行函数,参数逐个传入。apply(thisArg, [argsArray]):立即执行函数,参数以数组或类数组形式传入。bind(thisArg, arg1, ...):不立即执行,返回一个硬绑定(Hard Binding)的新函数,该新函数内部永久将this固定为传入的目标对象。
function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
const client = { name: "李四" };
greet.call(client, "你好", "!"); // 你好, 李四!
greet.apply(client, ["早上好", "。"]); // 早上好, 李四。
const boundGreet = greet.bind(client);
boundGreet("欢迎", "!"); // 欢迎, 李四!
4. new 绑定(New Binding)
在 JavaScript 中,构造函数只是普通的函数,当用 new 操作符来调用它们时,函数调用的性质发生了改变。使用 new 调用函数时,会自动执行以下四个关键步骤:
- 在内存中创建一个全新的空 JavaScript 对象;
- 将这个新对象的原型(
__proto__)链接到该构造函数的prototype对象; - 将这个新创建的对象绑定到函数调用的
this; - 如果构造函数没有显式返回非空对象,则
new表达式自动返回这个新创建的对象。
function Person(name) {
// 此时 this 指向刚刚实例化的新空对象
this.name = name;
}
const p = new Person("王五");
console.log(p.name); // 输出: "王五"
三、绑定规则的优先级推导与层层裁决
当一个调用位置同时符合多个规则的特征时,哪一个规则会胜出?我们需要通过两两对比来确立优先级秩序。
1. 显式绑定 vs 隐式绑定
如果一个方法通过对象调用(隐式绑定),但同时又使用了 call 或 apply(显式绑定),谁的优先级更高?
function printName() {
console.log(this.name);
}
const obj1 = { name: "对象1", printName: printName };
const obj2 = { name: "对象2" };
obj1.printName(); // 输出: "对象1"(隐式绑定)
obj1.printName.call(obj2); // 输出: "对象2"(显式绑定胜出)
显然,显式绑定 > 隐式绑定。显式指定的目标会直接覆盖上下文对象。
2. new 绑定 vs 隐式绑定
如果使用 new 来调用某个对象的属性函数,this 会指向该属性宿主对象还是指向 new 出来的新实例?
const factory = {
name: "工厂对象",
Person: function(val) {
this.name = val;
}
};
const item = new factory.Person("新实例");
console.log(item.name); // 输出: "新实例"
console.log(factory.name); // 输出: "工厂对象"
结果显示,factory.name 没有被改写,新创建的 item.name 被成功赋值。这证明 new 绑定 > 隐式绑定。
3. new 绑定 vs 显式绑定
因为 new 不能直接与 call/apply 同时书写(例如 new fn.call(obj) 在语法层面会报语法错误),我们需要借助 Function.prototype.bind 生成的硬绑定函数来进行检验:
function Base(val) {
this.val = val;
}
const fixedObj = {};
const BoundBase = Base.bind(fixedObj);
BoundBase("硬绑定修改");
console.log(fixedObj.val); // 输出: "硬绑定修改"
// 使用 new 实例化该硬绑定函数
const newInstance = new BoundBase("通过new创建");
console.log(fixedObj.val); // 输出依然是: "硬绑定修改"
console.log(newInstance.val); // 输出: "通过new创建"
在上述代码中,new BoundBase("通过new创建") 并没有将 val 属性写入 fixedObj,而是将其赋给了返回的新实例 newInstance。这揭示了 bind 内部的实现原理:bind 返回的高阶函数会检查当前是否被 new 调用(通常通过 this instanceof boundFn 进行判断);如果是被 new 调用,它会直接忽略绑定的预设目标,将新创建的对象作为 this 传入底层函数。
因此得出结论:new 绑定 > 显式绑定。
四、规则之外的特殊情况与边缘场景
掌握了上述四条规则并不意味着能避开所有坑。在实际工程中,还有几种需要格外留意的特殊情况:
1. 被忽略的显式绑定(传入 null 或 undefined)
如果将 null 或 undefined 作为 this 绑定对象传递给 call、apply 或 bind,这些值在调用时会被忽略,实际应用的将是默认绑定规则。
function show() {
console.log(this);
}
show.call(null); // 非严格模式下输出 window / global,严格模式下输出 null
在 ES6 展开运算符普及之前,开发者常用 fn.apply(null, [1, 2]) 来展开数组参数,这容易意外修改全局对象。更安全的做法是准备一个纯净的空对象(如 const DMZ = Object.create(null))作为占位符。
2. 间接函数引用(赋值表达式的结果)
在代码中无意创建了间接引用时,调用这个函数同样会触发默认绑定:
const a = { val: 2, foo: function() { console.log(this.val); } };
const b = { val: 3 };
// (b.foo = a.foo) 表达式的返回值是底层函数本身,而不是目标对象上的方法
(b.foo = a.foo)(); // 输出 undefined 或全局的 val,而非 3!
3. ES6 箭头函数:脱离四种规则的词法 this
ES6 引入的箭头函数(Arrow Function)并不遵循上述四种规则。箭头函数的 this 完全由定义它时的外层词法作用域决定,并且一旦确定就无法通过 call/apply/bind 或 new 覆盖。
function Outer() {
// 此时外层作用域的 this 是 obj
return () => {
console.log(this.name);
};
}
const objA = { name: "A" };
const objB = { name: "B" };
const arrowFn = Outer.call(objA);
arrowFn(); // 输出: "A"
arrowFn.call(objB); // 输出依然是: "A"(无法被显式覆盖)
五、全规则对比与实战排查速查
为了在开发与代码审查中快速定位 this 疑难问题,下表系统梳理了四种绑定规则的核心维度:
| 绑定规则 | 调用形式示例 | this 最终指向 | 优先级位阶 | 失误/隐式丢失常见场景 |
|---|---|---|---|---|
| new 绑定 | new Foo() |
新建的空实例对象 | 最高(优先级 1) | 函数内部显式 return 了其他对象导致实例被替换 |
| 显式绑定 | fn.call(obj)fn.bind(obj)() |
指定的上下文对象 obj |
次高(优先级 2) | 向 call/apply 传入了 null 或 undefined 导致退化为默认绑定 |
| 隐式绑定 | obj.fn() |
上下文宿主对象 obj |
中等(优先级 3) | 将对象方法赋值给独立变量、或作为回调函数传递给 setTimeout/事件系统 |
| 默认绑定 | fn() |
严格模式为 undefined非严格模式为全局对象 |
最低(优先级 4) | 意外污染全局作用域变量,或在严格模式下引发 TypeError |
六、排查 this 指向的标准化步骤
在日常排查 Bug 时,建议严格按照以下四步执行判断:
- 看函数类型:确认目标函数是否为箭头函数。如果是,直接跨过调用位置,向上寻找其外层最近的普通函数或全局环境的
this,该结果即为终值。 - 找实际调用位置:不要看函数被保存在哪里,只看触发该调用的那一行动作,确定真正的 Caller 栈帧。
- 套用优先级自顶向下匹配:
- 是否有
new关键字?如果有,指向新实例。 - 是否使用了
call、apply或经过bind包装?如果有,指向显式绑定的目标对象。 - 是否通过某个对象属性发起调用(形如
a.b.func())?如果有,指向最终调用的宿主对象。 - 如果以上都不是,归入默认绑定(检查严格模式标记,决定是全局还是
undefined)。
- 是否有
- 验证是否存在显式参数丢失或间接调用:检查是否存在将
null作为目标传入显式方法、或者逗号表达式赋值导致的方法引用提取。
JS动态构建复杂嵌套JSON对象的方法
2026-11-05 15:41:52
JS负数取余为什么是负数?详解余数与模运算区别
2026-11-05 13:34:41
JS 解压出现乱码或报错怎么解决?
2026-11-05 12:32:38
JS 中 startsWith、indexOf 与正则判断字符串前缀的对比
2026-11-04 22:19:43
JS解析Excel后怎么将数据转换为JSON格式
2026-11-04 20:58:28








