作用域,作用域链,域解析
转载自 陈玉
执行环境
JavaScript执行环境,也叫作JavaScript执行上下文(我百度翻译的(^-^),英文名叫做execution context)执行环境:JavaScript代码都是在执行环境中被执行的。执行环境是一个概念,一种机制,用来完成JavaScript运行时在作用域、生命周期等方面的处理,它定义了变量或函数是否有权访问其他数据,决定各自行为。
而且每个执行环境都会有一个就是变量对象VO(variable object),环境中变量和函数声明保存在VO中。注意,函数表达式(与函数声明相对)不包含在变量对象之中。,在代码执行开始,会进入不同的执行环境,这些执行环境共同构成了一个执行环境栈。
如果这个环境是函数,则将其活动对象AO(activation object)作为变量对象,活动对象最开始的两个属性是arguments和this
在下个图中,拥有一些函数上下文EC1和全局上下文Global EC,当EC1进入和退出全局上下文的时候下面的栈将会发生变化:
JS中一共有三种执行环境:
- 全局执行环境(Global code) JS代码开始运行时默认的
即全局的、不在任何函数里面的代码,例如:一个js文件、嵌入在HTML页面中的js代码等注意:
- 保存在全局执行环境中的所有变量和函数,当其应用程序退出,如:关闭网页或浏览器时被销毁
- 全局执行环境是window的对象(所有全局函数和变量都是作为window的属性和方法创建的)
1
2
3
4
5
6
7
8var a=2;
function setB(){
return 123;
}
console.log(a);
console.log(setB());
console.log(window.a);
console.log(window.setB());
- 函数执行环境(Function code) 代码进入了一个函数
每个函数有自己的执行环境,有相应的执行栈
- 函数里的局部作用域里的变量替换全局变量,但作用域仅限在函数体内的这个环境
1
2
3
4
5
6var a='哈哈哈~';
function setB(){
a='啧啧';
}
setB();
console.log(a);
通过传参,可以替换函数体内的局部变量,但作用域仅限在函数体内的这个环境
1
2
3
4
5
6var a='哈哈哈~';
function setB(a){
console.log(a);
}
setB('嗯嗯');
console.log(a);函数体内还包含有函数,只有通过外层函数,才能访问内层函数
1
2
3
4
5
6
7
8
9
10var a=1;
function setA(){
function setB(){
var b=2;
console.log(a);
console.log(b);
}
setB();
}
setA();
- eval()方法执行代码
eval()就像是一个完整的ECMAScript解析器,只接受一个参数,即要执行的ECMAScript(或JavaScript)字符串1
2
3eval("console.log('哈哈')");
等价于
console.log('哈哈');
通过eval()执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链1
2eval("function sayHi(){console.log('hi');}");
sayHi();
严格模式下,访问不到eval()中创建的任何变量和函数1
2
3"use strict";
eval("function sayHi(){console.log('hi');}");
sayHi();
应用:JSON解析器
危险:代码注入!!!(用户输入数据)
作用域
域:空间,范围,区域…作用:读,写
作用域(scope)。在很多语言中(C++,C#,Java),作用域都是通过代码块(由{}包起来的代码)来决定的,但是,在JavaScript作用域是跟函数相关的,也可以说成是function-based。1
2
3
4
5
6
7
8
9
10
11
12
13function test(o){
var i=0;
if(typeof o=="object"){
var j=0;
for(var k=0;k<10;k++){
console.log(k);
}
console.log(k);
}
console.log(j);
}
var obj = new Object();
test(obj);
- 全局作用域中的对象可以在代码的任何地方访问,一般来说,下面情况的对象会在全局作用域中:
最外层函数和在最外层函数外面定义的变量
1 | // 最外层函数和在最外层函数外面定义的变量 |
没有通过关键字”var”声明的变量
1 | function outFn2() { |
浏览器中,window对象的属性1
2window对象的内置属性都拥有全局作用域,
如: window.name、window.location、window.top 等
- 局部作用域(函数作用域)所有的变量和函数只能在作用域内部使用。函数在声明它们的函数体以及这个函数体嵌套的任意函数内都是有定义的
1
2
3
4
5
6
7
8var foo = 1;
window.bar = 2;
function baz(){
a = 3;
var b = 4;
}
// Global scope: foo, bar, baz, a
// Local scope: b
函数声明提前:
js的函数作用域是指在函数内声明的所有变量在函数内始终都是可以见,意味着变量在声明之前就可以使用了,这个特性被非正式命名为声明提前即:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var a=1;
function fn1(){
console.log(a);//???
var a=2;
console.log(a);
}
fn1();
console.log(a);
-------------------------------------------------
var a=1
function fn1(){
var a;//在函数顶部声明了局部变量
console.log(a);//变量存在,但其值是"undefined"
a=2;
console.log(a);
}
fn1();
console.log(a);
作用域链(scope chain)
当代码在一个环境中执行时,会创建变量对象的一个作用域链(是保证对执行环境有权访问的的所有变量和函数的有序访问),这个作用域链是一个对象列表或者链表,这组对象定义了代码“作用域”中的变量创建规则:
作用域链的前端,始终都是当前执行的代码
作用域链中的下一个变量对象,来自包含(外部环境)
而再下一个对象来自下一个包含环境
一直延续到全局执行环境(全局执行坏境的对象始终都是作用域链中的最后一个对象)
一个函数创建时,javascript后台(引擎)会默认创建一个仅供后台使用的内部属性[[Scope]],此属性存储函数的作用域链,如果是全局函数,此时则包含一个变量对象(全局变量),如果是嵌套函数(闭包),作用域链还加上了父函数的变量对象。例如下面的这个全局函数:1
2
3
4
5
6function add(num1,num2){
var sum = num1 + num2;
return sum;
}
var total=add(5,10);
console.log(total);
b) 函数被调用时–add(5,10),javascript后台会创建一个内部对象(execution context)–“执行环境”或“运行期上下文”,活动对象作为函数执行期的一个变量对象,包含所有局部变量(在函数内定义的)、命名参数、arguments、this,它会被推入到执行环境作用域链的前端(如下图)。每执行一次函数都会创建一个新的执行环境,当函数执行完毕执行环境就会被销毁。
图中矩形表示特定的环境。其中内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部变量环境中的任何变量和函数。这些环境之间的联系是线性的,有次序的。每个环境变量都可以向上搜索作用域链,以查询变量和函数名;反之则是不行。注意:函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。
查询标识符(标识符指的是:变量名称,函数声明,形参,等等。):标示符的解析是沿着作用域链一级一级的搜索标示符的过程,搜索过程都是从作用域链的前端开始,然后逐级向后回溯,直到找到标识符为止1
2
3
4
5var color="blue";
function getColor(){
return color;
}
console.log(getColor());//"blue"
延长作用域链
try-catch语句的catch块
with语句
这两个语句都会在原本的作用域链的前端添加一个变量对象。对于with语句来说,新添加的变量对象包含着with括号中指定对象的所有属性和方法所作的变量声明。对于catch来说,当try块发生错误时,代码执行流程自动转入到catch块,并将异常对象推入到作用域链的前端。catch块执行完毕后,作用域链就会返回原来的状态。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function initUI(){
with(document){
var bd = body,
links = getElementsByTagName("a"),
i = 0,
len = links.length;
while(i<len){
update(links[i++]);
}
getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active"
}
}
当代码流执行到一个with表达式时,执行环境的作用域链会被临时改变,此时with的变量对象会被创建添加到作用域链的前端,这就意味着此时函数的所有局部变量都被推入到第二个作用域链中的变量对象,如下图:
注意:在执行with语句时,访问局部变量的代价更高了。所以尽可能避免使用with语句,可以使用局部变量代替1
var doc = document; // 代替with(document){...}
##域解析
- 浏览器:
预解析:
“js解析器”
“找一些东西”:var, function
,参数
a=未定义
所有的变量,在正式运行代码之前,都提前赋了一个值(未定义)
fn1=function fn1(){console.log(1);}
所有的函数,在正式运行代码之前,都是整个函数块注意:遇到重名,变量和函数重名,就留下函数,与上下文没有关系,注只先找var ,function声明的,函数重名,则采用覆盖1
2
3
4
5console.log(a);
var a=2;
function fn1(){
console.log(1);
}
- 逐行解读代码
1
2
3
4
5
6表达式:= + - * / ++ -- !参数
表达式可以修改预解析的值!
函数调用:
同理函数作用域也发生:
预解析和逐行解读代码
函数:由里到外(作用域链)
1 | console.log(a);//不会从下面取值,直接从预解析的仓库里取出undefined |
参考链接:
https://gaohaoyang.github.io/2015/05/20/scope/
http://dmitrysoshnikov.com/ecmascript/javascript-the-core/#execution-context
参考资料:
javaScript高级程序设计
javaScript权威指南


