Javascript 面向對象

一直在說面向對象,也在說 Javascript 是面向對象、面向過程、函數式編程的語言。那麼到底什麼是面向對象?javascript

面向對象程序設計(Object Oriented Programming,OOP):是一種計算機編程架構。OOP 的一條基本原則是計算機程序由單個可以起到子程序做用的單元或對象組合而成。OOP 達到了軟件工程的三個主要目標:重用性、靈活性和擴展性,其中核心概念是類和對象。java

瞭解了什麼是面向對象,也知道了面向的核心概念是類和對象,那到底什麼是類?什麼是對象?類和對象的關係是什麼?typescript

對象:人們研究所研究的事物自己,就是對象,例如一個具體的人、一棵樹、一隻狗、一條規則等等。對象包含自身屬性,方法,實現數據和操做的結合。ps 學數據結構的時候,也看到過這句話,數據結構就是 數據 + 操做編程

:對相同的特性和行爲的對象的抽象,例如 Person、Tree、Dog、Rule 等等,其中包含數據的形式和操做。類的實例就是對象瀏覽器

對象和類的關係:能夠理解爲 類是模具,對象是根據模具創造的產品babel

// 這是一個Person類
// 對人的抽象,定義了人的特性,例如名字,年齡,身高,體重等等的數據形式,也定義了人的行爲,例如說話,跑步等等
class Person {
  name: string;
  age: number;
  height: number;
  weight: number;
  ...

  talk(){
    console.log('speak english');
  }
  run(){}
  ...
}

// keven 是一個對象,他是一個具體的人,他包含名字,年齡,身高...,也能夠說話,跑步...
const keven:Person = new Person();
複製代碼

面向對象三大特性

  1. 繼承:子類繼承父類,子類與父類表現得很像(繼承了父類的特性和行爲),固然子類也能夠包含本身的特性和行爲;數據結構

    // YellowPerson 繼承了 Person,則繼承了 Person 的特性和行爲
    // 雖然 YellowPerson 內部未聲明任何屬性和方法,但它已經具備 name,age,height...
    class YellowPerson extends Person {
      playTableTennis() {}
    }
    複製代碼
  2. 多態:子類重寫父類,繼承同一個父類的子類對同一個特性或行爲會表現得不一樣架構

    // 子類繼承了父類,但子類對同一個特性 talk 表現的不一樣,例如能夠說中文,能夠說非洲語
    class YellowPerson extends Person {
      talk() {
        console.log('說中文');
      }
    }
    
    class BlackPerson extends Person {
      talk() {
        console.log('說非洲語');
      }
    }
    複製代碼
  3. 封裝:內部實現細節對外部隱藏,使用屬性描述符來控制成員的訪問,屬性描述符通常有:private、protected、public函數式編程

    class Person {
      private assets: number; // 他有不少資產,除了他本身,並不想讓任何人知道
      protected houseKey: string; // 這我的不想讓外人知道本身家的鑰匙,除非是本身的家人,例如他的兒子
      public name: string; // 他的名字任何人均可以知道
    }
    複製代碼

Javascript"面向對象"

上面說到 Javascript 是能夠面向對象的,且面向對象的核心是類和對象,那麼類和對象在 Javascript 是如歌表現的?函數

Javascript 的對象

Javascript 的數據類型分爲:number,string,boolean,null,undefined,symbol 和 object,其中 object 就是咱們說的 Javascript 對象。因爲存在其餘的數據結構,因此 Javascript 並非全是對象,即在 Javascript 中,並不是萬物皆是對象

Javascript 對象有不少,例若有如下內置對象 Object,Array,Function,RegExp...,固然你還能夠本身建立對象,經常使用的有如下方式

const obj1 = {};
const obj2 = new Object(); // 構造調用,用的不多
const obj3 = new Person(); // 構造調用
const obj4 = Object.create(null);
複製代碼

Javascript 對象屬性、方法

屬性和方法

你可使用 "." 或 "[]" 來訪問屬性、方法。咱們通常會對對象的成員加以區分:成員值爲函數的稱爲方法,值爲非函數稱爲屬性,這是按照其餘面向對象的語言來稱呼的。

可是在 Javascript 中,一個函數其實不會屬於某個對象(其實僅僅是一個引用),即該函數不會是某個對象的方法,因此對方法這個稱呼不是十分嚴謹,它僅僅是在進行對象屬性訪問的時候,返回值是函數罷了《你不知道的 Javascript》。

有幾個注意點

  1. "." 和 "[]" 區別:
    • "." 通常稱爲屬性訪問,且屬性必須知足命名規範;"[]" 通常稱爲鍵訪問,鍵名可接受任意的 utf-8/unicode 字符串
    • "." 屬性名只能爲常量;"[]" 能夠爲變量
    • "[]" 在 ES6 中可用於計算屬性
    • "." 和 "[]" 在 AST 是不同的,. => PropertyAccessExpression; [] => ElementAccessExpression
