JS 中的物件導向 prototype class

wopen發表於2019-03-02

物件導向程式設計是將事物看成一個個物件,物件有自己的屬性有自己的方法。

比如人,我們先定義一個物件模板,我們可以定義一些屬性 比如,名字年齡和功能,比如走路。我們把這個叫做類。

然後幫們將具體資料傳入模板,成為一個個具體的人,我們將它叫做例項。

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 個步驟,

  1. 建立一個新物件
  2. 將建構函式的的作用域賦給新物件
  3. 執行程式碼
  4. 返回新物件,例項會儲存著一個 constructor 屬性,該屬性指向建構函式

我們也可以將walk函式寫在建構函式中this.walk=function(){},但是這樣寫的話,每新建一個例項,例項都會新建一個walk函式,這樣就浪費記憶體空間,我們將它放在prototype上這樣就會讓所有例項共享一個walk函式,但是如果都寫了它會呼叫自己的walk函式而不是共享的。

每一個函式都有一個prototype屬性,函式的prototype物件上的屬性方法,所有例項都是共享的。

prototype物件有個constructor屬性,它指向它的建構函式。

JS 中的物件導向 prototype class

當建立一個例項時,例項內有會有個[[Prototype]]指標指向建構函式的原型物件,在瀏覽器中檢視顯示為__proto__屬性。

當例項訪問一個屬性或者呼叫一個方法,比如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建構函式會忽略引數。

相關文章