如果你使用过 Swift 或者 C#,那么一定对 var
非常熟悉。它是 variable 或者是 variation 的简写,在编程语言中多用于定义变量的关键字,在一些操作系统中也能见到它的身影。
在 JavaScript(以下简称 JS)中也可以使用 var
来声明变量,不仅限于此,还有 let
、const
共三种方式创建变量。而如今却很少在 JS 中见到 var
的身影,这又是为什么?
var 的由来
var
在使用上与 let
十分相似,大部分情况下,我们可以直接用 let
来代替 var
或者用 var
去代替 let
,都能达到预期的效果。
1 | var name = '周树人'; |
但实际上,var
却是一座屎山。在 JS 设计初期,作为一门网页脚本语言,并未做过多的考虑(从设计到初步实现仅用了 10 天)。但随着时间的推移,项目中大量使用 var
会导致许多诡异的问题,这从而促使了 let
与 const
的诞生。
现如今,虽然在各大项目中已经很难在看到 var
的身影,但在百度上或者公司的祖传代码里你依然能找到它。如果在百度搜代码,看到有人还在教你使用 var
甚至是 DreamWeaver 写代码… 直接关掉吧,不用看了,那篇文章只是在浪费你的时间。
这不由得让人想起当年教我的 Java 老师,用的 MyEclipse 2012 + Tomcat 1.6 + JDK 1.6 开发,每次有人提出疑问都蜜汁自信说自己有 10 年开发经验,不可能出错。
当然,也并不是说使用 var
就完全是一件错事,任何事物存在就有着自身的价值。Babel 以及 TypeScript 等工具在特定情况下,编译出的代码都或多或少会使用到 var
来声明变量,因为有着极强的兼容性。不过,除开这种情况使用 var
就完全没必要了。我可从没说过只要用 var 写代码的都是辣鸡啊,你们不要乱说啊。
作用域
在 ES6 之前,JS 的变量作用域是非常笼统的,只有全局变量与函数变量。
而 var
坑就坑在它没有块级作用域,用 var
声明变量的变量不是函数作用域就是全局作用域。
1 | for (var i = 0; i < 10; i++) {} |
上列就是一个最简单的例子,for
循环都结束了,却仍然可以访问到变量 i
。试想一下,如果变量名不是用的 i,而是开发中最为常见的 item 会怎么样?
1 |
|
再来看这段代码,可以说这段代码在以前是非常常见的,如果你接触编程比较早甚至还觉得非常亲切,li
标签加上点击事件,当该 li
被点击时修改当前的背景颜色。
看着没什么问题对吧?但当你点击 li 元素后,控制台会提示 TypeError: Cannot read properties of undefined (reading 'style')
的错误信息。
这算是一个老生常谈的问题了,只要以前写过 ES5 都遇到过,也都知道该怎么避免。
当该段代码执行时仅仅是为标签元素绑定了 click 事件,但并未执行该函数。当页面元素被点击后会从 lis 集合里寻找下标为 i 的元素并修改颜色。但这时 i
的值是多少呢?没错,3,但并不存在下标为 3 的 li
元素,所以就抛 undefined
了。
要解决这个问题也很简单,使用闭包,因为 var
会受到函数作用域的影响。
1 | <script type="text/javascript"> |
哦,我的老伙计,尽管这段代码能顺利跑起来了,但这只是简单的修改样式。我发誓,如果,我是说如果,要是代码逻辑稍微复杂一点,后面项目维护起来,那一定和写小程序一样难受,就像隔壁苏珊太太的苹果派那样糟糕。
ES6 YES
1 | <script> |
这是一种比较现代的写法,let
和 const
都拥有块级作用域,所以完全避免了变量污染的问题。
重复声明
除了作用域导致的变量污染,var
还有一个奇怪的特性,就是变量允许重复声明。
1 | var liangfen = 1; |
使用 var
,我们可以重复声明一个变量,不管多少次都行。
1 | // SyntaxError: Identifier 'liangfen' has already been declared |
而使用 let
会直接提示语法错误,非常的银杏。
变量提升
在 JS 代码开始执行的时候,就会预先处理 var
的变量。也就是说,使用 var
声明的变量会在其作用域的开头被定义,与它在代码中定义的位置无关。
要注意的是,这不包括变量嵌套在函数中的情况,因为函数不管定义在代码的哪一行,都只有在被调用时才会执行。
1 | message = 'hello world'; |
它与下面这种情况是等价的(var message 被上移至作用域开头)
1 | var message; |
甚至与这种情况也一样(代码块会被忽略)
1 | message = 'hello world'; |
这种行为被称之为「提升」(英文为 hoisting 或 raising),因为所有的 var
都被提升到了作用域的顶部。
声明会被提升,但是赋值不会。
1 | console.log(message); |
var message = 'hello world'
这行代码包含两个行为:
- 使用
var
声明变量 - 使用
=
给变量赋值
变量的声明,在代码刚开始执行的时候,就被提升处理了,但是赋值操作始终是在它出现的地方才起作用。所以这段代码实际上是这样工作的:
1 | var message; |
因为所有的 var
声明都是在作用域开头处理的,我们可以在任何地方引用它们。但是在它们被赋值之前都是 undefined
。
所以就算你写出了这种代码,它也不会报错。
1 | console.log(message); // -> undefined |
而使用 let
会直接提示结构错误,非常的银杏。
1 | // ReferenceError: Cannot access 'message' before initialization |
暂时性死区
只要块级作用域内存在 let
关键字,它所声明的变量就绑定了这个区域,不再受外部的影响。
1 | // ReferenceError: Cannot access 'name' before initialization |
ES6 明确规定,如果区块中存在 let
和 const
关键字,这个区块对这些关键字声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
在代码块内,使用 let
关键字声明变量之前,该变量都是不可用的。这在语法上,称为「暂时性死区」(temporal dead zone,简称 TDZ)。
有些死区比较隐蔽,不太容易发现。
1 | function naming(fullname = `${surname}树人`, surname = '周') { |
上面代码与前面的有所不同,直到 naming
函数被调用前,程序都是可以正常运行的,不会出现报错,因为其通过了语法编译。
没错,尽管 JS 是一门弱类型的脚本语言,但它并不是解释型语言,其也是有语法编译的,很多人都不知道这一点PHP:你礼貌么?。前面的几个示例中,代码还没开始执行就报语法错误了,这就是最简单的证明。
而 naming()
在这里报错,是因为参数 fullname
默认值用到了另一个参数 surname
,而此时 surname
还没有声明,属于死区。如果将这两个参数的顺序调整,就不会报错,因为此时变量已经声明了。
1 | function naming(surname = '周', fullname = `${surname}树人`) { |
常量
在 ES6 之前,JS 内所有数据都是变量,可以修改任意值。而 const
关键字用来声明常量,一旦被声明,值将无法更改。
1 | // TypeError: Assignment to constant variable. |
为什么 Object 和 Array 可以随意修改值?因为这两类是引用类型,并非基础数据类型,引用类型内部的值不管怎么变,其引用地址都是不变的。
1 | const wifes = ['镜华']; |
当然,这是 JS 的基础知识,由此可以引申出深拷贝与浅拷贝,但并不与本文章相关,便不在此做过多的赘述。先挖个坑,才不是因为懒呢。