在 js 中创建对象,其中一个方式是使用构造函数,请看下面的一个例子
function Human(firstName, lastName) {
this.firstName = firstName,
this.lastName = lastName,
this.fullName = function() {
return this.firstName + " " + this.lastName;
}
}
让我们使用 Human 构造函数来创建 person1 和 person2 对象
var person1 = new Human("Virat", "Kohli");
var person2 = new Human("Sachin", "Tendulkar");
执行上面的代码,我们会得到构造函数的两个副本
每一个使用构造函数创建的对象都会拥有自己的属性和方法。通常情况下这是没有意义的,因为不同的对象的的方法如上面的 fullName
,是做一样的事情的,这只会浪费内存。下面我们会讨论如何解决这个问题。
Prototypes
在,JavaScript 中,当一个函数被创建时 js 引擎会将一个 prototype 属性添加进这个函数中。这个 prototype 属性是一个对象(称为原型对象),它默认有一个 constructor 属性,constructor 属性指向 prototype 所在的这个函数。我们可以使用 functionName.prototype 来得到 prototype 属性。
如上图所示,Human 构造函数有一个 prototype 属性,指向原型对象。原型对象有一个 constructor 属性,指向 Human 构造函数。接下来看下面的例子:
function Human(firstName, lastName) {
this.firstName = firstName,
this.lastName = lastName,
this.fullName = function() {
return this.firstName + " " + this.lastName;
}
}
console.log(Human);
使用下面的语法获取 Human 构造函数的 prototype 属性:
console.log(Human.prototype)
从上图可以看到,prototype 属性是一个对象,它有两个属性:
1. constructor 属性,指向 Human 函数本身
2. __proto__ 属性,在讲到继承的时候我们会谈论它
使用构造函数创建一个对象
当创建一个对象时,js 引擎为这个新对象添加了一个 __proto__ 属性,也称为dunder proto ,dunder 是 “double underscore”的简拼 。dunder proto 或 __proto__ 指向该构造函数的原型对象。
如上图所示,使用 Human 构造函数创建的 person1 对象拥有一个 dunder proto 或是 __proto__ 属性,且这个属性指向构造函数的原型对象。
var person1 = new Human("Virat", "Kohli");
从上图可以看出,person1 的 __proto__ 属性和 Human.prototype 属性是一样的,让我们使用 === 操作符来查看它们是否指向相同的地址。
Human.prototype === person1.__proto__ //true
这说明 person1 的 __proto__ 属性和 Human.prototype 指向了同一对象。
现在,让我们创建另一个对象 person2
var person2 = new Human("Sachin", "Tendulkar");
console.log(person2);
通过控制台的输出,我们发现 person2 的 __proto__ 属性也是跟 Human.prototype 一样。
Human.prototype === person2.__proto__ //true
person1.__proto__ === person2.__proto__ //true
从上面的判断中我们可以确定 person1 和 person2 的 __proto__ 属性都指向了 Human 构造函数的原型对象。
由此我们得出一个结论:构造函数的原型对象被所有由此构造函数创建的对象共享。
原型对象
因为原型对象是一个对象,我们可以为其增加属性和方法,这使得我们可以让所有由同样的构造函数创建的对象共享属性和方法。
要为原型对象增加属性,使用点号或者方括号的方式都可以
//使用 '.' 符号
Human.prototype.name = "Ashwin";
console.log(Human.prototype.name)//输出: Ashwin
//使用 '[]' 符号
Human.prototype["age"] = 26;
console.log(Human.prototype["age"]); //输出: 26
console.log(Human.prototype);
name 和 age 属性已经被添加到 Human 的 prototype
例子
//创建一个空的构造函数
function Person(){
}
//为原型对象添加 name 和 age 属性,和一个 sayName 方法
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
console.log(person1.name)// 输出" Ashwin
让我们来分析以下当我们执行 console.log(person.name)
时,都发生了什么事情。首先查看 person 对象是否有 name 属性
console.log(person1);
从上图可知 person1 除了 __proto__ ,没有任何属性,那为什么 console.log(person.name)
还是输出了 “Ashwin” 呢?
当我们尝试去获得一个对象的属性时,js 引擎首先在这个对象上寻找这个属性,如果有,就使用它,如果没有找到,那么它会尝试在原型对象,也就是这个对象的 __proto__ 所指向的原型对象上寻找这个属性,如果找到了,就使用它,如果没有,js 引擎就会继续往上寻找,即在原型对象的原型对象上寻找,就像是一条锁链,直到 null ,此时这个属性就会是 undefined。
所以,当执行到 person1.name
时,js 引擎先检查 person 对象是否有这个属性。这时,person 对象没有 name 属性,所以 js 引擎会去检查是否 person 的 __proto__ 指向的原型对象拥有这个属性,在原型对象这里找到了 name 属性,所以输出了 “Ashwin”。
让我们创建另一个对象 person2
var person2 = new Person();
console.log(person2.name)// 输出: Ashwin
现在,在 person1 对象上定义 name 属性
person1.name = "Anil"
console.log(person1.name)//输出: Anil
console.log(person2.name)//输出: Ashwin
在这里,person1.name 输出了 “Anil”,就如前面所说,js 引擎首先会尝试在该对象中寻找该属性,所以这里输出了 person1 的 name 属性。
prototype 中的问题
由于由同一构造函数创建的对象共享一个原型对象,这个原型对象上的属性和方法也因此被共享。如果一个对象 A 修改了原型的属性的值,那么其它对象并不会被影响。
console.log(person1.name);//输出: Ashwin
console.log(person2.name);//输出: Ashwin
person1.name = "Ganguly"
console.log(perosn1.name);//输出: Ganguly
console.log(person2.name);//输出: Ashwin
在第一行跟第二行,person1 和 person2 都没有 name 这个属性,因此它们使用了原型的属性,输出了同样的值。
当 person1 改变了 name 的值,它在自己本身创建了一个 name 属性。
考虑下面的例子,这个例子展示了当原型对象包含引用类型的一个问题。
function Person(){
}
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();
//为数组增加元素
person1.friends.push("Amit");
console.log(person1.friends);// 输出: "Jadeja, Vijay, Amit"
console.log(person2.friends);// 输出: "Jadeja, Vijay, Amit"
从上面的例子可以看到,person1 和 person2 都指向了原型对象上的 friends 数组。person1 向 friends 属性增加了一个元素。
由于 friends 数组存在于 Person.prototype 中,而不是 person1,在 person1 中对 friends 属性所做的改动也反映到了 person2.friends 中。
如果我们确实是希望所有对象共享这个数组,那么这是没有问题的,但这里不是这种情况。
结合构造器和原型
想要解决构造器和原型的问题,我们可以结合使用它们。
问题有两个:
- constructor: 每一个对象有自己的方法实例
- prototype: 一个对象对属性的修改影响其它对象
我们可以通过在构造函数中声明私有属性,在原型中声明方法和公共属性来解决这个问题。如:
//在构造函数中定义私有属性
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", "Kohli");
var person2 = new Human("Sachin", "Tendulkar");
//查看是否这两个对象的方法是否指向同一位置
console.log(person1.sayName === person2.sayName) // true
//修改 friends 属性
person1.friends.push("Amit");
console.log(person1.friends)// 输出: "Jadeja, Vijay, Amit"
console.log(person2.frinds)//输出: "Jadeja, Vijay"
在上面的例子中,person2 的 friends 属性没有被 person1 的修改影响到。