1. 程式人生 > >全方位深入理解JavaScript面向物件

全方位深入理解JavaScript面向物件

JavaScript面向物件程式設計

本文會碰到的知識點:
原型、原型鏈、函式物件、普通物件、繼承

讀完本文,可以學到

  • 面向物件的基本概念
  • JavaScript物件屬性
  • 理解JavaScript中的函式物件與普通物件
  • 理解prototype和proto
  • 理解原型和原型鏈
  • 詳解原型鏈相關的Object方法
  • 瞭解如何用ES5模擬類,以及各種方式的優缺點
  • 瞭解如何用ES6實現面向物件

目錄

1. 面向物件的基本概念

面向物件也即是OOP,Object Oriented Programming,是計算機的一種程式設計架構,OOP的基本原則是計算機是由子程式作用的單個或者多個物件組合而成,包含屬性和方法的物件是類的例項,但是JavaScript中沒有類的概念,而是直接使用物件來實現程式設計。
特性:

  • 封裝:能夠將一個實體的資訊、功能、響應都封裝到一個單獨物件中的特性。

    由於JavaScript沒有public、private、protected這些關鍵字,但是可以利用變數的作用域來模擬public和private封裝特性

var insObject = (function() {
    var _name = 'hello'; // private
    return {
        getName: function() { // public
            return _name; 
        }
    }
})();


insObject._name; // undefined
insObject.getName(); // hello

這裡只是實現了一個簡單的版本,private比較好的實現方式可以參考深入理解ES6 145頁
protected可以利用ES6的Symbol關鍵字來實現,這裡不展開,有興趣可以討論

  • 繼承:在不改變源程式的基礎上進行擴充,原功能得以儲存,並且對子程式進行擴充套件,避免重複程式碼編寫,後面的章節詳細描述
  • 多型:允許將子類型別的指標賦值給父類型別的指標;原生JS是弱型別語言,沒有多型概念

    但是JavaScript也不是不能實現多型的概念,只是如果你之前是學靜態語言的同學,理解起來可能有些誤差。例子:

    比如我們有臺電腦mac, 它有一個方法system來獲取系統

    var mac = {
        system: function(){
           console.log('mac');
        }
    }
    
    var getSystem = function() {
        mac.system();  
    }
    
    getSystem();// mac

    某一天我們換成win,為了防止後面又換成mac,我們讓getSystem函式有一定的彈性。

     var mac = {
      system: function(){
           console.log('mac');
       }
     }
    
     var win = {
       system: function(){
           console.log('win');
       }
     }
    
     var getSystem = function(type) {
       if (type == 'mac') {
           mac.system();
       } else if (type == 'win') {
           win.system();
       }
     }
    
     getSystem('mac');// mac
     getSystem('win');// win

    但是很明顯這個函式還是有問題,某天我又換成centos呢。。。。我們改寫一下getSystem這個函式

    var getSystem = function(ins) {
        if (ins.system instanceOf Function) {
            ins.system();
        }
    }

    這裡我們是假設每個系統獲取系統的名稱都是system,實際開發過程中可能不會這樣,這種情況可以用介面卡模式來解決。

JavsScript中面向物件的一些概念:

  • 類class: ES5以前就是建構函式,ES6中有class
  • 例項instance和物件object:建構函式創建出來的物件一般稱為例項instance
  • 父類和子類:JavaScript也可以稱為父物件和子物件

2. JavaScript物件屬性

想弄懂面向物件,是不是先看看物件是啥呢?
我們先看一個題目:

[] + {}; // "[object Object]"
{} + []; // 0

解釋:
在第一行中,{}出現在+操作符的表示式中,因此被翻譯為一個實際的值(一個空object)。而[]被強制轉換為”“因此{}也會被強制轉換為一個string:”[object Object]”。
但在第二行中,{}被翻譯為一個獨立的{}空程式碼塊兒(它什麼也不做)。塊兒不需要分號來終結它們,所以這裡缺少分號不是一個問題。最終,+ []是一個將[]明確強制轉換 為number的表示式,而它的值是0

