1.作用域鏈
1.1 作用域鏈是什么?
作用域鏈正是內(nèi)部上下文所有變量對象(包括父變量對象)的列表。
首先,代碼在其對應的環(huán)境中執(zhí)行時,數(shù)據(jù)是保存在其變量對象中的。但是有時會使用其范圍外的數(shù)據(jù)。當需要查找變量的值時,現(xiàn)將變量解析,然后從鏈表的第一項開始尋找,找到為止。
1.2 作用域鏈的形成
全局環(huán)境中,作用域鏈由一個全局對象組成。
而函數(shù)中,作用域鏈的形成分為兩個過程
(1)函數(shù)創(chuàng)建時:會創(chuàng)建一個預先包含全局變量對象的作用域鏈,被保存在內(nèi)部的【【scope】】屬性中。
(2)當調(diào)用函數(shù)時,會為函數(shù)創(chuàng)建一個執(zhí)行環(huán)境,然后通過復制函數(shù)的【【scope】】屬性中的對象構建起執(zhí)行環(huán)境的作用域鏈。然后又有一個活動對象被創(chuàng)建并推入執(zhí)行作用域鏈的最前端。
即函數(shù)作用域鏈 = 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: <reference to function>
};
在“foo”創(chuàng)建時,“foo”的[[scope]]屬性是:
foo.[[Scope]] = [
globalContext.VO
];
在“foo”激活時(進入上下文),“foo”上下文的活動對象是:
fooContext.AO = {
y: 20,
bar: <reference to function>
};
“foo”上下文的作用域鏈為:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
內(nèi)部函數(shù)“bar”創(chuàng)建時,其[[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 此過程中閉包的形成
函數(shù)(調(diào)用時),會創(chuàng)建一個執(zhí)行環(huán)境及相應的作用域鏈,然后使用arguments和其他命名參數(shù)的值來初始化函數(shù)的活動對象(AO),但在作用域鏈中,外部函數(shù)的活動對象始終處于第二位,外部的外部處于第三位,以此類推。
執(zhí)行環(huán)境有一個表示變量的對象--變量對象(VO),全局環(huán)境的VO始終存在,函數(shù)這樣的局部環(huán)境的VO只在函數(shù)的執(zhí)行過程中存在。
內(nèi)部函數(shù)中將會將外部函數(shù)的活動對象添加到其作用域鏈中,當外部函數(shù)執(zhí)行完畢后,其執(zhí)行環(huán)境的作用域鏈會被銷毀,而活動對象會仍然保留在內(nèi)存中。
由于作用域鏈的形成機制,導致了某些函數(shù)在執(zhí)行完畢后,并不會銷毀其活動對象,因為其被包含在了其他函數(shù)的作用域鏈中。
2. 閉包
可以以正常數(shù)據(jù)形式存在的函數(shù)(比方說:當參數(shù)傳遞,接受函數(shù)式參數(shù)或者以函數(shù)值返回)都稱作 第一類函數(shù)(一般說第一類對象)。在ECMAScript中,所有的函數(shù)都是第一類對象。
在函數(shù)中,如果包含了自由變量(并未在函數(shù)內(nèi)部聲明),那么即產(chǎn)生了閉包。因為它涉及到了另外一個環(huán)境的數(shù)據(jù),那么會將另外一個環(huán)境中的VO暫存起來。
2.2 閉包實戰(zhàn)
2.2.1如下代碼輸出多少?如果想輸出3,那如何改造代碼?
var fnArr = [];
for (var i = 0; i < 10; i ++) {
fnArr[i] = function(){
return i
};
}
console.log( fnArr[3]() )
分析:代碼的本意是想,fnArr3中,是調(diào)用的第數(shù)組中的四個函數(shù),并且函數(shù)范圍值與數(shù)組下標是一樣的。這里會直接輸出10,因為數(shù)組中函數(shù)僅僅是聲明了,I并未賦值,等到調(diào)用時,其作用域中并不包含I,就向上級尋找I,而此時所有的函數(shù)都是共享一個父作用域,I的值為10.
如果要輸出3,就是讓每一次為數(shù)組賦值時,能夠讓函數(shù)能夠記住此時i的值,并在調(diào)用的時候能夠取到。
所以我們將循環(huán)更改一下:
for (var i = 0; i < 10; i ++) {
(function(j){
fnArr[j] = function(){
return j
};
})(i);
}
這里我們將賦值過程用一個立即執(zhí)行函數(shù)封裝了起來,并且通過參數(shù)傳值,將i的值保存在了外層立即執(zhí)行函數(shù)的執(zhí)行上下文中。
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
這里用一個立即執(zhí)行函數(shù),并返回我們需求的對象的形式來封裝。這樣的好處是所有的數(shù)據(jù)都被封裝在了立即執(zhí)行函數(shù)中,不會暴露在外面。
2.2.3 如下代碼輸出多少?如何連續(xù)輸出 0,1,2,3,4
for(var i=0; i<5; i++){
setTimeout(function(){
console.log('delayer:' + i )
}, 0)
}
分析:setTimeout外層創(chuàng)建一個閉包
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都是返回了一個函數(shù)。且couter
與couter2所返回的函數(shù)是同名的。但是每次調(diào)用時,進入執(zhí)行上下文,然后綁定了新的作用域鏈了。
2.2.5 補全代碼,實現(xiàn)數(shù)組按姓名、年紀、任意字段排序
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 函數(shù),實現(xiàn)如下調(diào)用方式
console.log( sum(1)(2) ) // 3
console.log( sum(5)(-1) ) // 4
解答:
function sum(val1){
return function(val2){
return val1+val2;
}
}