freeCodeCamp JavaScript 数据基础教程:用 hasOwnProperty() 与 in 运算符检测对象是否拥有某个属性
本文围绕 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」。整块挑战的 helpCategory 为 JavaScript,blockLayout 为 legacy-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.ts:const 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 plain 为 true,而 plain.hasOwnProperty('constructor') 为 false。这也解释了为什么许多"属性白名单"校验场景更偏爱 hasOwnProperty():它不会把原型链上的方法误判为对象真实携带的数据键。
补充:ES2022 起标准库提供了静态方法
Object.hasOwn(obj, prop),其语义与obj.hasOwnProperty(prop)一致,但即便对象是通过Object.create(null)创建、完全不带原型方法时也能安全调用。本挑战要求使用经典语法,但理解这一现代替代方案有助于编写更健壮的防御性代码。
三、挑战任务拆解:确认四位成员全部在场
--instructions--(见 587d7b7d367417b2b2512b1c.md#L20-L22)给出的任务很明确:完成 isEveryoneHere 函数,使其在传入对象同时包含 Alan、Jeff、Sarah、Ryan 四个名字时返回 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)是键,值则是携带 age 与 online 的成员详情子对象。挑战考察的不是遍历详情,而是判定这些成员名是否作为键存在——这与上一道挑战(用 delete 删除属性,对应 587d7b7c367417b2b2512b1b.md 所在知识点)形成了"增、删、查"的闭环。
四、读懂自动判题:6 条 hints 分别约束什么
该挑战的 --hints--(见 587d7b7d367417b2b2512b1c.md#L24-L94)包含 6 条断言,是理解"这个函数到底要写成什么样"的最好说明书:
-
不得直接访问
users对象 判题器在剥离代码注释后(通过__helpers.removeJSComments)用正则统计users标识符出现次数,要求<= 2。种子代码中users已出现于声明行与console.log(isEveryoneHere(users))两处,因此你在isEveryoneHere内部不能再去引用全局users,必须把逻辑建立在形参userObj之上——这同时保证了函数可复用、可接收任意对象。 -
users只应包含这四个键 断言依次检查'Alan' in users、'Jeff' in users、'Sarah' in users、'Ryan' in users,并且要求Object.keys(users).length === 4。这是在锁定初始数据不被改动。 -
全量在场返回
true调用isEveryoneHere(users)必须严格等于true。
4~7. 任一成员缺席即返回 false
四条断言分别在立即执行函数(IIFE)中执行 delete users.Alan、delete users.Jeff、delete users.Sarah、delete 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 环境中:
- 把
--seed--的代码粘贴为初稿; - 将上文的
every + hasOwnProperty实现填入// Only change code below this line与上方注释之间; - 手动运行
console.log(isEveryoneHere(users))得到true; - 再执行
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.ts(challengeType 数字到视图/提交方式的映射)。透过这道小题,你可以同时看清"一道 freeCodeCamp 挑战是如何被定义、校验、判题"的完整工程链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00