分享人:宋恒
目录
1.背景介绍
2.知识剖析
3.常见问题
4.解决方案
5.编码实战
6.扩展思考
7.参考文献
8.更多讨论
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。 每个执行环境都有一个与之关联的变量对象, 环境中定义的所有变量和函数都保存在这个对象中。 我们编写的代码是无法访问这个对象的,但解析器在处理数据时会在后台使用它。
作用域是每种计算机语言最重要的基础之一,当然它也是JavaScript最重要的概念之一。要想真正的深入了解JavaScript, 了解JavaScript的作用域链非常必要。
作用域,在维基百科上解释是:在电脑程序设计中,作用域(scope,或译作有效范围)是名字(name)与实体(entity)的绑定(binding) 保持有效的那部分计算机程序。 简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中, 变量的作用域有全局作用域和局部作用域两种,局部作用域又称为函数作用域。
在代码任何地方都能访问全局作用域,一般来说以下几种情形拥有全局作用域:
(1)程序最外层定义的函数或者变量:
var a = "tsrot";
function hello(){
alert(a);
}
function sayHello(){
hello();
}
alert(a); //能访问到tsrot
hello(); //能访问到tsrot
sayHello(); //能访问到hello函数,然后也能访问到tsrot
(2)所有末定义直接赋值的变量(不推荐)
function hello(){
a = "tsrot";
var b = "hello tsrot";
}
alert(a); //能访问到tsrot
alert(b); //error 不能访问
(3)所以Windows对象的属性和方法
一般情况下,window对象的内置属性都拥有全局作用域, 例如window.name、window.location、window.top等等。
在函数内创建,函数外不可访问
function hello(){
var a = "tsrot";
alert(a);
}
hello(); //函数内可访问到tsrot
alert(a); //error not defined
了解作用域链之前我们要知道一下几个概念:
1.变量和函数的声明
2.函数的生命周期
3.Activetion Object(AO)、Variable Object(VO)
在JavaScript引擎解析JavaScript代码的时候,首先,JavaScript引擎会把 变量和函数的声明提前进行预解析,然后再去执行其他代码。
变量声明:变量的声明只有一种方式,那就是用var关键字声明,直接赋值不是一种声明方式。 这仅仅是在全局对象上创建了新的属性(而不是变量)。它们有一下区别:
alert(a); // undefined
alert(b); // error "b" is not defined
b = 10;
var a = 20;
(2)直接赋值形式是在执行阶段创建
alert(a); // undefined, 这个大家都知道
b = 10;
alert(b); // 10, 代码执行阶段创建
var a = 20;
alert(a); // 20, 代码执行阶段修改
a = 10;
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
var b = 20;
alert(window.b); // 20
alert(delete b); // false
alert(window.b); // 仍然为 20,因为变量是不能够删除的。
(1)function name( ){ }直接创建方式
(2)new Funtion构建函数创建
(3)给变量赋值匿名函数方法创建
函数的的生命周期分为创建和执行两个阶段。 在函数创建阶段,JS解析引擎进行预解析,会将函数声明提前,同时将该函数放到全局作用域中或当前函数的上一级函数的局部作用域中。 在函数执行阶段,JS引擎会将当前函数的局部变量和内部函数进行声明提前, 然后再执行业务代码,当函数执行完退出时,释放该函数的执行上下文,并注销该函数的局部变量。
AO:Activetion Object(活动对象)
VO:Variable Object(变量对象)
VO对应的是函数创建阶段,JS解析引擎进行预解析时,所有的变量和函数的声明,统称为Variable Object。
该变量与执行上下文相关,知道自己的数据存储在哪里,并且知道如何访问。VO是一个与执行上下文相关的特殊对象,
它存储着在上下文中声明的以下内容:
变量 (var, 变量声明);
函数声明 (FunctionDeclaration, 缩写为FD);
函数的形参
function add(a,b){
var sum = a + b;
function say(){
alert(sum);
}
return sum;
}
// sum,say,a,b 组合的对象就是VO,不过该对象的值基本上都是undefined
AO对应的是函数执行阶段,当函数被调用执行时,会建立一个执行上下文,该执行上下文包含了函数所需的所有变量,
该变量共同组成了一个新的对象就是Activetion Object。该对象包含了:
1.函数的所有局部变量
2.函数的所有命名参数
3.函数的参数集合
4.函数的this指向
unction add(a,b){
var sum = a + b;
function say(){
alert(sum);
}
return sum;
}
add(4,5);
// 我用JS对象来表示AO
// AO = {
// this : window,
// arguments : [4,5],
// a : 4,
// b : 5,
// say : ,
// sum : undefined
// }
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain) 来保证对执行环境有权访问的变量和函数的有序访问。 作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)。
function add(a,b){
var sum = a + b;
return sum;
}
假设函数是在全局作用域中创建的,在函数a创建的时候, 它的作用域链填入全局对象,全局对象中有所有全局变量,此时的全局变量就是VO。此时的作用域链就是:
此时作用域链(Scope Chain)只有一级,就为Global Object
scope(add) -> Global Object(VO)
VO = {
this : window,
add :
}
}
如果是函数执行阶段,那么将其activation object(AO)作为作用域链第一个对象, 第二个对象是上级函数的执行上下文AO,下一个对象依次类推。
add(4,5);
调用add后的作用域链是:
此时作用域链(Scope Chain)有两级,第一级为AO,然后Global Object(VO)
scope(add) -> AO -> VO
AO = {
this : window,
arguments : [4,5],
a : 4,
b : 5,
sum : undefined
}
VO = {
this : window,
add :
}
}
在函数运行过程中标识符的解析是沿着作用域链一级一级搜索的过程,从第一个对象开始, 逐级向后回溯,直到找到同名标识符为止,找到后不再继续遍历,找不到就报错
再举个例子
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
console.log(x + y + z);
};
bar()
};
foo();
}
上面代码的输出结果为”60″,函数bar可以直接访问”z”, 然后通过作用域链访问上层的”x”和”y”。此时的作用域链为:
此时作用域链(Scope Chain)有三级,第一级为bar AO,第二级为foo AO,然后Global Object(VO)
scope -> bar.AO -> foo.AO -> Global Object
bar.AO = {
z : 30,
__parent__ : foo.AO
}
foo.AO = {
y : 20,
bar : ,
__parent__ :
}
Global Object = {
x : 10,
foo : ,
__parent__ : null
}
}
标志符解析是什么
在执行上下文的作用域中查找变量的过程被称为标识符解析(indentifier resolution), 这个过程的实现依赖于函数内部另一个同执行上下文相关联的对象——作用域链。作用域链是一个有序链表, 其包含着用以告诉JavaScript解析器一个标识符到底关联着哪一个变量的对象。而每一个执行上下文都有其自己的作用域链Scope。 一句话:作用域链Scope其实就是对执行上下文EC中的变量对象VO|AO有序访问的链表。能按顺序访问到VO|AO,就能访问到其中存放的变量和函数的定义。
如何用这些知识实现代码优化?
从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。 因为全局变量总是存在于运行期上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。 所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。 一个好的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。
感谢大家观看
宋恒