2.1 屬性

物件的屬性

  • Object.prototype Object 的原型物件,不是每個物件都有prototype屬性
  • Object.prototype.proto 不是標準方法,不鼓勵使用,每個物件都有proto屬性,但是由於瀏覽器實現方式的不同,proto屬性在chrome、firefox中實現了,在IE中並不支援,替代的方法是Object.getPrototypeOf()
  • Object.prototype.constructor:用於建立一個物件的原型,建立物件的建構函式

可能大家會有一個疑問,為什麼上面那些屬性要加上prototype
在chrome中列印一下var a = {}

屬性描述符

資料屬性:

特性名稱 描述 預設值
value 屬性的值 undfined
writable 是否可以修改屬性的值,true表示可以,false表示不可以 true
enumerable 屬性值是否可列舉,true表示可列舉for-in, false表示不可列舉 true
configurable 屬性的特性是否可配置,表示能否通過delete刪除屬性後重新定義屬性 true

例子:
這裡寫圖片描述

訪問器屬性:

特性名稱 描述 預設值
set 設定屬性時呼叫的函式 undefined
get 寫入屬性時呼叫的函式 undefined
configurable 表示能否通過delete刪除屬性後重新定義屬性 true
enumerable 表示能否通過for-in迴圈返回屬性 true

訪問器屬性不能直接定義,一般是通過Object.defineProperty()方法來定義,但是這個方法只支援IE9+, 以前一般用兩個非標準方法來實現__defineGetter__()֖__defineSetter__()
例子:

var book = { _year: 2004, edition: 1 };


