es6变量声明和解构赋值
Posted 东庄
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了es6变量声明和解构赋值相关的知识,希望对你有一定的参考价值。
/*声明:
* 本文内容多为学习借鉴性内容,大部分非原创
* 特别感谢阮一峰的 ECMAScript6 入门,推荐大家学习
*/
一.es5变量声明的不足
1.变量提升和函数声明提升
es5的代码加载顺序是自上而下的但是 var和function(不包含函数表达式)的声明,(不包含赋值),会得到优先加载,加载时提升到作用域的top位置
函数内的变量声明会提升到函数内部代码的顶部,优先运行;
并且变量的值为undefined;
(1)栗子1
var time = new Date(); function tm(){ if (false) { var time = "aa"; }; console.log(time) } tm();
// 在控制台输出的是 undefined ,这是因为var 声明的变量会先提升
上述代码等价于一下内容
var time = new Date(); function tm(){ //var time; //函数内局部环境下声明会覆盖全局作用域,同时if内的声明得到提升到function的top位置,但此时time仅仅是声明提升,并没有赋值,所以运行console时输出unidefined; if (false) { var time = "aa"; }; console.log(time) } tm(); // 在控制台输出的是 undefined ,这是因为var 声明的变量会先提升
(2)栗子2
fn(); console.log(a);
var a = 0; console.log(a);
function fn(){ var a = 1; }
//控制台输出 undefined和0
此处,函数声明先得到提升,然后var a=0;变量声明得到提升(但此时并没有变量赋值,函数体内的为局部变量),如下代码
fn();
var a; //此处a是由 var a=0;提升而来; console.log(a); var a = 0; console.log(a); function fn(){ var a = 1; } //控制台输出 undefined和0
(3)栗子3
fn() alert(a) var a = 0; alert(a); function fn(){ a = 1; }
//此处控制台输出的是 1 和 0;
//因为 a=1;没有关键词直接赋值使用在非严格模式下默认挂到window环境下,优先执行了声明和赋值;
//但是在严格模式下这种写法是错误的;
(4)栗子4
重复声明的变量
在同一作用域下,后声明的变量会覆盖之前声明的变量
不同作用域下局部变量会覆盖外层变量(外层为window则覆盖全局变量)
此处,不举例;
二.es6的let
1.同一作用域下的let 声明的变量是不能被覆盖的,即不允许重复声明
(1)栗子1
{
let a = 1; let a = 2; } //Identifier ‘a‘ has already been declared
2.es6新增的 let 命令所声明的变量只在let命令所在的代码块内有效 --- 块级作用域
(1)栗子1
{ let a=1; var b = 22; }
console.log(b) //22;
console.log(a) //报错 Uncaught ReferenceError: a is not defined
(2)栗子2,
在for循环中十分适合使用let
for(let i = 0; i<15;i++){ console.log("inner"+i) //inner 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
}
console.log("outer"+i) //Uncaught ReferenceError: i is not defined 这样变量i不会覆盖外层的变量
(2)栗子3,
闭包问题的解决
<!-- 在页面添加三个按钮 --> <input type="button" value="1">
<input type="button" value="2"> <input type="button" value="3"> <script> // 获取页面所有的input var aBtn = document.getElementsByTagName(‘input‘); // 循环绑定点击事件,当然毫无疑问这里点击之后会弹出3,
//因为我们知道js是单线程的,当基本逻辑语句执行完之后,才会执行点击事件
//变量i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。
//每一次循环,变量i
的值都会发生改变,而循环内被赋给数组aBtn
的函数内部的alert(i)
,里面的i
指向的就是全局的i
。
也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是3。 //当你触发点击事件的时候,for循环都已经跑完了,所以i已经变成了3; for(var i = 0; i < aBtn.length; i++){ aBtn[i].onclick = function(){ alert(i); } } // 解决方法1:也是最简单的方法,就是将for循环的var变成let //这样当点击每个按钮的时候,就会依次弹出0,1,2; //let是ES6新增的一个变量声明方式,拥有块级作用域;
//变量i
是let
声明的,当前的i
只在本轮循环有效,所以每一次循环的i
其实都是一个新的变量,所以最后输出的是6
。
// javascript 引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算 for(let i = 0; i < aBtn.length; i++){ aBtn[i].onclick = function(){ alert(i); } } //解决方法2:利用闭包(说是闭包,貌似也不完全是,也就是函数作用域访问原则的特性 //函数内部可以访问外部的变量,外部却访问不了里边的; for(var i = 0; i < aBtn.length; i++){ (function(i){ aBtn[i].onclick = function(){ alert(i); } })(i); } </script>
(4)栗子4,
for的两个作用域
for (let i = 0; i < 3; i++) {//就是设置循环变量的那部分是一个父作用域 let i = ‘abc‘; //而循环体内部是一个单独的子作用域
console.log(i); }
// abc
// abc
// abc
for (let i = 0; i < 3; i++) { console.log(i); }
// 0
// 1
// 2
3.不存在变量提升
var 命令会发生"变量提升" 的现象,即变量可以在声明之前使用,值为undefined;
而 let 要求更严格,let命令要求它所声明的变量一定要在声明后使用,否则报错;
(1)栗子1
//使用var
console.log(a); //输出undefined var a = 1; //使用let console.log(b);
//此处会报错 Uncaught ReferenceError: b is not defined
//这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。
let b = 2;
***
注:以下很多问题可以通过let变量 "先声明后使用" 来进行理解
***
4.暂时性死区
* 只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响 *
(1)栗子1
var tmp = 123; if (true) { tmp = ‘abc‘; //Uncaught ReferenceError:tmp is not defined let tmp; }
上面代码存在全局变量tmp ,本来可以正常运行的;但是在块级作用域内let又声明了一个局部变量 tmp 导致 let 将tmp绑定在这个区域内,(并非整个作用域,从使用let到let所在作用域的顶部的区域)
所以tmp在let声明前就调用了,对tmp的赋值运算就会报错
ES6明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
(2)栗子2
暂时性死区的范围
if (true) { // TDZ开始 tmp = ‘abc‘; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ结束 console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123 }
虽然TDZ结束了但是在TDz开始时就已经报错,程序已经无法正常运行了
(3)栗子3
typeof 会报错
在没有let
之前,typeof
运算符是百分之百安全的,永远不会报错
“暂时性死区”也意味着typeof
不再是一个百分之百安全的操作
//暂时性死区导致报错 typeof x; // ReferenceError let x; //没有暂时性死区 使用typeof检测一个未申明的变量也不会报错而是返回undefined typeof un_defin_ed //undefined
(4)栗子4
比较隐蔽的暂时性死区
//使用var的情况 var x=x; console.log(x); //undefined //使用let的情况 let x = x; //ReferenceError : x is not defined console.log(x);
//解构赋值
function bar(x = y, y = 2) { return [x, y]; } bar(); // 报错
上面代码中,调用bar
函数之所以报错(某些实现可能不报错),是因为参数x
默认值等于另一个参数y
,而此时y
还没有声明,属于”死区“。如果y
的默认值是x
,就不会报错,因为此时x
已经声明了。
5.块级作用域
let
实际上为 JavaScript 新增了块级作用域。
外层作用域无法读取内层作用域的变量 , 这表示外层代码块不受内层代码块的影响。
内层作用域可以定义外层作用域的同名变量 , 内层作用域可以使用外层作用域的变量。
(1)栗子1
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式( IIFE )不再必要了
// IIFE 写法 (function () { var tmp = ...; ... }()); // 块级作用域写法 { let tmp = ...; ... }
(2)栗子2
函数能否在块级作用域内声明;
ES5规定函数只能在顶层作用域和函数作用域中声明,不能再块级作用域中声明(不包括函数表达式);
// 情况一 if (true) { function f() {} } // 情况二 try { function f() {} } catch(e) { // ... }
上面两种函数声明,根据 ES5 的规定都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
function f() { console.log(‘I am outside!‘); } (function () { if (false) { // 重复声明一次函数f function f() { console.log(‘I am inside!‘); } } f(); }()); //上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。 // ES5 环境 function f() { console.log(‘I am outside!‘); } (function () { function f() { console.log(‘I am inside!‘); } if (false) { } f(); }());
ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢? 原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6在附录B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。 允许在块级作用域内声明函数。 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。 同时,函数声明还会提升到所在的块级作用域的头部。 注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。 根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。
// 浏览器的 ES6 环境 function f() { console.log(‘I am outside!‘); } (function () { if (false) { // 重复声明一次函数f function f() { console.log(‘I am inside!‘); } } f(); }()); // Uncaught TypeError: f is not a function 上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。 // 浏览器的 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
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 函数声明语句 { let a = ‘secret‘; function f() { return a; } } // 函数表达式 { let a = ‘secret‘; let f = function () { return a; }; }
还有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。
// 报错 ‘use strict‘; if (true) function f() {}
三.const 命令
(一)基本特性
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
而且对于const
来说,只声明不赋值,就会报错。
const bntt; Uncaught SyntaxError: Missing initializer in const declaration
const 声明变量和let有相似之处 :
变量声明不提升,
暂时性死区,块级作用域,
不可重复声明.
(二)本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,
const
只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only Assignment to constant variable.
上述代码中常量foo储存的是一个地址,这个地址指向一个队形.不可变的是这个地址,即不能把foo指向另一个地址,但是对象本身是可变的,所以可以为其添加新的属性;
//这个栗子原理同上 coonst a = []; a.push("aa") ;//可执行 a.length = 0; //可执行 a=["bb"] ; //报错
(三) 冻结对象
Object.freeze()
方法可以冻结一个对象(浅冻结),
冻结指的是不能向这个对象添加新的属性,
不能修改其已有属性的值,
不能删除已有属性,
以及不能修改该对象已有属性的可枚举性、可配置性、可写性。
也就是说,这个对象永远是不可变的。该方法返回被冻结的对象。
语法 Object.freeze(obj)
//下面是一个将对象彻底冻结的函数。 var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === ‘object‘ ) { constantize( obj[key] ); } }); };
四.顶层对象的属性
1.顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。
2.
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window
对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1; // 如果在 Node 的 REPL 环境,可以写成 global.a // 或者采用通用方法,写成 this.a window.a // 1 let b = 1; window.b // undefined 上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。
3.
ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。
- 浏览器里面,顶层对象是
window
,但 Node 和 Web Worker 没有window
。 - 浏览器和 Web Worker 里面,
self
也指向顶层对象,但是 Node 没有self
。 - Node 里面,顶层对象是
global
,但其他环境都不支持。
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this
变量,但是有局限性。
- 全局环境中,
this
会返回顶层对象。但是,Node 模块和 ES6 模块中,this
返回的是当前模块。 - 函数里面的
this
,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this
会指向顶层对象。但是,严格模式下,这时this
会返回undefined
。 - 不管是严格模式,还是普通模式,
new Function(‘return this‘)()
,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全政策),那么eval
、new Function
这些方法都可能无法使用。
//综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。 // 方法一 (typeof window !== ‘undefined‘ ? window : (typeof process === ‘object‘ && typeof require === ‘function‘ && typeof global === ‘object‘) ? global : this); // 方法二 var getGlobal = function () { if (typeof self !== ‘undefined‘) { return self; } if (typeof window !== ‘undefined‘) { return window; } if (typeof global !== ‘undefined‘) { return global; } throw new Error(‘unable to locate global object‘); };
以上是关于es6变量声明和解构赋值的主要内容,如果未能解决你的问题,请参考以下文章