關於ES6之面向對象的理解

JavaScript中,對象是一組無序的相關屬性和方法的集合,所有的事物都是對象,例如字符串、數值、數組、函數等。
對象是由屬性和方法組成的。

  • 屬性:事物的特徵,在對象中用屬性來表示(常用名詞)
  • 方法:事物的行爲,在對象中用方法來表示(常用動詞)

0. 創建對象的三種方式

0.1 使用對象字面量創建對象:

就是花括號 { } 裏面包含了表達這個具體事物(對象)的屬性和方法;{ } 裏面採取鍵值對的形式表示 
var star = {
    name : 'Jon',
    age : 18,
    sex : '男',
    sayHi : function(){
        alert('我好帥');
    }
};

0.2 利用 new Object 創建對象

// 創建空對象
var andy = new Obect();

通過內置構造函數Object創建對象,此時andy變量已經保存了創建出來的空對象。

// 通過對象操作屬性和方法的方式,來爲對象增加屬性和方法
andy.name = '張三';
andy.age = 18;
andy.sex = '男';
andy.sayHi = function(){
    alert('我好帥~');
}

注意:Object():第一個字母大寫

0.3 利用構造函數創建對象

構造函數:是一種特殊的函數,主要用來初始化對象,即爲對象成員變量賦初始值,它總與 new 運算符一起使用。我們可以把對象中一些公共的屬性和方法抽取出來,然後封裝到這個函數裏面。

構造函數的封裝格式

function 構造函數名(形參1,形參2,形參3) {
     this.屬性名1 = 參數1;
     this.屬性名2 = 參數2;
     this.屬性名3 = 參數3;
     this.方法名 = 函數體;
}

構造函數的調用格式

var obj = new 構造函數名(實參1,實參2,實參3)

以上代碼中,obj即接收到構造函數創建出來的對象。

注意事項:

  1. 構造函數約定首字母大寫(建議這樣操作)。
  2. 函數內的屬性和方法前面需要添加 this ,表示當前對象的屬性和方法。
  3. 構造函數中不需要 return 返回結果
  4. 當我們創建對象的時候,必須用 new 來調用構造函數

使用構造函數創建一個對象的過程:

  1. 在構造函數代碼開始執行之前,創建一個空對象;

  2. 修改this的指向,把this指向創建出來的空對象;

  3. 執行函數的代碼

  4. 在函數完成之後,返回創建出來的對象

1. 變量、屬性、函數、方法的區別

1.1 屬性和變量的區別:

屬性是對象的一部分,而變量不是對象的一部分,變量是單獨存儲數據的容器

變量:單獨聲明賦值,單獨存在

屬性:對象裏面的變量稱爲屬性,不需要聲明,用來描述該對象的特徵

1.2 方法和函數的區別:

方法是對象的一部分,函數不是對象的一部分,函數是單獨封裝操作的容器

函數:單獨存在的,通過“函數名()”的方式就可以調用

方法:對象裏面的函數稱爲方法,方法不需要聲明,使用“對象.方法名()”的方式就可以調用,方法用來描述該對象的行爲和功能。

2. 靜態成員和實例成員

我們知道,JS中的一切皆是對象,那麼構造函數其實也是個對象,同時通過new 構造函數也可以創造出對象,我們一般也稱這樣的對象是實例。

function Cat(n, m){
    this.name = n;
    this.age = m;
    this.climb = function(){
        console.log('跑的真快'); 
    }
}
var c1 = new Cat('貓', 3);
// 使用instanceof可以檢測一個對象是否是另一個對象的實例
console.log(c1 instanceof Cat);

像上面的代碼中,構造函數內使用this開頭設置的成員,在獲得實例的時候,自動成爲實例的成員。

// Cat 的構造函數
function Cat(n, m){
    this.name = n;
    this.age = m;
    this.climb = function(){
        console.log('跑的真快'); 
    }
}
// 爲構造函數對象動態添加成員(靜態成員)
Cat.jump = function(){
    console.log('我會跳');
}
var c1 = new Cat('貓', 3);
// 此時調用c1.jump()會報錯
// c1.jump();
// 實例成員不能被構造函數對象調用
// Cat.climb();

// 爲實例動態添加成員
c1.jump = function(){
    console.log('跳的真高');
}
c1.jump();
Cat.jump();
// 對於一個實例動態添加的成員並不會被其它實例共有
var c2 = new Cat('黑貓', 2)
// c2.jump();
// Tiger 的構造函數
function Tiger(){
    this.say = function(){
        console.log('我是森林之王');
    }
}
// 使用Cat的實例作爲原型
Tiger.prototype = c2;

var t1 = new Tiger();
// 原型中有的成員也是實例成員
t1.climb();

通過上面的代碼我們可以看出:

  1. 實例成員:構造函數中this上添加的成員和後期動態添加的成員
  2. 靜態成員:構造函數本身添加的成員或者後期動態添加的成員
  3. 對於一個實例動態添加的成員並不會被其它實例共有
  4. 如果構造函數A有原型,那麼構造函數A原型中的成員也是通過該構造函數A獲取的實例的實例成員

通俗來講:

實例成員:由構造函數創建出來的實例能直接訪問的屬性和方法,包括:實例本身的成員 以及 原型中的所有的屬性和方法 和 後期動態添加的成員

