Javascript 雜談 :熟悉基本概念
1. 信息译断、變量萧恕、數(shù)據(jù)類型和變量對象
ECMAScript 對于變量的定義及語法漾狼,借鑒了眾多語言的特性鸠蚪,同時也形成了自己獨特的運作機制今阳。松散的變量和簡單的語法讓編程過程更加愜意自由。
1.1. 看不見的信息
眾所周知茅信,計算機是用來處理信息的工具盾舌,而程序就是處理信息的步驟。但是信息是看不見摸不到的蘸鲸,如何進行操作妖谴?我們首先使用符號來承載它,也就是經(jīng)常提到的數(shù)據(jù),但是這樣好像也不行膝舅,例如在黑漆漆的房間里我畫下了若干符號嗡载,你依然無法感知啊。所以仍稀,要有光洼滚,利用信號讓你感知數(shù)據(jù)和信息。比如這篇文章在我的腦中技潘,你是無法感知的遥巴,我用漢語(符號)將它寫出,然后通過光傳到你的眼睛里崭篡;你的大腦將光信號再翻譯回漢語(符號)挪哄,然后分析它的信息吧秕。
好了琉闪,請描述一下 “20 ” 這個信息。什么我已經(jīng)描述了砸彬,你看到的 “20” 就是這個信息的阿拉伯?dāng)?shù)字符號表示颠毙。那么你是怎么看到這個符號的?真正的過程可能比較復(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)變成了實際存在且能保存的物理信號庇忌。那么程序是如何讀取內(nèi)存中的 “20” 的舞箍?
1.2. 客觀的數(shù)據(jù)
用哲學(xué)的概念來引述,存儲在內(nèi)存中的 “20”皆疹,它是具體存在的疏橄,是客觀的。而將它映射到程序中的就是變量墙基,它是邏輯的(主觀的)软族。
1.3. 變量只是一個名字
變量在 ECMAScript 中是所謂的標識符刷喜,也就是一個名稱或名字。它的作用只是用來指示某個物體或?qū)ο螅?strong>變量沒有數(shù)據(jù)立砸。比如每個人都有名字掖疮,但是名字里含有你的數(shù)據(jù)嗎?名字只是用來辨識人颗祝,同理變量只是用來分辨具體數(shù)據(jù)或?qū)ο蟮摹?/p>
你可能會問 var a = 20
中的 “20” 不就是一個標識符嗎浊闪?這條語句的具體步驟應(yīng)該是:
- 人類輸入熟悉的標識符 “20”
- 編譯器將 “20” 編譯成相應(yīng)類型的數(shù)據(jù)存儲在內(nèi)存中
- 使用變量 a 來映射內(nèi)存中的數(shù)據(jù)
也就是說這里的 a 和 20 都是內(nèi)存中數(shù)據(jù)的標識符,20 是人類熟悉的標識符螺戳,通常出現(xiàn)在初始化過程搁宾,a 則是程序使用的標識符,并且因為是邏輯的倔幼,所以可以變動盖腿,這也是稱為變量的原因。當(dāng)然在 ECMAScript 中除了 20 這樣的數(shù)字损同,還定義多種數(shù)據(jù)類型翩腐。
1.4. 類型是數(shù)據(jù)的結(jié)構(gòu)
在人類世界中國際標準的基本單位有 7 個:長度m,時間s膏燃,質(zhì)量kg茂卦,熱力學(xué)溫度(Kelvin溫度)K,電流單位A组哩,光強度單位cd(坎德拉)等龙,物質(zhì)的量mol。這些基本單位可以推出物理世界的所有物理量伶贰。而在 ECMAScript 中定義了 6 種數(shù)據(jù)的類型蛛砰,并且不支持自定義類型,因此所有值都是這 6 中數(shù)據(jù)類型之一幕袱。它們又分為兩類暴备,第一類稱為基本數(shù)據(jù)類型,包含:Undefined
们豌, Null
涯捻, Boolean
, Number
望迎, String
障癌。另外一類稱為復(fù)雜數(shù)據(jù)類型 Object
,它是由多個無序的鍵值對組成辩尊。
根據(jù)數(shù)據(jù)類型的分類涛浙, ECMAScript 中的變量可能映射兩種不同的類型值,基本數(shù)據(jù)類型值和引用類型值〗瘟粒基本數(shù)據(jù)類型值就是該類型的數(shù)據(jù)疮薇,引用類型值則是該類型對象的引用,它們都和變量存儲在內(nèi)存椢易ⅲ空間按咒,而引用類型的對象存儲在內(nèi)存的堆空間。
大體上可以認為引用類型就是繼承自 Object
復(fù)雜數(shù)據(jù)類型的一種數(shù)據(jù)結(jié)構(gòu)但骨,它包含了數(shù)據(jù)和與數(shù)據(jù)相關(guān)的操作励七。常用的有 Function
, Array
奔缠, Date
掠抬, RegExp
,而這些類型數(shù)據(jù)在聲明時就已經(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)的操作阳准。
// 程序中聲明一個 ```string``` 類型數(shù)據(jù)元素,但是注意并沒有定義相關(guān)的操作
var str = 'some text'
var listItem = [1, 2, 3]
// 而在這里使用了```substring``` 方法馏臭,
// 也就是說 ECMAScript 將 'some text' 這個數(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
映射的是一個基本數(shù)據(jù)類型的值括儒,注意僅僅是數(shù)值绕沈,但是為什么可以調(diào)用 substring()
方法?這就是基本包裝類型的功能帮寻,基本包裝類型同樣繼承自 Object
乍狐,但是數(shù)據(jù)元素都定義為 Number
這種基本數(shù)據(jù)類型,同時給出一些相關(guān)的基本操作固逗,它包含有 Number
浅蚪, String
, Boolean
烫罩。是的和基本類型的樣子一模一樣惜傲,這樣的黑箱效應(yīng)讓程序員無須留意轉(zhuǎn)化過程的。程序員看到的是 'some text'
贝攒,而程序在調(diào)用它時轉(zhuǎn)化成一種 String
基本包裝類型的對象實例盗誊,但是基本包裝類型特殊處就是只在調(diào)用時轉(zhuǎn)化,調(diào)用結(jié)束后釋放。
// var sub_str = str.substring(2) 的等效程序
var str = new String('some text')
// 因為此時 str 已經(jīng)是基本包裝類型了哈踱,擁有了 substring 方法
var sub_str = str.substring(2)
// 最后釋放這個對象數(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ù)值中舅列,此時變量 b 所映射的數(shù)值與變量 a 所映射的數(shù)值存在不同的內(nèi)存單元中。這里可能需要補充一個概念卧蜓,在 ECMAScript 中賦值和參數(shù)傳遞都是傳值操作帐要,也就是將一個變量映射的值傳給另一個變量映射的值∶旨椋或者簡單的認為變量和映射的值是一個整體榨惠。
第三條語句將基本數(shù)據(jù)類型的數(shù)值 30 映射給變量 b 。
再看一個例子:
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 同時指向一個對象期揪, d 和 f 也是,當(dāng) e 和 f 被修改時规个,c 和 d 的值也隨之發(fā)生變化凤薛。
1.5. 變量對象
在之前的圖示中,已經(jīng)多次描述內(nèi)存的操作方式诞仓,即棧和堆缤苫。所謂的棧是一種對內(nèi)存單元的操作方式,使得內(nèi)存對外呈現(xiàn)出一種特殊的數(shù)據(jù)結(jié)構(gòu):堆棧墅拭。堆棧本身是一種受限的線性表活玲,其受限主要表現(xiàn)在對數(shù)據(jù)的操作位置只能是棧頂,擁有先進后出(FILO)的特點〉瘢現(xiàn)實中收納乒乓球的盒子就是一個堆棧舒憾。在程序中通常利用堆棧解決嵌套調(diào)用的問題,例如下面的程序:
function sum(a, b) {
return a + b
}
var result = sum(1, 2)
當(dāng)程序開始執(zhí)行屡萤,編譯器會為此創(chuàng)建一個全局執(zhí)行環(huán)境又稱為執(zhí)行上下文珍剑,以后每進入一個函數(shù),都會創(chuàng)建相應(yīng)的執(zhí)行環(huán)境死陆,并依次將執(zhí)行環(huán)境推入內(nèi)存的椪凶荆空間唧瘾。在執(zhí)行環(huán)境中最重要的兩個部分,一個是變量對象别凤,它用來保存在環(huán)境中所定義的變量和函數(shù)饰序,另一個是作用域鏈(后面章節(jié)說明)。
接下來通過圖示解釋上述程序的執(zhí)行過程规哪,在程序開始執(zhí)行時求豫,全局執(zhí)行環(huán)境被壓入棧空間:
當(dāng)程序調(diào)用 sum
函數(shù)時诉稍,sum 函數(shù)的執(zhí)行環(huán)境被壓入楎鸺危空間:
當(dāng)然執(zhí)行完 sum 函數(shù)后,sum 函數(shù)的執(zhí)行環(huán)境就會從棻蓿空間中彈出蚤告,把控制權(quán)交還給之前進入棧空間的執(zhí)行環(huán)境(此例中就是全局執(zhí)行環(huán)境)服爷。
之前已經(jīng)學(xué)習(xí)到變量和其映射的值都保存在內(nèi)存的椂徘。空間中,現(xiàn)在明白了其中的原因仍源,它們作為執(zhí)行環(huán)境中的變量對象壓入進執(zhí)行環(huán)境棧心褐。此時仔細思考會發(fā)現(xiàn)另外一個與變量對象相關(guān)的問題,那就是一個執(zhí)行環(huán)境中的程序是否可以訪問另外一個執(zhí)行環(huán)境的變量對象笼踩?這個問題的答案就是作用域鏈逗爹。
1.6. 作用域鏈
在執(zhí)行某一個環(huán)境中的程序時,會創(chuàng)建一個變量對象的作用域鏈戳表。它的作用是給出該執(zhí)行環(huán)境中的程序能夠有權(quán)訪問的變量和函數(shù)的有序鏈表桶至。作用域鏈的首個結(jié)點始終是當(dāng)前執(zhí)行環(huán)境的變量對象,下一個結(jié)點來自包含的外部環(huán)境匾旭,依此類推直到全局執(zhí)行環(huán)境。全局執(zhí)行環(huán)境的變量對象總是作用域鏈的最后一個結(jié)點圃郊。
請看下面的示例代碼:
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)境是一個函數(shù)在內(nèi)存中的投影竹伸,變量對象是函數(shù)的內(nèi)部數(shù)據(jù)泥栖,而作用域鏈是函數(shù)暴露的接口簇宽。每次執(zhí)行函數(shù),首先將執(zhí)行環(huán)境投影到內(nèi)存吧享,然后根據(jù)作用域鏈獲取參數(shù)魏割,執(zhí)行函數(shù)代碼并修改內(nèi)部數(shù)據(jù)也就是變量對象,這里對于可變的變量對象我們又稱之為活動對象钢颂。期間忽略了許多細節(jié)钞它,如活動對象和作用域鏈的構(gòu)建,垃圾收集等殊鞭。
1.7 變量對象與活動對象的區(qū)別
兩者都指向一個對象這是大家的共識遭垛,在一些文章中將兩者的區(qū)別歸結(jié)于執(zhí)行環(huán)境的不同階段,文中都把執(zhí)行環(huán)境的生命周期看成兩個階段操灿,分別是 創(chuàng)建階段 和 執(zhí)行階段 锯仪,變量對象處于創(chuàng)建階段而活動對象處于執(zhí)行階段。 這是沒有問題的牲尺,但是這些文章中對創(chuàng)建階段的概念還是比較模糊的卵酪,個人認為創(chuàng)建階段所指的是,執(zhí)行環(huán)境從開始到銷毀除了執(zhí)行階段外的所有生命周期谤碳。比如上例中如果進入 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)在類中瘸羡,作為該類實例(對象)的占位符使用。 ECMAScript 部分繼承和實現(xiàn)了上述規(guī)則搓茬,唯一例外之處正是 ECMAScript 中最美妙最古怪的函數(shù)犹赖。之前學(xué)習(xí)到所有的函數(shù)都是 Function
引用類型的實例(對象),按說應(yīng)該沒有 this 這個占位符卷仑,但是有一類函數(shù)起到了類的作用峻村,可以生成對象,這就是構(gòu)造函數(shù)锡凝。因此在函數(shù)中又引入了 this 占位符粘昨,這與經(jīng)典定義格格不入,出現(xiàn)了對象中有另一個對象占位符的混亂概念窜锯,使得 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í)行時才確認。
回顧之前的內(nèi)容浪听,函數(shù)在運行時創(chuàng)建執(zhí)行環(huán)境螟碎,執(zhí)行環(huán)境中包含變量對象和 this 對象,變量對象是在執(zhí)行代碼過程中形成迹栓,this 對象在執(zhí)行開始獲取并無法修改掉分。
構(gòu)造函數(shù)的 this 來自它所創(chuàng)造的實例。
非構(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 最基本的判斷方法酥郭,如果仔細分析作用域同時結(jié)合閉包思想,可以盡可能的減少 this 的出錯率愿吹。