JavaScript的面向對象

JavaScript的對象

對象是JavaScript的一種數據類型。對象能夠當作是屬性的無序集合,每一個屬性都是一個鍵值對,屬性名是字符串,所以能夠把對象當作是從字符串到值的映射。這種數據結構在其餘語言中稱之爲「散列(hash)」、「字典(dictionary)」、「關聯數組(associative array)」等。html

原型式繼承:對象不只僅是字符串到值的映射,除了能夠保持自有的屬性,JavaScript對象還能夠從一個稱之爲原型的對象繼承屬性,對象的方法一般是繼承的屬性,這是JavaScript的核心特徵。面試

JavaScript對象是動態的—能夠新增屬性也能夠刪除屬性,可是他們經常使用來模擬靜態以及靜態類型語言中的「結構體」編程

建立對象

一、對象直接量segmentfault

建立對象最簡單的方式就是在JavaScript代碼中使用對象直接量。數組

var book = {
            "main title": 'guide',  //屬性名字裏有空格,必須加引號
            "sub-title": 'JS',  //屬性名字裏有連字符,必須加引號
            for: 'development',  //for是關鍵字,不過從ES5開始,做爲屬性名關鍵字和保留字能夠不加引號
            author: {
                firstname: 'David',  //這裏的屬性名就都沒有引號
                surname: 'Flanagan'
            }
        }

注意: 從ES5開始,對象直接量中的最後一個屬性後的逗號將被忽略。數據結構

擴展: [JavaScript中的關鍵字和保留字]app

二、經過new建立對象ide

new 運算符建立並初始化一個新對象。關鍵字new後跟一個函數調用。這裏的函數稱作構造函數(constructor),構造函數用以初始化一個新建立的對象。JavaScript中的數據類型都包含內置的構造函數。函數

var o = new Object(); //建立一個空對象,和{}同樣。 
var arr = new Array(); //建立一個空數組,和[]同樣。
擴展 1:new

new 是一個一元運算符,專門運算函數的。new後面調用的函數叫作構造函數,構造函數new的過程叫作實例化。
當new去調用一個函數 : 這個時候函數中的this就指向建立出來的對象,並且函數的的返回值直接就是this(隱式返回)
有一個默認慣例就是構造函數的名字首字母大寫。ui

注意:
當return的時候,若是是後面爲簡單類型,那麼返回值仍是這個對象;
若是return爲對象類型,那麼返回的就是return後面的這個對象。

擴展 2:基本類型和對象類型(複雜類型)的區別

賦值:
基本類型 : 賦值的時候只是值的複製
對象類型 : 賦值不只是值的複製,並且也是引用的傳遞(能夠理解爲內存地址)能夠理解爲賦址。

比較相等
基本類型 : 值相同就能夠
對象類型 : 值和引用都相同才行

擴展 3:原型 prototype

每個JavaScript對象(null除外)都和另外一個對象相關聯,這個對象就是原型,每個對象都從原型繼承屬性。

三、Object.create()

Object.create() 這個方法是ES5定義的,它建立一個新對象,其中第一個參數是這個對象的原型。第二個參數是可選參數,用以對對象屬性進行進一步描述。

能夠經過傳入參數 null 建立一個沒有原型的新對象,不過這個新對象不會繼承任何東西,甚至不包括基礎方法。
var o = Object.create(null); //o不會繼承任何屬性和方法,空空的。

若是想建立一個普通的空對象,須要傳入Object.prototype
var o = Object.create(Object.prototype); //o至關於{}

對象屬性的獲取和設置

能夠經過點(.)或方括號([])運算符來獲取和設置屬性的值。

var author = book.author;
var title = book["main title"];

在JavaScript中能用 . 鏈接的均可以用 []鏈接。有不少 . 運算符不能用的時候,就須要用[]代替。
一、在屬性名可變的狀況下用[]

function getAttr (obj, attr) {
    console.log(obj[attr])
}

二、屬性名有空格或者連字符等時用[]

var title = book["main title"];

刪除屬性

