這篇筆記中有什麼:javascript
✔️JavaScript的極簡介紹
✔️JavaScript中數據類型的簡單梳理
✔️JavaScript中的面向對象原理前端
這篇筆記中沒有什麼:java
❌JavaScript的具體語法
❌JavaScript經過各類內置對象實現的其餘特性編程
最新的標準中,定義了8種數據類型。其中包括:後端
基本數據類型,有些版本也譯爲原始數據類型。數組
什麼是基本類型?看一下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
true
和false
。0
、""
、NaN
、null
、undefined
也會被轉換爲false
。null
。表示未被聲明的值。"object"
。var a; console.log(typeof a); // undefined console.log(typeof a); // "undefined"
Symbol()
函數構造,每一個從該函數返回的symbol值都是惟一的。var sym1 = Symbol("abc"); var sym2 = Symbol("abc"); console.log(sym1 == sym2); // false console.log(sym1 === sym2); // false
接觸了一些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引擎去作更好一些,手動建立封裝對象可能會致使不少問題。
包裝對象做爲一種技術上的實現細節,不須要過多關注。可是瞭解這個原理有助於咱們更好地理解和使用基本數據類型。
Function
對象Array
的長度可變,元素類型任意,所以多是非密集型的。數組索引只能是整數,索引從0開始new
操做符建立對象是一種特殊的數據,能夠看作是一組屬性的集合。屬性能夠是數據,也能夠是函數(此時稱爲方法)。每一個屬性有一個名稱和一個值,能夠近似當作是一個鍵值對。名稱一般是字符串,也能夠是Symbol
。
var obj = new Object(); // 經過new操做符 var obj = {}; // 經過對象字面量(object literal)
有兩種方式來訪問對象的屬性,一種是經過點操做符,一種是經過中括號。
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值就是該對象。
有些地方會用到引用類型這個概念來指代Object類型。要理解這個說法,就須要理解javascript中變量的訪問方式。
基本數據類型的值是按值訪問的
引用類型的值是按引用訪問的
按值訪問意味着值不可變、比較是值與值之間的比較、變量的標識符和值都存放在棧內存中。賦值時,進行的是值的拷貝,賦值操做後,兩個變量互相不影響。
按引用訪問意味着值可變(Object的屬性能夠動態的增刪改)、比較是引用的比較(兩個不一樣的空對象是不相等的)、引用類型的值保存在堆內存中,棧內存裏保存的是地址。賦值時,進行的是地址值的拷貝,複製操做後兩個變量指向同一個對象。經過其中一個變量修改對象屬性的話,經過另外一個變量去訪問屬性,也是已經被改變過的。
Object類型的概念和Lua中的table類型比較類似。變量保存的都是引用,數據組織都是類鍵值對的形式。table中用原表(metatable)來實現面向對象的概念,Javascript中則是用原型(prototype)。
目前看到的類似點比較多,差別性有待進一步比較。
編程時常常會有重用的需求。咱們但願可以大規模構建同種結構的對象,有時咱們還但願可以基於某個已有的對象構建新的對象,只重寫或添加部分新的屬性。這就須要「類型和繼承」的概念。
Javascript中並無class實現,除了基本類型以外只有Object這一種類型。可是咱們能夠經過原型繼承的方式實現面向對象的需求。
注:ECMAScript6中引入了一套新的關鍵字用來實現class。可是底層原理仍然是基於原型的。此處先不提。
Javascript中,每一個對象都有一個特殊的隱藏屬性[[Prototype]]
,它要麼爲null
,要麼就是對另外一個對象的引用。被引用的對象,稱爲這個對象的原型對象。
原型對象也有一個本身的[[Prototype]]
,層層向上,直到一個對象的原型對象爲null
。
能夠很容易地推斷出,這是一個鏈狀,或者說樹狀的關係。null
是沒有原型的,是全部原型鏈的終點。
如前文所說,JavaScript中的Object是屬性的集合。原型屬性將多個Obeject串連成鏈。當試圖訪問一個對象的屬性時,會首先在該對象中搜索,若是沒有找到,那麼會沿着原型鏈一路搜索上去,直到在某個原型上找到了該屬性或者到達了原型鏈的末尾。Javascript就是經過這種形式,實現了繼承。
從原理來看,能夠很天然地明白,原型鏈前端的屬性會屏蔽掉後端的同名屬性。
函數在JavaScript中是一等公民,函數的繼承與和其餘屬性的繼承沒有區別。
須要注意的是,在調用一個方法obj.method()
時,即便方法是從obj
的原型中獲取的,this
始終引用obj
。方法始終與當前對象一塊兒使用。
繼承一個對象能夠經過原型,那麼如何可複用地產生對象呢?
可使用函數來模擬咱們想要的「類」。實現一個相似於構造器的函數,在這個函數中定義並返回咱們想要的對象。這樣,每次調用這個函數的時候咱們均可以產生一個同「類」的新對象。
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
,使用在函數中時指代的老是當前對象——也就是調用了這個函數的對象。
咱們可使用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"
上面的實現能夠炮製咱們想要的自定義對象,可是它和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
所指向的對象的引用。