前端基礎(chǔ)進(jìn)階(十一):詳解面向?qū)ο蟆?gòu)造函數(shù)、原型與原型鏈

.

如果要我總結(jié)一下學(xué)習(xí)前端以來(lái)我遇到了哪些瓶頸,那么面向?qū)ο笠欢ㄊ堑谝粋€(gè)會(huì)想到的。盡管現(xiàn)在對(duì)于面向?qū)ο笥辛艘恍┑牧私猓钱?dāng)初那種似懂非懂的痛苦,依然歷歷在目。

為了幫助大家能夠更加直觀的學(xué)習(xí)和了解面向?qū)ο螅視?huì)用盡量簡(jiǎn)單易懂的描述來(lái)展示面向?qū)ο蟮南嚓P(guān)知識(shí)。并且也準(zhǔn)備了一些實(shí)用的例子幫助大家更加快速的掌握面向?qū)ο蟮恼嬷B。

  • jQuery的面向?qū)ο髮?shí)現(xiàn)
  • 封裝拖拽
  • 簡(jiǎn)易版運(yùn)動(dòng)框架封裝

這可能會(huì)花一點(diǎn)時(shí)間,但是卻值得期待。

這篇文章主要來(lái)聊一聊關(guān)于面向?qū)ο蟮囊恍┲匾幕竟Α?/p>

一、對(duì)象的定義

在ECMAScript-262中,對(duì)象被定義為“無(wú)序?qū)傩缘募希鋵傩钥梢园局担瑢?duì)象或者函數(shù)”

也就是說(shuō),在JavaScript中,對(duì)象無(wú)非就是由一些列無(wú)序的key-value對(duì)組成。其中value可以是基本值,對(duì)象或者函數(shù)。

// 這里的person就是一個(gè)對(duì)象
var person = {
    name: 'Tom',
    age: 18,
    getName: function() {},
    parent: {}
}
創(chuàng)建對(duì)象

我們可以通過(guò)new的方式創(chuàng)建一個(gè)對(duì)象。

var obj = new Object();

也可以通過(guò)對(duì)象字面量的形式創(chuàng)建一個(gè)簡(jiǎn)單的對(duì)象。

var obj = {};

當(dāng)想要給我們創(chuàng)建的簡(jiǎn)單對(duì)象添加方法時(shí),可以這樣表示,

// 可以這樣
var person = {};
person.name = "TOM";
person.getName = function() {
    return this.name;
}

// 也可以這樣
var person = {
    name: "TOM",
    getName: function() {
        return this.name;
    }
}
訪問(wèn)對(duì)象的屬性和方法

假如我們有一個(gè)簡(jiǎn)單的對(duì)象如下:

var person = {
    name: 'TOM',
    age: '20',
    getName: function() {
        return this.name
    }
}

當(dāng)我們想要訪問(wèn)他的name屬性時(shí),可以用如下兩種方式訪問(wèn)。

person.name

// 或者
person['name']

如果想要訪問(wèn)的屬性名是一個(gè)變量時(shí),常常會(huì)使用第二種方式。例如我們要同時(shí)訪問(wèn)person的name與age,可以這樣寫(xiě):

['name', 'age'].forEach(function(item) {
    console.log(person[item]);
})

這種方式一定要重視,記住它以后在我們處理復(fù)雜數(shù)據(jù)的時(shí)候會(huì)有很大的幫助。

二、工廠模式

使用上面的方式創(chuàng)建對(duì)象很簡(jiǎn)單,但是在很多時(shí)候并不能滿足我們的需求。就以person對(duì)象為例,假如在實(shí)際開(kāi)發(fā)中,不僅僅需要一個(gè)名字叫做TOM的person對(duì)象,同時(shí)還需要另外一個(gè)名為Jake的person對(duì)象,雖然他們有很多相似之處,但是我們不得不重復(fù)寫(xiě)兩次。

var perTom = {
    name: 'TOM',
    age: 20,
    getName: function() {
        return this.name
    }
};