[[GET]],[[PUT]]

對象的獲取屬性設置屬性操做。這裏注意一點,和 Getter、Setter 不同的是 [[GET]]、[[PUT]] 是針對對象的操做,Getter、Setter 是針對對象的某個屬性的操做

由下面代碼僞裝模擬一個[[GET]] 和 [[PUT]],須要注意的是,[[GET]] 和 [[PUT]]並不是只關注本對象,還要按照原型鏈往上查找

const obj = {};

const proxy = new Proxy(obj, {
  get() {}, // 僞裝當成[[GET]]
  set() {}, // 僞裝當成[[PUT]]
});
複製代碼
屬性描述符

須要注意的是:屬性描述符 configurable、enumerable、writable 默認都是 false

  1. 公共屬性描述符: configurable、enumerable

    {
      configurable: false, // 該屬性沒法被改變,沒法刪除,沒法從新設置屬性描述,且對沒法進行的操做靜默失敗。ps 嚴格模式下就會報錯哦
      enumerable: false, // 該屬性沒法被遍歷。ps 有些方法仍是能夠獲取到的,例如 Reflect.ownKeys(),Object.getOwnPropertyNames/getOwnPropertySymbols()
    }
    複製代碼
  2. 互斥屬性描述符,如下兩組屬性描述符互斥

    • get、set:對獲取、設置屬性攔截

      {
        get(){},
        set(){},
      }
      複製代碼
    • writable、value

      {
          writable: false, // 該屬性沒法被修改,若是進行修改則靜默錯誤。ps 嚴格模式下就會報錯哦
          value: xx,
        }
      複製代碼
存在性檢測

如下方法對對象的屬性、方法進行存在檢測:

是否檢測原型 是否包含 enumerable=false 是否包含 symbol
in
Reflect.has
hasOwnProperty
迭代

如下方法對對象的迭代

是否遍歷原型 是否遍歷 enumerable=false 是否遍歷 symbol
for...in
Object.keys()
getOwnPropertyNames
getOwnPropertySymbols
Reflect.ownKeys()

Javascript 的原型

每一個 Javascript 對象,都有一個 [[Prototype]] 的特殊屬性, 這就是對象的原型。[[Prototype]] 本質上是對其餘對象的引用

在有的瀏覽器,可使用 __proto__ 訪問該屬性,好比 Google,但按照 ECMAScript 標準,則需使用 Object.getPrototypeOf() 訪問

原型鏈:每一個對象都存在特殊屬性 [[Prototype]],[[Prototype]] 指向另外一個對象,另外一個對象也存在 [[Prototype]] 屬性...直到爲 null 結束,由此對象組成的原型連接就是原型鏈

原型鏈查找:在 [[GET]] 時,若是當前對象不存在該屬性,且該對象的 [[Prototype]] 不爲空,則會繼續沿着 [[Prototype]] 引用的對象繼續查找,直到找到該屬性或對象爲空爲止

prototype 和 [[Prototype]] 區別

函數也存在 prototype 屬性,且在 new 構造調用時,生成的對象能夠訪問 prototype 屬性、方法,因爲都叫原型,因此這裏對他們進行區分:

prototype 用於構造函數中,用於模擬的
[[Prototype]]用於對象中,用於實例。ps 函數也是對象,全部函數即有 prototype,也有 [[Prototype]]

function Person {}
Person.prototype.xx = xx;

// person 經過 [[Prototype]] 指向 Person.prototype ,從而訪問 prototype 對象
var person = new Person();
複製代碼

Javascript 的"類"

在 ES6 以前,Javascript 沒有類的概念,對象都是由 new 構造調用 構造函數 生成對象。在 ES6 以後,可使用 class 了,那麼是否是意味着 Javascript 在 ES6 後就有類了呢?

Javascript "類" 和其餘語言類的區別

這裏首選明確一個概念:Javascript 沒有類,Javascript 中的 class 也只是模擬類的而已(可使用 typescript 或 babel 編譯一下,查看編譯後的代碼)。也能夠這樣理解,Javascript 一直使用語法糖來裝成有類的樣子,其實並無。下面會從幾個方面來講明這個

實例化:類 => 對象

:在面向對象中,類是一個模具,經過模具生成事物的步驟叫作實例化,例如 new Person()。生成對象後對象和類互不影響,且對象之間互不影響
Javascript"類":Javascript 生成對象時,依靠的是構造調用,而非類的實例化,例如 new Person()。Javascript 生成對象不須要依靠類,而是直接生成,生成後,經過 [[Prototype]] 來模擬類,因爲 [[Prototype]] 是 prototype 的引用,因此對象和類、對象之間能夠互相影響

// cpp類

