变量提升
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。在 JavaScript 中,使用 var 声明的变量会提升到其所在的作用域顶部。这意味着,无论在作用域中的哪个位置声明了变量,它都可以在该作用域中的任何位置访问。
下面是一个例子来说明 var
变量提升的概念:
console.log(a); // undefined
var a = 1;
console.log(a); // 1
在这个例子中,我们在第一行尝试打印变量 a
的值,虽然这个变量在这行代码之前没有声明和赋值,但结果却是 undefined
而不是报错。这是因为 JavaScript 引擎在代码执行之前会先对 var
声明的变量进行提升,所以这段代码实际上等价于:
var a;
console.log(a); // undefined
a = 1;
console.log(a); // 1
在代码执行之前,变量 a
被提升到作用域的顶部,并被赋予了初始值 undefined
。因此,尽管我们在第一行尝试打印变量 a
的值,它的值为 undefined
。当代码执行到 var a = 1;
这一行时,变量 a
被赋予了新的值 1
。
需要注意的是,变量提升只会提升声明,而不会提升赋值。也就是说,只有变量名被提升到作用域顶部,而不是变量的实际值。所以在提升阶段之前,变量的值都是 undefined
。
总结一下,var
声明的变量会被提升到作用域顶部,但是赋值操作并不会提升,所以在变量被赋值之前,它的值是 undefined
。因此,最佳实践是在使用变量之前先声明和赋值,以避免可能的意外行为。
至于为什么要变量提升,在于 JavaScript 代码在执行前需要先编译,编译时变量和函数会被放到变量环境中。
例如:
// 这是我们正常写的代码:
sayHello();
console.log(hello);
var hello = "你好";
function sayHello() {
console.log("sayHello");
}
//模拟变量提升后的代码
var hello = undefined;
function sayHello() {
console.log("sayHello");
}
sayHello();
console.log(hello);
hello = "你好";
函数和变量在执行前都提升到代码开头。而对于出现了同名的变量或者函数,最终生效的是最后一个(覆盖)
块级作用域
-
作用域:是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期
-
全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期
-
函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁
块级作用域:就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。
ES6 引入了 let 和 const 关键字,从而使 JavaScript 拥有了块级作用域,能够解决一些变量提升带来的问题。
使用 let 关键字声明的变量是可以被改变的,而使用 const 声明的变量其值是不可以被改变的。
以下是变量提升和块级作用域的理解考察题:
function test() {
const a = 1;
const b = 2;
{
const b = 3;
var c = 4;
const d = 5;
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
test();
-
现在我们逐行分析代码:
-
const a = 1;
和const b = 2;
是在函数foo
的变量环境中声明了常量a
和b
。 -
const b = 3;
是在foo
函数内部的代码块中声明了一个新的常量b
,它的作用范围只在该代码块内部。 -
var c = 4;
使用var
关键字声明的变量c
会提升到函数作用域顶部,并且能在整个函数范围内访问。 -
const d = 5;
是在foo
函数内部代码块中声明了一个新的常量d
,它的作用范围只在该代码块内部。 -
console.log(a);
打印变量a
的值,输出1
。 -
console.log(b);
打印变量b
的值,输出3
,因为此处的b
是在内部代码块中的新声明。 -
console.log(b);
打印变量b
的值,输出2
,因为此处的b
是在函数范围内的变量。 -
console.log(c);
打印变量c
的值,输出4
,因为c
是在函数范围内声明的变量。 -
console.log(d);
打印变量d
的值,引发 ReferenceError 错误,因为d
是在代码块内部声明的变量,超出了它的作用范围。
-
综上最终的输出结果为:1、3、2、4、err
作用域链、闭包
作用域链:是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。
闭包:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 init,那么这些变量的集合就称为 init 函数的闭包。
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
displayName()
没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量
使用场景
-
在函数外部能够访问到函数内部的变量。
-
使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。