前端小白红宝书之旅:原型与原型链(三)

233 阅读8分钟

前端小白红宝书之旅:原型与原型链(三)

前文

上一篇我们简单回顾了一下js的垃圾回收机制,今天我们来看看js中的原型原型链,以及所延伸出的js继承模式

在这里插入图片描述

1、对象

1.1如何定义对象

简单地说,对象就是一堆属性的集合

在JavaScript中一切引用类型都是对象

也就是说,Array类型、Function类型、Object类型、Date类型、Math类型、RegExp类型、Map类型、WeakMap类型、Set类型、WeakSet类型、正则等都是引用类型,它们都属于对象。

关于对象的具体延伸可以参考下面这篇大神文章

深入JavaScript系列(六)

1.2对象解构

ES6新增语法,使用与对象匹配的结构来实现对象属性的赋值

let person = {
           name : "好恶法",
           age : 23
       } 
       let { name : personName, age : personAge } = person
       console.log(personName);  // 好恶法
       console.log(personAge);   // 23

嵌套解构

let person = {
          name : "好恶法",
          age : 23,
          job : {
              title : "who are you?"
          }
      } 
      let personCopy = {}
      let { job : {title} } = person
      console.log(title);

1.3对象迭代

在js中对对象进行迭代并不是一件容易的事情,ES2017中新增了两个静态方法,试图解决这一棘手的问题。这两个静态方法分别是Object.values()和Object.entries(),这两个方法分别接受一个对象,返回他们数组的内容。Object返回对象值的数组,Object.entries()返回键值对的数组

const o = {
        foo : "var",
        namm : "好饿发",
        qux : {}
    }
    console.log(Object.values(o));  // ["var", "好饿发", {}]
    console.log(Object.entries(o));  // [['foo', 'var'], ['namm', '好饿发'], ['qux', {…}]]

注意Symbol属性会被迭代器忽略

1.4创建对象

经常使用到的对象是怎样被创建出来的呢?

1.4.1工厂模式

下面例子展示了一种按照特定接口创建地对象的方式

function createPerson(name, age, job) {
          let o = new Object()
          o.name = name
          o.age = age
          o.job = job
          o.sayName = function() {
              console.log(this.name);
          }
          return o
        }
         
        let person1 = createPerson('好饿发', 24, "sdsds")
        let person2 = createPerson('玄德', 23, 'sctv')

        console.log(person1);
        console.log(person2);

以上只做了简单概述,详细可以参照这篇两好文章 Javascript:工厂模式

JavaScript设计模式与实践--工厂模式

1.4.2构造函数模式

什么是构造函数? 构造函数本身就是一个普通的函数,为了规范才令构造函数首字母大写,只要跟在new操作符之后,用于创建对象的函数,就可以被认定为是构造函数

function Person(name, age, job) {
            this.age = age
            this.name = name
            this.job = job
            this.sayName = function() {
                console.log(this.name);
            };
         }
         let person1 = new Person("好恶发", 23, "sd")
         let person2 = new Person('玄德', 23, 'sctv')
         person1.sayName()  // 好恶发
         person2.sayName()  // 玄德

上诉构造函数模式创建对象与工厂模式不太一样的地方在于: 1.没有显式地创建对象 2.属性和方法直接赋值给了this 3.没有return

2、原型与原型链

2.1概念

什么是原型?

上一节我们提到了如何创建对象,每次创建对象的过程中,被创建的对象总会与一个原型对象相关联。js中的继承都是基于原型的基础上所实现的

什么是原型链?

当我们访问一个实例化对象的内部元素时,会先从该对象本身进行查找,如果查不到,那么会查找这个实例化对象的原型对象内部是否有这个属性,如果也没有查找到该属性,那么就会查找原型对象的原型(原型属于对象,自然也会有一个关联的原型),直至查找到默认实例Object的原型都无法查到到这个属性的话,就停止查找 这条由对象及其原型组成的链就叫做原型链

2.2__proto__、prototype、constructor以及原型动态性

膜拜一篇大神文章: 用自己的方式(图)理解constructor、prototype、__proto__和原型链 代码示例

function SuperType() {
            this.flag = true
        }  

        SuperType.prototype.getSuperValue = function() {
            return this.flag
        } 

        function SubType() {
            this.flag = false
        }

        // 继承SuperType
        SubType.prototype = new SuperType()
        
        SubType.prototype.getSubValue = function() {
            return this.flag
        }
        let instance = new SubType()
         console.info(instance.getSubValue());  
        // true
        console.log(instance.flag);
        // false
       

什么是constructor?

constructor属性总是指向构造出这个实例化对象的函数。Function函数作为js内置的对象,constructor就指向它本身,constructor属性本质就是一个用于保存自己构造函数引用的属性

