JavaScript 編程:5.面向對象的程序設計

面向對象的程序設計

理解對象

在 ECMA-262 中,對象的定義:無序屬性的集合官册,其屬性可以包含基本值生兆、對象或者函數(shù)。

我們可以把 ECMAScript 的對象想象成散列表/字典:無非就是一組鍵值對膝宁,其中值可以是數(shù)據或函數(shù)鸦难。

每個對象都是基于一個引用類型創(chuàng)建的。

屬性類型

ECMA-262 第 5 版在定義只有內部才用的特性(attribute)時员淫,描述了屬性(property)的各種特征合蔽。ECMA-262 定義這些特性是為了實現(xiàn) JavaScript 引擎用的,因此在 JavaScript 中不能直接訪問它們介返。ECMAScript 中有兩種屬性:數(shù)據屬性訪問器屬性拴事。

1. 數(shù)據屬性

數(shù)據屬性包含一個數(shù)據值的位置。在這個位置可以讀取和寫入值圣蝎。

數(shù)據屬性 描述
[[Configurable]] 表示能否通過 delete 刪除屬性從而重新定義屬性刃宵,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性徘公。默認值為 true牲证。
[[Enumerable]] 表示能否通過 for-in 循環(huán)返回屬性。默認值為 true关面。
[[Writable]] 表示能否修改屬性的值坦袍。默認值為 true
[[Value]] 包含這個屬性的數(shù)據值等太。默認值為 undefined捂齐。

要修改屬性默認的特性,必須使用 ECMAScript 5 的 Object.defineProperty() 方法缩抡。

這個方法接收三個參數(shù):屬性所在的對象辛燥、屬性的名字和一個描述符對象。

其中缝其,描述符(descriptor)對象的屬性必須是:configurable挎塌、enumerablewritablevalue内边。設置其中的一或多個值榴都,可以修改對應的特性值:

var person = {};

// 修改屬性默認的特性
Object.defineProperty(person, "name", {
    writable: false,
    value: "Nicholas"
});

alert(person.name); //Nicholas
person.name = "Greg";
alert(person.name); //Nicholas
  • 一旦把屬性定義為不可配置的(configurable = false),就不能再把它變回可配置了漠其。也就是說嘴高,可以多次調用 Object.defineProperty() 方法修改同一個屬性竿音,但在把 configurable 特性設置為 false 之后就會有限制了。
  • 在調用 Object.defineProperty() 方法時拴驮,如果不指定 configurable春瞬、enumerable、和 writable 特性的默認值都是 false套啤。

2. 訪問器屬性

訪問器屬性不包含數(shù)據值宽气,它們包含一對 gettersetter 函數(shù)(不過,這兩個函數(shù)都不是必需的)潜沦。在讀取訪問器屬性時萄涯,會調用 getter 函數(shù),這個函數(shù)負責返回有效的值唆鸡。在寫入訪問器屬性時涝影,會調用 setter 函數(shù)并傳入新值,這個函數(shù)負責決定如何處理數(shù)據争占。訪問器屬性有如下 4 個特性:

訪問器屬性 描述
[[Configurable]] 表示能否通過 delete 刪除屬性從而重新定義屬性燃逻,能否修改屬性的特性,或者能否把屬性修改為數(shù)據屬性臂痕。對于直接在對象上定義的屬性唆樊,這個特性的默認值為 true
[[Enumerable]] 表示能否通過 for-in 循環(huán)返回屬性刻蟹。對于直接在對象上定義的屬性逗旁,這 5 個特性的默認值為 true
[[Get]] 在讀取屬性時調用的函數(shù)舆瘪。默認值為 undefined片效。
[[Set]] 在寫入屬性時調用的函數(shù)。默認值為 undefined英古。

訪問器屬性不能直接定義淀衣,必須使用 Object.defineProperty() 來定義。

var book = {
    _year: 2018, // _表示只能通過對象方法訪問的屬性
    edition: 1
};

Object.defineProperty(book, "year", {
    // getter 函數(shù)返回_year 的值召调,
    get: function() {
        return this._year;
    },
    // setter 函數(shù)通過計算來確定正確的版本膨桥。
    set: function(newValue) {
        if (newValue > 2018) {
            this._year = newValue;
            this.edition += newValue - 2018;
        }
    }
});

book.year = 2020;
console.log(book.edition); // 3

定義多個屬性

Object.defineProperties() 方法可以同時為對象定義多個屬性。
這個方法接收兩個對象參數(shù):

  • 參數(shù)一:要添加和修改其屬性的對象唠叛;
  • 參數(shù)二:該屬性與第一個對象中要添加或修改的屬性一一對應只嚣。
var book = {};
        
Object.defineProperties(book, {
    // 數(shù)據屬性
    _year: {
        value: 2004
    },
    // 數(shù)據屬性
    edition: {
        value: 1
    },
    // 訪問器屬性
    year: {            
        get: function(){
            return this._year;
        },

        set: function(newValue){
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }                  
        }            
    }        
});

