这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
这篇文章主要介绍了 javascript 中使用连等赋值操作带来的问题的相关资料, 需要的朋友可以参考下
前言
文章标题这句话原本是在国外某 JavaScript 规范里看到的,当时并没有引起足够的重视,直到最近一次出现了 bug 发现 JS 里的连等赋值操作的特色 (坑)。
网上搜索一番发现一个非常好的连等赋值的(来源 1,来源 2)例子:
- var a = {
- n: 1
- };
- a.x = a = {
- n: 2
- };
- console.log(a.x); // 输出?
答案是:
- console.log(a.x); // undefined
不知道各位有没有答对,至少我是答错了。
遂借此机会好好看看 JS 连等赋值是怎么回事
赋值顺序?
假设有一句代码: A=B=C; ,赋值语句的执行顺序是从右至左,所以问题在于:
是猜想 1: B = C; A = C; ?
还是猜想 2: B = C; A = B; ?
我们都知道若两个对象同时指向一个对象,那么对这个对象的修改是同步的,如:
- var a = {
- n: 1
- };
- var b = a;
- a.n = 2;
- console.log(b); //Object {n: 2}
所以可以根据这个特性来测试连续赋值的顺序。
按照猜想 1,把 C 换成具体的对象,可以看到对 a 的修改不会同步到 b 上,因为在执行第一行和第二行时分别创建了两个 {n:1} 对象。如:
- var b = {
- n: 1
- };
- var a = {
- n: 1
- };
- a.n = 0;
- console.log(b); //Object {n: 1}
再按照猜想 2,把 C 换成具体的对象,可以看到对 a 的修改同步到了 b,因为 a 和 b 同时引用了一个对象,如:
- var b = {
- n: 1
- };
- var a = b;
- a.n = 0;
- console.log(b); //Object {n: 0}
测试真正的连等赋值:
- var a, b;
- a = b = {
- n: 1
- };
- a.n = 0;
- console.log(b); //Object {n: 0}
可以看到是符合猜想 2 的,如果有人觉得这个测试不准确可以再来测试,使用 ECMA5 的 setter 和 getter 特性来测试。
首先 setter 和 getter 是应用于变量名的,而不是变量真正储存的对象,如下:
- Object.defineProperty(window, "obj", {
- get: function() {
- console.log("getter!!!");
- }
- });
- var x = obj;
- obj; //getter!!! undefined
- x; //undefined
可以看到只有 obj 输出了 "getter!!!",而 x 没有输出,用此特性来测试。
连等赋值测试 2:
- Object.defineProperty(window, "obj", {
- get: function() {
- console.log("getter!!!");
- }
- });
- a = b = obj; //getter!!! undefined
通过 getter 再次证实,在 A=B=C 中,C 只被读取了一次。
所以,连等赋值真正的运算规则是 B = C; A = B; 即连续赋值是从右至左永远只取等号右边的表达式结果赋值到等号左侧。
连续赋值能拆开写么?
通过上面可以看到连续赋值的真正规则,那么再回归到文章开头的那个案例,如果按照上述规则将连续赋值拆开会发现结果不一样了,如:
- var a = {
- n: 1
- };
- a = {
- n: 2
- };
- a.x = a;
- console.log(a.x); //Object {n: 2, x: Object}
所以连续赋值语句虽然是遵从从右至左依次赋值的规则但依然不能将语句拆开来写,至于为什么
我猜测:js 内部为了保证赋值语句的正确,会在一条赋值语句执行前,先把所有要赋值的引用地址取出一个副本,再依次赋值。
所以我认为这段代码 a.x=a={n:2}; 的逻辑是:
1、在执行前,会先将 a 和 a.x 中的 a 的引用地址都取出来,此值他们都指向 {n:1}
2、在内存中创建一个新对象 {n:2}
3、执行 a={n:2},将 a 的引用从指向 {n:1} 改为指向新的{n:2}
4、执行 a.x=a,此时 a 已经指向了新对象,而 a.x 因为在执行前保留了原引用,所以 a.x 的 a 依然指向原先的 {n:1} 对象,所以给原对象新增一个属性 x,内容为 {n:2} 也就是现在 a
5、语句执行结束,原对象由 {n:1} 变成{n:1,x:{n:2}},而原对象因为无人再引用他,所以被 GC 回收,当前 a 指向新对象{n:2}
6、所以就有了文章开头的运行结果,再执行 a.x,自然就是 undefined 了
上述过程按序号图示:
按照上述过程可以看出旧的 a.x 和新的 a 都指向新创建的对象 {n:2},所以他们应该是全等的。
测试:
- var a = {
- n: 1
- };
- var b = a;
- a.x = a = {
- n: 2
- };
- console.log(a === b.x); //true
因为我们增加了 var b=a,即将原对象增加了一条引用,所以在上述第 5 步时不会被释放,证实了上面的结论。
后记
通过这次了解了连续赋值的特点,再回过头看文章标题,似乎应该叫:
尽量不要使用 JS 的连续赋值操作,除非真的了解它的内部机制及可能会产生的后果。
来源: http://www.phperz.com/article/17/0408/268465.html