首页
/ freeCodeCamp JavaScript 数据基础教程:用 hasOwnProperty() 与 in 运算符检测对象是否拥有某个属性

freeCodeCamp JavaScript 数据基础教程:用 hasOwnProperty() 与 in 运算符检测对象是否拥有某个属性

2026-09-07 14:17:11作者:尤辰城Agatha

本文围绕 freeCodeCamp「JavaScript 算法与数据结构」课程中 Basic Data Structures 板块的一道核心编程挑战「Check if an Object has a Property」(挑战文件位于 curriculum/challenges/english/blocks/basic-data-structures/587d7b7d367417b2b2512b1c.md)展开。你将系统掌握 JavaScript 检测对象属性的两种标准手段——hasOwnProperty() 方法与 in 运算符——理解它们对自有属性与继承属性的不同判定语义,看懂该挑战内置自动判题(hints)的判定逻辑,并获取可直接落地的参考实现与边界情况处理方案。

一、挑战背景:它在课程与代码库中的位置

这道挑战隶属于 Basic Data Structures(基础数据结构) 代码块。在该块的挑战序列(见 curriculum/structure/blocks/basic-data-structures.json)中,它是 20 道挑战中的第 17 道:前面是「Use the delete Keyword to Remove Object Properties」,后面紧接着「Iterate Through the Keys of an Object with a for...in Statement」。整块挑战的 helpCategoryJavaScriptblockLayoutlegacy-challenge-list,属于经典的按序挑战列表。

从板块归属看,该块被收录于 JavaScript Algorithms and Data Structures(JavaScript 算法与数据结构)superblock 的 curriculum/structure/superblocks/javascript-algorithms-and-data-structures.json,对应的是这门经典认证课程的核心 JavaScript 部分。

在该 Markdown 挑战文件的 YAML 前置元数据中,可以看到课程工程对每道挑战的结构化描述(见文件开头):

id: 587d7b7d367417b2b2512b1c
title: Check if an Object has a Property
challengeType: 1
forumTopicId: 301155
dashedName: check-if-an-object-has-a-property

其中 challengeType: 1 并不是随意取的数字。freeCodeCamp 将 34 种挑战类型集中定义在 packages/shared/src/config/challenge-types.tsconst js = 1; 表示这是经典的 JavaScript 代码编辑挑战,对应视图类型 'classic'、提交类型 'tests'(即运行内置测试用例判定结果)。dashedName 则是该挑战在课程站点 URL 中使用的 slug。

而所有挑战文件都必须通过 curriculum/schema/challenge-schema.js 中基于 Joi 定义的结构校验,例如 challengeType: Joi.number().min(0).max(33).required()dashedName 必须匹配 ^[a-z0-9-]+$ 的 slug 正则,这保证了本文件所采用的 YAML 结构可以稳定地被课程构建系统解析。

二、核心知识点:检测对象属性的两种途径

