JavaScript 中每个代码都有自己的执行环境,所以如果以范围来划分的话,可以划分为全局执行环境和局部执行环境。其中全局执行环境属于最外部的执行环境、局部执行环境属于全局执行环境的子环境属于内部的执行环境。其中全局执行环境是由 ECMAScript 是实现的宿主环境所决定的,在浏览器的宿主环境中全局环境对象为 window 、但是在 node 宿主环境中全局环境对象为 global。
每一个执行环境中都会有一个变量对象来保存当前作用域中定义的变量与函数。但是该变量对象对开发者不透明。
所有的变量、函数都是作为 window 的属性与方法创建的。
var fruit = 'apple';
console.log(fruit);// apple
console.log(window.fruit);// apple
console.log('fruit' in window);// true
执行环境销毁的时机为当前环境中的代码执行完毕,同时保存在变量对象中的变量与函数也随之销毁(除依然有引用关系的除外)。
每个函数也有自己的执行环境不过相对于 window 对象所在的全局环境而言函数内部的执行环境就属于局部执行环境,但即使是局部执行环境也是有着自己的变量对象来存储当前局部执行环境中定义的变量与函数。
当代码执行的时候当前的执行环境会创建变量对象的作用域,这样由不同层级的作用域紧密结合在一起就形成了 - 作用域链。其中每层作用域的变量对象是其核心,作用域链的作用就是保证当前执行环境有权访问的所有变量、函数的有序访问。
作用域链的前端始终指向当前执行环境的变量对象,作用域链的下一个变量对象来自包含它的执行环境中,这样由不同层级的执行环境中的变量对象的作用域就组成了作用域链。
标识符的解析就是一级一级搜索的过程。并且始终是从作用域链的最前端开始逐级的进行回溯直到找到该标识符,如果上一级的作用域中没有该标识符则继续向上一级寻找直至到全局作用域,这个过程叫做回溯。如果找到了就可以使用如果未找到会抛出异常。
-1).在局部执行环境中未定义该变量依然可以通过作用域链找到改变量 :
var fruit = 'apple';
function eat() {
console.log(`eat fruit: ${fruit}`);// eat fruit: apple
}
eat();
上述示例可以看出在 eat 函数中并未定义一个叫做 "fruit" 的变量,但是却可以使用该变量。这是为什么? 有的同仁会说那不是全局变量吗? 自然可以访问的到。这种回答是不负责任的回答,正确的回答应该是虽然说当前的执行环境中并未定义一个叫做 "fruit" 的变量,但是当前的执行环境会沿着作用域链查找父级作用域中是否定义了一个叫做 "fruit" 的变量。针对本案例而言这就找到了全局作用域并发现确实定义了一个变量叫做 "fruit" 所以找到了就可以在局部执行环境中使用了。这才是真正负责任的答案。【以下是图解】:
-2). 在局部执行环境中定义的变量可以在局部执行环境中雨全局执行环境中定义的变量互换 :
void(() => {
var fruit = 'apple';
changeFruits();
function changeFruits() {
var anotherFruit = 'banana';
swapFruits();
function swapFruits() {
var tempFruit = anotherFruit;
anotherFruit = fruit;
fruit = tempFruit;
console.log(`swap area: fruit: ${fruit}; anotherFruit: ${anotherFruit}; tempFruit: ${tempFruit}`);// swap area: fruit: banana;
//anotherFruit: apple; tempFruit: banana
}
console.log(`change area: fruit: ${fruit}; anotherFruit: ${anotherFruit}`);// change area: fruit: banana; anotherFruit: apple
}
console.log(`window area: fruit: ${fruit}`);// window area: fruit: banana
})();
大家可能发现了在 swapFruits 这个函数的执行环境中我们可以通过作用域链来访问到 changeFruits 执行环境中的 anotherFruit 变量 和 window 执行环境中的 fruit 变量。所以才可以完成互换值的工作,但是在 swapFruits 执行环境中是绝对不可以完成这个工作的,因为虽然有作用域链,并且上至 window 全局执行环境,下到 swapFruits 执行环境,但是通过作用域链查找变量只能逐级向上(回溯)查找,不能逆流而下查找。所以 swapFruits 执行环境中的 tempFruit 这个变量拿不到就无法完成该工作。在 window 环境中也同样完不成该工作,因为作用域链只能逐级向上查找,window 执行环境所创建的作用域已经是顶级作用域了所以其当前执行环境拿不到 changeFruits 执行环境中的 anotherFruit 变量、也拿不到 swapFruits 执行环境中的 tempFruit 变量,所以更是完不成该工作。
所以说局部作用域权限最大可以访问祖先级作用域中的变量但是反过来父级作用域的权限就要比子级作用域的权限要低只能访问其所在的当前作用域和其父级作用域中的变量却无法访问其子级作用域中的变量,简而言之越往上返则权限越低,window 执行环境所创建的作用域权限最低。【图解】:
所谓的延长作用域链其实比较简单,试想一下怎样才可以延长作用域链呢? 再添加一个函数么? 肯定不是没有必要的前提下加上一个函数不是多此一举么?所谓延长作用域链是指一种情况在不添加新的函数的前提下在作用域链上临时添加一个变量对象。
with 语句 :
function buildUrl() {
var qs = "?debug=true";
with(location) {
var url = href + qs;
}
return url;
}
console.log(`url: ${buildUrl()}`);
你一定惊异为什么在 with 语句块中定义的 url 变量结果却可以在外部访问到 ?
原因就是作用域链延长了,由于 with 环境中的变量对象使只读的所以不能为其添加属性所以会将其要添加的属性、函数等添加到当前函数所在的执行环境的变量对象中(所以 url 这个变量被添加到了 buildUrl 函数执行环境的变量对象上了)。这无疑相对于 with
中的语句来说确实是作用域得到了延长因为要想访问自己需要的变量值必须得到父级作用域中的变量对象中才可以找得到,这个过程不正又是作用域链的回溯机制吗 ?
在 JavaScript 中是没有块级作用域的,除了函数有自己的块级作用域。这就会导致在 if 或者 for 等语句中定义的局部变量可以在外部访问到。
if(true) {
var fruit = "apple";
}
console.log(`fruit: ${fruit}`);// apple
for(var i = 0; i < 10; i++) {
// doSomeThing ...
}
console.log(`i: ${i}`);// i: 10
这一点说实话是 JavaScript 的特点但无疑也是一个坑。但是这些在 ES6 语法出现后得到了改变, ES6 语法使得 JavaScript 支持了块级作用域!!!
-1).使用 var 关键字声明变量其会被添加到最接近的执行环境的变量对象中 :
function add(num_1, num_2) {
var sum = num_1 + num_2;
return sum;
}
add(10, 20);
console.log(sum);
报错是因为,sum 变量是通过 var 声明的所以被添加到了 add 执行环境的变量对象中了,所以在作用域链上 window 是顶级作用域不能向下寻找所以拿不到 sum 变量,自然会报错。
-2).未使用 var 关键字声明变量其会被添加到全局执行环境的变量对象中 :
function add(num_1, num_2) {
sum = num_1 + num_2;
return sum;
}
add(10, 20);
console.log(sum);
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
加入交流群
请使用微信扫一扫!