Javascript 雜談 :熟悉基本概念
1. 信息、變量、數(shù)據(jù)類型和變量對象
ECMAScript 對于變量的定義及語法,借鑒了眾多語言的特性,同時(shí)也形成了自己獨(dú)特的運(yùn)作機(jī)制。松散的變量和簡單的語法讓編程過程更加愜意自由。
1.1. 看不見的信息
眾所周知,計(jì)算機(jī)是用來處理信息的工具,而程序就是處理信息的步驟。但是信息是看不見摸不到的,如何進(jìn)行操作?我們首先使用符號來承載它,也就是經(jīng)常提到的數(shù)據(jù),但是這樣好像也不行,例如在黑漆漆的房間里我畫下了若干符號,你依然無法感知啊。所以,要有光,利用信號讓你感知數(shù)據(jù)和信息。比如這篇文章在我的腦中,你是無法感知的,我用漢語(符號)將它寫出,然后通過光傳到你的眼睛里;你的大腦將光信號再翻譯回漢語(符號),然后分析它的信息。
好了,請描述一下 “20 ” 這個(gè)信息。什么我已經(jīng)描述了,你看到的 “20” 就是這個(gè)信息的阿拉伯?dāng)?shù)字符號表示。那么你是怎么看到這個(gè)符號的?真正的過程可能比較復(fù)雜:
- 我在電腦上輸入 “20” (符號),通過 ASCII 轉(zhuǎn)化成了 0、1格式(另一種符號)。
- 我的網(wǎng)卡將 0、1 轉(zhuǎn)化成電平(信號),通過網(wǎng)線傳到你的網(wǎng)卡。
- 你的網(wǎng)卡重新將電平翻譯回 0、1格式,然后根據(jù) ASCII 轉(zhuǎn)化為你屏幕上的 “20” 。
在上述過程中是否可以保存信號來間接的存儲數(shù)據(jù)和信息?當(dāng)然可以存儲設(shè)備都是這樣工作的,晶體管的穩(wěn)態(tài)表示 1,光盤的凹凸坑對應(yīng)了 0 、1 。我們把 “20” 存在內(nèi)存中,終于把信息從看不見摸不到的狀態(tài)變成了實(shí)際存在且能保存的物理信號。那么程序是如何讀取內(nèi)存中的 “20” 的?
1.2. 客觀的數(shù)據(jù)
用哲學(xué)的概念來引述,存儲在內(nèi)存中的 “20”,它是具體存在的,是客觀的。而將它映射到程序中的就是變量,它是邏輯的(主觀的)。

1.3. 變量只是一個(gè)名字
變量在 ECMAScript 中是所謂的標(biāo)識符,也就是一個(gè)名稱或名字。它的作用只是用來指示某個(gè)物體或?qū)ο螅?strong>變量沒有數(shù)據(jù)。比如每個(gè)人都有名字,但是名字里含有你的數(shù)據(jù)嗎?名字只是用來辨識人,同理變量只是用來分辨具體數(shù)據(jù)或?qū)ο蟮摹?/p>
你可能會問 var a = 20 中的 “20” 不就是一個(gè)標(biāo)識符嗎?這條語句的具體步驟應(yīng)該是:
- 人類輸入熟悉的標(biāo)識符 “20”
- 編譯器將 “20” 編譯成相應(yīng)類型的數(shù)據(jù)存儲在內(nèi)存中
- 使用變量 a 來映射內(nèi)存中的數(shù)據(jù)
也就是說這里的 a 和 20 都是內(nèi)存中數(shù)據(jù)的標(biāo)識符,20 是人類熟悉的標(biāo)識符,通常出現(xiàn)在初始化過程,a 則是程序使用的標(biāo)識符,并且因?yàn)槭沁壿嫷?,所以可以變動,這也是稱為變量的原因。當(dāng)然在 ECMAScript 中除了 20 這樣的數(shù)字,還定義多種數(shù)據(jù)類型。
1.4. 類型是數(shù)據(jù)的結(jié)構(gòu)
在人類世界中國際標(biāo)準(zhǔn)的基本單位有 7 個(gè):長度m,時(shí)間s,質(zhì)量kg,熱力學(xué)溫度(Kelvin溫度)K,電流單位A,光強(qiáng)度單位cd(坎德拉),物質(zhì)的量mol。這些基本單位可以推出物理世界的所有物理量。而在 ECMAScript 中定義了 6 種數(shù)據(jù)的類型,并且不支持自定義類型,因此所有值都是這 6 中數(shù)據(jù)類型之一。它們又分為兩類,第一類稱為基本數(shù)據(jù)類型,包含:Undefined, Null, Boolean, Number, String。另外一類稱為復(fù)雜數(shù)據(jù)類型 Object ,它是由多個(gè)無序的鍵值對組成。
根據(jù)數(shù)據(jù)類型的分類, ECMAScript 中的變量可能映射兩種不同的類型值,基本數(shù)據(jù)類型值和引用類型值?;緮?shù)據(jù)類型值就是該類型的數(shù)據(jù),引用類型值則是該類型對象的引用,它們都和變量存儲在內(nèi)存??臻g,而引用類型的對象存儲在內(nèi)存的堆空間。

