作用域
- 作用域指一个变量其作用的范围。它是静态的(相对于上下文对象),在编写代码时就确定了
- 分类
- 全局作用域
- 函数作用域
- 块级作用域(ES6 新增)
- 作用:隔离变量,不同作用域下同名变量不会有冲突
全局作用域
- 直接编写在 script 标签中的 JS 代码,都在全局作用域中。
- 全局作用域在页面打开时创建,在页面关闭时销毁。
- 在全局作用域中有一个全局对象 window,它代表的是浏览器的窗口,由浏览器创建,我们可以直接使用。
- 在全局作用域中(使用 var)
- 创建的变量都会作为 window 对象的属性保存。
- 创建的函数都会作为 window 对象的方法保存。
- 变量的声明提前:
- 使用 var 关键字声明的变量,会在所有的代码执行之前被声明好(提前声明但是不会赋值,赋值还是等程序执行到代码所处的该行时变量才会被赋值),但是如果声明变量时不使用 var 关键字,则变量不会被声明提前。
- 函数的声明提前:
- 使用函数声明形式创建的函数 (function 函数名(){})它会在所有的代码执行之前就被创建,所以我们可以将调用函数写在函数的声明之前。
- 使用函数表达式创建的函数 var 函数名 = function(){} (此种声明方式会把函数当变量声明,只会提前声明函数名,但是函数会在程序执行到该行时才会被当做赋值行为所创建),不会被提前声明,所以不能在声明前调用。
- 全局作用域中的变量都是全局变量,在页面的任意部分都可以访问。
JavaScript
//函数声明,会被提前创建
function fun(){
console.log("我是一个fun函数");
}
//函数表达式,不会被提前创建
var fun2 = function(){
console.log("我是一个fun2函数");
}
函数作用域
调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。
每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的。
在函数作用域中可以访问到全局作用域的变量(全局变量),但是在全局作用域中无法访问到函数作用域的变量。
当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错 ReferenceError。
在函数作用域中要访问全局变量可以使用 window 对象(window.a)。
在函数作用域中也有声明提前的特性,使用 var 关键字声明的变量,会在函数中所有的代码执行之前被声明。
函数声明也会在函数中所有代码执行之前被提前执行。
JavaScriptvar a = 10; function fun3(){ console.log(a); //undefined 由于var关键字声明提前,这里输出的是undefined,而不是10. var a = 20; }
在函数中,不使用 var 声明的变量都会成为全局变量。
JavaScriptvar c = 33; function fun5(){ console.log(c); //33 c = 10; //没有使用var声明,此时使用的c就是全局作用域中的c d = 20; //没有使用var声明,其实是创建了全局变量d,而不是函数作用域的d。 } console.log(c); //10 console.log(d); //在函数作用域中声明的全局变量d,一样能在全局作用域中读取
定义形参就相当于在函数作用域中声明了变量。
JavaScriptvar e = 23; function fun6(){ alert(e); } fun6(); //输出23 function fun7(e){ alert(e); } fun7(); //undefined,定义形参就相当于在函数作用域中声明了变量,没有传实参,故输出undefined。 var a = 123; function fun(a){ alert(a); a = 456; //注意:上面a定义了形参,所以a在函数域中找得到,此时是函数域的a = 456。对全局变量a没有影响 } fun(223); //223 alert(a); //还是输出123。
块级作用域(这里只说概念,其他会在 ES6 部分详细说明)
- 通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。
- 块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
作用域链
- 多个上下级关系的作用域形成的链,它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的(查找属性和方法沿着隐式原型链)
- 查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入 2
- 在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入 3
- 再次往上一级作用域中查找,直到全局作用域,如果还找不到就抛出做不到的异常
作用域链面试题
JavaScript
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
f();
}
show(fn); //10
var fn = function () {
console.log(fn)
}
fn() //fuNction(){console.log(fn)}
var obj = {
fn2: function () {
console.log(fn2) //现在自身找,找不到去全局中找
//console.log(this.fn2)才能找到obj中的fn2
}
}
obj.fn2() //报错:fn2 is not defined
执行上下文
代码分类(位置):
- 全局代码
- 函数(局部)代码
全局执行上下文
- 在执行全局代码前将 window 确定为全局执行上下文
- 对全局数据进行预处理
- var 定义的全局变量 ==> undefined,添加为 window 的属性
- function 声明的全局函数 ==> 赋值(func),添加为 window 的方法
- this ==> 赋值(window)
- 开始执行全局代码
函数执行上下文
- 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
- 对局部数据进行预处理
- 形参变量 ==> 赋值(实参) ==> 添加为执行上下文的属性
- arguments ==> 赋值(实参列表),添加为执行上下文的属性
- var 定义的局部变量 ==> 赋值 undefined(变量提升),添加为执行上下文的属性
- function 声明的函数 ==> 赋值(func),添加为执行上下文的方法
- this ==> 赋值(当前调用函数的对象)
- 开始执行函数体代码
JavaScript//函数执行上下文 function fn(a1){ console.log(a1) //2 console.log(a2) //undefined a3() // a3() console.log(this) // window console.log(arguments) // 伪数组[2,3] var a2 = 3 function a3(){ console.log('a3()') } } fn(2,3)
执行上下文栈(与栈对应的是队列,栈是后进先出[FILO]如容器,队列是先进先出如管道[FIFO],会在后续数据结构算法中详细说明)
- 在全局代码执行前,JS 引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后,将其添加到栈底中(压栈)
- 在函数执行上下文创建后,将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后,栈中只剩下 window
执行上下文栈面试题
JavaScript
//1.依次输出什么
//gb:undefined,fb:1,fb:2,fb:3,fe:3,fe:2,fe:1,ge:1
//整个过程产生了几个执行上下文
//5个
console.log('gb: '+ i)
var i = 1
foo(1)
function foo(i) {
if (i == 4) {
return
}
console.log('fb:' + i)
foo(i + 1) //递归调用: 在函数内部调用自己
console.log('fe:' + i)
}
console.log('ge: ' + i)
//测试题1: 先执行变量提升, 再执行函数提升
function a() {}
var a
console.log(typeof a) // 'function'
//测试题2:
if (!(b in window)) {
var b = 1 //变量声明提升
}
console.log(b) // undefined
//测试题3:
var c = 1
function c(c) {
console.log(c)
var c = 3
}
c(2) // 报错,c is not function
//实际顺序
var c = 1
function c(c) {
console.log(c)
var c = 3
}
c = 1
c(2)
作用域与执行上下文的区别
区别一
- 全局作用域之外,每个函数都会创建自己的作用域(即函数作用域),作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后,js 代码马上执行之前才创建
- 函数执行上下文环境是在调用函数时,函数体代码执行之前创建
区别二
- 作用域是静态的,只要函数定义好了就一直存在,且不会再变化
- 执行上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会自动释放
联系
- 执行上下文(对象)从属于所在的作用域(主从关系)
- 全局上下文环境 ==> 全局作用域
- 函数上下文环境 ==> 对应的函数使用域