delete運算符能夠刪除對象的屬性。
delete只是斷開屬性和宿主對象的聯繫,而不會去操做屬性中的屬性,若是刪除的屬性是個對象,那麼這個對象的引用仍是存在的。

var a = {b:{c:1}};
var b = a.b;
console.log(b.c); // 1
console.log(a.b); // {c:1}
delete a.b;
console.log(b.c); // 1
console.log(a.b); //undefined

delete只能刪除自有屬性,不能刪除繼承屬性。

返回值

返回值爲true

當delete表達式刪除成功或沒有任何反作用(好比刪除不存在的屬性),或者delete後不是一個屬性訪問表達式,delete會返回 true ;

var a = {b:{c:1}};
console.log(delete a.b);
console.log(delete a.b);
console.log(delete a.toString);
console.log(delete 1);

以上都會打印true
返回值爲false

delete不能刪除那些可配置性爲false的屬性,例如某些內置對象的屬性是不可配置的,經過變量聲明和函數聲明建立的全局對象的屬性。

var a = {};
Object.defineProperty(a,'b',{
    value:1,
    configurable: false // 設置爲不可配置
})
console.log(delete a.b)
console.log(delete Object.prototype)
var x = 1;
console.log(delete this.x);
console.log(delete x)

以上打印都爲false

檢測屬性

in 運算符

in 運算符的左側是屬性名(字符串),右側是對象。若是對象的自有屬性或繼承屬性中包含這個屬性則返回true。

var a = {b:1};
console.log('a' in window); // true 聲明的全局變量'a'是window的屬性
console.log('b' in a); // true 'b'是a的屬性
console.log('toString' in a); // true a繼承了toString屬性
console.log('c' in a); // false 'c'不是a的屬性

跟in運算符相似的,還能夠用"!=="判斷一個屬性是不是undefined,可是有一種場景只能使用in運算符,in能夠區分不存在的屬性和存在但值爲undefined的屬性。

var a = {b:undefined};
console.log(a.b !== undefined); //false
console.log(a.c !== undefined); //false
console.log('b' in a); //true
console.log('c' in a); //false

hasOwnProperty

對象的hasOwnProperty()方法用來檢測給定的名字是不是對象的自有屬性。對於繼承屬性它將返回false

var a = {b:1};
console.log(a.hasOwnProperty('b')); //true
console.log(a.hasOwnProperty('c')); //false
console.log(a.hasOwnProperty('toString')); //false toString是繼承屬性

propertyIsEnumerable

對象的propertyIsEnumerable()方法只有檢測到是自身屬性(不包括繼承的屬性)且這個屬性的可枚舉性爲true時它才返回true。

var a = {b:1};
console.log(a.propertyIsEnumerable('b'));
console.log(a.propertyIsEnumerable('toString'));

包裝對象

當使用原始類型的值(string、number、boolean),在調用對應屬性和方法的時候,內部會自動轉成對應的對象。隱式建立的這個對象,就成爲包裝對象。
基本類型都有本身對應的包裝對象 : String Number Boolean

包裝對象的特色 隱式建立對象後,能夠調用對應的屬性和方法 使用後,立馬銷燬,因此不能給原始類型的值添加屬性和方法

其過程舉例:str.substring - > new String(1234) - > 找到String的substring -> 將new String銷燬

對象方法和屬性的彙總

Object靜態方法

Object的實例方法(定義在Object.prototype上的)

面向對象

編碼思想

兩種編程方式:
(1)、面向過程
(2)、面向對象

二者的區別:
面向過程:關注實現過程和每一步的實現細節。
面向對象:關注特徵和功能。

面向對象編程

通俗點,用對象的思想寫代碼就是面向對象編程。

基本特徵

一、抽象:抓住核心問題(簡單理解爲抽出像的部分;將相同或表現與問題相關特徵的內容提取出來。)
其核心:抽出、抽離,將相同的部分(可能會維護、會迭代、會擴展)的代碼抽離出來造成一類

