JS 類的創建繼承 與 new原理實現

目錄

1. 類的創建

2. 類的繼承

3. new 的原理實現


學習和參考於

JS定義類的六種方式詳解

JS實現繼承的幾種方式

JavaScript深入之創建對象的多種方式以及優缺點

js new一個對象的過程,實現一個簡單的new方法

(一)類的創建

1. 工廠模式

function createPerson(name){
    //原料:
    var obj=new Object();
    //加工:
    obj.name=name;
    obj.showName=function(){
    alert(this.name);
    }
    //出場:
    return obj;
}

var p1=createPerson('小米');
p1.showName();
var arr=new Array();//生成一個系統數組對象

與系統對象的區別:

  • 系統對象是直接用new在外面生成,而工廠定義的是在函數內部生成

  • 工廠定義的函數名稱第一個是小寫開頭,而系統定義的是大寫開頭

工廠模式的優缺點

  • 缺點:對象無法識別,因爲所有的實例都指向一個原型

2. 構造函數模式

function Car(color,door){
    this.color = color;
    this.doors = door;
    this.showColor = function(){
    alert(this.color)
    };
}
var car1 = new Car(“red”,4);
var car2 = new Car(“blue”,4);

構造函數模式的優缺點:

  • 優點:實例可以識別爲一個特定的類型

  • 缺點:每次創建實例時,每個方法都要被創建一次

構造函數模式優化

function Person(name) {
    this.name = name;
    this.getName = getName;
}

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

var person1 = new Person('kevin');

優點:解決了每個方法都要被重新創建的問題

3. 原型模式

該方式利用了對象的prototype屬性。首先用空函數創建類名,然後所有的屬性和方法都被賦予prototype屬性。

function Car(){
}
Car.prototype.color = “red”;
Car.prototype.doors = 4;
Car.prototype.showColor = function(){
alert(this.color);
}
var car1 = new Car();
var car2 = new Car();

在這段代碼中,首先定義了一個空函數,然後通過prototype屬性來定義對象的屬性。調用該函數時,原型的所有屬性都會立即賦予要創建的對象,所有該函數的對象存放的都是指向showColor()的指針,語法上看起來都屬於同一個對象。但是這個函數沒有參數,不能通過傳遞參數來初始化屬性,必須要在對象創建後才能改變屬性的默認值。

原型模式的優缺點:

  • 優點:方法不會重新創建

  • 缺點:所有的屬性和方法都共享 、不能初始化參數。

    //屬性指向的是對象時,如數組:下面代碼由於數組的引用值,Car的兩個對象指向的都是同一個數組,所以當在car1添加值後,在car2中也可以看到。
    function Car(){
    }
    Car.prototype.color = “red”;
    Car.prototype.doors = 4;
    Car.prototype.arr = new Array(“a”,”b”);
    Car.prototype.showColor = function(){
    alert(this.color);
    }
    var car1 = new Car();
    var car2 = new Car();
    car1.arr.push(“cc”);
    alert(car1.arr); //output:aa,bb,cc
    alert(car2.arr); //output:aa,bb,cc
    

4. 組合模型

構造函數模式用於定義實例屬性,原型模式則用於定義方法和共享的屬性。這種混合模式不僅支持向構造函數傳入參數,還最大限度地節約了內存,可謂是集兩模式之長。推薦使用這種方式創建類(同類對象)

function Car(color,door){
    this.color = color;
    this.doors = door;
    this.arr = new Array(“aa”,”bb”);
}
Car.prototype.showColor(){
    alert(this.color);
}
var car1 = new Car(“red”,4);
var car2 = new Car(“blue”,4);
car1.arr.push(“cc”);
alert(car1.arr); //output:aa,bb,cc
alert(car2.arr); //output:aa,bb

優點:該共享的共享,該私有的私有,使用最廣泛的方式

缺點:有的人就是希望全部都寫在一起,即更好的封裝性

還有幾種創建方式,推薦查看

JavaScript深入之創建對象的多種方式以及優缺點

(二)類的繼承

這部分學習並摘自JS實現繼承的幾種方式(建議直接閱讀原文)

既然要實現繼承,那麼首先我們得有一個父類,代碼如下:

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

1. 原型鏈繼承

核心: 將父類的實例作爲子類的原型