var perJake = {
    name: 'Jake',
    age: 22,
    getName: function() {
        return this.name
    }
}

很顯然這并不是合理的方式,當(dāng)相似對(duì)象太多時(shí),大家都會(huì)崩潰掉。

可以使用工廠模式解決這個(gè)問(wèn)題。顧名思義,工廠模式就是我們提供一個(gè)模子,然后通過(guò)這個(gè)模子復(fù)制出需要的對(duì)象。我們需要多少個(gè),就復(fù)制多少個(gè)。

var createPerson = function(name, age) {

    // 聲明一個(gè)中間對(duì)象,該對(duì)象就是工廠模式的模子
    var o = new Object();

    // 依次添加我們需要的屬性與方法
    o.name = name;
    o.age = age;
    o.getName = function() {
        return this.name;
    }

    return o;
}

// 創(chuàng)建兩個(gè)實(shí)例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);

相信上面的代碼并不難理解,也不用把工廠模式看得太過(guò)高大上。很顯然,工廠模式幫助我們解決了重復(fù)代碼上的麻煩,讓我們可以寫(xiě)很少的代碼,就能夠創(chuàng)建很多個(gè)person對(duì)象。但是這里還有兩個(gè)麻煩,需要我們注意。

第一個(gè)麻煩就是這樣處理,我們沒(méi)有辦法識(shí)別對(duì)象實(shí)例的類型。使用instanceof可以識(shí)別對(duì)象的類型,如下例子:

var obj = {};
var foo = function() {}

console.log(obj instanceof Object);  // true
console.log(foo instanceof Function); // true

因此在工廠模式的基礎(chǔ)上,我們需要使用構(gòu)造函數(shù)的方式來(lái)解決這個(gè)麻煩。

三、構(gòu)造函數(shù)

在JavaScript中,new關(guān)鍵字可以讓一個(gè)函數(shù)變得與眾不同。通過(guò)下面的例子,我們來(lái)一探new關(guān)鍵字的神奇之處。

function demo() {
    console.log(this);
}

demo();  // window
new demo();  // demo

為了能夠直觀的感受他們不同,建議大家動(dòng)手實(shí)踐觀察一下。很顯然,使用new之后,函數(shù)內(nèi)部發(fā)生了事情,讓this指向改變。

new關(guān)鍵字到底做了什么?之前在文章里我用文字大概表達(dá)了一下new的作用,但是一些同學(xué)好奇心很足,總期望用代碼實(shí)現(xiàn)一下,我就大概以我的理解來(lái)表達(dá)一下吧。

// 先一本正經(jīng)的創(chuàng)建一個(gè)構(gòu)造函數(shù),其實(shí)該函數(shù)與普通函數(shù)并無(wú)區(qū)別
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}

// 將構(gòu)造函數(shù)以參數(shù)形式傳入
function New(func) {

    // 聲明一個(gè)中間對(duì)象,該對(duì)象為最終返回的實(shí)例
    var res = {};
    if (func.prototype !== null) {

        // 將實(shí)例的原型指向構(gòu)造函數(shù)的原型
        res.__proto__ = func.prototype;
    }

    // ret為構(gòu)造函數(shù)執(zhí)行的結(jié)果,這里通過(guò)apply,將構(gòu)造函數(shù)內(nèi)部的this指向修改為指向res,即為實(shí)例對(duì)象
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));

    // 當(dāng)我們?cè)跇?gòu)造函數(shù)中明確指定了返回對(duì)象時(shí),那么new的執(zhí)行結(jié)果就是該返回對(duì)象
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }

    // 如果沒(méi)有明確指定返回對(duì)象,則默認(rèn)返回res,這個(gè)res就是實(shí)例對(duì)象
    return res;
}

// 通過(guò)new聲明創(chuàng)建實(shí)例,這里的p1,實(shí)際接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());

// 當(dāng)然,這里也可以判斷出實(shí)例的類型了
console.log(p1 instanceof Person); // true

