作用域链
作用域(scope)作用域是程序源代码中定义变量的区域,规定了当前执行代码对变量和函数可访问的范围。
ES6之前只有全局作用域和函数作用域,没有块级作用域。
JavaScript采用静态作用域。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
// 当采用静态作用域时,在调用foo时,先从foo函数内部查找是否有局部变量value
// 如果没有,就在foo被定义的位置(不是调用时位置),查找上一层的代码(全局作用域)
查找变量时,先从当前上下文的变量对象中查找,如果没找到就到父级(词法层面的父级)执行上下文的变量对象中查找,一直到全局上下文的变量对象,也就是是全局对象,这样由多个执行上下文的变量对象构成的链表就是作用域链(scope chain)。
变量查找也包括原型链查找。
函数创建
函数的作用域在函数创建时已确定,函数在创建时有一个内部属性[[scope]],保存了所有父级变量对象在其中,可以把[[scope]理解为所有父级变量对象的层级链(注意:[[scope]]并不代表完整的作用域链!)。
在函数作用域中所定义的变量和内部函数在函数外边是不能直接访问到的,而且并不会污染全局变量对象。
函数激活
函数被调用时,进入函数上下文,创建VO/AO,将活动对象添加到作用域的前端。
此时执行上下文的作用域链可以表示为:
Scope = [AO].concat([[Scope]]);
var x = 10;
function foo () {
console.log(x);
}
function fun () {
var x = 20;
var bar = foo;
bar();
}
fun(); // 是10,而不是20
// 进入全局环境,创建变量对象,执行代码
globalContext.VO = {
foo: ,
fun: ,
x: 10
};
// 函数foo()创建时,[[scope]]保存父级作用域链
foo.[[scope]] = [globalContext.VO]
// 函数fun()创建时,[[scope]]保存父级作用域链
fun.[[scope]] = [globalContext.VO]
// 函数fun()调用时,funContext压入执行上下文栈
ECStack = [
funContext,
globalContext
];
// 创建funContext,1.复制fun函数[[scope]]属性,初始化作用域链
funContext = {
Scope: fun.[[scope]],
};
// 创建funContext,2.创建活动变量,arguments,函数声明,变量声明
funContext = {
AO: {
arguments: {length: 0},
x: undefined,
bar: undefined
}
};
// 创建funContext,3.将活动对象压入fun作用域顶端
funContext = {
AO: {
arguments: {length: 0},
x: undefined,
bar: undefined
},
Scope: [AO, fun.[[Scope]]]
};
// funContext创建完成,执行代码修改AO属性值
funContext = {
AO: {
arguments: {length: 0},
x: 20,
bar:
},
Scope: [AO, globalContext.VO]
};
// 调用bar(),引用地址指向foo(),fooContext压入执行上下文栈
ECStack = [
fooContext,
funContext,
globalContext
];
// 创建fooContext:
// 1.复制fun函数[[scope]]属性,初始化作用域链
// 2.创建活动对象,arguments,函数声明,变量声明
// 3.将活动对象压入foo作用域顶端
fooContext = {
AO: {
arguments: {length: 0},
},
Scope: [AO, foo.[[Scope]]]
};
// fooContext初始化完成,执行代码修改AO属性值
fooContext = {
AO: {
arguments: {length: 0},
},
Scope: [AO, globalContext.VO]
};
// 在foo函数中执行console.log(x)语句,查找变量x;
fooContext.AO; // not found
fooContext.Scope -> globalContext.VO -> x = 10 // found
闭包
闭包(Closures)是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见形式就是在一个函数内部创建另一个函数,内部函数在执行的时候,访问了外部函数的变量对象。此时,外部函数就是闭包。
var name = "Tom";
function getName(){
var name = "Leo";
function fn(){
console.log(name);
}
return fn;
}
var foo = getName(); //执行getName函数,讲返回结果fn函数的引用赋值给foo
foo(); // Leo
// 1.执行getName函数,创建getName函数执行上下文,getName执行上下文进栈
// 2.getName执行上下文初始化,创建变量对象、作用域链、this等
// 3.getName函数执行完毕,返回fn函数引用,getName执行上下文出栈
// 4.执行fn函数,创建fn函数执行上下文,fn执行上下文进栈
// 5.fn执行上下文初始化,创建变量对象、作用域链、this等
// 6.执行fn中代码,查找变量name,fnContext.AO中没有,在作用域链中查找
// 7.fnContext = {
Scope: [AO, getNameContext.AO, globalContext.VO],
}
// 8.在getNameContext.AO中找到变量name,不再向上查找,输出name="Leo"。
// 虽然fn在执行时,getNameContext已出栈(销毁),但getNameContext.AO还在内存中
// 这是因为fn的作用域链会引用这个活动对象,直到fn被销毁,getNameContext.AO才会被销毁
// 这种执行上下文已销毁,但它的子执行上下文依旧可以引用该上下文的变量的机制就形成了闭包。
通过闭包可以保存整个变量对象,但是只能取得变量的最后一个值。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
// 当执行data[0]函数的时候,全局变量i为3
// 此时,data[0]函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
// data[0]Context的AO没有i的值,所以会从globalContext.VO中查找,此时i=3
// data[0],data[2]同理
// 期望依次输出0,1,2
// 使用一个立即执行函数,参数num接收传入的变量i
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (num) {
return function(){
console.log(num);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
// 在执行data[0]函数时,data[0]函数的作用域链为:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO,globalContext.VO]
}
// 匿名函数Context.AO = {
arguments: {
0: 0,
length: 1
},
num:0 // 将变量i的当前值赋值给参数num
}
// data[0]Context的AO没有num值,沿着作用域链从匿名函数Context.AO中查找,找到num=0
只有内部函数访问了上层作用域链中的变量对象时,才会形成闭包,且与作用域链的访问顺序有关。
function fn1() {
var a = 1;
return function fn2() {
var b = 2;
return function fn3() {
// console.log(a);//1 闭包是fn1
// console.log(b);//2 闭包是fn2
console.log(a,b);//1 2 闭包是fn1 fn2
}
}
}
var fn2 = fn1();
var fn3 = fn2();
fn3();
参考资料:
《JavaScript高级程序设计》
《JavaScript 标准参考教程》
汤姆大叔-深入理解JavaScript系列