阅读 2324

JS进阶(2):人人都能懂的原型对象

封面.jpeg

凡是搞前端开发的或者玩 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 。因为我们在上一篇文章中就说过,每一次通过构造函数的形式来调用时,都会开辟一块新的内存空间,所以实例 p1p2 所指向的内存地址是不同的。但此时又会有一个尴尬的问题,p1p2 调用的say 方法,功能却是相同的,如果班里有 60 个学生,我们需要调用 60 次相同方法,但却要开辟 60 块不同的内存空间,这就会造成不必要的浪费。此时,原型对象就可以帮助我们解决这个问题。

二、如何使用原型对象

当一个函数 (注意:不仅仅只有构造函数) 创建好之后,都会有一个 prototype 属性,这个属性的值是一个对象,我们把这个对象,称为原型对象。同时,只要在这个原型对象上添加属性和方法,这些属性和方法都可以被该函数的实例所访问。

原型对象1.png

既然,函数的实例可以访问到原型对象上的属性和方法,那我们不妨把上面的代码改造一下。

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 和 实例 p2say 指向同一块内存空间。这是什么原因呢?我们通过控制台的打印结果来看看。

原型对象2.png

通过上面的截图我们可以看到,Person.prototypep1.__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.png

4. 实例中的属性和方法

p1 这个实例创建好之后,又会开辟一块新的内存空间。此时,依旧参照上面控制台的截图,我们可以知道:

(1)p1 实例中有一个 name 属性; (2)p1 实例中有一个 __proto__ 属性,指向构造函数 Person 的原型对象。

根据上面的分析,我们继续绘制:

原型对象6.png

四、总结

通过上面的解释,大家应该可以理解原型对象是什么以及为什么要使用原型对象了。最后,我们来总结一下本文的核心知识点。

  1. 一个函数创建好之后,就会有一个 prototype 属性,这个属性的值是一个对象,我们把这个 prototype 属性所指向的内存空间称为这个函数的原型对象。

  2. 某个函数的原型对象会有一个 constructor 属性,这个属性指向该函数本身。

function Person() {
    // ...
}
console.log(Person.prototype.constructor === Person); // true
复制代码
  1. 当某个函数当成构造函数来调用时,就会产生一个构造函数的实例。这个实例上会拥有一个 __proto__ 属性,这个属性指向该实例的构造函数的原型对象(也可以称为该实例的原型对象)。
function Person() {
    // ...
}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); // true
复制代码

最后,本文描述的仅仅是一个构造函数——原型对象——实例的关系图,并不是完整的原型链。大家可以先理解这一部分,等到讲解原型链的时候,我会绘制一张完整的原型链图供大家理解。童鞋们可以先试着理解今天的文章,并且自己绘制一下构造函数——原型对象——实例的关系图,相信你的收获将会更大。

最后的最后,我所说的不一定都对,你一定要自己试试!

(本文完)