JS 中的面向對象 prototype class

面向對象編程是將事物當作一個個對象,對象有本身的屬性有本身的方法。javascript

好比人,咱們先定義一個對象模板,咱們能夠定義一些屬性 好比,名字年齡和功能,好比走路。咱們把這個叫作類。java

而後幫們將具體數據傳入模板,成爲一個個具體的人,咱們將它叫作實例。編程

JS 中面向對象是使用原型(prototype)實現的。數組

function Person(name, age) {
    this.name = name
    this.age = age
    this.walk = function(){}
}

Person.prototype.walk = function () {}

var bob = new Person('bob', 10)
console.log(bob.age)
複製代碼

其中的Person函數叫作構造函數,構造函數通常會將第一個字母大寫, 構造函數建立特定類型的對象,構造函數中沒有,顯式的建立對象,和返回對象,直接將屬性賦值給 this瀏覽器

咱們使用new關鍵字建立對象實例,它會經歷 4 個步驟,bash

  1. 建立一個新對象
  2. 將構造函數的的做用域賦給新對象
  3. 執行代碼
  4. 返回新對象,實例會保存着一個 constructor 屬性,該屬性指向構造函數

咱們也能夠將walk函數寫在構造函數中this.walk=function(){},可是這樣寫的話,每新建一個實例,實例都會新建一個walk函數,這樣就浪費內存空間,咱們將它放在prototype上這樣就會讓全部實例共享一個walk函數,可是若是都寫了它會調用本身的walk函數而不是共享的。app

每個函數都有一個prototype屬性,函數的prototype對象上的屬性方法,全部實例都是共享的。函數

prototype對象有個constructor屬性,它指向它的構造函數。ui

當建立一個實例時,實例內有會有個[[Prototype]]指針指向構造函數的原型對象,在瀏覽器中查看顯示爲__proto__屬性。this

當實例訪問一個屬性或者調用一個方法,好比bob.walk(),內部會首先在自身上查找這個方法,若是找到的話就完成,若是沒有找到的話,就會沿着[[prototype]]向上查找,這就是爲何prototype上的方法都是共享,若是沿着[[prototype]]找到頭,還沒找到,那麼就會報錯bob.walk不是一個函數。

繼承

繼承主要是利用原型鏈,讓子類的prototype等於父類的實例,也就是利用實例尋找屬性和方法時,會沿着[[prototype]]向上找。

繼承就是,一個子類繼承父類的代碼,而不用從新編寫重複的代碼。好比咱們要寫Cat, Dog等類,咱們發現每一個類都有相似this.name = name; this.age = age這些重複的代碼,因此咱們能夠先寫一個Animal類,讓Cat,Dog繼承這個類,咱們就不用編寫重複的屬性和方法了。

function Animal(name) { this.name = name; this.age = 10 }
Animal.prototype.say = function () {
    console.log(this.name)
}
function Cat() { Animal.apply(this, arguments) }
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
複製代碼

咱們用apply改變Catthis指向,讓咱們能夠借用Animal的構造函數,而後再讓Catprototype指向一個Animal實例,並把constructor修改正常。

若是咱們初始化一個Cat類,而後調用say方法,那麼在內部的查找流程是:

自身 -> 沿着[[prototype]]找到Cat.prototype(它是一個Animal實例)-> 沿着Animal實例的[[prototype]]查找 -> 找到Animal.prototype(找到run方法並調用)

咱們發現Cat.prototype = new Animal()這樣就會讓Cat的prototype多出nameage兩個屬性。

function Animal(name) { this.name = name; this.age = 10 }
Animal.prototype.say = function () {
    console.log(this.name)
}
function Cat() { Animal.apply(this, arguments) }

function F(){}
F.prototype = Animal.prototype

Cat.prototype = new F()
Cat.prototype.constructor = Cat
複製代碼

咱們使用了一箇中間類函數F,讓它的prototype等於父級的prototype,那麼咱們查找到F.prototype時,就自動到了Animal.prototype上。

咱們若是想知道一個屬性是否是屬於自身而不是來自原型鏈則可使用

實例.hasOwnProperty(屬性) 查看該屬性是否來自自己。

Object.getOwnPropertyNames(obj) 返回全部對象自己屬性名數組,不管是否能枚舉

屬性 in 對象 判斷可否經過該對象訪問該屬性,不管是在自己仍是原型上

若是咱們想獲取一個對象的prototype,咱們可使用

Object.getPrototypeOf(obj) 方法,他返回對象的prototype

Object.setPrototypeOf(object, prototype) 方法,設置對象的prototype

