var x = y = 100;
可能很多人都写过这样的代码,首先来说这样的写法没有错,看起来也很简洁,但事实上这行代码是js中最容易错用的表达式之一。
你也许会说,这就是简单的声明赋值表达式,我总这么用,没有出过错。别急,下面的内容可能会颠覆你的认知。
声明
在JavaScript中一共有六条声明用的语句,严格滴说 JavaScript 中只有变量和常量两种标识符,六条声明语句分别为:
- let
- const
- var
- function
- class
- import
- *try catch(x)
题目中的var x就是一个声明,语句的后半部分,使用“=”引导了一个初始化语法,一般情况下可以将它理解为一个赋值运算。
var & let & 变量提升
声明是在语法分析阶段就完成了的,这样在当前代码上下文执行之前就拥有了被声明的标识符,如 x 。
JavaScript 虽然被称为动态语言,但是确实是拥有静态语义的,可以说这个静态语义并没有处理得当,变量提升就是体现之一。
console.log(x);// undefined
var x = 10;
console.log(x);// 10
由于标识符实在用户代码执行之前就已经由静态分析得到,并且创建在环境中,因此let和var声明的变量从这种角度来看就没有什么不同了。他们都是在读取一个已经存在的标识符名。再看:
fucntion fn(){
console.log(x); // undefined
console.log(y); // throw a Exception
var x = 10;
let y = 10;
}
let 声明的变量阻止变量提升,这点大家应该没有疑问,但是多数人会认为是在 let 声明前,不存在该变量,其实不然。上面所说的 let 和 var 声明的变量在静态分析时就已经创建好了。
那么造成这种执行结果的原因是什么呢?
先说打印x时输出 undefined 是因为 var x 声明的标识符在函数fn()创建时就已经存在了,但是并没有进行赋值操作,所以打印 undefined,没有问题。
同理,let y 声明的标识符y其实也已经在创建 fn() 函数的时候存在了,所以打印抛出异常并不是因为它不存在,而是这个用 let 声明的标识符被拒绝访问了!
为什么会拒绝访问?
es6 新增的 let/const 声明变量的方式,其实本质上与 var 没有异同,只是 JavaScript 拒绝了访问用 let/const 声明并且还没有进行赋值的标识符。
在 let/const 出现前,var 声明变量的方式叫做"变量声明",而在 es6 之后,let/const 声明变量的方式叫做"词法声明"。
"变量声明"方式声明变量后会初始化绑定一个 undefined 值,而"词法声明"方式声明一个变量,则不会初始化绑定一个 undefined 值,这个变量上会有一个“还没有值”的标签。
所以,题目中的 var x = 在语义上就是为 x 变量绑定一个初始值。
赋值
var x;
x = 100;
在 JavaScript 中赋值操作其实就是将“=”右边的值付给“=”左边的引用。
也就是说,在 JavaScript 中,一个赋值表达式的左右和右边其实都是表达式。
变量泄露
变量泄露是 JavaScript 语言之初九遗留的一个非常大的坑。这个坑对于刚接触 JavaScript 语言的同学来说异常友好。
何为变量泄露?
变量泄露就是当你向一个不存在的变量赋值时,JavaScript 会在全局范围内创建它。
这样带来的唯一好处就是变量可以使用的时候再去声明,不用提前去做些什么。
但是随着 JavaScript 功能越来越强大,代码量激增,这样的“好处”也就带来了一个严重的问题,如果你的项目很庞大,JavaScript 代码逻辑复杂,我在使用时才去创建的变量,在后续开发中,团队其他伙伴,甚至我自己都很难找到这个莫名其妙出现的全局变量,没有办法对这个变量进行溯源。在当今的前端项目中,这种问题带来的后果必然是灾难性的。
那么究竟是何种原因造成了这种缺陷?
这要追溯到JavaScript语言设计的早期,全局环境是 JavaScript 引擎是用一个称为“全局对象”的东西管理起来的,这个"全局对象"可以理解为一个普通对象,并且使用这个对象创建一个称为“全局对象闭包”的东西。
当你向一个不存在的变量进行赋值操作时,由于全局对象的属性表示可以动态添加的,因此 JavaScript 将变量名作为属性名添加到这个全局对象属性表中。再次访问这个变量时,就相当于访问了全局对象的这个属性。
为了兼容这个设计,在后续的更新中,JavaScript 环境仍然是通过将全局对象初始化为这样一个全局闭包来实现的。但是为了尽可能的弥补之前遗留的一些缺陷,es6 中规定在这个全局对象之外。再维护一个变量名列表,所有在静态语法分析期间或者通过var 声明的变量就放入这个列表中,然后约定这个变量名列表中的变量是直接声明的变量,不能使用 delete 删除,于是就有了这样的效果:
var a = 100;
x = 200;
delete a; // false
delete x; // true
表面看起来“泄漏到全局的变量”与使用 var 声明的变量都是全局变量,并且都实现为 global 的属性,但本质上他们有所区别,并且当 var 声明在 eval() 中的时候,又有所不同。
eval('var b=300');
delete b; // true
这种情况下使用var声明的变量名尽管也会添加到变量名列表(varNames),但它可以从中移除,这也是唯一特例。
可以移除的原因也是:变量名列表本身不限制删除,但是 global.x 删除后会同步删除掉变量名列表中对应的变量名,如果 configurable 为 false 那么就删不掉属性,于是就删除不掉变量名列表中的名字了。
如:
var a = 100;
b = 100;
Object.getOwnPropertyDescriptor(global, 'a');
Object.getOwnPropertyDescriptor(global, 'b');
// {value: 100, writable: true, enumerable: true, configurable: false}
// {value: 100, writable: true, enumerable: true, configurable: true}
回归正题
回到我们开篇引出的这行代码:
var x = y = 100;
我们试着拆解这行代码,看第一个“=”,“=”右边是一个表达式 y = 100 ,这个表达式实际上发生了一次想不存在的变量赋值操作,所以必然的隐式地声明了一个全局变量y,并赋值为100。
而这个表达式是有结果的,结果就是右侧操作数的值,不是引用(下面的例子很好的说明了这个特点,不太好理解,但是这个概念很重要),
obj = {f: function(){ return this === obj;}};
obj.f(); // true
var a = obj.f;
a(); // false
右侧操作数的值也就是100。那么 y = 100,赋值完成后返回结果100,100作为初始值赋值给变量 x。
最后的结果就是 x 和 y 的值都是100,但是 x 只是一个用 var 声明的普通变量,而 y 的赋值则触发了变量泄露,y 是一个创建在全局对象下的属性。参考上面变量泄露描述的特点,如果 JavaScript 代码复杂且庞大,那么这个 y 就留下了很大的隐患,如果你一直这样去声明变量,并且至今没有发现错误,只能说你是幸运的。
但是看了本篇文章,希望你可以改掉这种不好的写法。