二、封裝:就是將類的屬性包裝起來,不讓外界輕易知道它內部的具體實現;只提供對外接口以供調用

三、繼承:從已有對象上繼承出新的對象

四、多態:一個對象的不一樣形態

面向對象的好處

一、代碼的層次結構更清晰
二、更容易複用
三、更容易維護
四、更容易擴展

面向對象相關的屬性和概念

__proto__ 屬性原型鏈,實例對象與原型之間的鏈接,叫作原型鏈。

對象身上只有 proto 構造函數身上有prototype也有 proto

constructor 返回建立實例對象的構造函數的引用,每一個原型都會自動添加constructor屬性,for..in..遍歷原型是找不到這個屬性的。
var a = new A();
console.log(a.constructor == A) //true
hasOwnProperty 能夠用來判斷某屬性是否是這個構造函數的內部屬性(不包括繼承的)

語法: obj.hasOwnProperty(prop) 返回Boolean

function A (){
    this.b = 1;
}
var a = new A();
console.log(a.hasOwnProperty('b'));  //打印true 
console.log(a.hasOwnProperty('toString')); //toString是繼承屬性 打印 false
console.log(a.hasOwnProperty('hasOwnProperty')); //同上,打印false
nstanceof 二元運算符,用來檢測一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性。

語法: object instanceof constructor 即檢測 constructor.prototype 是否存在於參數 object 的原型鏈上。

// 定義構造函數
function C(){} 
function D(){} 

var o = new C();
o instanceof C; // true,由於 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,由於 D.prototype不在o的原型鏈上
o instanceof Object; // true,由於Object.prototype.isPrototypeOf(o)返回true
C.prototype instanceof Object // true,同上
toString 返回一個表示該對象的字符串

做用:
一、進行數字之間的進制轉換

例如:var num = 255;
alert( num.toString(16) ); //結果就是'ff'

二、利用toString作類型的判斷

例如:var arr = [];
alert( Object.prototype.toString.call(arr) == '[object Array]' );     彈出true
Object.prototype.toString.call()    獲得是相似於'[object Array]'  '[object Object]'

面向對象的寫法歷程

一、原始模式

假如咱們有一個對象是狗的原型,這個原型有「名字」和「顏色」兩個屬性。

var Dog = {
name: '',
color: ''
}
根據這個原型對象,咱們要生成一個實例對象以下

var hashiqi = {}; //建立空對象,以後根據原型對象的相應屬性賦值
hashiqi.name = 'hashiqi';
hashiqi.color = 'blackandwhite';

缺點:
一、若是要生成多個實例對象,要重複寫屢次。
二、實例和原型之間沒有聯繫。

二、工廠模式

上面原始模式有一個缺點是要很麻煩的寫不少重複的代碼,咱們能夠寫一個函數來解決代碼重複的問題。

function Dog(name, color) {
    var obj = {};
    obj.name = name;
    obj.color = color;
    return obj;
}

var hashiqi = Dog('hashiqi', 'blackandwhite');
var jinmao = Dog('jinmao', 'yellow');

這種方式只是解決了代碼重複的問題,可是生成的實例跟原型仍是沒有聯繫,並且hashiqi和jinmao也沒有聯繫,不能反映出他們是同一個原型對象的實例。

三、構造函數模式

用來建立對象的函數,叫作構造函數,其實就是一個普通函數,可是默認函數名首字母大寫,對構造函數使用new運算符,就能生成實例,而且this變量會綁定在實例對象上。

function Dog(name, color) {
    this.name = name;
    this.color = color;
}

var hashiqi = new Dog('hashiqi', 'blackandwhite');
var jinmao = new Dog('jinmao', 'yellow');
console.log(hashiqi.name); //hashiqi
console.log(jinmao.name); //jinmao

hasiqi 和 jinmao有一個共同的構造函數 hashiqi.constructor === jinmao.constructor 是true

有如下幾種方法能夠驗證原型對象與實例對象的關係:

hashiqi instanceof Dog; // true

Object.getPrototypeOf(hashiqi) === Dog.prototype // true