JavaScript內(nèi)部再通過(guò)其他的一些特殊處理,將var p1 = New(Person, 'tom', 20); 等效于var p1 = new Person('tom', 20);。就是我們認(rèn)識(shí)的new關(guān)鍵字了。具體怎么處理的,我也不知道,別刨根問(wèn)底了,一直回答下去我太難了 - -!

老實(shí)講,你可能很難在其他地方看到有如此明確的告訴你new關(guān)鍵字到底對(duì)構(gòu)造函數(shù)干了什么的文章了。理解了這段代碼,你對(duì)JavaScript的理解又比別人深刻了一分,所以,一本正經(jīng)厚顏無(wú)恥求個(gè)贊可好?

當(dāng)然,很多朋友由于對(duì)于前面幾篇文章的知識(shí)理解不夠到位,會(huì)對(duì)new的實(shí)現(xiàn)表示非常困惑。但是老實(shí)講,如果你讀了我的前面幾篇文章,一定會(huì)對(duì)這里new的實(shí)現(xiàn)有似曾相識(shí)的感覺(jué)。而且我這里已經(jīng)盡力做了詳細(xì)的注解,剩下的只能靠你自己了。

但是只要你花點(diǎn)時(shí)間,理解了他的原理,那么困擾了無(wú)數(shù)人的構(gòu)造函數(shù)中this到底指向誰(shuí)就變得非常簡(jiǎn)單了。

所以,為了能夠判斷實(shí)例與對(duì)象的關(guān)系,我們就使用構(gòu)造函數(shù)來(lái)搞定。

var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}

var p1 = new Person('Ness', 20);
console.log(p1.getName());  // Ness

console.log(p1 instanceof Person); // true

關(guān)于構(gòu)造函數(shù),如果你暫時(shí)不能夠理解new的具體實(shí)現(xiàn),就先記住下面這幾個(gè)結(jié)論吧。

  • 與普通函數(shù)相比,構(gòu)造函數(shù)并沒(méi)有任何特別的地方,首字母大寫(xiě)只是我們約定的小規(guī)定,用于區(qū)分普通函數(shù);
  • new關(guān)鍵字讓構(gòu)造函數(shù)具有了與普通函數(shù)不同的許多特點(diǎn),而new的過(guò)程中,執(zhí)行了如下過(guò)程:
    1. 聲明一個(gè)中間對(duì)象;
    2. 將該中間對(duì)象的原型指向構(gòu)造函數(shù)的原型;
    3. 將構(gòu)造函數(shù)的this,指向該中間對(duì)象;
    4. 返回該中間對(duì)象,即返回實(shí)例對(duì)象。
四、原型

雖然構(gòu)造函數(shù)解決了判斷實(shí)例類型的問(wèn)題,但是,說(shuō)到底,還是一個(gè)對(duì)象的復(fù)制過(guò)程。跟工廠模式頗有相似之處。也就是說(shuō),當(dāng)我們聲明了100個(gè)person對(duì)象,那么就有100個(gè)getName方法被重新生成。

這里的每一個(gè)getName方法實(shí)現(xiàn)的功能其實(shí)是一模一樣的,但是由于分別屬于不同的實(shí)例,就不得不一直不停的為getName分配空間。這就是工廠模式存在的第二個(gè)麻煩。

顯然這是不合理的。我們期望的是,既然都是實(shí)現(xiàn)同一個(gè)功能,那么能不能就讓每一個(gè)實(shí)例對(duì)象都訪問(wèn)同一個(gè)方法?

當(dāng)然能,這就是原型對(duì)象要幫我們解決的問(wèn)題了。

我們創(chuàng)建的每一個(gè)函數(shù),都可以有一個(gè)prototype屬性,該屬性指向一個(gè)對(duì)象。這個(gè)對(duì)象,就是我們這里說(shuō)的原型。

