ES6中var、let、const对比和各种踩坑解析

一:变量作用域问题:

  var a = 1; //此处声明的变量a为全局变量
  function foo(){
      var a = 2;//此处声明的变量a为函数foo的局部变量
      console.log(a);//2
  }
  foo();
  console.log(a);//1
复制代码
 var a = 1;
 console.log(a); // 1
 if (true) {
   var a = 2;
   console.log(a); //2
}
 console.log(a);// 2
复制代码

为何两段代码输出的不一样???

因为::JavaScript只有全局作用域和函数级作用域,没有块级作用域。所以在第一段代码中的var a=2 只在函数foo()中起作用,并不会污染外部的全局变量。而if代码块不会创建新的作用域,所以在第二段代码中的if语句块中,会污染全局变量a,从而改变了a的值。

【而在C语言或者Java语言中,if等块级作用域也会声明新的变量,不会污染外部的全局变量】

如果在函数作用域内声明变量时省略了var 那么该变量就会变为全局变量,从而污染全局变量的值。如:

  var a = 1; //此处声明的变量a为全局变量
  function foo(){
      a = 2;//此处的变量a也为全局变量
      console.log(a);//2
  }
  foo();
  console.log(a);//2
复制代码

那如何解决函数作用域内即不污染外部公共变量,又不污染函数作用域内的变量呢?
那就是在函数作用域内加个临时作用域

    var x = 3;
    function foo() {
        var x = 1;
        console.log(x); //foo作用域变量x,输出1

        (function () {
            var y = 2;
            console.log(x); //foo作用域变量x,输出1
            console.log(y); //临时作用域变量y,输出2
        })();

        console.log(y); //临时作用域变量y只在临时作用域内有效,提示 y is not defined
    }

    foo();
    console.log(x); //全局变量x,输出3
复制代码

此时,又出现了新的问题,临时作用域变量y在临时作用域外部无法获取,如何解决这个问题呢?

    var x = 3;
    function foo() {
        var x = 1;
        console.log(x); //foo作用域变量x,输出1

        var temporary_res = (function () {
            var y = 2;
            return y;
        })();

        console.log(temporary_res); //接收的临时作用域变量y,输出2
    }

    foo();
    console.log(x); //全局变量x,输出3
    console.log(temporary_res); //只在foo函数内有效,此处提示 temporary_res
复制代码

image.png

二:var变量提升问题:

    var v='Hello World';

    (function(){
        console.log(v);
    })()
复制代码

这种情况下,是可以打印出Hello World的。

下面,我们稍加改造:

    var v='Hello World';

    (function(){
        console.log(v);
        var v='I love you';
    })()
复制代码

这时,就会提示undefined,
这是为什么呢?

image.png

因为在临时作用域内多加了这么一个同名变量v的声明赋值语句,var v = ‘I love you’;

那么,本着块级作用域不会污染外部全局变量的原则,只要作用域内加了同名变量的声明,那么该声明的变量只会在该作用域内有效,外部的同名全局变量在本作用域内就失效了。

不是很理解的话可以看看下面这段代码打印输出的结果。

    var x = 3;
    function foo() {
        var x = 1;
        console.log(x); //foo作用域变量x,输出1

        (function () {

            var x = 2;

            console.log(x); //临时作用域变量x,输出2
        })();

        console.log(x); //foo作用域变量x,输出1
    }

    foo();
    console.log(x); //全局变量x,输出3
复制代码

image.png

那么,上面的代码我们再来改造一下,临时作用域内只给x赋值为2,不做变量声明,那么,此时在临时作用域内的x=2就代表着把foo作用域变量x的值重新赋值为2。

    var x = 3;
    function foo() {
        var x = 1;
        console.log(x); //foo作用域变量x,输出1

        (function () {
            console.log(x); //foo作用域变量x,输出1
            x = 2;
            console.log(x); //改变了foo作用域x的值,输出2
        })();

        console.log(x); //foo作用域x的值,输出2
    }

    foo();
    console.log(x); //全局变量x,输出3
复制代码

image.png

如果此时块级作用域内声明并赋值的变量不是v,而是其他的变量名,那么,这时运行代码就不会提示undefined了。

    var v='Hello World';

    (function(){

        console.log(v);
        var a='I love you';
    })()