Dog.prototype.isPrototypeOf(hashiqi) // true

缺點:
構造函數解決了代碼重複和實例與原型之間的聯繫,可是存在一個浪費內存的問題。好比遠行對象有一些不變的屬性和通用的方法,這樣沒生成一個實例,都必須爲重複的東西多佔一些內存。

擴展

咱們能夠嘗試實現new運算符的邏輯以下:

function New(func) {
    var obj = {};

    //判斷構造函數是否存在原型,若是有實例的__proto__屬性就指向構造函數的prototype
    if(func.prototype !== undefined) {
        obj.__proto__ = func.prototype;
    }

    // 模擬出構造函數內部this指向實例的過程,注意,咱們會拿到構造函數的返回值
    var res = func.apply(obj, Array.from(arguments).slice(1));

    // 正常構造函數是不須要顯式聲明返回值的,默認的返回值是生成的實例,可是一旦在構造函數中return 一個不是對象或者函數,就會改變構造函數的默認的返回值,其餘的類型是不變的
    if(typeof res === 'object' && res !== null || typeof res === 'function') {
        return res;
    }

    return obj;
}

var taidi = New(Dog, 'taidi', 'gray');

注意:
正常的構造函數是不須要本身寫return 的,若是寫了,當return的時候,若是是後面爲簡單類型,那麼返回值仍是構造函數生成的實例。若是return爲對象類型或者函數,那麼返回的就是return後面的這個對象或者函數。

四、prototype模式

每個構造函數都有 prototype 屬性,這個屬性指向的是一個對象,這個對象的全部屬性和方法,都會被構造函數的實例繼承。
基於這個屬性,咱們就能夠有選擇性的將一些通用的屬性和方法定義到 prototype 上,每個經過 new 生成的實例,都會有一個 proto 屬性指向構造函數的原型即 prototype ,這樣咱們定義到構造函數原型對象的屬性和方法,就會被每個實例訪問到,從而變成公用的屬性和方法。

function Dog(name, color) {
    this.name = name;
    this.color = color;
}
Dog.prototype.say = function () {
    console.log("汪汪");
}

var hashiqi = new Dog('hashiqi', 'blackandwhite');
var jinmao = new Dog('jinmao', 'yellow');

hashiqi.say(); // 汪汪
jinmao.say(); // 汪汪
console.log(hashiqi.say === jinmao.say); // true

注意:當實例對象和原型對象有相同的屬性或者方法時,會優先訪問實例對象的屬性或方法。

面向對象的繼承

一、構造函數內部的屬性和方法繼承

使用call或apply方法,將父對象的構造函數綁定在子對象上。

//父類
function Animal() {
    this.species = '動物';
}

//子類
function Dog(name, color) {
    Animal.call(this);
    this.name = name;
    this.color = color;
}

var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.species); //動物

二、prototype相關的繼承

子類的prototype指向父類生成實例
function Animal() {};
Animal.prototype.species = '動物';
function Dog(name, color) {
    this.name = name;
    this.color = color;
}
Dog.prototype = new Animal();
//只要是prototype被徹底覆蓋,都得重寫constructor。
Dog.prototype.constructor = Dog;
var hashiqi = new Dog('hashiqi', 'blackandwhite');

缺點: 每一次繼承都得生成一個父類實例,比較佔內存。

利用空對象做爲中介
function Animal() {}
Animal.prototype.species = '動物';
function Dog(name, color) {
    this.name = name;
    this.color = color;
}
//Middle生成的是空實例(除了__proto__),幾乎不佔內存
function Middle() {}
Middle.prototype = Animal.prototype;
Dog.prototype = new Middle();
Dog.prototype.constructor = Dog;
var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.species);

幾個月前在 CSDN 面試的時候,我說了這種繼承方式,面試官就糾結這樣修改子類的prototype不會影響父類麼?是真的不會影響的,由於子類的prototype是指向Middle構造函數生成的實例,若是真的有心要改,得Dog.prototype.__proto__這麼着來改。

