构造函数 原型对象 实例化对象之间的关系

1,104 阅读9分钟

对象

面向对象的本质是对面向过程的一种封装, 注重结果.

构造函数: new关键字调用的函数,用于创建对象.

(面试题)  new在创建这个对象的过程中有哪些作用?

a. 创建空对象
b. this指向这个对象
c. 对象赋值
d.返回这个对象(返回的其实是这个对象的地址,打印地址也就相当于打印了这个对象. )

        function Person(name, age, gender) {
            // (1) 创建空对象{}
            // (2) this指向这个对象 this = {}
            // (3) 赋值
            this.name = name
            this.age = age
            this.gender = gender
            // (4) 返回这个对象 return this
            // return 666 // 无效
            // return [10 ,20,30] // 有效
        }
        let p1 = new Person('小米粒', 20, '女')
        console.log(p1);

注意: 在构造函数内部使用return,
如果 return 的是值类型, 则无效, 返回 new 创建的对象.
如果 return 的是引用类型(函数 对象 数组), 则有效, 覆盖 new 创建的对象.

如果在构造函数内部放一个方法,会导致内存浪费, 为什么会出现内存浪费的情况呢?

    function Person(name, age) {
      this.name = name
      this.age = age
      this.tryHard = function () {
        console.log('努力学习');
      }
    }
    let p1 = new Person('小王', 9)
    let p2 = new Person('小李', 9)
    console.log(p1.tryHard === p2.tryHard); //false

执行过程图:

image.png

之所以会造成内存浪费:  是因为你每创建一个对象,就会在堆空间里又开辟个房间存tryHard里的代码, 严重造成了内存浪费,

console.log(p1.tryHard === p2.tryHard)之所以会错误是因为p1.tryHardp2.tryHard比较的是地址,不是值,一个地址是ox1111, 一个地址是0x2222, 所以打印的是false,
记住:  引用类型只比较地址,不比较值。

使用全局函数 解决内存浪费

    const tryHard = function () {
      console.log('努力学习');
    } // 这是个全局函数 无论Person调多少次, 这里只执行一次

    function Person(name, age) {
      this.name = name
      this.age = age
      this.tryHard = tryHard // 拷贝 引用类型拷贝的是地址 所以多次创建对象后,每个对象里的tryHard的地址就是那个全局函数tryHard的地址,每个对象找的都是这个全局函数tryHard.减少了资源浪费.
    }
    let p1 = new Person('小王', 9)
    let p2 = new Person('小李', 9)
    console.log(p1.tryHard === p2.tryHard); // 相同的地址,指向一个函数 

执行过程图:

image.png 使用全局函数,之所以能解决内存浪费的原因是:   全局函数只执行一次, this.tryHard = tryHard ,这里是拷贝,拷贝全局函数(引用类型)的地址, 即后面就算你无论调多少次构造函数进行创建对象, 永远访问的是全局函数tryHard的地址.

这么做的弊端:   造成变量污染(因为一个对象不止一个方法,比如还有学习方法等等,这时候又要创建一个学习的全局函数, 函数变多, 导致变量名增加, 会有重名风险(你的同事假如也起的名字和你一样), 导致变量污染) ,
在工作中过程中,如果你采用这种方式const tryHard = function () {} , 你的同事写的代码里也有这个函数,那么在代码合并的时候,会报错,需要重新费时间去改了,这种错误还好可以看得到;但是你在定义全局函数的时候如果这么写function tryHard() { console.log('你努力学习'); }, 你的同事呢也定义了这个函数名, function tryHard() { console.log('我也努力学习'); },那么在代码合并的时候, 先出现的tryHard()会被后面出现的所覆盖, 没有错误,你压根就不知道代码那里错了, 上面这都是变量污染.

使用对象解决 变量污染 + 内存浪费   使用对象将多个全局函数包起来,

    const obj = {
      tryHard: function () {
        console.log('努力学习');
      },
      sing: function () {
        console.log('唱歌')
      }
    }

    function Person(name, age) {
      this.name = name
      this.age = age
      this.tryHard = obj.tryHard
      this.sing = obj.sing
    }
    let p1 = new Person('小王', 9)
    let p2 = new Person('小李', 9)
    console.log(p1.sing == p2.sing) //true

弊端:   虽然使用对象解决来了变量污染,但是 obj 自身却成为了唯一的变量污染 后来js作者出手了,发明了原型机制.也就是接下来要介绍的原型对象.

原型对象

任何函数在被创建的时候,系统都会自动帮我们创建一个与之对应的对象,称之为原型对象 原型对象成为了构造函数的一个属性来实现, 即原型prototype, 它以对象的形式存在, 所以又可以称prototype为原型对象. 通过Person.prototype 能找到原型对象.

    // 1.构造函数
    function Person(name, age) {
      this.name = name
      this.age = age
    }

    // 2.原型对象
    Person.prototype.eat = function () {
      console.log('吃东西'); // 对象赋值 只要对象里没有 相当于增加个属性

    }
    // 吃方法和学习方法都会加到prototype 原型对象里, 只有一个原型对象.
    Person.prototype.learn = function () {
      console.log('学习');
    }

    // 3.实例对象
    let p1 = new Person('平安', 18)
    let p2 = new Person('宁姚', 17)
    console.log(p1);// 
    console.log(p1.learn());// 学习

执行过程图:

image.png

