JavaScript 類型、原型與繼承學習筆記


這篇筆記中有什麼:javascript

✔️JavaScript的極簡介紹
✔️JavaScript中數據類型的簡單梳理
✔️JavaScript中的面向對象原理前端

這篇筆記中沒有什麼:java

❌JavaScript的具體語法
❌JavaScript經過各類內置對象實現的其餘特性編程


1、概覽

  • 解釋型,或者說即時編譯型( Just-In-Time Compiled )語言。
  • 多範式動態語言,原生支持函數式編程,經過原型鏈支持面向對象編程。
  • 實際上是和Java是徹底不一樣的東西。設計中有參考Java的數據結構和內存管理、C語言的基本語法,但理念上並不類似。
  • 最開始是專門爲瀏覽器設計的一門腳本語言,但如今也被用於不少其餘環境,甚至能夠在任意搭載了JavaScript引擎的設備中執行。

2、數據類型

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
    • 取值爲truefalse
    • 0""NaNnullundefined也會被轉換爲false
  • Null
    • Null類型只有一個值:null。表示未被聲明的值。
    • 注意:因爲歷史緣由,typeof null的結果是"object"
  • 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會自動爲基本類型值封裝出一個封裝對象,以後從封裝對象中去訪問屬性、方法。並且,這個對象是臨時的,調用完屬性以後,包裝對象就會被丟棄。

這也就解釋了一件事:爲何給基本類型添加屬性不會報錯,可是並不會有任何效果。由於,添加的屬性其實添加在了臨時對象上,而臨時對象很快就被銷燬了,並不會對原始值形成影響。

封裝對象有: StringNumberBooleanSymbol

咱們也能夠經過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中也有)
    • 所以,能夠將函數理解爲,一種附加了可被調用功能的普通對象。
  • 數組 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)。
目前看到的類似點比較多,差別性有待進一步比較。

3、面向對象

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]]屬性都由Funtionprototype屬性賦值而來。

咱們定義的函數對象,默認的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所指向的對象的引用。

4、參考

MDN | 從新介紹JavaScript
MDN | Primitive
原型繼承
MDN | 原型與渲染鏈

相關文章
相關標籤/搜索