在 JavaScript 中,建立對象的方式有不少種,最經常使用的通常是經過字面量的方式,而要建立實例對象則通常經過建立一個構造函數,經過 new 關鍵字來構造。javascript
雖然 Object 函數和字面量均可以建立對象,但同時也會有一個問題:使用一個接口建立多個對象時,會出現大量重複代碼。下面來介紹一些建立對象的變體。java
function createPerson(name, age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function () {
console.log(this.name);
};
return obj;
}
var person = createPerson('mike', 18);
複製代碼
工廠模式解決了建立多個類似對象的問題,但缺點是沒法識別對象原型。bash
這裏打印 person 對象,能夠看到有 2 個屬性和 1 個方法,原型對象是 Obejct,constructor 屬性(指向構造函數的指針)指向 Object 對象。babel
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
var person = new Person('mike', 18);
var person2 = new Person('alice', 20);
// 至關於如下操做
var obj = new Object();
obj.__proto__ = Person.prototype;
Person.call(obj, 'mike', 18);
複製代碼
構造函數模式是比較常見的一種方式,經過大寫函數名的第一個字母來用以區分普通函數。函數
構造函數與工廠模式還有如下的不一樣:性能
此時建立 person 實例須要經過 new 關鍵字,經過 new 關鍵字調用構造函數的過程其實經歷瞭如下四個步驟:ui
構造函數解決了工廠模式不能識別實例類型的問題,可是也有一個缺點:在這個例子裏它會屢次建立了相同函數 sayName。this
咱們建立每個函數都有一個 prototype(原型)屬性,指向一個對象。這個對象的用途是包含全部特定類型(例子是 Person)的全部實例共享的屬性(name age)和方法(sayName)。spa
function Person() { }
Person.prototype = {
constructor: Person, // 不指定 constructor 會使 constructor 指向斷裂,致使對象類型沒法正確識別。
name: 'mike',
age: 19,
hobby: ['football', 'singing'],
sayName: function () {
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.hobby.push('dancing'); // person2.hobby: ['football', 'singing','dancing']
複製代碼
constructor 指向未斷裂的狀況:指向了 Personprototype
constructor 指向斷裂的狀況:失去了 constructor,默認指向了 Object
原型鏈示意圖:
下圖可見經過原型模式解決了構造函數模式屢次建立了 sayName 方法的問題,但聰明的電視機前的你確定發現了定義的原型屬性會被全部的實例共享。
當咱們操做了 person1 的 hobby 對象的時候,person2 的也同時被修改了,這是咱們不肯看到的。
function Person(name, age) {
this.name = name;
this.age = age;
this.hobby = ['football', 'singing']
}
Person.prototype = {
constructor: Person, // 不指定 constructor 會使 constructor 指向斷裂,致使對象類型沒法正確識別。
sayName: function () {
console.log(this.name);
}
}
var person1 = new Person('mike', 18);
person1.hobby.push('dancing');
var person2 = new Person('alice', 19);
複製代碼
經過以上的幾種方式的分析,咱們差很少也能獲得比較好的一種模式了,那就是組合模式。
在構造函數中添加實例屬性,在構造函數的原型鏈上添加實例方法,這樣既解決了實例共享,又解決了屢次建立相同函數的問題,是目前使用比較普遍的模式。
ES6 裏咱們能夠經過 class 關鍵字來定義一個類,class 其實是一個語法糖,雖然絕大部分的功能能夠經過 ES5 實現,可是 class 的寫法讓對象變的更加清晰,更接近面向對象的語法。 經過 class 來改寫組合模式:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.hobby = ['football', 'singing'];
}
sayName() {
console.log(this.name);
}
}
var person1 = new Person('mike', 18);
person1.hobby.push('dancing');
var person2 = new Person('alice', 19);
複製代碼
由此對比可見,和 ES5 的結果只有在 __proto__ 對象裏的 constructor 顯示的是 class,其他的部分都是一致。 經過 babel 編譯成 ES5,咱們進行一下對比。
'use strict';
var _createClass = function () {
// 定義屬性的配置項
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) {
defineProperties(Constructor.prototype, protoProps);
}
if (staticProps) {
defineProperties(Constructor, staticProps);
}
return Constructor;
};
}();
// 檢查實例是不是後者的實例
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
this.hobby = ['football', 'singing'];
}
// 掛載 sayName 方法
_createClass(Person, [{
key: 'sayName',
value: function sayName() {
console.log(this.name);
}
}]);
return Person;
}();
var person1 = new Person('mike', 18);
person1.hobby.push('dancing');
var person2 = new Person('alice', 19);
複製代碼
拋開對屬性的一些配置上的操做,與 ES5 咱們所用的組合模式並沒有不一樣。
首先咱們經過組合模式建立一個 Animal 父類對象
// 定義一個動物類
function Animal(name) {
// 屬性
this.name = name || 'Animal';
// 實例方法
this.sleep = function () {
return this.name + ' 正在睡覺!';
}
}
// 原型方法
Animal.prototype.eat = function (food) {
return this.name + ' 正在吃: ' + food;
};
複製代碼
核心: 將父類的實例做爲子類的原型(注意不能使用字面量方式定義原型方法,會重寫原型鏈)
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name); // cat
console.log(cat.eat('fish')); // cat 正在吃:fish
console.log(cat.sleep()); // cat 正在睡覺!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
複製代碼
特色:
缺點:
推薦指數:★★(三、4兩大體命缺陷)
核心:使用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類(沒用到原型)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name); // Tom
console.log(cat.sleep()); // Tom 正在睡覺
// console.log(cat.eat('fish')); // 會報錯,原型在這裏不可用
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
複製代碼
特色:
缺點:
推薦指數:★★(缺點3)
核心:爲父類實例添加新特性,做爲子類實例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
複製代碼
特色:不限制調用方式,不論是new 子類()仍是子類(),返回的對象具備相同的效果
缺點:
推薦指數:★★
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
複製代碼
特色:支持多繼承
缺點:
推薦指數:★(缺點1)
核心:經過調用父類構造,繼承父類的屬性並保留傳參的優勢,而後經過將父類實例做爲子類原型,實現函數複用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
複製代碼
特色:
缺點: 調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
推薦指數:★★★★(僅僅多消耗了一點內存)
核心:經過寄生方式,砍掉父類的實例屬性,這樣,在調用兩次父類的構造的時候,就不會初始化兩次實例方法/屬性,避免的組合繼承的缺點
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 建立一個沒有實例方法的類
var Super = function(){};
Super.prototype = Animal.prototype;
//將實例做爲子類的原型
Cat.prototype = new Super();
})();
// 等價於下面這種狀況
// function inheritPrototype(sub, sup) {
// var Fn= function() {}
// Fn.prototype = sup.prototype;
// sub.prototype = new Fn();
// }
// inheritPrototype(Cat, Animal);
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
複製代碼
特色:堪稱完美
缺點:實現較爲複雜
推薦指數:★★★★
首先仍是建立一個 Animal 類
class Animal {
constructor(name) {
this.name = name || 'Animal';
this.sleep = function () {
return this.name + ' 正在睡覺!';
}
}
eat(food) {
return this.name + ' 正在吃: ' + food;
};
}
複製代碼
而後經過 extends 關鍵字來繼承 Animal
class Cat extends Animal {
constructor(name, age) {
super(name);
this.age = age; // 新增的子類屬性
}
eat(food) {
const result = super.eat(food); // 經過 super 調用父類方法
return this.age + ' 歲的 ' + result;
}
}
const cat = new Cat('miao', 3);
複製代碼
總的來講,ES6 的 class 語法糖更清晰和優雅地實現了建立對象和對象繼承。 可是咱們要想更好的理解 class,那麼關於 ES5 的對象、對象繼承以及原型鏈等知識也是要掌握的很牢固。