挑战正文(--description-- 小节,见 587d7b7d367417b2b2512b1c.md#L11-L18)开门见山:在学会了为对象添加、修改、删除键之后,下一个问题是"如何只判断对象里是否存在某个键"。JavaScript 提供了两种等价写法。假设有一个名为 users 的对象,它带有属性 Alan,则下面两条语句都能检查其存在性:

users.hasOwnProperty('Alan');
'Alan' in users;

两种写法都会返回 true

2.1 hasOwnProperty():只认"自有属性"

Object.prototype.hasOwnProperty() 是一个继承自原型链的方法,接收一个属性名字符串(或 Symbol),返回布尔值,表示该属性是否为对象自身的直接属性(own property),即不沿原型链向上查找。

2.2 in 运算符:连同"继承属性"一起判断

in 是真正的运算符(左操作数为属性名,右操作数为对象),只要属性存在于该对象或其原型链上的任何一环,就返回 true

二者对普通字面量对象几乎没有差别,但对继承来的属性差别明显。例如任何对象都继承了 Object.prototype 上的 toString 属性:

const plain = {};

'toString' in plain;          // true —— 原型链上有
plain.hasOwnProperty('toString'); // false —— 不是自身属性

同理,'constructor' in plaintrue,而 plain.hasOwnProperty('constructor')false。这也解释了为什么许多"属性白名单"校验场景更偏爱 hasOwnProperty():它不会把原型链上的方法误判为对象真实携带的数据键。

补充:ES2022 起标准库提供了静态方法 Object.hasOwn(obj, prop),其语义与 obj.hasOwnProperty(prop) 一致,但即便对象是通过 Object.create(null) 创建、完全不带原型方法时也能安全调用。本挑战要求使用经典语法,但理解这一现代替代方案有助于编写更健壮的防御性代码。

三、挑战任务拆解:确认四位成员全部在场

--instructions--(见 587d7b7d367417b2b2512b1c.md#L20-L22)给出的任务很明确:完成 isEveryoneHere 函数,使其在传入对象同时包含 AlanJeffSarahRyan 四个名字时返回 true,否则返回 false

挑战的初始代码(--seed--,见 587d7b7d367417b2b2512b1c.md#L96-L127)为:

let users = {
  Alan: {
    age: 27,
    online: true
  },
  Jeff: {
    age: 32,
    online: true
  },
  Sarah: {
    age: 48,
    online: true
  },
  Ryan: {
    age: 19,
    online: true
  }
};

function isEveryoneHere(userObj) {
  // Only change code below this line

  // Only change code above this line
}

console.log(isEveryoneHere(users));

注意数据形态:users 是一个"成员目录"对象,每个成员名(Alan/Jeff/Sarah/Ryan)是键,值则是携带 ageonline 的成员详情子对象。挑战考察的不是遍历详情,而是判定这些成员名是否作为键存在——这与上一道挑战(用 delete 删除属性,对应 587d7b7c367417b2b2512b1b.md 所在知识点)形成了"增、删、查"的闭环。

四、读懂自动判题:6 条 hints 分别约束什么

该挑战的 --hints--(见 587d7b7d367417b2b2512b1c.md#L24-L94)包含 6 条断言,是理解"这个函数到底要写成什么样"的最好说明书:

  1. 不得直接访问 users 对象 判题器在剥离代码注释后(通过 __helpers.removeJSComments)用正则统计 users 标识符出现次数,要求 <= 2。种子代码中 users 已出现于声明行与 console.log(isEveryoneHere(users)) 两处,因此你在 isEveryoneHere 内部不能再去引用全局 users,必须把逻辑建立在形参 userObj 之上——这同时保证了函数可复用、可接收任意对象。

  2. users 只应包含这四个键 断言依次检查 'Alan' in users'Jeff' in users'Sarah' in users'Ryan' in users,并且要求 Object.keys(users).length === 4。这是在锁定初始数据不被改动。

  3. 全量在场返回 true 调用 isEveryoneHere(users) 必须严格等于 true

4~7. 任一成员缺席即返回 false 四条断言分别在立即执行函数(IIFE)中执行 delete users.Alandelete users.Jeffdelete users.Sarahdelete users.Ryan,再断言 isEveryoneHere(users) === false。也就是说函数必须对缺失任意一个键的对象返回 false,只有四键齐全才放行。

综合来看,判题器强制的是:① 逻辑不能依赖全局 users;② 必须做"四键齐备"的与逻辑判定。它并不限定你用 hasOwnProperty() 还是 in,两种都能通过。

五、参考实现:官方解法与多种变体

官方在 --solutions--(见 587d7b7d367417b2b2512b1c.md#L129-L161)给出的解法是将名字放入数组,再用 Array.prototype.every 结合 hasOwnProperty() 逐个校验:

let users = {
  Alan: {
    age: 27,
    online: true
  },
  Jeff: {
    age: 32,
    online: true
  },
  Sarah: {
    age: 48,
    online: true
  },
  Ryan: {
    age: 19,
    online: true
  }
};

function isEveryoneHere(userObj) {
  return [
    'Alan',
    'Jeff',
    'Sarah',
    'Ryan'
  ].every(user => userObj.hasOwnProperty(user));
}

console.log(isEveryoneHere(users));

这一写法值得拆解:

  • 声明式而非命令式:无需手写 for 循环与计数标志,['Alan', 'Jeff', 'Sarah', 'Ryan'].every(callback) 只有在数组每一项都令回调返回真值时才返回 true,天然实现"全部在场"语义;
  • 只读不写:函数对传入对象零副作用,不修改、不删除任何键;
  • 面向形参:逻辑完全基于 userObj,任意形态的成员目录对象都可传入。

如果你更倾向于使用挑战正文讲解的 in 运算符,也可以这样写:

function isEveryoneHere(userObj) {
  return (
    'Alan' in userObj &&
    'Jeff' in userObj &&
    'Sarah' in userObj &&
    'Ryan' in userObj
  );
}

两种方案对本题都能通过全部 6 条断言。二者在本题数据下结果一致的原因在于:这些键都是字面量对象自己的属性,原型链不会额外提供 Alan 之类的键。不过正如第 2.2 节分析,一旦判定目标可能来自继承链,语义就会出现分歧,hasOwnProperty() 更保守、更贴近"数据键是否真实存在"的业务诉求,因此官方采用它作为推荐答案。

六、从挑战走向实战:边界情况与健壮写法

把这道练习题的能力迁移到真实项目中,值得记住几个边界陷阱:

6.1 hasOwnProperty 可能被遮蔽(shadowing)

如果对象自身带有一个名为 hasOwnProperty 的键,直接调用就会失效:

const data = { hasOwnProperty: 'hijacked', id: 1 };
data.hasOwnProperty('id'); // TypeError: data.hasOwnProperty is not a function

规避手段是从原型上借用方法:

Object.prototype.hasOwnProperty.call(data, 'id'); // true

或用 ES2022 的静态方法:

Object.hasOwn(data, 'id'); // true

6.2 无原型对象没有 hasOwnProperty

Object.create(null) 创建的对象不继承 Object.prototype,直接调用 obj.hasOwnProperty(...) 同样会抛错,此时 in 运算符或 Object.hasOwn() 是更稳妥的选择。

6.3 for...in 与自有属性的配合(下一道挑战的伏笔)

for...in 遍历对象时会把可枚举的继承属性也包含进来,因此经典模式是在循环体内用 hasOwnProperty() 过滤,只处理对象自身的键。这正是本挑战之后紧邻的下一道挑战「Iterate Through the Keys of an Object with a for...in Statement」(587d7b7d367417b2b2512b1d.md) 及后续「Generate an Array of All Object Keys with Object.keys()」要训练的能力。把"属性存在性判断"学扎实,是掌握"属性遍历与键收集"的前提。

6.4 命名建议

为函数形参取 userObj 而非 users 并非偶然——它既满足判题器"不直接访问全局 users"的约束,也清晰表达"传入的是任意一份成员目录"的契约,这正是第 4 节第 1 条 hint 想逼你养成的抽象习惯。

七、如何在本仓库中验证与复现

本挑战的完整素材全部落在 curriculum/challenges/english/blocks/basic-data-structures/587d7b7d367417b2b2512b1c.md 这一个文件内(正文、指令、判题、种子代码与官方解法五位一体),这意味着你可以直接在浏览器控制台或本地 Node 环境中:

  1. --seed-- 的代码粘贴为初稿;
  2. 将上文的 every + hasOwnProperty 实现填入 // Only change code below this line 与上方注释之间;
  3. 手动运行 console.log(isEveryoneHere(users)) 得到 true
  4. 再执行 delete users.Jeff; console.log(isEveryoneHere(users)) 验证得到 false,即可在本机复现全部断言语义。

如果想进一步理解整套课程工程如何组织这类挑战,建议顺藤摸瓜查看 curriculum/structure/blocks/basic-data-structures.json(该块 20 道挑战的顺序与 ID)、curriculum/schema/challenge-schema.js(YAML 前言的字段校验规则),以及 packages/shared/src/config/challenge-types.tschallengeType 数字到视图/提交方式的映射)。透过这道小题,你可以同时看清"一道 freeCodeCamp 挑战是如何被定义、校验、判题"的完整工程链路。

登录后查看全文
热门项目推荐
相关项目推荐