理解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()在Hero的prototype内部,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方法添加在下面
// ...
现在可以在Warrior和Healer实例上成功使用Hero的prototype方法了。
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是一个基于原型的语言,与传统基于类的策略不同。本文学习如何JavaSscript中prototype如何工作,如何通过隐藏的[[Prototype]]来link对象属性和方法,使所有对象共享。我们也学习如何生成定制constructor,以及原型继承如何工作——传递属性和方法值。