Object.defineProperty(book, "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.2 方法

  • Object.prototype.toString() 返回物件的字串表示
  • Object.prototype.hasOwnProperty() 返回一個布林值,表示某個物件是否含有指定的屬性,而且此屬性非原型鏈繼承,也就是說不會檢查原型鏈上的屬性
  • Object.prototype.isPrototypeOf() 返回一個布林值,表示指定的物件是否在本物件的原型鏈中
  • Object.prototype.propertyIsEnumerable() 判斷指定屬性是否可列舉
  • Object.prototype.watch() 給物件的某個屬性增加監聽
  • Object.prototype.unwatch() 移除物件某個屬性的監聽
  • Object.prototype.valueOf() 返回指定物件的原始值
  • 獲取和設定屬性
    • Object.defineProperty 定義單個屬性
    • Object.defineProperties 定義多個屬性
    • Object.getOwnPropertyDescriptor 獲取屬性
  • Object.assign() 拷貝可列舉屬性 (ES6新增)
  • Object.create() 建立物件
  • Object.entries() 返回一個包含由給定物件所有可列舉屬性的屬性名和屬性值組成的 [屬性名,屬性值] 鍵值對的陣列,陣列中鍵值對的排列順序和使用for…in迴圈遍歷該物件時返回的順序一致
  • Object.freeze() 凍結一個物件,凍結指的是不能向這個物件新增新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該物件已有屬性的可列舉性、可配置性、可寫性。也就是說,這個物件永遠是不可變的。該方法返回被凍結的物件
  • Object.getOwnPropertyNames() 返回指定物件的屬性名組成的陣列
  • Object.getPrototypeOf 返回該物件的原型
  • Object.is(value1, value2) 判斷兩個值是否是同一個值 (ES6 新增)
  • Object.keys() 返回一個由給定物件的所有可列舉自身屬性的屬性名組成的陣列,陣列中屬性名的排列順序和使用for-in迴圈遍歷該物件時返回的順序一致
  • Object.setPrototypeOf(obj, prototype) 將一個指定的物件的原型設定為另一個物件或者null
  • Object.values 返回一個包含指定物件所有的可列舉屬性值的陣列,陣列中的值順序和使用for…in迴圈遍歷的順序一樣

2.3 應用

  • 如何檢測某個屬性是否在物件中?

    • in運算子,判斷物件是否包含某個屬性,會從物件的例項屬性、繼承屬性裡進行檢測
    function Dogs(name) {
        this.name = name
    }
    
    function BigDogs(size) {
        this.size = size;
    }
    
    BigDogs.prototype = new Dogs();
    
    var a = new BigDogs('big');
    
    'size' in a;
    'name' in a;
    'age' in a;
    • Object.hasOwnProperty(),判斷一個物件是否有指定名稱的屬性,不會檢查繼承屬性
    a.hasOwnProperty('size');
    a.hasOwnProperty('name');
    a.hasOwnProperty('age');
    • Object.propertyIsEnumerable(),判斷指定名稱的屬性是否為例項屬性並且是可列舉的
    // es6
    var a = Object.create({}, {
        name: {
            value: 'hello',
            enumerable: true,
        },
        age: {
            value: 11,
            enumerable: false,
        }
    });
    
    // es5
    var b = {};
    Object.defineProperties(b, {
        name: {
            value: 'hello',
            enumerable: true,
        },
        age: {
            value: 11,
            enumerable: false,
        } 
    });
    
    a.propertyIsEnumerable('name');
    a.propertyIsEnumerable('age');
  • 如何列舉物件的屬性,並保證不同了瀏覽器中的行為是一致的?

    • for/in 語句,可以遍歷可列舉的例項屬性和繼承屬性
    var a = {
      supername: 'super hello',
      superage: 'super name',
    }
    var b = {};
    Object.defineProperties(b, {
      name: {
          value: 'hello',
          enumerable: true,
      },
      age: {
          value: 11,
          enumerable: false,
      } 
    });
    
    Object.setPrototypeOf(b, a); // 設定b的原型式a 等效的是b.__proto__ = a
    
    for(pro in b) {
      console.log(pro); // name, supername, superage
    }
    • Object.keys(), 返回一個數組,內容是物件可列舉的例項屬性名稱
     var propertyArray = Object.keys(b);
     // name
    • Object.getOwnPropertyNames(),返回一個數組,內容是物件所有例項屬性,包括可列舉和不可列舉
     var propertyArray = Object.getOwnPropertyNames(b);
     // name, age
  • 如何判斷兩個物件是否相等?
    我只想說,這個問題說簡單很簡單,說複雜也挺複雜的傳送門
    我們看個簡單版的

    function isEquivalent(a, b) {
        var aProps = Object.getOwnPropertyNames(a);
        var bProps = Object.getOwnPropertyNames(b);
        if (aProps.length != bProps.length){
            return false;
        }
    
    
        for (var i = 0; i < aProps.length; i++) {
            var propName = aProps[i];
            if (a[propName] !== b[propName]) {
                return false;
            }
        }
        return true;
    }
    
    
    // Outputs: true
    console.log(isEquivalent({a:1},{a:1}));

    上面這個函式還有啥問題呢?

    • 沒有對傳入引數進行校驗,例如判斷是否是NaN,或者是其他內建屬性
    • 沒有判斷傳入物件的construct和prototype
    • 時間演算法複雜度是O(n2)

    有同學可能會有疑問,能不能用Object.is,答案是否定的,Object.is簡單來說就是在===的基礎上特別處理了NaN,+0,-0,保證了-0和+0不相同,Object.is(NaN, NaN)返回true

  • 物件的深拷貝和淺拷貝
    其實如果大家理解了上面的那些方法,是很容易寫出深拷貝和淺拷貝的程式碼的,我們先看一下這兩者的卻別。
    淺拷貝僅僅是複製引用,拷貝後a === b, 注意Object.assign方法實現的是淺複製(此處有深刻教訓!!!)
    深拷貝這是建立了一個新的物件,然後把舊的物件中的屬性和方法拷貝到新的物件中,拷貝後 a !== b
    深拷貝的實現由很多例子,例如jQuery的extend和lodash中的cloneDeep, clone。jQuery可以使用$.extend(true, {}, ...)來實現深拷貝, 但是jQuery無法複製JSON物件之外的物件,例如ES6引入的Map、Set等。而lodash加入的大量的程式碼來實現ES6新引入的標準物件
    這裡需要單獨研究分享/(ㄒoㄒ)/~~

3. 物件分為函式物件和普通物件

概念(什麼是函式物件和普通物件)

Object、Function、Array、Date等js的內建物件都是函式物件

問題:

function a1 () {}
const a2 = function () {}
const a3 = new Function();


const b1 = {};
const b2 = new Object();


const c1 = [];
const c2 = new Array();


const d1 = new a1();
const d2 = new b1();????
const d3 = new c1();????


typeof a1;
typeof a2;
typeof a3;


typeof b1;
typeof b2;


typeof c1;
typeof c2;


typeof d1;

上面兩行報錯的原因,是因為建構函式只能由函式來充當,而b1和c1不是Function的例項,所以不能充當構造器

但是隻有Function的例項都是函式物件、其他的例項都是普通物件

我們延伸一下,在看個例子

const e1 = function *(){};
const e2 = new e1();
// Uncaught TypeError: e1 is not a constructor
console.log(e1.constructor) // 是有值的。。。
// 規範裡面就不能new
const e2 = e1();

GeneratorFunction是一個特殊的函式物件
e1.__proto__.__proto__ === Function.prototype

e1的原型實際上是一個生成器函式GeneratorFunction,也就是說
e1.__proto__ === GeneratorFunction.prototype

這行程式碼有問題麼,啊哈哈哈,GeneratorFunction這個關鍵字主流的JavaScript還木有暴露出來,所以這個大家理解就好啦

雖然不能直接new e1
但是可以 new e1.constructor();哈哈哈哈

4. 理解prototype和proto

物件型別 prototype proto
函式物件 Yes Yes
普通物件 No Yes
  • 只有函式物件具有prototype這個屬性
  • prototype__proto__都是js在定義一個物件時的預定義屬性

  • prototype 被例項的__proto__指向

  • __proto__指向建構函式的prototype
const a = function(){}
const b = {}


typeof a // function
typeof b // object


typeof a.prototype // object
typeof a.__proto__ // function


typeof b.prototype // undefined
typeof b.__proto__ // object


a.__proto__ === Function.prototype
b.__proto__ === Object.prototype

理解了prototype__proto__之後,我們來看看之前一直說的為什麼JavaScript裡面都是物件

const a = {}
const b = function () {}
const c = []
const d = new Date()


a.__proto__
a.__proto__ === Object.prototype


b.__proto__
b.__proto__ === Function.prototype


c.__proto__
c.__proto__ === Array.prototype


d.__proto__
d.__proto__ === Date.prototype


Object.prototype.__proto__ //null


Function.prototype.__proto__ === Object.prototype


Array.prototype.__proto__ === Object.prototype


Date.prototype.__proto__ === Object.prototype

延伸一個問題:如何判斷一個變數是否是陣列?

  • typeof

我們上面已經解釋了,這些都是普通物件,普通物件是沒有prototype的,他們typeof的值都是object

typeof []
typeof {}
  • 從原型來看, 原理就是看Array是否在a的原型鏈中

a的原型鏈是 Array->Object

const a = [];
Array.prototype.isPrototypeOf(obj);
  • instanceof
const a = [];
a instanceof Array

從建構函式入手,但是這個方法和上面的方法都有一問題,不同的框架中建立的陣列不會相互共享其prototype屬性

  • 根據物件的class屬性,跨原型呼叫tostring方法
const a = [];
Object.prototype.toString.call(a);
// [Object Array]

ES5 中所有內建物件的[[Class]]屬性的值是由規範定義的,但是 ES6 中已經沒有了[[Class]]屬性,取代它的是[[NativeBrand]]屬性,這個大家有興趣可以自行去檢視規範
原理:
1. 如果this的值為undefined,則返回”[object Undefined]”.
2. 如果this的值為null,則返回”[object Null]”.
3. 讓O成為呼叫ToObject(this)的結果.
4. 讓class成為O的內部屬性[[Class]]的值.
5. 返回三個字串”[object “, class, 以及 “]”連線後的新字串.

問題?這個一定是正確的麼?不正確為啥?
提示ES6的Symbol屬性

  • Array.isArray()
    部分瀏覽器中不相容

桌面瀏覽器
這裡寫圖片描述
移動端瀏覽器
這裡寫圖片描述

5. 理解原型與原型鏈

其實上一節中的prototype和proto就是為了構建原型鏈而存在的,之前也或多或少的說到了原型鏈這個概念。

看下面的程式碼:

const Dogs = function(name) {
    this.name = name;
}


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


const jingmao = new Dogs('jingmao');
console.log(jingmao);
console.log(jingmao.getName());

這段程式碼的執行過程
1.首先建立了一個建構函式Dogs,傳入一個引數name,Dogs.prototype也會自動建立
2.給物件dogs增加了一個方法
3.通過建構函式Dogs例項化了一個物件jingmao
4.輸出jingmao的值
這裡寫圖片描述
可以看到jingmao有兩個值name和proto,其中proto指向Dogs.prototype
5.執行getName方法時,在jingmao中找不到這個方法,就會繼續向著原型鏈繼續往上找,也就是通過proto,然後就找到了getName方法。

這個過程實際上就是原型繼承,實際上JavaScript的原型繼承就是利用了proto並藉助prototype來實現的。

試一試下面 看輸出結果是啥?

jingmao.__proto__ === Function.prototype


Dogs.prototype 指向什麼
Dogs.prototype.__proto__ 指向什麼
Dogs.prototype.__proto__.__proto__ 指向什麼

上面例子中getName 最終是查詢到了,那麼如果在原型鏈中一直沒查詢到,會怎麼樣?
例如console.log(jingmao.age)

jingmao 是一個物件可以繼續
jingmao.age 不存在,繼續
jingmao.__proto__ 是一個物件可以繼續
jingmao.__proto__.age 不存在,繼續
jingmao.__proto__.__proto__ 是個物件可以繼續
jingmao.__proto__.__proto__.age 不存在,繼續
jingmao.__proto__.__proto__.__proto__ null,不是物件,到頭啦

原型鏈的概念其實不重要,重要的是要理解,簡單來說,原型鏈就是利用原型讓一個引用型別繼承另一個應用型別的屬性和方法。

最後我們用一張圖來結束本節
這裡寫圖片描述

Array.__proto__ === Function.prototype
Object.__proto__ === Function.prototype

還有三點需要注意的:

  • 任何內建函式物件(類)本身的 _proto_都指向 Function 的原型物件;
  • 除了 Object 的原型物件的_proto_ 指向 null,其他所有內建函式物件的原型物件的 _proto_ 都指向 object。
  • 所有建構函式的的prototype方法的proto都指向Object.prototype(除了….Object.prototype自身)

如果理解了上面這些內容,大家可以自行描述一下,建構函式、原型和例項之間的關係,也可以舉例說明

function Dogs (name) {
    this.name = name;
}


var jingmao = new Dogs('jingmao');

這個圖大家腦子裡面自己構想一下?

解釋:
建構函式首字母必須大寫,用來區分普通函式,內部使用this指標,指向要生成的例項物件,通過new來生成例項物件。
例項就是通過new一個建構函式產生的物件,它有一個屬性[[prototype]]指向原型
原型中有一個屬性[[constructor]],指向建構函式

6.與原型鏈相關的方法

這裡只是簡單介紹一下

6.1 hasOwnProperty

Object.hasOwnProperty() 返回一個布林值,表示某個物件的例項是否含有指定的屬性,而且此屬性非原型鏈繼承。用來判斷屬性是來自例項屬性還是原型屬性。類似還有in操作符,in操作符只要屬性存在,不管實在例項中還是原型中,就會返回true。同時使用in和hasOwnProperty就可以判斷屬性是在原型中還是在例項中

const Dogs = function (age) {
    this.age = age
}


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


const jingmao = new Dogs(14);


jingmao.hasOwnProperty(age);

6.2 isPrototypeOf

Object.prototype.isPrototypeOf() 返回一個布林值,表示指定的物件是否在本物件的原型鏈中

const Dogs = function (age) {
    this.age = age
}


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


const jingmao = new Dogs(11);
Object.prototype.isPrototypeOf(Dogs);
Dogs.prototype.isPrototypeOf(jingmao);

6.3 getPrototypeOf

Object.getPrototypeOf 返回該物件的原型

const Dogs = function (age) {
    this.age = age
}


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


const jingmao = new Dogs(11);


jingmao.__proto__ === Object.getPrototypeOf(jingmao) 

7. ES5 物件繼承

7.1 原型繼承

原型繼承就是利用原型鏈來實現繼承

function SuperType() {
    this.supername = 'super';
}


SuperType.prototype.getSuperName= function(){
    return this.supername;
}


function SubType () {
    this.subname='subname';
}


SubType.prototype = new SuperType();


SubType.prototype.getSubName = function (){
    return this.subname;
}


var instance1 = new SubType();
console.log(instance1.getSubName());
console.log(instance1.getSuperName());

這裡寫圖片描述

需要注意的地方:
實現原型繼承的時候不要使用物件字面量建立原型方法,因為這樣做,會重寫原型鏈。

function SuperType() {
    this.supername = 'super';
}


SuperType.prototype.getSuperName= function(){
    return this.supername;
}


function SubType () {
    this.subname='subname';
}


SubType.prototype = new SuperType();


SubType.prototype =  {
    getSubName: function (){
        return this.subname;
    }
}


var instance1 = new SubType();
console.log(instance1.getSubName());
console.log(instance1.getSuperName()); // error

這裡寫圖片描述

上面使用SubType.prototype = {...}之後,SubType的原型就是Object了,而不是SuperType了。

優點:原型定義的屬性和方法可以複用
缺點:
1. 引用型別的原型屬性會被所有例項共享
2. 建立子物件時,不能向父物件的建構函式中傳遞引數

7.2 建構函式繼承

這裡的例子來源是JavaScript高階程式設計

在說建構函式繼承之前,我們先看一個例子

var a = {
    name: 'a',
};


var name = 'window';


var getName = function(){
    console.log(this.name);
}


getName() // 輸出window
getName.call(a) // 輸出a

執行getName()時,函式體的this指向window,而執行getName.call(a)時,函式體的this指向的是a物件,所以就可以理解啦。接下來我們看如何實現建構函式繼承

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


function SubType () {
    // 繼承SuperType
    SuperType.call(this);
}


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


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

SuperType.call(this); 這一行程式碼,實際上意思是在SubType的例項初始化過程中,呼叫了SuperType的建構函式,因此SubType的每個例項都有colors這個屬性

優點:子物件可以傳遞引數給父物件。

function SuperType(name) {
    this.name = name;
}
function SubType(name, age) {
    name = name || 'hello';
    SuperType.call(this, name);
    this.age = age;
}


var instance1 = new SubType('scofield', 28);
console.log(instance1.name);
console.log(instance1.age);

需要注意的地方是在呼叫父物件的建構函式之後,再給子型別中的定義屬性,否則會被重寫。

缺點:方法都需要在建構函式中定義,難以做到函式的複用,而且在父物件的原型上定義的方法,對於子型別是不可見的。 ??? 為什麼不可見

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


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


SuperType.prototype.prefix = function() {
    return 'prefix';
}


function SubType(name) {
    SuperType.call(this, name);
}


var instance1 = new SubType('scofield');
console.log(instance1.name);
console.log(instance1.prefix);
console.log(instance1.getName());
// Uncaught TypeError: instance1.getName is not a function

7.2 組合式繼承

組合式繼承顧名思義,就是組合兩種模式實現JavaScript的繼承,藉助原型鏈和建構函式來實現。這樣子在原型上定義方法實現了函式的複用,而且能夠保證每個例項都有自己的屬性。

function SuperType (name) {
    this.name = name;
    this.con = [];
}


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


function SubType (name, age) {
    SuperType.call(this, name);
    this.age = age;
}


SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.getAge = function() {
    return this.age;
};


var instance1 = new SubType('li', 18);
instance1.con.push('test1');
console.log(instance1.con); // test1
console.log(instance1.getAge()); // 18
console.log(instance1.getName()); // li


var instance2 = new SubType('hang', 18);
console.log(instance1.con); // test1
console.log(instance1.getAge()); // 18
console.log(instance1.getName()); // hang

優點:彌補了原型繼承和建構函式的缺點
缺點:父類建構函式呼叫了兩次

7.3 原型式繼承

原型式繼承並沒有使用嚴格意義上的建構函式,藉助原型可以基於已有的物件建立新的物件,例如:

function createObject(o) {
    function newOrient () {};
    newOrient.prototype = o;
    return new newOrient();
}

簡單來說createObject函式,對傳入的o物件進行的一次淺拷貝。在ES5中新增加了一個方法Object.create(), 它的作用和createObject是一樣的,但是隻支援IE9+。

var Dogs = {
    name: 'jingmao',
    age: 1
}


var BigDogs = Object.create(Dogs);
BigDogs.name= 'bigjingmao';
BigDogs.size = 'big';
console.log(BigDogs.age);

其中Object.create還支援傳入第二個引數,引數與Object.defineProperties()方法的格式相同,並且會覆蓋原型上的同名屬性。

7.4 寄生式繼承

寄生式繼承其實和原型式繼承很類似,區別在於,寄生式繼承建立的一個函式把所有的事情做完了,例如給新的物件增加屬性和方法。

function createAnother(o) {
    var clone = Object.create(o);
    clone.size = 'big';
    return clone;
}


var Dogs = {
    name: 'jingmao',
    age: 1
}


var BigDogs = createAnother(Dogs);
console.log(BigDogs.size);

7.5 寄生組合式繼承

到最後一個了,看看我們之前遺留的問題:
組合繼承會呼叫兩次父物件的建構函式,並且父型別的屬性存在兩組,一組在例項上,一組在SubType的原型上。解決這個問題的方法就是寄生組合式繼承。

function inheritPrototype(subType, superType){ 
    // 繼承父類的原型
    var prototype = Object.create(superType.prototype);
    // 重寫被汙染的construct
    prototype.constructor = subType; 
    // 重寫子類的原型  
    subType.prototype = prototype; 
}

這個函式就是寄生組合式繼承的最簡單的實現方式

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


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


function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}


