- Published on
- 约 2540 字
深入解析 JavaScript 中的逗号表达式与函数引用:为何 (0, a.b) === a.b 但是 this 指向不同?
- Authors

- Name
- 小辉辉
在 JavaScript 开发中,你或许遇到过这样一个 “矛盾” 场景:(0, a.b) === a.b 的结果明明是 true,但调用 a.b() 时函数内部 this 指向对象 a,调用 (0, a.b)() 时(非严格模式下)this 却指向 window。这背后藏着逗号表达式的特性、函数引用的本质,以及 this 绑定规则等核心知识点。本文将一步步拆解这些逻辑,帮你彻底搞懂这一容易混淆的现象。
先理清:为什么 (0, a.b) === a.b 成立?
要理解这个等式的合理性,我们需要从 逗号表达式的返回规则 和 JavaScript 引用类型的比较逻辑 两个维度展开分析。
1. 逗号表达式:只返回 “最后一个表达式” 的结果
逗号表达式是 JavaScript 中一种特殊的运算符,语法格式为 (表达式1, 表达式2, ..., 表达式N),它的核心行为有两点:
按 “从左到右” 的顺序依次执行所有表达式(执行过程中,前 N-1 个表达式的结果会被忽略);
最终仅返回 最后一个表达式(表达式 N)的结果,且不会对这个结果做任何额外加工(比如复制、包装)。
以 (0, a.b) 为例,它的执行过程很简单:
第一步,执行第一个表达式
0:0是无副作用的数字字面量,执行后不会改变程序状态,结果直接被忽略;第二步,执行第二个表达式
a.b:获取对象a上的b属性(这里b是一个函数),得到a.b函数的 “引用”;最终,逗号表达式返回的就是
a.b函数本身的引用 —— 没有创建新函数,也没有修改原函数的任何属性。
2. 函数引用比较:本质是 “内存地址” 的匹配
在 JavaScript 中,函数属于 引用类型(本质是特殊的对象)。引用类型的比较规则非常明确:只有当两个引用指向 “内存中同一个对象” 时,它们的严格相等(===)结果才是 true。
由于 (0, a.b) 返回的是 a.b 原函数的引用(而非新生成的函数副本),所以 (0, a.b) 和 a.b 指向的是同一块内存中的函数对象。这就像两个人拿的是同一把钥匙 —— 钥匙本身没变,只是获取钥匙的方式不同(一个是直接拿,一个是通过逗号表达式 “中转” 拿)。
我们可以通过一段代码直观验证这一点:
const a = {
b: function() {
console.log("我是函数 a.b");
}
};
// 验证引用是否相等
console.log(a.b === a.b); // true(直接引用同一个函数)
console.log((0, a.b) === a.b); // true(逗号表达式返回原函数引用)
console.log((0, a.b) === (0, a.b)); // true(两次逗号表达式返回同一个引用)
// 进一步验证:修改一个引用的属性,另一个会同步变化
(0, a.b).name = "testFn"; // 给逗号表达式返回的函数引用加属性
console.log(a.b.name); // "testFn"(证明两者指向同一个函数对象)
关键疑问:引用相同,为什么 this 指向不一样?
既然 (0, a.b) 和 a.b 是同一个函数,为什么调用时 this 指向会有差异?答案藏在 JavaScript 的 this 绑定规则 里 ——this 指向不取决于函数本身,而是取决于 “函数的调用方式”。
1. 先回顾:非严格模式下的 this 绑定优先级
在非严格模式中,函数调用时 this 的指向遵循以下优先级(从高到低):
new 绑定:用
new调用函数(如new fn()),this指向新创建的实例对象;显式绑定:用
call()/apply()/bind()调用函数(如fn.call(obj)),this指向手动指定的obj;隐式绑定:通过对象调用函数(如
obj.fn()),this指向调用函数的对象obj;默认绑定:独立调用函数(如
fn()),this指向全局对象(浏览器环境中是window,Node.js 中是global)。
我们关注的 a.b() 和 (0, a.b)(),正好对应 “隐式绑定” 和 “默认绑定” 这两种场景,这也是两者 this 指向不同的核心原因。
2. a.b ():隐式绑定,this 指向 a
当函数通过 “对象.函数()” 的形式调用时,会触发 隐式绑定 规则:函数内部的 this 会自动 “绑定” 到 “. 前面的对象”(也就是调用函数的主体对象)。
以 a.b() 为例,逻辑很清晰:
函数
b是通过对象a调用的(a是 “.前面的主体”);根据隐式绑定规则,
this会指向a,所以函数内部访问this时,能拿到a的属性和方法。
代码验证(直观看到 this 指向):
const a = {
name: "对象 a",
b: function() {
console.log("this 指向的对象:", this); // 打印 this 指向
console.log("this.name:", this.name); // 访问 this 上的属性
}
};
a.b();
// 输出结果:
// this 指向的对象: { name: '对象 a', b: \[Function: b] }
// this.name: 对象 a
3. (0, a.b)():默认绑定,this 指向 window
当用 (0, a.b)() 调用函数时,整个逻辑发生了变化,可拆成两步理解:
第一步:逗号表达式
(0, a.b)返回的是a.b的 “独立引用”—— 此时函数已经和原对象a脱离了绑定关系,就像把函数从a上 “摘下来”,变成了一个独立的函数变量(类似const fn = a.b);第二步:用
()直接调用这个 “独立引用”(相当于fn()),触发 默认绑定 规则;在非严格模式下,默认绑定的
this会指向全局对象window,所以函数内部的this不再是a,而是window。
代码验证(模拟执行过程,看清差异):
const a = {
name: "对象 a",
b: function() {
console.log("this 指向的对象:", this);
console.log("this.name:", this.name);
}
};
// 第一步:模拟逗号表达式的“解绑定”过程
const independentFn = a.b; // 把 a.b 摘下来,变成独立函数引用
// 第二步:独立调用函数,触发默认绑定
independentFn();
// 输出结果(非严格模式):
// this 指向的对象: Window(浏览器环境)/ global(Node.js)
// this.name: ""(window 上默认没有 name 属性,值为空字符串)
// 直接执行 (0, a.b)(),结果和上面一致
(0, a.b)();
// 输出结果同上:this 指向 window,this.name 为空
总结:核心逻辑一句话讲透
通过以上分析,我们可以用三句关键结论梳理清楚整个逻辑:
逗号表达式不改变引用:
(0, a.b)只是 “提取”a.b的原函数引用,没有创建新函数,所以(0, a.b) === a.b成立;this 指向由 “调用方式” 决定:函数引用是否相同,和
this指向无关 ——this只看 “函数是怎么被调用的”;两种调用触发不同绑定:
a.b():通过对象调用 → 隐式绑定 →this指向a;(0, a.b)():独立调用 → 默认绑定 → 非严格模式下this指向window。
四、实际场景:逗号表达式的 “解绑定” 有什么用?
虽然 (0, a.b)() 看起来像 “小众技巧”,但在实际开发中很有用 —— 它的核心作用是 将对象方法转换为独立函数,消除和原对象的隐式绑定。
常见的应用场景有三类:
代码压缩优化:压缩工具(如 Terser)会把
a.b()压缩成(0,a.b)(),这样能去掉 “.” 和空格,缩短代码长度,同时不影响功能(前提是函数不依赖this指向原对象);避免修改原对象:某些场景下,我们需要调用对象的方法,但不希望
this指向原对象(比如怕误改原对象的属性),这时用逗号表达式 “解绑定” 就很合适;函数参数传递:把对象方法作为参数传递时(如
setTimeout(a.b, 1000)),本质上也是传递 “独立引用”,this会指向window,和(0, a.b)()原理完全一致。
举个实际开发中容易踩坑的例子(定时器传递对象方法):
const a = {
count: 0,
increment: function() {
this.count++; // 依赖 this 指向 a
console.log("当前 count:", this.count);
}
};
// 错误用法:直接传递 a.increment,this 指向 window
setTimeout(a.increment, 1000);
// 1 秒后输出:当前 count: NaN(window.count 是 undefined,++ 后变成 NaN)
// 正确用法:用 bind() 显式绑定 this 到 a
setTimeout(a.increment.bind(a), 1000);
// 1 秒后输出:当前 count: 1(this 指向 a,count 正常自增)
五、避坑建议:开发中如何避免 this 指向问题?
理解了底层逻辑后,我们可以总结三个实用建议,避免踩坑:
警惕 “独立函数引用” 的调用:当函数通过 “变量赋值(
const fn = a.b)”“逗号表达式((0, a.b))”“参数传递(setTimeout(a.b))” 获取时,会失去和原对象的隐式绑定,this可能指向全局对象,需提前处理;明确 this 指向的两种方式:
想让
this指向原对象:直接用a.b()调用,或用a.b.bind(a)显式绑定;想让
this指向其他对象:用a.b.call(obj)或a.b.apply(obj)手动指定(两者区别是参数传递方式,call传参数列表,apply传参数数组);
- 优先开启严格模式:严格模式下,独立函数调用的
this会指向undefined(而非window),这样一旦误写调用方式,会直接报 “Cannot read property 'xxx' of undefined” 错误,能更早发现问题(而非默默返回undefined,排查时更难定位)。
开启严格模式的方式很简单,在文件或函数顶部加一句 'use strict' 即可:
'use strict'; // 开启严格模式
const a = {
b: function() {
(0, a.b)(); // 严格模式下,this 指向 undefined
}
};
通过本文的分析,相信你已经彻底搞懂了 “(0, a.b) === a.b 且 this 指向不同” 的底层逻辑。这一知识点看似是细节,但能帮你更深入理解 JavaScript 中 “函数引用” 和 “this 绑定” 的核心机制,在实际开发中避开这类容易混淆的坑。
