全方位理解JavaScript面向?qū)ο?/h2>

JavaScript面向?qū)ο蟪绦蛟O計

本文會碰到的知識點:
原型、原型鏈、函數(shù)對象、普通對象、繼承

讀完本文,可以學到

  • 面向?qū)ο蟮幕靖拍?/li>
  • JavaScript對象屬性
  • 理解JavaScript中的函數(shù)對象與普通對象
  • 理解prototype和proto
  • 理解原型和原型鏈
  • 詳解原型鏈相關的Object方法
  • 了解如何用ES5模擬類,以及各種方式的優(yōu)缺點
  • 了解如何用ES6實現(xiàn)面向?qū)ο?/li>

目錄

1. 面向?qū)ο蟮幕靖拍?/h2>

面向?qū)ο笠布词荗OP,Object Oriented Programming,是計算機的一種編程架構(gòu),OOP的基本原則是計算機是由子程序作用的單個或者多個對象組合而成,包含屬性和方法的對象是類的實例,但是JavaScript中沒有類的概念,而是直接使用對象來實現(xiàn)編程。
特性:

  • 封裝:能夠?qū)⒁粋€實體的信息、功能、響應都封裝到一個單獨對象中的特性。

    由于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

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

  • 繼承:在不改變源程序的基礎上進行擴充,原功能得以保存,并且對子程序進行擴展,避免重復代碼編寫,后面的章節(jié)詳細描述

  • 多態(tài):允許將子類類型的指針賦值給父類類型的指針;原生JS是弱類型語言,沒有多態(tài)概念

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

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

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

    某一天我們換成win,為了防止后面又換成mac,我們讓getSystem函數(shù)有一定的彈性。

     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
    

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

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

    這里我們是假設每個系統(tǒng)獲取系統(tǒng)的名稱都是system,實際開發(fā)過程中可能不會這樣,這種情況可以用適配器模式來解決。

JavsScript中面向?qū)ο蟮囊恍└拍睿?/p>

  • 類class: ES5以前就是構(gòu)造函數(shù),ES6中有class
  • 實例instance和對象object:構(gòu)造函數(shù)創(chuàng)建出來的對象一般稱為實例instance
  • 父類和子類:JavaScript也可以稱為父對象和子對象

2. JavaScript對象屬性

想弄懂面向?qū)ο?,是不是先看看對象是啥呢?br> 我們先看一個題目:

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

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

2.1 屬性

對象的屬性

  • Object.prototype Object 的原型對象,不是每個對象都有prototype屬性
  • Object.prototype.proto 不是標準方法,不鼓勵使用,每個對象都有proto屬性,但是由于瀏覽器實現(xiàn)方式的不同,proto屬性在chrome、firefox中實現(xiàn)了,在IE中并不支持,替代的方法是Object.getPrototypeOf()
  • Object.prototype.constructor:用于創(chuàng)建一個對象的原型,創(chuàng)建對象的構(gòu)造函數(shù)

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

15208193887037.jpg

屬性描述符

數(shù)據(jù)屬性:

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

例子:

15208197547965.jpg

訪問器屬性:

特性名稱 描述 默認值
set 設置屬性時調(diào)用的函數(shù) undefined
get 寫入屬性時調(diào)用的函數(shù) undefined
configurable 表示能否通過delete刪除屬性后重新定義屬性 true
enumerable 表示能否通過for-in循環(huán)返回屬性 true