當(dāng)我們?cè)趧?chuàng)建對(duì)象時(shí),可以根據(jù)自己的需求,選擇性的將一些屬性和方法通過(guò)prototype屬性,掛載在原型對(duì)象上。而每一個(gè)new出來(lái)的實(shí)例,都有一個(gè)__proto__屬性,該屬性指向構(gòu)造函數(shù)的原型對(duì)象,通過(guò)這個(gè)屬性,讓實(shí)例對(duì)象也能夠訪問(wèn)原型對(duì)象上的方法。因此,當(dāng)所有的實(shí)例都能夠通過(guò)__proto__訪問(wèn)到原型對(duì)象時(shí),原型對(duì)象的方法與屬性就變成了共有方法與屬性。

我們通過(guò)一個(gè)簡(jiǎn)單的例子與圖示,來(lái)了解構(gòu)造函數(shù),實(shí)例與原型三者之間的關(guān)系。

由于每個(gè)函數(shù)都可以是構(gòu)造函數(shù),每個(gè)對(duì)象都可以是原型對(duì)象,因此如果在理解原型之初就想的太多太復(fù)雜的話,反而會(huì)阻礙你的理解,這里我們要學(xué)會(huì)先簡(jiǎn)化它們。就單純的剖析這三者的關(guān)系。

// 聲明構(gòu)造函數(shù)
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 通過(guò)prototye屬性,將方法掛載到原型對(duì)象上
Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('tim', 10);
var p2 = new Person('jak', 22);
console.log(p1.getName === p2.getName); // true
圖示

通過(guò)圖示我們可以看出,構(gòu)造函數(shù)的prototype與所有實(shí)例對(duì)象的__proto__都指向原型對(duì)象。而原型對(duì)象的constructor指向構(gòu)造函數(shù)。

除此之外,還可以從圖中看出,實(shí)例對(duì)象實(shí)際上對(duì)前面我們所說(shuō)的中間對(duì)象的復(fù)制,而中間對(duì)象中的屬性與方法都在構(gòu)造函數(shù)中添加。于是根據(jù)構(gòu)造函數(shù)與原型的特性,我們就可以將在構(gòu)造函數(shù)中,通過(guò)this聲明的屬性與方法稱為私有變量與方法,它們被當(dāng)前被某一個(gè)實(shí)例對(duì)象所獨(dú)有。而通過(guò)原型聲明的屬性與方法,我們可以稱之為共有屬性與方法,它們可以被所有的實(shí)例對(duì)象訪問(wèn)。

當(dāng)我們?cè)L問(wèn)實(shí)例對(duì)象中的屬性或者方法時(shí),會(huì)優(yōu)先訪問(wèn)實(shí)例對(duì)象自身的屬性和方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        console.log('this is constructor.');
    }
}

Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('tim', 10);

p1.getName(); // this is constructor.

在這個(gè)例子中,我們同時(shí)在原型與構(gòu)造函數(shù)中都聲明了一個(gè)getName函數(shù),運(yùn)行代碼的結(jié)果表示原型中的訪問(wèn)并沒(méi)有被訪問(wèn)。

我們還可以通過(guò)in來(lái)判斷,一個(gè)對(duì)象是否擁有某一個(gè)屬性/方法,無(wú)論是該屬性/方法存在于實(shí)例對(duì)象還是原型對(duì)象。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('tim', 10);

console.log('name' in p1); // true

in的這種特性最常用的場(chǎng)景之一,就是判斷當(dāng)前頁(yè)面是否在移動(dòng)端打開(kāi)。

isMobile = 'ontouchstart' in document;

// 很多人喜歡用瀏覽器UA的方式來(lái)判斷,但并不是很好的方式

更簡(jiǎn)單的原型寫(xiě)法

根據(jù)前面例子的寫(xiě)法,如果我們要在原型上添加更多的方法,可以這樣寫(xiě):

function Person() {}

Person.prototype.getName = function() {}
Person.prototype.getAge = function() {}
Person.prototype.sayHello = function() {}
... ...

除此之外,我還可以使用更為簡(jiǎn)單的寫(xiě)法。

function Person() {}

