块级作用域
es5语法规则中,只有全局作用域和函数作用域,不存在块级作用域(一个块级作用域即为一个{}内)。
这会导致很多场景不合理。
第一种场景,内层变量可能会覆盖外层变量。
function test4(){
var tmp =new Date();
function f () {
console.log(tmp); // undefined
if (false) {
var tmp = `hello world`;
}
}
f () ;
};
test4();
第二种场景,用来计数的循环变量泄露为全局变量。
var s = `hello`;
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
};
console.log(i); // 5
块级作用域的应用:实际上使得原来广泛应用的立即执行匿名函数(IIFE)不再必要。
// IIFE 写法
( function(){
var tmp = `hello dengjing!`;
// 该变量只在该匿名函数作用域内有效
}() );
// 块级作用域 写法
{
let tmp = `hello dengjing!`;
}
块级作用域与函数声明
function test9(){
function f(){ console.log(`I am outside!`) }
( function(){
if(false){
// 重复声明一次函数f
function f(){ console.log(`I am inside`); }
}
f();
}() )
}
test9();
上面代码,在ES5中运行结果:I am inside。因为被声明的函数会被提升到函数头部,实际运行代码如下:
function test9(){
function f(){ console.log(`I am outside!`) }
( function(){
function f(){ console.log(`I am inside`); }
if(false){
// 重复声明一次函数f
}
f();
}() )
}
test9();
ES6浏览器实现由如下规则:
1、允许在块级作用域内声明函数
2、函数声明类似于var,即会提升到 全局作用域 或 函数作用域 的头部
3、同时,函数声明还会提升到所在的块级作用域的头部
上段代码,在符合ES6的浏览器中都会报错,其实际运行的代码如下所示:
function test9(){
function f(){ console.log(`I am outside!`) }
( function(){
var f = undefined;
if(false){
// 重复声明一次函数f
function f(){ console.log(`I am inside`); }
}
// console.log(f); // undefined
f();
}() )
}
test9(); //Uncaught TypeError: f is not a function.
总结:考虑到环境导致的行为差异太大,应避免在块级作用域内声明函数。如确有需求,也应写成函数表达式的形式,而非函数声明语句。
// 函数声明语句
{
let a = `hello dengjing!`;
function f(){
return a;
}
}
// 函数表达式
{
let a = `hello dengjing!`;
let f = function(){
return a;
}
}
let
function test5(){
// snippet 1
var a = [];
for(var i = 0;i<10;i++){
a[i] = function(){
console.log(i);
}
}
a[6](); // => 10
}
test5();
上面代码:变量i是var声明的,全局范围有效。全局只有一个变量i,只是每次循环变量i都会重新赋值。而循环内,被赋给数组a函数内部的console.log(i)中的i指向全局变量i。也就是说,所有数组a的成员中的i指向的都是同一个i,导致运行时输出的是最后一轮的i值,也就是10。
function test6(){
// snippet 2
{
let n = 11;
{ let n = 4;let m = 100; console.log(n);}
// 4
{console.log(n);}
// 11
}
let m = 0;
var a = [];
let i = 9; // location 1
for(let i = 0,m = 10;i<10;i++){ // location 2
// 使用let声明循环变量i,相当于此处又重新声明了一个变量i,let i ;
// 使用let声明循环变量i 类似于 {let i = 0;{let i }}
a[i] = function(){
console.log(i);
}
// console.log(ff); // 报错:ff is not defined
// console.log('m:',m); // Uncaught ReferenceError: Cannot access 'm' before initialization
let m = 20;
console.log('m:',m); // 20
}
a[6](); // => 6
};
test6();
// location 1、location 2、location 3 注意此3处let i的声明。
let声明的变量仅在块级作用域内有效{}。
上面代码:变量i是let声明,当前的i只在本轮循环{}有效。so每一次循环的i其实都是一个新的变量。js引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
attention:for循环有一个特别之处,那就是设置循环变量的那部分 是一个父作用域,而循环体内部是一个单独的子作用域;可以理解为 {let i = 0;{let i }}。for循环声明的变量i 与 循环变量i 不在同一个作用域,而是有各自单独的作用域。
function test7(){
// snippet 3
var a = [];
let i = 0,m = 10;
for(;i<10;i++){
a[i] = function(){
console.log(i);
}
}
console.log('a = '+a);
console.log('i = '+i); // => 10
a[6](); // => 10
};
test7();
1、不存在变量提升
console.log(bar); // Uncaught ReferenceError
let bar = 2;
2、暂时性死区
只要块级作用域内存在let命令,它所声明的变量就‘绑定’(binding)这个区域,不再受外部的影响。
ES6明确规定:如果区块中存在let和const命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域。只要在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。在语法上称为“暂时性死区”(temporal dead zone,简称TDZ)。
function test8(){
if(true){
// TDZ开始
tmp = `abc`; // ReferenceError
console.log(tmp); // ReferenceError
let tmp ; // TDZ结束
console.log(tmp); // undefined
tmp = '123';
console.log(tmp); // 123
}
typeof x; // ReferenceError
let x;
// 如一个变量根本没被声明,使用typeof则不会报错
typeof undeclared_variable; // undefined
function bar(x = y,y = 2){
return [x,y];
}
bar(); // 报错
function bar1(x = 2,y = x){
return [x,y];
}
bar1(); // [2,2]
var y = 1;
var y = y; // 不报错
let y = y; // ReferenceError: x is not defined
// 以上报错也是因为暂时性死区
};
test8();
暂时性死区的本质就是:只要进入当前作用域,所要使用的变量就已经存在,但是不可获取,只有等到声明变量的那一行代码出现后,才可以获取和使用该变量。
3、不允许重复声明
let不允许在同一作用域内部重复声明同一个变量。
{
{ let insane = `hello world` }
console.log(insane); //报错
}
{
let insane = `hello world0`;
{ let insane = `hello world1` }
console.log(insane); //报错
}
const
const声明一个只读常量。一旦声明就必须立即赋值,不能留后赋值,过后变量就不能再次赋值。
因此,对于const而言,只声明不赋值就会报错。
const作用域:只在声明所在的块级作用域内有效,这点与let相同。
const也存在以下特性:
1、不存在变量提升
2、暂时性死区
3、不允许重复声明
if(1){
const MAX = 10;
}
// console.log(MAX); // Uncaught ReferenceError: MAX is not defined
if(1){
// console.log(MAX); // Uncaught ReferenceError: Cannot access 'MAX' before initialization
const MAX = 10;
}
var message = `hello dengjing`;
let age = 18;
// Uncaught SyntaxError: Identifier 'message' has already been declared
// const message = `hello dengjing`;
// Uncaught SyntaxError: Identifier 'age' has already been declared
// const age = 18;
const本质
const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。
我们知道,对于简单数据类型(数值,字符串,布尔值等)而言,值就保存在变量指向的内存地址中。但是,对于复合数据类型(对象、数组等)而言,变量指向的内存地址保存的只是一个指针,而const只能保证这个指针是固定的,至于指针它指向的数据结构是不是可变的,这是完全无法控制的。
故而,当将一个对象声明为常量时必须当心。
const foo = {};
// 为foo字面量对象添加一个属性,成功
foo.prop = `I am dengjing.`;
console.log(foo.prop);
// 将foo地址指针改变指向,报错
// foo = {}; // Uncaught TypeError: Assignment to constant variable.
const a = [];
a.push(`hello`);
a.length = 0;
// a = ['kelly']; // Uncaught TypeError: Assignment to constant variable.
如真想将对象冻结,应使用Object.freeze方法。
对象冻结后,添加新属性时不起作用,严格模式时甚至还会报错。
const foo = Object.freeze({});
// 常规模式时,下行代码不起作用
// 严格模式时,下行报错
foo.prop = `I am dengjing.`;
console.log(foo.prop); // undefined
除了将对象本身冻结,对象的属性也应该冻结。彻底冻结对象函数如下:
var constantize = (obj)=>{
Object.freeze(obj);
Object.keys(obj) && Object.keys(obj).forEach( (key,i)=>{
if(typeof obj[key] === 'object'){
constantize(obj[key]);
}
})
};