作用域和作用域链
作用域
什么是作用域
作用域可以理解成一个独立地盘,让变量不会泄露,暴露出去。 也就是说,作用域最大的作用就是隔离变量,不同作用域下的同样名称变量不会有冲突。
ES6 之前 JS 是没有块级作用域的,只有全局作用域和函数作用域。 ES6 的到来为我们提供了 快级作用域,可以通过 let
和 const
来体现
全局作用域
最外层的函数和在外层外面定义的变量拥有全局作用域
var outVar = '最外层变量'
// 最外层函数
function outFun() {
var inVar = '内层变量'
// 内层函数 innerFun
function innerFun() {
console.log(inVar)
}
innerFun()
}
console.log(outVar) // ==> 最外层变量
outFun() // ==> 内层变量
console.log(inVar) // ==> inVar is not defined
innerFun() // ==> innerFun is not defined
所有未定义的直接赋值的变量,会自动声明为拥有全局作用域
function fun2() {
var1 = '未定义直接赋值的变量'
var var2 = '内部变量'
}
fun2() // 要执行这个函数,否则不知道里面是啥
console.log(var1) // 未定义直接赋值的变量 (全局变量,在函数 fun2执行后,会挂载到 window 下)
console.log(var2) // 报错(函数内部变量,无法访问)
所有 window
对象的属性拥有全局作用域
全局作用域的弊端:污染全局命名空间,容易引起命名冲突
函数作用域
函数作用域是指 函数内部声明 的变量,和全局作用域相反
局部作用域一般只是在固定的代码片段内可以访问到,常见是函数内部
jQuery、Zepto 等库的源码,代码放在 (function(){....})()
中,变量不会被外泄和暴露, 不会污染到外面,不会对其他js库造成影响
function fun3() {
var var1 = '内部变量'
function innerSay() {
alert(var1)
}
innerSay()
}
alert(var1) // 报错 (var1 是函数内部的变量,访问不到)
innerSay() // 报错 (innerSay 是函数内部的函数,访问不到)
作用域分层
作用域是分层的,内层作用域可以访问外层作用域的变量,反之不行
如图
块语句(大括号“{}”中间的语句 如if、switch、for、while
,不像函数,它们不会创建一个新的作用域
块级作用域
块级作用域可以用 let
和 const
声明,所声明的变量只能在指定块的作用域中访问
在一个函数内部
在一个代码块(花括号{})内部
声明的变量不会提升到代码块顶部
同一个作用域禁止重复声明
循环中绑定块作用域
var 声明 for 循环
变量是全局变量,每次循环变量的i的值都会发生改变。
let 声明 for 循环:
只存在于块作用域内有效,每次循环都是一个新的变量, js 引擎内部会记住上一轮循环的值,在初始化本轮的变量 i 时,就在上一轮的基础值进行计算
TIP
for 循环还有一个特别之处,设置循环变量的那部分是一个父作用域,而循环体内部是一个单独子作用域
作用域链
什么是自由变量
没有在当前作用域定义的变量,可以通过向父级作用域寻找(不严谨)的变量——自由变量
var a = 10
function fn() {
var b = 20
console.log(a) // 10 (a 在这里就是自由变量)
console.log(b)
}
fu()
作用域链
如果父级没有呢?再一层一层的向上找,直到找到全局作用域还是没有找到,就宣布放弃, 这一层关系,就是作用域链
var a = 10
function f1() {
var b = 20
function f2() {
var c = 30
console.log(a) // 自由变量,顺着作用域链找
console.log(b) // 自由变量,顺着作用域链找
console.log(c) // 当前作用域变量
}
f2()
}
f1()
自由变量的取值
要到创建这函数的那个作用域中取值,而不是调用, 这里强调是创建
,无论函数将在哪里调用,这也就是所谓的 静态作用域
例子1:
var x = 10
function fn() {
console.log(x) // 10
}
function show(f) {
var x = 20
(function () {
f() // 10, 而不是20
})()
}
show(fn)
简单过程
fn 函数在取值变量 x 时,要到自己定义的作用域中去寻找 x,x = 10
例子2:
var a = 10
function fn() {
var b = 20
function bar() {
console.log(a + b) // 30
}
return bar
}
var x = fn() // x = bar
b = 200
x() // bar() ==> 30
简单过程
fn() 返回 bar 函数,赋值x,执行 x(),执行bar
bar 作用域没有b,去向创建 bar 的作用域的 fn 作用域取找到 b = 20
fn作用域中没有 a,去创建 fn 的作用域中查处,找到 a = 10
作用域与执行上下文
js 属于解释型语言。解释和执行两个阶段
解析阶段
词法分析
语法分析
作用域规则确定
执行阶段
创建执行上下文(this 指向是执行的时候确定的)
执行函数代码
垃圾回收
作用域与 this 关系
执行上下问是在运行的时候确定,随时可以改变
作用域在定义的时候确定,并且不会改变
同一个作用域下,不同的调用,会产生不同的执行上下文环境,继而产生不同的变量的值
一个作用域下包括若干个上下文环境,也可能没有上下文环境(函数未调用),有可能同时存在一个或多个(闭包)
变量提升 和 函数提升
为什么会有 变量提升 和 函数提升
- js 在读取代码过程分为两步
- 第一步解析 js 代码,第二步执行 js 代码
- 浏览器在解析遇到 var 和 function 整个函数时,会提升到当前作用域的最前面
变量提升
- 在 es6 之前,js 没有块级作用域这一说,只有全局作用域和函数作用域。 var声明的变量提升到他所在的作用域的最顶端。
函数提升
- 函数声明 可被提升:
function foo() {}
- 函数表达式 不可提升:
var foo = function () {}
例子1:函数声明可被提升
console.log(square(5)); // 25
function square(n) {
return n * n;
}
过程相当于
function square(n) {
return n * n
}
console.log(square(5)) // 25
例子2:函数表达式不可被提升
console.log(square) // undefined
console.log(square(5)) // square is not a function =》 初始化并未提升,此时 square 值为 undefined
var square = function (n) {
return n * n
}
过程:
var square
console.log(square) // undefined =》赋值没有被提升
console.log(square(5)) // square is not a function =》 square 值为 undefined 故报错
square = function (n) {
return n * n
}