关于 var x = y = 100 你真的会用吗?(上)

229 阅读7分钟
    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 就留下了很大的隐患,如果你一直这样去声明变量,并且至今没有发现错误,只能说你是幸运的。

但是看了本篇文章,希望你可以改掉这种不好的写法。


附加一道题:

var a, b;
(function () {
  alert(a);
  alert(b);
  var a = b = 3;
  alert(a);
  alert(b);
})();
alert(a);
alert(b);

乍一看是变量提升的题,但是也融入了本章的 var x = y = 100 知识点。 前面4个alert应该没有什么问题,分别是 undefined undefined 3 3 但是最后一个alert(b)应该是什么呢? 如果在function内这么去定义变量

var a = 3
var b = 3

那毫无疑问最后的alert(b)应该是 undefined 但是使用了var a = b = 3 这种方式去定义变量,意义就完全不一样了,所以答案应该是输出3。