大體上可以認(rèn)為引用類型就是繼承自 Object 復(fù)雜數(shù)據(jù)類型的一種數(shù)據(jù)結(jié)構(gòu),它包含了數(shù)據(jù)和與數(shù)據(jù)相關(guān)的操作。常用的有 Function, Array, Date, RegExp ,而這些類型數(shù)據(jù)在聲明時(shí)就已經(jīng)轉(zhuǎn)化成對象存在內(nèi)存中,只有在沒有引用情況下會被垃圾收集。除此之外還有一種特殊的引用類型,就是基本包裝類型,在說明基本包裝類型前先了解一下什么是數(shù)據(jù)類型,《數(shù)據(jù)結(jié)構(gòu)》中定義了 “數(shù)據(jù)類型 = 數(shù)據(jù)元素 + 關(guān)系集 + 關(guān)系集的基本操作” ,也就是說數(shù)據(jù)類型不僅僅包含數(shù)據(jù)還有和數(shù)據(jù)相關(guān)的操作。
// 程序中聲明一個(gè) ```string``` 類型數(shù)據(jù)元素,但是注意并沒有定義相關(guān)的操作
var str = 'some text'
var listItem = [1, 2, 3]
// 而在這里使用了```substring``` 方法,
// 也就是說 ECMAScript 將 'some text' 這個(gè)數(shù)據(jù)元素自動轉(zhuǎn)化成某種數(shù)據(jù)類型的數(shù)據(jù)存儲在內(nèi)存中,隨后釋放。
var sub_str = str.substring(2) // 'me text'
var subList = listItem.slice(2) // [3]
在語句 var str = 'some text' 中變量 str 映射的是一個(gè)基本數(shù)據(jù)類型的值,注意僅僅是數(shù)值,但是為什么可以調(diào)用 substring() 方法?這就是基本包裝類型的功能,基本包裝類型同樣繼承自 Object ,但是數(shù)據(jù)元素都定義為 Number 這種基本數(shù)據(jù)類型,同時(shí)給出一些相關(guān)的基本操作,它包含有 Number, String, Boolean 。是的和基本類型的樣子一模一樣,這樣的黑箱效應(yīng)讓程序員無須留意轉(zhuǎn)化過程的。程序員看到的是 'some text',而程序在調(diào)用它時(shí)轉(zhuǎn)化成一種 String 基本包裝類型的對象實(shí)例,但是基本包裝類型特殊處就是只在調(diào)用時(shí)轉(zhuǎn)化,調(diào)用結(jié)束后釋放。
// var sub_str = str.substring(2) 的等效程序
var str = new String('some text')
// 因?yàn)榇藭r(shí) str 已經(jīng)是基本包裝類型了,擁有了 substring 方法
var sub_str = str.substring(2)
// 最后釋放這個(gè)對象數(shù)據(jù)
str = null
可將上述內(nèi)容總結(jié)為:數(shù)據(jù)賦值過程中需要判斷數(shù)據(jù)是基本數(shù)據(jù)類型還是引用類型,基本數(shù)據(jù)類型的數(shù)據(jù)直接賦值給變量,而引用類型的數(shù)據(jù)將引用賦值給變量。
下面通過例子來鞏固學(xué)習(xí)內(nèi)容:
var a = 20
var b = a
b = 30
console.log(a)
第一條語句所賦值的數(shù)據(jù)是 Number 基本數(shù)據(jù)類型,那么只需要將值映射給變量 a 即可。
第二條語句首先調(diào)用了變量 a ,將數(shù)值 20 復(fù)制給變量 b 映射的數(shù)值中,此時(shí)變量 b 所映射的數(shù)值與變量 a 所映射的數(shù)值存在不同的內(nèi)存單元中。這里可能需要補(bǔ)充一個(gè)概念,在 ECMAScript 中賦值和參數(shù)傳遞都是傳值操作,也就是將一個(gè)變量映射的值傳給另一個(gè)變量映射的值?;蛘吆唵蔚恼J(rèn)為變量和映射的值是一個(gè)整體。