book.year = 2005;
alert(book.edition);   //2

讀取屬性的特性

ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以獲取給定屬性的描述符艺沼。
這個方法接收兩個參數(shù):屬性所在的對象和要讀取其描述符的屬性名稱册舞。

返回值是一個對象:
如果是訪問器屬性,這個對象的屬性有 configurable障般、enumerable调鲸、getset;
如果是數(shù)據屬性盛杰,這個對象的屬性有 configurableenumerable藐石、writablevalue即供。

var book = {};

// 同時定義多個屬性
Object.defineProperties(book, {
    // 定義數(shù)據屬性
    _year: {
        value: 2016
    },
    edition: {
        value: 1
    },
    // 定義訪問器屬性
    year: {
        get: function () {
            return this._year;
        },
        set: function () {
            if (newValue > 2018) {
                this._year = newValue;
                this.edition += newValue - 2018;
            }
        }
    }
});

// 獲取數(shù)據屬性
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value); // 2016
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // undefined

// 獲取訪問器屬性
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // function

創(chuàng)建對象

創(chuàng)建對象的兩種方式:

  1. 構造函數(shù)模式
var person = new Object();
person.name = "Tom";
person.age = 29;
person.job = "Software Engineer";

person.sayName = function() {
    alert(this.name); // this.name 將被解析為 person.name
};
  1. 字面量模式
var person = {
    name: "Tom",
    age : 29,
    job:"Software Engineer",

    sayName: function(){
        alert(this.name);
    }
};

缺點:使用同一個接口創(chuàng)建很多對象,會產生大量的重復代碼于微。

工廠模式

工廠模式抽象了創(chuàng)建具體對象的過程逗嫡,考慮到在 ECMAScript 中無法創(chuàng)建類,開發(fā)人員就發(fā)明了一種函數(shù)角雷,用函數(shù)來封裝以特定接口創(chuàng)建對象的細節(jié)

// 把創(chuàng)建對象的所有過程封裝在一個函數(shù)中
function createPerson(name, age, job) {

  // 把構造函數(shù)放在函數(shù)體中性穿,并返回這個構造函數(shù)創(chuàng)建的實例對象
  var person = new Object();
  person.name = name;
  person.age = age;
  person.job = job;
  
  person.sayName = function() {
    console.log(this.name);
  };

  return person;
}

var person1 = createPerson('Andy', 25, 'Software Engineer');
var person2 = createPerson('Grey', 24, 'Teacher');

工廠模式雖然解決了創(chuàng)建多個相似對象的問題勺三,但卻沒有解決對象識別的問題(無法識別一個對象的類型)。

構造函數(shù)模式

ECMAScript 中的構造函數(shù)可用來創(chuàng)建特定類型的對象需曾。
原生構造函數(shù):Object吗坚、Array ...

創(chuàng)建自定義的構造函數(shù),從而定義自定義對象類型的屬性和方法:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
  };
}

var person1 = new Person('Andy', 25, 'Software Engineer');
var person2 = new Person('Grey', 24, 'Teacher');

// 檢測對象的類型
console.log(person1 instanceof Object); // true呆万,因為所有對象均繼承自 Object
console.log(person1 instanceof Person); // true

構造函數(shù)模式&工廠模式的區(qū)別

  • 沒有顯式地創(chuàng)建對象结闸;
  • 直接將屬性和方法賦給了 this 對象镣隶;
  • 沒有 return 語句。

?? 構造函數(shù)始終都應該以一個大寫字母開頭,而非構造函數(shù)則應該以一個小寫字母開頭碍粥。

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

// 把函數(shù)定義轉移到構造函數(shù)外部
// 所有對象都共享同一個全局的 sayName 函數(shù)
function sayName(){
    alert(this.name);
}

var person1 = new Person("Andy", 23, "Software Engineer");
var person2 = new Person("Bob", 35, "Army");

將構造函數(shù)當作函數(shù)

  • 構造函數(shù)與其他函數(shù)的唯一區(qū)別,就在于調用它們的方式不同再悼。
  • 任何函數(shù)馅袁,只要通過 new 操作符來調用,那它就可以作為構造函數(shù)严就;
  • 任何函數(shù)总寻,如果不通過 new 操作符來調用,那它跟普通函數(shù)也不會有什么兩樣梢为。
// 當作構造函數(shù)使用
// 使用 new 操作符創(chuàng)建一個新對象
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"

// 作為普通函數(shù)使用渐行,屬性和方法都被會添加給 window 對象
// 當在全局作用域中調用一個函數(shù)時,this 對象總是指向 Global 對象(在瀏覽器中就是 window 對象)铸董。
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"

// 在另一個對象的作用域中調用 Person() 函數(shù)
// 在對象 o 的作用域中調用祟印,因此調用后 o 就擁有了所有屬性和 sayName() 方法。
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

構造函數(shù)的問題

