JavaScript建構函式,物件導向程式設計

某億發表於2023-03-16
前言

大家應該都聽說過物件導向程式設計吧,在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建構函式的工作原理,並且能夠在自己的程式碼中應用它們。

相關文章