JavaScript 连续赋值
let a=10 和 a=10 的区别
前者不是赋值语句,后者是。
- var/let/const 后面不允许跟随表达式
对于声明语句来说,紧随于“var/let/const”之后的,一定是变量名(标识符),不可以是表达式。
// 这里的 x 应该被视为一个标识符,而非表达式 let x=2 // 会直接报错,因为 obj.x 是一个表达式,不能跟在 var/let/const 后面 let obj.x=2
- 初始器
我们把声明语句中,去掉 var/let/const x 后的语句剩余部分称为初始器,是一种语法组件。用于在代码执行阶段绑定初始值
let x=2 // =2 是初始器部分,会在 parse 阶段被解析,确定下来“在执行阶段为 x 绑定初始值”这一任务。
值的绑定
那些用 var/let/const 声明的变量,收到初始器的作用,在代码执行(上下文环境已经创建完毕)时,会作值的绑定。
以下代码中,尽管在 fn 执行之前,变量 x 已经被创建(在 fn 上下文环境被创建阶段),但它没有被绑定值。直到 fn 被调用,x 被绑定初始值 undefined,注意这不属于赋值。这种“声明的变量在执行阶段被绑定初始值”的现象被称为执行期语义。
function fn(){ console.log(x) let x // 等价于 let x=undefined,如果是 let x=2,那就初始化为 2 } fn() // 报错:Cannot access 'x' before initialization
对于 let a=10, = 10
作为初始器,在执行的时候调用的是 InitializeReferencedBinding( a
, GetValue(rhs));而 a = 10
作为赋值,在执行的时候调用的是 PutValue( a
, GetValue(rhs))。
- 可是 var 不是会在创建之后就初始化为 undefiend 吗,这是初始器的工作结果吗?
不是的,注意上面的讨论,初始器的工作时间是代码的执行阶段。而 var 声明的变量被初始化为 undefiend,是在上下文环境被创建时候,伴随着变量的创建而被初始化的。
赋值表达式
- 例 1
var x = y = 100;
x 是一个标识符(不是表达式,准确的说,只是一个表达名字的、静态语法分析期作为标识符来理解的字面文本),而 y 和 100 都是表达式,且 y = 100 是一个赋值表达式。
此外,要注意这里变量泄漏了:y
- 例 2
let a={n:1} a.x = a = {n:2}
a.x 是一个表达式,a 是一个表达式,{n:2} 是一个表达式
a.x = a = {n:2}
以下过程应该从内存变化角度理解
let a={n:1} a.x=a={n:2} console.log(a.x)// undefined
等效于
// 我其实不太喜欢这种等价写法(虽然很多解释标题的文章都有提到这种等价写法),它多出来的 ref 可能使人迷惑,从内存角度理解这个问题才是最直观的。 let a={n:1} let ref=a a={n:2} ref.x=a console.log(a.x) // undefiend
- 内存变化如下
/*计算表达式 a,{n:1},再计算表达式 a={n:1}*/ let a={n:1} // 栈内存 a:A1 // 堆内存 A1:{n:1}
/*计算表达式 a.x*/ 现在我们获取到了堆内存 A1 处的控制权。如果之后 a.x 作为 LHS,如 a.x=2,则堆内存 A1 处变为 A1:{n:1,x:2}。如果之后 a.x 作为 RHS,如 b=a.x,则返回 undefiend,因为堆内存 A1 处没有存储 x 这个属性的值,即 GetValue(a.x)=undefined /*计算表达式 a*/ 现在我们获取到了栈内存 a 处的控制权。 /*计算表达式{n:1}*/ 就是 {n:1}
/*计算表达式 a={n:2}*/ 这是对栈内存 a 处的值进行修改 // 栈内存 a:B1 // 堆内存 B1:{n:2}
/*计算表达式 a.x=a*/ 这是对堆内存 A1 处的值进行修改 // 堆内存 A1:{n:1,x:B1}
现在,我们要输出 a.x,那么来看看栈内存 a 处的值是多少?嗯,是 B1,那么我们只要去访问堆内存 B1 处的属性 x,结果是 undefiend,即 GetValue(a.x)=undefiend,console.log 方法会导致 JS 引擎默认调用 GetValue 方法。
我们注意到,在堆内存 A1 处,x 的属性值是 B1(一个堆内存地址值)。且 B1 的内存分配晚于 A1。这实际上反映了 a.x = a = {n:2}
的真正作用:给旧的变量添加一个指向新变量的属性。这一使用模式在 JQuery 中有所应用。
一个看起来类似,结果不同的例子
let a={n:1} a={n:2} a.x=a console.log(a) // {n: 2, x: {…}}
- 内存变化如下
/*计算表达式 a,{n:1},再计算表达式 a={n:1}*/ let a={n:1} // 栈内存 a:A1 // 堆内存 A1:{n:1}
/*计算表达式 a*/ 现在我们获取到了栈内存 a 处的控制权。 /*计算表达式{n:2}*/ 就是 {n:2}
/*计算表达式 a={n:2}*/ 这是对栈内存 a 处的值进行修改 // 栈内存 a:B1 // 堆内存 B1:{n:2}
/*计算表达式 a.x*/ 现在我们获取到了堆内存 B1 处的控制权。 /*计算表达式 a*/ 现在我们获取到了栈内存 a 处的控制权。
/*计算表达式 a.x=a*/ // 堆内存 B1:{n:2,x:B1}
现在,我们要输出 a.x,那么来看看栈内存 a 处的值是多少?嗯,是 B1,那么我们只要去访问堆内存 B1 处的属性 x,结果是 {n: 2, x: {…}},也就是发生了循环引用(这不是我们这里讨论的重点)。
为什么上面两个例子的输出结果不同?其实最关键的就是子表达式的求值顺序。第一个例子中,子表达式的求值顺序是 a.x,a,{n:2}。而第二个例子中,子表达式的求值顺序是 a,{n:2},a,a.x。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 语句执行和表达式执行
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论