每個方法都要在每個實例上重新創(chuàng)建一遍粟害。(對象無法共用同名函數(shù))
ECMAScript 中的函數(shù)是對象旁理,因此每定義一個函數(shù),也就是實例化了一個對象我磁。因此孽文,不同實例上的同名函數(shù)是不相等的驻襟。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    // 每個 Person 實例都包含一個不同的 Function 實例
    this.sayName = new Function("alert(this.name)"); // 與聲明函數(shù)在邏輯上是等價的 
}

// 不同實例上的同名函數(shù)是不相等的
alert(person1.sayName == person2.sayName);  //false

原型模式

prototype(原型)屬性
該屬性指向函數(shù)的原型對象。原型對象中包含 constructor (構造函數(shù))屬性芋哭、共享的屬性和方法沉衣。

  • 每個函數(shù)都有一個 prototype (原型)屬性,這個屬性是一個指針减牺,指向一個對象豌习,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。
  • prototype 就是「通過調用構造函數(shù)而創(chuàng)建的那個實例對象」的原型對象 拔疚。使用原型對象的好處是讓所有對象實例共享它所包含的屬性和方法肥隆。
// 構造函數(shù)變成了空函數(shù)
function Person(){
}

// 將 sayName() 方法和所有屬性直接添加到了 Person 的 prototype 屬性中。
Person.prototype.name = "andy";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
person1.sayName(); // "andy"

var person2 = new Person();
person2.sayName(); // "andy"

alert(person1.sayName == person2.sayName); // true稚失,所有實例共享同一個方法

// ??????
// isPrototypeOf():判斷實例對象與原型對象之間的關系
// 因為它們內部都有一個指向 Person.prototype 的指針栋艳,因此都返回了 true。
alert(Person.prototype.isPrototypeOf(person1)); //Person 是不是 person1 實例的原型句各?true
alert(Person.prototype.isPrototypeOf(person2)); //true

// ??????
// ES 5 新增吸占,Object.getPrototypeOf():返回 [[Prototype]] 的值。
alert(Object.getPrototypeOf(person1) == Person.prototype); // true
alert(Object.getPrototypeOf(person1).name); // andy

理解原型對象

無論什么時候凿宾,只要創(chuàng)建了一個新函數(shù)矾屯,就會根據一組特定的規(guī)則為該函數(shù)創(chuàng)建一個 prototype 屬性,這個屬性指向函數(shù)的原型對象初厚。

在默認情況下件蚕,所有原型對象都會自動獲得一個 constructor (構造函數(shù))屬性,這個屬性包含一個指向 prototype 屬性所在函數(shù)的指針(即:原型對象中的 constructor 屬性是一個指針产禾,這個指針指回了 prototype 屬性所在的函數(shù))骤坐。

原型對象示意圖

圖中:

  • Person.prototype 指向了原型對象。
  • Person.prototype.constructor 又指回了 Person 的構造函數(shù)下愈。
  • 實例的內部屬性 [[Prototype]] 指針僅指向原型對象纽绍,而不指向構造函數(shù)。

每當代碼讀取某個對象的某個屬性時势似,都會執(zhí)行一次搜索拌夏,目標是具有給定名字的屬性。搜索首先從對象實例本身開始履因。如果在實例中找到了具有給定名字的屬性障簿,則返回該屬性的值;如果沒有找到栅迄,則繼續(xù)搜索指針指向的原型對象站故,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性的值西篓。

當為對象實例添加一個屬性時愈腾,這個屬性就會屏蔽原型對象中保存的同名屬性;換句話說岂津,添加這個屬性只會阻止我們訪問原型中的那個屬性虱黄,但不會修改那個屬性。即使將這個屬性設置為 null吮成,也只會在實例中設置這個屬性橱乱,而不會恢復其指向原型的連接。不過粱甫,使用 delete 操作符則可以完全刪除實例屬性泳叠,從而讓我們能夠重新訪問原型中的屬性:

function Person(){
}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "Greg";
alert(person1.name);   //"Greg" ——來自實例
alert(person2.name);   //"Nicholas" ——來自原型

delete person1.name;   // delete 刪除實例屬性
alert(person1.name);   //"Nicholas" ——來自原型

使用 hasOwnProperty() 方法可以檢測一個屬性是存在于實例中(返回 true),還是存在于原型中(返回 false)茶宵。

function Person(){
}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

alert(person1.hasOwnProperty("name"));  //false

person1.name = "Greg"; // 設置實例屬性
alert(person1.name);   //"Greg" ——來自實例
alert(person1.hasOwnProperty("name"));  //true

alert(person2.name);   //"Nicholas" ——來自原型
alert(person2.hasOwnProperty("name"));  //false

delete person1.name;   // delete 刪除實例屬性
alert(person1.name);   //"Nicholas" ——來自原型
alert(person1.hasOwnProperty("name"));  //false

原型與 in 操作符

  • 在單獨使用時危纫,in 操作符會在通過對象能夠訪問給定屬性時返回 true,無論該屬性存在于實例中還是原型中节预。
  • 只要 in 操作符返回 truehasOwnProperty() 返回 false叶摄,就可以確定屬性是原型中的屬性属韧。
