在这篇文章中我们会讨论什么是JavaScript的原型,以及原型是如何帮助JavaScript实现面向对象编程的。
在上一篇文章中,我们已经学习了多种在JavaScript中创建对象的方法,其中一种就是用构造函数。
用构造函数创建对象存在的问题
思考下面这个构造函数
function Human(firstName, lastName) {
this.firstName = firstName,
this.lastName = lastName,
this.fullName = function() {
return this.firstName + " " + this.lastName;
}
}
var person1 = new Human("Virat", "Kohli");
console.log(person1)
用Human构造函数创建person1和person2两个对象:
var person1 = new Human("Virat", "Kohli");
var person2 = new Human("Sachin", "Tendulkar");
在执行以上代码时,js引擎会分别为person1和person2创建两个备份的构造函数
copy.png
就是说每个通过构造函数创建的对象都会复制一份属于自己的属性和方法。两个fullName实例做同样的事情是没有意义的,给每个对象存储单独的函数实例会造成内存浪费,接下来看看我们要如何解决这个问题。
Prototypes
当在js中创建一个函数时,js引擎就会给函数加上一个prototype属性,这个prototype属性是一个对象(称为原型对象),默认有constructor属性
prototype.png
如上图所示,Human构造函数有一个prototype属性指向prototype对象,prototype对象有一个constructor属性指向Human构造函数,再看下面这个例子:
function Human(firstName, lastName) {
this.firstName = firstName,
this.lastName = lastName,
this.fullName = function() {
return this.firstName + " " + this.lastName;
}
}
var person1 = new Human("Virat", "Kohli");
console.log(person1)
person1.png
要访问Human构造函数的prototype属性,执行以下语句:
console.log(Human.prototype)
human-prototype.png
从上图可以看出,函数的prototype属性是一个有两个属性的对象(原型对象)
1.constructor属性指向Human函数本身;
2._proto属性:我们将在讲解js继承的时候再讨论这点;
使用构造函数创建对象
当在js中创建对象时,js引擎就会给新建的对象添加一个_proto属性,称为dunder(double underscore的简称) proto,_proto指向构造函数的原型对象。
从上图可以看出,使用Human构造函数创建的person1对象有一个_proto属性指向构造函数的原型对象
//用Human构造函数创建person1对象
var person1 = new Human("Virat", "Kohli");
compare.png
从以上图片可以看出,person1的_proto属性和Human.prototype属性是一样的,
现在用操作符 === 检查一下他们是否指向相同的内存
Human.prototype === person1.__proto__ //true
这表明person1的_proto属性和Human.prototype指向相同的对象。
现在,我们用Human构造函数创建另一个person2对象
var person2 = new Human("Sachin", "Tendulkar");
console.log(person2);
image.png
上面的控制台输出表明甚至连person2的_proto属性都等于Human.prototype属性并且指向同一个对象。
Human.prototype === person2.__proto__ //true
person1.__proto__ === person2.__proto__ //true
以上结果表明person1和person2的_proto属性指向Human构造函数的原型对象
image.png构造函数的原型对象在所有使用该构造函数创建的对象中共享
原型对象
既然原型对象是一个对象,那我们可以在原型对象上添加属性和方法,这使得所有通过构造函数创建的对象都可以共享这些属性和方法。
可以用点表示法或者方括号表示法将新属性添加到构造函数的prototype属性中,如下图所示:
//点表示法
Human.prototype.name = "Ashwin";
console.log(Human.prototype.name); // Ashwin
//方括号表示法
Human.prototype["age"] = 26;
console.log(Human.prototype["age"]); // 26
console.log(Human.prototype);
add.png
name和age属性已经被添加到Human的原型中。
// 创建一个空的构造函数
function Person(){
}
// 添加name, age 属性到Person构造函数的原型对象中
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.sayName = function(){
console.log(this.name);
}
// 用Person 构造函数创建一个对象
var person1 = new Person();
// 用person1对象访问name属性
console.log(person1.name) // Ashwin
来分析一下当我们执行console.log(person1.name)时发生了什么,看看person1对象是否有name属性
image.png
可以看到person1对象是空的,除了它的 _proto属性之外它没有任何属性。
那么console.log(person1.name)输出的“Ashwin”是哪来的呢?
当我们想访问一个对象的属性时,js引擎首先尝试在对象上查找属性,如果对象中存在属性则输出属性值,如果该对象上不存在该属性,那么它会尝试在原型对象上查找该属性,找到了就返回属性值,否则js引擎会通过_proto 继续查找,直到_proto值为null时这条链才会停止。
所以,当person1.name被调用时,js引擎会检查person1对象上是否存在该属性,这种情况下,name属性并不存在于person1对象中,所以js引擎会继续检查name属性是否存在于person1对象的原型属性中,显然是存在的,所以输出结果返回“Ashwin”。
现在用Person构造函数创建另一个对象person2
var person2 = new Person();
// 通过person2对象访问name属性
console.log(person2.name)// Output: Ashwin
接着给person1对象定义一个name属性
person1.name = "Anil"
console.log(person1.name)//Output: Anil
console.log(person2.name)//Output: Ashwin
这里person1.name会输出“Anil”,根据之前提到的,js引擎首先向对象本身查找属性,这时name属性已经存在于person1对象上,因此js引擎会输出person1的name属性值。
而person2对象本身并没有name属性,于是输出person2原型对象的name属性。
原型存在的问题
由于原型对象在使用构造函数创建的所有对象之间共享,因此其属性和方法也在所有对象之间共享,如果object A 修改了原型的私有属性值,其他的对象不会受此影响,因为object A 会在其对象上创建一个它自己的属性,如下所示:
console.log(person1.name);//Output: Ashwin
console.log(person2.name);//Output: Ashwin
person1.name = "Ganguly"
console.log(perosn1.name);//Output: Ganguly
console.log(person2.name);//Output: Ashwin
第一次输出时person1和person2都没有name属性,因此他们都访问原型的name属性并且输出了相同的值。
当person1给name属性赋值一个新值时,它在自己的对象中创建了name属性。
思考另一个例子——当原型对象中包含引用类型的属性时会发生什么问题
function Person() {} // 创建空的构造函数
// 将name age 属性添加到Person构造函数的prototype属性中
Person.prototype.name = "Ashwin";
Person.prototype.age = 26;
Person.prototype.friends = ['Jadeja', 'Vijay'];
Person.prototype.sayname = function() {
console.log(this.name);
}
// 使用构造函数创建对象
var person1 = new Person();
var person2 = new Person();
// 往friends 数组中添加一个元素
person1.friends.push("Amit");
console.log(person1.friends); // Output: "Jadeja, Vijay, Amit"
console.log(person2.friends); // Output: "Jadeja, Vijay, Amit"
在上面的例子中,person1和person2都指向原型对象的同一个friends数组,person1通过往数组里加字符串元素改变了friends属性。
因为friends数组是存在于Person.prototype而不是person1中,person1对象改变了friend属性,同时也影响了person2.friends的值(指向的是同一个数组)
如果想让所有实例共享一个数组,那是ok的,但事实却并不尽然。
结合 constructor/Prototype
可以结合构造函数和函数来解决原型和构造函数存在的问题。
1.构造函数问题:每个对象都有自己的函数实例;
2.原型问题: 一个对象修改的属性(引用类型的)会影响其他对象;
为了解决这个问题,我们可以在构造函数中定义所有特定对象的属性,并在原型中定义所有共享属性和方法,如下所示:
// 在构造函数中定义对象特有的属性
function Human(name, age){
this.name = name,
this.age = age,
this.friends = ["Jadeja", "Vijay"]
}
// 在原型中定义共享的属性和方法
Human.prototype.sayName = function(){
console.log(this.name);
}
// 使用构造函数创建两个对象
var person1 = new Human("Virat", 31);
var person2 = new Human("Sachin", 40);
// 看一下person1和person2是否指向sayName函数的同一实例
console.log(person1.sayName === person2.sayName) // true
// 更改friends属性
person1.friends.push("Amit");
console.log(person1.friends)// Output: "Jadeja, Vijay, Amit"
console.log(person2.friends)//Output: "Jadeja, Vijay"
这里我们期望每个对象都有它们自己的name、age和friends属性,因此我们用this在构造函数中定义了这些属性,而sayName是定义在对象原型上的,可以在所有的对象中共享;
在上面的例子中,更改person1的friends属性时,person2的friends属性没有改变
image.png
网友评论