原型,原型链

80 阅读7分钟

原型,原型链的相关理解

在理解原型与原型链的相关概念之前,我们先来了解一些概念:

  1. JS分为函数对象普通对象,每个对象都有__protp__属性,但只有函数对象才有prototype属性。
  2. Object,Function是JS的内置函数,类似的还有Array,RegExp,Date,Boolean,Number,String
  3. 属性__proto__是一个对象,它有两个属性,constructor和[[Prototype]]。
  4. 原型对象prototypr有一个默认的constructor属性,用于记录实例是由哪个构造函数创建的。

构造函数

讲原型之前,离不开构造函数,在讲原型之前,我们先来了解一下构造函数。

构造函数的组成

构造函数里面有实例成员静态成员
实例成员:在构造函数内部通过this添加的成员。实例成员只能通过实例化的对象来访问。
静态成员:在构造函数本身添加的成员,只能通过构造函数来访问。

    function Person(name,age) {
        // 实例成员
        this.name = name
        this.age = age
    }

    // 静态成员
    Person.sex = '男'

    let person1 = new Person('zz',22)
    console.log(person1);
    console.log(person1.sex);//实例对象无法访问静态成员

    console.log(Person.name);//构造函数无法直接访问实例成员
    console.log(Person.sex);//构造函数可以直接访问静态成员

image.png

构造函数创建对象

在JAVA中,对象一般都是通过new关键字来创建的,在JS中同样也可以通过new来创建一个对象。

    function Animal(name,sex) {
        this.name = name
        this.sex = sex
    }

    let dog = new Animal('h',7)
    console.log(dog);

image.png

此时就通过构造函数Animal创建了一个对象dog

通过new关键字创建一个对象时发生了什么

  1. 创建一个新对象dog{}
  2. 为dog准备原型链连接dog.__ proto__ = Animal.prototype
  3. 重新绑定this,使构造函数的this指向为新的对象Animal.call(dog)
  4. 为新对象重新赋值dog.name = hdog.sex = 7
  5. 返回this return this,此时的新对象就有了构造函数的属性和方法。

实例上的方法

  1. 在构造函数上定义的方法
    function Animal() {
        this.eat = function () {
            console.log('会吃饭');
        }
    }

    let dog = new Animal()
    let cat = new Animal()
    dog.eat()
    cat.eat()
    console.log(dog.eat == cat.eat);

image.png

在构造函数上定义的方法,所有通过构造函数实例化的对象都能调用,但是他们调用的都不是同一个方法。学过JAVA的都知道,通过new关键字创建一个新的对象时,会在内存空间空间中的堆空间中开辟一个新的空间来存储。每new一个对象都会在堆空间中开辟一个新空间。所以每个对象都是不一样的。在JS中也差不多是这样的,所以每个实例对象都可以调到构造函数上定义的方法,但这个方法在每个实例对象上是不一样的,

  1. 通过原型添加方法
    function Animal(name) {
        this.name = name
    }

    Animal.prototype.eat = function () {
        console.log('会吃饭',this.name);
    }

    let dog = new Animal('zz')
    let cat = new Animal('hh')
    dog.eat()
    cat.eat()
    console.log(dog.eat == cat.eat);

image.png

当将构造函数的方法放到它的原型上时,两个实例化对象也都可以使用,并且两个对象使用的方法是同一个方法。

原型

原型的理解

在学习JS之前,我学习过JAVA,我觉得JS的原型类似于JAVA的父类。
在JS中,创建一个函数(非箭头函数)时,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有的原型对象都会获得一个名为constructor的属性,指回与之相关的构造函数

在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object。每次调用构造函数创建一个实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。脚本中没有访问[[Prototype]]特性的标准方法,但Firefox、Safari和Chrome会在每个对象上暴露__proto__属性来访问对象的原型。

    function Person() {}
    Person.prototype.name = 'zzz'
    Person.age = 22
    Person.prototype.sayName = function () {
        console.log(this.name);
    }

    let person1 = new Person()
    let person2 = new Person()

    // 声明之后构造函数就有与之相关的原型对象
    console.log(Object.prototype.toString.call(Person.prototype));
    console.log(Person.prototype);

    // 构造函数有一个prototype属性指向其原型对象,其原型对象有一个constructor的属性指回Person
    console.log(Person.prototype.constructor == Person);

    // 构造函数、原型对象是实例是三个完全不同的对象
    console.log(person1 !== Person);
    console.log(person1 !== Person.prototype);
    console.log(Person.prototype !== Person);

    // 实例通过__proto__链接到原型对象,它实际上是指向隐藏特性[[Prototype]]
    // 构造函数通过prototype属性链接原型对象
    // 实例与构造函数之间没有什么直接联系,但与原型对象有联系
    console.log(person1.__proto__ == Person.prototype);
    console.log(Person.prototype.constructor == Person);

    // 同一个构造函数创建两个实例对象,两个实例对象共享一个原型
    console.log(person1.__proto__ == person2.__proto__);

    // Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值,用于获取原型对象,兼容性更好
    console.log(Object.getPrototypeOf(person1) == Person.prototype);

image.png

  1. 构造函数Person中存在一个prototype属性指向其原型对象,而原型对象中有个constructor属性来反指回构造函数
  2. 实例对象上有一个__proto__属性来指向原型对象。
  3. 实例对象和原型对象没有什么直接的联系,实例对象可以调取原型对象上的实例方法,实例属性,但每个实例对象都是不一样的。
  4. 通过同一个构造函数创建出来的实例对象直接是没有直接的联系的,他们在内存空间中都有自己独立的空间,但他们有同一个原型,可以通过在他们原型上添加方法,使他们有共享的方法。
  5. 实例对象和构造函数也是通过原型链接产生联系的。

更直面的关系如下:

image.png

原型的层级

在通过对象访问属性时,会通过这个属性的名称开始搜索,搜索的过程如下:

  1. 如果在这个实例上发现了给定的名称,则返回该名称对应的值。
  2. 如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回其对应的值。

例如我们在调用*person1.sayName()*时,会发生两步搜索:

JS引擎先问实例对象person1有sayName方法吗?如果没有,会接着问person1的原型对象,也就是Person.prototype有sayName方法吗?有则返回其值。

原型链

在原型中,构造函数与实例对象是通过原型对象直接产生了联系,构造函数有一个prototype指向原型对象,而原型对象有一个constructor属性返回构造函数,实例对象有一个__proto __ 指向原型对象,这是最基本的原型的理解。

那如果原型是另一个构造函数的实例呢?那就意味着这个原型本身就有一个内部指针指向另一个原型,相应的另一个原型也有一个内部指针指向其他的原型,如此重复下去就形成了一条原型链

    function SuperType() {
        this.property = true
    }
    SuperType.prototype.getSuperValue = function () {
        return this.property
    }
    function SubType() {
        this.subproperty = false
    }
    SubType.prototype = new SuperType()
    SubType.prototype.getSubValue = function () {
        return this.subproperty
    }

    let instance = new SubType()
    console.log(instance.getSuperValue());

image.png

SubTypeSuperType两个构造函数分别定义了一个属性,同时他们的原型对象定义了一个方法,用来返回两个构造函数所定义的值,SubType通过创建SuperType的实例对象并将其值赋值给SubType的原型对象实现了对SuperType的继承,这个赋值重写了SubType最初的原型对象,并将其替换成SuperType的实例对象

此时,SubType实例可以访问的属性和方法同样存在于SubType.prototype,同时SubType.prototype继承了SubType的实例,也就是说,在SubType的实例化对象中,我们可以访问到SuperType.prototype的所有属性和方法。此时就形成了一条简单的原型链。

image.png