Javascript 雜談 :熟悉基本概念

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”,它是具體存在的,是客觀的。而將它映射到程序中的就是變量,它是邏輯的(主觀的)。

map.png

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)存的堆空間。

variable.png

大體上可以認(rèn)為引用類型就是繼承自 Object 復(fù)雜數(shù)據(jù)類型的一種數(shù)據(jù)結(jié)構(gòu),它包含了數(shù)據(jù)和與數(shù)據(jù)相關(guān)的操作。常用的有 Function, Array, DateRegExp ,而這些類型數(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, StringBoolean 。是的和基本類型的樣子一模一樣,這樣的黑箱效應(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è)整體。

copy.png

第三條語句將基本數(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ā)生變化。

referrence.png

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:

global_variable_object.png

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

sum_AO.png

當(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)境中分離。

scope_chain.png

程序解析變量是沿著作用域鏈一級一級的搜索,搜索過程始終從作用域鏈的前端開始,然后逐級向后回溯,直到找到變量為止。
上例中 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 的出錯率。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容