還可使用對象的__proto__屬性獲取和修改對象的prototype(不推薦)

屬性描述符

在 js 中定義了只有內部才能用的特性,描述了屬性的各類特性。

對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。存取描述符是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是二者。

數據屬性

  1. configurable 是否能配置此屬性,爲false時不能刪除,並且再設置時會報錯除了Writable
  2. enumerable 當且僅當該屬性的enumerabletrue時,該屬性纔可以出如今對象的枚舉屬性中
  3. value 包含了此屬性的值。
  4. writable 是否能修改屬性值

存取描述符

  1. configurable
  2. enumerable
  3. get 讀取時調用
  4. set 寫入時調用

咱們可使用Object.defineProperty方法定義或修改一個對象屬性的特性。

var obj = {}

Object.defineProperty(obj, "key", {
  enumerable: false, // 默認爲 false
  configurable: false, // 默認爲 false
  writable: false, // 默認爲 false
  value: "static" // 默認爲 undefined
});

Object.defineProperty(obj, 'k', {
    get: function () { // 默認爲 undefined
        return '123'
    },
    set: function (v) {
        this.kk = v
    } // 默認爲 undefined
})
複製代碼

使用Object.getOwnPropertyDescriptor能夠一次定義多個屬性

var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});
複製代碼

class

ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,做爲對象的模板。經過class關鍵字,能夠定義類。

這樣編寫面向對象就更加的簡單。

和類表達式同樣,類聲明體在嚴格模式下運行。構造函數是可選的。

類聲明不能夠提高(這與函數聲明不一樣)。

class Person {
    age = 0 // 屬性除了寫在構造函數中也能夠寫在外面。
    static a = 0 // 靜態屬性

    constructor (name) { 
    // 構造函數,可選(若是沒有顯式定義,一個空的constructor方法會被默認添加)
        this.name = name
    } 
    
    // 類的內部全部定義的方法,都是不可枚舉的
    say () { // 方法 共享函數
        return this.name
    }
    
    static walk() { // 靜態方法
        
    }
}

typeof Person // "function"
Person === Person.prototype.constructor // true
複製代碼

使用的時候,也是直接對類使用new命令,跟構造函數的用法徹底一致,可是忘記加new會報錯。

靜態屬性和靜態方法,是屬於類的,而不是屬於實例的,要使用Person.walk()調用。

類的全部方法都定義在類的prototype屬性上面。

// 上面等同於

Person.prototype = {
  constructor() {},
  say() {}
};
Person.a = 0
Person.walk = function () {}
複製代碼

ES6 爲new命令引入了一個new.target屬性,該屬性通常用在構造函數之中,返回new命令做用於的那個構造函數。若是構造函數不是經過new命令或Reflect.construct()調用的,new.target會返回undefined,所以這個屬性能夠用來肯定構造函數是怎麼調用的。

function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必須使用 new 命令生成實例');
  }
}
複製代碼

Class 內部調用new.target,返回當前 Class

與函數同樣,類也可使用表達式的形式定義。

const AA = class A {}
// 這個類的名字是A,可是A只在內部用,指代當前類。在外部,這個類只能用AA引用
const BB = class {}

let person = new class { // 當即執行的 Class
  constructor(name) {
    this.name = name;
  }
}('張三');
複製代碼

Class 繼承

Class 能夠經過extends關鍵字實現繼承。

class Animal {
    constructor (name) {
        this.name = name
    }
}

class Cat extends Animal {
    constructor (...args) {
        super(...args) // 調用父類的 constructor 方法
                        // 必須調用且放在 constructor 最前面
    }
}
複製代碼

若是子類沒有定義constructor方法,這個方法會被默認添加。

class ColorPoint extends Point {
}

// 等同於
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}
複製代碼

父類函數的靜態屬性和方法也會繼承

super這個關鍵字,既能夠看成函數使用,也能夠看成對象使用。

super做爲函數時,只能用在子類的構造函數之中,用在其餘地方就會報錯。

super做爲對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。

在子類普通方法中經過super調用父類的方法時,方法內部的this指向當前的子類實例。

構造函數方法是不能繼承原生對象的,

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
複製代碼

可是 class 能夠繼承。這樣就能夠構造本身的Array子類。

能夠繼承了Object,可是沒法經過super方法向父類Object傳參。這是由於 ES6 改變了Object構造函數的行爲,一旦發現Object方法不是經過new Object()這種形式調用,ES6 規定Object構造函數會忽略參數。

相關文章
相關標籤/搜索