function Person(){
}

Person.prototype.name = "andy";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

// hasOwnProperty():實例屬性存在于原型中安拟,返回 false
console.log(person1.hasOwnProperty("name")); // false

// in:不判斷存在于原型中還是實例中,只要有就返回 true
console.log("name" in person1); // true

// 只要 in 操作符返回 true 而 hasOwnProperty() 返回 false宵喂,就可以確定屬性是原型中的屬性糠赦。
function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

枚舉對象屬性

  • 在使用 for-in 循環(huán)時,返回的是所有能夠通過對象訪問的锅棕、可枚舉的(enumerated)屬性拙泽,其中既包括存在于實例中的屬性,也包括存在于原型中的屬性裸燎。
  • 要取得對象上所有可枚舉的實例屬性顾瞻,可以使用 ECMAScript 5 的 Object.keys() 方法。這個方法接收一個對象作為參數(shù)德绿,返回一個包含所有可枚舉屬性的字符串數(shù)組荷荤。
  • 如果你想要得到所有實例屬性,無論它是否可枚舉移稳,都可以使用 Object.getOwnPropertyNames() 方法蕴纳。
function Person(){
}

Person.prototype.name = "andy";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
};

// 原型屬性
var keys = Object.keys(Person.prototype);
console.log(keys); //[ 'name', 'age', 'job', 'sayName' ]

var person1 = new Person();
person1.name = "Andy";
person1.age = 24;

// 實例屬性
var person1Key = Object.keys(person1);
console.log(person1Key); //[ 'name', 'age' ]

更簡單的原型語法

用一個包含所有屬性和方法的對象字面量來重寫整個原型對象。

function Person(){
}

// 本質上完全重寫了默認的 prototype 對象
Person.prototype = {
    name : "Andy",
    age : 28,
    job : "Doctor",
    sayName : function () {
        alert(this.name);
    }
};

// 上面 constructor 屬性不再指向 Person个粱。
// 默認情況下古毛,每創(chuàng)建一個函數(shù),就會同時創(chuàng)建它的 prototype 對象都许,這個對象也會自動獲得 constructor 屬性稻薇。
// 而這里使用的語法嫂冻,本質上完全重寫了默認的 prototype 對象,因此 constructor 屬性也就變成了新對象的 constructor 屬性(指向 Object 構造函數(shù))颖低,不再指向 Person 函數(shù)絮吵。

// 解決方案
// 注意,以這種方式重設 constructor 屬性會導致它的 [[Enumerable]] 特性被設置為 true忱屑。
// 默認情況下蹬敲,原生的 constructor 屬性是不可枚舉的
function Person(){
}

Person.prototype = {
    constructor : Person, // 將它設置為適當?shù)闹?    name : "Andy",
    age : 28,
    job : "Doctor",
    sayName : function () {
        alert(this.name);
    }
};

原型的動態(tài)性

  • 對原型對象所做的任何修改都能夠立即從實例上反映出來——即使是先創(chuàng)建了實例后修改原型也照樣如此。
  • 實例中的指針僅指向原型莺戒,而不指向構造函數(shù)伴嗡。
  • 調用構造函數(shù)時會為實例添加一個指向最初原型的 [[Prototype]] 指針,而把原型修改為另外一個對象就等于切斷了構造函數(shù)與最初原型之間的聯(lián)系从铲。
function Person() {

}
var friend = new Person();

Person.prototype = {
  constructor: Person,
  name: "Tom",
  age: 24,
  job: "Software Engineer",
  sayName: function () {
      console.log(this.name);
  }
};

friend.sayName(); // TypeError: friend.sayName is not a function
重寫整個原型對象

原生對象的原型

  • 原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面瘪校,就連所有原生的引用類型,都是采用這種模式創(chuàng)建的名段。
  • 所有原生引用類型(Object阱扬、Array、String伸辟,等等)都在其構造函數(shù)的原型上定義了方法麻惶。
// 給基本包裝類型 String 添加了一個名為 startsWith() 的方法
String.prototype.startsWith = function (text) {
    return this.indexOf(text) == 0;
};

var msg = "Hello world!";
alert(msg.startsWith("Hello"));   //true

不推薦在產品化的程序中修改原生對象的原型。

原型對象的問題

  1. 無法為構造函數(shù)傳遞初始化參數(shù)信夫,默認情況下窃蹋,所有實例的初始化屬性值相同。
  2. 所有屬性静稻、方法均共享警没。(多個實例共享同一個屬性的問題!)
function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

// 修改 person1.friends 引用的數(shù)組振湾,向數(shù)組中添加了一個字符串杀迹。
// 由于 friends 數(shù)組存在于 Person.prototype 而非 person1 中,
// 所以修改也會通過 person2.friends (與 person1.friends 指向同一個數(shù)組))反映出來
person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true

組合使用構造函數(shù)模式和原型模式

