1. 創建類
1.1. 簡單的類
類指一組對象從同一個原型對象繼承屬性,原型對象
是類的核心特征。
定義一個原型對象,然后用Object.create()創建一個繼承它的對象,我們就定義了一個JavaScript類。
//工廠函數,用于創建Range對象
function range(from, to) {
//使用Object.create()創建一個對象,繼承原型對象
let r = Object.create(range.methods);
r.from = from;
r.to = to;
return r;
}
// 定義一個原型對象
range.methods = {
includes(x){
//通過this引用調用from和to的對象
return this.from<=x && x<=this.to;
},
// 生成器函數:讓這個類的實例可迭代
*[Symbol.iterator](){
for(let x = Math.ceil(this.from); x<=this.to; x++){
yield x;
}
},
toString(){return "("+this.from +"..."+this.to+")";}
}
let r = range(1,10); // 創建一個對象
console.log(r.includes(2));
console.log(r.includes(11));
console.log(r.toString());
console.log([...r]);
1.2. 使用構造函數的類
上面的方法定義了JavaScript類,但是它沒有定義構造函數。構造函數
是專門用于初始化新對象的函數,使用new
關鍵字調用構造函數會自動創建新對象。構造函數調用的關鍵在于構造函數的prototype
屬性被用作新對象的原型。
只有函數對象才有prototype屬性,同一個構造函數創建的所有對象都繼承同一個對象。
在不支持的ES6 class關鍵字的JavaScript版本中,用以下的方法創建類。
// 構造函數
function Range(from, to){
this.from = from;
this.to = to;
}
// 所有Range對象都繼承這個對象,prototype這個名字是強制的
Range.prototype={
//不要使用箭頭函數,因為箭頭函數沒有prototype屬性,this是從定義它的上下文繼承的
includes:function(x) {
return this.from<=x && x<=this.to;
},
[Symbol.iterator]:function*() {
for(let x = Math.ceil(this.from); x<=this.to; x++){
yield x;
}
},
toString:function(){return "("+this.from +"..."+this.to+")";}
}
//以new關鍵字調用構造函數
let r = new Range(1,10);
console.log(r.includes(2));
console.log(r.includes(11));
console.log(r.toString());
console.log([...r]);
對比以上兩個例子,有以下區別:
- 工廠函數命名為range(),構造函數命名為Range();
- 創建對象的時候,工廠函數使用raneg(),構造函數使用new Range()
函數體內有一個特殊表達式new.target
用于判斷函數是否作為構造函數,如果new.target不是undefined,說明函數作為構造函數,會自動創建新對象
function F(){
if(!new.target) return new F();
}
上面提到,構造函數調用的關鍵在于構造函數的prototype
屬性被用作新對象的原型。所以,每個普通JavaScript函數自動擁有一個prototype
屬性,這個屬性有一個不可枚舉的constructor
屬性。
constructor
屬性的值就是該函數對象本身
let F = function (x) {this.x = x}
let p = F.prototype;
let c = p.constructor;
console.log("c === F: ", c === F); // true, F.prototype.constructor === F
注意,上面的Range的例子中,由于用自己定義的對象Range.prototype = {}重寫了預定義的Range.prototype對象,所以Range的實例都沒有constructor屬性。
let o = new F();
console.log(o.constructor === F); // true
console.log(r.constructor === Range); // ?
上面r.constructor === Range返回的是false。
常用的方法是使用預定義的原型對象及其constructor屬性,然后通過以下方式添加方法:
Range.prototype.includes = function(x){}
1.3. 使用ES6的class
ES6引入class
關鍵字,可以使用新語法創建類
class Range{
//實際上定義的函數不叫constructor
//class會定義一個新變量Range,并將這個特殊構造函數的值賦給改變量
constructor(from, to){
this.from = from;
this.to = to;
}
//methods
//方法之間沒有逗號
//不支持key:value形式
includes(x){
//通過this引用調用from和to的對象
return this.from<=x && x<=this.to;
}
*[Symbol.iterator](){
for(let x = Math.ceil(this.from); x<=this.to; x++){
yield x;
}
}
toString(){return "("+this.from +"..."+this.to+")";}
}
2. 繼承的幾種方法
// 父類
function Animal(name){
this.name = name || "cat";
this.sleep = function(){console.log(`${this.name} is sleeping.`);}
}
- 原型鏈繼承
function Cat(){}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
let cat = new Cat();
cat.sleep();
- 構造繼承
function Cat2(name){
//使用父類的構造函數來增強子類實例, 復制父類的實例屬性給子類
Animal.call(this);
this.name = name;
}
//實例并不是父類的實例,只是子類的實例
//只能繼承父類的屬性和方法,不能繼承原型鏈的
//無法實現函數復用,每個子類都有父類實例函數的副本,影響性能
let c2 = new Cat2("cat2");
c2.sleep();
- 實例繼承
function Cat3(name){
//為父類實例添加新特性,作為子類實例返回
let instance = new Animal();
instance.name = name||'Tom';
return instance;
}
//實例是父類的實例,不是子類的實例
let c3 = new Cat3();
c3.sleep();
- 拷貝繼承
function Cat4(name){
let a = new Animal();
//拷貝父類的屬性和方法
//效率較低,內存占用高
//無法獲取父類不可枚舉的方法(不能使用for in訪問)
for(let p in a){
Cat4.prototype[p] = a[p];
}
this.name = name;
}
let c4 = new Cat4('cat4');
c4.sleep();
- 組合繼承
function Cat5(name){
Animal.call(this);
this.name = name ||'Tom';
}
//通過調用父類構造,可以繼承實例屬性/方法,也可以繼承原型屬性/方法
//將父類實例作為子類原型,實現函數復用
//調用了兩次父類構造函數,生成了兩份實例
Cat5.prototype = new Animal();
Cat5.prototype.constructor = Cat;
//既是子類的實例,也是父類的實例
let c5 = new Cat5('cat5');
c5.sleep();
- 寄生組合繼承
//調用兩次父類的構造的時候,就不會初始化兩次實例方法/屬性,避免的組合繼承的缺點
function Cat6(name){
Animal.call(this);
this.name = name || "Tom";
}
(function(){
//通過寄生方式,砍掉父類的實例屬性
let Super = function(){};
Super.prototype = Animal.prototype;
//將實例作為子類的原型
Cat6.prototype = new Super();
})();
Cat6.prototype.constructor = Cat6;
let c6 = new Cat6('cat6');
c6.sleep();
- 基于ES6的class的繼承
class Span extends Range{
constructor(start, length){
if(length>0){
super(start, start+length);
}else{
super(start+length, start);
}
}
}
最近在看高頻面試題,經常看到繼承的問題,之后再來補充一下~~~~
還有類涉及對象和原型鏈的問題,可能也會總結一下