面向對象編程是將事物當作一個個對象,對象有本身的屬性有本身的方法。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
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
改變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
}
});
複製代碼
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 能夠經過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
構造函數會忽略參數。