JavaScript 函数的命名参数,第二部分:走向结构化






4.98/5 (11投票s)
众所周知的“传递对象”方法,略有不同
1 引言
这篇短文是为了延续我上一篇关于命名参数的文章,从一个非常不同的角度来看待这个问题。让我们回到我在2. 一些好主意一节中描述的非常基本且众所周知的想法:传递一个对象。我描述了这种方法的缺点,并提出了一种替代方案。
从这一点来看,让我们明确一点:在 JavaScript 中,没有通过名称传递函数参数的机制,我们只能设计一些作为语法糖的机制,旨在达到按名称传递的一些目标;正式的 JavaScript 参数传递机制要么根本不使用,就像我上一篇文章中那样,要么参数按位置传递。在本文中,我将只关注一个参数,一个实际的 JavaScript 函数位置参数,它是一个结构化对象。它的属性承载着参数的语义。
如果我们思考为什么使用这种机制,我们会注意到按名称对应为我们提供了实际参数和形式参数之间的松散耦合:属性可以以任何顺序出现并且可以缺失。所以,这里最大的重点是使用缺失的参数,或者说默认值问题。有趣的是,对于非对象属性(字符串、数字、null
和undefined
),默认值问题看起来相当简单:缺失的属性作为undefined
传递。这可以在函数的实现中检查。如果一个或多个属性也是对象类型,那就不是那么简单了。然后我们可能会得到任意深度嵌套的层次结构;一个不那么简单的事实是,可以构建具有通用拓扑的对象图,而不仅仅是树状结构。
为了感受这个问题,最好的方法可能是考虑一些用例。
2 用例
比方说,我们开发一些要在 Web 或浏览器应用程序中使用的 UI 组件。当我们开始开发时,我们定义了一套合理的 UI 样式和其他属性,使其看起来相当不错,同时考虑到以后用户可以自定义这套样式。这是一个有代表性的例子
function createControl() {
var defaultOptionSet = {
behavior: {
selectable: true,
editable: true,
shiftEditPosition: false,
RTL: false
}, //behavior
colors: {
activeCell: { foreground: "Yellow", background: "Navy"},
inactiveCell: { foreground: "Black", background: "Silver"},
unselectedCell: { foreground: "Black", background: "White"}
}, //colors
styles: {
cell: "border: solid thin black",
table: "border-spacing: 0; padding: 0; outline-offset: -4px",
superscript: "font-size:70%"
} // styles
};
var options = defaultOptionSet;
// use options here
}
请看代码示例“demoA-structured-option-set.html”。
自然地,我们计划将选项作为用户提供的参数传递。然后我们将删除“var option = …
”这一行,因为它将由用户提供。但是defaultOptionSet
怎么办呢?我们不想将其赋值给options
,因为用户想要更改一些选项,但我们又不想完全删除它,因为在这种情况下,用户如何知道要提供哪些属性呢?通过扫描实现代码吗?或者我们应该将defaultOptionSet
以注释掉的形式保留,以便为用户提供某种文档。这很傻,不是吗?
但我们最好承认,我们面临的是一个真正的问题,而不是一个意外复杂性问题。我们真正想要的是:我们想同时使用两者。粗略地说,我们希望将defaultOptionSet
深度克隆到options
,但以某种特殊方式:我们期望options
的某些属性得到保留。哪些属性?这个问题乍看起来很简单,但要精确表述它应该是什么却不容易。这些属性的名称应该与defaultOptionSet
中的某些属性匹配,但不是所有属性。相同的属性名可以多次出现在结构中的不同位置。但这又意味着什么,“相同位置”?等等……比如说,我们可以定义以下选项值
{ colors: { activeCell: { foreground: "LightGoldenRodYellow" } } }
看看defaultOptionSet
结构。难道不明显吗?值为“Yellow”的属性应该将其值更改为“LightGoldenRodYellow”;所有其他属性都应该与defaultOptionSet
的深度克隆所拥有的相同。这正是我们真正想要的。看看本文顶部的图片——它应该能很好地说明问题。
无论如何,经过一番思考,很明显这不是克隆。再思考一下可能会得出结论,这也不是合并。然而……
3 深度克隆
实际上,本节是附带的,简要讨论 JavaScript 中的深度克隆。出于好奇,我决定看看开发人员在需要深度克隆时会建议怎么做。我真的很惊讶。我发现了一些代码示例,其中属性是分层地在目标对象中递归创建的。至少有一篇文章提出了一个很棒的建议:使用 AJAX 序列化一个对象,然后将其反序列化到一个全新的对象;当然,这应该可行。另一种方法是使用eval
。好吧,开发人员的想象力是无限的。
同时,很容易发现对象的深度克隆就像这样简单
var clone = Object.create(sourceObject);
请参阅https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
知道这些真好,不是吗?:-)
4 解决方案
这是完整的解决方案
var constants = { objectType: typeof {} };
function populateWithDefault(value, defaultValue) {
if (!defaultValue) return;
if (!value) return;
if (typeof defaultValue == constants.objectType && typeof value == constants.objectType) {
for (var index in defaultValue)
if (!(index in value))
value[index] = defaultValue[index];
else
populateWithDefault(value[index], defaultValue[index]);
} else
value = defaultValue;
} //populateWithDefault
请注意,value
和defaultValue
的角色不是对称的。这不是合并,也不是克隆。目标对象是value
;defaultValue
保持不变,但这个事实并不重要。理解它的关键是:如果value
树结构中的某个属性不是undefined
,它就会被保留,无论它在结构中的哪个位置,重要的是这个值永远不会被defaultValue
中的任何数据覆盖。如果你仔细想想,你会发现:这正是我们真正需要的。
5 用法
让我们看看它在其他工作示例上是如何工作的。首先,在我上一篇文章的4. 在 JavaScript Playground 上测试代码示例部分,我解释了如何使用微平台进行 JavaScript 执行、开发和代码示例操作演示。
代码示例“demoB-structured-argument.html”演示了如何使用对象值,该值配置为仅修改层次结构第三层中的一个颜色属性(foreground
)。填充后两个对象的转储都显示出来了。
代码示例“demoB-structured-argument.html”演示了 null 属性与 undefined 有很大不同。Null 将被真正视为任何其他已定义属性,也就是说,它将被保留,而不会被defaultValue
中取出的同源值覆盖。
如果我们的value
是{ colors: null }
,则结果属性colors
变为null
。
6 如果对象图不是树形结构怎么办?
快速回答:没问题!
我在引言部分提到,可以创建一个非树形的对象图。根据定义,树是没有循环的图。换句话说,这是一个对象的层次结构。我们可以通过添加额外的引用来打破层次结构,这些引用可以形成一个循环。这不是一件很明显的事情,所以我将通过一些简单的例子来解释如何在 JavaScript 中实现这种功能,基于我们用例中的结构(省略了一些属性)
var defaultOptionSet = {
behavior: { /* ... */ },
colors: { /* ... */ },
// ...
badProperty: undefined
};
defaultOptionSet.badProperty = defaultOptionSet.colors;
由于一些非常合理的 JavaScript 语法限制,只能分两步生成,所以上面显示的代码示例中有两个语句。这种结构在粗心或经验不足的人手中是相当危险的。它们臭名昭著地容易破坏递归代码的执行。当然,这种问题可以而且应该得到解决。举个例子,看看演示代码中提供的 JavaScript Playground 的 JavaScript 文件。找到Object.prototype.dump
的代码和一个局部对象recursionBreaker
。顾名思义,它有助于打破“无限”递归。我想你可以通过查看这个函数来理解。但是populateWithDefault
呢?
让我们冒点风险,执行我们的递归代码
var value = { };
populateWithDefault(value, defaultOptionSet);
为了简单起见,我从一个空项目开始,它当然应该生成defaultOptionSet
的深层克隆。它会陷入无限递归吗?令人惊讶的是,不会。为什么?这并没有发生,因为有!(index in value)
的检查,这与帮助保留目标value
的现有属性的检查完全相同。
请参见代码示例“demoD-not-a-tree-object-graph.html”。
7 结论
我曾花时间根据几个项目得出结论,我描述的简单技术可以发挥非常重要的作用。它不仅提供了选项和其他传递给函数的数据的自定义功能,还提供了一种非常自然的结构化项目开发工作流。