美文网首页JavaScript 进阶营让前端飞程序员
JS进阶(2) —— 人人都能懂的原型对象

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

作者: 零和幺 | 来源:发表于2018-03-18 11:10 被阅读73次
    封面.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
    

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

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

    (本文完)

    相关文章

      网友评论

        本文标题:JS进阶(2) —— 人人都能懂的原型对象

        本文链接:https://www.haomeiwen.com/subject/vjheqftx.html