请看实例:

let instance1 = new SubType()
let instance2 = new SubType()

图片: 在这里插入图片描述 1、由构造函数创建的实例化对象instance1、instance2它们的属性自然就指向他们的构造函数Person() 2、构造函数Person()本身也是Function的实例对象,因此指向Function函数 3、Function函数,他本身是js内置对象,它的构造函数就是它本身,因此指向它本身

似乎这样也可以实现继承,那为什么要引入prototype属性呢?

比如说我们要给两个实例化对象instance1、instance2都添加一个公共的方法,如果使用constructor属性的话,就会开辟两个独立的内存空间,用于存储这个方法:

// 下面是给instance1和instance2实例添加了同一个效果的方法logSum
instance1.logSum = function(n) {
    return n++
}
instance2.logSum = function(n) {
    return n++
}
console.info(instance1.sayHello === instance2.sayHello) // false,它们不是同一个方法,各自占有内存

也就是如果需要给成千上百次实例提供一个公共类的方法的话,也需要重新开辟成千上百此内存空间,这显然会极大影响程序性能,因此我们引入prototype属性就是为了解决这一问题

什么是prototype?

前面我们提到过每个函数都是一个对象,函数对象都会拥有一个子对象prototype对象,类是以函数的形式来定义的,prototype表示该函数的原型,也表示一个类的成员的集合 prototype属性的使用,使得了被同一构造函数所创建的实例化对象都可以继承到公共的方法或者变量,而不需要开辟独立的存储空间

请看以下代码:

function SubType() { 
            this.flag = false
        }
SubType.prototype.getSubValue = function() {
            return this.flag
        }
console.info(instance1.getSubValue === instance2.getSubValue);    // true   它们是同一个方法,占用的内存为同一个

由代码可得出结论,当需要为大量实例添加相同效果的方法时,可以将它们存放在prototype对象中,并将该prototype对象放在这些实例的构造函数上,达到共享、公用的效果。

图例: 在这里插入图片描述 prototype对象用于存放某一类型的方法或者变量,这样可以在不开辟多个内存空间的情况下,为所有继承至该构造函数的实例化对象,共享参数与方法

其中以上的图例并不完整,因为前面我们提到过所有函数对象都会有一个prototype对象,用于存储公共变量与方法,此处略去

constructor属性究竟存储在哪里?

如果各位在控制台输出实例化对象instance会发现,constructor属性不会直接放置在instance中 instance的constructor属性存储在自身的原型当中(此处显示为SuperType是因为instance的构造函数SubType自身原型被重写)。也就是说,constructor属性被抽取成为了一个公共的属性,存放在构造函数的原型对象里,凡是有这个构造函数(构造器)所创键的实例化对象,都会自动继承这个constructor属性,通过这个属性指向自己的构造函数

 SuperType.prototype.getSuperValue = function() {
            return this.flag
        } 

        function SubType() {
            this.flag = false
        }
        SubType.prototype.getSubValue = function() {
            return this.flag
        }

        let instance = new SubType()

在这里插入图片描述 为什么不将constructor属性存储于对象instance本身? 同理,也是为了避免不必要的内存使用,将constructo属性抽取成为一个公共的属性,这样就不用在每个实例化对象当中开辟新内存,存储constructo属性

那么问题又来了,为什么我们在某个实例化对象上修改constructor属性的指向之后,并不是所有的实例对象的constructor属性指向都进行了改变?

        function SubType() {
            this.flag = false
        }
        SubType.prototype.getSubValue = function() {
            return this.flag
        }
        let instance = new SubType()
        let instance1 = new SubType()
        let instance2 = new SubType()
         instance1.constructor = Function
        console.log(instance1);

在这里插入图片描述 由上图可见,在实例化对象上修改constructor属性本质是在该实例化对象上,增添了一个constructor属性,而并非对构造函数的原型对象当中的constructor属性做出修改

但是问题再一次出现,我们已经知道constructor属性存储在实例化对象的原型 当中,它负责指向构造函数,但是实例化对象和原型之间的指向,由哪个属性来提供呢?

什么是__proto__?

__proto__属性存储在实例化对象中,指向它本身的原型对象,这样我们就可以找到constructor属性了

也就是说存在以下引用关系

实例化对象instance.proto = instance的原型对象

instance.proto .constructor = 构造函数 SubType()

3、小结

1.一切引用类型数据皆可看作对象

2.每个对象在创建之初就会带有一个原型对象

3.当访问对象中的一个属性时,首先,查找实例化对象本身,没有则通过__proto__ 属性指向自己的原型对象,依然找不到,则原型对象通过constructor属性指向构造函数,继续查找,直至查找到Object的原型对象