構造函數(shù)模式用于定義實例屬性押搪,而原型模式用于定義方法和共享的屬性树酪。
構造函數(shù)與原型混成的模式,是目前在 ECMAScript 中使用最廣泛嵌言、認同度最高的一種創(chuàng)建自定義類型的方法嗅回。

// 構造函數(shù)中定義實例屬性
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Andy", "Bob"];
}

// 原型中定義所有實例共享的屬性和方法
Person.prototype = {
    constructor: Person,
    sayName: function () {
        alert(this.name);
    }
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Grey", 27, "Doctor");

// 修改 person1.friends 并不會影響到 person2.friends
person1.friends.push("Van");
console.log(person1.friends); //[ 'Andy', 'Bob', 'Van' ]
console.log(person2.friends); //[ 'Andy', 'Bob' ]
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true

動態(tài)原型模式

動態(tài)原型模式:把所有信息都封裝在了構造函數(shù)中,而通過在構造函數(shù)中初始化原型(僅在必要的情況下)摧茴,又保持了同時使用構造函數(shù)和原型的優(yōu)點绵载。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型娃豹。

解釋:使用「組合使用構造函數(shù)模式和原型模式」的方式創(chuàng)建一個自定義對象時焚虱,構造函數(shù)的代碼和原型的代碼是獨立開來的。因此懂版,為了解決這個“代碼分散”的問題鹃栽,通過在一個構造函數(shù)中加入 if 表達式進行判斷,可以實現(xiàn)將所有代碼匯集在一個方法中的目的躯畴。

// 構造函數(shù)
function Person(name, age, job) {

    // 屬性
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Andy", "Bob"];

    // 方法
    // 只在 sayName() 方法不存在的情況下民鼓,才會將它添加到原型中
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

var friend = new Person("Andy", 24, "Doctor");
friend.sayName();

??

使用動態(tài)原型模式時,不能使用對象字面量重寫原型蓬抄。如果在已經創(chuàng)建了實例的情況下重寫原型丰嘉,那么就會切斷現(xiàn)有實例與新原型之間的聯(lián)系。

寄生構造函數(shù)模式(不推薦使用)

寄生構造函數(shù)模式:創(chuàng)建一個函數(shù)嚷缭,該函數(shù)的作用僅僅是封裝創(chuàng)建對象的代碼饮亏,然后再返回新創(chuàng)建的對象。

// Person 函數(shù)創(chuàng)建了一個新對象阅爽,并以相應的屬性和方法初始化該對象路幸,然后又返回了這個對象
function Person(name, age, job){
    var object = new Object();
    object.name = name;
    object.age = age;
    object.job = job;
    object.sayName = function(){
        alert(this.name);
    }
    return object;
}

// 除了使用 new 操作符并把使用的包裝函數(shù)叫做構造函數(shù)之外,這個模式跟工廠模式其實是一模一樣的付翁。
var friend = new Person("Andy", 24, "Doctor");
friend.sayName();

關于寄生構造函數(shù)模式简肴,有一點需要說明:首先,返回的對象與構造函數(shù)或者與構造函數(shù)的原型屬性之間沒有關系胆敞;也就是說着帽,構造函數(shù)返回的對象與在構造函數(shù)外部創(chuàng)建的對象沒有什么不同杂伟。為此移层,不能依賴 instanceof 操作符來確定對象類型。由于存在上述問題赫粥,我們建議在可以使用其他模式的情況下观话,不要使用這種模式。

穩(wěn)妥構造函數(shù)模式

所謂穩(wěn)妥對象越平,指的是沒有公共屬性频蛔,而且其方法也不引用 this 的對象。
穩(wěn)妥對象最適合在一些安全的環(huán)境中(這些環(huán)境中會禁止使用 thisnew)秦叛,或者在防止數(shù)據被其他應用程序(如 Mashup 程序)改動時使用晦溪。
穩(wěn)妥構造函數(shù)遵循與寄生構造函數(shù)類似的模式,但有兩點不同:

  1. 新創(chuàng)建對象的實例方法不引用 this挣跋;
  2. 不使用 new 操作符調用構造函數(shù)三圆。
function Person(name, age, job){

    // 創(chuàng)建要返回的對象
    var object = new Object();

    // 定義私有變量和函數(shù)
    object.name = name;
    object.age = age;
    object.job = job;

    // 添加方法
    object.sayName = function(){
        alert(name);
    }
    // 返回對象
    return object;
}

var friend = Person("Andy", 24, "Doctor");
// 除了調用 sayName() 方法外,沒有別的方式可以訪問其數(shù)據成員。
friend.sayName();

穩(wěn)妥構造函數(shù)模式提供的這種安全性舟肉,使得它非常適合在某些安全執(zhí)行環(huán)境——例如修噪,ADsafe(www.adsafe.org)和 Caja(http://code.google.com/p/google-caja/)提供的環(huán)境下使用。

繼承

許多 OO 語言都支持兩種繼承方式:

  1. 接口繼承:只繼承方法簽名路媚;
  2. 實現(xiàn)繼承:繼承實際的方法黄琼。

由于函數(shù)沒有簽名,在 ECMAScript 中無法實現(xiàn)接口繼承整慎。ECMAScript 只支持實現(xiàn)繼承脏款,而且其實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)的。

原型鏈

基本思想:利用原型讓一個引用類型繼承另一個引用類型的屬性和方法裤园。

構造函數(shù)弛矛、原型和實例的關系:每個構造函數(shù)都有一個原型對象,原型對象包含一個指向構造函數(shù)的指針比然,而實例包含一個指向原型對象的內部指針丈氓。

假如我們讓原型對象等于另一個類型的實例,結果會怎么樣呢强法?顯然万俗,此時的原型對象將包含一個指向另一個原型的指針,相應地饮怯,另一個原型中也包含著一個指向另一個構造函數(shù)的指針闰歪。假如另一個原型又是另一個類型的實例,那么上述關系依然成立蓖墅,如此層層遞進库倘,就構成了實例與原型的鏈條。

// SuperType
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function () {
    return this.property;
};

// SubType
function SubType() {
    this.subproperty = false;
}

/* 
 * SubType 繼承了 SyperType
 * 繼承是通過創(chuàng)建 SuperType 的實例论矾,并將該實例賦給 SubType.prototype 實現(xiàn)的教翩。
 * 這里 SubType 重寫了 prototype 屬性, [[prototype]] 的 constructor 屬性指向 SuperType贪壳。
 * 即:讓 SubType 的原型對象等于 SuperType 的實例
 */
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue()); // true
繼承關系示意圖

說明:
instance 實例指向 SubType 的原型饱亿,SubType 的原型又指向 SuperType 的原型。

默認的原型

所有函數(shù)的默認原型都是 Object 的實例闰靴,因此默認原型都會包含一個內部指針彪笼,指向 Object.prototype
這也正是所有自定義類型都會繼承 toString()蚂且、valueOf() 等默認方法的根本原因配猫。

確定原型和實例的關系

1. instanceof 操作符

通過 instanceof 操作符來測試實例與原型鏈中出現(xiàn)過的構造函數(shù),結果就會返回 true杏死。

// instance 是 Object泵肄、SuperType 或 SubType 中任何一個類型的實例
alert(instance instanceof Object); // true
alert(instance instanceof SuperType); // true
alert(instance instanceof SubType); // true
2. isPrototypeOf() 方法

只要是原型鏈中出現(xiàn)過的原型佳遣,都可以說是該原型鏈所派生的實例的原型,因此 isPrototypeOf() 方法也會返回 true凡伊。

alert(Object.prototype.isPrototypeOf(instance)); // true
alert(SuperType.prototype.isPrototypeOf(instance)); // true
alert(SubType.prototype.isPrototypeOf(instance)); // true

謹慎地定義方法

子類型有時候需要重寫超類型中的某個方法零渐,或者需要添加超類型中不存在的某個方法。但不管怎樣系忙,給原型添加方法的代碼一定要放在替換原型的語句之后诵盼。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

// SubType 繼承了 SuperType
SubType.prototype = new SuperType();

// SubType 添加新方法
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

// SubType 重寫超類型中的方法
SubType.prototype.getSuperValue = function (){
    return false;
};

var instance = new SubType();
alert(instance.getSuperValue());   //false

在通過原型鏈實現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法银还。因為這樣做就會重寫原型鏈:

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

// SubType 繼承了 SuperType
SubType.prototype = new SuperType();

// 使用字面量添加新方法风宁,會導致上一行代碼無效
SubType.prototype = {

    // SubType 的新方法
    getSubValue : function (){
        return this.subproperty;
    },

    // SubType 的新方法
    someOtherMethod : function (){
        return false;
    }
};

var instance = new SubType();
alert(instance.getSuperValue());   //error!

原型鏈的問題

  • 問題一:在通過原型來實現(xiàn)繼承時,原型實際上會變成另一個類型的實例蛹疯。于是戒财,原先的實例屬性也就順理成章地變成了現(xiàn)在的原型屬性了。
// 包含引用類型值的原型屬性會被所有實例共享
function SuperType(){
  this.colors = ["red","green","blue"];
}

function SubType(){
}

// 繼承了 SuperType
// 相當于 SubType.prototype 變成了 SuperType 的一個實例
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black"); 
console.log(instance1.colors); //[ 'red', 'green', 'blue', 'black' ]

var instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'green', 'blue', 'black' ]
  • 問題二:在創(chuàng)建子類型的實例時捺弦,不能向超類型的構造函數(shù)中傳遞參數(shù)饮寞。

