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



這篇筆記中有什么:

✔️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
    • 取值為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)。
目前看到的相似點比較多,差異性有待進一步比較。

三、面向對象

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所指向的對象的引用。

四、參考

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM