1、RHS(Right-Hand-Side)查詢與LHS(Left-Hand-Side)查詢
“RHS 查詢與簡單地查找某個(gè)變量的值別無二致(a),而LHS 查詢則是試圖找到變量的容器本身,從而可以對(duì)其賦值(a=2)?!百x值操作的目標(biāo)是誰(LHS)”以及“誰是賦值操作的源頭(RHS)”
RHS查詢:相當(dāng)于查找某個(gè)變量的值(a),如果是RHS查詢,當(dāng)引擎發(fā)現(xiàn)沒有a變量的時(shí)候,會(huì)拋出ReferenceError異常。當(dāng)查詢到該變量時(shí),如果該查詢對(duì)變量進(jìn)行了非正確的操作(非函數(shù)類型的值進(jìn)行函數(shù)調(diào)用),就會(huì)拋出TypeError異常
LHS查詢,相當(dāng)于對(duì)某個(gè)變量進(jìn)行賦值(a=2),如果是LHS查詢,當(dāng)引擎發(fā)現(xiàn)沒有a變量時(shí),會(huì)在改作用域創(chuàng)建一個(gè)a變量(在非嚴(yán)格模式下)
2、變量查找
“作用域查找會(huì)在找到第一個(gè)匹配的標(biāo)識(shí)符時(shí)停止。在多層的嵌套作用域中可以定義同名的標(biāo)識(shí)符,這叫作“遮蔽效應(yīng)”(內(nèi)部的標(biāo)識(shí)符“遮蔽”了外部的標(biāo)識(shí)符)。拋開遮蔽效應(yīng),作用域查找始終從運(yùn)行時(shí)所處的最內(nèi)部作用域開始,逐級(jí)向外或者說向上進(jìn)行,直到遇見第一個(gè)匹配的標(biāo)識(shí)符為止。”
全局變量會(huì)成為全局對(duì)象的屬性,我們可以通過對(duì)全局對(duì)象的引用,來使用被遮蔽的全局變量。
3、欺騙詞法
通過兩種機(jī)制對(duì)詞法作用域進(jìn)行 “修改”。
1)、eval()
"eval(..) 函數(shù)可以接受一個(gè)字符串為參數(shù),并將其中的內(nèi)容視為好像在書寫時(shí)就存在于程序中這個(gè)位置的代碼"
function foo(str, a){
eval( str ); // 欺騙!--eval將var b=3寫在了該位置,在該作用域中聲明了一個(gè)新變量b
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
JavaScript 中還有其他一些功能效果和eval(..) 很相似。setTimeout(..) 和 setInterval(..) 的第一個(gè)參數(shù)可以是字符串,字符串的內(nèi)容可以被解釋為一段動(dòng)態(tài)生成的 函數(shù)代碼。
2)、with關(guān)鍵字
with 通常被當(dāng)作重復(fù)引用同一個(gè)對(duì)象中的多個(gè)屬性的快捷方式,可以不需要重復(fù)引用對(duì)象本身。
with 可以將一個(gè)沒有或有多個(gè)屬性的對(duì)象處理為一個(gè)完全隔離的詞法作用域,因此這個(gè)對(duì) 象的屬性也會(huì)被處理為定義在這個(gè)作用域中的詞法標(biāo)識(shí)符。
var str={a="kk",b="is",c="happpy"};
str.a="ss";//平常的引用,需要重復(fù)使用str來引用
with(str){a="ss";b="isn't";c="sad"}//快捷引用
function foo(obj) {
with (obj) {a = 2;}
}
var o1 = {a: 3};
var o2 = {b: 3};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
因?yàn)樵趏2對(duì)象中,并不存在屬性a,所以,不會(huì)創(chuàng)建a屬性,o2.a=undefined,但是因?yàn)檫M(jìn)行了LHS標(biāo)識(shí)符查找,所以就創(chuàng)建了一個(gè)全局變量a。
所以如果使用with對(duì)對(duì)象進(jìn)行屬性讀取的話,相當(dāng)于進(jìn)行了LHS標(biāo)識(shí)符查詢,當(dāng)全局都不存在變量a的情況下,會(huì)在全局的作用域創(chuàng)建一個(gè)變量a
欺騙詞法的方式對(duì)javaScript的性能會(huì)造成影響:
JavaScript 引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標(biāo)識(shí)符。
如果引擎在代碼中發(fā)現(xiàn)了eval(..) 或with,它只能簡單地假設(shè)關(guān)于標(biāo)識(shí)符位置的判斷 都是無效的,因?yàn)闊o法在詞法分析階段明確知道eval(..) 會(huì)接收到什么代碼,這些代碼會(huì) 如何對(duì)作用域進(jìn)行修改,也無法知道傳遞給with 用來創(chuàng)建新詞法作用域的對(duì)象的內(nèi)容到底 是什么。
如果代碼中大量使用eval(..) 或with,那么運(yùn)行起來一定會(huì)變得非常慢。無論引擎多聰 明,試圖將這些悲觀情況的副作用限制在最小范圍內(nèi),也無法避免如果沒有這些優(yōu)化,代 碼會(huì)運(yùn)行得更慢這個(gè)事實(shí)。
4、作用域
javaScript具有基于函數(shù)的作用域
通過作用域,我們可以進(jìn)行隱藏內(nèi)部的實(shí)現(xiàn)
可以把變量和函數(shù)包裹在一個(gè)函數(shù)的作用域中,然后用這個(gè)作用域 來“隱藏”它們。
可以規(guī)避沖突
可以避免同名標(biāo)識(shí)符之間的沖突, 兩個(gè)標(biāo)識(shí)符可能具有相同的名字但用途卻不一樣,無意間可能造成命名沖突。沖突會(huì)導(dǎo)致 變量的值被意外覆蓋。
function foo() {
function bar(a) {
i = 3; // 修改for 循環(huán)所屬作用域中的i
console.log( a + i );}
for (var i=0; i<10; i++) { bar( i * 2 ); // 糟糕,無限循環(huán)了! }
}
foo();
函數(shù)與函數(shù)表達(dá)式
區(qū)分函數(shù)聲明和表達(dá)式最簡單的方法是看function 關(guān)鍵字出現(xiàn)在聲明中的位 置(不僅僅是一行代碼,而是整個(gè)聲明中的位置)。如果function 是聲明中 的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式。
var a = 2;
(function foo(){ // <-- 添加這一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及這一行
console.log( a ); // 2
使用函數(shù)表達(dá)式,可以不需要顯示的聲明foo()后還有通過函數(shù)名調(diào)用這個(gè)函數(shù)才能使其運(yùn)行。此時(shí),foo 被綁定在函數(shù)表達(dá)式自身的函數(shù)中而不是所在作用域中。換句話說,(function foo(){ .. }) 作為函數(shù)表達(dá)式意味著foo 只能在.. 所代表的位置中被訪問,外部作用域則不行。foo 變量名被隱藏在自身中意味著不會(huì)非必要地污染外部作用域。
匿名與具名
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );
這叫作匿名函數(shù)表達(dá)式,因?yàn)閒unction().. 沒有名稱標(biāo)識(shí)符。函數(shù)表達(dá)式可以是匿名的, 而函數(shù)聲明則不可以省略函數(shù)名——在JavaScript 的語法中這是非法的。
IIFE,代表立即執(zhí)行函數(shù)表達(dá)式 (Immediately Invoked Function Expression),第一個(gè)()將函數(shù)變成表達(dá)式,第二個(gè)()執(zhí)行了這個(gè)函數(shù)。還有另一種形式:(function(){ .. }())。用于調(diào)用的()被移進(jìn)了用來包裝的()中
匿名函數(shù)表達(dá)式簡單快捷,但由于匿名,不容易調(diào)試,在需要引用自身時(shí),只能使用已經(jīng)過期的arguments.callee引用,可讀性差
塊作用域
{
//塊作用域
}
1、用with 從對(duì)象中創(chuàng)建出的作用域僅在with 聲明中而非外 部作用域中有效。
functionfoo(obj) {
with(obj) {var a=2;console.log(a);//2}
}
varo2 ={b:3};
foo(o2);
console.log(o2.a);// undefined
2、ES3 規(guī)范中規(guī)定try/catch 的catch 分句會(huì)創(chuàng)建一個(gè)塊作 用域,其中聲明的變量僅在catch 內(nèi)部有效。
3、ES6 中,引入了新的let 關(guān)鍵字,提供了除var 以外的另一種變量聲明方式。 let 關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是{ .. } 內(nèi)部)。換句話說,let 為其聲明的變量隱式地了所在的塊作用域。使用let 進(jìn)行的聲明不會(huì)在塊作用域中進(jìn)行提升(見5)。聲明的代碼被運(yùn)行之前,聲明并不 “存在”。
由于let 聲明附屬于一個(gè)新的作用域而不是當(dāng)前的函數(shù)作用域(也不屬于全局作用域), 當(dāng)代碼中存在對(duì)于函數(shù)作用域中var 聲明的隱式依賴時(shí),就會(huì)有很多隱藏的陷阱,如果用 let 來替代var 則需要在代碼重構(gòu)的過程中付出額外的精力。
4、ES6 還引入了const,同樣可以用來創(chuàng)建塊作用域變量,但其值是固定的 (常量)。之后任何試圖修改值的操作都會(huì)引起錯(cuò)誤
5、提升
提升是指聲明會(huì)被視為存在于其所出現(xiàn)的作用域的整個(gè)范圍內(nèi)
引擎在解釋js代碼前,會(huì)先進(jìn)行編譯,編譯中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺麄冴P(guān)聯(lián)起來。所以var a=2中,var a是在編譯階段進(jìn)行,a=2;會(huì)被留在執(zhí)行階段。
這個(gè)過程就好像變量和函數(shù)聲明從它們?cè)诖a中出現(xiàn)的位置被“移動(dòng)” 到了最上面。這個(gè)過程就叫作提升。先有蛋(聲明)后有雞(賦值)。
函數(shù)聲明會(huì)被提升,但是函數(shù)表達(dá)式卻不會(huì)被提升
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
foo(); // 不是ReferenceError, 而是TypeError!
var foo = function bar() { // ... };
函數(shù)聲明和變量聲明都會(huì)被提升。但是一個(gè)值得注意的細(xì)節(jié)(這個(gè)細(xì)節(jié)可以出現(xiàn)在有多個(gè) “重復(fù)”聲明的代碼中)是函數(shù)會(huì)首先被提升,然后才是變量。
總結(jié):引擎是先執(zhí)行 詞法作用域,所以聲明會(huì)先被運(yùn)行,其中函數(shù)聲明會(huì)優(yōu)先于變量聲明。但是let的聲明方式,是不能進(jìn)行提升的。
6、作用域閉包
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用 域之外執(zhí)行。
function foo() {
var a = 2;
function bar() { console.log( a ); }
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果。
bar()在自己的詞法作用域之外執(zhí)行。在foo()執(zhí)行之后,本該對(duì)其進(jìn)行回收(引擎的垃圾回收器來釋放不在使用的內(nèi)存空間)。但由于存在閉包,bar()本身在使用內(nèi)部作用域,所以該作用域可以一直存活。使得bar()可以在之后的任何時(shí)間進(jìn)行引用。
循環(huán)與閉包
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
},0 );
}//6 6 6 6 6 6
五個(gè)setTimeout都被封閉在一個(gè)共享的全局作用域中,所以只有一個(gè)i。
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer()
{ console.log( j );
}, j*1000 );
})( i );
}//1、2、3、4、5
在迭代內(nèi)使用IIFE 會(huì)為每個(gè)迭代都生成一個(gè)新的作用域,使得延遲函數(shù)的回調(diào)可以將新的 作用域封閉在每個(gè)迭代內(nèi)部,每個(gè)迭代中都會(huì)含有一個(gè)具有正確值的變量供我們?cè)L問。
使用IIFE來創(chuàng)建一個(gè)函數(shù)作用域,這樣,就會(huì)擁有五個(gè)相互獨(dú)立的作用域
for (let i=1; i<=5; i++)
{
setTimeout( function timer() {
console.log( i ); }, 0 );
}//1 2 3 4 5
for 循環(huán)頭部的let 聲明還會(huì)有一 個(gè)特殊的行為。這個(gè)行為指出變量在循環(huán)過程中不止被聲明一次,每次迭代都會(huì)聲明。隨 后的每個(gè)迭代都會(huì)使用上一個(gè)迭代結(jié)束時(shí)的值來初始化這個(gè)變量
通過let來建立塊級(jí)作用域,這樣,也會(huì)擁有五個(gè)相互獨(dú)立的作用域
7、模塊
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
CoolModule()是一個(gè)函數(shù),必須要通過調(diào)用它來創(chuàng)建一個(gè)模塊實(shí)例。它的返回值是{key:value}的形式,通過這個(gè)形式來表示對(duì)象。在這個(gè)函數(shù)中,返回的是內(nèi)部函數(shù),而不是內(nèi)部數(shù)據(jù)變量。所以變量是隱藏且私有的狀態(tài)。doSomething()和doAnother()函數(shù)具有涵蓋模塊實(shí)例內(nèi)部作用域的閉包。
模塊模式需要具備兩個(gè)必要條件:
1、必須有外部的封閉函數(shù),該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例)
2、封閉函數(shù)必須至少返回一個(gè)內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態(tài)。一個(gè)具有函數(shù)屬性的對(duì)象本身并不是真正的模塊。從方便觀察的角度看,一個(gè)從函數(shù)調(diào)用所返回的,只有數(shù)據(jù)屬性而沒有閉包函數(shù)的對(duì)象并不是真正的模塊。
var foo = (function CoolModule(id) {
function change() {
// 修改公共API
publicAPI.identify = identify2;
}
function identify1() {
console.log( id );
}
function identify2() {
console.log( id.toUpperCase() );
}
var publicAPI = {
change: change,
identify: identify1
};
return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
命名將要作為公共API返回的對(duì)象。通過在模塊實(shí)例的內(nèi)部保留對(duì)公共API對(duì)象的內(nèi)部引用,可以從內(nèi)部對(duì)模塊進(jìn)行修改,包括添加刪除方法、屬性,以及修改他們的值。
8、this
- 弄清楚this指向的意義,在于知道他所指向的作用域。
this是在運(yùn)行時(shí)被綁定的,并不是在編寫時(shí),他的上下文取決于函數(shù)調(diào)用時(shí)的各種條件。this的綁定和函數(shù)聲明的位置沒有關(guān)系,只取決于函數(shù)的調(diào)用方式。
/**使用call函數(shù),使得this指向foo函數(shù)**/
function foo(num) {
console.log( "foo: " + num );
// 記錄 foo 被調(diào)用的次數(shù)
// 注意, 在當(dāng)前的調(diào)用方式下(參見下方代碼), this 確實(shí)指向 foo
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以確保 this 指向函數(shù)對(duì)象 foo 本身
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調(diào)用了多少次?
console.log( foo.count ); // 4
調(diào)用位置
調(diào)用位置是指函數(shù)在代碼中被調(diào)用的位置。我們需要關(guān)注的是當(dāng)前正在執(zhí)行的函數(shù)的前一個(gè)調(diào)用中。
function baz() {
// 當(dāng)前調(diào)用棧是: baz
// 因此, 當(dāng)前調(diào)用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的調(diào)用位置
}
function bar() {
// 當(dāng)前調(diào)用棧是 baz -> bar
// 因此, 當(dāng)前調(diào)用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的調(diào)用位置
}
function foo() {
// 當(dāng)前調(diào)用棧是 baz -> bar -> foo
// 因此, 當(dāng)前調(diào)用位置在 bar 中
console.log( "foo" );
} b
az(); // <-- baz 的調(diào)用位置
調(diào)用的綁定規(guī)則
- 默認(rèn)綁定
獨(dú)立函數(shù)調(diào)用
this的默認(rèn)綁定:在代碼中,直接使用,不帶任何的修飾進(jìn)行調(diào)用。(在嚴(yán)格模式中,全局對(duì)象無法使用默認(rèn)綁定,this就會(huì)綁定到undefined)
/**this指向全局對(duì)象**/
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
- 隱式綁定
調(diào)用位置是否有上下文對(duì)象,是否被某個(gè)對(duì)象擁有或者包含。
/****/
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
首先需要注意的是 foo() 的聲明方式, 及其之后是如何被當(dāng)作引用屬性添加到 obj 中的。但是無論是直接在 obj 中定義還是先定義再添加為引用屬性, 這個(gè)函數(shù)嚴(yán)格來說都不屬于
obj 對(duì)象。然而, 調(diào)用位置會(huì)使用 obj 上下文來引用函數(shù), 因此你可以說函數(shù)被調(diào)用時(shí) obj 對(duì)象“擁有” 或者“包含” 它。
對(duì)象屬性引用鏈中只有最頂層或者說最后一層會(huì)影響調(diào)用位置。 舉例來說:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
- 隱式丟失
被隱式綁定的函數(shù)會(huì)丟失綁定對(duì)象,會(huì)應(yīng)用默認(rèn)綁定,從而把this綁定到全局或者undefined上,取決于是否是嚴(yán)格模式。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數(shù)別名!
var a = "oops, global"; // a 是全局對(duì)象的屬性
bar(); // "oops, global"
上面的例子,bar是obj.foo的一個(gè)引用,bar()是一個(gè)不帶任何修飾符的函數(shù)調(diào)用,所以是默認(rèn)綁定。
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其實(shí)引用的是 foo
fn(); // <-- 調(diào)用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對(duì)象的屬性
doFoo( obj.foo ); // "oops, global"
參數(shù)傳遞是一種隱式賦值。所以obj.foo會(huì)被賦值給fn()。所以又是默認(rèn)綁定。
- 顯式綁定
使用call(對(duì)象,參數(shù))、apply(對(duì)象,[參數(shù)])函數(shù)在某個(gè)對(duì)象里強(qiáng)制調(diào)用函數(shù)。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
顯式綁定仍然存在丟失綁定問題,可以采取硬綁定的方式解決
- 硬綁定
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬綁定的 bar 不可能再修改它的 this
bar.call( window ); // 2
- 創(chuàng)建一個(gè)可以重復(fù)使用的輔助函數(shù)
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
};
//輔助綁定函數(shù)
function bind(fn,obj){
return function () {
return fn.apply(obj,arguments)
}
}
var bar = bind(foo,obj)
var a = "oops, global";
bar();
- 在ES5中,可以采用function.prototyper.bind
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
};
var bar = foo.bind(obj)
var a = "oops, global";
bar();
以上三種都是硬綁定的方式,還可以采用API調(diào)用的“上下文”
第三方庫的許多函數(shù), 以及 JavaScript 語言和宿主環(huán)境中許多新的內(nèi)置函數(shù), 都提供了一個(gè)可選的參數(shù), 通常被稱為“上下文”(context), 其作用和 bind(..) 一樣, 確保你的回調(diào)函數(shù)使用指定的 this。這些函數(shù)實(shí)際上就是通過 call(..) 或者 apply(..) 實(shí)現(xiàn)了顯式綁定, 這樣你可以少些一些代碼。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
};
[1].forEach(foo,obj)
- new綁定
構(gòu)造函數(shù)只是一些使用 new 操作符時(shí)被調(diào)用的函數(shù)。 它們并不會(huì)屬于某個(gè)類, 也不會(huì)實(shí)例化一個(gè)類。 實(shí)際上,它們甚至都不能說是一種特殊的函數(shù)類型, 它們只是被 new 操作符調(diào)用的普通函數(shù)而已。所以, 包括內(nèi)置對(duì)象函數(shù)(比如 Number(..), 詳情請(qǐng)查看第 3 章) 在內(nèi)的所有函數(shù)都可以用 new 來調(diào)用, 這種函數(shù)調(diào)用被稱為構(gòu)造函數(shù)調(diào)用。
使用new進(jìn)行函數(shù)調(diào)用的時(shí)候,會(huì)發(fā)生以下操作
- 創(chuàng)建(或者說構(gòu)造) 一個(gè)全新的對(duì)象。
- 這個(gè)新對(duì)象會(huì)被執(zhí)行 [[ 原型 ]] 連接。
- 這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的 this。
- 如果函數(shù)沒有返回其他對(duì)象, 那么 new 表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
優(yōu)先級(jí)
- 顯示綁定>隱式綁定
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
- new 綁定>隱式綁定
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4