Published on
2540

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

Authors
  • avatar
    Name
    小辉辉
    Twitter

在 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) 为例,它的执行过程很简单:

  • 第一步,执行第一个表达式 00 是无副作用的数字字面量,执行后不会改变程序状态,结果直接被忽略;

  • 第二步,执行第二个表达式 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 的指向遵循以下优先级(从高到低):

  1. new 绑定:用 new 调用函数(如 new fn()),this 指向新创建的实例对象;

  2. 显式绑定:用 call()/apply()/bind() 调用函数(如 fn.call(obj)),this 指向手动指定的 obj

  3. 隐式绑定:通过对象调用函数(如 obj.fn()),this 指向调用函数的对象 obj

  4. 默认绑定:独立调用函数(如 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 为空

总结:核心逻辑一句话讲透

通过以上分析,我们可以用三句关键结论梳理清楚整个逻辑:

  1. 逗号表达式不改变引用(0, a.b) 只是 “提取”a.b 的原函数引用,没有创建新函数,所以 (0, a.b) === a.b 成立;

  2. this 指向由 “调用方式” 决定:函数引用是否相同,和 this 指向无关 ——this 只看 “函数是怎么被调用的”;

  3. 两种调用触发不同绑定

  • a.b():通过对象调用 → 隐式绑定 → this 指向 a

  • (0, a.b)():独立调用 → 默认绑定 → 非严格模式下 this 指向 window

四、实际场景:逗号表达式的 “解绑定” 有什么用?

虽然 (0, a.b)() 看起来像 “小众技巧”,但在实际开发中很有用 —— 它的核心作用是 将对象方法转换为独立函数,消除和原对象的隐式绑定

常见的应用场景有三类:

  1. 代码压缩优化:压缩工具(如 Terser)会把 a.b() 压缩成 (0,a.b)(),这样能去掉 “.” 和空格,缩短代码长度,同时不影响功能(前提是函数不依赖 this 指向原对象);

  2. 避免修改原对象:某些场景下,我们需要调用对象的方法,但不希望 this 指向原对象(比如怕误改原对象的属性),这时用逗号表达式 “解绑定” 就很合适;

  3. 函数参数传递:把对象方法作为参数传递时(如 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 指向问题?

理解了底层逻辑后,我们可以总结三个实用建议,避免踩坑:

  1. 警惕 “独立函数引用” 的调用:当函数通过 “变量赋值(const fn = a.b)”“逗号表达式((0, a.b))”“参数传递(setTimeout(a.b))” 获取时,会失去和原对象的隐式绑定,this 可能指向全局对象,需提前处理;

  2. 明确 this 指向的两种方式

  • 想让 this 指向原对象:直接用 a.b() 调用,或用 a.b.bind(a) 显式绑定;

  • 想让 this 指向其他对象:用 a.b.call(obj)a.b.apply(obj) 手动指定(两者区别是参数传递方式,call 传参数列表,apply 传参数数组);

  1. 优先开启严格模式:严格模式下,独立函数调用的 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.bthis 指向不同” 的底层逻辑。这一知识点看似是细节,但能帮你更深入理解 JavaScript 中 “函数引用” 和 “this 绑定” 的核心机制,在实际开发中避开这类容易混淆的坑。