JavaScript原型、原型链、原型式继承
原链接https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object_prototypes
本文基于原文进行了修改及简化,便于理解
基于原型的语言
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype
属性上,而非对象实例本身。
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
使用JavaScript中的原型
在javascript中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型(prototype)
,正如下面所展示的。
function doSomething(){}
console.log( doSomething.prototype );
// It does not matter how you declare the function, a
// function in javascript will always have a default
// prototype property.
var doSomething = function(){};
console.log( doSomething.prototype );
doSomething
函数有一个默认的原型属性,它在控制台上面呈现了出来.
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
现在,我们可以添加一些属性到 doSomething 的原型上面,如下所示.
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );
结果:
{
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
然后,我们可以使用 new 运算符来在现在的这个原型基础之上,创建一个 doSomething
的实例。正确使用 new 运算符的方法就是在正常调用函数时,在函数名的前面加上一个 new
前缀. 通过这种方法,在调用函数前加一个 new
,它就会返回一个这个函数的实例化对象. 然后,就可以在这个对象上面添加一些属性. 看.
function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );
结果:
{
prop: "some value",
__proto__: {
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
}
就像上面看到的, doSomeInstancing
的 __proto__
属性就是doSomething.prototype
.
但是这又有什么用呢?
好吧,当你访问 doSomeInstancing
的一个属性, 浏览器首先查找 doSomeInstancing
是否有这个属性.
如果 doSomeInstancing
没有这个属性, 然后浏览器就会在 doSomeInstancing
的 __proto__
中查找这个属性(也就是 doSomething.prototype).
如果 doSomeInstancing 的 __proto__
有这个属性, 那么 doSomeInstancing 的 __proto__
上的这个属性就会被使用. 否则, 如果 doSomeInstancing 的 __proto__
没有这个属性, 浏览器就会去查找 doSomeInstancing 的 __proto__
的 __proto__
,看它是否有这个属性.
默认情况下, 所有函数的原型属性的 __proto__
就是 window.Object.prototype
.
所以 doSomeInstancing 的 __proto__
的 __proto__
(也就是 doSomething.prototype 的 __proto__
(也就是 Object.prototype
)) 会被查找是否有这个属性. 如果没有在它里面找到这个属性, 然后就会在 doSomeInstancing 的 __proto__
的 __proto__
的 __proto__
里面查找. 然而这有一个问题: doSomeInstancing 的 __proto__
的 __proto__
的 __proto__
不存在. 最后, 原型链上面的所有的 __proto__
都被找完了, 浏览器所有已经声明了的 __proto__
上都不存在这个属性,然后就得出结论,这个属性是 undefined
.
function doSomething(){}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop: " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo: " + doSomeInstancing.foo);
console.log("doSomething.prop: " + doSomething.prop);
console.log("doSomething.foo: " + doSomething.foo);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo: " + doSomething.prototype.foo);
结果:
doSomeInstancing.prop: some value
doSomeInstancing.foo: bar
doSomething.prop: undefined
doSomething.foo: undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo: bar
constructor属性
每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。
例如,在控制台中尝试下面的指令:
function Person(first, last, age, gender, interests) {
this.name = {
'first': first,
'last': last
};
this.age = age;
this.gender = gender;
this.interests = interests;
this.bio = function() {
alert(this.name.first + ' ' + this.name.last +
' is ' + this.age + ' years old. He likes ' +
this.interests[0] + ' and ' + this.interests[1] + '.');
};
this.greeting = function() {
alert('Hi! I\'m ' + this.name.first + '.');
};
};
var person1 = new Person('Tammi', 'Smith', 32, 'neutral',
['music', 'skiing', 'kickboxing']);
person1.constructor
都将返回 Person()
构造器,因为该构造器包含这些实例的原始定义。
一个小技巧是,你可以在 constructor
属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new
关键字,便能将此函数作为构造器使用。
var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female',
['playing drums', 'mountain climbing']);
person3.name.first
person3.age
person3.bio()
通常你不会去用这种方法创建新的实例;但如果你刚好因为某些原因没有原始构造器的引用,那么这种方法就很有用了。constructor
属性还有其他用途。比如,想要获得某个对象实例的构造器的名字,可以这么用:
person1.constructor.name
修改原型
//这段代码将为构造器的 prototype 属性添加一个新的方法:
Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
}
person1.farewell();
这个函数执行成功了。但更关键的是,整条继承链动态地更新了,任何由此构造器创建的对象实例都自动获得了这个方法。
function Person(first, last, age, gender, interests) {
// 属性与方法定义
};
var person1 = new Person('Tammi', 'Smith', 32, 'neutral',
['music', 'skiing', 'kickboxing']);
Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
}
但是 farewell()
方法仍然可用于 person1
对象实例——旧有对象实例的可用功能被自动更新了。这证明了先前描述的原型链模型。这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。
你很少看到属性定义在 prototype 属性中,因为如此定义不够灵活。比如,你可以添加一个属性:
Person.prototype.fullName = 'Bob Smith';
但这不够灵活,因为人们可能不叫这个名字。用 name.first
和 name.last
组成 fullName
会好很多:
Person.prototype.fullName = this.name.first + ' ' + this.name.last;
然而,这么做是无效的,因为本例中 this
引用全局范围,而非函数范围。访问这个属性只会得到 undefined undefined
。但这个语句若放在 先前定义在 prototype
上的方法中则有效,因为此时语句位于函数范围内,从而能够成功地转换为对象实例范围。你可能会在 prototype
上定义常属性 (constant property) (指那些你永远无需改变的属性),但一般来说,在构造器内定义属性更好。
事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype
属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
// 构造器及其属性定义
function Test(a,b,c,d) {
// 属性定义
};
// 定义第一个方法
Test.prototype.x = function () { ... }
// 定义第二个方法
Test.prototype.y = function () { ... }
// 等等……
原型式继承
JavaScript通过原型链继承(通常被称为 原型式继承 —— prototypal inheritance)
//在构造器上定义属性
function Person(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
};
//所有方法都定义在构造器的原型上
Person.prototype.greeting = function() {
alert('Hi! I\'m ' + this.name.first + '.');
};
比如我们想要创建一个Teacher
类,就像我们前面在面向对象概念解释时用的那个一样。这个类会继承Person
的所有成员,同时也包括:
- 一个新的属性,
subject
——这个属性包含了教师教授的学科。 - 一个被更新的
greeting()
方法,这个方法打招呼听起来比一般的greeting()
方法更正式一点——对于一个教授一些学生的老师来说。
//第一步,创建一个Teacher()构造器
function Teacher(first, last, age, gender, interests, subject) {
//调用Person()构造函数
//重新指定您调用的函数里所有“`this`”指向的对象,即this指向Teacher()
//其他的变量指明了所有目标函数运行时接受的参数
Person.call(this, first, last, age, gender, interests);
//suject属性,教师教授的学科
this.subject = subject;
}
//我们也可以这么做,但Teacher()不是将他们从Person()中继承过来的
function Teacher(first, last, age, gender, interests, subject) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
this.subject = subject;
}
//第二步,我们已经定义了一个新的构造器,这个构造器默认有一个空的原型属性。
//我们需要让Teacher()从Person()的原型对象里继承方法。
Teacher.prototype = Object.create(Person.prototype);
//这时出现一个问题,
//Teacher()的prototype的constructor属性指向的是Person(),
//这是由我们生成Teacher()的方式决定的。
//所以我们需要将Teacher()的prototype的constructor指向Teacher
Teacher.prototype.constructor = Teacher;
//这时我们重写Teacher()原型上的greeting()方法
Teacher.prototype.greeting = function() {
var prefix;
if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
prefix = 'Mr.';
} else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
prefix = 'Mrs.';
} else {
prefix = 'Mx.';
}
alert('Hello. My name is ' +
prefix + ' ' +
this.name.last + ', and I teach ' +
this.subject + '.');
};
//创建teacher1实例对象
var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male',
['football', 'cookery'], 'mathematics');
//调用teacher1的属性和方法
teacher1.name.first;
teacher1.interests[0];
teacher1.subject;
teacher1.greeting();
跑一下题,请注意,如果您继承的构造函数不从传入的参数中获取其属性值,则不需要在call()
中为其指定其他参数。所以,例如,如果您有一些相当简单的东西:
function Brick() {
this.width = 10;
this.height = 20;
}
您可以这样继承width
和height
属性(以及下面描述的其他步骤):
function BlueGlassBrick() {
Brick.call(this);
this.opacity = 0.5;
this.color = 'blue';
}
请注意,我们仅传入了this
到call()
中 - 不需要其他参数,因为我们不会继承通过参数设置的父级的任何属性。