第三條語句將基本數(shù)據(jù)類型的數(shù)值 30 映射給變量 b 。
再看一個(gè)例子:
var c = [1, 2, 3]
var d = {x: 10, y: 20, z: 30}
var e = c
e[2] = 4
var f = d
f.y = 40
// c: [1, 2, 4], e: [1, 2, 4]
// d: {x: 10, y: 40, z: 30}, f: {x: 10, y: 40, z: 30}
上例中,c 是 Array 引用類型, d 是 Object 引用類型,它們的值都是可變的。而更需要說明的是 e 和 c 同時(shí)指向一個(gè)對象, d 和 f 也是,當(dāng) e 和 f 被修改時(shí),c 和 d 的值也隨之發(fā)生變化。

1.5. 變量對象
在之前的圖示中,已經(jīng)多次描述內(nèi)存的操作方式,即棧和堆。所謂的棧是一種對內(nèi)存單元的操作方式,使得內(nèi)存對外呈現(xiàn)出一種特殊的數(shù)據(jù)結(jié)構(gòu):堆棧。堆棧本身是一種受限的線性表,其受限主要表現(xiàn)在對數(shù)據(jù)的操作位置只能是棧頂,擁有先進(jìn)后出(FILO)的特點(diǎn)。現(xiàn)實(shí)中收納乒乓球的盒子就是一個(gè)堆棧。在程序中通常利用堆棧解決嵌套調(diào)用的問題,例如下面的程序:
function sum(a, b) {
return a + b
}
var result = sum(1, 2)
當(dāng)程序開始執(zhí)行,編譯器會為此創(chuàng)建一個(gè)全局執(zhí)行環(huán)境又稱為執(zhí)行上下文,以后每進(jìn)入一個(gè)函數(shù),都會創(chuàng)建相應(yīng)的執(zhí)行環(huán)境,并依次將執(zhí)行環(huán)境推入內(nèi)存的??臻g。在執(zhí)行環(huán)境中最重要的兩個(gè)部分,一個(gè)是變量對象,它用來保存在環(huán)境中所定義的變量和函數(shù),另一個(gè)是作用域鏈(后面章節(jié)說明)。
接下來通過圖示解釋上述程序的執(zhí)行過程,在程序開始執(zhí)行時(shí),全局執(zhí)行環(huán)境被壓入??臻g:

當(dāng)程序調(diào)用 sum 函數(shù)時(shí),sum 函數(shù)的執(zhí)行環(huán)境被壓入??臻g:

當(dāng)然執(zhí)行完 sum 函數(shù)后,sum 函數(shù)的執(zhí)行環(huán)境就會從棧空間中彈出,把控制權(quán)交還給之前進(jìn)入棧空間的執(zhí)行環(huán)境(此例中就是全局執(zhí)行環(huán)境)。
之前已經(jīng)學(xué)習(xí)到變量和其映射的值都保存在內(nèi)存的棧空間中,現(xiàn)在明白了其中的原因,它們作為執(zhí)行環(huán)境中的變量對象壓入進(jìn)執(zhí)行環(huán)境棧。此時(shí)仔細(xì)思考會發(fā)現(xiàn)另外一個(gè)與變量對象相關(guān)的問題,那就是一個(gè)執(zhí)行環(huán)境中的程序是否可以訪問另外一個(gè)執(zhí)行環(huán)境的變量對象?這個(gè)問題的答案就是作用域鏈。
1.6. 作用域鏈
在執(zhí)行某一個(gè)環(huán)境中的程序時(shí),會創(chuàng)建一個(gè)變量對象的作用域鏈。它的作用是給出該執(zhí)行環(huán)境中的程序能夠有權(quán)訪問的變量和函數(shù)的有序鏈表。作用域鏈的首個(gè)結(jié)點(diǎn)始終是當(dāng)前執(zhí)行環(huán)境的變量對象,下一個(gè)結(jié)點(diǎn)來自包含的外部環(huán)境,依此類推直到全局執(zhí)行環(huán)境。全局執(zhí)行環(huán)境的變量對象總是作用域鏈的最后一個(gè)結(jié)點(diǎn)。
請看下面的示例代碼:
function interestRate (x) {
return function yearBalance (y) {
return y * (1 + x)
}
}
var currentDeposit = interestRate(0.03)
var balance = currentDeposit(10000)
console.log(balance)
下圖根據(jù)定義給出了各執(zhí)行環(huán)境的作用域鏈,為了清楚的展現(xiàn)之間的關(guān)系將變量對象從執(zhí)行環(huán)境中分離。

