《你不知道的JS》上篇整理

讀完《你不知道的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底層會分為一下幾步

  1. 遇到var a,編譯器會詢問作用域是否已經(jīng)有一個(gè)該名稱的變量存在于同一個(gè)作用域得集合中,若有,編譯器會跳過這條聲明,繼續(xù)編譯。若沒有,編譯器會要求作用域在當(dāng)前作用域得集合中生命一個(gè)變量并將其命名為 a

  2. 接下來編譯器會為引擎生成運(yùn)行時(shí)所需要的代碼,來處理 a = 2 這條賦值語句,引擎會詢問當(dāng)前作用域,是否存在一個(gè)變量 a,若有,引擎會使用這個(gè)變量,若沒有,引擎會繼續(xù)查找該變量(也就是后面說到的作用域連)

  3. 如果找到了,那么將 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 aa = 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ù)timersetTimeout的回調(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è)問題的原因,

  1. 首先我們知道setTimeout定時(shí)器是一個(gè)異步任務(wù),會在所用的同步任務(wù)執(zhí)行完成之后再去執(zhí)行異步任務(wù),這是JS的EventLoop事件循環(huán)機(jī)制(其中還細(xì)分微任務(wù),宏任務(wù)兩個(gè)概念)

  2. 其次我們發(fā)現(xiàn),內(nèi)部的回調(diào)函數(shù)timer是一個(gè)閉包,它仍舊引用著外部的變量i

  3. 然后我們回過頭來發(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);
}
  1. 首先還是來一下這個(gè)timer函數(shù),依舊是一個(gè)閉包,不過這次保留的不是for循環(huán)中的i而是moment函數(shù)的形參i,而timer保留的也是moment函數(shù)內(nèi)部的作用域。

  2. 然后我們來看一下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ù)barobj.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ā)生了如下步驟:

  1. 創(chuàng)建一個(gè)全新的對象

  2. 這個(gè)新對象執(zhí)行[[Prototype]]綁定

  3. 這個(gè)新的對象將會綁定到函數(shù)調(diào)用得this

  4. 如果函數(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)先級

尤高到底分別為:

  1. new 綁定

  2. bind 綁定

  3. call, apply綁定

  4. 隱式綁定

  5. 默認(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è)對象出來。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內(nèi)容