前言
大家應該都聽説過面向對象編程吧,在java和c語言中,是有”類(class)”的概念的,所謂“類”就是對象的模板,對象就是“類”的實例。而在JavaScript語言,他的對象體系是基於構造函數(constructor)和原型鏈(prototype)的。
你可能會問,不對啊,es6不是有個class麼?實際上es6的class只是模仿java起了一個面向對象的習慣性的名字,讓對象原型的寫法更加清晰、更像面向對象編程的語法而已,而且es6的class自身指向的就是構造函數。 所以可以認為ES6中的類其實就是構造函數的語法糖或者是構造函數的另外一種寫法而已!
通常生成一個對象的傳統方式就是通過構造函數,這也是JS面向對象唯一起點。
一、構造函數基礎學習
1.構造函數的定義和使用方法
JavaScript構造函數是用於創建對象的函數。它們與普通函數的區別在於,構造函數被用於創建一個新的對象,並且該對象的屬性和方法可以在函數內部定義,為了區分普通函數,構造函數一般首字母大寫。
構造函數的使用方法非常簡單。您只需要使用new關鍵字來調用構造函數,並將其賦值給一個變量即可。例如:
function Car(name, age) {
this.name = name;
this.age = age;
}
const car1 = new Car('小明', 20);
在這個例子中,我們創建了一個名為Car的構造函數,並使用new關鍵字創建了一個名為car1的Car對象。該對象具有兩個屬性:name和age。這些屬性是在構造函數內部定義的。
2.構造函數的參數和返回值
構造函數可以接受任意數量的參數,並且可以返回一個新的對象。在構造函數內部,使用this關鍵字來定義對象的屬性和方法。
下面是一個使用構造函數返回對象的例子:
function Rectangle(width, height) {
this.width = width;
this.height = height;
this.area = function() {
return this.width * this.height;
}
}
const rect1 = new Rectangle(5, 10);
console.log(rect1.area()); // 50
在這個例子中,我們創建了一個名為Rectangle的構造函數,並使用它創建了一個名為rect1的對象。該對象具有三個屬性:width、height和area。其中,area是一個函數,用於計算矩形的面積。
3.原型和原型鏈的概念
在JavaScript中,每個對象都有一個原型。原型是一個對象,包含了該對象的屬性和方法。當您嘗試訪問一個對象的屬性或方法時,JavaScript會首先查找該對象本身是否具有該屬性或方法。如果對象本身沒有該屬性或方法,JavaScript會查找該對象的原型,並在原型中查找該屬性或方法。
原型鏈是一系列由對象原型組成的鏈。當您嘗試訪問一個對象的屬性或方法時,JavaScript會沿着該對象的原型鏈向上查找,直到找到該屬性或方法為止。
<aside> 💡 js構造函數的執行過程是怎樣的?
</aside>
- 創建一個空對象(this)。
- 將this綁定到新創建的對象上。
- 執行構造函數內部的代碼,給this添加屬性和方法。
- 默認返回this(除非顯式返回其他值)
實際上,用new調用函數後,JS引擎就會在內存中創建一個空對象{},創建出來的新對象的__proto__屬性指向構造函數的原型對象(通俗理解就是新對象隱式原型__proto__鏈接到構造函數顯式原型prototype上。)構造函數內部的this會指向這個新對象, 然後從上到下執行函數體(只有這步是我們能直觀看到代碼的)最後,返回創造出來的對象,也就是我們得到的實例對象
原型對象,構造函數和實例三者的關係如圖所示:
也就是説,你每次new,都會得到一個全新的對象,有自己的內存空間,所以創建多個實例對象,他們之間互不影響,只會共用一個原型對象上的屬性和方法,這裏就要注意,儘量把相同的屬性或者方法都放在構造函數內部,這樣多個實例使用時可以節省自身空間
4.如何繼承構造函數
JavaScript允許您通過繼承構造函數來創建新的對象類型。這可以通過使用原型來實現。下面是一個使用原型繼承構造函數的例子:
function Animal(name) {
this.name = name;
}
Animal.prototype.getName = function() {
return this.name;
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.getBreed = function() {
return this.breed;
}
const dog1 = new Dog('小黑', '拉布拉多');
console.log(dog1.getName()); // '小黑'
console.log(dog1.getBreed()); // '拉布拉多'
在這個例子中,我們創建了一個名為Animal的構造函數,並在其原型中定義了一個名為getName的方法。然後,我們創建了一個名為Dog的構造函數,並通過調用Animal.call方法來繼承Animal構造函數。最後,我們在Dog原型中定義了一個名為getBreed的方法,並將Dog.prototype設置為Animal.prototype的一個新實例,從而實現了繼承。
通過繼承構造函數,您可以創建複雜的對象類型,並將其組織成易於管理和維護的代碼結構。
5.構造函數使用場景
- 當你需要創建相同類型的多個對象時,構造函數可以避免重複編寫代碼,提高效率和可讀性
- 當你需要給對象的成員變量賦予初始值時,構造函數可以保證對象在創建時就被正確初始化
- 當你需要給對象的成員變量賦予常量或引用類型的值時,構造函數必須使用初始化列表來完成,因為常量和引用不能被重新賦值
- 當你需要給子類對象的父類部分賦予初始值時,構造函數必須調用父類的構造函數來完成
如果您已經熟悉JavaScript構造函數的基礎知識,那麼您可以進一步學習深度JavaScript構造函數。以下是一些深入的話題,您可以在這些話題中深入瞭解JavaScript構造函數。
二、構造函數進階學習
1.使用類定義構造函數
類的定義:
在ES6中,引入了類的概念。類是一種定義對象的模板。您可以使用類來定義JavaScript構造函數。以下是一個使用類定義構造函數的例子:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const person1 = new Person('小明', 20);
在這個例子中,我們使用class關鍵字定義了一個名為Person的類,並在其構造函數中定義了兩個屬性:name和age。然後,我們使用new關鍵字創建一個名為person1的Person對象。
constructor 方法:
constructor 方法就是類的構造方法,this 關鍵字代表實例對象。其對應的也就是 ES5 的構造函數 Person。
constructor 方法是類的默認方法,通過 new 命令生成對象實例時,會自動調用該方法。一個類必須有 constructor 方法,如果沒有顯式定義,會默認添加一個空的 constructor 方法。
類的繼承:
es6的class類繼承可以通過extends關鍵字實現,以下是一個使用類繼承的例子:
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
getBreed() {
return this.breed;
}
}
const dog1 = new Dog('小黑', '拉布拉多');
console.log(dog1.getName()); // '小黑'
console.log(dog1.getBreed()); // '拉布拉多'
在這個例子中,我們定義了一個名為Animal的類,並在其構造函數中定義了一個屬性和一個方法。然後,我們使用extends關鍵字創建了一個名為Dog的類,並使用super關鍵字調用了Animal構造函數。最後,我們在Dog中定義了一個新屬性和一個新方法。
使用類繼承,您可以輕鬆地創建複雜的對象類型,並將其組織成易於管理和維護的代碼結構。
super 關鍵字:
super 這個關鍵字,既可以當作函數使用,也可以當作對象使用。用法完全不同。
super() 方法:
super 作為函數調用時,代表父類的構造函數。子類的構造函數必須執行一次 super() 方法。
因為 ES6 的繼承機制與 ES5 構造函數不同,ES6 的子類實例對象 this 必須先通過父類的構造函數創建,得到與父類同樣的實例屬性和方法後再添加子類自己的實例屬性和方法。因此如果不調用 super() 方法,子類就得不到 this 對象。
super 雖然代表了父類的構造函數,但返回的是子類的實例,即通過super 執行父類構造函數時,this 指的都是子類的實例。也就是 super() 相當於 Person.call(this)。
class A {
constructor() {
console.log(this.constructor.name)
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
作為函數時,super() 只能在子類的構造函數之中,用在其他地方就會報錯。
super 對象:
在普通方法中指向父類的 prototype 原型
super 作為對象時,在普通方法中,指向父類的 prototype 原型,因此不在原型 prototype 上的屬性和方法不可以通過 super 調用。
class A {
constructor() {
this.a = 3;
}
p() {return 2;}
}
A.prototype.m = 6;
class B extends A {
constructor() {
super();
console.log(super.a); // undefined
console.log(super.p()); // 2
console.log(super.m); // 6
}
}
new B();
let a = new A() console.log(a.__proto__) // {constructor: ƒ, p: ƒ}
問題:為什麼super.a是undefined?
因為上面説了,普通方法裏面指向的是父類的 prototype 原型,從打印可以看出來,他只能拿到constructor這個方法,而a屬性並不直接掛到A原型對象下面,所以拿不到
在子類普通方法中通過 super 調用父類方法時,方法內部的 this指向當前的子類實例。
class A {
constructor() {
this.x = 'a';
}
aX() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 'b';
}
bX() {
super.aX();
}
}
(new B()).bX() // 'b'
在靜態方法中,指向父類
class A {
static m(msg) {
console.log('static', msg);
}
m(msg) {
console.log('instance', msg);
}
}
class B extends A {
static m(msg) {
super.m(msg);
}
m(msg) {
super.m(msg);
}
}
B.m(1); // "static" 1
(new B()).m(2) // "instance" 2
在子類靜態方法中通過 super 調用父類方法時,方法內部的 this 指向當前的子類,而不是子類的實例。
屬性攔截:
與 ES5 一樣,在 Class 內部可以使用 get 和 set 關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行為。
class Person {
constructor() {
this.name = 'dora';
}
get author() {
return this.name;
}
set author(value) {
this.name = this.name + value;
console.log(this.name);
}
}
let p = new Person();
p.author // dora
p.author = 666; // dora666
且其中 author 屬性定義在 Person.prototype 上,但 get 和 set 函數是設置在 author 屬性描述對象 Descriptor 上的。
Class 的 static 靜態方法:
類相當於實例的原型,所有在類中定義的方法,都會被實例繼承。但如果在一個方法前,加上 static 關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱為“靜態方法”。
class Person {
static sayHi() {
console.log('Hi');
}
}
Person.sayHi() // "Hi"
let p = new Person();
p.sayHi() // TypeError: p.sayHi is not a function
2.使用閉包定義構造函數
閉包是一種定義函數的方式,可以捕獲函數被創建時的環境。您可以使用閉包來定義JavaScript構造函數。以下是一個使用閉包定義構造函數的例子:
function createPerson(name, age) {
return function() {
return {
name: name,
age: age
}
}
}
const person1 = createPerson('小明', 20)();
在這個例子中,我們定義了一個名為createPerson的函數,並返回一個新函數。返回的函數創建一個對象,該對象包含兩個屬性:name和age。我們使用createPerson函數來創建一個名為person1的Person對象。
3.使用工廠函數定義構造函數
工廠函數是一種定義函數的方式,可以返回一個新的對象。您可以使用工廠函數來定義JavaScript構造函數。以下是一個使用工廠函數定義構造函數的例子:
function createPerson(name, age) {
return {
name: name,
age: age
}
}
const person1 = createPerson('小明', 20);
在這個例子中,我們定義了一個名為createPerson的函數,並返回一個新的對象。返回的對象包含兩個屬性:name和age。我們使用createPerson函數來創建一個名為person1的Person對象。
總結
以上是一些深入的JavaScript構造函數話題。掌握這些話題可以幫助您更好地理解JavaScript構造函數的工作原理,並且能夠在自己的代碼中應用它們。