[TOC]
有这样一个热门问题:
~~~
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
alert(a.x); // --> undefined
alert(b.x); // --> {n: 2}
~~~
其实这个问题很好理解,关键要弄清下面两个知识点:
* JS引擎对**赋值表达式**的处理过程
* 赋值运算的**右结合性**
# 一. 赋值表达式
形如
~~~
A = B
~~~
的表达式称为**赋值表达式**。其中A和B又分别可以是表达式。B可以是任意表达式,但是A必须是一个**左值**。
所谓左值,就是可以被赋值的表达式,在ES规范中是用内部类型**引用(Reference)**描述的。例如:
* 表达式`foo.bar`可以作为一个左值,表示对foo这个对象中bar这个名称的引用;
* 变量`email`可以作为一个左值,表示对当前执行环境中的环境记录项envRec中email这个名称的引用;
* 同样地,函数名`func`可以做左值,然而函数调用表达式`func(a, b)`不可以。
那么JS引擎是怎样计算一般的赋值表达式`A = B`的呢?简单地说,按如下步骤:
1. 计算表达式A,得到一个引用`refA`;
2. 计算表达式B,得到一个值`valueB`;
3. 将`valueB`赋给`refA`指向的名称绑定;
4. 返回`valueB`。
# 二. 结合性
所谓结合性,是指表达式中同一个运算符出现多次时,是左边的优先计算还是右边的优先计算。
赋值表达式是右结合的。这意味着:
~~~
A1 = A2 = A3 = A4
~~~
等价于
~~~
A1 = (A2 = (A3 = A4))
~~~
# 三. 连等的解析
好了,有了上面两部分的知识。下面来看一下JS引擎是怎样运算连等赋值表达式的。
以下面的式子为例:
~~~
Exp1 = Exp2 = Exp3 = Exp4
~~~
首先根据右结合性,可以转换成
~~~
Exp1 = (Exp2 = (Exp3 = Exp4))
~~~
然后,我们已经知道对于单个赋值运算,JS引擎总是先计算左边的操作数,再计算右边的操作数。所以接下来的步骤就是:
1. 计算Exp1,得到Ref1;
2. 计算Exp2,得到Ref2;
3. 计算Exp3,得到Ref3;
4. 计算Exp4,得到Value4。
现在变成了这样的:
~~~
Ref1 = (Ref2 = (Ref3 = Value4))
~~~
接下来的步骤是:
1. 将Value4赋给Exp3;
2. 将Value4赋给Exp2;
3. 将Value4赋给Exp1;
4. 返回表达式最终的结果Value4。
注意:这几个步骤体现了右结合性。
总结一下就是:
> 先**从左到右**解析各个引用,然后计算最右侧的表达式的值,最后把值**从右到左**赋给各个引用。
# 四. 问题的解决
现在回到文章开头的问题。
首先前两个var语句执行完后,`a`和`b`都指向同一个对象`{n: 1}`(为方便描述,下面称为对象N1)。然后来看
~~~
a.x = a = {n: 2};
~~~
根据前面的知识,首先依次计算表达式`a.x`和`a`,得到两个引用。其中`a.x`表示对象N1中的x,而`a`相当于`envRec.a`,即当前环境记录项中的a。所以此时可以写出如下的形式:
~~~
[[N1]].x = [[encRec]].a = {n: 2};
~~~
其中,`[[]]`表示引用指向的对象。
接下来,将`{n: 2}`赋值给`[[encRec]].a`,即将`{n: 2}`绑定到当前上下文中的名称`a`。
接下来,将**同一个**`{n: 2}`赋值给`[[N1]].x`,即将`{n: 2}`绑定到N1中的名称`x`。
由于`b`仍然指向`N1`,所以此时有
~~~
b <=> N1 <=> {n: 1, x: {n: 2}}
~~~
而`a`被重新赋值了,所以
~~~
a <=> {n: 2}
~~~
并且
~~~
a === b.x
~~~
# 五. 最后的最后
如果你明白了上面所有的内容,应该会明白`a.x = a = {n:2};`与`b.x = a = {n:2};`是完全等价的。因为在解析`a.x`或`b.x`的那个`时间点`。`a`和`b`这两个名称指向同一个对象,就像C++中同一个对象可以有多个引用一样。而在这个`时间点`之后,不论是`a.x`还是`b.x`,其实早就不存在了,它已经变成了`那个内存中的对象.x`了。
最后用一张图表示整个表达式的运算过程:
![](https://box.kancloud.cn/5ebc07896c828056f4152955fd9a91f5_435x329.png)
# 连等与var
~~~
(function (){
var a = 20
var b = c = a
})()
alert(c) // 20
~~~
# 更新
事实上,解析器在接受到`a = a.x = {n:2}`这样的语句后,会这样做:
1. 找到 a 和 a.x 的指针。如果已有指针,那么不改变它。如果没有指针,即那个变量还没被申明,那么就创建它,指向 null。
a 是有指针的,指向`{n:1}`;a.x 是没有指针的,所以创建它,指向 null。
2. 然后把上面找到的指针,都指向最右侧赋的那个值,即`{n:2}`。
![](https://img.kancloud.cn/cf/64/cf64725c9595f066f7c254e67bcd63e3_1670x526.png)
# 参考资料
[javascript 连等赋值问题](https://segmentfault.com/q/1010000002637728)
[由ES规范学JavaScript(二):深入理解“连等赋值”问题](https://segmentfault.com/a/1190000004224719)
- 第一部分 HTML
- meta
- meta标签
- HTML5
- 2.1 语义
- 2.2 通信
- 2.3 离线&存储
- 2.4 多媒体
- 2.5 3D,图像&效果
- 2.6 性能&集成
- 2.7 设备访问
- SEO
- Canvas
- 压缩图片
- 制作圆角矩形
- 全局属性
- 第二部分 CSS
- CSS原理
- 层叠上下文(stacking context)
- 外边距合并
- 块状格式化上下文(BFC)
- 盒模型
- important
- 样式继承
- 层叠
- 属性值处理流程
- 分辨率
- 视口
- CSS API
- grid(未完成)
- flex
- 选择器
- 3D
- Matrix
- AT规则
- line-height 和 vertical-align
- CSS技术
- 居中
- 响应式布局
- 兼容性
- 移动端适配方案
- CSS应用
- CSS Modules(未完成)
- 分层
- 面向对象CSS(未完成)
- 布局
- 三列布局
- 单列等宽,其他多列自适应均匀
- 多列等高
- 圣杯布局
- 双飞翼布局
- 瀑布流
- 1px问题
- 适配iPhoneX
- 横屏适配
- 图片模糊问题
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 内存空间
- 作用域
- 执行上下文栈
- 变量对象
- 作用域链
- this
- 类型转换
- 闭包(未完成)
- 原型、面向对象
- class和extend
- 继承
- new
- DOM
- Event Loop
- 垃圾回收机制
- 内存泄漏
- 数值存储
- 连等赋值
- 基本类型
- 堆栈溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍历对象属性
- 宽度、高度
- performance
- 位运算
- tostring( ) 与 valueOf( )方法
- JavaScript技术
- 错误
- 异常处理
- 存储
- Cookie与Session
- ES6(未完成)
- Babel转码
- let和const命令
- 变量的解构赋值
- 字符串的扩展
- 正则的扩展
- 数值的扩展
- 数组的扩展
- 函数的扩展
- 对象的扩展
- Symbol
- Set 和 Map 数据结构
- proxy
- Reflect
- module
- AJAX
- ES5
- 严格模式
- JSON
- 数组方法
- 对象方法
- 函数方法
- 服务端推送(未完成)
- JavaScript应用
- 复杂判断
- 3D 全景图
- 重载
- 上传(未完成)
- 上传方式
- 文件格式
- 渲染大量数据
- 图片裁剪
- 斐波那契数列
- 编码
- 数组去重
- 浅拷贝、深拷贝
- instanceof
- 模拟 new
- 防抖
- 节流
- 数组扁平化
- sleep函数
- 模拟bind
- 柯里化
- 零碎知识点
- 第四部分 进阶
- 计算机原理
- 数据结构(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 搜索算法
- 动态规划
- 二叉树
- 浏览器
- 浏览器结构
- 浏览器工作原理
- HTML解析
- CSS解析
- 渲染树构建
- 布局(Layout)
- 渲染
- 浏览器输入 URL 后发生了什么
- 跨域
- 缓存机制
- reflow(回流)和repaint(重绘)
- 渲染层合并
- 编译(未完成)
- Babel
- 设计模式(未完成)
- 函数式编程(未完成)
- 正则表达式(未完成)
- 性能
- 性能分析
- 性能指标
- 首屏加载
- 优化
- 浏览器层面
- HTTP层面
- 代码层面
- 构建层面
- 移动端首屏优化
- 服务器层面
- bigpipe
- 构建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack优化
- Webpack原理
- 实现loader
- 实现plugin
- tapable
- Webpack打包后代码
- rollup.js
- parcel
- 模块化
- ESM
- 安全
- XSS
- CSRF
- 点击劫持
- 中间人攻击
- 密码存储
- 测试(未完成)
- 单元测试
- E2E测试
- 框架测试
- 样式回归测试
- 异步测试
- 自动化测试
- PWA
- PWA官网
- web app manifest
- service worker
- app install banners
- 调试PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 饿了么整理
- 样式
- 技巧
- Vue音乐播放器
- Vue源码
- Virtual Dom
- computed原理
- 数组绑定原理
- 双向绑定
- nextTick
- keep-alive
- 导航守卫
- 组件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 动画(未完成)
- 异常监控、收集(未完成)
- 数据采集
- Sentry
- 贝塞尔曲线
- 视频
- 服务端渲染
- 服务端渲染的利与弊
- Vue SSR
- React SSR
- 客户端
- 离线包
- 第五部分 网络
- 五层协议
- TCP
- UDP
- HTTP
- 方法
- 首部
- 状态码
- 持久连接
- TLS
- content-type
- Redirect
- CSP
- 请求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服务端
- Linux
- Linux命令
- 权限
- XAMPP
- Node.js
- 安装
- Node模块化
- 设置环境变量
- Node的event loop
- 进程
- 全局对象
- 异步IO与事件驱动
- 文件系统
- Node错误处理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服务
- 负载均衡
- 获取用户IP
- 解决跨域
- 适配PC与移动环境
- 简单的访问限制
- 页面内容修改
- 图片处理
- 合并请求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自动化(未完成)
- docker
- 创建CLI
- 持续集成
- 持续交付
- 持续部署
- Jenkins
- 部署与发布
- 远程登录服务器
- 增强服务器安全等级
- 搭建 Nodejs 生产环境
- 配置 Nginx 实现反向代理
- 管理域名解析
- 配置 PM2 一键部署
- 发布上线
- 部署HTTPS
- Node 应用
- 爬虫(未完成)
- 例子
- 反爬虫
- 中间件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源码
- app.js
- config.js
- 消息队列
- RPC
- 性能优化
- 第七部分 总结
- Web服务器
- 目录结构
- 依赖
- 功能
- 代码片段
- 整理
- 知识清单、博客
- 项目、组件、库
- Node代码
- 面试必考
- 91算法
- 第八部分 工作代码总结
- 样式代码
- 框架代码
- 组件代码
- 功能代码
- 通用代码