inheritPrototype(SubType, SuperType);


SubType.prototype.sayAge = function(){ 
    alert(this.age); 
};


var instance1 = new SubType('hello', 18);


instance1.__proto__.constructor == SubType

這裡寫圖片描述
可以看到
1. 子類繼承了父類的屬性和方法,同時屬性沒有建立在原型鏈上,因此多個子類不會共享同一個屬性。
2. 子類可以動態傳遞引數給父類
3. 父類建構函式只執行了一次

但是還有一個問題:
子類如果在原型上新增方法,必須要在繼承之後新增,否則會覆蓋原來原型上的方法。但是如果這兩個類是已存在的類,就不行了

優化一下:

function inheritPrototype(subType, superType){ 
    // 繼承父類的原型
    var prototype = Object.create(superType.prototype);
    // 重寫被汙染的construct
    prototype.constructor = subType; 
    // 重寫子類的原型  
    subType.prototype = Object.assign(prototype, subType.prototype); 
}

雖然通過Object.assign來進行copy解決了覆蓋原型型別的方法的問題,但是Object.assign只能夠拷貝可列舉的方法,而且如果子類本身就繼承了一個類,這個辦法也不行。

8. ES6 實現繼承

我們知道了ES5中可以通過原型鏈來實現繼承,ES6提供了extends關鍵字來實現繼承,這相對而言更加清晰和方便,首先看看ES6 Class的語法,此處參考http://es6.ruanyifeng.com/#docs/class

