物件導向程式設計是將事物看成一個個物件,物件有自己的屬性有自己的方法。
比如人,我們先定義一個物件模板,我們可以定義一些屬性 比如,名字年齡和功能,比如走路。我們把這個叫做類。
然後幫們將具體資料傳入模板,成為一個個具體的人,我們將它叫做例項。
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 個步驟,
- 建立一個新物件
- 將建構函式的的作用域賦給新物件
- 執行程式碼
- 返回新物件,例項會儲存著一個
constructor
屬性,該屬性指向建構函式
我們也可以將walk
函式寫在建構函式中this.walk=function(){}
,但是這樣寫的話,每新建一個例項,例項都會新建一個walk
函式,這樣就浪費記憶體空間,我們將它放在prototype
上這樣就會讓所有例項共享一個walk
函式,但是如果都寫了它會呼叫自己的walk
函式而不是共享的。
每一個函式都有一個prototype
屬性,函式的prototype
物件上的屬性方法,所有例項都是共享的。
prototype
物件有個constructor
屬性,它指向它的建構函式。
當建立一個例項時,例項內有會有個[[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
改變Cat
的this
指向,讓我們可以借用Animal
的建構函式,然後再讓Cat
的prototype
指向一個Animal
例項,並把constructor
修改正常。
如果我們初始化一個Cat
類,然後呼叫say
方法,那麼在內部的查詢流程是:
自身 -> 沿著[[prototype]]找到Cat.prototype(它是一個Animal例項)-> 沿著Animal例項的[[prototype]]查詢 -> 找到Animal.prototype(找到run方法並呼叫)
我們發現Cat.prototype = new Animal()
這樣就會讓Cat
的prototype多出name
和age
兩個屬性。
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函式對描述的屬性。描述符必須是這兩種形式之一;不能同時是兩者。
資料屬性
configurable
是否能配置此屬性,為false
時不能刪除,而且再設定時會報錯除了Writableenumerable
當且僅當該屬性的enumerable
為true
時,該屬性才能夠出現在物件的列舉屬性中value
包含了此屬性的值。writable
是否能修改屬性值
存取描述符
configurable
enumerable
get
讀取時呼叫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
建構函式會忽略引數。