执行环境
为方便起见,执行环境可简称为环境。
环境分为两种:
- 全局环境
- 局部环境
每个环境都有一个与之关联的变量对象,在该环境中定义的所有变量和函数都会存储在这个变量对象中。
那么环境是怎么产生的?在什么情况下会创建环境?
在 ES 标准中,每个函数都有一个自己的执行环境,可称为局部环境(或者由 let 搭配花括号创建的块级作用域也拥有自己的局部环境)。在函数中定义的所有变量和函数,都被存储在该函数当前所处环境的变量对象中。
除了函数所创建的局部环境,另一个就是全局环境了。
全局环境是最外围的一个执行环境,根据实现 ES 的宿主环境不同,表示全局环境的变量对象也不同。
对 Web 浏览器来说,全局环境的变量对象是 window 对象,所有在全局环境中定义的变量和对象都会保存在 window 对象中。
当环境中的代码执行完毕后,这个环境将会被销毁,该变量对象也会随之销毁。
Web 浏览器中的全局执行环境只有当页面被关闭或者浏览器被关闭时才会被销毁。
/*
在全局环境中定义的 color 变量,该变量存储在全局环境的 window 变量对象中
*/
let color = 'red'
/*
在全局环境中声明一个函数,此时全局环境的 window 变量对象中已有两个属性,一个是变量 color,一个是函数 getColor
*/
function getColor() {
/*
在函数内部定义的 color 变量,属于局部变量,存储在函数自己的执行环境所关联的变量对象中,而不是全局 window 变量对象中。
*/
let color = 'blue'
return color
}
/*
调用函数执行函数内的代码,函数体内 return 的是局部环境中的 color 变量。代码执行完毕后,getColor 函数的环境和变量对象随之销毁。
为什么在获取 color 变量时获取的是局部环境中的 color 而不是全局环境中的 color,跟作用域链有关系(请看后面的作用域链)
*/
let color2 = getColor()
console.log(color2) //blue
作用域链
当代码在一个环境中执行时,会创建该变量对象的作用域链。
用途是为了确保能有序地访问当前环境可访问的所有变量和函数。
比如局部环境和全局环境都有一个 color 变量,那么我在局部环境中获取 color 变量时,我获取到的是局部环境内的 color,还是全局环境中的 color,这个先后顺序就和作用域链有关。
作用域链的最前端始终是代码执行所在环境的变量对象,如果这个环境是函数,则把函数当前的活动对象作为这个变量对象,这个活动对象最开始只有一个变量,就是 arguments 对象。
接着,作用域链的下一个对象是外部执行环境的变量对象,作用域链的末尾是全局执行环境的变量对象。
如下:
let color = 'red'
function fn(){
let msg = '测试'
function innerFn() {
let text = 'hh'
}
}
在上面的例子中,全局环境的变量对象中有一个 color 变量和 fn 函数。
对于 fn 函数的局部环境来说,除了 arguments 变量外,变量对象中还存储了 msg 变量和 innerFn 函数。
对于 innerFn 函数的局部环境来说,除了 arguments 变量外,变量对象还存储了 text 变量。
由于作用域链的最前端是当前环境的变量对象,因此对于 innerFn 函数,作用域链最前端的变量对象就是他自己的变量对象,也就是存储了 arguments 变量和 text 变量的变量对象,沿着作用域链往上,fn 函数是包含 innerFn 函数的包含环境,因此作用域链的下一个变量对象就是 fn 函数所处环境的变量对象,再下一个,由于 fn 函数没有其他包含环境了,直接到达全局环境,因此作用域链的下一个变量对象就是全局环境的变量对象。
查询标识符
读取变量时,先从作用域链的最前端开始查找是否有与该变量同名的属性,如果有,则停止向上搜索,如果没有,继续沿着作用域链向上在其他变量对象中查找。
如下
let color = 'red'
function getColor(){
/*
声明局部变量 color 存储在 getColor 函数所在环境的变量对象中。
*/
let color = 'blue'
function innerFn() {
/*
innerFn 的变量对象中没有声明 color 变量,便沿着作用域链向上查找,作用域链上一个变量对象是 getColor 函数所在环境的变量对象,结果在这个变量对象中找到了 color,于是停止查找行为。
执行赋值操作,将 getColor 函数的对象变量中的 color 变量赋值为 yellow
*/
color = 'yellow'
}
innerFn()
// 返回当前局部环境中变量对象的 color 变量
return color
}
let color2 = getColor()
console.log(color2) //yellow
console.log(color) //red 全局环境的作用域链最前端和末尾都是他自己的变量对象,因此从头到尾只有一个变量对象,他无法获取到 getColor 变量对象中的 color 变量的值。
由于全局变量对象中存储了一个名为 color 的变量,因此这里可以获取到 color 的值。
块级作用域
ES6 之前,ES 和其他编程语言有个区别,就是没有块级作用域。
{
var test = 'hhh'
}
console.log(test) //hhh
上面的例子中,对于那些支持块级作用域的编程语言来说,由两个花括号封闭的代码块生成了一个块级作用域,test 变量是在这个代码块中定义的局部变量,在外部获取是无法获取到的。
对于 ES 来说,由于 ES6 之前没有块级作用域这个概念,上面的 test 变量实际是一个全局变量,在任何地方都可以获取到他的值。
声明变量时我们用的是 var 操作符。
var 操作符会将变量存储在最接近的环境中,在函数内部,最接近的环境就是函数的局部环境。如果不用var,变量就会被直接存储在全局变量对象中。
function fn() {
/*
检查 fn 函数所在环境的作用域链,发现作用域链中包含的所有变量对象都没有 test 这个变量,将会直接在全局环境中定义 test 变量,并为其赋值为 hhh,存储在全局变量对象中
*/
test = 'hhh'
}
console.log(test) //hhh
在 ES6 标准中,多了一个声明变量的操作符,是 let 操作符,这个操作符使 ES 拥有块级作用域成为现实。
{
let test = 'hhh'
}
console.log(test) //undefined