本文翻译自 https://medium.com/better-programming/prototypes-in-javascript-5bba2990e04b,作者 Rupesh Mishra,翻译时有删改,标题有改动。
在这篇文章中,我们将会讨论 JavaScript 中的原型是什么,以及在 JavaScript 中如何实现面向对象编程。
constructor
创建对象的问题看看下面的代码:
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");
当执行上面的代码之后,JavaScript 引擎将会创建两个该构造函数的拷贝,一个是 person1
另一个是 person2
。
从图中可以看出,每个对象都拥有 firstName
lastName
fullName
这三个属性。每个使用构造函数创建的对象都会拥有自己的属性和方法,但是我们没有必要声明多个 fullName
函数,这会浪费内存。接下来,我们看看如何解决这个问题。
在 JavaScript 中创建一个函数的时候,JavaScript 引擎会添加一个 prototype
属性给这个函数,这个 prototype
属性指向一个对象,这就是我们所说的原型对象。原型对象默认会有一个 constructor
属性,这个 constructor
指向的就是有 prototype
属性的函数。看下面的图示:
如上图,Human
构造函数有一个 prototype
属性指向原型对象。而原型对象有一个 constructor
属性又指回了这个构造函数。让我们看下面的代码:
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
构造函数的原型属性:
console.log(Human.prototype)
从上面的图片可以看出,函数的原型属性是一个有两个属性的对象(原型对象),两个属性如下:
constructor
属性,这个属性指向构造函数 Human
;
__proto__
属性,后面我们讨论继承的文章中将会讨论这个属性;
当一个对象被创建的时候,JavaScript 引擎添加了一个 __proto__
属性给新创建的对象。__proto__
属性会指向构造函数的原型对象(构造函数的 prototype
属性指向的对象)。
如上图所示, 使用 Human
构造函数创建的 person1
对象有一个 __proto__
属性,这个属性指向的就是构造函数的原型对象。
var person1 = new Human("Virat", "Kohli");
从上面图片可以看出,person1
的 __proto__
属性所指向的对象和 Human.prototype
属性所指向的对象相同。我们使用 ===
检验一下:
// true
console.log(Human.prototype === person1.__proto__ )
上面全等符号得到的结果是 true
,所以我们可以进一步确定新对象的 __proto__
属性指向的对象就是构造函数的原型对象。
接下来我们是使用 Human
构造函数创建 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
构造函数的原型对象。
构造函数的原型对象会在所有使用该构造函数创建的对象中共享。
因为原型对象是对象,所以我们可以添加属性和方法给原型对象。因此允许使用构造函数创建的所有对象共享那些属性和方法。
可以使用点表示法或方括号表示法将新属性添加到构造函数的原型对象中,如下所示:
//Dot notation
Human.prototype.name = "Ashwin";
console.log(Human.prototype.name)//Output: Ashwin
//Square bracket notation
Human.prototype["age"] = 26;
console.log(Human.prototype["age"]); //Output: 26
console.log(Human.prototype);
name
和 age
属性已添加到 Human
原型对象中。
//Create an empty constructor function
function Person(){
}
//Add property name, age to the prototype property of the Person constructor function
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.sayName = function(){
console.log(this.name);
}
//Create an object using the Person constructor function
var person1 = new Person();
//Access the name property using the person object
console.log(person1.name)// Output" Ashwin
我们分析一下 console.log(person.name)
执行的时候发生了什么。让我们看看 person1
是否有 name
属性。
console.log(person1);
正如我们所见, person1
对象是空的,并且它只有一个 __proto__
属性。那么 console.log(person.name)
的结果是 Ashwin
是如何发生的呢?
当我们尝试访问对象的属性时,JavaScript 引擎首先尝试在对象上查找属性,如果该属性存在于对象上,则它会输出其值。但是,如果该属性不存在于对象上,则它将尝试在 __proto__
上找到该属性。如果找到了属性,则返回值,否则 JavaScript 引擎将尝试在对象的 __proto__
上找到该属性。这条查找链一直持续到 __proto__
属性为 null
,在这种情况下,输出将为 undefined
。
因此,当获取 person1.name
的值时,JavaScript 引擎会检查 person1
对象上是否存在该属性。此时,name
属性不在 person1
对象上。所以 JavaScript 引擎会依据 __proto__
找到上层对象,并且在该对象中查找 name
属性是否存在。此时,name
属性存在于 person1.__proto__
中, 所以返回查找到的值 Ashwin
。
让我们使用 Person
构造函数创建一个对象 person2
。
var person2 = new Person();
//Access the name property using the person2 object
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
。如前所述,JavaScript 引擎首先尝试在对象本身上查找属性。在这种情况下,person1
对象本身存在 name
属性,因此 JavaScript 引擎输出person1
的 name
属性值 Anil
。
而对于 person2
,对象上不存在 name
属性。因此,它输出 person2
原型对象上的 name
属性值 Ashwin
。
由于原型对象在使用构造函数创建的所有对象之间共享,因此其属性和方法也在所有对象之间共享。如果对对象 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
上面代码前两行,获取 name
属性都是在原型对象上获取的,而 person1.name = "Ganguly"
则直接给 person1
对象添加了 name
属性,所以下面打印的时候,是直接从 person1
对象上获取到的 name
属性的值,而 person2
由于没有这个属性,所以依然会到原型对象上获取 name
属性。
让我们来看另外一个原型的例子,在这个例子中原型对象有一个引用类型的属性。更改这个引用数据将会出现问题。
//Create an empty constructor function
function Person(){
}
//Add property name, age to the prototype property of the Person constructor function
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.friends = ['Jadeja', 'Vijay'],//Arrays are of reference type in JavaScript
Person.prototype.sayName = function(){
console.log(this.name);
}
//Create objects using the Person constructor function
var person1= new Person();
var person2 = new Person();
//Add a new element to the friends array
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
数组是存在于 Person.prototype
(原型对象)上的,所以它不属于 person1
,因此 person1
更改 friends
数组时,更改的是原型对象的数组。所以 person2.friends
访问的同样也是这个被更改过的数组。
上面的操作如果是为了让所有的实例都共享这个被变更过的数组,那这样做是没问题的。但是明显,这里我们并不想要它这样。我们想要 person2
访问到的数组内的数据依然是最开始的那几个,并不想让数组被改变。
我们知道,使用构造函数或者原型创建对象都会存在问题,接下来我们组合使用这两者来解决上面的问题。
构造函数的问题:每个对象都会声明对应的函数,浪费内存;
原型的问题:更改引用类型的原型属性的值会影响到其他实例访问该属性;
为了解决上面的问题,我们可以用把所有对象相关的属性定义在构造函数内,把所有共享属性和方法定义在原型上。
//Define the object specific properties inside the constructor
function Human(name, age){
this.name = name,
this.age = age,
this.friends = ["Jadeja", "Vijay"]
}
//Define the shared properties and methods using the prototype
Human.prototype.sayName = function(){
console.log(this.name);
}
//Create two objects using the Human constructor function
var person1 = new Human("Virat", 31);
var person2 = new Human("Sachin", 40);
//Lets check if person1 and person2 have points to the same instance of the sayName function
console.log(person1.sayName === person2.sayName) // true
//Let's modify friends property and check
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
属性没有更改。这是因为 person1
对象更改的是自己的 friends
属性,不会影响到 person2
内的。
往期精彩:
前端面试必会 | 一文读懂 JavaScript 中的 this 关键字
前端面试必会 | 一文读懂现代 JavaScript 中的变量提升 - let、const 和 var
前端面试必会 | 一文读懂 JavaScript 中的闭包
前端面试必会 | 一文读懂 JavaScript 中的作用域和作用域链
前端面试必会 | 一文读懂 JavaScript 中的执行上下文
InterpObserver 和懒加载
初探浏览器渲染原理
CSS 盒模型、布局和包含块
详细解读 CSS 选择器优先级
关注公众号可以看更多哦。
感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。
公众号 交流群