訪問器屬性不能直接定義,一般是通過Object.defineProperty()方法來定義,但是這個方法只支持IE9+, 以前一般用兩個非標準方法來實現(xiàn)__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() 給對象的某個屬性增加監(jiān)聽
  • Object.prototype.unwatch() 移除對象某個屬性的監(jiān)聽
  • Object.prototype.valueOf() 返回指定對象的原始值
  • 獲取和設置屬性
    • Object.defineProperty 定義單個屬性
    • Object.defineProperties 定義多個屬性
    • Object.getOwnPropertyDescriptor 獲取屬性
  • Object.assign() 拷貝可枚舉屬性 (ES6新增)
  • Object.create() 創(chuàng)建對象
  • Object.entries() 返回一個包含由給定對象所有可枚舉屬性的屬性名和屬性值組成的 [屬性名,屬性值] 鍵值對的數(shù)組,數(shù)組中鍵值對的排列順序和使用for…in循環(huán)遍歷該對象時返回的順序一致
  • Object.freeze() 凍結(jié)一個對象,凍結(jié)指的是不能向這個對象添加新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該對象已有屬性的可枚舉性、可配置性、可寫性。也就是說,這個對象永遠是不可變的。該方法返回被凍結(jié)的對象
  • Object.getOwnPropertyNames() 返回指定對象的屬性名組成的數(shù)組
  • Object.getPrototypeOf 返回該對象的原型
  • Object.is(value1, value2) 判斷兩個值是否是同一個值 (ES6 新增)
  • Object.keys() 返回一個由給定對象的所有可枚舉自身屬性的屬性名組成的數(shù)組,數(shù)組中屬性名的排列順序和使用for-in循環(huán)遍歷該對象時返回的順序一致
  • Object.setPrototypeOf(obj, prototype) 將一個指定的對象的原型設置為另一個對象或者null
  • Object.values 返回一個包含指定對象所有的可枚舉屬性值的數(shù)組,數(shù)組中的值順序和使用for…in循環(huán)遍歷的順序一樣

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(), 返回一個數(shù)組,內(nèi)容是對象可枚舉的實例屬性名稱
     var propertyArray = Object.keys(b);
     // name
    
    • Object.getOwnPropertyNames(),返回一個數(shù)組,內(nèi)容是對象所有實例屬性,包括可枚舉和不可枚舉
     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}));