Person.prototype = {
    constructor: Person,
    getName: function() {},
    getAge: function() {},
    sayHello: function() {}
}

這種字面量的寫(xiě)法看上去簡(jiǎn)單很多,但是有一個(gè)需要特別注意的地方。Person.prototype = {}實(shí)際上是重新創(chuàng)建了一個(gè){}對(duì)象并賦值給Person.prototype,這里的{}并不是最初的那個(gè)原型對(duì)象。因此它里面并不包含constructor屬性。為了保證正確性,我們必須在新創(chuàng)建的{}對(duì)象中顯示的設(shè)置constructor的指向。即上面的constructor: Person

五、原型鏈

原型對(duì)象其實(shí)也是普通的對(duì)象。幾乎所有的對(duì)象都可能是原型對(duì)象,也可能是實(shí)例對(duì)象,而且還可以同時(shí)是原型對(duì)象與實(shí)例對(duì)象。這樣的一個(gè)對(duì)象,正是構(gòu)成原型鏈的一個(gè)節(jié)點(diǎn)。因此理解了原型,那么原型鏈并不是一個(gè)多么復(fù)雜的概念。

我們知道所有的函數(shù)都有一個(gè)叫做toString的方法。那么這個(gè)方法到底是在哪里的呢?

先隨意聲明一個(gè)函數(shù):

function add() {}

那么我們可以用如下的圖來(lái)表示這個(gè)函數(shù)的原型鏈。

原型鏈

其中add是Function對(duì)象的實(shí)例。而Function的原型對(duì)象同時(shí)又是Object的實(shí)例。這樣就構(gòu)成了一條原型鏈。原型鏈的訪問(wèn),其實(shí)跟作用域鏈有很大的相似之處,他們都是一次單向的查找過(guò)程。因此實(shí)例對(duì)象能夠通過(guò)原型鏈,訪問(wèn)到處于原型鏈上對(duì)象的所有屬性與方法。這也是foo最終能夠訪問(wèn)到處于Object原型對(duì)象上的toString方法的原因。

基于原型鏈的特性,我們可以很輕松的實(shí)現(xiàn)繼承

六、繼承

我們常常結(jié)合構(gòu)造函數(shù)與原型來(lái)創(chuàng)建一個(gè)對(duì)象。因?yàn)闃?gòu)造函數(shù)與原型的不同特性,分別解決了我們不同的困擾。因此當(dāng)我們想要實(shí)現(xiàn)繼承時(shí),就必須得根據(jù)構(gòu)造函數(shù)與原型的不同而采取不同的策略。

我們聲明一個(gè)Person對(duì)象,該對(duì)象將作為父級(jí),而子級(jí)cPerson將要繼承Person的所有屬性與方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.getName = function() {
    return this.name;
}

首先我們來(lái)看構(gòu)造函數(shù)的繼承。在上面我們已經(jīng)理解了構(gòu)造函數(shù)的本質(zhì),它其實(shí)是在new內(nèi)部實(shí)現(xiàn)的一個(gè)復(fù)制過(guò)程。而我們?cè)诶^承時(shí)想要的,就是想父級(jí)構(gòu)造函數(shù)中的操作在子級(jí)的構(gòu)造函數(shù)中重現(xiàn)一遍即可。我們可以通過(guò)call方法來(lái)達(dá)到目的。

// 構(gòu)造函數(shù)的繼承
function cPerson(name, age, job) {
    Person.call(this, name, age);
    this.job = job;
}

原型的繼承,只需要將子級(jí)的原型對(duì)象設(shè)置為父級(jí)的一個(gè)實(shí)例,加入到原型鏈中即可。

// 繼承原型
cPerson.prototype = new Person(name, age);

// 添加更多方法
cPerson.prototype.getLive = function() {}
原型鏈

當(dāng)然關(guān)于繼承還有更好的方式。

七、更好的繼承

假設(shè)原型鏈的終點(diǎn)Object.prototype為原型鏈的E(end)端,原型鏈的起點(diǎn)為S(start)端。

