最近在学习ES6的语法,刚好把自己整理的笔记搬到简书上
一、let
1.作用:声明变量
2.特点:块级作用域 + 没有变量提升 + 暂时性死区 + 不允许重复声明
① 作用域:块级作用域
在ES5之前没有块级作用域的概念,所以会出现下面两种情况:
情况一:内层变量覆盖外层变量【本质是变量的提升】
var tem = 'abc';
function add(){
console.log(tem);
if(true){
var tem = 'ABC';
console.log(tem);
}
}
add() // undefined ABC
本意是希望第一个打印语句打印abc,第二个打印语句是ABC。但是由于没有块级作用域的概念,函数内的tem会提升到函数作用域的最顶部,所以先打印出undefined,在打印出ABC。
情况二:泄露成全局变量【还是变量的提升】
var s='hello'
for(var i=0;i<s.length;i++){
console.log(s[i]);
}
console.log(i) //5
原本只是希望通过 i 来控制循环的进行,但是当循环执行完毕时,i 并没有消失,反而泄露成了全局变量,所以打印 i 的值是5.
随后,为了解决上述的两种情况,ES6中提出了块级作用域的概念。
{
let a=1;
var b=2;
}
console.log(a); //ReferenceError:a is not defined
console.log(b) //2
上述代码中,a的作用域只在{}内有效,所以在{}外打印a会报错;而var声明的变量具有全局作用域的,所以打印b的结果为2;
应用一:作用域的嵌套
{{{{ {let a=10;} console.log(a) //报错}}}}
第四层中的作用域无法使用第五层作用域中声明的变量。
应用二:内层作用域可以使用外层作用域同名的变量
{{{{
let a=10;
{let a=11;}
console.log(a) //10
}}}}
应用三:for循环中i变量的声明
#使用var声明的变量
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
var声明的变量具有变量提升的概念。会将for循环内声明的 i 提升到整个作用域并将 i 挂载到全局对象下,因此此时的i是全局变量,而函数内的i指向的就是全局变量,当for循环执行完成后,i的值变成了10,所以不管调用几次,打印出来的结果都是10
#使用let声明的变量
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
let声明的变量没有变量提升的概念。因此for循环内声明的 i 只在本轮循环有效,相当于每执行一次for循环就会声明一个新的 i 并赋值,所以最后输出的结果是6.
注:
① 如何知道上一轮i的值? JavaScript 引擎内部能够记录上一轮循环的值,在执行本次循环初始化i时,其实是基于上一轮i的值做的计算;
② for循环具有两个作用域。设置循环变量的是父级作用域,而循环体内的是子级作用域,并且两者的作用域并不相同,都是单独运行。
for ( let i=0;i<10;i++){
let i='a';
console.log(i) //a
}
应用四:在块级作用域内声明函数
在ES5之前,函数的声明只能在全级作用域或者函数最顶层作用域之中,不允许在块级作用与中声明。
if(true){
function add(){
console.log(1);
}
}
function add(){
if(true){
function mul(){
console.log(2);
}
}
}
上面两种情况再ES5中是非法的,浏览器为兼容以前的代码,还是支持这种函数声明的方式,此时整个函数会被提升到作用域的最前面。
随后ES6明确,函数的声明可以在块级作用域中,在块级作用域之外是不可以使用的。
(function () {
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
//ES5环境: I am inside
//ES6环境: f is not a funcion
类似于全局变量和局部变量同时存在时局部变量优先的原则,函数会选用第二个声明的函数。
在ES5环境中,会将整个函数都提升到全局作用域或者函数作用域的最顶部,即等价于下面的代码:
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
所以在ES5环境中打印的是 I am inside。但是在ES6环境中声明的函数依旧会报错,这是因为会对老代码产生影响,因此允许浏览器有以下行为:
第一:允许在块级内声明函数;
第二:函数的声明类似var,即将函数的声明提升到全局作用域和函数作用域的最顶层;
第三:函数的声明还可以提升到块级作用域的最前面。
但是仍然不建议在块级作用域中声明函数,而应该使用函数的表达式。
// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}}
// 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};}
② 没有变量提升
所谓的变量提升,就是将变量的声明提升到全局作用域的最前面或者函数作用域的最顶部,而赋值还是在原来的位置。
var声明的变量具有变量的提升,也就是在声明语句之前可以使用变量,但是变量的值是undefined;而let声明的变量不具有变量提升的概念,变量只能在声明语句之后使用,否则会报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
③ 暂时性死区
本质:当进入到某个变量的作用域时,该变量已经存在了,但是不能使用,直到出现变量的声明语句,该变量才可以被使用。
常见的暂时性死区如下:
场景一: 全局变量+局部变量
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
当全局变量和局部变量同时存在时,局部变量优先的原则。所以会使用let声明的变量;而let声明的变量在没有声明该变量时就提前使用,会报错,因为在声明语句之前都是该变量的"死区",只有等到声明语句出现,该变量才可以使用。
场景二:使用typeof
typeof x; // ReferenceError
let x;
在声明语句之前都是变量的死区,所以使用typeof时,会报错,因为该变量没有声明却被使用了。
场景三: 一个变量引用另一个变量
function bar(x = y, y = 2) {
return [x, y];}
bar(); // 报错
function bar(x = 2, y = x) {
return [x, y];}
bar(); // [2,2]
将y的值赋值给x,此时的y并没有声明,在y的死区使用y是错误的,但是如果将x的值赋值给y就不报错,因为x已经被声明了。
场景四: 同一个变量声明和赋值
var x = x //不报错
let x = x //报错
var声明的变量具有变量提升的概念,将x提升到作用域之前,因此在给左边的x赋值时,右边的x已经存在并,并且值为undefined;但是let声明的变量没有变量提升的概念,在给左边的x赋值时,右边的x存在但是不可以使用,因为声明语句没有出现,右边的x处于'死亡'的状态,,没有被激活。
④ 不允许重复声明
这句话的本质是:不允许在相同的作用域内,重复声明同一个变量
let声明的变量不允许在同一个作用域内重复声明,但是在不同的作用域内是可以重复声明的。
场景一:同作用域同变量
function add(){
let a = 10;
var a = 11;
}
add() //报错
等价于:
function add(){
var a;
let a = 10;
a = 11;
}
add()
var声明的变量具有变量提升的概念,将a提升在函数内部的最前面,而let声明的变量是块级作用域的概念。也就是说var和let声明的a的作用域都是在函数add内部而且声明的都是同一个变量a,所以会报错。
场景二:对函数参数重复声明
function add(args){
let args=10;
}
add(1);
函数的参数args的作用域就是在函数的内部,而let声明的args作用域也是在函数的内部,并且两者同名,是同一个变量,所以会报错,但是缩小let声明变量的作用域就不会报错。
场景二:对函数参数重复声明
function add(args){
{
let args=10;
}
}
add(1);
此时let声明的args作用域只是在函数内的{}中,而函数的参数args作用域是整个函数的内部,作用域不同,所以不会报错。
二、const
1.作用:声明变量和只读的常量
声明的常量值不能够改变,否则会报错
const PI = 3.14159
PI = 3 //TypeError: Assignment to constant variable. 对常量赋值
声明的变量必须在声明的同时也要初始化
const a //Uncaught SyntaxError: Missing initializer in const declaration 缺少对变量的初始化
2.特点: 块级作用域 + 没有变量提升 + 不允许重复声明 + 暂时性死区
① 块级作用域:和let用法相同
{
const a=12;
}
console.log(a) //Uncaught ReferenceError: a is not defined
② 没有变量提升:和let用法相同
console.log(b);
const b=12; //Uncaught ReferenceError: Cannot access 'b' before initialization 初始化前不能使用b
③ 不允许重复声明:和let用法相同
var a=10;
let b=10;
const a=11; //Uncaught SyntaxError: Identifier 'a' has already been declared
const b=11; //Uncaught SyntaxError: Identifier 'b' has already been declared
④ 暂时性死区:和let用法相同
if(true){
console.log(a);
const a=10; //Uncaught ReferenceError: Cannot access 'a' before initialization
}
3.本质
const声明的变量并不是指变量的值不可以改变,而是变量指向的内存地址中的数据不得改变,一般对基本数据类型和复合数据类型分开讨论
对于基本数据类型,数据是存放在栈中,变量的指向就是实际的数据。因此使用const声明的基本数据类型,在栈中的实际数据是不可以改变的。
对于复合数据类型【对象、数组】,数据存放在栈中的是指向堆中实际数据的指针,也就是说变量的指向实际上只是一个指向实际数据的指针,而实际的数据则存放在堆中,因此使用const声明的复合数据类型,不可改变的是在栈中存放指向实际数据的指针的指向不可以改变,而实际的数据可以发生改变。
const foo ={}
foo.name='zhangsan';
console.log(foo.name); //zhangsan
foo = {} //Uncaught TypeError: Assignment to constant variable.
可以对对象增加新的属性,说明对象的数据是可以发生改变的。但是将foo重新赋值成一个空的对象,实际上就是将foo存放在栈中的指针指向从原来的空对象变成指向另一个空对象,这是不允许的。
const arry = [];
arry.push(1);
console.log(a[0]); //1
arry = ['A']; //Uncaught TypeError: Assignment to constant variable.
对于数组而言,同理,可以对数组增加新的数组元素,但是将数组重新赋值到另一个新的数组时,就会报错,原理同上。
如果不想对象的数据也发生改变,那么应该使用 object.freeze() 方法将对象冻结起来。
const foo =Object.freeze({})
对象被冻结了,那么对象的属性应该也需要被冻结,下面是将对象的属性冻结的函数
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});};
彩蛋系列
系列一:ES6声明变量的6中方式
ES5中使用 var 和 function声明变量,ES6中除了新增的**let **和 const,还有后面提及的 import 和 class。
系列二:顶层对象
所谓的顶层对象,在浏览器中指的是window,在Node环境中指的是global。
在ES5之前,声明的全局变量和顶层对象的属性是等价的,但是由于种种原因,我们并不希望全局变量和顶层对象的属性之间是等价的,所以在ES6之后,使用 var 和 function 声明的变量依旧和顶层对象的属性是等价关系,使用 let 、const 和 class 声明的全局变量不再等价于顶层对象的属性.
var a=10;
console.log(window.a); //10
let b=11;
console.log(window.b); //undefined
系列三:globalThis对象
JavaScript语言存在一个顶层对象,该对象主要提供全局环境,所有的代码都运行子啊全局环境中。
不同的环境顶层对象是不一样的,比如在浏览器中,window是顶层对象;在 Web Worker中self是顶层对象,在Node中,global是顶层对象。
同一段代码,为了能够在各个环境中都能够去得到顶层对象,我们采用this变量来获取,但是有以下的局限性:
第一:全局环境中,this返回顶层对象,在Node.js模块中,this返回的是当前的模块,在ES6模块中,this返回的是undefined。
第二:函数如果不作为对象的方法运行,那么this就会返回顶层对象。但是在严格模式中,this指向的是undefined。
因此,为了在任何环境中都可以取得到顶层对象,ES6引入了globalThis顶层对象,该对象在任何环境下都是存在的,可以通过该对象来取得顶层对象,然后指向全局的this。
网友评论