2、let和const命令
2.1 let命令
基本用法
作用范围:代码块内。
使用场景:for循环(待补充)
在for循环中,每个let生命的变量都是独立的。而var声明的变量指向同一个。特别的是,for循环中循环变量是一个父作用域,而循环体内是一个作用域。
for(let i = 0; i < 10; i++) {
let i = 'abc';
console.log(i)
}
// abc
不存在变量提升
变量提升,变量可以在生命之前使用,只不过值为undefined
console.log(foo); // undefined
var foo = 2;
console.log(bar); // ReferenceError
let bar = 'abc';
暂时性死区
暂时性死区:let 声明的变量,在未声明之前不能使用,同时该块区域封锁,外面的变量也进不来。
var tmp = 123;
if(true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
// 可以看到tmp在这个块级区域内被封锁了,即使有外部tmp,它也不会使用
在死区内使用typeof 是不安全的(ReferenceError)。需要注意的是,typeof一个没有声明的变量确实允许的,死区和暂未声明是有区别的。
var tmp = 123;
if(true) {
console.log(typeof tmp); // ReferenceError
console.log(typeof undeclared_variable); // undefined
let tmp;
}
隐蔽的死区
function bar (x = y, y = 2) {
return [x, y];
}
// 由y决定x,但是y属于死区
死区的本质:只要进入当前作用域(let const),所要使用的变量就已经存在,但是不可获取,只有等到声明变量那行代码出现,才可以使用使用该变量。
不允许重复声明
let 允许在相同的作用域内重复声明同一个变量。
function () {
var a;
let a; // 报错
}
function(a) {
let a; // 报错
}
function(a) {
{
let a; // 不会报错
}
}
2.2 块级作用域
为什么需要块级作用域
ES5只有全局作用域和函数作用域,没有块级作用域。问题:1. 变量提升带来的麻烦。 2. 泄露为全局变量
-
变量提升
var tmp = 1; function f() { console.log(tmp); if(false) { var tmp = 'hello world' } } // 想要打印外部变量,但是却是undefined
-
泄露
for(var i = 0; i < 5; i++) { } console.log(i); // 5 // 内存没有给销毁,而是成为全局变量。
ES6的块级作用域
let 为JS带来块级作用域
块级作用域的出现可以让立即执行匿名函数退出舞台。
// IIFE写法
(function() {
var tmp = ''
} ())
// 块级作用域写法
{
let tmp = ''
}
块级作用域和函数声明
ES5规定,函数只能在顶层作用域和函数作用域之中使用。不能在块级范围内使用。但是浏览器没有遵循这项规则。
function f() {
console.log('I am outside!');
}
(function() {
if(false) {
function f() {
console.log('I am inside')
}
}
f();
})
ES5相当于以下代码。由于ES5只有函数作用域和全局作用域,而且变量会被提前。所以里面的f函数实际上被提前了(IIFE函数顶部)。
function f() {
console.log('I am outside!');
}
(function() {
function f() {
console.log('I am inside')
}
if(false) {
}
f();
})()
// I am inside
ES6由于存在块级作用域,所以里面的f实际上并不会污染外面,所以调用的是外面的f函数,输出I am outside
但是但是!!!实际在ES6浏览器会报错,这是因为具体实现与规定有出入。具体实现如下
-
允许块级作用域内声明函数。有花括号存在才行。
if(false) function a() {} // 不允许
-
函数声明类似于var,也就是会提升到全局作用域和函数作用的头部。
-
函数声明还会提升到块级作用域的头部。
以上是浏览器的实现,在node等其他环境依然当作let的行为就ok
回来看之前的例子,其在ES6的具体实现为:
function f() {console.log('I am outside')}
(function () {
var f = undefined;
if(false) {
function f() {console.log('I am inside')}
}
f()
}())
// f is not a function
应避免在块级作用域内声明函数,确实需要,写成函数表达式的方式。
do表达式
块级作用域内封闭,外部不能访问。
提案:利用do关键字使得块级作用域转变为表达式,得到返回值。是一个提案而已
let x = do {
}
2.3 const命令
基本用法
- 和let行为类似。(暂时性死区,不提升,不可重复声明)
- 声明的同时必须赋值。
本质
保存变量的地址不变。对于简单类型(数值,字符串,布尔值),其值就保存在变量指向的内存地址中;对于复合类型(对象,数组),
变量指向的内存地址保存只是一个指针,const保证该指针是固定的,至于复合类型内部怎么变,不是const能决定的。
const foo = {};
foo.a = 'b'; // 可行
const bar = [];
bar.length = 0; // 可行
bar.push('Hello'); // 可行
bar = ['aa']; // 报错
如果想将一个对象冻结,应该使用Object.freeze()方法
const foo = Object.freeze({});
foo.prop = 123; // 不起作用,在严格模式下报错
彻底冻结该对象
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach((key, i) => {
if(typeof obj[key] === 'object') {
constantize(obj[key]);
}
})
}
ES6声明变量的6中方式
- var
- let
- const
- function
- import
- class
2.4 顶层对象属性
浏览器中的顶层对象:window;Node中的顶层对象:global
顶层对象的属性和全局变量相关是JS设计的败笔。
- 无法再编译时提示变量未声明的错误,只有运行时才能发现,(对象的属性是动态的,无法在编译时就预测到)
- 程序员容易创建全局变量而自己不知情
- 顶层对象的属性到处可以读写,不利于模块化编程
- window指窗口对象,有具体实际意义
ES6逐渐将全局变量和顶层对象隔离。
- var function 任然属于定策对象的属性
- let const class的全局变量不属于顶层对象的属性。
let a = 1;
console.log(window.a); // undefined
var b = 3;
console.log(window.b); // 3
// 在node中写成global.a或者统一用this.a
2.5 global对象
- 在浏览器中,顶层对象是window,但是Node和Web Worked没有window
- 在浏览器和Web Worker中,self指向顶层对象,但是Node没有self
- 在Node中,global是顶层对象,但是其他环境没有。
用this可以取到顶层对象,但是任然有局限
- 全局环境下,可以获得顶层对象,但是在模块中指的是当前模块
- 在函数中,this指向不尽相同
- 不管严格模式还是普通模式,new Function(‘return this’) ()返回全局对象,但是如果浏览器用了CSP(内容安全策略),那么eval和new Function将不能使用。
可以通过,对self window global依次判断存在(==undefined)与否,获得全局对象。还可以通过插件方式。
提案:在语言层面直接引入global对象。直接了断。