看到这里,有人就该发出疑问了, 构造函数里放一个属性,而这个属性又指向原型对象,那岂不是每次调用构造函数就会创建一个原型对象, 这不还是没解决根本问题吗?
其实可以这么理解: 构造函数也是引用类型, 在构造函数的内存当中, 也可以像对象一样拥有很多小房间, 其中某个小房间专门存代码, 我们每次调用构造函数, 只有这个存代码的小房间会调一次走一次, 你就算调无数次, 其他小房间里的也是只走一次,也是只有一个原型对象 ,用console.log(Person)只能看到走代码的小房间, 用console.dir(Person) 能看到好多小房间.

原型对象解决内存消耗和变量污染的的实质是:是因为这个对象是构造函数的属性,且这个属性只执行一次
仔细看一下下面两行代码的结果图

console.log(p1)

image.png

console.log(p1.learn())

image.png

对比这两个打印的结果, 发现实例化对象P1里面根本就没有learn这个属性,它为什么能打印出learn里的内容呢? 这是因为实例化对象里有个Prototype这个属性,但是你用这个属性拿不到原型对象, 要通过__proto__才能拿到这个原型对象, 即 p1.__proto__, 所以p1.learn()全部写法是 p1.__proto__.learn() , 用的是原型对象里的learn属性, 但为什么谷歌不直接在实例对象里显示__proto__呢? 是因为__proto__是有争议的, 它从来没有被包括在EcmaScript语言规范中(不是Ecma的标准属性), 但是现代浏览器都实现了它, 所以在开发中强制省略__proto__呀.\

构造函数 原型对象 实例对象三者间的关系:

prototype: 属于构造函数, 指向原型对象 作用:   解决构造函数内存浪费 + 变量污染

__proto__: 属于实例对象, 指向原型对象 作用:   让实例对象直接使用原型对象的成员(函数+属性)

constructor: 属于原型对象, 指向构造函数 作用:   让实例对象知道自己的构造函数是谁

图例关系:

image.png console.log(p1.__proto__.constructor); // 让实例对象知道自己的构造函数谁, 即Person console.log(p1.__proto__ === Person.prototype); //true 表示构造函数和实例对象指向的是一个原型对象 js作者提前写好的对象, 里面有一些预先定义的方法,我们可直接使用,这类对象叫内置对象. 有Array对象、Date对象、String对象等。

数组对象

操作数组的方法,数组本身没有,用的其实是原型对象里的方法.

先声明个数组, 再来具体介绍数组对象的一些方法.
let arr = [10, 20, 30]

1.连接数组: arr.concat(数组): 连接两个或多个数组, 有返回值, 返回一个新的数组.
arr = arr.concat([40, 50, 60])//arr = [10, 20, 30, 40, 50, 60]
应用场景: 浏览某东网页时,鼠标下拉加载更多资源如图片的时候,会把新数组连接到原来数组的后面.

2.数组拼接字符串: arr.join('分隔符'): 把数组的每个元素拼接成字符串, 有返回值, 返回一个字符串.
['许嵩', '嵩嵩'].join('&')//许嵩&嵩嵩
应用场景: 歌单里有一些歌曲是合唱的,显示多人名字就用到了这个方法.

3.反转数组: arr.reverse() // 这个没有返回值.

4.数组排序: arr.sort()

arr.sort(function (a, b) {
            // [10, 20, 30, 40, 50, 60]
            return a - b //从小到大
            // [60, 50, 40, 30, 20, 10]
            //return b-a // 从大到小
        })
        console.log(arr);

字符串对象

操作字符串的方法,字符串本身没有,用的也是原型对象里的方法.

先声明个字符串,再来具体介绍字符串对象里的一些方法.

let str = '青春正美加油努力'

字符串类似于数组, 也有长度 + 下标

1.返回字符下标: str.inddexOf('字符串')

console.log(str.indexOf('努力')); // 结果为6,注意语法检测的时候是努力二字一起检测,如果你写美力,会检测不到,从而返回-1
有返回值 返回字符串在str中的首字符下标如果存在,则返回首字符下标 如果不存在 则返回固定值-1
应用: 检测字符串在不在str中.

2.字符串分割为字符串数组: str.split('分隔符')
把 str 按照分隔符切割成一个数组 有返回值
应用: 切割 url 网址

 let url = 'http://www.baidu.com?username=青春正美&加油努力=123456'

        let arr1 = url.split('?')//['http://www.baidu.com', 'username=青春正美&加油努力=123456']

3.截取部分字符串:str.substr(起始位置,截取长度) :
应用场景 : 一般后台返回的数据 不会和前端完全匹配。 有时候需要自己截取一部分

console.log(str.substr(0, 2)); // 从0下标开始, 截取2个字符 青春

4.大小写转换(中文没有大小写:)

 console.log('wasd'.toLocaleUpperCase()); //大写WASD
 console.log('WASD'.toLocaleLowerCase()); // 小写wasd

了解知识
静态成员: 函数自己的成员
实例成员: 实例对象的成员

后面常用的静态成员:
Object.values(对象名)(Object表示内置的构造函数)
作用: 获取对象所有的属性值
Object.keys(对象名)
作用: 获取对象所有的属性名

        /* 需求:获取对象所有的属性值 */
        let person = {
            name: '陈平安',
            age: 19,
            gender: '男'
        }

        //以前 : for-in
        //对象.属性名   对象[ 变量 ]
        //for (let key in person) {
            //console.log(person[key])
        // }

        /* 常用的静态成员 : Object.values(对象名) Object.keys(对象名) */
        //返回数组
        console.log(Object.values(person)) //['陈平安', 19, '男']
        console.log(Object.keys(person)) //['name', 'age', 'gender']