8.1 Class基本語法

1.需要注意的地方。ES6 中類內部定義的所有方法都是不可列舉的
類的屬性名稱可以使用表示式(區別1)

2.嚴格模式,ES6 class類和模組內部預設是嚴格模式

3.construct方法
也就是類的預設方法,如果沒有顯示的定義,那麼會新增一個空的contruct方法
返回值:預設返回例項物件,也就是this,當然也可以顯式的返回另外一個物件。
例如:

Class Foo {
    constructor() {
    }
}


new Foo() instanceof Foo // true


Class FakeFoo {
    constructor() {
        return Object.create(null);
    }
}


new Foo() instanceof Foo // false

此外類必須通過new 操作符來呼叫,否則會報錯,這個它與普通的建構函式的區別

Foo()


// TypeError: Class constructor Foo cannot be invoked without 'new'

4.類的例項物件

類的例項的屬性,除非顯式的定義在this上,否則都是定義在原型上,這裡與ES5保持一致

5.類的表示式

與函式一樣,類也可以用表示式的方式來定義

const HClass = class Me {
    getClassName() {
        return Me.name;
    }
}


const hIns = new HClass();
HClass.getClassName(); // Me
Me.getClassName(); // error

這裡只有HClass是暴露在外部的,Me只有在class的內部使用,如果不需要使用Me,完全可以省略

