执行环境及作用域
执行环境(execution context
)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
每个执行环境都会对应一个变量对象(variable object
),环境中定义的所有变量和函数都保存在这个对象中,即为变量对象的属性或方法。这个对象是无法通过编写代码访问的,供解析器在后台处理数据时使用。
全局执行环境是最外围的一个执行环境,在web浏览器中被认为是 window
对象。
某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。全局执行环境直到应用程序退出(关闭网页/浏览器)时被销毁。
每个函数都有自己的执行环境(可以理解为函数自身就是一个执行环境)。
执行流机制 - 当执行到一个函数时,函数的环境就会被推入一个环境栈中,函数执行完毕后,栈将其环境弹出,把控制权返回给之前的执行环境。
当代码在一个执行环境中执行时,会创建变量对象一个的作用域链,用于保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端,始终都是当前执行的代码所在环境的变量对象。
如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始只包含一个变量 - arguments
对象,下一个变量对象来自包含环境,再下一个变量对象来自下一个包含环境。
作用域链中的最后一个对象始终都是全局执行环境的变量对象。
标识符(变量的名字)解析是沿着作用域链一级一级地搜索标识符的过程,这个过程始终从作用域链的前端开始,然后逐级向后回溯,直至找到标识符为止;如果找不到标识符,通常会发生错误。
var color = "blue";
function changeColor(){
if (color === "blue"){
color === "red";
} else{
color === "blue";
}
}
changeColor();
这个例子中,changeColor()
函数的作用域包括两个对象,自身的 arguments
对象和全局执行环境的变量对象。color
变量是在全局环境中定义的,之所以在changeColor()
函数内部能够访问这个变量,就是因为在作用域链中可以找到它。
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColor(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问 color 、 anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor
swapColor();
}
changeColor();
每个环境可以沿着作用域链向上搜索变量和函数名,但不能向下。
也可以这样说,内部环境能访问外部环境的变量对象,但是外部环境不能访问内部环境。
延长作用域链
try-catch
语句的 catch
块和 with
语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。
没有块级作用域
对于以 {}
包含起来的代码块,在JavaScript中并不具有独立的作用域,而是会被添加到当前的执行环境中。
// 拥有块级作用域,会在if语句结束后立即销毁 color 变量
if(true){
var color = "blue";
}
alert(color); // "blue" 在JavaScript会返回"blue",但在拥有块级作用域的语言中会发生错误
// 同理
for(var i = 0; i<10; i++){
doSomething(i);
}
alert(i); // 10 在JavaScript中i在for循环结束后,依然存在于循环外部的
是否使用 var
决定了变量的执行环境。使用 var
声明变量,该变量会被添加到最近的执行环境中(局部变量);若果变量没有使用 var
声明(不建议),默认该变量为全局变量。
// EX1
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
var result = add(10, 20); // 30
alert(sum); // 错误!!! sum 定义在函数内部,无法再函数外不访问
// EX2
function add(num1, num2){
sum = num1 + num2; // 不用 var 声明
return sum;
}
var result = add(10, 20); // 30
alert(sum); // 30
// EX3
// 在自身环境中有指定查询标识符,就用自身的
var color = "blue";
function getColor(){
var color = "red";
return color;
}
alert(getColor()); // "red"
alert(color); // "blue"
递归
递归函数是在一个函数通过名字调用自身的。
// 经典递归阶乘函数
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
// 这个函数表面看没有什么问题,但却会导致下面的错误
var anotherFactorial = factorial;
factorial = null;
anotherFactorial(4); // 错误!!!
/*
* 以上代码先将 factorial() 函数保存在变量 anotherFactorial 中;
* 然后将factorial变量设为null,结果指向原始函数的引用只剩下anotherFactorial
* 但接下来调用anotherFactorial()时,由于必须先执行factorial()此时factorial()不再是函数,就会导致错误。
*/
// 使用 arguments.callee 解决,arguments.callee 是一个指向正在执行的函数的指针
function factorial(num){
if(num<=1){
return 1;
} else {
return num*arguments.callee(num-1);
}
}
// 但在严格模式下,不能通过脚本访问 arguments.callee ,可使用下面方法 (最佳实践)
var factorial = (function f(num){
if(num<=1){
return 1;
} else {
return num*f(num-1);
}
});
闭包
匿名函数
var functionName = function(arg0, arg1, ...){ //... };
闭包 是指有权访问另一个函数作用域中的变量的函数。常见的创建方式就是在函数内部创建另一个函数。
function createComparisonFunction(propertyName){
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object1[propertyName];
if(value1<value2){
return -1;
} else if(value1>value2){
return 1;
} else {
return 0;
}
}
}
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后使用 arguments
和其他命名参数的值来初始化函数的活动对象。在作用域链中,外部函数的活动对象始终处在第二位,外部函数的外部函数的活动对象处在第三位,…,直至作用域链中点的全局执行环境。
前面说到,每个执行环境都对应一个变量对象。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局环境的变量对象。
但是闭包有所不同!!
在另一个函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中。
var compare = createComparisonFunction("num");
var result = compare({name: "Leo"}, {name: "Jeo"});
当匿名函数从 createComparisonFunction()
中被返回后,它的作用域链被初始化为包含 createComparisonFunction()
函数的活动对象和全局变量对象。这样,匿名函数就可以访问在 createComparisonFunction()
中定义的所有变量。更重要的是, createComparisonFunction()
函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。
换句话说,当 createComparisonFunction()
函数被返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()
的活动对象才会被销毁。
var compareNames = createComparisonFunction("name");
var result = compareNames({name: "Leo"}, {name: "Jeo"});
compareNames = null;
闭包与变量
作用域链会产生一个副作用,即闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(){
return i;
};
}
return result;
}
表面上看,似乎每个函数都应该返回自己的索引值;但实际上,每个函数都会返回10。因为每个函数的作用域链中都保存着 createFunctions()
函数的活动对象,所以它们引用的都是同一个变量i。当 createFunctions()
函数返回后,变量i的值为10,此时每个函数都引用着保存变量i的同一个变量对象。
可以通过创建一个另一个匿名函数强制让闭包的行为符合预期。
function createFuncitons(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
这样重写后,没有把闭包直接赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。
this
this
对象是在运行时基于函数的执行环境绑定的:在全局函数中,this
指向 window
,而当函数作为某个对象的方法调用时,this
指向那个对象。不过,匿名函数的执行环境具有全局性,因此其 this
对象通常指向 window
。但有时候由于编写闭包的方式不同,又会呈现不同。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); // "The Window" (非严格模式下)
每个函数在被调用时都会自动取得两个特殊变量: this
和 arguments
。内部函数在搜索这两个变量时,只会搜索到其活动变量为止,因此永远不可能直接访问外部函数中的这两个变量。
// 解决方法
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); // "My Object"