抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Yuki 妙妙屋

不要因为走得太远,就忘了当初为什么出发。

如果你使用过 Swift 或者 C#,那么一定对 var 非常熟悉。它是 variable 或者是 variation 的简写,在编程语言中多用于定义变量的关键字,在一些操作系统中也能见到它的身影。

在 JavaScript(以下简称 JS)中也可以使用 var 来声明变量,不仅限于此,还有 letconst 共三种方式创建变量。而如今却很少在 JS 中见到 var 的身影,这又是为什么?

var 的由来

var 在使用上与 let 十分相似,大部分情况下,我们可以直接用 let 来代替 var 或者用 var 去代替 let,都能达到预期的效果。

1
2
3
4
5
var name = '周树人';
let pseudonym = '鲁迅';

console.log(name); // -> 周树人
console.log(pseudonym); // -> 鲁迅

但实际上,var 却是一座屎山。在 JS 设计初期,作为一门网页脚本语言,并未做过多的考虑(从设计到初步实现仅用了 10 天)。但随着时间的推移,项目中大量使用 var 会导致许多诡异的问题,这从而促使了 letconst 的诞生。

现如今,虽然在各大项目中已经很难在看到 var 的身影,但在百度上或者公司的祖传代码里你依然能找到它。如果在百度搜代码,看到有人还在教你使用 var 甚至是 DreamWeaver 写代码… 直接关掉吧,不用看了,那篇文章只是在浪费你的时间。

这不由得让人想起当年教我的 Java 老师,用的 MyEclipse 2012 + Tomcat 1.6 + JDK 1.6 开发,每次有人提出疑问都蜜汁自信说自己有 10 年开发经验,不可能出错。

低情商:用的都是 10 年前的老技术 高情商:10 年开发经验高级工程师 他甚至连 class 都不 new!什么数据都直接塞 Map 里去传值,写 SQL 不换行,还一脸自信的说 你们看我多厉害,一行就写完了(指三张表联查 + 一大堆条件语句不换行)

当然,也并不是说使用 var 就完全是一件错事,任何事物存在就有着自身的价值。Babel 以及 TypeScript 等工具在特定情况下,编译出的代码都或多或少会使用到 var 来声明变量,因为有着极强的兼容性。不过,除开这种情况使用 var 就完全没必要了。我可从没说过只要用 var 写代码的都是辣鸡啊,你们不要乱说啊。

作用域

在 ES6 之前,JS 的变量作用域是非常笼统的,只有全局变量函数变量

var 坑就坑在它没有块级作用域,用 var 声明变量的变量不是函数作用域就是全局作用域。

1
2
for (var i = 0; i < 10; i++) {}
console.log(i); // -> 10

上列就是一个最简单的例子,for 循环都结束了,却仍然可以访问到变量 i。试想一下,如果变量名不是用的 i,而是开发中最为常见的 item 会怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="GBK" />
<title>Document</title>
</head>

<body>
<ul>
<li>li1</li>
<li>li2</li>
<li>li3</li>
</ul>

<script type="text/javascript">
var lis = document.getElementsByTagName('li');

for (var i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', function () {
lis[i].style.background = 'skyblue';
});
}
</script>
</body>
</html>

再来看这段代码,可以说这段代码在以前是非常常见的,如果你接触编程比较早甚至还觉得非常亲切,为了复古我甚至都在 script 里加了 type=text/javascript,charset 是万恶的 GBK。 要实现的功能也很简单,为每个 li 标签加上点击事件,当该 li 被点击时修改当前的背景颜色。

看着没什么问题对吧?但当你点击 li 元素后,控制台会提示 TypeError: Cannot read properties of undefined (reading 'style') 的错误信息。

这算是一个老生常谈的问题了,只要以前写过 ES5 都遇到过,也都知道该怎么避免。

当该段代码执行时仅仅是为标签元素绑定了 click 事件,但并未执行该函数。当页面元素被点击后会从 lis 集合里寻找下标为 i 的元素并修改颜色。但这时 i 的值是多少呢?没错,3,但并不存在下标为 3 的 li 元素,所以就抛 undefined 了。

要解决这个问题也很简单,使用闭包,因为 var 会受到函数作用域的影响。

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
var lis = document.getElementsByTagName('li');

for (var i = 0; i < lis.length; i++) {
(function (j) {
lis[j].addEventListener('click', function () {
lis[j].style.background = 'skyblue';
});
})(i);
}
</script>

哦,我的老伙计,尽管这段代码能顺利跑起来了,但这只是简单的修改样式。我发誓,如果,我是说如果,要是代码逻辑稍微复杂一点,后面项目维护起来,那一定和写小程序一样难受,就像隔壁苏珊太太的苹果派那样糟糕。

ES6 YES

1
2
3
4
5
6
7
8
9
<script>
const lis = document.querySelectorAll('li');

for (let i = 0; i < lis.length; i++) {
lis[j].addEventListener('click', () => {
lis[j].style.background = 'skyblue';
});
}
</script>