class Person {
 public:
  int age;

  // 構造函數
  Person(int _age) { this->age = _age; }
};

Person *p = new Person(22);
複製代碼
// js"類"

// 構造函數
function Person(age) {
  this.age = age;
}
Person.prototype.getAge = function () {
  return this.age;
};
Person.prototype.ind = [1, 2, 3];

var p = new Person(22); // new 爲構造調用
p.ind.push(4);

var p2 = new Person(23);
p2.ind; // [1, 2, 3, 4],對象之間互相影響了
複製代碼

Javascript new 簡單實現

function newSelf(construct, ...args) {
  var sc = Object.create(construct.prototype); // [[Prototype]]賦值
  // 也可使用 var sc = Object.setPrototypeOf({}, construct.prototype);
  var result = construct.call(sc, ...args); // 構造函數運行

  return typeof result === 'object' && result !== null ? result : sc;
}
複製代碼
繼承:父類 => 子類

:繼承後,子類繼承父類的屬性和方法
Javascript"類":繼承後,子類並非繼承父類的屬性和方法,而是依靠 [[Prototype]] 去訪問父類的 prototype

// cpp 類繼承
class YellowPerson : public Person {
 public:
  YellowPerson(int _age) : Person(_age) { this->age = _age; }
};

YellowPerson *yp = new YellowPerson(22);
複製代碼
function YellowPerson(age) {
  Person.call(this, age); // 構造函數繼承
}

// 原型繼承
YellowPerson.prototype = new Person();
YellowPerson.prototype.constructor = YellowPerson;

var yp = new YellowPerson(22);
複製代碼
多態

:經過函數重載實現多態
Javascript"類":經過 [[GET]] 操做時,若是已找到該屬性,則不會沿着 [[Prototype]] 繼續查詢的特性

Javascript"面向委託"

面向委託:某些對象在自身沒法尋找屬性和方法時,把該請求委託給另外一個對象《你不知道的 Javascript》。

var person = {
  showAge() {
    return this.age;
  },
};

var yellowMan = Object.create(Person);
yellowMan.age = 22; // yellowMan 本身不包含 age 屬性,依靠 [[Prototype]] 訪問 person 的 age 屬性
複製代碼

我的感受面向委託在 Javascript 中和麪向對象表現差很少,只是在如下有點小區別:

  • 在面向對象中:利用父類(Person)保存屬性和方法,再利用多態來實現不一樣的操做
  • 在面向委託中:最好將狀態保存在委託者上(YellowPerson),而不是委託對象(Person)

Javascript 的繼承

// 父類
function Person(name) {
  this.name = name;
  this.ind = [1, 2, 3];
}
Person.prototype.getName = function () {
  return this.name;
};
複製代碼

構造函數繼承

function YellowPerson(name) {
  Person.call(this, name);
}
複製代碼

優勢

  1. 子類之間屬性不共用,即便爲引用數據類型也是不共用的
  2. 構造函數能夠傳參數

缺點

  1. 沒法獲取父類的 prototype 上定義的方法和屬性

原型鏈繼承

function YellowPerson() {}

YellowPerson.prototype = new Person();
YellowPerson.prototype.constructor = YellowPerson;
複製代碼

切記切記不要這樣搞 YellowPerson.prototype = Person.prototype,這樣會形成子類和父類的 prototype 指向同一個對象,若是子類修改 prototype,則其餘類(父類和其餘繼承過父類的子類)都會發生改變

優勢

  1. 可獲取父類的 prototype 上定義的方法和屬性

缺點

  1. 構造函數沒法傳參
  2. 子類雖然能訪問到父類的屬性,但子類共用父類的屬性,若是是基本類型還好說,若是引用類型,則會互相影響
  3. 須要修復 constructor 屬性

組合繼承

function YellowPerson(name) {
  Person.call(this, name);
}
YellowPerson.prototype = new Person();
YellowPerson.prototype.constructor = YellowPerson;
複製代碼

優勢

  1. 可獲取父類的 prototype 上定義的方法和屬性
  2. 子類屬性不共用,即便爲引用數據類型也是不共用的
  3. 構造函數能夠傳參數

缺點

  1. 父類運行了兩次,多餘父類實例
  2. 須要修復 constructor 屬性

寄生組合繼承

function YellowPerson(name) {
  Person.call(this, name);
}
YellowPerson.prototype = Object.create(Person.prototype); // Object.setPrototypeOf 也能夠
YellowPerson.prototype.constructor = YellowPerson;
複製代碼

優勢

  1. 可獲取父類的 prototype 上定義的方法和屬性
  2. 子類屬性不共用,即便爲引用數據類型也是不共用的
  3. 構造函數能夠傳參數
  4. 父類只運行一次,無多餘實例

缺點

  1. 須要修復 constructor 屬性
相關文章
相關標籤/搜索