那麼我們知道利用函式表示式可以建立一個立即執行函式,類可以麼?



let person = new class {
    constructor(name) {
        this.name = name;
    },
    sayName() {
        console.log(this.name);
    }
}('jack');


persion.sayName()

6.不存在變數提升
這點是和ES5不一樣的, ES6並不會把class的宣告提到當前作用域的頂部,這與下一節的繼承有關係

new Foo()
class Foo {}

7.私有屬性和私有方法

私有方法ES6並不提供,但是可以變通

  • 命名區分
  • 把方法移出模組
  • 利用Symbol來命名方法名
const getAge = Symbol('getAge');


export defalut class Person {
    // 公有方法
    getName(name) {
        return name;
    },
    // 私有方法
   [getAge](age) {
    return age;
   }
}

私有屬性ES6也不支援,有提案說加個#表示私有屬性

8.this的指向(仔細看看)
類的內部this的指向預設是指向this的例項的,如果單獨使用類中的一些包含this的方法,很有可能會報錯

class Logger {
    printName (name = 'there') {
        this.print(`Hello ${name}`);
    },
    print (text) {
        console.log(text);
    }
}


const logger = new Logger();
const {printName} = logger;
printName();
// Uncaught TypeError: Cannot read property 'print' of undefined
logger.printName()
// Hello there

