一、块级作用域
概念:通过 let 和 const 声明的变量拥有块级作用域 ,该变量必须声明在一个函数内部或在一个代码块(由一对花括号包裹)内部
优点:块级作用域可以解决由于过多全局变量和函数产生的命名冲突的问题
二、var 变量提升机制
2-1 变量提升
什么是变量提升?
JavaScript 引擎的工作方式是,先预解析代码, 获取所有变量声明和函数声明,然后再一行一行地运行,这就使所有变量声明和函数声明的语句,都会被提升到代码的顶部,这就是变量提升
var 变量提升
var 关键字声明的变量,无论实际声明的位置在何处,都会被视为声明在函数顶部(如果声明不在任意函数内,则视为在全局作用域的顶部)
function fn(){
var a = 1
}
console.log(a) //报错
-----------------分割线------------------
{
var b = 1;
}
console.log(b); //1
// 等价于
var b
{
b = 1;
}
-----------------分割线------------------
if(true){
var c = 1;
}
console.log(c); //1
// 等价于
var c
if(true){
c = 1;
}
2-2 变量声明和函数声明优先级
结论:函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重写赋值
原因:函数每次执行,都会形成一个新的私有作用域(函数作用域)然后执行以下三个步骤
-
如果有形参,先给形参赋值
-
进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
-
私有作用域中的代码从上到下执行
var a
function a(){}
console.log(a) // [Function a]
function a(){}
var a
console.log(a)// [Function a]
//当变量声明时没有赋值或初始化,函数声明优先级高于变量
--------------------------分割线-------------------------
var a = 1
function a(){}
console.log(a) // 1
console.log(a()) // 报错
function a(){}
var a = 1
console.log(a) // 1
console.log(a()) // 报错
等价于
function a(){}
var a
a = 1
console.log(a) // 1
console.log(a()) // 报错
2-2 函数表达式和函数声明
函数声明:一个标准的函数声明,由关键字function 、函数名、形参和代码块组成。
函数表达式:函数没有名称,而且位于赋值语句右边,被赋给一个变量。在语句(如赋值语句)中,以这样的方式使用关键function时,创建的是函数表达式。
function test() {
foo(); // 报错 Uncaught TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function() {
alert("this won't run!");
};
function bar() {
alert("this will run!");
}
}
test();
代码解析:变量名和函数都会上升,而遇到函数表达式 var foo = function(){ } 时,首先会将var foo上升到函数体顶部,而此时的foo的值为undefined,所以执行foo()报错。
三、let 块级作用域
- let 和 var 都是用来定义变量,使用 let 声明的变量没有 var 那样的变量提升,let 声明的变量只在当前的块级作用域中有效
- 禁止在同一个作用域中重复声明同一个变量,一旦相同立刻报错
// 1、不存在变量提升,只在当前的块级作用域中有效
function person(status) {
if (status) {
let value = "蛙人"
} else {
console.log(value) // 报错
}
console.log(value) // 报错
}
person(false)
// 2、在同一个作用域中声明同名变量会报错
var value = "蛙人"
let value = "蛙人" // 报错
// 再来看一下不同作用域的情况
var value = "蛙人" // 全局作用域
if(true) {
let value = "蛙人" // 代码块中声明,毫无影响
}
四、const 常量
- const 声明指的是常量,和 let 一样,同样都是块级作用域,常量一旦定义完就不可以被修改,而且常量的定义时必须初始化值,否则会报错
- const 实际上保证的不是变量的值不可以被改动,而是变量指向的那个地址不可以被改动,也就是 this 指针不可以被改动。当 const 声明的对象属性被修改时,对象的地址(指向对象的指针)是没有被修改的,所以说 const 声明的对象是可以被修改的
基本数据类型的变量名和值都是保存在栈内存中
引用数据类型的值是一个保存在栈内存中的指针,指向堆内存中的对象。const 声明的只是保证栈区内容不变
五、暂时性死区
只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
代码解析:上面代码中,存在全局变量 tmp ,但是块级作用域内 let 又声明了一个局部变量 tmp ,导致后者绑定这个块级作用域,所以在 let 声明变量前,对 tmp 赋值会报错。因为此时在 let 命令声明变量 tmp 之前,都属于变量tmp的“死区”
ES6明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
“暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
// 变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError
typeof y // "undefined"
// y是一个不存在的变量名,结果返回“undefined”。所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
不太容易看到的暂时性死区
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
代码解析:调用bar函数之所以报错,是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了。
六、代码示例,加深理解
let 示例
// let 声明的成员只会在所声明的块中生效 -------------------------------------------
if (true) {
// var foo = 'zce' // 具有全局作用域
let foo = 'zce' // 具有块级作用域,在块级外部是无法访问的
console.log(foo)
}
// let 在 for 循环中的表现 ---------------------------------------------------
for (var i = 0; i < 3; i++) {
for (var i = 0; i < 3; i++) { // 内层循环声明的 i 会覆盖外层循环声明的 i
console.log(i)
}
console.log('内层结束 i = ' + i) // 只打印了三次
}
for (var i = 0; i < 3; i++) {
for (let i = 0; i < 3; i++) { // let 声明的 i 只在当前所在的代码块生效
console.log(i)
}
console.log('内层结束 i = ' + i) // 打印 9 次
}
// let 应用场景:循环绑定事件,事件处理函数中获取正确索引 -----------------------------------------------------
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
elements[i].onclick = function () {
console.log(i) // 因为这里打印的 i 是全局作用域下的 i
}
}
elements[2].onclick() // 打印的 i 都是3
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
elements[i].onclick = (function (i) {
return function () { // 借助闭包来解决
console.log(i)
}
})(i)
}
elements[0].onclick()
var elements = [{}, {}, {}]
for (let i = 0; i < elements.length; i++) {
elements[i].onclick = function () {
console.log(i) // 这里的 i 只能在块级作用域内被访问,其实内部也是一种闭包的机制
}
}
elements[0].onclick()
// for 循环会产生两层作用域 ----------------------------------
for (let i = 0; i < 3; i++) { // 这两个 i 是互不影响的
let i = 'foo'
console.log(i)
}
// let i = 0
// if (i < 3) {
// let i = 'foo'
// console.log(i)
// }
// i++
// if (i < 3) {
// let i = 'foo'
// console.log(i)
// }
// i++
// if (i < 3) {
// let i = 'foo'
// console.log(i)
// }
// i++
// let 修复了变量声明提升现象 --------------------------------------------
console.log(foo) // undefined
var foo = 'zce'
console.log(foo) // 报错
let foo = 'zce'
const 示例
// const 在 let 的基础之上多了一个只读特性
// 1、常量声明过后不允许重新赋值
const name = 'zce'
name = 'jack' // 报错
// 2、常量声明必须有一个初始值,也就是说声明的同时必须赋值
// const name // 报错
// name = 'zce'
// 3、常量只是要求内层指向不允许被修改,修改内存指向数据成员属性是被允许的
// const obj = {}
// obj.name = 'zce'
// obj = {} // 改变内存指向会报错