复制代码

image.png

深层次的解释一下:
全局声明变量赋值 var x = 1;
作用域内声明变量赋值 var x = 2;
这相当于在内存地址中开辟了两块存储空间,虽然两者同名,但是在内存中却是不同的存储地址,所以在代码调用时,实际上是根据不同的内存地址找到的对应的值。

如果在局部作用域中不声明变量而是直接写x=2,那就是把全局变量x内存地址中的值改为了2。

image.png

那么,这样表面上看是多了一句 var v=’I love you’; 是在临时作用域内做变量声明并赋值,实际上就是在内存中新开辟了一块栈存储空间,并进行赋值。

那这样能理解明白的话,那么临时作用域中的console.log打印的v就不难理解了,就是在内存中新开辟的存储空间的值。

其实这里还隐藏着变量提升的问题。

var v=’I love you’; 这句代码可以拆分成两部分,一是声明变量,二是给变量赋值。
拆分之后如下:

    var v='Hello World';

    (function(){
        var v;
        console.log(v);
        v ='I love you';
    })()
复制代码

所以才会提示undefined。因此,我们在写代码时就应该养成变量声明赋值应放在作用域顶部,防止出现上面类似的问题而一头雾水。

三:函数提升

声明函数方式可以进行函数提升

    function test(){
		    
        function foo(){
            alert("我来自 foo");
        }

        foo();
    }

    test();
复制代码
    function test(){
	foo();	    
        function foo(){
            alert("我来自 foo");
        }

    }

    test();
复制代码

后面这个方法就存在函数提升,提升后就是上面的写法。

函数表达式方式就不能进行函数提升了。

    function test(){
	foo();	    
	var foo =function foo(){
            alert("我来自 foo");
        }

    }
		
    test();
复制代码

image.png

正确的写法应该是:

    function test(){
	    
	var foo =function foo(){
            alert("我来自 foo");
        }
        
        foo();

    }
		
    test();
复制代码

感谢浪迹天涯博主分享的文章

www.cnblogs.com/damonlan/ar…

四:ES6 let

其实,在ES6(ES2015)出现之前,JavaScript声明变量只有两种方式,那就是var和function,可是,上面出现的那些问题,很多时候对于刚入职甚至入职很久的朋友来说都很难避免出现失误,所以在实际工作中踩的坑多,总结的经验就多。别人踩过的坑总结的经验我们可以学习借鉴。

ES6之后,声明变量增加了let、const,而且ES6标注的很明确,let、const声明的变量只在本作用域内有效,这样的话,我们在ES6开发中只要记住使用let、const声明的变量只在本作用域内有效就行了,而且同一个作用域内不能重复声明变量。如果作用域内的变量没有使用let声明而是直接赋值,那说明就是将作用域外部的全局变量更新值。这样记起来多方便。

下面来看一个经典案例。

    for (let i = 0; i < 10; i++) {
	setTimeout(function(){
            console.log(i);
	},100)
    };
		
    console.log(i);
复制代码

image.png

因为let i是在for循环内部定义的,所以i的作用域范围只能是for循环内部,外部打印i时就提示 i is not defined。for循环内部每一次循环其实都是一个新的变量i,所以打印从0到9的数字。至于为什么先显示外部的i找不到,是因为setTimeout延时造成的。

for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

    for (let i = 0; i < 3; i++) {
	let i = 'abc';
	console.log(i);
    }
复制代码

image.png

上面代码正确运行,输出了 3 次abc。这表明函数内部声明的变量i与循环体变量i不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)。

如果for循环内的i直接赋值为abc的话,那么,此时就是改变的for循环体内i的值,那么console也只能打印一次abc出来。

    for (let i = 0; i <= 3; i++) {
	i = 'abc';
	console.log(i);
    }
复制代码

image.png

    for (let i = 0; i < 3; i++) {
	console.log(i);
	let i = 'abc';
	console.log(i);
    }
复制代码

image.png

如果在for循环体内的let i声明变量之前打印i时,就是提示初始化前无法访问i。这样也更加证明了函数内部的变量i跟循环体变量i不在同一个作用域。

    for (let i = 0; i <= 3; i++) {

	console.log(i);
	i = 'abc';
	console.log(i);
    }