解決辦法:

  • 在建構函式中繫結this,這樣就不會找不到print方法了
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }


  // ...
}
  • 在建構函式中使用箭頭函式
  • 使用proxy代理函式,包裝

9.name屬性

10.class中使用get和set函式,可以用來攔截這個屬性的存取行為,利用getOwnPropertyDescriptor來檢視屬性的get和set函式是否有定義

11.如果在類裡面在某個方法上加上*,則表示這個方法是Generator函式

12.在類的某個方法前面加上static關鍵字,表示這個方法是靜態方法,這個方法不會被例項繼承,只能夠通過類來呼叫,如果這個靜態方法中有this,那麼this指向的是類,而不是例項
此外靜態方法,和非靜態方法是可以重名滴

class Foo {
  static bar () {
    this.baz();
  }
  static baz () {
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}


Foo.bar() // hello

父類的靜態方法可以被子類繼承

13.類的靜態屬性,也就是說是通過類直接訪問的屬性

Class Foo {
    p = 1,

    static: 1,
}

上面的兩種方法都是錯誤的,目前靜態屬性還處於提案中,

Class Foo {
    p = 1static p = 1;
}

以前我們定義例項屬性只能夠在construct中定義

14.new.target屬性, new.target返回new命令作用的那個建構函式,如果沒有通過new來例項物件,那麼這個屬性的值是undefined

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {