理解JavaScript中的Prototype和继承

理解JavaScript中的Prototype和继承

January 12, 2018

 

介绍

JavaScript是基于prototype的语言,意味着对象属性和方法可以通过泛化的对象来共享,能够被克隆、扩展。这就是prototype继承,与class继承不同。在流行的OOP语言中,JavaScript相对独特,其它主流语言如PHP、Python和Java是基于class的语言,它们定义classes作为对象的蓝图。

本教程中,将学习什么是对象原型、如何使用constructor来扩展prototype为新对象。我们还将学习继承、原型链。

JavaScript原型

在Understanding Objects in JavaScript中,我们学习了对象数据类型,如何创建对象,如何访问、修改对象属性。现在,我们将学习如何好似用prototype来扩展对象。

每个JavaScript对象有一个内部属性Prototype。首先生成一个新的空对象。

let x = {};

另一个方式是用constructor:

let x = new Object().

[[Prototype]]的双层括号情调它是一个内部属性,不能直接在代码中访问。

要找到新对象的[[Prototype]],使用getPrototypeOf()方法。

Object.getPrototypeOf(x);

输出将包括几个内置属性和方法。

Output

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

另一个找到[[Prototype]]的方法是通过__proto__属性。__proto__暴露对象的内部[[Prototype]]。

注意,.__proto__是一个legacy feature,不能用于产品代码中,在现代的浏览器中不存在了。然而,我们在此使用它进行展示说明。

x.__proto__;

输出相同,如同你使用了getPrototypeOf()。

Output

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

注意,每个JavaScript对象有一个[[Prototype]],它创造了一个将多个对象link起来的方法。

你创建的对象有[[Prototype]],与内建对象一样(如Date、Array)。通过prototype属性,可以从一个对象到另一个对象引用该内部属性,后面将详细介绍。

Prototype继承

当你尝试访问对象的属性和方法时,JavaScript将首先搜索对象本身,如果没有发现,它将搜索对象[[Prototype]]。如果都没有发现,JavaScript将检查linked对象的prototype,并继续搜索直到prototype chain的尽头。

在prototype chain的尽头是Object.prototype。所有对象都继承Object的属性和方法。本例中,x是一个继承自Object空对象。X可以好似用 Object的任何属性和方法如toString()。

x.toString();

Output

[object Object]

该原型链(prototype chain)只有一个link长:从x -> Object。因此:

x.__proto__.__proto__;

Output null

我们来看另一个类型的对象。如果你经历过Working with Arrays in JavaScript,你知道有很多内置方法,比如pop()和push()。当你创建一个数组时,你能够访问这些内置方法的原因是任何创建的数组都能访问Array.prototype的属性和方法。

可以测试一下:

let y = [];

记住,也可以是:let y = new Array()。

如果查看y数组的[[Prototype]],将看到它有更多属性和方法。它从Array.prototype继承了所有东西。

y.__proto__;

[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, ]

这里,prototype的constructor属性被设为Array()。属性constructor返回对象的构造函数——用来从函数构建对象

现在可以把两个prototype链接在一起,原型链就更长了:y -> Array -> Object。

y.__proto__.__proto__;

Output

{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]]上的属性和方法。

这些prototype可以被链起来,每个附加的对象都继承链上的任何东西。链的尽头是Object.prototype。

构造函数

构造函数被用来构建新对象。New算用来生成新实例(based off构造函数)。我们已看到一些内置JavaScript构造函数,比如new Array()/new Date(),但是我们也可以创建自己的定制模板,以构建新对象。

例如,如果要生成一个简单的、基于文本的角色扮演游戏。用户可以选择一个角色,并选择他们能有什么character类别,比如warrior、healer、thief。

因为每个character将分享很多特性,比如有名字、级别、打击点数等,合理的方案是构建一个constructor作为模板。然而,因为每个character类有不同的能力,我们希望确保每个character只能访问自己的能力。我们来看一下如何用prototype继承和constructor实现这一点。

一开始,构造函数只是一个普通的函数。当它被一个实例用new调用时,就成为一个constructor。在JavaScript中,惯例是大写constructor的首字母。

// characterSelect.js

// Initialize a constructor function for a new Hero

function Hero(name, level) {

  this.name = name;

  this.level = level;

}

这是构造函数Hero。因为每个character都有name和level,每个新character都有这两个属性。关键字this指向被创建的新的实例,所以设置this.name为name确保新对象设置了name属性。

现在可以创建一个新的实例。

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

如果控制台输出hero1,将看到生成了一个新对象,新属性设置正确。

Output

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

现在,如果拿到hero1[[Prototype]],可以看到constructor就是Hero()。

Object.getPrototypeOf(hero1);

Output

constructor: ƒ Hero(name, level)

你可以已经注意到,我们在constructor中只定义了属性而没有方法。这是JavaScript的惯例:在prototype上定义方法,提高效率和代码可读性。

在Hero上用prototype添加方法。

//characterSelect.js

//...

// Add greet method to the Hero prototype

Hero.prototype.greet = function () {

  return `${this.name} says hello.`;

}

因为greet()在Heroprototype内部,hero1是Hero的实例,该方法可被hero1使用。

hero1.greet();

Output

"Bjorn says hello."

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

这很好,但是现在我们希望创建character类来让heroes使用。没有道理将每个类的所有能力都放进Hero构造函数,因为不同classes有不同的能力。我们希望创建新的constructor,但是我们也希望与原来的Hero连接起来。

可以用call()来从一个constructor到另一个constructor拷贝属性。如下:

// characterSelect.js

//...

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

}

两个新constructor就有了Hero的属性,也有自己独特的属性。我们将添加attack()方法到Warrior,添加heal()到Healer。

//characterSelect.js

//...

Warrior.prototype.attack = function () {

  return `${this.name} attacks with the ${this.weapon}.`;

}

Healer.prototype.heal = function () {

  return `${this.name} casts ${this.spell}.`;

}

此时,创建新的角色实例。

// characterSelect.js

const hero1 = new Warrior('Bjorn', 1, 'axe');

const hero2 = new Healer('Kanin', 1, 'cure');

hero1是一个Warrior,且具有新的属性。

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

可以用Warrior prototype上的新方法。

hero1.attack();

Console:"Bjorn attacks with the axe."

如果尝试使用prototype chain“上一层”的方法呢?

hero1.greet();

Output:Uncaught TypeError: hero1.greet is not a function

调用call()来链其构造桉树时,Prototype属性和方法不是自动linked的。我们将用Object.create()来link这些prototypes,要确保在任何额外的方法被创建、添加到该prototype之前做这件事。

//characterSelect.js

//...

Warrior.prototype = Object.create(Hero.prototype);

Healer.prototype = Object.create(Hero.prototype);

// 所有其它prototype方法添加在下面

// ...

现在可以在WarriorHealer实例上成功使用Heroprototype方法了。

hero1.greet();

Output:"Bjorn says hello."

以下是角色生成页的全部代码。

// characterSelect.js

// 初始化构造函数

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

Warrior.prototype = Object.create(Hero.prototype);

Healer.prototype = Object.create(Hero.prototype);

// add prototype methods

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,从原来的constructor构建两个角色类Warrior和Healer,向prototypes添加方法,生成单个角色实例。

结论

JavaScript是一个基于原型的语言,与传统基于类的策略不同。本文学习如何JavaSscriptprototype如何工作,如何通过隐藏的[[Prototype]]来link对象属性和方法,使所有对象共享。我们也学习如何生成定制constructor,以及原型继承如何工作——传递属性和方法值。


猜你喜欢

转载自blog.csdn.net/xingyanchao/article/details/79425563
今日推荐