研究源代码无疑可以改变你的开发生涯轨迹。即使只深入研究一个层面,你也能与大多数普通开发人员区分开来。
这是迈向精通的第一步!
以下是我的故事:在我目前在一家 AI/ML 初创公司工作时,团队无法弄清楚如何将 Neo4j 数据从服务器传输到前端进行可视化,他们在 12 小时内完成了演示。我是以自由职业者的身份加入的,你可以清楚地看到他们的恐慌。问题是 Neo4j 返回的数据不符合可视化工具neo4jd3所期望的正确格式。
想象一下:Neo4jd3 期望一个三角形,而 Neo4j 返回一个正方形。这就是不兼容的不匹配!
neo4jd3
我们可能很快就会在Mastered中使用 JavaScript 和 Neo4j 进行图形数据科学!这张图片令人怀旧。
只有两个选择:重做整个 Neo4j 后端或研究 Neo4jd3 的源代码,弄清楚预期的格式,然后创建一个适配器将正方形转换为三角形。
neo4jd3 <- adapter <- Neo4j
适配器即
我的大脑默认阅读源代码,并创建了一个适配器: neo4jd3-ts 。
import createNeoChart, { NeoDatatoChartData } from "neo4jd3-ts";
适配器是NeoDatatoChartData
,其他一切都已成为历史。我牢记这一教训,每次有机会,我都会降低使用每个工具的级别。它已经变得如此普遍,有时我甚至不阅读文档。
这种方法极大地改变了我的职业生涯。我做的每件事都像变魔术一样。几个月后,我就开始领导关键的服务器迁移和项目,这一切都是因为我向源头迈出了一步。
本系列的主题是:不要满足于 API,而是要超越 API,学习重新创建这些工具。在这个人工智能炒作的世界里,打破平庸是让开发人员变得超越平庸的关键!
我计划通过这个系列来研究流行的 JavaScript 库和工具,一次一个工具地一起弄清楚它们的工作原理以及我们可以从中学到什么模式。
由于我主要是一名后端工程师(是的,全栈,但 90% 的时间都在处理后端),所以没有比 Express.js 更好的入门工具了。
我认为您有编程经验,并且很好地掌握了编程基础知识!您可能被归类为高级初学者。
在教授基础知识的同时尝试学习/教授源代码将非常困难和乏味。你可以加入这个系列,但要预料到它会很难。我无法涵盖所有内容,但我会尽力而为。
本文之所以选择在 Express 之前发表,是因为我决定介绍 Express 所依赖的一个非常小的库, merge-descriptors ,在我撰写本文时,它的下载量为 27,181,495 次,并且仅有 26 行代码。
这将使我们有机会建立一个结构,并允许我介绍构建 JavaScript 模块至关重要的对象基础知识。
设置
在我们继续之前,请确保您的系统中有 Express源代码和合并描述符。这样,您可以在 IDE 中打开它,我可以通过行号指导您查看我们正在寻找的位置。
Express 是一个功能强大的库。在介绍其他工具之前,我们将在几篇文章中尽可能多地介绍它。
在 IDE 中打开 Express 源,最好带有行号,导航到lib
文件夹,然后打开express.js
文件(入口文件)。
第 17 行,这是我们的第一个库:
var mixin = require('merge-descriptors');
用法在第42和43行:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
在探索这里发生的事情之前,我们需要退一步,讨论 JavaScript 中的对象,而不仅仅是数据结构。我们将讨论组合、继承、原型和混合——本文的标题。
JavaScript 对象
关闭 Express 源代码,并在某处创建一个新文件夹,以便在我们学习这些关键对象基础知识时跟随。
对象是数据和行为的封装,是面向对象编程 (OOP)的核心。有趣的事实:JavaScript 中的几乎所有东西都是对象。
const person = { // data name: "Jane", age: 0, // behavior grow(){ this.age += 1; } };
person
对象中左括号和右括号之间的所有内容都称为对象自身属性。这很重要。
自身属性是直接存在于对象上的属性。name、 name
grow
age
person
的自身属性。
这很重要,因为每个 JavaScript 对象都有一个prototype
属性。让我们将上述对象编码为函数蓝图,以便我们能够动态创建person
对象。
function createNewPerson(name, age){ this.name = name; this.age = age; } createNewPerson.prototype.print = function(){ console.log(`${this.name} is ${this.age}`); }; const john = new createNewPerson("John", 32);
原型是 JavaScript 对象从其他对象继承属性和方法的方式。访问对象上的属性时Own Properties
和Prototype
之间的区别在于:
john.name; // access
JavaScript 会首先查找Own Properties
,因为它们具有较高的优先级。如果找不到该属性,它会递归查找对象自己的prototype
对象,直到找到 null 并抛出错误。
原型对象可以通过其自身的原型继承另一个对象。这称为原型链。
console.log(john.hasOwnProperty('name')); // true console.log(john.hasOwnProperty('print')); // false, it's in the prototype
但是, print
对john
有效:
john.print(); // "John is 32"
这就是 JavaScript 被定义为基于原型的语言的原因。除了添加属性和方法(例如继承),我们还可以借助原型做更多的事情。
继承的“hello world”是mama对象。让我们用JavaScript重新创建它。
// our Mammal blueprint function Mammal(name) { this.name = name; } Mammal.prototype.breathe = function() { console.log(`${this.name} is breathing.`); };
在 JavaScript 中, Object
对象内部有一个静态函数:
Object.create();
它类似于{}
和new functionBlueprint
创建一个对象,但不同之处在于create
可以将原型作为要继承的参数。
// we use a cat blueprint function here (implemented below) Cat.prototype = Object.create(Mammal.prototype); // correction after we inherited all the properties Cat.prototype.constructor = Cat;
现在Cat
将拥有在Mammal
中找到的breathe
方法,但重要的是要知道Cat
指向Mammal
作为它的原型。
解释:
-
哺乳动物蓝图:我们首先定义
Mammal
函数并在其原型中添加breathe
方法。 -
Cat 继承:我们创建
Cat
函数并将Cat.prototype
设置为Object.create(Mammal.prototype)
。这使得Cat
原型继承自Mammal
,但它将constructor
指针更改为Mammal
。 -
修正构造函数:我们修正了
Cat.prototype.constructor
,使其指回Cat
,确保Cat
对象在从Mammal
继承方法时保留其身份。最后,我们为Cat
添加了meow
方法。
这种方法允许Cat
对象访问来自Mammal
方法(例如breathe
)以及它自己的原型(例如meow
)的方法。
我们需要纠正这个问题。让我们创建完整的示例:
function Cat(name, breed) { this.name = name; this.breed = breed; } Cat.prototype = Object.create(Mammal.prototype); // cat prototype pointing to mammal // correction after we inherited all the properties Cat.prototype.constructor = Cat; // we are re-pointing a pointer, the inherited properties are still there Cat.prototype.meow = function() { console.log(`${this.name} is meowing.`); };
要理解Cat.prototype.constructor = Cat
,您需要了解指针。当我们使用Object.create
从Mammal
继承时,它会将Cat
原型的指针更改为Mammal
,这是错误的。尽管有父级Mammal
,但我们仍然希望Cat
是自己的个体。
这就是我们必须纠正它的原因。
在此示例中, Cat
使用原型链从Mammal
继承。Cat Cat
可以访问breathe
和meow
方法。
const myCat = new Cat("Misty", "Ragdoll"); myCat.breathe(); // Misty is breathing. myCat.meow(); // Misty is meowing.
我们可以创造一只也从哺乳动物继承的狗:
function Dog(name, breed) { this.name = name; this.breed = breed; } Dog.prototype = Object.create(Mammal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { console.log(`${this.name} is barking.`); }; const myDog = new Dog('Buddy', 'Golden Retriever'); myDog.breathe(); // Buddy is breathing. myDog.bark(); // Buddy is barking.
我们已经创建了基本的经典继承,但这为什么重要?我以为我们正在讨论源代码!
是的,但原型是构建高效灵活模块的核心,超越了继承。即使是简单、编写良好的模块也充斥着原型对象。我们只是奠定了基础。
继承的替代方法是对象组合,它松散地获取两个或多个对象并将它们合并在一起以形成一个“超级”对象。
Mixins 允许对象借用其他对象的方法,而无需使用继承。它们对于在不相关的对象之间共享行为非常有用。
这就是我们的第一次探索所做的事情:我们承诺首先介绍merge-descriptors
库。
合并描述符模块
我们已经了解了它在 Express 中的用途和用法。现在我们知道了它用于对象组合。
在第 17 行,这是我们的第一个库:
var mixin = require('merge-descriptors');
用法在第42和43行:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
根据我们已知的信息,我们已经可以推断出mixin
将EventEmitter.prototype
和proto
组合成一个名为app
的对象。
当我们开始谈论 Express 时,我们就会谈到app
。
这是merge-descriptors
的完整源代码:
'use strict'; function mergeDescriptors(destination, source, overwrite = true) { if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); } for (const name of Object.getOwnPropertyNames(source)) { if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; } // Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor); } return destination; } module.exports = mergeDescriptors;
从一开始,始终查看函数的使用方式及其所采用的参数:
// definition mergeDescriptors(destination, source, overwrite = true) // usage var mixin = require('merge-descriptors'); mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
App
是我们的目的地。我们知道mixin
表示对象组合。大致来说,这个包的作用是将源对象组合到目标对象中,并提供覆盖选项。
假设,如果app
(目标)具有源所具有的确切属性,则true
,否则保持不变并跳过。
我们知道对象不能拥有两次相同的属性。在键值对(对象)中,键应该是唯一的。
使用 Express 时,覆盖为false
。
以下是基本管理工作,始终处理预期的错误:
if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); }
这是有趣的地方:第 12 行。
for (const name of Object.getOwnPropertyNames(source)) {
从上面我们知道了OwnProperty
是什么意思,那么getOwnPropertyNames
显然就是获取自己属性的键。
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]
它将键作为数组返回,我们在以下实例中循环遍历这些键:
for (const name of Object.getOwnPropertyNames(source)) {
接下来检查目标和源是否具有我们当前正在循环的相同密钥:
if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; }
如果 overwrite 为 false,则跳过该属性;不覆盖。这就是continue
所做的 — — 它将循环推进到下一次迭代,并且不运行下面的代码,即以下代码:
// Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor);
我们已经知道getOwnProperty
是什么意思了。新词是descriptor
。让我们在自己的person
对象上测试一下这个函数:
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyDescriptor(person, "grow"); // { // value: [Function: grow], // writable: true, // enumerable: true, // configurable: true // }
它将我们的grow
函数作为值返回,下一行是不言自明的:
Object.defineProperty(destination, name, descriptor);
它从源中取出我们的描述符并将其写入目标。它将源自身的属性复制到我们的目标对象作为其自身的属性。
让我们在我们的person
对象中举一个例子:
const val = { value: function isAlien() { return false; }, enumerable: true, writable: true, configurable: true, }; Object.defineProperty(person, "isAlien", val);
现在person
应该已经定义了isAlien
属性。
总之,这个高度下载的模块将自己的属性从源对象复制到目标,并可以选择覆盖。
我们已经成功覆盖了该源代码层中的第一个模块,未来还会有更多令人兴奋的内容。
这是介绍。我们首先介绍了理解模块所需的基础知识,并顺便了解了大多数模块中的模式,即对象组合和继承。最后,我们浏览了merge-descriptors
模块。
这种模式在大多数文章中都很常见。如果我觉得有必要介绍基础知识,我们将在第一部分中介绍它们,然后再介绍源代码。
幸运的是,Express 中使用了merge-descriptors
,这是我们开始源代码研究的重点。因此,在我们觉得 Express 运行得足够好之前,请期待更多 Express.js 源代码文章,然后再切换到其他模块或工具,例如 Node.js。
与此同时,您可以做的挑战是导航到合并描述符中的测试文件,阅读整个文件。自己阅读源代码很重要,尝试弄清楚它的作用和测试,然后打破它,是的,再次修复它或添加更多测试!