Object.create()
function Animal() {}
Animal.prototype.species = '動物';
function Dog(name, color) {
    this.name = name;
    this.color = color;
}
Dog.prototype = Object.create(Animal.prototype,{
    constructor: {
        value: Dog
    }
})

var hashiqi = new Dog('hashiqi','blackandwhite');
console.log(hashiqi.species); //動物

三、拷貝繼承

淺拷貝
function Animal() {}
Animal.prototype.species = '動物';
function Dog(name, color) {
    this.name = name;
    this.color = color;
}
function extend(child, parent) {
    var c = child.prototype;
    var p = parent.prototype;
    for(key in p) {
        c[key] = p[key]
    }
}
extend(Dog, Animal);
var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.species) // 動物
深拷貝
function deepCopy(parent, child) {
    var child = child || {};
    for(key in parent) {
        if(typeof parent[key] === 'object') {
            child[key] = parent[key].constructor === Array?[]:{};
            deepCopy(parent[key],child[key])
        } else {
            child[key] = parent[key];
        }
    }
    return child;
}

ES6的面向對象

上面所說的是JavaScript語言的傳統方法,經過構造函數,定義並生成新的對象。
ES6中提供了更接近傳統語言的寫法,引入了Class(類)的概念,經過class關鍵字,能夠定義類。

語法

ES6的類徹底能夠當作是構造函數的另一種寫法。

var method = 'say';
class Dog {
    constructor (name,color) {
        this.name = name;
        this.color = color;
    }
    //注意,兩個屬性之間跟對象不一樣,不要加逗號,而且類的屬性名可使用變量或者表達式,以下
    [method] () {
        console.log('汪汪');
    }
}
console.log(typeof Dog); // function 類的數據類型就是函數
console.log(Dog === Dog.prototype.constructor); // true 類自己就是構造函數

既然是構造函數,因此在使用的時候,也是直接對類使用new命令,跟構造函數的用法徹底一致。

var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.color); // blackandwhite

//上面採用表達式聲明的類的屬性能夠用一下兩種方式調用
hashiqi[method](); // 汪汪
hashiqi.say(); // 汪汪

注意:
一、先聲明定義類,再建立實例,不然會報錯
class 不存在變量提高,這一點與ES5的構造函數徹底不一樣

new Dog('hashiqi','blackandwhite')
class Dog {
    constructor (name,color) {
        this.name = name;
        this.color = color;
    }
}
//Uncaught ReferenceError: Dog is not defined
//上面代碼,Dog類使用在前,定義在後,由於ES6不會把類的聲明提高到代碼頭部,因此報錯Dog沒有定義。

二、必須使用new關鍵字來建立類的實例對象
類的構造函數,不使用new是無法調用的,會報錯。 這是它跟普通構造函數的一個主要區別,後者不用new也能夠執行。

class Dog {
    constructor (name,color) {
        this.name = name;
        this.color = color;
    }
}
Dog(); // Uncaught TypeError: Class constructor Dog cannot be invoked without 'new'

三、定義「類」的方法的時候,前面不須要加上function這個關鍵字,直接把函數定義放進去了就能夠了。而且,方法之間不須要逗號分隔,加了會報錯。

屬性概念

constructor 構造函數

構造方法constructor是一個類必需要有的方法,默認返回實例對象;建立類的實例對象的時候,會調用此方法來初始化實例對象。若是你沒有編寫constructor方法,執行的時候也會被加上一個默認的空的constructor方法。

constructor方法是必須的,也是惟一的,一個類體不能含有多個constructor構造方法。

class Dog {
    constructor (name,color) {
        this.name = name;
        this.color = color;
    }
    //定義了兩個constructor,因此會報錯
    constructor () {
        
    }
}
new Dog('hashiqi', 'blackandwhite')
//Uncaught SyntaxError: A class may only have one constructor
Class表達式

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

