首頁 >web前端 >js教程 >深入了解JavaScript中的原型和繼承

深入了解JavaScript中的原型和繼承

青灯夜游
青灯夜游轉載
2019-11-26 18:11:331937瀏覽

本文主要講了原型如何在JavaScript中工作,以及如何透過[Prototype]所有物件共享的隱藏屬性連結物件屬性和方法;以及如何建立自訂建構函式以及原型繼承如何運作以傳遞屬性和方法值。

深入了解JavaScript中的原型和繼承

介紹

JavaScript是一種基於原型的語言,這意味著物件屬性和方法可以透過具有克隆和擴展能力的通用物件共享。這被稱為原型繼承,與類別繼承不同。在流行的物件導向程式語言中,JavaScript是相對獨特的,因為其他著名的語言,如PHP、Python和Java都是基於類別的語言,它們將類別定義為物件的藍圖。

【相關課程推薦:JavaScript影片教學

在文中,我們將學習什麼是物件原型,以及如何使用建構函式將原型擴展為新物件。我們還將學習繼承和原型鏈。

JavaScript原型

JavaScript中的每個物件都有一個稱為[[Prototype]]的內部屬性。我們可以透過建立一個新的空物件來演示這一點。

let x = {};

這是我們通常創建物件的方法,但是請注意,另一種實作方法是使用物件建構子:

let x = new object()

包圍[[Prototype]]的雙方括號表示它是一個內部屬性,不能在程式碼中直接存取。

要找到這個新建立物件的[[Prototype]],我們將使用getPrototypeOf()方法。

Object.getPrototypeOf(x);

輸出將由幾個內建屬性和方法組成。

輸出:

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

找到的另一種方法[[Prototype]]是透過__proto__財產。 __proto__是一個公開[[Prototype]]物件內部的屬性。

要注意的是,. _proto__是一個遺留特性,不應該在生產程式碼中使用,而且它也不是在每個現代瀏覽器中都存在。但是,我們可以在本文中使用它來進行演示。

x.__proto__;

輸出將與使用getPrototypeOf()相同。

輸出

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

重要的是JavaScript中的每個物件都有一個[[Prototype]],因為它為任何兩個或多個物件建立了連結的方法。

您建立的物件和內建物件(如Date和Array)一樣具有[[Prototype]]。可以透過prototype屬性將這個內部屬性從一個物件引用到另一個對象,我們將在本教學的後面看到這一點。

原型繼承

當您試圖存取物件的屬性或方法時,JavaScript將首先搜尋物件本身,如果沒有找到,它將搜尋物件的[[Prototype]]。如果在查詢物件及其[[Prototype]]後仍然沒有找到匹配項,JavaScript將檢查被連結物件的原型,並繼續搜尋,直到到達原型鏈的末端。

原型鏈的結尾是Object.prototype。所有物件都繼承物件的屬性和方法。任何超出鏈末端的搜尋都會導致null。

在我們的範例中,x是一個從object繼承而來的空物件。 x可以使用物件具有的任何屬性或方法,例如toString()。

x.toString();

輸出

[object Object]

這個原型鏈只有一個鍊長。 x - > Object。我們知道這一點,因為如果我們試圖將兩個[[Prototype]]屬性連結在一起,它將為null。

x.__proto__.__proto__;

輸出

null

讓我們看看另一種類型的物件。如果您有使用JavaScript處理陣列的經驗,您就會知道它們有許多內建方法,例如pop()和push()。建立新陣列時可以存取這些方法的原因是,建立的任何陣列都可以存取array .prototype上的屬性和方法。

我們可以透過建立一個新的陣列來測試它。

let y = [];

請記住,我們也可以把它寫成陣列建構函數,讓y = new array()。

如果我們查看新y數組的[[Prototype]],我們將看到它比x物件具有更多的屬性和方法。它繼承了Array.prototype中的所有內容。

y.__proto__;
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]

您將注意到原型上的建構子屬性被設定為Array()。建構函數屬性傳回物件的建構函數,這是一種用於從函數建構物件的機制。

我們現在可以將兩個原型連結在一起,因為在這種情況下,我們的原型鏈更長。它看起來像y -> Array -> Object。

y.__proto__.__proto__;
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

這個鏈現在引用Object.prototype。我們可以根據建構函式的Prototype屬性來測試內部的[[Prototype]],以確定它們所引用的是相同的東西。

y.__proto__ === Array.prototype;            // true
y.__proto__.__proto__ === Object.prototype; // true

我們也可以使用isPrototypeOf()方法來實現這一點。

Array.prototype.isPrototypeOf(y);      // true
Object.prototype.isPrototypeOf(Array); // true

我们可以使用instanceof操作符来测试构造函数的prototype属性是否出现在对象原型链中的任何位置。

y instanceof Array; // true

总而言之,所有JavaScript对象都具有隐藏的内部[[Prototype]]属性(可能__proto__在某些浏览器中公开)。对象可以扩展,并将继承[[Prototype]]其构造函数的属性和方法。

这些原型可以被链接,并且每个额外的对象将继承整个链中的所有内容。链以Object.prototype结束。

构造器函数

