这里是一个(比我最初预期的)现象描述,从更严谨的角度来看。 为什么更严谨? 我想调查这个问题,因为我不确定是否存在关于函数默认参数的特殊规则,或者是否有一些关于解构的基本知识我没有理解。结果证明是后者。
我将使用伪语法来描述我的发现,这与您在ECMA-262中看到的情况有些相似。那是我的唯一参考资料。
关键点:
有解构赋值和解构绑定模式。两者的目的都是引入名称并分配值。
解构赋值:
ObjectAssignmentPattern: '{' AssignmentPropertyList '}' = AssignmentExpression
AssignmentPropertyList: AssignmentProperty [',' AssignmentProperty]
这两个只是说明了解构赋值的一般形式。
AssignmentProperty:IdentifierReference [Initializer]
这是LHS中名称的“默认值”。
赋值属性: 属性名 ':' 赋值元素
赋值元素: 左手表达式 [初始化器]
这使得析构嵌套递归,但语义需要被定义。
语义
如果你看一下DestructuringAssignmentEvaluation, 就可以看到谁被指派给了什么。 ObjectAssignmentPattern 不是很有趣,它给出了LHS的基本的'{' assignments '}'
结构,更有意思的是12.15.5.3,PropertyDestructuringAssignmentEvaluation。这展示了当你实际分配默认值时以及绑定更深层次嵌套名称时会发生什么。
赋值属性: 标识符引用 [初始化器]
算法中的第三步非常重要,调用了GetV
。在这个调用中,它试图从RHS的value 中获取当前正在被赋值的名称(LHS)的值。这可能会抛出异常,这就是为什么以下代码片段会抛出异常的原因:
y = Object.defineProperty({},'foo',{get: () => {throw new Error("get foo");}})
{foo} = y;
下一步,第四步,只有在存在初始化程序且RHS获取的
值未定义时才会评估初始化程序。例如:
y = Object.defineProperty({},'foo',{get: () => undefined})
{foo = 3} = y
请注意,这一步骤以及实际将值“放置”到需要的位置的步骤都可能会抛出异常。下一个项目更加棘手,这是混淆最容易发生的地方:
AssignmentProperty: PropertyName ':' AssignmentElement
本语义是将问题推迟到KeyedDestructuringAssignmentEvaluation,传递
PropertyName和当前值(RHS)。以下是其运行时语义的标题:
AssignmentElement:DestructuringAssignmentTarget [Initializer]
随后算法的步骤有些熟悉,但也有一些意外和间接性。在此算法中,几乎任何步骤都可能会引发异常,因此不会明确指出。第1步是另一个“递归基础”,表示如果目标不是对象或数组文字(例如只是标识符),则让
lref成为该目标(注意,它不必是标识符,只需是可分配的东西,例如。
w = {}
{x:w.u = 7} = {x:3} // w == {u:3}
然后,使用GetV尝试检索“目标值”value.propertyName
,
如果该值未定义,则尝试获取初始化程序值,在第6步将其放入lref中。
第5步是递归调用,剥离出尽可能多的层以实现解构赋值的基本情况。
以下是我认为可以说明这一点的几个例子。
更明确的示例:
{x={y:1}} = {}
{x:{y=1}} = {}
x = 'foo'
{x:{y=1}} = {x}
{x:{y=1}} = {x:{y}}
y = 'foo'
{x:{y=1}} = {x:{y}}
{x:{y=1}} = {x:{y:2}}
{x=1} = undefined
{x=1} = null
{x=1} = null || undefined
{x=1} = null | undefined
函数声明
我实际上是在看到 react-redux 源代码中的以下代码后开始研究解构:
export function createConnect({
connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory
} = {}) {
"So,我开始挖掘的第一个地方是:"
"
14.1 函数定义
"
"这里是一个小的“堆栈跟踪”,试图追踪相关的产生式,以便让我进入绑定内容。"
"
函数声明
"
"
形式参数
"
"
形式参数列表
"
"
形式参数
"
"
-> 13.3.3 解构绑定模式
"
"
绑定元素
"
"
+单名称绑定
"
"
++绑定标识符,初始化器
"
+绑定模式
+对象绑定模式
+绑定属性列表
+绑定属性
+SingleNameBinding,
+PropertyName ':' BindingElement
语义:解构绑定与解构赋值
据我所知,解构绑定和解构赋值的唯一区别在于它们可以使用的地方以及如何处理不同的词法环境。在形式参数列表(等等)之外使用解构绑定需要初始值,并且显式地传递了一个环境,而赋值则从“环境”中获取其值,因为按照其定义,它们意味着要有初始值。如果我的理解有误,我将非常乐意听取更正,以下是一个快速演示:
var {x};
function noInit({x}) { return x; }
noInit()
noInit({})
noInit({x:4})
function binding({x:y} = {x:y}){ return y; }
function assigning(){({x:y} = {x:y}); return y}
binding()
assigning()
y = 0
binding()
assigning()
结论:
我得出以下结论。解构绑定和赋值的目的是将名称引入当前词法环境,可选择为它们分配值。嵌套解构是为了刻画出您想要的数据形状,您无法免费获取上面的名称。您可以将初始化程序作为默认值,但是一旦使用它们,您就无法进一步刻画任何内容。如果您刻画出特定的形状(事实上是树形),则尝试绑定到的内容可能具有未定义的叶子,但分支节点必须与您描述的内容(名称和形状)相匹配。
附录
当我开始时,我发现查看tsc(typescript编译器)将这些内容转换为不支持解构的目标时,这很有帮助和有趣。
以下代码:
function f({A,B:{BB1=7,BB2:{BBB=0}}}) {}
var z = 0;
var {x:{y=8},z} = {x:{},z};
将代码转译(tsc --target es5 --noImplicitAny false
)为:
function f(_a) {
var A = _a.A,
_b = _a.B,
_c = _b.BB1,
BB1 = _c === void 0 ? 7 : _c,
_d = _b.BB2.BBB,
BBB = _d === void 0 ? 0 : _d;
}
var z = 0;
var _a = { x: {}, z: z },
_b = _a.x.y,
y = _b === void 0 ? 8 : _b,
z = _a.z;
={}
语法! - skiabox