一、函数作用域
1. 特点
(1)函数作用域:
- 属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
- 外部无法访问到包裹在函数内部的任何内容。
function foo(a) {
var b = 2;
function bar() {
//...
}
var c = 3;
}
bar() // 失败
console.log(a, b, c) // 三个全都失败
(2)作用:
- 变量私有化(最小权限原则),阻止某些变量或函数的访问
- 规避冲突,避免标识符之间的冲突
(3)立刻执行表达式
- 由于函数被包含在一对()括号内部,因此成为了一个表达式
- 通过在**末尾加上一个()**可以立刻执行这个函数。
var a = 2;
(function IIFE(golbal) {
var a = 3;
console.log(a); // 3
console.log(global.a); //2
})(window)
console.log(a)// 2
- 这里将window对象的引用传递进去,但将参数命名为global。
还有另一种形式
(function(){...}()) //括号在内部,两者功能一样
2. 声明提升
直觉上JavaScript是一行一行执行的,当实际上这并不完全准确。
console.log(a);
var a = 2;
上面这段代码生成的是undefined,这是为什么?
- JavaScript代码编译前需要找到所有声明,并用合适的作用域将它们关联起来。(这也是词法作用域的核心)
- 包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
- 只有声明本身会被提升,而赋值操作或其他逻辑会留在原地。
- 每个作用域都会进行提升操作。
例如
var a = 2;
- 当你看到var a = 2;时,你以为是一个声明。
- 但JavaScript实际上会将其看成两个声明:var a; 和 a = 2;。
- 第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
- 因此,可以看作是变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程叫做提升。
foo()
function foo() {
console.log(a); // undefined
var a= 2;
}
function foo() {
var a;
console.log(a); //undefined
}
foo()
3. 函数优先
函数声明会被提升,但函数表达式不会被提升
foo(); // typeError,foo此时没有赋值
var foo = function bar() {
//...
}
- 函数声明和变量声明都会被提升
- 但是函数会首先被提升,然后才是变量。
这些理论其实主要是用来解释避免重复声明,因为var声明和函数声明混合使用时,会出现奇怪的问题。
二、块级作用域
先来看个for循环
for(var i=0; i<10; i++) {
console.log(i);
}
- 我们在for循环头部直接定义了变量i,通常只是因为想在for循环内部的上下文使用i
- 但问题是i会被绑定在外部作用域(函数或全局)中。
这就体现块级作用域的用处。
1. let
- let关键字可以将变量绑定在所在的任意作用域中(通常是{…}内部)。
- 换句话说,let为其声明的变量隐式地劫持了所在的块作用域。
看个例子
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
console.log(bar); // 报错
垃圾回收(优化)
- 块级作用域非常有用的原因,和闭包及回收内存垃圾的回收机制有关。
- 以前面的代码为例,if 块执行完成之后,就可以将内部的数据结构进行垃圾回收了。
- 如果只有函数作用域,还要JavaScript引擎需要顾虑其他地方是否对if块中的代码有引用,所以依然保持内部这个结构。
循环
for (let i=0; i<10; i++) {
console.log(i)
}
console.log(i) // 报错
这是一个可以让let发挥优势的典型例子。
- for循环头部的let不仅绑定到了for循环的块中。
- 事实上,它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
相当于:
{
let j;
for(j=0; j<10; j++) {
let i = j; // 每个迭代重新绑定!
console.log(i);
}
}
2. const
和let一样是用来创建块作用域的,但其值是固定的(常量)。之后的修改操作会引起错误。
if (foo) {
var a = 2;
const b = 3;
a = 3; // 正常
b = 4; // 错误
}
console.log(a) // 正常
console.log(b) // 错误
注意:如果是引用类型,那么可以修改引用类型的属性值。因为变量中保存的是引用,引用不变就不会引起报错。
3. 提升
- 使用let进行的声明不会在块作用域中进行提升。
- 也就是说声明的代码被运行之前,声明并不“存在”。
三、总结
结合之前了解到的内容,一起做个总结
1. var
- 变量提升:只有声明本身被提升,而赋值操作或其他运行逻辑会留在原地。
- 作用域:有全局作用域和函数作用域,函数作用域外的无法访问到包裹在函数内部的任何内容。
- 重复声明:同一作用域内可以重复声明,但是第二个声明会被忽略,只是将第二次的赋值进行覆盖。(需要避免)
- for循环中:迭代遍历只有一个。退出循环时,迭代变量保存的是导致循环退出的值,在之后执行超时逻辑时,迭代变量是同一个。
2. let
-
块级作用域:只要是两个花括号包含的区域,就形成块(即使没有if、for等语句)。块外部无法访问到内部的内容。
-
暂性死区:let声明一个变量,就会与该块进行锁定。在块的内部,如果let初始化前的代码如果使用了该变量,不会去搜索上一级块内的变量,而是出现报错。只有let声明的变量初始化之后,才能访问。
-
不能重复声明:同一作用域内,同一标识符不能重复声明。
-
for循环中:每个迭代循环声明一个新的迭代变量。
3. const
基本同let
与let的区别:
- 定义的是常量,不能修改,如果是对象可以修改属性。
- 必须在声明时初始化
4. 声明方式选择
优先使用 let 和 const
(1)因为变量有了明确的作用域、声明位置,以及不变的值。
(2)块级作用域可能带来的优化。
5. 函数声明
函数声明提升(与var区别)
- 函数会首先被提升,然后才是变量
- 可以看作是整个函数都提到作用域顶部,也就是在第一行就可以开始执行函数
同名覆盖(应该避免同名)
变量名和函数名同名,会发生相互覆盖的问题
-
声明变量时未初始化,函数优先级更高
定义变量时只使用var定义变量,不分配变量初始值,此时函数的优先级更高,函数会覆盖变量;
-
声明变量时初始化,变量优先级更高
定义变量时为变量指定了初始值,此时变量的优先级更高,变量会覆盖函数。
函数表达式
- 函数表达式会将变量提升,但是代码在执行的时候才会被赋值为函数
- 除了函数什么时候真正有定义这个区别之外,这两种语法是等价的。