凡是搞前端开发的或者玩 JavaScript 的同学都知道,原型对象和原型链是 JavaScript 中最为重要的知识点之一,也是前端面试必问的题目,所以,掌握好原型和原型链势在必行。因此,我会用两篇文章(甚至更多)来分别讲解原型对象以及原型链。
在上一篇文章中,我们详细介绍了构造函数的执行过程以及返回值,如果没有看的同学,请点击链接 JS进阶(1) —— 人人都能懂的构造函数 阅读,因为这是本篇文章的基础知识。
废话不多说,进入正题。
一、为什么要使用原型对象
通过上一篇文章的介绍,我们知道:
function Person(name, age) {
this.name = name;
this.age = age;
}
var p1 = new Person('Tom', 18);
var p2 = new Person('Jack', 34);
console.log(p1.name, p1.age); // 'Tom', 18
console.log(p2.name, p2.age); // 'Jack', 34
但是,在一个对象中可能不仅仅存在属性,还存在方法:
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function() {
console.log('Hello');
};
}
var p1 = new Person('Tom', 18);
p1.say(); // 'Hello'
var p2 = new Person('Jack', 34);
p2.say(); // 'Hello'
我们发现,实例 p1
和 实例 p2
调用了相同的方法,都打印出 Hello
的结果。但是,它们的内存地址是一样的么?我们打印看看:
console.log(p1.say == p2.say); // false
结果当然为 false
。因为我们在上一篇文章中就说过,每一次通过构造函数的形式来调用时,都会开辟一块新的内存空间,所以实例 p1
和 p2
所指向的内存地址是不同的。但此时又会有一个尴尬的问题,p1
和 p2
调用的say
方法,功能却是相同的,如果班里有 60 个学生,我们需要调用 60 次相同方法,但却要开辟 60 块不同的内存空间,这就会造成不必要的浪费。此时,原型对象就可以帮助我们解决这个问题。
二、如何使用原型对象
当一个函数 (注意:不仅仅只有构造函数) 创建好之后,都会有一个 prototype
属性,这个属性的值是一个对象,我们把这个对象,称为原型对象。同时,只要在这个原型对象上添加属性和方法,这些属性和方法都可以被该函数的实例所访问。
既然,函数的实例可以访问到原型对象上的属性和方法,那我们不妨把上面的代码改造一下。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log('Hello');
};
var p1 = new Person('Tom', 18);
var p2 = new Person('Jack', 34);
console.log(p1.say === p2.say); // true
此时,我们看到实例 p1
和 实例 p2
的 say
指向同一块内存空间。这是什么原因呢?我们通过控制台的打印结果来看看。
通过上面的截图我们可以看到,Person.prototype
与 p1.__proto__
、p2.__proto__
似乎是一样的。为了验证我们的猜想,我们试着在打印:
Person.prototype === p1.__proto__; // true
Person.prototype === p2.__proto__; // true
p1.__proto__ === p2.__proto___; // true
我们发现,所有的结果都为 true
。 而这正好解释了为什么 p1.say === p2.say
为 true 。
三、绘制 构造函数——原型对象——实例 关系图
现在你大概理解了原型对象,也知道了使用原型对象有什么好处。下面我们通过绘制图形的方式再来深刻地理解一下上面的过程。
我们就以下面的代码为例:
function Person(name) {
this.name = name;
}
Person.prototype.say = function() {
console.log('I am saying');
}
var p1 = new Person('Tom');
1. Person 函数创建之后,会产生一块内存空间,并且有一个 prototype
属性
原型对象3.png
2. prototype
属性的值是一个对象,我们称之为原型对象
原型对象4.png
3. 原型对象中的属性和方法
参照上面控制台的截图,我们可以知道:
(1)原型对象上,有一个
constructor
属性指向 Person;
(2)原型对象上,有一个say
方法,会开辟一块新的内存空间;
(3)原型对象上,有一个__proto__
属性,这个我们下篇文章再来解释。
根据上面我们的分析,继续绘制:
原型对象5.png4. 实例中的属性和方法
当 p1
这个实例创建好之后,又会开辟一块新的内存空间。此时,依旧参照上面控制台的截图,我们可以知道:
(1)
p1
实例中有一个name
属性;
(2)p1
实例中有一个__proto__
属性,指向构造函数Person
的原型对象。
根据上面的分析,我们继续绘制:
原型对象6.png四、总结
通过上面的解释,大家应该可以理解原型对象是什么以及为什么要使用原型对象了。最后,我们来总结一下本文的核心知识点。
-
一个函数创建好之后,就会有一个
prototype
属性,这个属性的值是一个对象,我们把这个prototype
属性所指向的内存空间称为这个函数的原型对象。 -
某个函数的原型对象会有一个
constructor
属性,这个属性指向该函数本身。
function Person() {
// ...
}
console.log(Person.prototype.constructor === Person); // true
- 当某个函数当成构造函数来调用时,就会产生一个构造函数的实例。这个实例上会拥有一个
__proto__
属性,这个属性指向该实例的构造函数的原型对象(也可以称为该实例的原型对象)。
function Person() {
// ...
}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); // true
最后,本文描述的仅仅是一个构造函数——原型对象——实例的关系图,并不是完整的原型链。大家可以先理解这一部分,等到讲解原型链的时候,我会绘制一张完整的原型链图供大家理解。童鞋们可以先试着理解今天的文章,并且自己绘制一下构造函数——原型对象——实例的关系图,相信你的收获将会更大。
最后的最后,我所说的不一定都对,你一定要自己试试!
(本文完)
网友评论