1.作用域链
1.1 作用域链是什么?
作用域链正是内部上下文所有变量对象(包括父变量对象)的列表。
首先,代码在其对应的环境中执行时,数据是保存在其变量对象中的。但是有时会使用其范围外的数据。当需要查找变量的值时,现将变量解析,然后从链表的第一项开始寻找,找到为止。
1.2 作用域链的形成
全局环境中,作用域链由一个全局对象组成。
而函数中,作用域链的形成分为两个过程
(1)函数创建时:会创建一个预先包含全局变量对象的作用域链,被保存在内部的【【scope】】属性中。
(2)当调用函数时,会为函数创建一个执行环境,然后通过复制函数的【【scope】】属性中的对象构建起执行环境的作用域链。然后又有一个活动对象被创建并推入执行作用域链的最前端。
即函数作用域链 = AO + scope
1.3 作用域链形成的实际列子
引用汤姆大叔的文章
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo(); // 60
全局上下文的变量对象是:
globalContext.VO === Global = {
x: 10
foo:
};
在“foo”创建时,“foo”的[[scope]]属性是:
foo.[[Scope]] = [
globalContext.VO
];
在“foo”激活时(进入上下文),“foo”上下文的活动对象是:
fooContext.AO = {
y: 20,
bar:
};
“foo”上下文的作用域链为:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
内部函数“bar”创建时,其[[scope]]为:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
在“bar”激活时,“bar”上下文的活动对象为:
barContext.AO = {
z: 30
};
“bar”上下文的作用域链为:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];
1.4 此过程中闭包的形成
函数(调用时),会创建一个执行环境及相应的作用域链,然后使用arguments和其他命名参数的值来初始化函数的活动对象(AO),但在作用域链中,外部函数的活动对象始终处于第二位,外部的外部处于第三位,以此类推。
执行环境有一个表示变量的对象--变量对象(VO),全局环境的VO始终存在,函数这样的局部环境的VO只在函数的执行过程中存在。
内部函数中将会将外部函数的活动对象添加到其作用域链中,当外部函数执行完毕后,其执行环境的作用域链会被销毁,而活动对象会仍然保留在内存中。
由于作用域链的形成机制,导致了某些函数在执行完毕后,并不会销毁其活动对象,因为其被包含在了其他函数的作用域链中。
2. 闭包
可以以正常数据形式存在的函数(比方说:当参数传递,接受函数式参数或者以函数值返回)都称作 第一类函数(一般说第一类对象)。在ECMAScript中,所有的函数都是第一类对象。
在函数中,如果包含了自由变量(并未在函数内部声明),那么即产生了闭包。因为它涉及到了另外一个环境的数据,那么会将另外一个环境中的VO暂存起来。
2.2 闭包实战
2.2.1如下代码输出多少?如果想输出3,那如何改造代码?
var fnArr = [];
for (var i = 0; i < 10; i ++) {
fnArr[i] = function(){
return i
};
}
console.log( fnArr[3]() )
分析:代码的本意是想,fnArr3中,是调用的第数组中的四个函数,并且函数范围值与数组下标是一样的。这里会直接输出10,因为数组中函数仅仅是声明了,I并未赋值,等到调用时,其作用域中并不包含I,就向上级寻找I,而此时所有的函数都是共享一个父作用域,I的值为10.
如果要输出3,就是让每一次为数组赋值时,能够让函数能够记住此时i的值,并在调用的时候能够取到。
所以我们将循环更改一下:
for (var i = 0; i < 10; i ++) {
(function(j){
fnArr[j] = function(){
return j
};
})(i);
}
这里我们将赋值过程用一个立即执行函数封装了起来,并且通过参数传值,将i的值保存在了外层立即执行函数的执行上下文中。
2.2.2 封装一个 Car 对象
var Car = (function(){
var speed = 0;
function set(speed1){
speed = speed1
console.log(speed)
}
function get(){
console.log(speed)
return speed;
}
function speedUp(){
speed++;
console.log(speed)
}
function speedDown(){
speed--;
console.log(speed)
}
return {
set: set,
get: get,
speedUp: speedUp,
speedDown: speedDown
}
})()
Car.set(30)
Car.get() //30
Car.speedUp()
Car.get() //31
Car.speedDown()
Car.get() //30
这里用一个立即执行函数,并返回我们需求的对象的形式来封装。这样的好处是所有的数据都被封装在了立即执行函数中,不会暴露在外面。
2.2.3 如下代码输出多少?如何连续输出 0,1,2,3,4
for(var i=0; i<5; i++){
setTimeout(function(){
console.log('delayer:' + i )
}, 0)
}
分析:setTimeout外层创建一个闭包
for(var i=0; i<5; i++){
(function (i){
setTimeout(function(){
console.log('delayer:' + i )
}, 0)
})(i)
}
2.2.4 如下代码输出多少?
function makeCounter() {
var count = 0
return function() {
return count++
};
}
var counter = makeCounter()
var counter2 = makeCounter();
console.log( counter() ) // 0
console.log( counter() ) // 1
console.log( counter2() ) // 0
console.log( counter2() ) // 1
分析:makeCounter都是返回了一个函数。且couter
与couter2所返回的函数是同名的。但是每次调用时,进入执行上下文,然后绑定了新的作用域链了。
2.2.5 补全代码,实现数组按姓名、年纪、任意字段排序
var users = [
{ name: "John", age: 20, company: "Baidu" },
{ name: "Pete", age: 18, company: "Alibaba" },
{ name: "Ann", age: 19, company: "Tecent" }
]
function byField(string){
return function(user1,user2){
return user1[string] >user2[string]
}
}
users.sort(byField('age'))
2.2.6 写一个 sum 函数,实现如下调用方式
console.log( sum(1)(2) ) // 3
console.log( sum(5)(-1) ) // 4
解答:
function sum(val1){
return function(val2){
return val1+val2;
}
}