目錄
- 一、概覽
- 二、資料型別
- 1. JavaScript中的資料型別
- 2. 什么是基本型別(Primitive Data Type)
- 2.1 概念
- 2.2 七個基本型別
- 2.3 基本型別封裝物件
- 3. 什么是物件型別(Object)
- 3.1 四類特殊物件
- 3.2 物件是屬性的集合
- 3.3 物件的創建
- 3.4 物件的訪問
- 3.5 參考型別
- 3.6 和Lua中Table的比較
- 三、面向物件
- 1. 意義
- 2. 原型與繼承
- 3. 自定義物件
- 如何創建類似物件
- 構造器和new
- 構造器的prototype屬性
- 四、參考
這篇筆記中有什么:
??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所指向的物件的參考,
四、參考
MDN | 重新介紹JavaScript
MDN | Primitive
原型繼承
MDN | 原型與渲染鏈
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/255537.html
標籤:其他
