Skip to content

in 操作符与原型链查找:为什么 'toString' in obj 为 true?它的查找路径是怎样的?

in 操作符不是检查对象自身,而是启动一次完整的原型链遍历——它询问的不是‘你有没有’,而是‘你或你的祖先有没有’。”

当你写下:

js
const obj = {};
'toString' in obj; // true

你可能会惊讶:obj 自身并没有 toString 属性,
为何返回 true

答案藏在 JavaScript 的属性查找协议中:
in 操作符的语义,是检查一个属性名是否存在于对象自身或其原型链中的任意对象上

我们来从 规范定义、查找算法、与 hasOwnProperty 的对比、性能影响 四个维度,彻底拆解:

一、规范定义:in 的运行时语义

来自 ECMA-262 §13.10.1

in 操作符的求值过程如下:

  1. 计算左操作数(属性键)
  2. 计算右操作数(对象)
  3. 调用 HasProperty(O, P) 抽象操作
  4. 返回布尔值

其中,HasProperty(O, P) 是关键。

二、HasProperty 抽象操作:原型链遍历的起点

来自 ECMA-262 §7.3.18

js
HasProperty(O, P)
  • O:目标对象
  • P:属性键(字符串或 Symbol)

执行步骤:

  1. 断言:Type(O) 是 Object
  2. 返回 ? O.[[HasProperty]](P)

[[HasProperty]] 是对象的内部方法,其行为由对象类型决定。

三、[[HasProperty]] 的实现:标准内置对象的查找算法

对于标准内置对象(如普通对象),[[HasProperty]](P) 的算法如下:

  1. 调用 O.[[GetOwnProperty]](P)
  2. 如果返回值不是 undefined,返回 true
  3. 否则,获取 O 的原型 parent = O.[[Prototype]]
  4. 如果 parentnull,返回 false
  5. 否则,递归调用 parent.[[HasProperty]](P)

这就是原型链查找的核心:先查自身,再沿 [[Prototype]] 向上递归

四、图解 'toString' in obj 的查找路径

js
const obj = {};
'toString' in obj;

执行过程:

obj.[[HasProperty]]('toString')

  ├─ obj.[[GetOwnProperty]]('toString') → undefined(自身无此属性)

  └─ obj.[[Prototype]] → Object.prototype

       └─ Object.prototype.[[HasProperty]]('toString')

            ├─ Object.prototype.[[GetOwnProperty]]('toString') → 存在!

            └─ 返回 true

因为 Object.prototype 上定义了 toString 方法,
所以 'toString' in obj 返回 true

五、与 hasOwnProperty 的本质区别

obj.hasOwnProperty(prop)

来自 ECMA-262 §20.1.3.4

检查属性是否为对象的自有属性(own property),不查找原型链。

js
const obj = {};
obj.hasOwnProperty('toString'); // false
Object.prototype.hasOwnProperty('toString'); // false(它也是继承的?)

// 实际上:
Object.prototype.propertyIsEnumerable('toString'); // true

hasOwnProperty 只检查 [[GetOwnProperty]] 是否返回描述符,
不向上查找。

对比表

操作查找范围是否查原型链典型用途
'prop' in obj自身 + 原型链检查属性是否存在(无论来源)
obj.hasOwnProperty('prop')仅自身检查是否为自有属性
Object.prototype.hasOwnProperty.call(obj, 'prop')仅自身安全检查(避免 hasOwnProperty 被覆盖)

六、边界案例:Object.create(null)in

js
const clean = Object.create(null);
'toString' in clean; // false

因为:

  • clean.[[Prototype]] = null
  • HasProperty 在第一步查自身无结果,第二步原型为 null,直接返回 false

这就是 Object.create(null) 被称为“干净对象”的原因之一:
它的 in 检查只针对自身属性,不涉及任何继承。

七、in 的性能影响:原型链越深,查找越慢

in 操作符的时间复杂度为 O(d),其中 d 是原型链深度。

js
// 深层继承
function A() {}
function B() {}
B.prototype = new A();

function C() {}
C.prototype = new B();

const c = new C();
'toString' in c; // 需要遍历 c → C.prototype → B.prototype → A.prototype → Object.prototype

虽然现代引擎对常见属性(如 toString)有缓存优化,
但在性能敏感场景,应避免在循环中使用 in 查找深层属性。

八、infor...in 的关系

for...in 的迭代逻辑也依赖 [[HasProperty]][[Enumerate]]
但它只枚举可枚举的自有和继承属性

js
const obj = { x: 1 };
Object.defineProperty(obj, 'y', {
  value: 2,
  enumerable: false
});

for (let k in obj) {
  console.log(k); // 只输出 'x'
}

'y' in obj 仍为 true,因为 in 不关心 enumerable

一句话总结

in 操作符通过 HasProperty(O, P) 抽象操作,执行一次完整的原型链查找:

  1. 先检查对象自身是否拥有该属性([[GetOwnProperty]]
  2. 若无,沿 [[Prototype]] 链向上递归查找
  3. 直到原型为 null 或找到属性为止

因此,即使 obj 自身没有 toString,只要其原型链上的某个对象有,'toString' in obj 就为 true

结语:理解 in,就是理解“属性存在的语义”

大多数人认为 'prop' in obj 等价于 obj.prop !== undefined
而你理解的是:

in 是一个存在性查询,它不求值,只问‘是否存在’;
它的查找路径是一次从具体到抽象的继承链遍历,体现了 JavaScript 原型系统的动态性。”

你不再只是“检查属性”,
而是在理解语言的查找协议