构造函数是用来构造新对象的函数。new操作符用于基于构造函数创建新实例。我们已经看到了一些内置的JavaScript构造函数,比如new Array()和new Date(),但是我们也可以创建自己的自定义模板来构建新对象。

例如,我们正在创建一个非常简单的基于文本的角色扮演游戏。用户可以选择一个角色,然后选择他们将拥有的角色类别,例如战士、治疗者、小偷等等。

由于每个字符将共享许多特征,例如具有名称、级别和生命值,因此创建构造函数作为模板是有意义的。然而,由于每个角色类可能有非常不同的能力,我们希望确保每个角色只能访问自己的能力。让我们看看如何使用原型继承和构造函数来实现这一点。

首先,构造函数只是一个普通函数。当使用new关键字的实例调用它时,它将成为一个构造函数。在JavaScript中,我们按照惯例将构造函数的第一个字母大写。

// Initialize a constructor function for a new Hero
function Hero(name, level) {
  this.name = name;  this.level = level;
}

我们创建了一个名为Hero的构造函数,它有两个参数:name和level。因为每个字符都有一个名称和一个级别,所以每个新字符都有这些属性是有意义的。this关键字将引用创建的新实例,因此将this.name设置为name参数将确保新对象具有name属性集。

现在我们可以用new创建一个新的实例。

let hero1 = new Hero('Bjorn', 1);

如果我们在控制台输出hero1,我们将看到已经创建了一个新对象,其中新属性按预期设置。

输出

Hero {name: "Bjorn", level: 1}

现在,如果我们得到hero1的[[Prototype]],我们将能够看到构造函数Hero()。(记住,它的输入与hero1相同。,但这是正确的方法。)

Object.getPrototypeOf(hero1);

输出

constructor: ƒ Hero(name, level)

您可能注意到,我们只在构造函数中定义了属性,而没有定义方法。在JavaScript中,为了提高效率和代码可读性,通常在原型上定义方法。

我们可以使用prototype向Hero添加一个方法。我们将创建一个greet()方法。

// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
  return `${this.name} says hello.`;
}

因为greet()在Hero的原型中,而hero1是Hero的一个实例,所以这个方法对hero1是可用的。

hero1.greet();

输出

"Bjorn says hello."

如果检查Hero的[[Prototype]],您将看到greet()现在是一个可用选项。

这很好,但是现在我们想要为英雄创建角色类。将每个类的所有功能都放到Hero构造函数中是没有意义的,因为不同的类具有不同的功能。我们希望创建新的构造函数,但也希望它们连接到原始的Hero。

我们可以使用call()方法将属性从一个构造函数复制到另一个构造函数。让我们创建一个战士和一个治疗构造器。

// Initialize Warrior constructor
function Warrior(name, level, weapon) {
  // Chain constructor with call
  Hero.call(this, name, level);  // Add a new property
  this.weapon = weapon;
}// Initialize Healer constructor
function Healer(name, level, spell) {
  Hero.call(this, name, level);  this.spell = spell;
}

两个新的构造函数现在都具有Hero和unqiue的属性。我们将把attack()方法添加到Warrior中,而heal()方法添加到Healer中。

Warrior.prototype.attack = function () {
  return `${this.name} attacks with the ${this.weapon}.`;
}

Healer.prototype.heal = function () {
  return `${this.name} casts ${this.spell}.`;
}

此时,我们将使用两个可用的新字符类创建字符。

const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');

hero1现在被认为是拥有新属性的战士。

输出

Warrior {name: "Bjorn", level: 1, weapon: "axe"}

我们可以使用我们在战士原型上设置的新方法。

hero1.attack();
Console
"Bjorn attacks with the axe."

但是如果我们尝试使用原型链下面的方法会发生什么呢?

hero1.greet();

输出

Uncaught TypeError: hero1.greet is not a function

使用call()链接构造函数时,原型属性和方法不会自动链接。我们将使用Object.create()来链接原型,确保在创建并添加到原型的任何其他方法之前将其放置。

Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);
// All other prototype methods added below…

现在我们可以在一个战士或治疗者的实例上成功地使用Hero的原型方法。

hero1.greet();

输出

"Bjorn says hello."

这里是我们的角色创建页面的完整代码。

// Initialize constructor functions
function Hero(name, level) {
  this.name = name;
  this.level = level;
}
 
function Warrior(name, level, weapon) {
  Hero.call(this, name, level);
 
  this.weapon = weapon;
}
 
function Healer(name, level, spell) {
  Hero.call(this, name, level);
 
  this.spell = spell;
}
 
// Link prototypes and add prototype methods
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);
 
Hero.prototype.greet = function () {
  return `${this.name} says hello.`;
}
 
Warrior.prototype.attack = function () {
  return `${this.name} attacks with the ${this.weapon}.`;
}
 
Healer.prototype.heal = function () {
  return `${this.name} casts ${this.spell}.`;
}
 
// Initialize individual character instances
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');

使用这段代码,我们已经用基本属性创建了Hero类,从原始构造函数创建了两个名为Warrior和Healer的字符类,向原型添加了方法,并创建了单独的字符实例。

本文來自 js教學 欄目,歡迎學習!

以上是深入了解JavaScript中的原型和繼承的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:digitalocean.com。如有侵權,請聯絡admin@php.cn刪除