美文网首页前端开发那些事让前端飞Web前端之路
原型与原型链最强解析-(看完秒懂)

原型与原型链最强解析-(看完秒懂)

作者: 废柴码农 | 来源:发表于2019-08-12 12:04 被阅读18次

    前言

    与大部分面向对象语言不同,ES6之前中并没有引入类(class)的概念,JavaScript并非通过类而是直接通过构造函数来创建实例。在介绍原型和原型链之前,我们有必要先复习一下构造函数的知识。

    构造函数

    构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例。构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的,即实例识别。
    构造函数就是一个普通的函数,但是仍然有几点差别:

    区别:

    1.构造函数习惯上首字母大写
    2.调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用。
    3.执行流程不同:
    构造函数的执行流程:

    A、立刻在堆内存中创建一个新的对象
    B、然后传递给构造函数的this
    C、逐个执行函数中的代码
    D、将新建的对象作为返回值

    4.普通函数因为没有返回值,所以打印常为undefined 构造函数会马上创建一个新对象,并将该新对象作为返回值返回

    //普通函数
    function person(){} 
    var per = person();
    console.log(per) //undefined
    //构造函数
    function Person(){} 
    var per =new Person();
    console.log(per) //Person {}
    

    5.构造函数内部用this 来构造属性和方法
    栗子🌰

    function Person(name, color) {
      this.name = name;
      this.color = color;
      this.sayHello = function () {
        console.log('hello');
      };
    }
    
    var person1 = new Person('jack', '白色');
    var person2 = new Person('rose', '黑色');
    
    person1.sayHello === person2.sayHello
    // false
    

    上面代码中,person1和person2是同一个构造函数的两个实例,它们都具有sayHello方法。由于sayHello方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个sayHello方法。这既没有必要,又浪费系统资源,因为所有sayHello方法都是同样的行为,完全应该共享。

    这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。

    prototype 属性的作用

    JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

    下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个prototype属性,指向一个对象。

    function Person() {
    
    }
    // 虽然写在注释里,但是你要注意:
    // prototype是函数才会有的属性
    Person.prototype.name = 'Kevin';
    var person1 = new Person();
    var person2 = new Person();
    console.log(person1.name) // Kevin
    console.log(person2.name) // Kevin
    

    那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?

    其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。

    那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。

    在举个栗子:

    function Person(name) {
      this.name = name;
    }
    Person.prototype.color = 'white';
    
    var person1 = new Person('大毛');
    var person2 = new Person('二毛');
    
    person1.color // 'white'
    person2.color // 'white'
    

    上面代码中,构造函数Personprototype属性,就是实例对象person1person2原型对象。原型对象上添加一个color属性,结果,实例对象都共享了该属性。
    原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。

    Person.prototype.color = 'yellow';
    person1.color // "yellow"
    person2.color // "yellow"
    

    上面代码中,原型对象的color属性的值变为yellow,两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

    如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
    function Person(name) {
      this.name = name;
    }
    var person1 = new Person('大毛');
    var person2 = new Person('二毛');
    person1.color = 'black';
    Person.prototype.color = 'yellow';
    
    person1.color // 'black'
    person2.color // 'yellow'
    
    

    上面代码中,实例对象person1color属性改为black,就使得它不再去原型对象读取color属性,后者的值依然为yellow

    总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。

    让我们用一张图表示构造函数和实例原型之间的关系:


    屏幕快照 2019-08-12 下午2.33.33.png
    对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

    在这张图中我们用 Object.prototype 表示实例原型。

    那么我们该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性:

    _proto_

    这是每一个JavaScript对象(除了 null )都具有的一个属性,叫_proto_,这个属性会指向该对象的原型。

    function Person() {
    
    }
    var person = new Person();
    console.log(person.__proto__ === Person.prototype); // true
    

    于是我们更新下关系图:


    屏幕快照 2019-08-12 下午2.53.20.png

    既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

    constructor

    指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。

    为了验证这一点,我们可以尝试:

    function Person() {
    
    }
    console.log(Person === Person.prototype.constructor); // true
    

    所以再更新下关系图:


    屏幕快照 2019-08-12 下午2.59.00.png
    function Person() {
    
    }
    
    var person = new Person();
    
    console.log(person.__proto__ == Person.prototype) // true
    console.log(Person.prototype.constructor == Person) // true
    // 顺便学习一个ES5的方法,可以获得对象的原型
    console.log(Object.getPrototypeOf(person) === Person.prototype) // true
    

    现在我们就了解了构造函数、实例原型、和实例之间的关系了

    原型链

    JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……

    如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOftoString方法的原因,因为这是从Object.prototype继承的。

    那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。

    Object.getPrototypeOf(Object.prototype)   //null
    

    上面代码表示,Object.prototype对象的原型是null,由于null没有任何属性,所以原型链到此为止。Object.getPrototypeOf方法返回参数对象的原型。


    屏幕快照 2019-08-12 下午3.11.17.png

    读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”。

    注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

    举例来说,如果让构造函数的prototype属性指向一个数组,就意味着实例对象可以调用数组方法。

    var MyArray = function () {};
    
    MyArray.prototype = new Array();
    
    var mine = new MyArray();
    mine.push(1, 2, 3);
    mine instanceof Array // true
    

    参考文献:http://javascript.ruanyifeng.com/oop/prototype.html#toc3
    https://github.com/mqyqingfeng/Blog/issues/2

    相关文章

      网友评论

        本文标题:原型与原型链最强解析-(看完秒懂)

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