JavaScript中的原型对象
原型
原型是JavaScript中继承的基础,JavaScript的继承就是基于原型的继承。
一 理解原型
1.1 函数的原型对象
- 无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。
- 默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之相关的构造函数。
// 声明一个函数
function Person() {}
// 声明之后,函数就有了一个与之关联的原型对象
console.log(typeof Person.prototype);
console.log(Person.prototype);
下面是代码的运行结果:
下面可以看出Person函数和Person.prototype之间的关系:
1.2 对象实例的原型对象
- 每次调用构造函数创建一个新实例,这个实例内部的
[[Prototype]]
指针就会被赋值为构造函数的原型对象。 - 脚本中没有访问这个
[[Prototype]]
特性的标准方式,但是Firefox、Safari和Chrome会在每个对象上暴露__proto__
属性。
//声明构造函数Person后,函数就有了一个与之关联的原型对象
function Person(){
}
//给Person关联的原型对象添加属性和方法
//这些属性和方法为各个对象实例所共享
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
//调用Person构造函数,创建对象实例
//对象实例调用原型对象中共享的方法
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
下图展示了上面例子各个对象之间的联系:
说明:
- 构造函数、原型对象和实例是3个完全不同的对象(在js中,函数也是对象)。
- 实例通过内部的隐藏特性
[[Prototype]]
链接到原型对象(在某些浏览器中通过__proto__
访问该特性),构造函数通过prototype属性链接到原型对象。实例与构造函数没有直接联系,与原型对象有直接联系。 - 同一个构造函数创建的多个实例,共享同一个原型对象。
- 正常的原型链会终止于Object的原型对象,Object原型的原型是null。
关于说明的最后一点,请看下面的代码:
function Person() {}
console.log(Person.prototype.__proto__ === Object.prototype);//true
console.log(Person.prototype.__proto__.constructor === Object);//true
console.log(Person.prototype.__proto__.__proto__ === null);//true
二 原型层级
2.1两步搜索
在通过对象访问属性时,会按照这个属性名开始搜索。
- 搜索开始于对象实例本身。如果在这个实例上发现了给定的属性名,则返回对应的属性值。
- 如果没有找到这个属性,则搜索会沿着指针进入原型对象。在原型对象上找到属性后,再返回属性值。
注意:前面提到的constructor属性只存在于原型对象,因此通过实例对象也是可以访问到的。
function Person() {}
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Soft Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.hobby = "play basketball";
console.log(person1.hobby);//play basketball: 在实例上搜索到
console.log(person1.name);//Jack:在原型上搜索到
console.log(person2.hobby);//undefined:在实例和原型都没有搜索到
2.2属性遮蔽
只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性。
function Person() {}
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Soft Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Greg";
console.log(person1.name);// "Greg":来自实例
console.log(person2.name);//"Jack":来自原型
2.3hasOwnProperty()
hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自Object的,会在属性存在于调用它的对象实例上时返回true。
function Person () {
}
Person.prototype.name = "Jack";
var p1 = new Person();
p1.sex = "男";
//sex属性是直接在p1属性中添加,所以是true
alert("sex属性是对象本身的:" + p1.hasOwnProperty("sex"));
//name属性是在原型中添加的,所以是false
alert("name属性是对象本身的:" + p1.hasOwnProperty("name"));
//age属性不存在,所以也是false
alert("age属性是存在于对象本身:" + p1.hasOwnProperty("age"));
三 原型和in操作符
3.1in操作符
in操作符会在可以通过对象访问指定属性时返回true,无论该属性是否在实例上还是在原型上。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" ——来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" ——来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" ——来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
3.2 hasOwnProperty()和 in 操作符配合
在以上代码执行的整个过程中,name 属性要么是直接在对象上访问到的,要么是通过原型访问到的。因此,调用"name" in person1 始终都返回 true,无论该属性存在于实例中还是存在于原型中。
同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示。
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false
四 创建对象的两种模型
对于创建对象来说,我们希望的是尽可能像其他语言,比如java中的那样,每个对象实例有自己独一份的属性,但是方法最好是多个对象实例可以共享,不造成内存浪费。而下面介绍的js中创建对象的两种方法都有其缺陷。
4.1 构造函数模型创建对象的缺陷
在构造函数中添加的属性和方法,每个对象都有自己独有的一份,大家不会共享。这个特性对属性比较合适,但是对方法又不太合适。因为对所有对象来说,他们的方法应该是一份就够了,没有必要每人一份,造成内存的浪费和性能的低下。
function Person() {
this.name = "李四";
this.age = 20;
this.eat = function() {
alert("吃完东西");
}
}
var p1 = new Person();
var p2 = new Person();
//每个对象都会有不同的方法
//也就是说每调用一次构造函数,都会重复创建一遍方法
alert(p1.eat === p2.eat); //fasle
可以使用下面的方法解决:
<script>
function Person() {
this.name = "李四";
this.age = 20;
this.eat = eat;
}
function eat() {
alert("吃完东西");
}
var p1 = new Person();
var p2 = new Person();
//因为eat属性都是赋值的同一个函数,所以是true
//将方法的定义从构造函数提取出来,可以避免重复创建同一个方法
alert(p1.eat === p2.eat); //true
</script>
但是上面的这种解决方法具有致命的缺陷:封装性太差。使用面向对象,目的之一就是封装代码,这个时候为了性能又要把代码抽出对象之外,这是反人类的设计。
4.2 原型模型创建对象的缺陷
原型中的所有的属性都是共享的。也就是说,用同一个构造函数创建的对象去访问原型中的属性的时候,大家都是访问的同一个对象,如果一个对象对原型的属性进行了修改,则会反映到所有的对象上面。
但是在实际使用中,每个对象的属性一般是不同的。张三的姓名是张三,李四的姓名是李四。
但是,这个共享特性对方法(属性值是函数的属性)又是非常合适的。所有的对象共享方法是最佳状态。这种特性在c#和Java中是天生存在的。
4.3使用组合模式解决上述两种缺陷
原型模式适合封装方法,构造函数模式适合封装属性,综合两种模式的优点就有了组合模式。
<script type="text/javascript">
//在构造方法内部封装属性
function Person(name, age) {
this.name = name;
this.age = age;
}
//在原型对象内封装方法
Person.prototype.eat = function (food) {
alert(this.name + "爱吃" + food);
}
Person.prototype.play = function (playName) {
alert(this.name + "爱玩" + playName);
}
var p1 = new Person("李四", 20);
var p2 = new Person("张三", 30);
p1.eat("苹果");
p2.eat("香蕉");
p1.play("Jack");
p2.play("Mike");
</script>
4.4动态原型模式创建对象
前面讲到的组合模式,也并非完美无缺,有一点也是感觉不是很完美。把构造方法和原型分开写,总让人感觉不舒服,应该想办法把构造方法和原型封装在一起,所以就有了动态原型模式。
动态原型模式把所有的属性和方法都封装在构造方法中,而仅仅在需要的时候才去在构造方法中初始化原型,又保持了同时使用构造函数和原型的优点。
请看下面的代码:
<script type="text/javascript">
//构造方法内部封装属性
function Person(name, age) {
//每个对象都添加自己的属性
this.name = name;
this.age = age;
/*
判断this.eat这个属性是不是function,如果不是function则证明是第一次创建对象,
则把这个funcion添加到原型中。
如果是function,则代表原型中已经有了这个方法,则不需要再添加。
perfect!完美解决了性能和代码的封装问题。
*/
if(typeof this.eat !== "function"){
Person.prototype.eat = function () {
alert(this.name + " 在吃");
}
}
}
var p1 = new Person("志玲", 40);
p1.eat();
</script>
五 原型的其他注意点
5.1 其他原型语法
- 可以使用对象字面量来重写原型,而不需要将属性和方法逐个赋值给原型对象。
- 但是要注意,如果不在其中重写constructor属性,则默认等于Object。
function Person() {}
Person.prototype = {
name: "Jack",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
let friend = new Person();
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object);//true
最好是在重写原型对象时专门设置一下它的值。
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Jack",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
//以这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性
//而原生constructor属性默认是不可枚举的。
//因此,最好使用Object.defineProperty()方法定义constructor属性:
function Person() {}
Person.prototype = {
name: "Jack",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
Object.defineProperty(Person.prototype,"constructor",{
enumerable: false,
value: Person
});
5.2 原型的动态性
- 因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
let friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
}
friend.sayHi(); //"hi"
- 实例对象的
[[Prototype]]
指针是在调用构造函数时自动赋值的,这个指针即使吧原型修改为不同的对象也不会改变。重写整个原型会切断最初原型与构造函数的联系,但是实例引用的仍然是最初的原型。
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Jake",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
friend.sayName(); //错误
5.3 原生对象原型
- 所有原生引用类型的构造函数都在原型上定义了实例方法。比如数组实例的sort()方法就是Array.prototype上定义的,而字符串的substring()方法也是在String.prototype上定义的。
- 通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。
- 可以像修改自定义对象原型一样修改原生对象原型,但是不推荐这么做。
String.prototype.startsWith = function(text) {
return this.indexOf(text) ===0 ;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
5.4原型的问题
- 首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
- 原型的最主要问题源自它的贡献性。
- 原型上的所有属性是在实例间共享的,这对函数来说比较合适。对于包含原始值的属性也还好,如前面例子所示,可以通过实例上添加同名属性来简单遮蔽原型上的属性。真正的问题来自包含引用值的属性。看下面的例子。
function Person() {}
Person.prototype = {
constructor: Person,
name: "Jake",
age: 29,
job: "Software Engineer",
friends: ["Shelby","Court"],
sayName() {
console.log(this.name);
}
};
let p1 = new Person();
let p2 = new Person();
p1.friends.push("Van");
console.log(p1.friends);// "Shelby,Court,Van"
console.log(p2.friends);// "Shelby,Court,Van"
console.log(p1.friends==p2.friends);//true
//如果是有意在多个实例间共享数组,那没什么问题
//但是一般来说,不同的实例应该有自己的属性副本