复制代码

如果循环体内的i是直接赋值,那么,此时打印出来的就是0和abc。

image.png

再来看一个很有迷惑性的for循环代码

    var a = [];
    var b = [];
    for (var i = 0; i < 10; i++) {
	console.log(i); //打印 0-9

	a[i] = function () { //数组下标 0-9,每次循环的时候,都会给全局变量a赋值不同的下标i。 a[i]实际就是一个函数表达式

            console.log(i); //这里不要被迷惑了,因为i是全局变量,虽然每次循环时,这里的i是0-9的数字,但是,这里的方法不会立即执行,i作为全局变量是一直累加的,直到循环体i++ 后等于10不满足循环条件i<10 才停止下来,所以,全局变量i最后的值是10
	};

	b[i]=i; //每次循环时都给全局变量b赋值不同的下标i,对应下标也赋值不同的值i

    }


    a[6](); //这里实际就是执行函数表达式,打印全局变量i,而通过for循环后,全局变量i最后的值是10,所以此处打印值为10

    console.log(b[6]); //b[i]在循环体内每次被赋值后作为全局变量b的子内容被保存下来,所以这里打印值为6
复制代码

image.png

五:ES6 let不存在变量提升

    // var 的情况
    console.log(a); // 输出undefined
    var a = 2;

    // let 的情况
    console.log(b); // 报错ReferenceError【引用错误】
    let b = 2;
复制代码

image.png

六:ES6 暂时性死区和块级作用域

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

下面我们把文章一开头的代码段使用let进行变量声明来看一下打印结果:

    let a = 1;
    console.log(a); // 1
    if (true) {
       let a = 2;
       console.log(a); //2
    }
    console.log(a);// 1
复制代码

image.png

同样的代码,使用var进行变量声明时,最后打印的结果是 1 2 2。

    var a = 1;
    console.log(a); // 1
    if (true) {
       var a = 2;
       console.log(a); //2
    }
    console.log(a);// 2
复制代码

image.png

这里就说明了if代码块中使用let 重新声明a变量时,if区块内是该新声明的变量a的作用域,出了这个区域就失效了。

而使用var声明变量时,if区块就不会作为有效区域了,只有在函数内声明才能作为有效作用域。

注意:ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

    // 第一种写法,报错
    if (true) let x = 1;

    // 第二种写法,不报错
    if (true) {
      let x = 1;
    }
复制代码

上面代码中,第一种写法没有大括号,所以不存在块级作用域,而let只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。

那么,这里的暂时性死区的举例代码又牵扯出了一个知识点。

七:ES6 let不允许变量重复声明。

    var a = 1;
    var a = 2;
    console.log(a);// 2
复制代码

image.png

    let a = 1;
    let a = 2;
    console.log(a);
复制代码

提示变量a被重复声明

image.png

八:使用var声明的循环变量泄露为全局变量

示例:

var s = 'hello';

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

console.log(i); // 5
复制代码

image.png

按说,当循环结束后,i应该被销毁,可是却被泄露成了全局变量,假如在for循环内部添加判断条件进行变量赋值等,也应该是在for循环外提前定义好变量,在for循环内部根据判断条件赋值后,在for循环结束后使用重新赋值的变量,而不是使用i,这样非常容易造成污染全局变量。

九:ES6 const常量声明

const声明一个只读的常量。一旦声明,常量的值就不能改变。

    const PI = 3.1415;
    PI = 3;
复制代码

上面代码表明改变常量的值会报错,说明const声明的变量不得改变值。

const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined
复制代码

image.png

const声明的常量也不能提升,同样存在暂时性死区,只能在声明的位置后面使用。

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}
复制代码

const声明的常量,也与let一样不可重复声明。

var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
复制代码

const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。

但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

因此,将一个对象声明为常量必须非常小心。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
复制代码

上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错
复制代码

上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

总结:
ES6 之前只有两种声明变量的方法:var命令和function命令。ES6 除了添加letconst命令,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

上面已经对比了这么多的代码和示例,对于以后使用ES6还是ES5,应该自己有明确的判断了吧。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享