讀完《你不知道的js》已經(jīng)過去很久了,正好順著春招的氣息,整理一波,復(fù)習(xí)一下JS基礎(chǔ),不得不說這本書刷新了我對JS得認(rèn)知,看完紅色的那本JS經(jīng)典之后再來看這本你不知道的js簡直就是爽爆了,如果能認(rèn)認(rèn)真真得啃完這兩本書,理解深層次得js基礎(chǔ),那么js水準(zhǔn)雖說距離阮一峰老師還有很大一截但至少能在中上游站穩(wěn)腳跟。春招加油!!!努力進(jìn)字節(jié)!!!
作用域
理解作用域首先來理解三個(gè)概念
引擎:從頭到尾負(fù)責(zé)整個(gè)JS程序得編譯及執(zhí)行過程
編譯器:負(fù)責(zé)語法分析及代碼生成等臟活累活
作用域:負(fù)責(zé)收集并維護(hù)由所有聲明得標(biāo)識符(變量)組成得一系列查詢,并實(shí)施一套非嚴(yán)格得規(guī)則,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符得訪問權(quán)限。
知道了這三個(gè)概念之后,我們來理解一下var a = 2
這句代碼的含義。這句代碼在js底層會分為一下幾步
遇到
var a
,編譯器會詢問作用域是否已經(jīng)有一個(gè)該名稱的變量存在于同一個(gè)作用域得集合中,若有,編譯器會跳過這條聲明,繼續(xù)編譯。若沒有,編譯器會要求作用域在當(dāng)前作用域得集合中生命一個(gè)變量并將其命名為a
接下來編譯器會為引擎生成運(yùn)行時(shí)所需要的代碼,來處理
a = 2
這條賦值語句,引擎會詢問當(dāng)前作用域,是否存在一個(gè)變量a
,若有,引擎會使用這個(gè)變量,若沒有,引擎會繼續(xù)查找該變量(也就是后面說到的作用域連)如果找到了,那么將
2
賦值給該變量,若沒有,引擎將會拋出一個(gè)異常
LHS RHS 查詢
上面說到引擎會去查找使用存在變量 a
,這個(gè)查詢稱為LHS查詢,另一種查詢稱為RHS查詢
從字面意思上可以大致猜出一個(gè)是‘左’查詢,一個(gè)是‘右’查詢,簡單的理解就是當(dāng)變量出現(xiàn)在賦值操作得左側(cè)的時(shí)候就是LHS
,反之,出現(xiàn)在右側(cè)得時(shí)候就是RHS
,這兩種查詢方式很好理解。
在通俗一點(diǎn)得講就是,RHS查詢是查找變量對應(yīng)的值,而LHS查詢是查找變量的容器本身,從而可以對其賦值。從這個(gè)角度講,RHS并不是真正意義上的“賦值操作得右側(cè)”,更準(zhǔn)確得講是“非左側(cè)”
function foo(a) {
console.log(a);
}
foo(a);
觀察上方一段代碼,一個(gè)簡單的打印形參a
得函數(shù)。我們來找一下其中的變量查詢方式。
- 第 1 行,當(dāng)調(diào)用
foo
函數(shù)得時(shí)候,會發(fā)生一個(gè)隱士得賦值操作就是a = 2
,將傳遞得實(shí)參傳遞給形參,這邊是對a
得LHS查詢 - 第 2 行,調(diào)用
console
對象重的log
方法,是對console
變量得RHS查詢 - 第 2 行,打印
a
變量,是對a
變量得RHS查詢 - 第 4 行,調(diào)用了
foo
函數(shù),是對foo
變量的RHS查詢
作用域嵌套
當(dāng)一個(gè)塊或者一個(gè)函數(shù)嵌套在另一個(gè)塊或者另一個(gè)函數(shù)時(shí),就發(fā)生了作用域嵌套。
在es5之前,只有全局作用域和函數(shù)作用域
es6 出來之后,let const 語句產(chǎn)生了塊作用域
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(2); // 4
這段代碼中,foo
函數(shù)內(nèi)部對變量b
做了一個(gè)RHS查詢,但是foo
函數(shù)內(nèi)部并沒有變量b
得聲明,于是引擎會向上一級作用域中查找,當(dāng)前代碼中也就是全局作用域,找到了var b = 2
,完成了對變量b
得查詢。
像這樣一級一級得嵌套就產(chǎn)生了作用域鏈的概念。
進(jìn)行RHS查詢時(shí),引擎會在當(dāng)前作用域中查找,若在當(dāng)前作用域中沒有找到該變量,就會向上一級作用中進(jìn)行查找,一直到最外層的全局作用域,若還是沒有找到所需變量,引擎會拋出
ReferenceError
而LHS查詢,未找到變量的話,就會在全局作用域中隱式的創(chuàng)建一個(gè)具有該名稱的變量,但是在ES5中引入了嚴(yán)格模式,嚴(yán)格模式下有很多不同的行為,其中一個(gè)行為就是不允許自動或隱式的創(chuàng)建全局變量
詞法作用域
作用域有兩種工作模型,第一種是最為普遍的,被大多數(shù)編程語言采用的詞法作用域,我們的JS也是采用的這種工作模型,另一種稱為動態(tài)作用域,比如說Bash腳本,Perl中的一些模式等。后面提到的this的工作機(jī)制有一點(diǎn)像動態(tài)作用域但也只是有一點(diǎn)像而已,JS里是沒有動態(tài)作用域的只有詞法作用域。
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2, 4, 12
來分析一下這一段代碼
全局作用域下包含一個(gè)標(biāo)識符
foo
foo
函數(shù)作用域下包含三個(gè)標(biāo)識符a
b
bar
bar
函數(shù)作用域下包含一個(gè)標(biāo)識符c
當(dāng)函數(shù)bar
在對b
進(jìn)行RHS查詢的時(shí)候,返現(xiàn)bar
函數(shù)作用域內(nèi)沒有標(biāo)識符b
,那么引擎就會向上一級作用域也就是foo
函數(shù)作用域,找到其中的標(biāo)識符b
。
作用域查找會在找到第一個(gè)匹配的標(biāo)識符時(shí)停止。在多層的嵌套作用域中可以定義同名的標(biāo)識符,這叫做 遮蔽效應(yīng)(內(nèi)部的標(biāo)識符遮蔽了外部的標(biāo)識符),作用域查找始終從運(yùn)行時(shí)所處的最內(nèi)部的作用域開始,逐漸向外或者向上進(jìn)行,直到遇見第一個(gè)匹配的標(biāo)識符停止。
無論函數(shù)在哪被調(diào)用,也無論函數(shù)如何被調(diào)用,他的詞法作用域都只由函數(shù)被聲明時(shí)所處的位置決定(這是理解詞法作用域最重要的一點(diǎn),理解了這一點(diǎn)后面的閉包才能更好的理解)
考慮如下代碼:
function foo() {
console.log(a); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
foo();
這邊foo
函數(shù)最終打印出來的值是2
,而不是3
,那是因?yàn)檎{(diào)用foo
函數(shù)的時(shí)候,foo
函數(shù)內(nèi)部沒有變量a
,那么就會上一級作用域查找,而foo
函數(shù)是定義在全局作用域的,因此上一級作用域是全局作用域,拿到全局的變量a
。要謹(jǐn)記,js中只有詞法作用域,作用域只由函數(shù)聲明的位置來決定
eval with 欺騙詞法作用域
JS有兩種可以實(shí)現(xiàn)欺騙詞法作用域的方法,這兩種方法被各大開發(fā)者禁止使用,建議大家直接eslint禁止掉這兩個(gè)方法的使用,欺騙了詞法作用域會導(dǎo)致性能下降。謹(jǐn)記:不要使用這兩個(gè)方法
eval
考慮一下代碼
function foo(str, a) {
eval(str);
console.log(a, b);
}
var b = 2;
foo("var b = 3", 1) // 1, 3
這段代碼不會向預(yù)期的那樣拿到全局作用域下的b
,因?yàn)?code>eval方法,導(dǎo)致var b = 3
這段代碼就好像本來就在foo
函數(shù)里一樣,事實(shí)上,引擎的確會在foo
函數(shù)內(nèi)部創(chuàng)建一個(gè)標(biāo)識符b
,并賦值為 3
, 這樣就屏蔽了全局作用域,達(dá)到了欺騙詞法作用域的目的。
在嚴(yán)格模式下,eval
方法有其自己的詞法作用域,意味著其中的聲明無法影響外部的作用域
function foo(str) {
"use strict"
eval(str);
console.log(a); // ReferenceError: a is not defined
}
foo("var a = 3")
with
這個(gè)就不說了
函數(shù)作用域
函數(shù)作用域的定義是:屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)使用及復(fù)用(事實(shí)上在嵌套的作用域中可以使用)
首先來理解兩個(gè)概念
函數(shù)聲明:以
function
關(guān)鍵詞開始,形如function foo() {}
的被稱為函數(shù)聲明函數(shù)表達(dá)式:以
(function
開始,形如(function() {})
的被稱為函數(shù)表達(dá)式
區(qū)分這兩種的方法很簡單,就是看function
是否出現(xiàn)在整個(gè)聲明的第一個(gè)詞,如果是的話那就是函數(shù)聲明,若不是,那么就是函數(shù)表達(dá)式
下面來考慮這一段代碼
var a = 2;
function foo() {
var a = 3;
console.log(a); // 3
}
foo();
console.log(a); // 2
這段代碼使用foo
函數(shù)包裹了一段代碼段,避免了函數(shù)內(nèi)部的a
影響到外部的標(biāo)識符a
,但是foo
這個(gè)名稱本身“污染”了所在作用域(這個(gè)例子也就是全局作用域),理想狀態(tài)是函數(shù)不需要名稱這樣就不會影響所在作用域,并可以可以自動運(yùn)行,這是最理想的狀態(tài)
考慮這段代碼
var a = 2;
(function() {
var a = 3;
console.log(a); // 3
})();
console.log(a); // 2
使用函數(shù)表達(dá)式的形式,可以避免給函數(shù)起一個(gè)名字,同時(shí)直接調(diào)用,在代碼運(yùn)行的同時(shí)他就會立即調(diào)用,這種表達(dá)式成為IIFE(立即執(zhí)行表達(dá)式)
匿名/具名
具名很好理解就是一個(gè)函數(shù)有一個(gè)確切的名字,我們可以通過名字()
的形式來調(diào)用。
匿名出現(xiàn)最多的場景就是回調(diào)函數(shù)
考慮一下代碼
setTimeout(function() {
console.log('I waited 1 second');
}, 1000);
這叫做匿名函數(shù)表達(dá)式,因?yàn)?code>function() {...}沒有具體的名字。函數(shù)表達(dá)式可以匿名,但是函數(shù)聲明必須有一個(gè)具體的名字。
匿名函數(shù)在開發(fā)中被大量使用,但是它具有一下幾個(gè)小缺點(diǎn)
匿名函數(shù)在棧追蹤中不會顯示出來有意義的函數(shù)名,不方便調(diào)試
如果沒有函數(shù)名,當(dāng)函數(shù)需要引用自身的話,需要使用已經(jīng)過期的
arguments.callee
來使用,比如在遞歸中,再比如在事件監(jiān)聽器觸發(fā)后需要解綁自身(這是一個(gè)非常令人不舒服的地方,所以我一般都是直接修改window.onkeydown
類似屬性)匿名函數(shù)省略了具有一個(gè)描述性的函數(shù)名,對代碼的可讀性造成了些許的影響。
塊作用域
考慮一下代碼
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something(bar);
console.log(bar);
}
標(biāo)識符bar
僅在 if 聲明的上下文中使用,但是但使用 var
關(guān)鍵字聲明時(shí),它寫在哪里都一樣,因?yàn)樗紝儆谕獠孔饔糜颍@個(gè)現(xiàn)象也就是后面會說的變量提升
var a = 1;
if (a) {
var b = a * 2;
}
console.log(b); // 2
我們的理想狀態(tài)是標(biāo)識符b
只在if聲明的上下文中存在。
在看一個(gè)例子
for (var i = 0; i < 10; i++) {
console.log(i);
}
標(biāo)識符i
只在for
循環(huán)中使用,我們?yōu)槭裁匆屗廴玖宋覀冋麄€(gè)所在的作用域呢?
with
嚴(yán)格模式下,用width
從對象中創(chuàng)建的作用域僅在with
聲明中而非外部作用域中有效,這是塊作用域的一種形式。
try/catch
這是一個(gè)很有意思的事情,我也是在看這本書的時(shí)候才了解到,try/catch
語句中的catch
代碼塊會創(chuàng)建一個(gè)塊級作用域。
例子:
try {
undefined(); // 強(qiáng)行制造異常
} catch (error) {
console.log(error); // 正常打印
}
console.log(error); // ReferenceError: error is not defined
標(biāo)識符error
只存在與catch
語句內(nèi)部
let/const
在ES6出來之后,我們有了另外兩種聲明變量的關(guān)鍵字let const
,這是JS開發(fā)者的福音。
const
聲明的變量是常量,聲明時(shí)必須賦值,并且之后不可在修改
let
關(guān)鍵字可以將變量綁定到任意作用域,(通常是{...}
內(nèi)部),并且let const
聲明的變量不具有變量提升的特性
考慮以下代碼
if(true) {
let a = 1;
}
console.log(a); // ReferenceError: a is not defined
很明顯a
只存在于if
上下文的內(nèi)部。
考慮一下代碼
if(true) {
{
// 塊及作用域 - 作用域A
let a = 1;
}
console.log(a); // ReferenceError: a is not defined
}
這段代碼很好的解釋了,let
聲明的變量被綁定到了if內(nèi)部的作用域A內(nèi)部了,通常被綁定到{...}
內(nèi)部,因?yàn)樵谧饔糜駻外部無法拿到標(biāo)識符a
同樣的for循環(huán)
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // ReferenceError: i is not defined
我們通過babel
來看一下轉(zhuǎn)換之后的代碼
// 轉(zhuǎn)換前
if (true) {
let a = 1;
console.log(a);
}
console.log(a);
// 轉(zhuǎn)換后
"use strict";
if (true) {
var _a = 1;
console.log(_a);
}
console.log(a);
猜測babel監(jiān)測到了{}
同時(shí)內(nèi)部使用了let
關(guān)鍵字,然后就將使用了let
關(guān)鍵子換了個(gè)名字
變量提升
變量提升在這本書中僅僅講了三頁紙不到,這是一個(gè)很簡單的問題,我把它理解成一個(gè)現(xiàn)象,雖說簡單,但是在開發(fā)過程中很多剛接觸JS新手經(jīng)常會發(fā)生這樣一種疑問“為什么能拿到,為什么拿到的是undefined”
直覺上會認(rèn)為JS代碼執(zhí)行時(shí)是由上到下一行一行運(yùn)行的,但實(shí)際上并不完全正確,有一種特殊情況會導(dǎo)致這個(gè)假設(shè)是錯(cuò)誤的。
考慮以下代碼
console.log(a); // undefined
var a = 1;
產(chǎn)生這一現(xiàn)象的原因: 編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來
因此,變量提升的含義可以理解為:包括變量和函數(shù)在內(nèi)的所有聲明都會在任何代碼被執(zhí)行前首先被處理。
當(dāng)你看到var a = 2
的時(shí)候,編譯器會將這一條語句理解為兩段代碼var a
和 a = 2
,第一段代碼會在編譯時(shí)運(yùn)行,而第二段代碼會留在原地等待執(zhí)行階段。
因此,我們上方的代碼實(shí)例可以轉(zhuǎn)換為:
var a;
console.log(a);
a = 1;
考慮一下代碼
foo(); // is raise
function foo() {
console.log('is raise');
}
從這個(gè)例子我們可以看出來,不僅是var
聲明的變量會被提升,函數(shù)聲明也會被提升,并且函數(shù)提升時(shí),會帶著其整個(gè)函數(shù)一起提升,以上代碼可以理解為:
function foo() {
console.log('is raise');
}
foo(); // is raise
考慮以下代碼:
foo(); // TypeError
var foo = function() {
console.log('is raise');
}
變量標(biāo)識符foo
會被提升,但是賦值操作還留在原地等待執(zhí)行階段。上方代碼可以理解為:
var foo;
foo(); // TypeError
foo = function() {
console.log('is raise');
}
可以看出函數(shù)聲明會被提升,而函數(shù)表達(dá)式卻不會被提升。
函數(shù)聲明和var
聲明的變量都會提升,那么提升的優(yōu)先級呢,順序不一樣將會導(dǎo)致不一樣的代碼。
結(jié)論是函數(shù)提升優(yōu)先于變量提升
函數(shù)提升優(yōu)先于變量提升
考慮以下代碼:
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function() {
console.log(2);
}
以上代碼可以理解為:
function foo() {
console.log(1);
}
foo(); // 1
foo = function() {
console.log(2);
}
從上面的實(shí)例可以看出,函數(shù)聲明的提升時(shí)優(yōu)先于變量提升的,而且 var foo
這條語句會被引擎給省略掉,因?yàn)樵诋?dāng)前作用域中,已經(jīng)有一個(gè)名稱為foo
的標(biāo)識符了,重復(fù)聲明會被引擎給跳過。這一點(diǎn)在第一節(jié)說到了。
那么如果多個(gè)重名的函數(shù)聲明呢,提升的優(yōu)先級呢?
考慮以下代碼:
foo(); // 3
function foo() {
console.log(1);
}
var foo = function() {
console.log(2);
}
function foo() {
console.log(3);
}
以上代碼可以理解為:
function foo() {
console.log(1);
}
function foo() {
console.log(3);
}
foo(); // 3
可以看出,多個(gè)重名的函數(shù)聲明,會按照書寫的順序進(jìn)行提升,后者可以覆蓋前者,因此這里會打印出 3
閉包
閉包在開發(fā)過程中很常見,你只需要知道去認(rèn)識它,并知道你寫的這一段代碼就是閉包,而不應(yīng)該去為了閉包而閉包
閉包是基于詞法作用域書寫代碼所產(chǎn)生的自然結(jié)果。
閉包的概念可以定義為:函數(shù)在當(dāng)前詞法作用域之外被調(diào)用。
首先我們來看一下經(jīng)典閉包代碼:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();
函數(shù)foo
內(nèi)部有一個(gè)函數(shù)bar
并將整個(gè)函數(shù)返回出來,然后調(diào)用這個(gè)返回值,這是這段代碼的字面意思。事實(shí)上標(biāo)識符baz
還是指向的bar
函數(shù),但是整個(gè)函數(shù)卻在foo
函數(shù)外部被調(diào)用,也就是說bar
函數(shù)在自己的詞法作用域之外被調(diào)用。這一段代碼是一段經(jīng)典閉包代碼。
當(dāng)
foo
函數(shù)被執(zhí)行之后,通常會期待foo
函數(shù)內(nèi)部整個(gè)作用域都被銷毀,因?yàn)镴S有一個(gè)垃圾回收機(jī)制,會釋放不再使用的內(nèi)存,但是在這邊,閉包打斷了整個(gè)回收,因?yàn)?code>bar函數(shù)本身仍在使用這個(gè)作用域,bar
函數(shù)依然持有該作用域的引用,這個(gè)引用就叫做閉包。
考慮下面這段代碼,你會對閉包有著更加深刻的認(rèn)知。
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
fn() // 這就是閉包
}
foo();
函數(shù)baz
被當(dāng)作參數(shù)傳遞給函數(shù)bar
的行參fn
,之后調(diào)這個(gè)fn
,實(shí)質(zhì)就是調(diào)用整個(gè)baz
,調(diào)用位置不再函數(shù)baz
的詞法作用域,而是在bar
函數(shù)的內(nèi)部調(diào)用,這就是閉包。
無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執(zhí)行這個(gè)函數(shù)都會使用閉包。
考慮以下代碼
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000)
}
wait('hello 閉包');
這是一段大家都很熟悉的代碼,函數(shù)timer
是setTimeout
的回調(diào),但是timer
函數(shù)內(nèi)部使用了外部的變量message
,涵蓋了wait
函數(shù)的作用域,當(dāng)wait
調(diào)用時(shí),其內(nèi)部的作用域并不會被銷毀,因?yàn)檫^一秒后執(zhí)行的回調(diào)timer
依然保留著wait
的作用域。
循環(huán) + 閉包
我們來看一段經(jīng)典的代碼
for (var i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
理想狀態(tài)下是每隔一秒打印出對應(yīng)的數(shù)字,但是實(shí)際情況每隔一秒打印一個(gè) 5
我們來分析一下產(chǎn)生這個(gè)問題的原因,
首先我們知道
setTimeout
定時(shí)器是一個(gè)異步任務(wù),會在所用的同步任務(wù)執(zhí)行完成之后再去執(zhí)行異步任務(wù),這是JS的EventLoop事件循環(huán)機(jī)制(其中還細(xì)分微任務(wù),宏任務(wù)兩個(gè)概念)其次我們發(fā)現(xiàn),內(nèi)部的回調(diào)函數(shù)
timer
是一個(gè)閉包,它仍舊引用著外部的變量i
然后我們回過頭來發(fā)現(xiàn),當(dāng)for循環(huán)整個(gè)同步執(zhí)行完成之后,變量
i
會自增到5,從而退出循環(huán),而這個(gè)時(shí)候,沒有同步任務(wù)了,就開始執(zhí)行異步任務(wù),異步任務(wù)時(shí)每隔一秒鐘打印一下這個(gè)i
,而現(xiàn)在變量i
就是5
,因此會產(chǎn)生這種現(xiàn)象。
那么定位到了問題在哪那就好辦了,我們可以給每個(gè)timer
函數(shù)一個(gè)變量i
的副本,也可以理解為一個(gè)瞬時(shí)值
。那么如何給它一個(gè)瞬時(shí)值呢,答案就是給他創(chuàng)建一個(gè)獨(dú)立的作用域,作用域內(nèi)部的i
會屏蔽掉for循環(huán)的游標(biāo)i
,這正是我們一開始說到的屏蔽效應(yīng)
因此我們的代碼改寫為:
for (var i = 0; i < 5; i++) {
(function moment(i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i);
}
首先還是來一下這個(gè)
timer
函數(shù),依舊是一個(gè)閉包,不過這次保留的不是for循環(huán)中的i
而是moment
函數(shù)的形參i
,而timer
保留的也是moment
函數(shù)內(nèi)部的作用域。然后我們來看一下
moment
函數(shù),這是一個(gè)IIFE
,接受一個(gè)形參i
,這個(gè)形參用來屏蔽循環(huán)中的i
,這樣就可以達(dá)到瞬時(shí)值的目的。
那么除了這種方法,還有一種更為簡單的就是使用ES6的let
關(guān)鍵字聲明變量i
,因?yàn)槲覀冎灰_(dá)到一個(gè)塊級作用域的目的就可以了。
代碼修改為:
for (let i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
模塊
模塊正是利用了閉包的強(qiáng)大威力。
考慮如下代碼:
function CalModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another);
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
var foo = CalModule();
foo.doSomething(); // cool
foo.doAnother(); // [1, 2, 3]
這個(gè)模式被稱為模塊
函數(shù)CalModule
返回一個(gè)對象,對象中包含了兩個(gè)方法,用于在其他的地方進(jìn)行調(diào)用,但是函數(shù)內(nèi)部的變量something another
不會被直接修改, 只能通過函數(shù)CalModule
暴露出來的方法來進(jìn)行修改數(shù)據(jù),這就是模塊的魅力,利用了閉包的強(qiáng)大威力。
模塊必須具有兩個(gè)條件:
必須有外部的封閉函數(shù),該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會產(chǎn)生一個(gè)新的模塊實(shí)例)
封閉函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態(tài)。
單例模式
每次調(diào)用封閉函數(shù)的時(shí)候都會返回一個(gè)新的模塊實(shí)例,那么當(dāng)我們只需要一個(gè)實(shí)例的時(shí)候,我們可以使用單例模式
var foo = (function CalModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another);
}
return {
doSomething: doSomething,
doAnother: doAnother
}
})();
foo.doSomething();
使用IIFE
表達(dá)式,得到一個(gè)模塊實(shí)例,這樣每次使用的都是同一個(gè)實(shí)例。
this
this
一直都是一個(gè)非常神奇的東西,在使用函數(shù)的時(shí)候經(jīng)常會用到this,我們首先得搞明白為什么需要this
,其次需要理解JS中this
的執(zhí)行機(jī)制是什么,只有了解了執(zhí)行機(jī)制我們就能靈活的運(yùn)用this。
this提供了一種優(yōu)雅的方式來隱式的傳遞執(zhí)行上下文,可以將API設(shè)計(jì)的更加簡潔并易于復(fù)用。
this是在運(yùn)行時(shí)進(jìn)行綁定的,并不是在編寫時(shí)綁定的,它的上下文取決于函數(shù)調(diào)用時(shí)的各種條件。this和函數(shù)的聲明沒有任何關(guān)系,只取決于函數(shù)的調(diào)用方式
默認(rèn)綁定
當(dāng)函數(shù)不加任何修飾直接調(diào)用時(shí),或者不匹配其他規(guī)則時(shí)都會引用默認(rèn)綁定規(guī)則,默認(rèn)綁定規(guī)則下,this 將指向全局,在瀏覽器環(huán)境中也就是window
但是在嚴(yán)格模式下,this 會綁定到 undefined
考慮如下代碼:
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
函數(shù)foo
不加任何修飾直接調(diào)用。
考慮如下代碼:
var a = 2;
var obj = {
foo: function() {
console.log(this.a);
}
};
var bar = obj.foo;
bar(); // 2
即使函數(shù)bar
是obj.foo
的一個(gè)引用,但是bar
函數(shù)是直接調(diào)用的,所以this指向全局。謹(jǐn)記:this 指向是運(yùn)行時(shí)確定的,和函數(shù)聲明的位置無關(guān),只與函數(shù)的調(diào)用方式有關(guān)
考慮如下代碼:
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn(); // 不加任何修飾直接調(diào)用,指向全局
}
var obj = {
a: 2,
foo: foo
}
var a = 'ops';
doFoo(obj.foo); // ops
隱式綁定
隱式綁定指的是函數(shù)有明確的調(diào)用者,這種綁定規(guī)則下,this 指向調(diào)用者。
考慮如下代碼:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
函數(shù)foo
并不是不加任何修飾的調(diào)用了,他的前面有了一個(gè)調(diào)用者obj
,這個(gè)時(shí)候函數(shù)的this指向是指向的obj
調(diào)用者了。
考慮如下代碼:
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj = {
a: 2,
obj2: obj2
};
obj.obj2.foo(); // 42
函數(shù)foo
的前面有了多個(gè)調(diào)用者,這個(gè)時(shí)候,foo函數(shù)只看最后一層調(diào)用,這邊也就是對象obj2
顯示的綁定
call/apply
使用call函數(shù)可以修改一個(gè)函數(shù)的this指向。
考慮如下代碼:
var a = 'window';
var obj = {
a: 'obj'
};
function foo() {
console.log(this.a);
}
foo.call(obj); // obj
foo函數(shù)在調(diào)用時(shí)通過call方法修改了this指向,這是顯示的綁定。與之類似的是apply
,兩者的第一個(gè)參數(shù)都是新的this指向,唯一的區(qū)別在于參數(shù)傳遞的方式不一樣
call傳遞的是一個(gè)參數(shù)列表
apply傳遞的是一個(gè)數(shù)組
考慮如下代碼:
var a = 'window';
var obj = {
a: 'call'
};
var obj2 = {
a: 'apply'
};
function foo(param1, param2) {
console.log(this.a, param1, param2);
}
foo.call(obj, 'param1', 'params2'); //call param1 params2
foo.apply(obj2, ['param1', 'params2']); // apply param1 params2
bind
bind綁定是call apply
綁定得一個(gè)變種,是一種顯示得強(qiáng)制綁定,被稱為“硬綁定”。bind函數(shù)返回一個(gè)新的函數(shù),這個(gè)新的函數(shù)不能再被call apply
等二次修改this指向。
使用方式如下:
var obj = {
a: 'bind'
};
var obj2 = {
a: 'second change'
};
function foo(param1, param2) {
console.log(this.a, arguments);
}
var newFoo = foo.bind(obj, 'pram1', 'param2');
newFoo.call(obj2); // bind ['param1', 'param2']
newFoo('param3', 'param4'); // bind ['param1', 'param2', 'param3', 'param4']
首先第 10 行,我們使用bind方法,返回一個(gè)新的函數(shù),第一個(gè)參數(shù)為新函數(shù)的this指向,之后會傳遞一個(gè)參數(shù)列表作為函數(shù)的參數(shù)。最終的
foo
函數(shù)的得到的參數(shù)是bind時(shí)傳遞的參數(shù)列表加上調(diào)用newFoo
新函數(shù)傳遞的參數(shù)組合起來得一個(gè)參數(shù)列表第 11 行,可以發(fā)現(xiàn),通過
bind
修飾后的新函數(shù)不能再被call apply
等二次修改 this 指向。
new 綁定
該綁定規(guī)則適用于 new
綁定關(guān)鍵字,該規(guī)則this指向new出來的那個(gè)實(shí)例。
當(dāng)我們執(zhí)行new
操作的時(shí)候,實(shí)際上發(fā)生了如下步驟:
創(chuàng)建一個(gè)全新的對象
這個(gè)新對象執(zhí)行[[Prototype]]綁定
這個(gè)新的對象將會綁定到函數(shù)調(diào)用得this
如果函數(shù)沒有返回對象,那么new表達(dá)式中的函數(shù)調(diào)用會自動返回這個(gè)新對象。
考慮如下代碼:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
優(yōu)先級
尤高到底分別為:
new 綁定
bind 綁定
call, apply綁定
隱式綁定
默認(rèn)綁定
當(dāng)你把 null 或者 undefined 作為 this 得綁定對象傳入 call, apply, bind,這些值在調(diào)用時(shí)會被忽略,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則
箭頭函數(shù)
箭頭函數(shù)是ES6新增一個(gè)函數(shù)寫法,寫法非常cool,也非常簡潔,至少我本人非常喜歡這個(gè)。唯一要注意的是箭頭函數(shù)得this指向不應(yīng)用上方介紹的任何一種規(guī)則,而是根據(jù)外層(函數(shù)或者全局)作用域來決定this,這個(gè)時(shí)候得看箭頭函數(shù)的詞法作用域,并且箭頭函數(shù)得this不可以經(jīng)過二次修改。
總結(jié)為以下幾點(diǎn):
箭頭函數(shù)的this是定義時(shí)確定的,繼承至父級的(函數(shù)或者全局)作用域
箭頭函數(shù)得this不可二次修改
function foo() {
return (a) => {
console.log(this.a);
}
}
var obj1 = {
a: 'obj1'
}
var obj2 = {
a: 'obj2'
}
var bar = foo.call(obj1);
bar.call(obj2); // obj1
函數(shù)foo
返回一個(gè)箭頭函數(shù),根據(jù)我們總結(jié)得第一點(diǎn),我們得箭頭函數(shù)中的this是父級作用域也就是foo
函數(shù)內(nèi)部,這個(gè)時(shí)候通過call
方法修改foo
函數(shù)得this,使其指向了obj1
,也就是說這個(gè)箭頭函數(shù)的this已經(jīng)定死了執(zhí)行obj1
(除非修改foo函數(shù)的this指向), 之后第 13 行再次使用call試圖修改箭頭函數(shù)的指向,結(jié)果證明并沒有成功,這也證實(shí)了我們的第二點(diǎn)。
類
JS是一門面向?qū)ο蟮恼Z言,而ES6帶來了
Class
語法,但是這并不意味著JS中實(shí)際上有類得,看到babel對類解析之后得代碼的話就知道,Class 最終還是會被轉(zhuǎn)換為普通的函數(shù),只不過這些函數(shù)得函數(shù)名首字母大寫了,我們稱之為“構(gòu)造函數(shù)”,但是沒有任何規(guī)范要求你構(gòu)造函數(shù)的首字母大寫。
Class 關(guān)鍵字可以使你創(chuàng)建一個(gè)類,然后這個(gè)類中使用
new
關(guān)鍵字來構(gòu)造一個(gè)實(shí)例,JS中得類同樣滿足封裝、繼承、多態(tài)。
但是每次
new
出一個(gè)實(shí)例的時(shí)候,這個(gè)實(shí)例都是一個(gè)單獨(dú)得實(shí)例,它不和其他實(shí)例有什么關(guān)聯(lián),唯一的聯(lián)系是它們都屬于同一個(gè)類,所以無法實(shí)現(xiàn)實(shí)例間得數(shù)據(jù)共享。而使用原生函數(shù)加上原型鏈我們可以輕松的實(shí)現(xiàn)數(shù)據(jù)共享且構(gòu)造簡單。
對象
JS中的object是以key-value形式存在的,那么這邊要介紹的點(diǎn)是對象上的兩種屬性描述符:數(shù)據(jù)描述符和訪問描述符
我們可以通過 Object.defineProperty
來給一個(gè)對象添加一個(gè) key 并且為這個(gè) key 設(shè)置屬性描述符。
考慮如下代碼:
var obj = {};
Object.defineProperty(obj, 'foo', {
configurable: true,
enumerable: true,
writable: true,
value: 'obj'
});
console.log(obj); // { foo: 'obj' }
方法接受三個(gè)參數(shù),第一個(gè)參數(shù)為要操作的對象,第二個(gè)參數(shù)為添加的 key 值,第三個(gè)參數(shù)為屬性描述符。
屬性描述符可能的參數(shù)如下:
configurable
:boolean,當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時(shí),該屬性描述符才能夠被改變,同時(shí)該屬性也能從對應(yīng)的對象上被刪除。默認(rèn)為 false。enumerable
:boolean,當(dāng)且僅當(dāng)該屬性的enumerable為true時(shí),該屬性才能夠出現(xiàn)在對象的枚舉屬性中。默認(rèn)為 false。
數(shù)據(jù)描述符同時(shí)具有以下可選鍵值:
writable
:boolean,當(dāng)且僅當(dāng)該屬性的writable為true時(shí),value才能被賦值運(yùn)算符改變。默認(rèn)為 false。value
:該屬性對應(yīng)的值。可以是任何有效的 JavaScript 值(數(shù)值,對象,函數(shù)等)。默認(rèn)為 undefined。
訪問描述符同時(shí)具有以下可選鍵值:
get
:一個(gè)給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。當(dāng)訪問該屬性時(shí),該方法會被執(zhí)行,方法執(zhí)行時(shí)沒有參數(shù)傳入,但是會傳入this對象(由于繼承關(guān)系,這里的this并不一定是定義該屬性的對象)。set
:一個(gè)給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。當(dāng)屬性值修改時(shí),觸發(fā)執(zhí)行該方法。該方法將接受唯一參數(shù),即該屬性新的參數(shù)值。
數(shù)據(jù)描述符和訪問描述符不可同時(shí)存在。
考慮如下代碼:
var obj = {};
Object.defineProperty(obj, 'foo', {
configurable: true,
enumerable: true,
set: function(val) {
this._foo_ = val;
},
get: function() {
return this._foo_
}
});
obj.foo = 'obj';
console.log(obj); // { foo: 'obj' }
這邊注意的是需要設(shè)置名為一個(gè)屬性,一般以_
開始,表示內(nèi)部元素,如果寫成this.foo = val
的話,那么會陷入死循環(huán),因?yàn)橘x值操作就是在調(diào)用set
方法,方法內(nèi)部還是在對該屬性賦值,所以會陷入死循環(huán),get
同理。
原型
JS中的對象有一個(gè)特殊的名為[[Prototype]]的內(nèi)置屬性,其實(shí)就是對其他對象的引用。
考慮如下代碼:
var obj = {
a: '1'
}
obj.a // 1
當(dāng)獲取obj.a
的時(shí)候,首先會檢測對象obj
中是否具有a
屬性,如果有的話會觸發(fā)該屬性的[[GET]]方法,若沒有的話,就需要用到原型鏈了,引擎會繼續(xù)在原型鏈上繼續(xù)查找是否具有a
這個(gè)key值。
原型鏈的盡頭
所有普通的原型鏈最終都會指向Object.prototype
,它包含了很多方法,比如說toString, valueOf, hasOwnProperty
屬性設(shè)置和屏蔽
這一點(diǎn)是大多數(shù)開發(fā)者經(jīng)常會犯的錯(cuò)誤,它是一個(gè)非常細(xì)小的操作,只是一個(gè)簡單的賦值操作但是產(chǎn)生的結(jié)果往往會導(dǎo)致代碼運(yùn)行失敗。
考慮如下代碼:
var obj = {
foo: 'obj'
}
var bar = Object.create(obj);
console.log(bar); // {}
console.log(bar.foo); // 1
bar.foo = 'bar'
console.log(bar); // { foo: 'bar' }
console.log(Object.getPrototypeOf(bar)); // { foo: 'obj' }
通過Object.create
方法將bar
的原型指向obj
,但是我們現(xiàn)在的bar
是一空對象,我們沒有設(shè)置任何的 key 值,所以直接打印這個(gè) bar 得到的是一個(gè)空對象。
當(dāng)我們試圖打印bar.foo
?的時(shí)候,引擎首先會檢查 bar 中是否具有這個(gè) ke y找不到的話會繼續(xù)查找其原型鏈,而原型鏈上是有一個(gè)名為 foo 的key值,他的 value 是 1, 所以打印1
當(dāng)我們試圖執(zhí)行bar.foo = 'bar'
的時(shí)候,我們首先看一下打印出來的結(jié)果 以及打印 bar 原型鏈的結(jié)果,結(jié)果表明這個(gè)foo
添加在了 bar 上,而不是我們預(yù)期的修改原型鏈上的 foo 屬性。這個(gè) foo 屬性變成了 bar 對象上的一個(gè)私有屬性,它屏蔽掉了原型鏈上的 foo 屬性。
事實(shí)上,如果 foo 不存在于 bar 中,而是存在于原型鏈上時(shí)執(zhí)行 bar.foo = 'something'
賦值操作時(shí),會發(fā)生三種情況:
如果原型鏈上存在 foo 屬性,并且沒有被標(biāo)記為只讀(
writable: false
),那么會直接在bar
對象上添加一個(gè)foo
屬性如果原型鏈上存在 foo 屬性,并且被標(biāo)記為只讀,那么無法修改已有屬性或者在 bar 上創(chuàng)建屏蔽屬性,如果運(yùn)行在嚴(yán)格模式下,代碼會拋出一個(gè)異常,否則,這條賦值語句會被忽略
如果原型鏈上存在并且是一個(gè) setter(訪問描述符),那么會調(diào)用這個(gè) setter 方法,foo 屬性不會添加到 bar 上,也不會重新定義 foo 這個(gè) setter 方法。
構(gòu)造函數(shù)
考慮如下代碼;
function Foo() {
//...
}
Foo.prototype.constructor === Foo // true
var a = new Foo();
a.constructor === Foo // true
當(dāng)我們執(zhí)行 new 操作的時(shí)候,有一個(gè)步驟是原型關(guān)聯(lián),將a.prototype
指向Foo.prototype
,而Foo.prototype
中有一個(gè)公有并且不可枚舉的屬性constructor
,這個(gè)屬性引用的是對象關(guān)聯(lián)的函數(shù),在這里也就是Foo
函數(shù)。
那么Foo函數(shù)就是我們所說的構(gòu)造函數(shù)
,事實(shí)上也只是一個(gè)普通的函數(shù),只不過首字母大寫了而已,為什么大寫呢,類名首字母大寫,所以。。。大寫。這顯然不是將它稱之為構(gòu)造函數(shù)的理由,唯一的理由是,這個(gè)函數(shù)被 new 操作符調(diào)用。
實(shí)際上,new 操作會劫持所有的普通函數(shù)并且構(gòu)造對象的形式來調(diào)用它。換句話說,在JS 中,對于“構(gòu)造函數(shù)”最準(zhǔn)確的解釋是,所有帶 new 的函數(shù)調(diào)用
函數(shù)不是構(gòu)造函數(shù),但是當(dāng)且僅當(dāng)使用new時(shí),函數(shù)調(diào)用會變成“構(gòu)造函數(shù)調(diào)用”
數(shù)據(jù)共享
之前說過 class 語法每次new 出來的實(shí)例都是一個(gè)新的實(shí)例,每個(gè)實(shí)例的數(shù)據(jù)并沒有實(shí)現(xiàn)共享。
考慮如下代碼:
class C {
constructor() {
this.count = 0;
}
}
var c1 = new C();
var c2 = new C();
c1.count++;
console.log(c1, c2); // { count: 1 } { count: 0 }
你會發(fā)現(xiàn)c1 c2都有一個(gè)獨(dú)立的count,那么如果向要實(shí)現(xiàn)一個(gè)共享的 count 就必須使用原型鏈,代碼修改為
class C {
constructor() {
C.prototype.count++;
}
}
C.prototype.count = 0;
var c1 = new C();
var c2 = new C();
console.log(c1.count, c2.count); // 2 2
但是這不是 class 的本意,class 就是不想把 prototype 暴露出來。所以我們可以使用Object.create
我已經(jīng)愛上了這個(gè)方法
var parent = {
count: 0,
addCount: function() {
parent.count++;
}
}
var child = Object.create(parent);
child.addCount();
var child2 = Object.create(parent);
console.log(child, child2);
唯一的缺點(diǎn)是我們需要創(chuàng)建一個(gè)對象出來。