zires 博客

[NOTE] - var, let, const

March 28, 2018

我们知道,几乎所有的编程语言都离不开变量,而与变量紧密关联的便是作用域

作用域是用来限定变量的可用性范围

JS在设计之初使用var关键字来定义变量,var的作用域是全局或者函数体。全局变量在整个程序(页面)都有定义,而函数作用域是指,如果声明一个变量,那么在整个函数体中都是有定义的。

想到这里,便有了几点疑惑,这些也肯定是设计JS时需要考虑的:

  • 如果一个变量在使用的时候,还没有声明,会出现怎样的情况?
  • 如果一个变量没有声明,直接定义,会出现怎样的情况?
  • 如果一个变量在全局和函数内都声明并且赋值了,在使用的时候,哪个优先级高?

使用未声明的变量

从常理上思考,使用未声明的变量应该出错(抛出异常),如果编写测试用例的话,应该是:

expect(console.log(abc)).to.throw('abc is not defined')

测试通过,考虑到顺序执行,更完善的用例,下面这个也应该测试通过

expect(console.log(abc)).to.throw('abc is not defined')
var abc = 'abc'

但是测试失败了,原因在于,JS有一个独有的特性:声明提升(Hoisting)

Hoisting is JavaScript’s default behavior of moving all declarations to the top of the current scope (to the top of the current script or the current function).

所以,上面的例子等价于:

var abc
expect(console.log(abc)).to.throw('abc is not defined') // abc is undefined
abc = 'abc'

在咀嚼这个特性的时候,不妨思考下,为什么需要设计这个特性,究竟可以带给我们什么好处?

先看下面这段代码:

foo() // 输出'Hello'

function foo() {
  console.log('Hello') 
}

可以看到,函数声明也被提升了。一个显而易见的好处是,我们在调用函数的时候,不用考虑函数声明的位置了。没有强制函数调用必须在声明之后。

JS在设计之初的定位是脚本语言,虽然是一门动态语言,但执行过程却类似编译语言,特别是后来的JITs,JS的性能得到了巨大的提升,也迎来了JS的第二个春天。

一段JS代码会经过编译 => 执行,两个阶段。

而在编译阶段,如果没有声明提升,编译器必须扫完整个代码才会发现声明错误,另外,如果没有声明提升,我们每次更改代码,编译器都必须全部重新扫描一遍,因为它不知道我们会在文件的哪个地方增加或者减少函数声明与定义。

所以,声明提升可以显著提升编译器的性能。

变量直接定义

下面我们来看看直接定义变量会有哪些变化。

a = 1 // 等价于 var a = 1

这里又是JS比较宽松的约定,当在当前作用域下没有找到变量的声明,会自动隐式的创建一个全局变量。

function foo() {
  a = 1 // Oooops...一不小心就创建了一个全局变量
}

变量优先级

同时声明且定义相同的变量,在使用的时候,哪个优先级高呢?

首先我们先明确一点,var声明的变量是可以重复定义的。

var foo = 1
var foo
console.log(foo) // 1  重复声明不会覆盖原有的赋值
var foo = 1
var foo = 2
console.log(foo) // 2  后一个声明且赋值会覆盖前一个
var foo = 1

function test() {
 var foo = 2
 console.log(foo) 
}

test() // 2 函数内的变量优先于全局变量
var foo = 1
function foo() {}
console.log(typeof foo) // number 变量赋值优先于函数声明
console.log(typeof foo) // function 函数声明优先于变量声明 --- 巨坑吧
var foo = 1
function foo() {}

总结

可以看到,var变量的灵活,稍有不小心,会带来许多意料之外的问题,特别是在某个循环中的时候。虽然,我们有严格模式,但总不能在不小心出错的时候,再一遍遍的检查吧。

所以ES6新增了let变量和const变量。

let

let的作用域不再是全局或者函数,而是块级作用域,块就是指由{}包裹的代码块。

{
  let foo = 1
}

let带来的好处是:

  • 作用域限定在块级,粒度更细
  • 未声明前使用会报错
  • 不允许重复声明
  • 声明的全局变量不是全局对象的属性
  • 在循环的每次迭代中都会创建新的绑定

补充一点,网上有资料说,let变量不存在声明提升了,究竟是否是这样呢?之前也说到了声明提升对于编译器来说是有帮助的,使用let变量是否就放弃了这种好处?

其实不然,let变量也会提升,只是提升的行为和var不一样了:

var => 提升 => 初始化为undefined

let => 提升 => 未初始化

未初始化的好处是带来了TDZ,中文名叫暂时性死区,可以理解为变量在声明和被初始化之间的这段时间(代码)是不可以被访问的(锁死),这样就解释了为什么下面的代码会报错:

console.log(foo) // ReferenceError
let foo = 1

const

const表示常量。

const的作用域规则基本上和let一致。只是多了一个限制:

  • 不可以重新赋值

因为只能赋值一次,所以const在声明的时候就必须赋值(想想也是,不然所有的const变量都是undefined了)

总结

基本上能用var的变量都用let或者const替代吧


Zires

Hi~,i am zires, an IT engineer, live in Shanghai. You can get in touch with me via Github stackoverflow