function Cat() {

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

var cat = new Cat;
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true

特點

  • 非常純粹的繼承關係,實例是子類的實例,也是父類的實例

  • 父類新增原型方法/原型屬性,子類都能訪問到

  • 簡單,易於實現

缺點

  • 要想爲子類新增屬性和方法,必須要在new Animal()這樣的語句之後執行,不能放到構造器中

  • 無法實現多繼承

  • 來自原型對象的所有屬性被所有實例共享(來自原型對象的引用屬性是所有實例共享的)(詳細請看附錄代碼: 示例1)

  • 創建子類實例時,無法向父類構造函數傳參

推薦指數:★★(後兩個缺點是兩大致命缺陷)

2. 構造繼承

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

function Cat(name){
  Animal.call(this);
  this.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中,子類實例共享父類引用屬性的問題

  • 創建子類實例時,可以向父類傳遞參數

  • 可以實現多繼承(call多個父類對象)

缺點

  • 實例並不是父類的實例,只是子類的實例

  • 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法

  • 無法實現函數複用,每個子類都有父類實例函數的副本,影響性能

推薦指數:★★(缺點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 子類()還是子類(),返回的對象具有相同的效果

缺點

  • 實例是父類的實例,不是子類的實例

  • 不支持多繼承

推薦指數:★★

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

特點

  • 支持多繼承

缺點

  • 效率較低,內存佔用高(因爲要拷貝父類的屬性)

  • 無法獲取父類不可枚舉的方法(不可枚舉方法,不能使用for in 訪問到)

推薦指數:★(缺點1)

5. 組合繼承

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

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

Cat.prototype.constructor = Cat;

// 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

特點

  • 彌補了方式2的缺陷,可以繼承實例屬性/方法,也可以繼承原型屬性/方法

  • 既是子類的實例,也是父類的實例

  • 不存在引用屬性共享問題

  • 可傳參

  • 函數可複用

缺點

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

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

6. 寄生組合繼承

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

function Cat(name) {
	Animal.call(this);
	this.name = name || 'Tom';
}
(function() {
	var Super = function(){};
	Super.prototype = Animal.prototype;
	Cat.prototype = new Super()
})()

console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

特點

  • 堪稱完美

缺點

  • 實現較爲複雜

推薦指數:★★★★(實現複雜,扣掉一顆星)

7. 附錄代碼:

function Animal (name) {
  // 屬性
  this.name = name || 'Animal';
  // 實例方法
  this.sleep = function(){
    console.log(this.name + '正在睡覺!');
  }
  //實例引用屬性
  this.features = [];
}
function Cat(name){
}
Cat.prototype = new Animal();

var tom = new Cat('Tom');
var kissy = new Cat('Kissy');

console.log(tom.name); // "Animal"
console.log(kissy.name); // "Animal"
console.log(tom.features); // []
console.log(kissy.features); // []

tom.name = 'Tom-New Name';
tom.features.push('eat');

//針對父類實例值類型成員的更改,不影響
console.log(tom.name); // "Tom-New Name"
console.log(kissy.name); // "Animal"
//針對父類實例引用類型成員的更改,會通過影響其他子類實例
console.log(tom.features); // ['eat']
console.log(kissy.features); // ['eat']

/*
原因分析:
關鍵點:屬性查找過程
執行tom.features.push,首先找tom對象的實例屬性(找不到),
那麼去原型對象中找,也就是Animal的實例。發現有,那麼就直接在這個對象的
features屬性中插入值。
在console.log(kissy.features); 的時候。同上,kissy實例上沒有,那麼去原型上找。
剛好原型上有,就直接返回,但是注意,這個原型對象中features屬性值已經變化了。
*/

(三)new 原理實現

new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象類型之一

1. new一個對象中間做了什麼操作

  • 以構造器的prototype屬性爲原型,創建新對象;
  • 將this(也就是上一句中的新對象)和調用參數傳給構造器,執行;
  • 如果構造器沒有手動返回對象,則返回第一步創建的新對象,如果有,則捨棄掉第一步創建的新對象,返回手動return的對象。

new過程中會新建對象,此對象會繼承構造器的原型與原型上的屬性,最後它會被作爲實例返回這樣一個過程。

2. 實現

function newMethod(){
  //拿到傳入的參數中的第一個參數,即構造函數名Func
  let constr = [].shift.call(arguments);//這行代碼的意思:刪除並拿到arguments的第一項
  // 1.以構造器的prototype屬性爲原型,創建新對象:             
  let obj = Object.create(constr.prototype);
 // 2.將this和調用參數傳給構造器執行 (使用apply,將構造函數中的this指向新對象,這樣新對象就可以訪問構造函數中的屬性和方法):
  let result = constr.apply(obj, arguments);
  // 3.如果構造器沒有手動返回對象,則返回第一步的對象(構造函數的一個實例對象)
  return typeof result === "object" ? result : obj;
}

測試

// 構造器函數
let Parent = function (name, age) {
    this.name = name;
    this.age = age;
};
Parent.prototype.sayName = function () {
    console.log(this.name);
};

//創建實例,將構造函數Parent與形參作爲參數傳入
const child = newMethod(Parent, 'echo', 26);
child.sayName() //'echo';

//最後檢驗,與使用new的效果相同
child instanceof Parent//true
child.hasOwnProperty('name')//true
child.hasOwnProperty('age')//true
child.hasOwnProperty('sayName')//false
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章