JavaScript:var/let/const

188 阅读6分钟

变量提升

下面这段代码没有按照:先声明,后应用,但是并没有报错

b();//b is called
console.log(a);//undefined
var a=1;
function b(){
    console.log("b is called");
}

函数b正常执行了,但a是undefined。

JavaScript 引擎会将变量的声明提升,但赋值还是原来位置,可以理解为:

function b(){
    console.log("b is called");
}
var a;//declare
b();//b is called
console.log(a);//undefined
a=1;//set value
function b(){
    console.log("b is called");
}

将所有的变量声明提到它所在执行环境的最前面(全局执行环境或者函数执行环境),声明之后就再内存中开辟了一块空间,放入‘undefine’ 。赋值之后这块内存里才是我们想要的值。

而函数声明会被整个提到最上面,在执行时,这块内存中已经时我们写的代码了。

var关键词声明的变量都有声明部分的提升,即:在运行时之前将变量的声明提到当前作用域的最顶部。(注意:提升的只有声明,赋值还是在写下的地方)

let和const声明的变量没有提升,必须先声明后使用

// console.log('inside code block ' + a);//ReferenceError: a is not defined
// console.log('inside code block ' + c)//ReferenceError: c is not defined
// console.log('inside code block ' + b);//inside code block !!!undefined!!!!
let a = 'let';
var b = 'var';
const c = 'const'

块作用域

在ES6之前,变量的声明都使用var,JS中没有“块作用域”的概念。只存在全局作用域和函数作用域。

但用let和const声明的变量都只在块作用域内生效,即生命周期从进入块开始({),到出块结束(})。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

在第一个demo中,i使用var声明,作用域是全局的,最后数组a中保存的10个函数中的i都是指向这同一个i
在第二个demo中,i使用let声明,for循环实际上是10个代码块,数组a中保存的函数中的i是指向十个不同的i。(算闭包吗?)

块作用域防止了变量对外层覆盖

var tmp = 'outside';
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'inside';
  }
}
f();//undefined

上面这段代码因为变量提升,实际运行之前的状态是

var tmp = 'outside';
function f() {
  var tmp;//js引擎会为所有未赋值的变量赋值为undefined
  console.log(tmp);
  if (false) {
    tmp = 'inside';
  }
}
f();//undefined

但如果对if代码块内部的变量声明使用let

var tmp = 'outside';
function f() {
  console.log(tmp);
  if (false) {
    let tmp = 'inside';
  }
}
f();//outside

块作用域防止了对全局环境的污染

var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}
console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

var s = 'hello';
for (let i = 0; i < s.length; i++) {
  console.log(s[i]);
}
console.log(i); // ReferenceError: i is not defined

块作用域改变了函数声明的提升

下面的domo

function f() { console.log('I am outside!'); }
(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());

在ES5下

function f() { console.log('I am outside!'); }
(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}()); // I am inside

在ES6浏览器下

function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}()); //Uncaught TypeError: f is not a function

避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

不允许重复声明

var可以重复声明变量,而let和const都不可以。

不可修改

const声明的变量不可修改。因为不可修改,所以const声明的变量也必须立即赋值。

首先:const声明的变量是在对应的栈内存中不可修改。很多文章都提到了这一点,如果是用const声明的对象,因为栈中存储的是指针,真正的值在堆内存中,这个对象属性是可以被修改,添加的。

但栈内存的特点是不可修改(即:即使使用var声明的原始类型变量,在修改变量时,实际也不是在原有内存空间上做改动,而是去重新开辟一块新的内存,存储新的值)。const声明的变量本质应该是:对应的栈内存空间不可改变。

for (const i = 0; i < 3; i++) {
    console.log(i);
}
// TypeError: Assignment to constant variable.

上面这段代码在i++操作试图修改i的值,实际是给i去重新分配一块栈内存,const不允许这种操作。

但如果是 const in语句是可以的,效果同var in

obj = {
    first:'1',
    second:'2'
}
for (const pro in obj) {
    console.log(pro);
}
//first
//second

因为const in 操作并没有试图改变原有值,而是每次循环都是一个代码块,都有一个const声明在当前代码块中的变量。没有出现过在栈中重新分配内存的操作。

for循环与新特性

使用var时:

for (var i = 0; i < 3; i++) {
    var i = 'abc';
    console.log(i);// 一次abc
}
console.log('abc'==3)

因为var存在变量提升和重复声明,上面的代码可以看作

var i;
for (i = 0; i < 3; i++) {
    i = 'abc';
    console.log(i);
}

'abc'<3返回false,for代码块只循环依次

使用let时:

for (let i = 0; i < 3; i++) {
    let i = 'abc';
    console.log(i);// 三次abc
}

前面说过let不允许重复声明变量,这里也说明了for括号内声明的变量有一个隐式的代码块。可以看成

{
    let i = 0;
    {
        let i = 'abc';
        console.log(i);
    }
}
{
    let i = 1;
    {
        let i = 'abc';
        console.log(i);
    }
}
{
    let i = 2;
    {
        let i = 'abc';
        console.log(i);
    }
}

暂时性死区

在JS中,变量可以不声明,直接赋值,这时候被赋值的变量会被绑定到全局对象上。

if (true) {
    tmp = 'abc'; // 这句必须要,没有声明可以,但一定要赋值
    console.log(tmp); // ReferenceError
}
console.log(this)
//window{
    ...
    tem:'abc',
}

但在使用了let之后,产生暂时性死区,在let存在的块作用域内,let关键词之前都是暂时性死区。

if (true) {
    // TDZ开始
    tmp = 'abc'; // ReferenceError
    console.log(tmp); // ReferenceError
    let tmp; // TDZ结束
}

顶层对象

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

浏览器下var

var a = 1;
console.log(this.a) // 1

浏览器下var

let a = 1;
console.log(this.a) // undefine

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。

ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。