```

上面這個函數(shù)還有啥問題呢?

  • 沒有對傳入?yún)?shù)進行校驗,例如判斷是否是NaN,或者是其他內(nèi)置屬性
  • 沒有判斷傳入對象的construct和prototype
  • 時間算法復雜度是O(n2)

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

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

3. 對象分為函數(shù)對象和普通對象

概念(什么是函數(shù)對象和普通對象)

Object、Function、Array、Date等js的內(nèi)置對象都是函數(shù)對象

問題:

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;

上面兩行報錯的原因,是因為構(gòu)造函數(shù)只能由函數(shù)來充當,而b1和c1不是Function的實例,所以不能充當構(gòu)造器

但是只有Function的實例都是函數(shù)對象、其他的實例都是普通對象

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

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

GeneratorFunction是一個特殊的函數(shù)對象
e1.__proto__.__proto__ === Function.prototype

e1的原型實際上是一個生成器函數(shù)GeneratorFunction,也就是說
e1.__proto__ === GeneratorFunction.prototype

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

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

4. 理解prototype和proto

對象類型 prototype proto
函數(shù)對象 Yes Yes
普通對象 No Yes
  • 只有函數(shù)對象具有prototype這個屬性

  • prototype__proto__都是js在定義一個對象時的預定義屬性

  • prototype 被實例的__proto__指向

  • __proto__指向構(gòu)造函數(shù)的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

延伸一個問題:如何判斷一個變量是否是數(shù)組?

  • typeof

我們上面已經(jīng)解釋了,這些都是普通對象,普通對象是沒有prototype的,他們typeof的值都是object

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

a的原型鏈是 Array->Object

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

從構(gòu)造函數(shù)入手,但是這個方法和上面的方法都有一問題,不同的框架中創(chuàng)建的數(shù)組不會相互共享其prototype屬性

  • 根據(jù)對象的class屬性,跨原型調(diào)用tostring方法
const a = [];
Object.prototype.toString.call(a);
// [Object Array]

ES5 中所有內(nèi)置對象的[[Class]]屬性的值是由規(guī)范定義的,但是 ES6 中已經(jīng)沒有了[[Class]]屬性,取代它的是[[NativeBrand]]屬性,這個大家有興趣可以自行去查看規(guī)范
原理:

  1. 如果this的值為undefined,則返回"[object Undefined]".
  2. 如果this的值為null,則返回"[object Null]".
  3. 讓O成為調(diào)用ToObject(this)的結(jié)果.
  4. 讓class成為O的內(nèi)部屬性[[Class]]的值.
  5. 返回三個字符串"[object ", class, 以及 "]"連接后的新字符串.

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

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

桌面瀏覽器


15209337258039.jpg

移動端瀏覽器


15209337373169.jpg

5. 理解原型與原型鏈

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

看下面的代碼:

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());

這段代碼的執(zhí)行過程
1.首先創(chuàng)建了一個構(gòu)造函數(shù)Dogs,傳入一個參數(shù)name,Dogs.prototype也會自動創(chuàng)建
2.給對象dogs增加了一個方法
3.通過構(gòu)造函數(shù)Dogs實例化了一個對象jingmao
4.輸出jingmao的值


15203928680236.jpg

可以看到jingmao有兩個值name和proto,其中proto指向Dogs.prototype
5.執(zhí)行g(shù)etName方法時,在jingmao中找不到這個方法,就會繼續(xù)向著原型鏈繼續(xù)往上找,也就是通過proto,然后就找到了getName方法。

這個過程實際上就是原型繼承,實際上JavaScript的原型繼承就是利用了proto并借助prototype來實現(xiàn)的。

試一試下面 看輸出結(jié)果是啥?

jingmao.__proto__ === Function.prototype

Dogs.prototype 指向什么
Dogs.prototype.__proto__ 指向什么
Dogs.prototype.__proto__.__proto__ 指向什么

上面例子中g(shù)etName 最終是查找到了,那么如果在原型鏈中一直沒查找到,會怎么樣?
例如console.log(jingmao.age)

jingmao 是一個對象可以繼續(xù)
jingmao.age 不存在,繼續(xù)
jingmao.__proto__ 是一個對象可以繼續(xù)
jingmao.__proto__.age 不存在,繼續(xù)
jingmao.__proto__.__proto__ 是個對象可以繼續(xù)
jingmao.__proto__.__proto__.age 不存在,繼續(xù)
jingmao.__proto__.__proto__.__proto__ null,不是對象,到頭啦

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

最后我們用一張圖來結(jié)束本節(jié)


數(shù)據(jù)庫升級流程 -1--1.png
Array.__proto__ === Function.prototype
Object.__proto__ === Function.prototype

還有三點需要注意的:

  • 任何內(nèi)置函數(shù)對象(類)本身的 proto都指向 Function 的原型對象;
  • 除了 Object 的原型對象的proto 指向 null,其他所有內(nèi)置函數(shù)對象的原型對象的 proto 都指向 object。
  • 所有構(gòu)造函數(shù)的的prototype方法的proto都指向Object.prototype(除了....Object.prototype自身)

如果理解了上面這些內(nèi)容,大家可以自行描述一下,構(gòu)造函數(shù)、原型和實例之間的關系,也可以舉例說明

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

var jingmao = new Dogs('jingmao');

這個圖大家腦子里面自己構(gòu)想一下?

解釋:
構(gòu)造函數(shù)首字母必須大寫,用來區(qū)分普通函數(shù),內(nèi)部使用this指針,指向要生成的實例對象,通過new來生成實例對象。
實例就是通過new一個構(gòu)造函數(shù)產(chǎn)生的對象,它有一個屬性[[prototype]]指向原型
原型中有一個屬性[[constructor]],指向構(gòu)造函數(shù)

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 原型繼承

原型繼承就是利用原型鏈來實現(xiàn)繼承

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());
15204911058742.jpg

需要注意的地方:
實現(xiàn)原型繼承的時候不要使用對象字面量創(chuàng)建原型方法,因為這樣做,會重寫原型鏈。

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

15204916934445.jpg

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


優(yōu)點:原型定義的屬性和方法可以復用
缺點:

  1. 引用類型的原型屬性會被所有實例共享
  2. 創(chuàng)建子對象時,不能向父對象的構(gòu)造函數(shù)中傳遞參數(shù)

7.2 構(gòu)造函數(shù)繼承

這里的例子來源是JavaScript高級程序設計

在說構(gòu)造函數(shù)繼承之前,我們先看一個例子

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

var name = 'window';

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

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

執(zhí)行g(shù)etName()時,函數(shù)體的this指向window,而執(zhí)行g(shù)etName.call(a)時,函數(shù)體的this指向的是a對象,所以就可以理解啦。接下來我們看如何實現(xiàn)構(gòu)造函數(shù)繼承

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的實例初始化過程中,調(diào)用了SuperType的構(gòu)造函數(shù),因此SubType的每個實例都有colors這個屬性

優(yōu)點:子對象可以傳遞參數(shù)給父對象。

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);

需要注意的地方是在調(diào)用父對象的構(gòu)造函數(shù)之后,再給子類型中的定義屬性,否則會被重寫。

缺點:方法都需要在構(gòu)造函數(shù)中定義,難以做到函數(shù)的復用,而且在父對象的原型上定義的方法,對于子類型是不可見的。 ??? 為什么不可見

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 組合式繼承

組合式繼承顧名思義,就是組合兩種模式實現(xiàn)JavaScript的繼承,借助原型鏈和構(gòu)造函數(shù)來實現(xiàn)。這樣子在原型上定義方法實現(xiàn)了函數(shù)的復用,而且能夠保證每個實例都有自己的屬性。

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

優(yōu)點:彌補了原型繼承和構(gòu)造函數(shù)的缺點
缺點:父類構(gòu)造函數(shù)調(diào)用了兩次

7.3 原型式繼承

原型式繼承并沒有使用嚴格意義上的構(gòu)造函數(shù),借助原型可以基于已有的對象創(chuàng)建新的對象,例如:

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

簡單來說createObject函數(shù),對傳入的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還支持傳入第二個參數(shù),參數(shù)與Object.defineProperties()方法的格式相同,并且會覆蓋原型上的同名屬性。

7.4 寄生式繼承

寄生式繼承其實和原型式繼承很類似,區(qū)別在于,寄生式繼承創(chuàng)建的一個函數(shù)把所有的事情做完了,例如給新的對象增加屬性和方法。

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 寄生組合式繼承

到最后一個了,看看我們之前遺留的問題:
組合繼承會調(diào)用兩次父對象的構(gòu)造函數(shù),并且父類型的屬性存在兩組,一組在實例上,一組在SubType的原型上。解決這個問題的方法就是寄生組合式繼承。

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

這個函數(shù)就是寄生組合式繼承的最簡單的實現(xiàn)方式

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
15204945107427.jpg

可以看到

  1. 子類繼承了父類的屬性和方法,同時屬性沒有創(chuàng)建在原型鏈上,因此多個子類不會共享同一個屬性。
  2. 子類可以動態(tài)傳遞參數(shù)給父類
  3. 父類構(gòu)造函數(shù)只執(zhí)行了一次

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

優(yōu)化一下:

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 實現(xiàn)繼承

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

8.1 Class基本語法

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

2.嚴格模式,ES6 class類和模塊內(nèi)部默認是嚴格模式

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 操作符來調(diào)用,否則會報錯,這個它與普通的構(gòu)造函數(shù)的區(qū)別

Foo()

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

4.類的實例對象

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

5.類的表達式

與函數(shù)一樣,類也可以用表達式的方式來定義

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

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

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

那么我們知道利用函數(shù)表達式可以創(chuàng)建一個立即執(zhí)行函數(shù),類可以么?


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

persion.sayName()

6.不存在變量提升
這點是和ES5不一樣的, ES6并不會把class的聲明提到當前作用域的頂部,這與下一節(jié)的繼承有關系

new Foo()
class Foo {}

7.私有屬性和私有方法

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

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

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

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

8.this的指向(仔細看看)
類的內(nèi)部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

解決辦法:

  • 在構(gòu)造函數(shù)中綁定this,這樣就不會找不到print方法了
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}
  • 在構(gòu)造函數(shù)中使用箭頭函數(shù)
  • 使用proxy代理函數(shù),包裝

9.name屬性

10.class中使用get和set函數(shù),可以用來攔截這個屬性的存取行為,利用getOwnPropertyDescriptor來查看屬性的get和set函數(shù)是否有定義

11.如果在類里面在某個方法上加上*,則表示這個方法是Generator函數(shù)

12.在類的某個方法前面加上static關鍵字,表示這個方法是靜態(tài)方法,這個方法不會被實例繼承,只能夠通過類來調(diào)用,如果這個靜態(tài)方法中有this,那么this指向的是類,而不是實例
此外靜態(tài)方法,和非靜態(tài)方法是可以重名滴

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

Foo.bar() // hello

父類的靜態(tài)方法可以被子類繼承

13.類的靜態(tài)屬性,也就是說是通過類直接訪問的屬性

Class Foo {
    p = 1,
    
    static: 1,
}

上面的兩種方法都是錯誤的,目前靜態(tài)屬性還處于提案中,

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

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

14.new.target屬性, new.target返回new命令作用的那個構(gòu)造函數(shù),如果沒有通過new來實例對象,那么這個屬性的值是undefined

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必須使用 new 命令生成實例');
  }
}

var person = new Person('Jack'); // 正確
var notAPerson = Person.call(person, 'Jack');  // 報錯

在Class內(nèi)部調(diào)用的時候,new.target返回當前的Class,需要注意一點就是當子類繼承父類的時候,返回當前的Class

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj = new Square(3); // 輸出 false

利用這個特點我們可以寫出這樣的代碼

class Rectangle {
  constructor(length, width) {
    if(new.Target === Rectangle) {
     throw new Error('本類不能實例化');
    }
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj = new Square(3);
var notobj = new Rectangle();

8.2 Class的繼承

1.基本概念

Class可以通過extends關鍵字來實現(xiàn)繼承,而ES5中是通過修改原型鏈來實現(xiàn)繼承

子類必須在constructor中調(diào)用super方法,否則新建實例的時候會報錯,因為子類沒有自己的this,是繼承與父類,然后進行加工。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

我們回憶一下ES5的繼承,實質(zhì)是首先創(chuàng)建了子類的實例對象,然后把父類的方法添加到子類上。而ES6是先創(chuàng)建父類的實例對象,然后再用子類的構(gòu)造函數(shù)修改this,如果子類沒有添加constructor,這個方法會被自動添加

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

還有一點需要注意,在子類的構(gòu)造函數(shù)中,只有調(diào)用super后,才可以使用this關鍵字,否則會報錯

2.super關鍵字,super可以作為函數(shù)和對象來使用

  • super作為函數(shù)調(diào)用時代表父類的構(gòu)造函數(shù),這里代表A的構(gòu)造函數(shù),但是返回的是B的實例。作為函數(shù)調(diào)用時,只能在子類的構(gòu)造函數(shù)調(diào)用,如果在其他地方調(diào)用會報錯。
class A {}

class B extends A {
  constructor() {
    super();
    // 等價于A.prototype.constructor.call(this)
  }
}
  • super作為對象,在普通的方法中,指向父類的原型對象;在靜態(tài)函數(shù)中,指向父類。
class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() 

ES6 規(guī)定,通過super調(diào)用父類的方法時,方法內(nèi)部的this指向當前的子類實例

由于this指向子類的實例,當對super的一個屬性復制的時候,賦值會變成子類的屬性

3.ES6的proto和prototype

我們知道在ES5中,每個對象的proto屬性,指向?qū)獦?gòu)造函數(shù)的prototype。而ES6里面有兩條繼承鏈路,先看一個例子

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
  • 子類的proto屬性指向父類,表示構(gòu)造函數(shù)的繼承
  • 子類的原型的proto指向父類的原型,表示方法的繼承
class A {
}

class B {
}

// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 繼承 A 的靜態(tài)屬性
Object.setPrototypeOf(B, A);

const b = new B();

再看下一個問題,我們知道ES6是通過extends關鍵字來實現(xiàn)繼承的,那么extends后面的值可以是什么類型呢?我們根據(jù)上的兩條繼承鏈路就知道,父類應該要有prototype屬性,也就是說函數(shù)都可以作為父類被繼承,此外我們看3中特殊情況

  • 子類繼承于Object類
class A extends Object {
}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
  • 不存在繼承
class A {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
  • 子類繼承null
class A extends null {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
  1. 原生構(gòu)造函數(shù)的繼承

我們知道,以前原生構(gòu)造函數(shù)是無法繼承的,原因是因為子類無法獲得原生構(gòu)造函數(shù)的內(nèi)部屬性。原生構(gòu)造函數(shù)會忽略apply方法傳入的this,也就是說,原生構(gòu)造函數(shù)的this無法綁定,導致拿不到內(nèi)部屬性

function MyArray() {
  Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});

var colors = new MyArray();
colors[0] = "red";
colors.length  // 0

colors.length = 0;
colors[0]  // "red"

ES6 允許繼承原生構(gòu)造函數(shù)定義子類,因為 ES6 是先新建父類的實例對象this,然后再用子類的構(gòu)造函數(shù)修飾this,使得父類的所有行為都可以繼承。因此我們可以自定義原生數(shù)據(jù)結(jié)構(gòu)的子類,這些是ES5無法做到的

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

6.Mixin的實現(xiàn),也就是將多個對象合并成一個對象

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}

上面是一個比較簡單的做法,我們看一個完整的實現(xiàn)方式

function mix(...mixins) {
  class Mix {}

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷貝實例屬性
    copyProperties(Mix.prototype, mixin.prototype); // 拷貝原型屬性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== "constructor"
      && key !== "prototype"
      && key !== "name"
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}

8.3 總結(jié),ES5和ES6面向?qū)ο蟮膮^(qū)別

9.AOP 面向切面編程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容