物件導向,搞定物件

你的聲先生發表於2019-07-19

前言:此物件,非彼物件。今天想跟大家分享一下一個大的概念,物件導向。本來想繼續分享ES6的class,搜尋文章,發現我ES5的原型方面的知識並不牢靠。所以,為了能讓自己更加深刻的瞭解ES6中的class,還是回過頭來,先認認真真的學習ES5的物件導向的知識。在這裡,還是想表達一下對 --JavaScript高階程式--由衷的感謝,每次閱讀,都能讓我體會到不同的感受。這本書真的是前端開發工作人員的聖經。希望我的解讀能讓你對物件導向設計的更加深刻的瞭解。

什麼是物件

前端的小夥伴對物件的認識肯定相當的深刻的,因為我們每天的開發就時時刻刻在建立、應用這物件。可以說,每一個需求、功能的實現,都離不開物件。(還沒物件的抓緊搞物件!)

var person = new Object;
person.name = 'hanson';
person.age = 18 ;
person.sayName = function (){
    return this.name
}
複製程式碼

上面的程式碼是最基本的建立物件的方法,平時開發中,基本上不會這麼寫。

var person = {
   name: 'hanson',
   age: 18,
   sayName:function(){
       return this.name;
   }
}
複製程式碼

上面的建立物件的方式,是我們開發中經常用到的。

屬性型別

ECMA-262定義了一些為實現JavaScript引擎用的屬性,不能直接訪問,為了表示特殊,把他們放在兩對兒方括號中。例如[[[Enumerable]]。

資料屬性

一般資料屬性的值都為基礎資料型別。

  • [[Configurable]] 表示能否通過delete刪除屬性從而從新定義...(一旦修改,不能反悔)
  • [[Enumerable]] 表示能否通過for-in迴圈返回屬性...(預設為true)
  • [[Writable]] 表示能否修改屬性值...(false:不能改,true:能改)
  • [[Value]] 包含屬性的資料值...(就是上例中的‘hanson’)

如果想修改預設屬性呢?

ECMAScript給我提供了一個方法,Object.defineProperty()方法。這個方法接受三個引數:屬性所在物件、屬性的名字和一個描述物件。舉個例子就明白了。

var person = {};
Object.defineProperty(person,'name',{
    Writable:false,
    value:'hanson'
})
console.log(person.name) //'hanson'
person.name = 'hansheng';
console.log(person.name) // 'hanson'
複製程式碼

通俗易懂,不允許修改,那你就改不了。完成還是建議大家看書,這裡就不過多去解讀了,畢竟還要找物件。。。

訪問器屬性

除了資料屬性前兩了屬性,多了[[get]]和[[set]]兩個屬性。
不用看,就單從字面上,想象一下,也能明白,這是設定和獲取值。

  • [[get]]: 在讀取屬性時,呼叫的函式,預設undefined。
  • [[set]]: 在寫入屬性時,呼叫的函式,預設undefeated。

舉個例子都懂了!

var book = {
    _year:2018,
    edition:1
};
Object.defineProperty(book,"year",{
    get : function(){
        return this._year;
    }
    set : function(newValue){
        if(newValue > 2018){
            this._year = newValue;
            this.edition += newValue - 2018
        }
    }
})
book.year = 2019;
console.log(book.edition) //2
複製程式碼

那如果想獲取一下這些描述屬性呢?ECMAScript也給我們提供了一個方法:Object.getOwnPropertyDescriptor();

var hanson = {};
Object.defineProperties(book,{ //給物件定義多個屬性,用到Object.defineProperties();
    _year:{
        value:18
    },
    name:{
        value:'hanson'
    }
})
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value) // 18
複製程式碼

建立物件

雖然上面兩種建立物件的方式,都可以用來建立單個物件,但是有個明顯的缺點,使用同一介面建立很多物件,會產生大量的重複程式碼。 (建立個person、建立個book。。。建立無數物件)

工廠模式

工廠模式抽象了建立具體物件的過程。來看例子。

function creatObj(name,age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sayName = function(){
        return this.name;
    }
    return obj;
}
var person1 = creatObj('hanson',18);
var person2 = creatObj('hansheng','20');
複製程式碼

高階程式一書說,這種模式雖然解決了建立多個相似物件的問題,但是沒有解決物件識別的問題。為什麼沒解決物件識別問題?很簡單。

console.log(person1.constructor) //Object
console.log(person2.constructor) //Obejct
console.log(person1.constructor === person2.constructor) //false
複製程式碼

執行相同的邏輯,例項的建構函式應相同。都能得到Object是所有的實力的建構函式都是Object。為了解決上面的問題,出現了建構函式模式。 說到底,還是因為,每次呼叫都建立了一個新的物件,person1與person2是相對獨立的兩個個體。

建構函式模式

為了解決上述問題,出現了建構函式。

function Person (name,age){
 // let this = new Object()
    this.name = name;
    this.age = age;
    this.sayName = function(){
        return this.name;
    }
    //return this
}
var person1 = new Person('hanson',18);
var person2 = new Person('hansheng',20);
複製程式碼

對比:

  • 沒有顯式建立物件;
  • 直接將屬性和方法賦給了this;
  • 沒有return語句;

要建立Person例項,要使用New操作符。呼叫建構函式的過程如下:

  • 使用new這個關鍵詞來建立物件
  • 在建構函式內部把新建立出來的物件賦予給this
  • 在建構函式內部把新建立(將來new的物件)的屬性方法綁到this上
  • 預設是返回新建立的物件(很重要)
  • 特別需要注意的是,如果顯式return一個物件資料型別,那麼將來new的物件,就是顯式return的物件

我們來看一下是否解決了工廠模式的問題。

console.log(person1.constructor) // Person
console.log(person2.constructor) //Person
console.log(person1.constructor === person2.constructor) //true
複製程式碼

完美解決工廠模式的問題。

問你一下

第一個:
function Person1(name,age){
    this.name = name
    this.age = age
    
    return "1"
}
let p = new Person("hanson",18);
第二個:
function Person2(name,age){
    this.name = name
    this.age = age
    
    return [1,2,3]
}

let p = new Person("hanson",18)
複製程式碼

連個函式分別返回什麼?
答案是:
person1返回:{name:'hanson',age:18}
person2返回:[1,2,3]
解釋:
現在你能知道為什麼嗎?還記得使用建構函式建立person例項麼,預設情況,建構函式無return語句,預設返回一個物件(this),如果強行return,返回基礎資料型別,那麼執行後會自動忽略,如果返回物件,則直接返回,替換例項物件。
繼續。。。 難道建構函式就沒有缺點麼?當然不是,接著往下看。
還是剛才的例子。

function Person (name,age){
 // let this = new Object()
    this.name = name;
    this.age = age;
    this.sayName = function(){
        return this.name;
    }
    //return this
}
var person1 = new Person('hanson',18);
var person2 = new Person('hansheng',20);
複製程式碼

我們改造一下,可能看得更明白。

function Person (name,age){
// let this = new Object()
   this.name = name;
   this.age = age;
   this.sayName = new Function(){return this.name}
}
var person1 = new Person('hanson',18);
var person2 = new Person('hansheng',20);
console.log(person1.sayName === person2sayName) //false
複製程式碼

明顯,在呼叫sayName的時候,建立了新的函式,也就是物件,那麼person1.sayName與person2.sayName肯定不同。這樣,每次呼叫,我們都會新建立一個sayName函式。
有的同學問題,我們可以把這個函式單獨拿出來啊,用的時候再去呼叫就好了啊!

 function Person (name,age){
 // let this = new Object()
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(name){
    return this.name = name;
}
複製程式碼

這樣的方法可以暫時解決現在的需求,但是,如果在世紀開發中,我們需要呼叫更多的方法,難道我們都要寫到全域性中嗎?很明顯,這種方法在世紀的開發中,並不是最優解。怎麼辦呢?讓我更近一步,瞭解一下原型模式。

原型模式

如何解決呼叫建構函式內的方法時,每次都建立新函式或者要在全域性中建立多個函式的問題。我們引入原型模式,簡單明瞭還是來看程式碼。

function Person (){};
Person.prototype.name = 'hanson';
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    return this.name;
}
let person1 = new Person();
let person2 = new Person();
console.log(person1.sayName === person2.sayName) //true
複製程式碼

嘿嘿,解決上述問題。雖然解決了,但是我們得知道原因。

定義

我們建立的每一個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件,二這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。這個是官方給的解釋。我給你翻譯一下: 所有物件例項,可以共享原型物件上面的屬性和方法。
上面的例子,就證明了這段結論。person1和person2的sayName方法指向相同。為了加深理解,我們來看個圖。

物件導向,搞定物件
很清晰,雖然我也挺喜歡高階程式設計的圖,但是總覺得,這個更圓潤有沒有。 這個圖中,有一點沒有體現,就是,其實例項P也有一個屬性,[[prototype]],(很多瀏覽器的_proto_)在所有實現中,都無法訪問到。但是,我們可以通過isPrototype()方法來確定物件之間是否存在這種關係。

console.log(Person.prototype.isPrototype(person1) //true
複製程式碼

如果我們要給原型物件新增大量屬性方法時,我們不斷的Person.prototype.xxx = xxx、Person.prototype.xxxx = xxxx,這樣也是很繁瑣,那麼我們該怎麼解決這個問題?

function Person(name){
    this.name = name
}
// 讓Person.prototype指標指向一個新的物件
Person.prototype = {
    eat:function(){
        console.log('吃飯')
    },
    sleep:function(){
        console.log('睡覺')
    }
}
複製程式碼

需要注意的是,這樣的改變,雖然程式碼在可讀性,便利性方面有了很大的提高,但是,Person.prototype.constructor屬性不再指向Person了。因為,每建立一個函式,就會同時建立他的prototype屬性。這個Person.prototype也會建立屬於它的constructor屬性。我們這裡使用的語法,本質上完全重寫了prototype物件。解決的辦法很簡單,在Person.prototype物件上,加上contructor:Person就可以了。

Person.prototype = {
    constructor:Person,
    eat:function(){
        console.log('吃飯')
    },
    sleep:function(){
        console.log('睡覺')
    }
}
複製程式碼

和原型物件有關幾個常用方法

// 1.hasOwnProperty 在物件自身查詢屬性而不到原型上查詢
function Person(){
    this.name = 'hanson'
}

let p = new Person()

let key = 'name'
if((key in p) && p.hasOwnProperty(key)){
    // name僅在p物件中
}

// 2.isPrototypeOf 判斷一個物件是否是某個例項的原型物件
function Person(){
    this.name = 'hanson'
}

let p = new Person()

let obj = Person.prototype 
obj.isPrototypeOf(p) // true

複製程式碼

組合使用建構函式模式和原型模式

建立自定義型別的最常見方式,就是組合使用建構函式模式與原型模式。建構函式模式用於定義例項屬性,而原型模式用於定義方法和共享屬性。

 function Person (name,age){
 // let this = new Object()
    this.name = name;
    this.age = age;
    this.firends = ['xiaoqiang','xiaoyu','xian'];
}
Person.prototype = {
   constructor:Person,
   sayName:()=>this.name
}
let person1 = new Person('hanson',18);
let person2 = new Person('hansheng',19);
person1.friends.push('teacher sha');
console.log(person1.friends) //  ['xiaoqiang','xiaoyu','xian','teacher sha']
console.log(person2.friends) //  ['xiaoqiang','xiaoyu','xian']
console.log(person1.friends === person2.friends) //false;
console.log(person1.sayName == person2.sayName) //true
複製程式碼

在這個例子中,例項屬性都是在建構函式中定義的,而由所有例項共享的屬性constructor和方法sayName則是在原型中繫結的。目前在ESMAScript中,使用最廣發、認同度最高的一種建立自定義型別的方法。高階程式設計還介紹了寄生建構函式模式和 穩妥建構函式模式,感興趣的小夥伴可自行前往檢視。


“物件”都有了,一段時間後,是不是得考慮家產“繼承”的問題呢?彆著急,下一次,我們就來說說這個“家產”繼承!千萬不要錯過哦。如果覺得小編的內容能讓你有所啟發,至少能讓你加深一下記憶,記得給點贊、關注哦!有問題的小夥伴可以在下面留言,看到後,一定給回覆~我們下期再見嘍~

相關文章