理解 JavaScript 的核心:基元和对象
JavaScript 是一种数据类型多样且灵活的语言,从根本上分为两类:基元和 对象。这种区别对于各个级别的开发人员来说都至关重要,因为它构成了 JavaScript 运行的基础。让我们重新审视这些概念以巩固我们的理解。
原始值:基础知识
- String :像
"hello"
和"farewell"
这样的文本数据封装在引号内,作为 JavaScript 中文本操作的基石。
- Number :无论是整数 (
-5
) 还是小数 (3.14
),数字都是该语言中数学运算的基石。
- BigInt :对于超出 Number 安全整数限制的计算,BigInt 发挥作用,可以对大整数进行精确算术。
- Boolean :布尔类型具有 true 和 false 值,对于逻辑运算和控制流至关重要。
- 未定义:未赋值的变量会自动赋予未定义值,表示在特定上下文中不存在值。
- Symbol :引入是为了确保唯一的属性键,Symbol 是不可变的且唯一的,非常适合向对象添加非冲突属性。
- Null :表示故意不存在任何对象值。与未定义不同,null 被显式分配以表示“无值”。
基元是不可变的,这意味着它们的值一旦创建就无法更改。这一特性常常会导致混乱,尤其是当我们将保存原始值的变量误认为该值本身时。
了解原语是不可变的有助于阐明 JavaScript 行为的许多方面,特别是在比较和赋值操作中。
在我们的 JavaScript 之旅中,虽然我们可能不会直接使用其中一些类型,但认识和理解它们的角色可以丰富我们的编码工具包,为更复杂、更高效的代码铺平道路。
除了原语领域之外,JavaScript 的世界是由对象主导的。此类别包含多种数据结构,其中一些可能会让您感到惊讶,例如数组。首先,我们遇到:
- 对象:由标准对象的
{}
或数组的[]
表示,这些结构是对相关数据和功能进行分组的支柱。
- 函数:表示为
x => x * 2
等,函数是 JavaScript 中的一等公民,允许将代码分配给变量、作为参数传递或从其他函数返回。
对象与基元有根本的不同;它们是可变的,可以在我们的代码中直接操作。一个常见的误解是将 JavaScript 中的所有内容视为对象。由于原始值的某些类似对象的行为,这在一定程度上是正确的。例如,表达式"hi".toUpperCase()
可能会引发问题:原始字符串如何具有方法?
这是通过一个称为“装箱”的过程发生的,其中 JavaScript 临时将原始值包装在对象包装器中以访问方法,仅在操作完成后丢弃这些对象。
这是 JavaScript 设计的一个令人着迷的方面,它允许基元从类似对象的方法中受益,而不必真正成为对象。当我们深入研究 JavaScript 的类型学时,理解这种区别至关重要。
探索 JavaScript 的typeof
运算符和null
的独特情况
区分 JavaScript 中的各种数据类型有时感觉有点神奇。输入typeof
运算符,它是 JavaScript 工具包中的一个强大工具,可以显示给定值的类型。下面是它在实践中的运作方式:
console.log(typeof(5)); // Outputs "number" console.log(typeof("hi")); // Outputs "string" console.log(typeof(undefined)); // Outputs "undefined" console.log(typeof({})); // Outputs "object" console.log(typeof([])); // Outputs "object" console.log(typeof(x => x * 2)); // Outputs "function"
然而,在 JavaScript 领域,并非一切都像看上去那样。以typeof
运算符对null
的处理为例。尽管有预期, typeof null
返回"object"
,这个结果让许多开发人员感到困惑。这种行为与其说是一个错误,不如说是该语言的一个怪癖,植根于 JavaScript 的早期设计决策。
值null
旨在表示故意不存在任何对象值,但typeof
将其分类为对象。由于对向后兼容性的担忧,这种怪癖是众所周知的,并且在 JavaScript 的整个发展过程中一直存在。
重要的是要记住,与表示尚未分配的值的undefined
不同, null
明确用于表示故意分配“无值”。虽然 JavaScript 并不强制区分null
和undefined
之间的用法,但对代码采用一致的方法可以帮助阐明您的意图,并有助于提高可读性和可维护性。
理解 JavaScript 中的表达式:与代码对话
在充满活力的 JavaScript 世界中,编写代码就像提出问题,语言会给出答案。这些交互是通过我们所说的表达式来捕获的。 JavaScript 中的表达式是解析为值的任何有效代码单元。
让我们看一个简单的例子:
console.log(5 + 5); // Outputs 10
在本例中, 5 + 5
是 JavaScript 计算结果为10
的表达式。
表达式是 JavaScript 代码的构建块,可在程序中实现动态交互和计算。它们可以像上面的示例一样简单,也可以更复杂。事实上,表达式是您与语言进行通信的直接途径,用于创建交互式动态 Web 应用程序。
在 JavaScript 中创建对象:从调用到垃圾回收
虽然 JavaScript 中的原始数据类型(例如字符串、数字和布尔值)作为预定义实体而存在,但对象的操作原理不同。每次我们使用{}
(大括号)时,我们不仅仅是引用现有的蓝图;而是引用现有的蓝图。我们正在创造一个全新的物体。考虑创建两个简单对象:
const cat = {}; const dog = {};
在这里dog
cat
是不同的物体,各自在记忆中拥有自己的空间。这一原则超越了单纯的对象文字,涵盖了 JavaScript 中的所有复杂数据结构,包括数组、日期和函数。
虽然创建这些实体的方法有多种,但使用{}
表示对象、使用[]
表示数组以及使用new Date()
表示日期是创建对象实例的最直接方法。
但是当这些对象不再需要时会发生什么?它们会无限期地保留在 JavaScript 世界中吗?答案在于 JavaScript 的垃圾收集机制——一个有效清理不再使用的内存的过程。
垃圾收集是一种自动操作,这意味着一旦代码中没有任何对对象的引用,对象就会被销毁,并回收其分配的内存。
比较 JavaScript 中的值:超越表面的平等
在 JavaScript 中,比较值有时感觉就像在迷宫中导航,有多种路径到达同一目的地:理解相等性。比较值的主要方法有以下三种:
严格相等(
===
):这种形式的相等是最精确的,它同时检查两个操作数的值和类型。这相当于问“这两个值的类型和内容是否相同?”
松散相等(
==
):比严格相等宽松,松散相等允许在比较之前进行类型强制。这就像问:“如果我们忽略这两个值的类型,是否可以认为它们是相同的?”
同值相等(
Object.is
):此方法与严格相等类似,但有一些关键差异,特别是在它如何处理特殊 JavaScript 情况方面。
让我们看看Object.is
实际效果:
console.log(Object.is(2, 2)); // true console.log(Object.is({}, {})); // false
为什么Object.is({}, {})
返回 false?由于每个对象字面量{}
在内存中创建一个唯一的对象,因此尽管结构相似,但Object.is
将它们视为不同的实体。
深入探讨严格平等的细微差别
虽然严格相等很简单,但它也有自己的一套特性,特别是对于某些 JavaScript 值:
-
NaN === NaN
:令人惊讶的是,此比较返回false
。在 JavaScript 中,NaN
(非数字)被认为与其自身不相等,这是一种罕见的特征,旨在表示未定义或错误计算的结果。
- 比较
-0
和0
:-0 === 0
和0 === -0
都返回true
,尽管-0
和0
在 JavaScript 的数字系统中是不同的值。这种等式忽略了零的符号,只关注它的大小。
实际影响
了解相等性检查中的这些差异对于编写精确且无错误的 JavaScript 代码至关重要。虽然===
和==
有其作用,但了解何时使用Object.is
至关重要,特别是对于涉及NaN
、 0
和-0
的边缘情况。
这些知识使开发人员能够就相等性检查做出明智的决策,确保他们的代码在各种场景中都按预期运行。
在 JavaScript 中导航对象属性和引用
当涉及到在 JavaScript 中操作对象属性时,您有两个主要工具: 点表示法和方括号表示法。这两种方法都提供了访问和修改对象内容的直接路径。这是一个快速入门:
- 点表示法:直接通过点运算符访问属性(例如,
object.key
)。
- 括号表示法:使用括号和字符串来指定属性名称(例如,
object['key']
)。
这些技术是与对象交互的基础。然而,需要掌握的一个关键方面是 JavaScript 如何处理对象引用。与原始数据类型不同,JavaScript 中的对象是引用类型,这意味着当您操作对象时,您正在使用对该对象在内存中的位置的引用,而不是对象本身的直接副本。
现实世界的例证:代码中的协作编写
为了将这个概念变为现实,让我们考虑这样一个场景:两位有抱负的作家艾米丽和托马斯合作写一本小说。他们决定使用 JavaScript 对象来构建故事的角色和设置:
const project = { title: "Adventures in Code", characters: { protagonist: { name: "Alex", traits: ["brave", "curious"] } }, setting: { location: "Virtual World", era: "future" } };
在他们发展故事的过程中,艾米丽引入了一个受主角启发但又具有独特转折的配角角色:
const sidekick = project.characters.protagonist; sidekick.name = "Sam"; sidekick.traits.push("loyal");
与此同时,托马斯决定扩展他们小说的背景:
const newSetting = project.setting; newSetting.location = "Cyber City"; newSetting.era = "2040";
乍一看,您可能想知道这些更改如何影响原始project
对象。结果如下:
- 主角的名字现在显示为“Sam”,他们的特质包括“忠诚”。这是因为
sidekick
不是一个新对象,而是对project.characters.protagonist
引用,修改sidekick
会直接影响原始project
对象。
- 小说的背景演变到了“2040年”的“网络城市”。与字符类似,
newSetting
是对project.setting
引用,这意味着对newSetting
的任何更改都会直接影响project.setting
。
了解参考文献的力量
此示例强调了 JavaScript 中的一个关键概念:使用对象意味着使用引用,并且当您将对象分配给变量时,您正在分配对该对象的引用。
您通过该引用所做的任何修改都会反映到对该对象的所有引用中。这种行为可以实现复杂的、互连的数据结构,但也需要仔细管理以避免意外的副作用。
在我们的故事中,Emily 和 Thomas 的协作过程完美地说明了对象引用如何为编码中的创造性工作服务,从而允许共享、动态地开发复杂的叙述,或者更实际地说,应用程序中的复杂数据结构。
掌握 JavaScript 中的对象复制:浅复制与深复制
在 JavaScript 中使用对象时,由于引用复制的性质,直接赋值可能会导致无意的修改。创建对象的副本可以在不影响原始对象的情况下进行安全操作;这样,我们将减少无意的修改。
根据您的需求和场景,您可以在浅复制和深复制之间进行选择。
浅复制技术:
Object.assign :此方法通过将属性从源对象复制到目标对象 (
{}
) 来生成新对象。需要注意的是,Object.assign
执行浅复制,这意味着任何嵌套对象或数组都是通过引用复制的,而不是通过它们的值复制的。
const original = { a: 1, b: { c: 2 } }; const copy = Object.assign({}, original); copy.bc = 3; // Affects both 'copy' and 'original'
Spread 运算符(
...
):与Object.assign
类似,spread 运算符将原始对象的属性扩展为新对象,从而产生浅拷贝。
const copyUsingSpread = { ...original }; copyUsingSpread.bc = 4; // Also affects the 'original' object
深复制方法:
JSON.parse和JSON.stringify :此方法将对象序列化为 JSON 字符串,然后将其解析回新对象。它有效地创建了深层副本,但无法处理函数、日期对象、未定义和其他不可序列化的值。
const deepCopy = JSON.parse(JSON.stringify(original)); deepCopy.bc = 5; // Does not affect the 'original' object
库:对于更复杂的场景,Lodash 等库提供了可以深度克隆对象的函数(例如
_.cloneDeep()
),包括比 JSON 方法更有效地处理各种数据类型。
实际应用
让我们回顾一下我们的协作写作项目示例:
const project = { title: "Adventures in Code", characters: { protagonist: { name: "Alex", traits: ["brave", "curious"] } }, setting: { location: "Virtual World", era: "future" } };
要修改项目而不影响原始项目:
- 对于深层复制:使用
JSON.parse(JSON.stringify(project))
安全地添加新字符或更改设置。
- 对于浅拷贝:使用
Object.assign
或展开运算符进行顶层更改,其中嵌套结构不是问题。
在浅复制和深复制之间进行选择取决于对象的复杂性和操作的具体要求。浅拷贝速度更快,更适合简单的对象,而深拷贝对于具有嵌套结构的对象是必需的,以确保原始对象保持不变。
通过理解和应用这些应对技术,您可以自信地浏览 JavaScript 基于引用的系统,确保您的数据操作精确且有意。
原型:JavaScript 中隐藏的继承线程
正如小说中的角色继承祖先的特征一样,JavaScript 中的对象也继承其原型的属性和方法。这个概念反映了艾米丽和托马斯在他们的小说《代码冒险》中探索的讲故事技巧。
为了加深我们对原型的理解,让我们继续他们的故事,引入一个反映 JavaScript 中继承模型的新角色弧。
在他们的小说世界里,存在着一位被称为“古代编码员”的传奇抄写员,以智慧和精通语言而闻名。艾米丽和托马斯决定以这个神话人物为基础创造一个新角色“Coder Leo”,代表下一代程序员。
// The Ancient Coder, known for his profound wisdom const ancientCoder = { wisdom: 100 }; // Coder Leo, a young scribe in training const coderLeo = { __proto__: ancientCoder, age: 15 };
在这个叙述中,编码员利奥通过被称为“原型链”的神奇遗产与古代编码员直接联系在一起。这种联系让利奥能够汲取他祖先的智慧。
console.log(coderLeo.wisdom); // 100
得益于原型链,Coder Leo 尽管年纪轻轻,却能接触到古代程序员的智慧。但是,当他们遇到古代编码员不具备的挑战或特征时会发生什么?
console.log(coderLeo.courage); // undefined
这种情况说明了 JavaScript 原型系统的一个关键原理:如果在对象上找不到属性,JavaScript 将查找原型链来查找它。如果仍未找到该属性,则返回undefined
,表明不存在该特征。
为了进一步叙述,艾米丽和托马斯探索了如何为后代添加独特的特征,使他们与祖先区分开来:
// Introducing a unique trait to Coder Leo coderLeo.courage = 50; console.log(ancientCoder.courage); // undefined console.log(coderLeo.courage); // 50
在这里,Coder Leo 发展出了一种与古代 Coder 不同的勇气特质。此更改不会改变古代编码器的属性,说明了由于 JavaScript 的动态特性,对象(或角色)如何能够独立于其原型(或祖先)而进化。
故事中的这个故事不仅推进了 Emily 和 Thomas 的小说,而且还阐明了 JavaScript 基于原型的继承。就像小说中的人物一样,物体可以继承祖先的特征。然而,他们也有能力开辟自己的道路,开发反映他们个人旅程的独特财产。
揭开魔力:原型和内置方法的力量
当艾米丽和托马斯深入研究他们的小说《代码冒险》时,他们偶然发现了一个神秘的章节:古老的普罗托斯之书。他们发现,这本书不仅仅是一个情节设计,而且是理解 JavaScript 中的原型和内置方法的隐喻,这些概念将为他们的故事世界和他们对编码的理解增添一层魔力。
在他们小说的虚构背景 Scriptsville 中,每个角色和物体都充满了来自普罗托斯之书的能力。这本神奇的书是所有知识和技能的源泉,类似于JavaScript中的原型,对象从中继承属性和方法。
// A seemingly ordinary quill in Scriptsville const quill = {};
艾米丽通过她的角色艾莉探索了这支羽毛笔,却发现它通过一条神奇的线索与《普罗托斯之书》相连——这直接类比 JavaScript 对象中的__proto__
属性,将它们连接到它们的原型。
console.log(quill.__proto__); // Reveals the Tome's ancient scripts!
这一启示使 Ellie 能够接触到《Tome》的智慧,包括调用hasOwnProperty
和toString
的能力,展示了 JavaScript 继承自 Object 原型的内置方法。
然后叙述介绍了托马斯的角色多诺万大师,他是一位著名的面包师,以其迷人的甜甜圈而闻名。为了分享他的烹饪天赋,多诺万创造了一个咒语,就像在 JavaScript 中定义构造函数一样:
function EnchantedDoughnut() { this.flavor = "magic"; } EnchantedDoughnut.prototype.eat = function() { console.log("Tastes like enchantment!"); };
多诺万制作的每一个甜甜圈都带有魔力的精髓,让任何吃过它们的人都能体验到魔法。故事的这一部分说明了在 JavaScript 中使用构造函数创建的对象如何从其构造函数原型继承方法,就像多诺万的甜甜圈继承了被吃的能力一样。
随着 Scriptsville 的发展,它的魔力也在不断发展,从古老的咒语过渡到现代的类语法艺术。 Emily 和 Thomas 使用新语法重新构想了 Donovan 的技艺,使他的魔法更容易理解,并与 JavaScript 的当代实践保持一致:
class ModernEnchantedDoughnut { constructor() { this.flavor = "modern magic"; } eat() { console.log("Tastes like modern enchantment!"); } }
这种转变不仅更新了 Donovan 的烘焙艺术,还反映了 JavaScript 的演变,突出了类语法的优雅和高效,同时保留了底层基于原型的继承。
我们从艾米丽和托马斯身上学到了什么?
Emily 和 Thomas 创作《代码历险记》的旅程成为理解原型、内置方法和 JavaScript 本身演变的迷人寓言。
通过他们的角色和故事,他们以一种引人入胜且深刻的方式阐明了复杂的概念,表明每个对象和角色,就像每个 JavaScript 对象一样,都是一个更大的、相互关联的继承和创新挂毯的一部分。
他们的故事强调了讲故事和编程的一个基本真理:了解过去对于掌握现在和创新未来至关重要。
Scriptsville 的魔法世界拥有古老的书籍、魔法甜甜圈和现代魔法,为探索 JavaScript 原型系统的深度和继承的力量提供了生动的背景。
Scriptsville 的神奇实用工具:迷人的物体并确保和谐
当艾米丽和托马斯深入研究他们的小说《代码历险记》时,他们发现需要一种方法来管理充满角色、场景和魔法物品的复杂世界。
他们求助于艾莉,她是来自斯克茨维尔的一位聪明的抄写员,她以在迷人物品和确保整个土地和谐方面的专业知识而闻名。
路径的魅力:确保嵌套现实
艾莉与他们分享了一个神奇的公式, ensureObjectPath
,能够确保嵌套领域在他们的世界中的存在:
function ensureObjectPath({obj, path}) { path.split('.').reduce((acc, part) => { if (!acc[part]) acc[part] = {}; return acc[part]; }, obj); return obj; } // Ensuring the path to a hidden forest in their novel const world = {}; ensureObjectPath({obj: world, path: 'hidden.forest.clearing'}); console.log(world); // Outputs: { hidden: { forest: { clearing: {} } } }
艾莉解释说,这种魔力使他们能够在小说的宇宙中创造任何地点,确保每个角色都可以开始他们的任务,而不必担心冒险进入不存在的领域。
所需元素卷轴:确保角色的必需品
此外,艾莉向他们介绍了另一个咒语checkForRequiredKeys
,旨在确保每个角色都拥有旅程所需的基本属性:
const REQUIRED_KEYS = ['age', 'address', 'gender']; function checkForRequiredKeys(obj) { REQUIRED_KEYS.forEach(key => { if (!Object.hasOwn(obj, key)) { obj[key] = {}; } }); } // Ensuring every character has the essential attributes const character = { name: "Ellie" }; checkForRequiredKeys(character); console.log(character); // Outputs: { name: "Ellie", age: {}, address: {}, gender: {} }
这个咒语使艾米丽和托马斯能够将复杂性融入到他们的角色中,确保任何细节都不会被忽视,无论他们的叙述变得多么复杂。
Scriptsville 的回响:从迷恋到启蒙
随着故事的展开,Ellie 分享的魔法不仅丰富了“代码历险记”,而且阐明了 JavaScript 中对象操作和结构的基本原理。
正如ensureObjectPath
咒语允许创建嵌套现实一样,JavaScript 开发人员也可以使用类似的功能在应用程序中构建数据结构。
同样, checkForRequiredKeys
拼写反映了确保数据完整性所必需的防御性编程实践。
结论
在我们徜徉在 Scriptsville 的迷人国度中,Emily、Thomas、Ellie 和一系列神奇的生物一直陪伴着我们,他们以与这片土地本身一样迷人的方式揭开了 JavaScript 的秘密。
通过冒险和发现的故事,我们深入研究了 JavaScript 的核心,从最简单的语法到在其表面下运行的复杂引擎。
这是一次与众不同的旅程,讲故事的魔力与编程逻辑融为一体,揭示了 JavaScript 宇宙的分层奇迹。
- 原始类型与对象类型:我们首先区分 JavaScript 的原始类型和对象类型,揭示这些构建块如何构成该语言的基础。通过角色的探索,我们理解了基元的不可变本质以及对象的动态、基于引用的本质。
- 创建和比较对象:我们学习了对象创建、比较值以及 JavaScript 中相等性的细微差别。 Scriptsville 中的魔法文物和咒语阐明了理解严格相等 (
===
)、松散相等 (==
) 以及使用Object.is
进行细致比较的重要性。
对象属性和原型:深入研究对象属性,我们发现了点和括号表示法用于访问和修改属性的强大功能,以及原型在对象继承中的关键作用。
Scriptsville 中的继承线和魔法卷轴的故事使原型链栩栩如生,展示了对象如何继承和覆盖特征。
- 对象的实用函数:最后,我们探索了操作对象结构并确保数据完整性的实用函数。 Ellie 分享的神奇公式(例如
ensureObjectPath
和checkForRequiredKeys
)演示了处理嵌套对象并确保基本属性存在的实用技术。
通过 Scriptsville 的叙述,我们看到了 JavaScript 的功能如何既神奇又合乎逻辑,为开发人员提供了一个发挥创造力和解决问题的广阔游乐场。
Scriptsville 的迷人故事不仅仅是故事;更是故事。它们是编程艺术的隐喻,强调了理解 JavaScript 核心原则对于巧妙地打造我们的数字世界的重要性。
当我们结束这段旅程的书时,请记住,冒险并没有就此结束。我们探索的每个概念都是更深入理解和掌握 JavaScript 的垫脚石。
正如艾米丽和托马斯编织他们的“代码冒险”故事一样,你也可以在无边的编程宇宙中创造你的叙述、迷人的物体和神奇的领域。