JavaScript面向對象程序設計
轉載:https://blog.csdn.net/lihangxiaoji/article/details/79753473#72__871本文會碰到的知識點:
原型、原型鏈、函數對象、普通對象、繼承
讀完本文,可以學到
- 面向對象的基本概念
- 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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
這里只是實現了一個簡單的版本,private比較好的實現方式可以參考深入理解ES6 145頁
protected可以利用ES6的Symbol關鍵字來實現,這里不展開,有興趣可以討論
-
繼承:在不改變源程序的基礎上進行擴充,原功能得以保存,並且對子程序進行擴展,避免重復代碼編寫,后面的章節詳細描述
-
多態:允許將子類類型的指針賦值給父類類型的指針;原生JS是弱類型語言,沒有多態概念但是JavaScript也不是不能實現多態的概念,只是如果你之前是學靜態語言的同學,理解起來可能有些誤差。例子:
比如我們有台電腦mac, 它有一個方法system來獲取系統
var mac = { system: function(){ console.log('mac'); } }
var getSystem = function() {
mac.system();
}getSystem();// mac
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
某一天我們換成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- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
但是很明顯這個函數還是有問題,某天我又換成centos呢。。。。我們改寫一下getSystem這個函數
var getSystem = function(ins) { if (ins.system instanceOf Function) { ins.system(); } }
- 1
- 2
- 3
- 4
- 5
這里我們是假設每個系統獲取系統的名稱都是system,實際開發過程中可能不會這樣,這種情況可以用適配器模式來解決。
JavsScript中面向對象的一些概念:
- 類class: ES5以前就是構造函數,ES6中有class
- 實例instance和對象object:構造函數創建出來的對象一般稱為實例instance
- 父類和子類:JavaScript也可以稱為父對象和子對象
2. JavaScript對象屬性
想弄懂面向對象,是不是先看看對象是啥呢?
我們先看一個題目:
[] + {}; // "[object Object]"
{} + []; // 0
- 1
- 2
解釋:
在第一行中,{}出現在+操作符的表達式中,因此被翻譯為一個實際的值(一個空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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
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;- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- Object.hasOwnProperty(),判斷一個對象是否有指定名稱的屬性,不會檢查繼承屬性
a.hasOwnProperty('size'); a.hasOwnProperty('name'); a.hasOwnProperty('age');
- 1
- 2
- 3
- 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');- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
-
如何枚舉對象的屬性,並保證不同了瀏覽器中的行為是一致的?
- 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
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- Object.keys(), 返回一個數組,內容是對象可枚舉的實例屬性名稱
var propertyArray = Object.keys(b); // name
- 1
- 2
- Object.getOwnPropertyNames(),返回一個數組,內容是對象所有實例屬性,包括可枚舉和不可枚舉
var propertyArray = Object.getOwnPropertyNames(b); // name, age
- 1
- 2
-
如何判斷兩個對象是否相等?
我只想說,這個問題說簡單很簡單,說復雜也挺復雜的傳送門
我們看個簡單版的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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
}
// Outputs: true
console.log(isEquivalent({a:1},{a:1}));
```
- 1
- 2
- 3
上面這個函數還有啥問題呢?
- 沒有對傳入參數進行校驗,例如判斷是否是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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
上面兩行報錯的原因,是因為構造函數只能由函數來充當,而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();
- 1
- 2
- 3
- 4
- 5
- 6
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
理解了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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
延伸一個問題:如何判斷一個變量是否是數組?
- typeof
我們上面已經解釋了,這些都是普通對象,普通對象是沒有prototype的,他們typeof的值都是object
typeof []
typeof {}
- 1
- 2
- 從原型來看, 原理就是看Array是否在a的原型鏈中
a的原型鏈是 Array->Object
const a = [];
Array.prototype.isPrototypeOf(obj);
- 1
- 2
- instanceof
const a = [];
a instanceof Array
- 1
- 2
從構造函數入手,但是這個方法和上面的方法都有一問題,不同的框架中創建的數組不會相互共享其prototype屬性
- 根據對象的class屬性,跨原型調用tostring方法
const a = [];
Object.prototype.toString.call(a);
// [Object Array]
- 1
- 2
- 3
ES5 中所有內置對象的[[Class]]屬性的值是由規范定義的,但是 ES6 中已經沒有了[[Class]]屬性,取代它的是[[NativeBrand]]屬性,這個大家有興趣可以自行去查看規范
原理:
- 如果this的值為undefined,則返回"[object Undefined]".
- 如果this的值為null,則返回"[object Null]".
- 讓O成為調用ToObject(this)的結果.
- 讓class成為O的內部屬性[[Class]]的值.
- 返回三個字符串"[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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
這段代碼的執行過程
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 指向什么
- 1
- 2
- 3
- 4
- 5
- 6
上面例子中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,不是對象,到頭啦
- 1
- 2
- 3
- 4
- 5
- 6
- 7
原型鏈的概念其實不重要,重要的是要理解,簡單來說,原型鏈就是利用原型讓一個引用類型繼承另一個應用類型的屬性和方法。
最后我們用一張圖來結束本節
Array.__proto__ === Function.prototype
Object.__proto__ === Function.prototype
- 1
- 2
還有三點需要注意的:
- 任何內置函數對象(類)本身的 **proto**都指向 Function 的原型對象;
- 除了 Object 的原型對象的**proto** 指向 null,其他所有內置函數對象的原型對象的 proto 都指向 object。
- 所有構造函數的的prototype方法的__proto__都指向Object.prototype(除了…Object.prototype自身)
如果理解了上面這些內容,大家可以自行描述一下,構造函數、原型和實例之間的關系,也可以舉例說明
function Dogs (name) {
this.name = name;
}
var jingmao = new Dogs('jingmao');
- 1
- 2
- 3
- 4
- 5
- 6
這個圖大家腦子里面自己構想一下?
解釋:
構造函數首字母必須大寫,用來區分普通函數,內部使用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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
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)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
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());
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
需要注意的地方:
實現原型繼承的時候不要使用對象字面量創建原型方法,因為這樣做,會重寫原型鏈。
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
上面使用SubType.prototype = {...}
之后,SubType的原型就是Object了,而不是SuperType了。
優點:原型定義的屬性和方法可以復用
缺點:
- 引用類型的原型屬性會被所有實例共享
- 創建子對象時,不能向父對象的構造函數中傳遞參數
7.2 構造函數繼承
這里的例子來源是JavaScript高級程序設計
在說構造函數繼承之前,我們先看一個例子
var a = {
name: 'a',
};
var name = 'window';
var getName = function(){
console.log(this.name);
}
getName() // 輸出window
getName.call(a) // 輸出a
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
執行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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
需要注意的地方是在調用父對象的構造函數之后,再給子類型中的定義屬性,否則會被重寫。
缺點:方法都需要在構造函數中定義,難以做到函數的復用,而且在父對象的原型上定義的方法,對於子類型是不可見的。 ??? 為什么不可見
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
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(instance2.con); // test1
console.log(instance2.getAge()); // 18
console.log(instance2.getName()); // hang
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
優點:彌補了原型繼承和構造函數的缺點
缺點:父類構造函數調用了兩次
7.3 原型式繼承
原型式繼承並沒有使用嚴格意義上的構造函數,借助原型可以基於已有的對象創建新的對象,例如:
function createObject(o) {
function newOrient () {};
newOrient.prototype = o;
return new newOrient();
}
- 1
- 2
- 3
- 4
- 5
簡單來說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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
其中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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
7.5 寄生組合式繼承
到最后一個了,看看我們之前遺留的問題:
組合繼承會調用兩次父對象的構造函數,並且父類型的屬性存在兩組,一組在實例上,一組在SubType的原型上。解決這個問題的方法就是寄生組合式繼承。
function inheritPrototype(subType, superType){
// 繼承父類的原型
var prototype = Object.create(superType.prototype);
// 重寫被污染的construct
prototype.constructor = subType;
// 重寫子類的原型
subType.prototype = prototype;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
這個函數就是寄生組合式繼承的最簡單的實現方式
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
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
可以看到
- 子類繼承了父類的屬性和方法,同時屬性沒有創建在原型鏈上,因此多個子類不會共享同一個屬性。
- 子類可以動態傳遞參數給父類
- 父類構造函數只執行了一次
但是還有一個問題:
子類如果在原型上添加方法,必須要在繼承之后添加,否則會覆蓋原來原型上的方法。但是如果這兩個類是已存在的類,就不行了
優化一下:
function inheritPrototype(subType, superType){
// 繼承父類的原型
var prototype = Object.create(superType.prototype);
// 重寫被污染的construct
prototype.constructor = subType;
// 重寫子類的原型
subType.prototype = Object.assign(prototype, subType.prototype);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
雖然通過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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
此外類必須通過new 操作符來調用,否則會報錯,這個它與普通的構造函數的區別
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
- 1
- 2
- 3
- 4
4.類的實例對象
類的實例的屬性,除非顯式的定義在this上,否則都是定義在原型上,這里與ES5保持一致
5.類的表達式
與函數一樣,類也可以用表達式的方式來定義
const HClass = class Me {
getClassName() {
return Me.name;
}
}
const hIns = new HClass();
HClass.getClassName(); // Me
Me.getClassName(); // error
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
這里只有HClass是暴露在外部的,Me只有在class的內部使用,如果不需要使用Me,完全可以省略
那么我們知道利用函數表達式可以創建一個立即執行函數,類可以么?
let person = new class {
constructor(name) {
this.name = name;
},
sayName() {
console.log(this.name);
}
}('jack');
persion.sayName()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
6.不存在變量提升
這點是和ES5不一樣的, ES6並不會把class的聲明提到當前作用域的頂部,這與下一節的繼承有關系
new Foo()
class Foo {}
- 1
- 2
7.私有屬性和私有方法
私有方法ES6並不提供,但是可以變通
- 命名區分
- 把方法移出模塊
- 利用Symbol來命名方法名
const getAge = Symbol('getAge');
export defalut class Person {
// 公有方法
getName(name) {
return name;
},
// 私有方法
getAge {
return age;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
私有屬性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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
解決辦法:
- 在構造函數中綁定this,這樣就不會找不到print方法了
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 在構造函數中使用箭頭函數
- 使用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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
父類的靜態方法可以被子類繼承
13.類的靜態屬性,也就是說是通過類直接訪問的屬性
Class Foo {
p = 1,
static: 1,
}
- 1
- 2
- 3
- 4
- 5
上面的兩種方法都是錯誤的,目前靜態屬性還處於提案中,
Class Foo {
p = 1;
static p = 1;
}
- 1
- 2
- 3
- 4
- 5
以前我們定義實例屬性只能夠在construct中定義
14.new.target屬性, new.target返回new命令作用的那個構造函數,如果沒有通過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'); // 報錯
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在Class內部調用的時候,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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
利用這個特點我們可以寫出這樣的代碼
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();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
8.2 Class的繼承
1.基本概念
Class可以通過extends關鍵字來實現繼承,而ES5中是通過修改原型鏈來實現繼承
子類必須在constructor中調用super方法,否則新建實例的時候會報錯,因為子類沒有自己的this,是繼承與父類,然后進行加工。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我們回憶一下ES5的繼承,實質是首先創建了子類的實例對象,然后把父類的方法添加到子類上。而ES6是先創建父類的實例對象,然后再用子類的構造函數修改this,如果子類沒有添加constructor,這個方法會被自動添加
class ColorPoint extends Point {
}
// 等同於
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
還有一點需要注意,在子類的構造函數中,只有調用super后,才可以使用this關鍵字,否則會報錯
2.super關鍵字,super可以作為函數和對象來使用
- super作為函數調用時代表父類的構造函數,這里代表A的構造函數,但是返回的是B的實例。作為函數調用時,只能在子類的構造函數調用,如果在其他地方調用會報錯。
class A {}
class B extends A {
constructor() {
super();
// 等價於A.prototype.constructor.call(this)
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- super作為對象,在普通的方法中,指向父類的原型對象;在靜態函數中,指向父類。
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()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
ES6 規定,通過super調用父類的方法時,方法內部的this指向當前的子類實例
由於this指向子類的實例,當對super的一個屬性復制的時候,賦值會變成子類的屬性
3.ES6的__proto__和prototype
我們知道在ES5中,每個對象的__proto__屬性,指向對應構造函數的prototype。而ES6里面有兩條繼承鏈路,先看一個例子
class A {
}
class B extends A {
}
B.proto === A // true
B.prototype.proto === A.prototype // true
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 子類的__proto__屬性指向父類,表示構造函數的繼承
- 子類的原型的__proto__指向父類的原型,表示方法的繼承
class A {
}
class B {
}
// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 繼承 A 的靜態屬性
Object.setPrototypeOf(B, A);
const b = new B();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
再看下一個問題,我們知道ES6是通過extends關鍵字來實現繼承的,那么extends后面的值可以是什么類型呢?我們根據上的兩條繼承鏈路就知道,父類應該要有prototype屬性,也就是說函數都可以作為父類被繼承,此外我們看3中特殊情況
- 子類繼承於Object類
class A extends Object {
}
A.proto === Object // true
A.prototype.proto === Object.prototype // true
- 1
- 2
- 3
- 4
- 5
- 6
- 不存在繼承
class A {
}
A.proto === Function.prototype // true
A.prototype.proto === Object.prototype // true
- 1
- 2
- 3
- 4
- 5
- 6
- 子類繼承null
class A extends null {
}
A.proto === Function.prototype // true
A.prototype.proto === undefined // true
- 1
- 2
- 3
- 4
- 5
- 6
- 原生構造函數的繼承
我們知道,以前原生構造函數是無法繼承的,原因是因為子類無法獲得原生構造函數的內部屬性。原生構造函數會忽略apply方法傳入的this,也就是說,原生構造函數的this無法綁定,導致拿不到內部屬性
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"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
ES6 允許繼承原生構造函數定義子類,因為 ES6 是先新建父類的實例對象this,然后再用子類的構造函數修飾this,使得父類的所有行為都可以繼承。因此我們可以自定義原生數據結構的子類,這些是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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
6.Mixin的實現,也就是將多個對象合並成一個對象
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
上面是一個比較簡單的做法,我們看一個完整的實現方式
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) {
// ...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
</div>
<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-258a4616f7.css" rel="stylesheet">
</div>