es6变量声明和解构赋值

时间:2023-01-01 23:06:21

/*声明:

* 本文内容多为学习借鉴性内容,大部分非原创

* 特别感谢阮一峰的 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是单线程的,当基本逻辑语句执行完之后,才会执行点击事件
//变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量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新增的一个变量声明方式,拥有块级作用域;
//变量ilet声明的,当前的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明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用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,内容安全政策),那么evalnew 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');
};