【JavaScript】ES5/ES6 建立對象與繼承

1. ES5 建立對象的方式

在 JavaScript 中,建立對象的方式有不少種,最經常使用的通常是經過字面量的方式,而要建立實例對象則通常經過建立一個構造函數,經過 new 關鍵字來構造。javascript

雖然 Object 函數和字面量均可以建立對象,但同時也會有一個問題:使用一個接口建立多個對象時,會出現大量重複代碼。下面來介紹一些建立對象的變體。java

1.1 工廠模式

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

1.2 構造函數

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);
複製代碼

構造函數模式是比較常見的一種方式,經過大寫函數名的第一個字母來用以區分普通函數。函數

構造函數與工廠模式還有如下的不一樣:性能

  • 沒有顯示建立對象
  • 直接將屬性賦值給了 this
  • 沒有 return

此時建立 person 實例須要經過 new 關鍵字,經過 new 關鍵字調用構造函數的過程其實經歷瞭如下四個步驟:ui

  1. 建立一個新對象: var obj = new Object();
  2. 將構造函數的原型對象賦值給新的對象 obj: obj.__proto__ = Person.prototype;
  3. 執行構造函數中的代碼,給新對象 obj 添加屬性和方法: Person.call(obj, 'mike', 18);
  4. 返回 obj 對象

構造函數解決了工廠模式不能識別實例類型的問題,可是也有一個缺點:在這個例子裏它會屢次建立了相同函數 sayName。this

1.3 原型模式

咱們建立每個函數都有一個 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 的也同時被修改了,這是咱們不肯看到的。

1.4 組合模式

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);
複製代碼

經過以上的幾種方式的分析,咱們差很少也能獲得比較好的一種模式了,那就是組合模式。

在構造函數中添加實例屬性,在構造函數的原型鏈上添加實例方法,這樣既解決了實例共享,又解決了屢次建立相同函數的問題,是目前使用比較普遍的模式。

2. ES6 建立對象的方式

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 咱們所用的組合模式並沒有不一樣。

3. ES5 實現繼承

首先咱們經過組合模式建立一個 Animal 父類對象

// 定義一個動物類
function Animal(name) {
    // 屬性
    this.name = name || 'Animal';
    // 實例方法
    this.sleep = function () {
        return this.name + ' 正在睡覺!';
    }
}
// 原型方法
Animal.prototype.eat = function (food) {
    return this.name + ' 正在吃: ' + food;
};
複製代碼

3.1 原型鏈繼承

核心: 將父類的實例做爲子類的原型(注意不能使用字面量方式定義原型方法,會重寫原型鏈)

function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

//&emsp;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
複製代碼

特色:

  1. 很是純粹的繼承關係,實例是子類的實例,也是父類的實例
  2. 父類新增原型方法/原型屬性,子類都能訪問到
  3. 簡單,易於實現

缺點:

  1. 能夠在Cat構造函數中,爲Cat實例增長實例屬性。若是要新增原型屬性和方法,則必須放在new Animal()這樣的語句以後執行。
  2. 沒法實現多繼承
  3. 來自原型對象的引用屬性是全部實例共享的
  4. 建立子類實例時,沒法向父類構造函數傳參

推薦指數:★★(三、4兩大體命缺陷)

3.2 構造繼承

核心:使用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類(沒用到原型)

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
複製代碼

特色:

  1. 解決了原型鏈繼承中,子類實例共享父類引用屬性的問題
  2. 建立子類實例時,能夠向父類傳遞參數
  3. 能夠實現多繼承(call 多個父類對象)

缺點:

  1. 實例並非父類的實例,只是子類的實例
  2. 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
  3. 沒法實現函數複用,每一個子類都有父類實例函數的副本,影響性能

推薦指數:★★(缺點3)

3.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 子類()仍是子類(),返回的對象具備相同的效果

缺點:

  1. 實例是父類的實例,不是子類的實例
  2. 不支持多繼承

推薦指數:★★

3.4 拷貝繼承

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. 效率較低,內存佔用高(由於要拷貝父類的屬性)
  2. 沒法獲取父類不可枚舉的方法(不可枚舉方法,不能使用for in 訪問到)

推薦指數:★(缺點1)

3.5 組合繼承

核心:經過調用父類構造,繼承父類的屬性並保留傳參的優勢,而後經過將父類實例做爲子類原型,實現函數複用

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
複製代碼

特色:

  1. 彌補了方式2的缺陷,能夠繼承實例屬性/方法,也能夠繼承原型屬性/方法
  2. 既是子類的實例,也是父類的實例
  3. 不存在引用屬性共享問題
  4. 可傳參
  5. 函數可複用

缺點: 調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)

推薦指數:★★★★(僅僅多消耗了一點內存)

3.6 寄生組合繼承

核心:經過寄生方式,砍掉父類的實例屬性,這樣,在調用兩次父類的構造的時候,就不會初始化兩次實例方法/屬性,避免的組合繼承的缺點

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
複製代碼

特色:堪稱完美

缺點:實現較爲複雜

推薦指數:★★★★

4. ES6 實現繼承

首先仍是建立一個 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 的對象、對象繼承以及原型鏈等知識也是要掌握的很牢固。

相關文章
相關標籤/搜索