靜態成員:由構造函數直接訪問到的屬性和方法。

3. __proto__(原型)constructor(構造器)prototype(原型類型)

JS中, __proto__constructorprototype三者關係是比較難搞懂的,爲了搞懂它們,我們必須記住3個基本點:

  1. prototype 屬性是函數所獨有的,其它對象沒有。
  2. __proto__constructor 屬性是對象獨有,不過函數也是對象,因此函數也有這兩個屬性。
  3. __proto__ 並不是JS標準中規定的屬性,不同的瀏覽器解釋器實現的名字可能不一樣,在ES標準中它叫 [[Prototype]]
function Cat(){

}
var c1 = new Cat();
var o1 = {};

console.dir(Cat);
console.dir(c1);
console.dir(o1);

3.1 __proto__ 屬性

__proto__ 屬性 都是從一個對象指向一個對象,即指向它們的原型對象(可以理解爲父對象)。它的作用就是當訪問一個對象的屬性時,如果該對象內部不存在這個屬性,那麼就會去它的__proto__ 屬性所指向的那個對象(父對象)裏找,如果父對象也不存在這個屬性,則繼續往父對象的__proto__ 屬性所指向的那個對象(爺爺對象)裏找,依次類推,一直到最頂級對象,如果還找不到,那麼就會報錯。這個從當前對象一直到最頂級對象,通過__proto__ 屬性連起來的一條鏈,就是我們說的原型鏈。

3.2 prototype 屬性

prototype 屬性是函數獨有的屬性,它是從一個函數指向一個對象,表示函數的原型對象,通過new這個函數獲得到的實例對象的__proto__ 屬性都指向這個對象。

function Cat(){
    this.describe = '貓';
}
var c1 = new Cat();

function Tiger(){
    
}
// 指定原型
Tiger.prototype = c1;
var t1 = new Tiger();
// 他們兩個完全一樣,就是同一個對象
console.log(t1.__proto__ === Tiger.prototype);
var t2 = new Tiger();
// 他們兩個完全一樣,就是同一個對象
console.log(t2.__proto__ === Tiger.prototype);

由上面的代碼我們可以看出,其實prototype 屬性其實主要是爲了 共享(重用)成員(屬性和方法),通過該函數得到實例,都能共享(重用)到這些成員。試想一下,如果每一次繼承,都在內存中創建一份成員方法,那是多麼的消耗內存啊。

3.3 constructor 屬性

constructor 屬性也是對象才擁有的(其實是__proto__指向的對象或者prototype指向的對象擁有的),它是從一個對象指向一個函數,含義就是指向該對象的構造函數,

,有一些特殊的屬性的構造函數就是它自己本身。

// Cat
function Cat(){
    this.describe = '貓';
}
var c1 = new Cat();
console.log(c1);
console.log(c1.constructor === Cat)

// Tiger
function Tiger(){
    this.name = '老虎';
}
// 指定原型
Tiger.prototype = c1;
var t1 = new Tiger();
console.log(t1);
// 原型對象發生了覆蓋
console.log(t1.constructor === Cat)

// NortheastTiger
function NortheastTiger(){
    this.xxx = '東北虎';
}
// 指定原型
NortheastTiger.prototype = t1;
var n1 = new NortheastTiger();
console.log(n1)
// 原型對象發生了覆蓋
console.log(n1.constructor === Cat)

每個對象都可以找到其對應的constructor,因爲創建對象的前提是需要有constructor,而這個constructor可能是對象自己本身顯式定義的或者通過__proto__在原型鏈中找到的,故通過函數創建的對象即使自己沒有constructor屬性,它也能通過__proto__找到對應的constructor

constructor 修正

上面的代碼,可能很多人會在後面發生迷惑,我們看一下下面的代碼:

// Cat
function Cat() {
    this.describe = '貓';
}
var c1 = new Cat();
// Tiger
function Tiger() {
    this.name = '老虎';
}
// Tiger.prototype 指向的對象的 constructor 指向構造函數本身,也就是Tiger
console.log(Tiger.prototype);

如果我們改變了Tiger.prototype的指向,會造成原有的constructor丟失:

// Cat
function Cat() {
    this.describe = '貓';
}
var c1 = new Cat();
// Tiger
function Tiger() {
    this.name = '老虎';
}
// Tiger.prototype 指向的對象的 constructor 指向構造函數本身,也就是Tiger構造函數
console.log(Tiger.prototype);
Tiger.prototype = c1;
// 此時 Tiger.prototype 指向的對象 是 c1 , c1的constructor執行的是自己的構造函數,也就是Cat構造函數
// 因此通過new Tiger() 獲取到的對象的 constructor指向也爲Cat構造函數
console.log(Tiger.prototype);
// t1的constructor指向Cat構造函數
var t1 = new Tiger();
console.log(t1.constructor);

很明顯,這是有偏差的,我們可以手動將其修正:

function Cat() {
    this.describe = '貓';
}
var c1 = new Cat();
// Tiger
function Tiger() {
    this.name = '老虎';
}
// 從這裏開始出現偏差
Tiger.prototype = c1;
// 修正一下
Tiger.prototype.constructor = Tiger
// 再獲取的對象的constructor指向就正確了
var t2 = new Tiger();
console.log(t2.constructor);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章