一、一个面试题
看代码说输出 🙃 :
function a() {
console.log("Hello World!");
}
var a;
console.log(a);//[Function: a]
function a() {
console.log("Hello World!");
}
var a = 10;
console.log(a); //10
var a = 10;
var a = function () {
console.log("Hello World!");
};
console.log(a); //[Function: a]
var a = function () {
console.log("Hello World!");
};
var a = 10;
console.log(a); //10
在搞清楚JavaScript中作用域
和执行上下文
的知识后,就能很好地解释上面的代码结果了。
二、作用域
作用域即函数或变量的可见区域。
通俗点说,函数或者变量不在这个区域内,就无法访问到。
再ES6之前,JavaScript只有全局作用域
和函数作用域
,ES6之后引入了块级作用域
。
2.1 全局作用域
全局作用域,也就是定义在最外层
的变量或者函数,可以在任何地方访问到它们。
2.2 函数作用域
用函数形式以function(){……}
类似的代码包起来的(省略号……)区域,即函数作用域。
let myName = "window";
let country = "China";
function sayHi() {
//函数作用域
let myName = "yancy";
let age = 20;
console.log(`Hi! My name is ${myName}, I'm ${age}, I'm from ${country}.`);
}
sayHi(); //Hi! My name is yancy, I'm 20, I'm from China.
console.log(myName); //window
console.log(age); //ReferenceError: age is not defined
可以看到,在全局作用域中,无法访问函数作用域中的age变量,但是在函数作用域中却可以访问全局作用域中的country变量,因为全局变量在任何地方都可见。
2.3 块级作用域
ES6规定,在某个花括号对{ }
的内部用let关键字
生声明的变量和函数拥有块级作用域
,这些变量和函数它们只能被花括号对{ }的内部的语句使用,外部不可访问。
在你写下代码的时候,变量和函数的块级作用域就已经确定下来。块级作用域和函数作用域也可以统称为局部作用域。
function func() {
var name = "a";
{
var name = "b";
}
console.log(name);
}
func();//b
用var声明的变量并没有实现块级作用域概念。
function func() {
var name = "a";
{
let name = "b";
}
console.log(name);
}
func(); //a
在{}
中,let
声明的name属于块级作用域
,所以外部无法直接访问。
三、执行上下文
了解了作用域的相关概念,我们再来看看执行上下文。
3.1 类型
全局执行上下文
:这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1.创建一个全局对象,在浏览器中这个全局对象就是 window 对象; 2. 将 this指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。函数执行上下文
:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。- eval执行上下文:运行在 eval 函数中的代码也获得了自己的执行上下文,ES6 之后不再推荐使用 eval函数。
3.2 函数执行上下文生命周期
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段,本文重点介绍创建阶段。
- 创建阶段
当函数被调用,但未执行任何其内部代码之前,会做以下三件事:
1、创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明(变量的声明提前有赖于var关键字)。
2、创建作用域链:在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
3、确定 this 指向。
- 执行阶段
创建完成之后,就会开始执行代码,在这个阶段,会完成变量赋值
、函数引用
、以及执行其他代码。 - 回收阶段
函数调用完毕后,函数出栈,对应的执行上下文也出栈,等待垃圾回收器回收执行上下文。
3.3 变量提升
什么是变量提升呢?
function func() {
console.log(a); //undefined
console.log(f1); //undefined
console.log(f2); //[Function: f2]
var a = "Hello World";
var f1 = function () {
console.log("f1");
};
function f2() {
console.log("f2");
}
console.log(a); //Hello World
console.log(f1); //[Function: f1]
console.log(f2); //[Function: f2]
}
func();
可以看到,在a和f1声明之前,可以访问到a和f1,并没有报错,但是值为undefined
,这就是变量提升。
而使用function声明的函数f2,可以直接访问,并且得到声明时的引用。
3.4 创建阶段三条规则
创建阶段的三条规则可以很好地解释变量提升以及文章开头的代码输出结果。
首先
,建立arguments
对象。检查当前执行上下文中的参数,建立该对象下的属性与属性值。其次
,检查当前执行上下文的函数
声明,也就是使用function
关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果该属性之前已经存在
,那么该属性将会被新的引用所覆盖
。最后
,检查当前执行上下文中的变量声明
,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined
。如果该变量名的属性已经存在
,为了防止同名的函数被修改为undefined,则会直接跳过
,原属性值不会被修改。
注意,创建阶段仅仅不会有赋值
操作。
function a() {
console.log("Hello World!");
}
var a = 10;
console.log(a); //10
解释上面代码:
创建阶段:
1、发现函数声明function a(){…},创建属性a指向函数内存地址,此时a为函数。
2、发现变量声明 var a = …,由于已经有了a属性,所以直接跳过。
所以创建阶段执行完毕,a仍然时一个函数。
执行阶段:
1、将a赋值为10,此时a属性变为了number 10。
2、输出a,值为10。
注意,仅var声明的变量存在提升。