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

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

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

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值