这是一种比较现代的写法,letconst 都拥有块级作用域,所以完全避免了变量污染的问题。

重复声明

除了作用域导致的变量污染,var 还有一个奇怪的特性,就是变量允许重复声明。

1
2
3
4
5
var liangfen = 1;
// 我说你吃了两碗凉粉那就是两碗
var liangfen = 2;

console.log(liangfen); // -> 2

使用 var,我们可以重复声明一个变量,不管多少次都行。

1
2
3
4
// SyntaxError: Identifier 'liangfen' has already been declared
let liangfen = 1;
// 我就吃了一碗粉你凭什么说我吃了两碗?
let liangfen = 2;

而使用 let 会直接提示语法错误,非常的银杏。

变量提升

在 JS 代码开始执行的时候,就会预先处理 var 的变量。也就是说,使用 var 声明的变量会在其作用域的开头被定义,与它在代码中定义的位置无关。

要注意的是,这不包括变量嵌套在函数中的情况,因为函数不管定义在代码的哪一行,都只有在被调用时才会执行。

1
2
3
4
message = 'hello world';

console.log(message);
var message;

它与下面这种情况是等价的(var message 被上移至作用域开头)

1
2
3
4
var message;
message = 'hello world';

console.log(message);

甚至与这种情况也一样(代码块会被忽略)

1
2
3
4
5
6
message = 'hello world';

if (false) {
var message;
}
console.log(message);

这种行为被称之为「提升」(英文为 hoisting 或 raising),因为所有的 var 都被提升到了作用域的顶部。

声明会被提升,但是赋值不会。

1
2
console.log(message);
var message = 'hello world';

var message = 'hello world' 这行代码包含两个行为:

  1. 使用 var 声明变量
  2. 使用 = 给变量赋值

变量的声明,在代码刚开始执行的时候,就被提升处理了,但是赋值操作始终是在它出现的地方才起作用。所以这段代码实际上是这样工作的:

1
2
3
4
5
var message;

console.log(message); // -> undefined
// 赋值
message = 'hello world';

因为所有的 var 声明都是在作用域开头处理的,我们可以在任何地方引用它们。但是在它们被赋值之前都是 undefined

所以就算你写出了这种代码,它也不会报错。

1
2
console.log(message); // -> undefined
var message;

而使用 let 会直接提示结构错误,非常的银杏。

1
2
3
// ReferenceError: Cannot access 'message' before initialization
console.log(message);
let message;

暂时性死区

只要块级作用域内存在 let 关键字,它所声明的变量就绑定了这个区域,不再受外部的影响。

1
2
3
4
5
6
7
// ReferenceError: Cannot access 'name' before initialization
var name;

if (true) {
name = '周树人';
let name;
}

ES6 明确规定,如果区块中存在 letconst 关键字,这个区块对这些关键字声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

在代码块内,使用 let 关键字声明变量之前,该变量都是不可用的。这在语法上,称为「暂时性死区」(temporal dead zone,简称 TDZ)。

有些死区比较隐蔽,不太容易发现。

1
2
3
4
5
6
7
8
function naming(fullname = `${surname}树人`, surname = '周') {
return fullname;
}

console.log('naming start'); // -> naming start
// ReferenceError: Cannot access 'surname' before initialization at naming
console.log(naming());
console.log('naming end');

上面代码与前面的有所不同,直到 naming 函数被调用前,程序都是可以正常运行的,不会出现报错,因为其通过了语法编译

没错,尽管 JS 是一门弱类型的脚本语言,但它并不是解释型语言,其也是有语法编译的,很多人都不知道这一点PHP:你礼貌么?。前面的几个示例中,代码还没开始执行就报语法错误了,这就是最简单的证明。

naming() 在这里报错,是因为参数 fullname 默认值用到了另一个参数 surname,而此时 surname 还没有声明,属于死区。如果将这两个参数的顺序调整,就不会报错,因为此时变量已经声明了。

1
2
3
4
5
6
7
function naming(surname = '周', fullname = `${surname}树人`) {
return fullname;
}

console.log('naming start'); // -> naming start
console.log(naming()); // -> 周树人
console.log('naming end'); // -> naming end

常量

在 ES6 之前,JS 内所有数据都是变量,可以修改任意值。而 const 关键字用来声明常量,一旦被声明,值将无法更改。

1
2
3
// TypeError: Assignment to constant variable.
const name = '周樟寿';
name = '周树人';

为什么 Object 和 Array 可以随意修改值?因为这两类是引用类型,并非基础数据类型,引用类型内部的值不管怎么变,其引用地址都是不变的。

1
2
3
4
5
6
7
8
const wifes = ['镜华'];
const harem = wifes;

wifes.push('美美');
wifes.push('未奏希');

console.log(harem); // -> ['镜华', '美美', '未奏希']
console.log(harem === wifes); // -> true

当然,这是 JS 的基础知识,由此可以引申出深拷贝浅拷贝,但并不与本文章相关,便不在此做过多的赘述。先挖个坑,才不是因为懒呢。

评论