通過(guò)前面原型鏈的學(xué)習(xí)我們知道,處于S端的對(duì)象,可以通過(guò)S -> E的單向查找,訪問(wèn)到原型鏈上的所有方法與屬性。因此這給繼承提供了理論基礎(chǔ)。我們只需要在S端添加新的對(duì)象,那么新對(duì)象就能夠通過(guò)原型鏈訪問(wèn)到父級(jí)的方法與屬性。因此想要實(shí)現(xiàn)繼承,是一件非常簡(jiǎn)單的事情。

因?yàn)榉庋b一個(gè)對(duì)象由構(gòu)造函數(shù)與原型共同組成,因此繼承也會(huì)分別有構(gòu)造函數(shù)的繼承與原型的繼承。

假設(shè)我們已經(jīng)封裝好了一個(gè)父類對(duì)象Person。如下。

var Person = function(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.getName = function() {
    return this.name;
}

Person.prototype.getAge = function() {
    return this.age;
}

構(gòu)造函數(shù)的繼承比較簡(jiǎn)單,我們可以借助call/apply來(lái)實(shí)現(xiàn)。假設(shè)我們要通過(guò)繼承封裝一個(gè)Student的子類對(duì)象。那么構(gòu)造函數(shù)可以如下實(shí)現(xiàn)。

var Student = function(name, age, grade) {
    // 通過(guò)call方法還原Person構(gòu)造函數(shù)中的所有處理邏輯
   Person.call(this, name, age);
    this.grade = grade;
}


// 等價(jià)于
var Student = function(name, age, grade) {
    this.name = name;
    this.age = age;
    this.grade = grade;
}

原型的繼承則稍微需要一點(diǎn)思考。首先我們應(yīng)該考慮,如何將子類對(duì)象的原型加入到原型鏈中?我們只需要讓子類對(duì)象的原型,成為父類對(duì)象的一個(gè)實(shí)例,然后通過(guò)__proto__就可以訪問(wèn)父類對(duì)象的原型。這樣就繼承了父類原型中的方法與屬性了。

因此我們可以先封裝一個(gè)方法,該方法根據(jù)父類對(duì)象的原型創(chuàng)建一個(gè)實(shí)例,該實(shí)例將會(huì)作為子類對(duì)象的原型。

function create(proto, options) {
    // 創(chuàng)建一個(gè)空對(duì)象
    var tmp = {};

    // 讓這個(gè)新的空對(duì)象成為父類對(duì)象的實(shí)例
    tmp.__proto__ = proto;

    // 傳入的方法都掛載到新對(duì)象上,新的對(duì)象將作為子類對(duì)象的原型
    Object.defineProperties(tmp, options);
    return tmp;
}

簡(jiǎn)單封裝了create對(duì)象之后,我們就可以使用該方法來(lái)實(shí)現(xiàn)原型的繼承了。

Student.prototype = create(Person.prototype, {
    // 不要忘了重新指定構(gòu)造函數(shù)
    constructor: {
        value: Student
    }
    getGrade: {
        value: function() {
            return this.grade
        }
    }
})

那么我們來(lái)驗(yàn)證一下我們這里實(shí)現(xiàn)的繼承是否正確。

var s1 = new Student('ming', 22, 5);

console.log(s1.getName());  // ming
console.log(s1.getAge());   // 22
console.log(s1.getGrade()); // 5

全部都能正常訪問(wèn),沒(méi)問(wèn)題。在ECMAScript5中直接提供了一個(gè)Object.create方法來(lái)完成我們上面自己封裝的create的功能。因此我們可以直接使用Object.create.

Student.prototype = create(Person.prototype, {
    // 不要忘了重新指定構(gòu)造函數(shù)
    constructor: {
        value: Student
    }
    getGrade: {
        value: function() {
            return this.grade
        }
    }
})

完整代碼如下:

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.getName = function() {
    return this.name
}
Person.prototype.getAge = function() {
    return this.age;
}

function Student(name, age, grade) {
    // 構(gòu)造函數(shù)繼承
    Person.call(this, name, age);
    this.grade = grade;
}

// 原型繼承
Student.prototype = Object.create(Person.prototype, {
    // 不要忘了重新指定構(gòu)造函數(shù)
    constructor: {
        value: Student
    }
    getGrade: {
        value: function() {
            return this.grade
        }
    }
})


var s1 = new Student('ming', 22, 5);

console.log(s1.getName());  // ming
console.log(s1.getAge());   // 22
console.log(s1.getGrade()); // 5
八、屬性類型

在上面的繼承實(shí)現(xiàn)中,使用了一個(gè)大家可能不太熟悉的方法defineProperties。并且在定義getGrade時(shí)使用了一個(gè)很奇怪的方式。

getGrade: {
    value: function() {
        return this.grade
    }
}

這其實(shí)是對(duì)象中的屬性類型。在我們平常的使用中,給對(duì)象添加一個(gè)屬性時(shí),直接使用object.param的方式就可以了,或者直接在對(duì)象中掛載。

var person = {
    name: 'TOM'
}

在ECMAScript5中,對(duì)每個(gè)屬性都添加了幾個(gè)屬性類型,來(lái)描述這些屬性的特點(diǎn)。他們分別是

  • configurable: 表示該屬性是否能被delete刪除。當(dāng)其值為false時(shí),其他的特性也不能被改變。默認(rèn)值為true
  • enumerable: 是否能枚舉。也就是是否能被for-in遍歷。默認(rèn)值為true
  • writable: 是否能修改值。默認(rèn)為true
  • value: 該屬性的具體值是多少。默認(rèn)為undefined
  • get: 當(dāng)我們通過(guò)person.name訪問(wèn)name的值時(shí),get將被調(diào)用。該方法可以自定義返回的具體值是多少。get默認(rèn)值為undefined
  • set: 當(dāng)我們通過(guò)person.name = 'Jake'設(shè)置name的值時(shí),set方法將被調(diào)用。該方法可以自定義設(shè)置值的具體方式。set默認(rèn)值為undefined

需要注意的是,不能同時(shí)設(shè)置value、writable 與 get、set的值。

我們可以通過(guò)Object.defineProperty方法來(lái)修改這些屬性類型。

下面我們用一些簡(jiǎn)單的例子來(lái)演示一下這些屬性類型的具體表現(xiàn)。

configurable

// 用普通的方式給person對(duì)象添加一個(gè)name屬性,值為TOM
var person = {
    name: 'TOM'
}

// 使用delete刪除該屬性
delete person.name;  // 返回true 表示刪除成功

// 通過(guò)Object.defineProperty重新添加name屬性
// 并設(shè)置name的屬性類型的configurable為false,表示不能再用delete刪除
Object.defineProperty(person, 'name', {
    configurable: false,
    value: 'Jake'  // 設(shè)置name屬性的值
})

// 再次delete,已經(jīng)不能刪除了
delete person.name   // false

console.log(person.name)    // 值為Jake

// 試圖改變value
person.name = "alex";
console.log(person.name) // Jake 改變失敗

enumerable

var person = {
    name: 'TOM',
    age: 20
}

// 使用for-in枚舉person的屬性
var params = [];

for(var key in person) {
    params.push(key);
}

// 查看枚舉結(jié)果
console.log(params);  // ['name', 'age']

// 重新設(shè)置name屬性的類型,讓其不可被枚舉
Object.defineProperty(person, 'name', {
    enumerable: false
})

var params_ = [];
for(var key in person) {
    params_.push(key)
}

// 再次查看枚舉結(jié)果
console.log(params_); // ['age']

writable

var person = {
    name: 'TOM'
}

// 修改name的值
person.name = 'Jake';

// 查看修改結(jié)果
console.log(person.name); // Jake 修改成功

// 設(shè)置name的值不能被修改
Object.defineProperty(person, 'name', {
    writable: false
})

// 再次試圖修改name的值
person.name = 'alex';

console.log(person.name); // Jake 修改失敗

value

var person = {}

// 添加一個(gè)name屬性
Object.defineProperty(person, 'name', {
    value: 'TOM'
})

console.log(person.name)  // TOM

get/set

var person = {}

// 通過(guò)get與set自定義訪問(wèn)與設(shè)置name屬性的方式
Object.defineProperty(person, 'name', {
    get: function() {
        // 一直返回TOM
        return 'TOM'
    },
    set: function(value) {
        // 設(shè)置name屬性時(shí),返回該字符串,value為新值
        console.log(value + ' in set');
    }
})

// 第一次訪問(wèn)name,調(diào)用get
console.log(person.name)   // TOM

// 嘗試修改name值,此時(shí)set方法被調(diào)用
person.name = 'alex'   // alex in set

// 第二次訪問(wèn)name,還是調(diào)用get
console.log(person.name) // TOM

請(qǐng)盡量同時(shí)設(shè)置get、set。如果僅僅只設(shè)置了get,那么我們將無(wú)法設(shè)置該屬性值。如果僅僅只設(shè)置了set,我們也無(wú)法讀取該屬性的值。

Object.defineProperty只能設(shè)置一個(gè)屬性的屬性特性。當(dāng)我們想要同時(shí)設(shè)置多個(gè)屬性的特性時(shí),需要使用我們之前提到過(guò)的Object.defineProperties

var person = {}

Object.defineProperties(person, {
    name: {
        value: 'Jake',
        configurable: true
    },
    age: {
        get: function() {
            return this.value || 22
        },
        set: function(value) {
            this.value = value
        }
    }
})

person.name   // Jake
person.age    // 22
讀取屬性的特性值

我們可以使用Object.getOwnPropertyDescriptor方法讀取某一個(gè)屬性的特性值。

var person = {}

Object.defineProperty(person, 'name', {
    value: 'alex',
    writable: false,
    configurable: false
})

var descripter = Object.getOwnPropertyDescriptor(person, 'name');

console.log(descripter);  // 返回結(jié)果如下

descripter = {
    configurable: false,
    enumerable: false,
    value: 'alex',
    writable: false
}
九、總結(jié)

關(guān)于面向?qū)ο蟮幕A(chǔ)知識(shí)大概就是這些了。我從最簡(jiǎn)單的創(chuàng)建一個(gè)對(duì)象開(kāi)始,解釋了為什么我們需要構(gòu)造函數(shù)與原型,理解了這其中的細(xì)節(jié),有助于我們?cè)趯?shí)際開(kāi)發(fā)中靈活的組織自己的對(duì)象。因?yàn)槲覀儾⒉皇撬械膱?chǎng)景都會(huì)使用構(gòu)造函數(shù)或者原型來(lái)創(chuàng)建對(duì)象,也許我們需要的對(duì)象并不會(huì)聲明多個(gè)實(shí)例,或者不用區(qū)分對(duì)象的類型,那么我們就可以選擇更簡(jiǎn)單的方式。

我們還需要關(guān)注構(gòu)造函數(shù)與原型的各自特性,有助于在創(chuàng)建對(duì)象時(shí)準(zhǔn)確的判斷我們的屬性與方法到底是放在構(gòu)造函數(shù)中還是放在原型中。如果沒(méi)有理解清楚,這會(huì)給我們?cè)趯?shí)際開(kāi)發(fā)中造成非常大的困擾。

最后接下來(lái)的幾篇文章,我會(huì)挑幾個(gè)面向?qū)ο蟮睦樱^續(xù)幫助大家掌握面向?qū)ο蟮膶?shí)際運(yùn)用。

下一篇:前端基礎(chǔ)進(jìn)階(十二):面向?qū)ο髮?shí)戰(zhàn)之封裝拖拽對(duì)象
上一篇:前端基礎(chǔ)進(jìn)階(十):深入詳解函數(shù)的柯里化
前端基礎(chǔ)進(jìn)階目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容