??????

實踐中很少會單獨使用原型鏈。

借用構造函數(shù)

基本思想:在子類型構造函數(shù)的內部調用超類型構造函數(shù)列吼。

在子類型構造函數(shù)的內部調用超類型構造函數(shù)幽崩。這樣就可以做到每個實例都具有自己的屬性,同時還能保證只使用構造函數(shù)模式來定義類型寞钥。

// SuperType
function SuperType() {
    this.colors = ["red", "blue", "green"];
}

// SubType
function SubType() {
    // 繼承了 SuperTyoe
    // 在(未來將要)新創(chuàng)建的 SubType 實例的環(huán)境下調用了 SuperType 構造函數(shù)慌申。
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //[ 'red', 'blue', 'green', 'black' ]

var instance2 = new SubType();
console.log(instance2.colors); //[ 'red', 'blue', 'green' ]

傳遞參數(shù)

相對于原型鏈而言,借用構造函數(shù)有一個很大的優(yōu)勢理郑,即可以在子類型構造函數(shù)中向超類型構造函數(shù)傳遞參數(shù)蹄溉。

// SuperType
function SuperType(name) {
    this.name = name;
}

// SubType
function SubType() {
    // 繼承了 SuperType,同時還傳遞了參數(shù)
    SuperType.call(this, "Bob");

    // 實例屬性
    this.age = 34;
}

var instance1 = new SubType();
console.log(instance1.name); // Bob
console.log(instance1.age); //34

借用構造函數(shù)的問題

如果僅僅是借用構造函數(shù)您炉,那么也將無法避免構造函數(shù)模式存在的問題——方法都在構造函數(shù)中定義柒爵,因此函數(shù)復用就無從談起了。而且邻吭,在超類型的原型中定義的方法餐弱,對子類型而言也是不可見的宴霸,結果所有類型都只能使用構造函數(shù)模式囱晴。考慮到這些問題瓢谢,借用構造函數(shù)的技術也是很少單獨使用的畸写。

組合繼承

將原型鏈和借用構造函數(shù)的技術組合到一塊,從而發(fā)揮二者之長的一種繼承模式氓扛。

基本思想:使用原型鏈實現(xiàn)對原型屬性和方法的繼承枯芬,而通過借用構造函數(shù)來實現(xiàn)對實例屬性的繼承论笔。

// SuperType 組合使用構造函數(shù)模式和原型模式
function SuperType(name){
    this.name = name;
    this.colors = ["red","green","blue"];
}

SuperType.prototype.sayName = function (){
    console.log(this.name);
};

// SubType 組合繼承(原型鏈+借用構造函數(shù))
function SubType(name, age){
    // 通過借用構造函數(shù),繼承屬性
    SuperType.call(this, name);

    this.age = age;
}

// 通過原型鏈千所,繼承方法
// 將 SuperType 的實例賦值給 SubType 的原型狂魔,然后又在該新原型上定義了方法 sayAge()。
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function (){
    console.log(this.age);
};

var instance1 = new SubType("Andy", 23);
instance1.colors.push("black"); 
console.log(instance1.colors); //[ 'red', 'green', 'blue', 'black' ]
instance1.sayName(); // Andy
instance1.sayAge(); //23

var instance2 = new SubType("Bob", 24);
console.log(instance2.colors); //[ 'red', 'green', 'blue' ]
instance2.sayName(); // Bob
instance2.sayAge(); //24

原型式繼承

借助原型可以基于已有的對象創(chuàng)建新對象淫痰,同時還不必因此創(chuàng)建自定義類型最楷。

function object(o) {
    function F() {} // 先創(chuàng)建一個臨時性的構造函數(shù)
    F.prototype = o; // 然后將傳入的對象作為這個構造函數(shù)的原型
    return new F(); // 返回這個臨時類型的新實例
}
// 從本質上來說,object() 對傳入其中的對象執(zhí)行了一次淺復制待错。


// 前提條件:1.有一個對象可以作為另一個對象的基礎籽孙。
var person = {
    name : "Andy",
    friends : ["Shelby", "Court", "Van"]
};

// 2.把基礎對象 person 傳遞給 object() 函數(shù),然后再根據具體需求對得到的對象加以修改
var anotherPerson = object(person);
anotherPerson.name = "Grey";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
anotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
console.log(anotherPerson.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
console.log(yetAnotherPerson.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]

console.log(anotherPerson.name); //Grey
console.log(yetAnotherPerson.name); //linda

Object.create() 方法

ECMAScript 5 通過新增 Object.create() 方法規(guī)范化了原型式繼承火俄。這個方法接收兩個參數(shù):

  • 一個用作新對象原型的對象犯建;
  • 一個為新對象定義額外屬性的對象(可選的);

在傳入一個參數(shù)的情況下瓜客,Object.create()object() 方法的行為相同适瓦。

var person = {
    name : "Andy",
    friends : ["Shelby", "Court", "Van"]
};

// 接收一個參數(shù)
var anotherPerson = Object.create(person);
anotherPerson.name = "Grey";
anotherPerson.friends.push("Rob");

// 接收兩個參數(shù)
// 以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。
var anotherPerson = Object.create(person ,{
    name:{
        value: "Grey"
    }
});

寄生式繼承

寄生式繼承的思路與寄生構造函數(shù)和工廠模式類似谱仪,即創(chuàng)建一個僅用于封裝繼承過程的函數(shù)犹菇,該函數(shù)在內部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象芽卿。

function createAnother(original) {
    var clone =object(original); // 通過調用函數(shù)創(chuàng)建一個新對象
    clone.sayHi = function() {   // 以某種方式來增強這個對象
        console.log("Hi");
    };
    return clone; // 返回這個對象
}

使用寄生式繼承來為對象添加函數(shù)揭芍,會由于不能做到函數(shù)復用而降低效率;這一點與構造函數(shù)模式類似卸例。

寄生組合式繼承

組合繼承是 JavaScript 最常用的繼承模式称杨;不過,它也有自己的不足筷转。組合繼承最大的問題就是無論什么情況下姑原,都會調用兩次超類型構造函數(shù)

  • 一次是在創(chuàng)建子類型原型的時候;

  • 另一次是在子類型構造函數(shù)內部呜舒。

沒錯锭汛,子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數(shù)時重寫這些屬性袭蝗。

// SuperType 組合使用構造函數(shù)模式和原型模式
function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = function () {
    console.log(this.name);
};

// 當調用 SubType 構造函數(shù)時唤殴,又會調用一次 SuperType 構造函數(shù),這一次又在新對象上創(chuàng)建了實例屬性 name 和 colors到腥。于是朵逝,這兩個屬性就屏蔽了原型中的兩個同名屬性。
function SubType(name, age) {
    SuperType.call(this, name); // 第二次調用 SuperType()

    this.age = age;
}

// 在第一次調用 SuperType 構造函數(shù)時乡范, SubType.prototype 會得到兩個屬性:name 和 colors;
// 它們都是 SuperType 的實例屬性配名,只不過現(xiàn)在位于 SubType 的原型中啤咽。
SubType.prototype = new SuperType(); // 第一次調用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

所謂寄生組合式繼承,即通過借用構造函數(shù)來繼承屬性渠脉,通過原型鏈的混成形式來繼承方法宇整。其背后的基本思路是:不必為了指定子類型的原型而調用超類型的構造函數(shù),我們所需要的無非就是超類型原型的一個副本而已芋膘。本質上没陡,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型索赏。

function inheritProtoptype(subType, superType) {
    var prototype = object(superType.prototype); // 創(chuàng)建對象
    prototype.constructor = subType; // 增強對象
    subType.prototype = prototype; // 指定對象
}

// 函數(shù)內部執(zhí)行流程:
// 1. 創(chuàng)建超類型原型的一個副本;
// 2. 為創(chuàng)建的副本添加 constructor 屬性盼玄,從而彌補因重寫原型而失去的默認的 constructor 屬性。
// 3. 將新創(chuàng)建的對象(即副本)賦值給子類型的原型潜腻;

這個例子的高效率體現(xiàn)在它只調用了一次 SuperType 構造函數(shù)埃儿,并且因此避免了在 SubType.prototype 上面創(chuàng)建不必要的、多余的屬性融涣。與此同時童番,原型鏈還能保持不變;因此,還能夠正常使用instanceofisPrototypeOf()威鹿。開發(fā)人員普遍認為寄生組合式繼承是引用類型最理想的繼承范式剃斧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市忽你,隨后出現(xiàn)的幾起案子幼东,更是在濱河造成了極大的恐慌,老刑警劉巖科雳,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件根蟹,死亡現(xiàn)場離奇詭異,居然都是意外死亡糟秘,警方通過查閱死者的電腦和手機简逮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尿赚,“玉大人散庶,你說我怎么就攤上這事×杈唬” “怎么了悲龟?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長泻蚊。 經常有香客問我躲舌,道長,這世上最難降的妖魔是什么性雄? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任没卸,我火速辦了婚禮,結果婚禮上秒旋,老公的妹妹穿的比我還像新娘约计。我一直安慰自己,他們只是感情好迁筛,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布煤蚌。 她就那樣靜靜地躺著,像睡著了一般细卧。 火紅的嫁衣襯著肌膚如雪尉桩。 梳的紋絲不亂的頭發(fā)上劳秋,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天喇辽,我揣著相機與錄音,去河邊找鬼育拨。 笑死止邮,一個胖子當著我的面吹牛这橙,可吹牛的內容都是我干的。 我是一名探鬼主播导披,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼屈扎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了撩匕?” 一聲冷哼從身側響起鹰晨,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎止毕,沒想到半個月后并村,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡滓技,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年哩牍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片令漂。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡膝昆,死狀恐怖,靈堂內的尸體忽然破棺而出叠必,到底是詐尸還是另有隱情荚孵,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布纬朝,位于F島的核電站收叶,受9級特大地震影響,放射性物質發(fā)生泄漏共苛。R本人自食惡果不足惜判没,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一蜓萄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧澄峰,春花似錦嫉沽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至魂毁,卻和暖如春玻佩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背席楚。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工咬崔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酣胀。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓刁赦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親闻镶。 傳聞我的和親對象是個殘疾皇子甚脉,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內容