const Hashiqi = class Dog {
    constructor (name,color) {
        this.name = name;
        this.color = color;
    }
    getName () {
        //此處的Dog就是Dog構造函數,在表達式形式中,只能在構造函數內部使用
        console.log(Dog.name);
    }
}
var hashiqi = new Hashiqi('hashiqi', 'blackandwhite'); // 真正的類名是Hashiqi
var jinmao = new Dog('jinmao', 'yellow'); // 會報錯,Dog沒有定義

一般咱們的表達式會寫成以下,省略掉類後面的名稱

const Hashiqi = class {
    constructor (name,color) {
        this.name = name;
        this.color = color;
    }
}
var hashiqi = new Hashiqi('hashiqi', 'blackandwhite');
實例方法和靜態方法 實例化後的對象才能夠調用的方法叫作實例方法。 直接使用類名便可訪問的方法,稱之爲「靜態方法」

類至關於實例的原型,全部在類中定義的方法,都會被實例繼承。若是在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接經過類來調用,這就稱爲「靜態方法」。

class Dog {
    constructor (name,color) {
        this.name = name;
        this.color = color;
    }
    static say () {
        console.log('汪汪');
    }
}
Dog.say(); //汪汪

靜態方法和實例方法不一樣的是:靜態方法的定義須要使用static關鍵字來標識,而實例方法不須要;此外,靜態方法經過類名來的調用,而實例方法經過實例對象來調用。

類的繼承

extends

類之間能夠經過extends關鍵字實現繼承,這比ES5的經過修改原型鏈實現繼承,要清晰和方便不少。

class Dog extends Animal{}

extends的繼承目標
extends關鍵字後面能夠跟多種類型的值,有三種特殊狀況

一、子類繼承Object類

class A extends Object {}
console.log(A.__proto__ === Object) //true
console.log(A.prototype.__proto__ == Object.prototype) //true
//這種狀況下,A其實就是構造函數Object的複製,A的實例就是Object的實例。

二、不存在繼承

class A {}

console.log(A.__proto__ === Function.prototype) // true
console.log(A.prototype.__proto__ === Object.prototype) // true
//這種狀況下,A做爲一個基類(即不存在任何繼承),就是一個普通函數,因此直接繼承Funciton.prototype。
//可是,A調用後返回一個空對象(即Object實例),因此A.prototype.__proto__指向構造函數(Object)的prototype屬性。

三、子類繼承null

class A extends null {}
console.log(A.__proto__ === Function.prototype) //true
console.log(A.prototype) //只有一個constructor屬性,沒有__proto__屬性
這種狀況與第二種狀況很是像。A也是一個普通函數,因此直接繼承Funciton.prototype。
可是,A調用後返回的對象不繼承任何方法,因此沒有__proto__這屬性
super

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

一、super做爲函數調用時,表明父類的構造函數。做爲函數時,super()只能用在子類的構造函數之中,用在其餘地方就會報錯。

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

class Animal {
    constructor (name) {
        this.name = name;
        this.species = '動物';
    }
    say (){
        return this.species;
    }
}
class Dog extends Animal{
    constructor (name, color) {
        // 只要是本身在子類中定義constructor,必須調用super方法,不然新建實例會報錯
        //super做爲函數調用,只能用在子類的constructor中
        super(name);
        this.color = color;
    }
    getInfo () {
        //普通方法中,super指向父類的原型對象
        console.log(super.say()+': '+this.name +','+this.color);
    }
}
var hashiqi = new Dog('hashiqi', 'blackandwhite');
hashiqi.getInfo() //動物:hashiqi,balckandwhite

注意:
一、子類必須在constructor方法中調用super方法,不然新建實例時會報錯。這是由於子類沒有本身的this對象,而是繼承父類的this對象,而後對其進行加工。若是不調用super方法,子類就得不到this對象。

二、在子類的普通方法中,因爲super指向父類的原型對象,因此定義在父類實例上的方法或屬性,是沒法經過super調用的。

三、使用super的時候,必須顯式指定是做爲函數、仍是做爲對象使用,不然會報錯。

原文地址:https://segmentfault.com/a/1190000012728341

相關文章
相關標籤/搜索