這篇筆記中有什麼:
✔️JavaScript的極簡介紹
✔️JavaScript中資料型別的簡單梳理
✔️JavaScript中的物件導向原理
這篇筆記中沒有什麼:
❌JavaScript的具體語法
❌JavaScript通過各種內建物件實現的其他特性
一、概覽
- 解釋型,或者說即時編譯型( Just-In-Time Compiled )語言。
- 多正規化動態語言,原生支援函數語言程式設計,通過原型鏈支援物件導向程式設計。
- 其實是和Java是完全不同的東西。設計中有參考Java的資料結構和記憶體管理、C語言的基本語法,但理念上並不相似。
- 最開始是專門為瀏覽器設計的一門指令碼語言,但現在也被用於很多其他環境,甚至可以在任意搭載了JavaScript引擎的裝置中執行。
二、資料型別
1. JavaScript中的資料型別
最新的標準中,定義了8種資料型別。其中包括:
- 7種基本型別:Number、String、Boolean、BigInt、Null、Undefined以及ES2016新增的Symbol。
- 1種複雜型別:Object。
2. 什麼是基本型別(Primitive Data Type)
2.1 概念
基本資料型別,有些版本也譯為原始資料型別。
什麼是基本型別?看一下MDN上給出的定義:
In JavaScript, a primitive (primitive value, primitive data type) is data that is not an object and has no methods.
基本型別是最底層的型別,不是物件,沒有方法。
所有基本資料型別的值都是不可改變的——可以為變數賦一個新值、覆蓋原來的值,但是無法直接修改值本身。
這一點對於number、boolean來說都很直觀,但是對於字串來說可能需要格外注意:同一塊記憶體中的一個字串是不可以部分修改的,一定是整體重新賦值。
var a = "hello"; // 一個string型別的變數,值為“hello”
console.log(a); // hello
console.log(typeof a); // string
a[0] = "H";
console.log(a); // hello
var c = a; // world
c = c + " world"; // 這裡,並沒有改變本來的hello,而是開闢了新的記憶體空間,構造了新的基本值“hello world”
console.log(c); // hello world
2.2 七個基本型別
- 布林 boolean
- 取值為
true
和false
。 0
、""
、NaN
、null
、undefined
也會被轉換為false
。
- 取值為
- Null
- Null型別只有一個值:
null
。表示未被宣告的值。 - 注意:由於歷史原因,typeof null的結果是
"object"
。
- Null型別只有一個值:
- undefined
- 未初始化的值(宣告瞭但是沒有賦值)。
var a;
console.log(typeof a); // undefined
console.log(typeof a); // "undefined"
- 數字 number
- 64位雙精度浮點數(並沒有整數和浮點數的區別)。
- 大整數 bigint
- 可以用任意精度表示整數。
- 通過在整數末尾附加n或呼叫建構函式來建立。
- 不可以與Number混合運算,會報型別錯誤。需要先進行轉換。
- 字串 string
- Unicode字元序列。
- 符號 Symbol
- 可以用來作為Object的key的值(預設私有)。
- 通過
Symbol()
函式構造,每個從該函式返回的symbol值都是唯一的。 - 可以使用可選的字串來描述symbol,僅僅相當於註釋,可用於除錯。
var sym1 = Symbol("abc");
var sym2 = Symbol("abc");
console.log(sym1 == sym2); // false
console.log(sym1 === sym2); // false
2.3 基本型別封裝物件
接觸了一些JavaScript的程式碼,又瞭解了它對型別的分類之後,可能會感到非常困惑:基本資料型別不是物件,沒有方法,那麼為什麼又經常會看到對字串、數字等“基本型別”的變數呼叫方法呢?
如下面的例子:
var str = "hello";
console.log(typeof str); // string
console.log(str.charAt(2)); // "l"
可以看到,str的型別確實是基本型別string
,理論上來說並不是物件。但是我們實際上卻能夠通過點運算子呼叫一些為字串定義的方法。這是為什麼呢?
其實,執行str.charAt(2)
的時候發生了很多事情,遠比我們所看到的一個“普通的呼叫”要複雜。
Java中有基本型別包裝類的概念。比如:Integer
是對基本int
型別進行了封裝的包裝類,提供一些額外的函式。
在JavaScript中,原理也是如此,只是在形式上進行了隱藏。JavaScript中,定義了原生物件String
,作為基本型別string
的封裝物件。我們看到的charAt()
方法,其實是String物件中的定義。當我們試圖訪問基本型別的屬性和方法時,JavaScript會自動為基本型別值封裝出一個封裝物件,之後從封裝物件中去訪問屬性、方法。而且,這個物件是臨時的,呼叫完屬性之後,包裝物件就會被丟棄。
這也就解釋了一件事:為什麼給基本型別新增屬性不會報錯,但是並不會有任何效果。因為,新增的屬性其實新增在了臨時物件上,而臨時物件很快就被銷燬了,並不會對原始值造成影響。
封裝物件有: String
、Number
、Boolean
和 Symbol
。
我們也可以通過new去顯性地建立包裝物件(除了Symbol
)。
var str = "hello";
var num = 23;
var bool = false;
var S = new String(str)
var N = new Number(num)
var B = new Boolean(bool);
console.log(typeof S); //object
console.log(typeof N); // object
console.log(typeof B); // object
一般來說,將這件事託付給JavaScript引擎去做更好一些,手動建立封裝物件可能會導致很多問題。
包裝物件作為一種技術上的實現細節,不需要過多關注。但是瞭解這個原理有助於我們更好地理解和使用基本資料型別。
3. 什麼是物件型別(Object)
3.1 四類特殊物件
- 函式 Function
- 每個JavaScript函式實際上都是一個
Function
物件 - JavaScript中,函式是“一等公民”,也就是說,函式可以被賦值給變數,可以被作為引數,可以被作為返回值。(這個特性Lua中也有)
- 因此,可以將函式理解為,一種附加了可被呼叫功能的普通物件。
- 每個JavaScript函式實際上都是一個
- 陣列 Array
- 用於構造陣列的全域性物件。陣列是一種類列表的物件。
Array
的長度可變,元素型別任意,因此可能是非密集型的。陣列索引只能是整數,索引從0開始 - 訪問元素時通過中括號
- 日期 Date
- 通過
new
操作符建立
- 用於構造陣列的全域性物件。陣列是一種類列表的物件。
- 正則 RegExp
- 用於將文字與一個模式進行匹配
3.2 物件是屬性的集合
物件是一種特殊的資料,可以看做是一組屬性的集合。屬性可以是資料,也可以是函式(此時稱為方法)。每個屬性有一個名稱和一個值,可以近似看成是一個鍵值對。名稱通常是字串,也可以是Symbol
。
3.3 物件的建立
var obj = new Object(); // 通過new操作符
var obj = {}; // 通過物件字面量(object literal)
3.4 物件的訪問
有兩種方式來訪問物件的屬性,一種是通過點操作符,一種是通過中括號。
var a = {};
a["age"] = 3; // 新增新的屬性
console.log(a.age); // 3
for(i in a){
console.log(i); // "age"
console.log(a[i]); // 3
}
對於物件的方法,如果加括號,是返回撥用結果;如果不加括號,是返回方法本身,可以賦值給其他變數。
var a = {name : "a"};
a.sayHello = function(){
console.log(this.name + ":hello");
}
var b = {name : "b"};
b.saySomething = a.sayHello;
b.saySomething(); //"b:hello"
注:函式作為物件的方法被呼叫時,this值就是該物件。
3.5 引用型別
有些地方會用到引用型別這個概念來指代Object型別。要理解這個說法,就需要理解javascript中變數的訪問方式。
-
基本資料型別的值是按值訪問的
-
引用型別的值是按引用訪問的
按值訪問意味著值不可變、比較是值與值之間的比較、變數的識別符號和值都存放在棧記憶體中。賦值時,進行的是值的拷貝,賦值操作後,兩個變數互相不影響。
按引用訪問意味著值可變(Object的屬性可以動態的增刪改)、比較是引用的比較(兩個不同的空物件是不相等的)、引用型別的值儲存在堆記憶體中,棧記憶體裡儲存的是地址。賦值時,進行的是地址值的拷貝,複製操作後兩個變數指向同一個物件。通過其中一個變數修改物件屬性的話,通過另一個變數去訪問屬性,也是已經被改變過的。
3.6 和Lua中Table的比較
Object型別的概念和Lua中的table型別比較相似。變數儲存的都是引用,資料組織都是類鍵值對的形式。table中用原表(metatable)來實現物件導向的概念,Javascript中則是用原型(prototype)。
目前看到的相似點比較多,差異性有待進一步比較。
三、物件導向
1. 意義
程式設計時經常會有重用的需求。我們希望能夠大規模構建同種結構的物件,有時我們還希望能夠基於某個已有的物件構建新的物件,只重寫或新增部分新的屬性。這就需要“型別和繼承”的概念。
Javascript中並沒有class實現,除了基本型別之外只有Object這一種型別。但是我們可以通過原型繼承的方式實現物件導向的需求。
注:ECMAScript6中引入了一套新的關鍵字用來實現class。但是底層原理仍然是基於原型的。此處先不提。
2. 原型與繼承
Javascript中,每個物件都有一個特殊的隱藏屬性[[Prototype]]
,它要麼為null
,要麼就是對另一個物件的引用。被引用的物件,稱為這個物件的原型物件。
原型物件也有一個自己的[[Prototype]]
,層層向上,直到一個物件的原型物件為null
。
可以很容易地推斷出,這是一個鏈狀,或者說樹狀的關係。null
是沒有原型的,是所有原型鏈的終點。
如前文所說,JavaScript中的Object是屬性的集合。原型屬性將多個Obeject串連成鏈。當試圖訪問一個物件的屬性時,會首先在該物件中搜尋,如果沒有找到,那麼會沿著原型鏈一路搜尋上去,直到在某個原型上找到了該屬性或者到達了原型鏈的末尾。Javascript就是通過這種形式,實現了繼承。
從原理來看,可以很自然地明白,原型鏈前端的屬性會遮蔽掉後端的同名屬性。
函式在JavaScript中是一等公民,函式的繼承與和其他屬性的繼承沒有區別。
需要注意的是,在呼叫一個方法obj.method()
時,即使方法是從obj
的原型中獲取的,this
始終引用obj
。方法始終與當前物件一起使用。
3. 自定義物件
如何建立類似物件
繼承一個物件可以通過原型,那麼如何可複用地產生物件呢?
可以使用函式來模擬我們想要的“類”。實現一個類似於構造器的函式,在這個函式中定義並返回我們想要的物件。這樣,每次呼叫這個函式的時候我們都可以產生一個同“類”的新物件。
function makePerson(name, age){
return {
name: name,
age: age,
getIntro:function(){
return "Name:" + this.name + " Age:" + this.age;
};
};
}
var xiaoming = makePerson("Xiaoming", 10);
console.log(xiaoming.name, xiaoming.age); // "Xiaoming" 10
console.log(xiaoming.getIntro()); // "Name:Xiaoming Age:10"
關鍵字this
,使用在函式中時指代的總是當前物件——也就是呼叫了這個函式的物件。
構造器和new
我們可以使用this
和關鍵字new
來對這個構造器進行進一步的封裝。
關鍵字new
可以建立一個嶄新的空物件,使用這個新物件的this來呼叫函式,並將這個this
作為函式返回值。我們可以在函式中對this
進行屬性和方法的設定。
這樣,我們的函式就是一個可以配合new
來使用的真正的構造器了。
通常構造器沒有return
語句。如果有return
語句且返回的是一個物件,則會用這個物件替代this
返回。如果是return
的是原始值,則會被忽略。
function makePerson(name, age){
this.name = name;
this.age = age;
this.getIntro = function(){
return "Name:" + this.name + " Age:" + this.age;
};
}
var xiaoming = new makePerson("Xiaoming", 10);
console.log(xiaoming.name, xiaoming.age); // "Xiaoming" 10
console.log(xiaoming.getIntro()); // "Name:Xiaoming Age:10"
構造器的prototype屬性
上面的實現可以炮製我們想要的自定義物件,但是它和C++中的class
比還有一個很大的缺點:每個物件中都包含了重複的函式物件。但是如果我們把這個函式放在外面實現,又會增加不必要的全域性函式。
JavaScript提供了一個強大的特性。每個函式物件都有一個prototype
屬性,指向某一個物件。通過new
建立出來的新物件,會將構造器的prototype
屬性賦值給自己的[[Prototype]]
屬性。也就是說,每一個通過new
構造器函式生成出來的物件,它的[[Prototype]]
都指向構造器函式當前的prototype
所指向的物件。
注意,函式的prototype
屬性和前文所說的隱藏的[[Prototype]]
屬性並不是一回事。
函式物件的prototype
是一個名為“prototype”的普通屬性,指向的並不是這個函式物件的原型。函式物件的原型儲存在函式物件的[[Prototype]]
中。
事實上,每個函式物件都可以看成是通過
new Function()
構造出來的,也就是說,每個函式物件的[[Prototype]]
屬性都由Funtion
的prototype
屬性賦值而來。
我們定義的函式物件,預設的prototype
是一個空物件。我們可以通過改變這個空物件的屬性,動態地影響到所有以這個物件為原型的物件(也就是從這個函式生成的所有物件)。
於是上面的例子可以改寫為:
function makePerson(name, age){
this.name = name;
this.age = age;
}
var xiaoming = new makePerson("Xiaoming", 10);
makePerson.prototype.getIntro = function(){
return "Name:" + this.name + " Age:" + this.age;
};
console.log(xiaoming.name, xiaoming.age); // "Xiaoming" 10
console.log(xiaoming.getIntro()); // "Name:Xiaoming Age:10"
這裡是先構造了物件xiaoming
,再為它的原型增加了新的方法。可以看到,xiaoming
可以通過原型鏈呼叫到新定義的原型方法。
需要注意的是,如果直接令函式的prototype
為新的物件,將不能影響到之前生成的繼承者們——因為它們的[[Prototype]]
中儲存的是原來的prototype
所指向的物件的引用。