程序解析變量是沿著作用域鏈一級一級的搜索,搜索過程始終從作用域鏈的前端開始,然后逐級向后回溯,直到找到變量為止。
上例中 yearBalance 函數(shù)通過作用域鏈訪問到了 interestRate 函數(shù)的變量 x 。
最后我們來疏通概念間彼此糾纏的關(guān)系,執(zhí)行環(huán)境是一個(gè)函數(shù)在內(nèi)存中的投影,變量對象是函數(shù)的內(nèi)部數(shù)據(jù),而作用域鏈?zhǔn)呛瘮?shù)暴露的接口。每次執(zhí)行函數(shù),首先將執(zhí)行環(huán)境投影到內(nèi)存,然后根據(jù)作用域鏈獲取參數(shù),執(zhí)行函數(shù)代碼并修改內(nèi)部數(shù)據(jù)也就是變量對象,這里對于可變的變量對象我們又稱之為活動對象。期間忽略了許多細(xì)節(jié),如活動對象和作用域鏈的構(gòu)建,垃圾收集等。
1.7 變量對象與活動對象的區(qū)別
兩者都指向一個(gè)對象這是大家的共識,在一些文章中將兩者的區(qū)別歸結(jié)于執(zhí)行環(huán)境的不同階段,文中都把執(zhí)行環(huán)境的生命周期看成兩個(gè)階段,分別是 創(chuàng)建階段 和 執(zhí)行階段 ,變量對象處于創(chuàng)建階段而活動對象處于執(zhí)行階段。 這是沒有問題的,但是這些文章中對創(chuàng)建階段的概念還是比較模糊的,個(gè)人認(rèn)為創(chuàng)建階段所指的是,執(zhí)行環(huán)境從開始到銷毀除了執(zhí)行階段外的所有生命周期。比如上例中如果進(jìn)入 yearBalance 執(zhí)行環(huán)境的執(zhí)行階段,那么 interestRate 執(zhí)行環(huán)境就處在創(chuàng)建階段。
2. 面向?qū)ο?/h1>
2.1. this (strict)
在經(jīng)典的面向?qū)ο笳Z言中,this 只會出現(xiàn)在類中,作為該類實(shí)例(對象)的占位符使用。 ECMAScript 部分繼承和實(shí)現(xiàn)了上述規(guī)則,唯一例外之處正是 ECMAScript 中最美妙最古怪的函數(shù)。之前學(xué)習(xí)到所有的函數(shù)都是 Function 引用類型的實(shí)例(對象),按說應(yīng)該沒有 this 這個(gè)占位符,但是有一類函數(shù)起到了類的作用,可以生成對象,這就是構(gòu)造函數(shù)。因此在函數(shù)中又引入了 this 占位符,這與經(jīng)典定義格格不入,出現(xiàn)了對象中有另一個(gè)對象占位符的混亂概念,使得 this 在函數(shù)中晦澀難懂。
我們從最基本的定義分析,以此梳理 this 的使用規(guī)則。
-
除函數(shù)外,this 不會出現(xiàn)在對象中。
// 對象字面量 var o = { a: 1, b: this.a + 1 } // Object {a: 1, b: NaN} // 數(shù)組 var arr = [1, this, 3] // [1, Object, 3] 箭頭函數(shù)是匿名函數(shù),不會擔(dān)任構(gòu)造函數(shù)的角色,因此沒有 this 。
-
函數(shù)中的 this 是在函數(shù)被執(zhí)行時(shí)才確認(rèn)。
回顧之前的內(nèi)容,函數(shù)在運(yùn)行時(shí)創(chuàng)建執(zhí)行環(huán)境,執(zhí)行環(huán)境中包含變量對象和 this 對象,變量對象是在執(zhí)行代碼過程中形成,this 對象在執(zhí)行開始獲取并無法修改。
構(gòu)造函數(shù)的 this 來自它所創(chuàng)造的實(shí)例。
非構(gòu)造函數(shù)的 this 是引用它的對象。
function Person (name, age) {
this.name = name
this.age = age
this.sayName = sayName
}
function sayName () {
console.log(this.name)
}
var alien = {
name: 'alien',
age: 1000,
sayName: sayName
}
var tom = new Person('tom', 22)
alien.sayName()
tom.sayName()
上述只是 this 最基本的判斷方法,如果仔細(xì)分析作用域同時(shí)結(jié)合閉包思想,可以盡可能的減少 this 的出錯率。