深度透彻掌握原型
1.为什么要用原型,好处?
原型上所有的方法和属性都可以被构造函数(实际开发原型主要共享方法和所有实例公用引用属性)的实例共享,那为什么要共享呢?
案例
function QQUsers (QQNo_, QQAge_, QQMark_) {
this.QQNo = QQNo_;//QQ号
this.QQAge = QQAge_;//Q龄
this.QQMark = QQMark_;//QQ标签
//引用对象类型=引用类型=对象类型=引用数据类型 (有箭头指向的都是引用类型)
// 数组也是一种引用数据类型
this.commonfriends = ['骑驴看海', '大漠上的英雄', '坚实的果子', '小草']//共同好友
// 方法(函数)也是一种引用数据类型 (因为此函数放在了特定环境下,可以通过对象调用出来,所以叫方法)
this.show = function () {
console.log(`QQ号:${this.QQNo},QQ龄:${this.QQAge},QQ标注:${this.QQMark}`)
console.log(`共同的好友是:${this.commonfriends}`);
}
}
// 对象也叫实例(instance)
// QQZhangSan叫做对象变量 对象是等号右边通过new出来的一个实例 而且是运行期间才在堆中开辟对象的内存空间
let QQZhangSan = new QQUsers("37834522", 15, "王阳明传人")
let QQLisi = new QQUsers("30424232", 10, "袁隆平的徒弟")
//let QQLiuwu = new QQUsers("刘武", 12, "飞起来的鸭子")
QQZhangSan.show();
QQLisi.show();
//QQLiuwu.show();
new QQUsers(...)的QQUsers是构造函数,QQZhangSan、QQLisi都是他的实例
2.没有用原型会有什么问题?
总结问题: 所有 QQUser 对象(也叫 QQUser 实例)都有相同的好友属性,好友属性用 commonfriends 英文表示,所有 QQUser 对象都有相同的 show 方法。但我们发现每一个 QQUser对象 (也叫 QQUser 实例)都单独分配一个 commonfriends 属性空间和 show 方法空间,浪费了大量内存空间
答案:使用原型解决所有实例上的方法,还有所有实例上的共同属性都可以放到原型上去定义
原型图
3.认识函数、原型定义
(1)函数也是一个对象,当真正开始执行函数,执行环境(开发时为浏览器或控制台)会为函数分配一个函数对象变量空间和函数对象空间,函数对象变量用函数名表示,存在栈空间中, 函数对象空间是在堆中开辟的一个内存空间,这个空间中有一个默认的 prototype 属性,这个 prototype 属性就是一个原型对象属性(也叫对象变量)
(2)函数和构造函数的区别
当通过 new 函数()时,此刻这个函数就是构造函数(日后会演变成TS 类的构造器)
(3)定义:原型( prototype )是定义函数由 JS 自动分配给函数的一个可以被所有构造函数实例对象变量共享的对象变量(也叫对象属性)
4. 如何访问原型对象空间上的属性和方法
(1)构造函数所有实例对象都可以访问型对象空间上的属性和方法(每一个实例都有默认的__proto__属性,这个__proto__属性指向原型对象空间)
(2)关于__proto__:new 在创建新对象的时候,会赋予新对象一个属性指向构造函数的 prototype 对象空间,这个属性就是__proto__
(3)可以直接通过构造函数.prototype 对象属性来访问原型对象空间上的属性和方法
5. 构造函数实例(也叫对象)如何访问原型对象空间上的属性和方法
(1)构造函数实例访问一个属性和方法,首先从实例空间中查找(当执行环境执行 new 构造函数()时,构造函数中通过 this 定义的属性和方法会分配在这个空间中),如果找到该属性和方法,就停止查找,表示找到了;如果没有找到,就继续在该实例的原型对象空间中去查找该属性和方法(实例中默认的__proto__对象属性指向原型对象空间)
(2)实例正是借助自身的__proto__ 对象属性 来查找原型对象空间中的属性和方法,有点像儿子去和爸爸要他没有的东西一样。
(3)增加或修改原型对象的属性或方法后, 所有的实例或叫对象立即可以访问的到 (但创建实例后再覆盖原型除外)
高频面试题:创建实例后再覆盖原型,实例对象无法访问到,为什么?
覆盖对象:
let obj = { username: "wangwu", age: 23 }
let objnew = obj;
obj = { address: "北京海淀区西三环", age: 39 }
console.log("obj:", obj);
console.log("obj2:", objnew)
// 结果:
// obj: { address: '北京海淀区西三环', age: 39 }
// objnew: { username: 'wangwu', age: 23 }
obj指向了新的内存空间,原来的内存空间还在被objnew使用,所以未被垃圾回收机制回收,objnew依然能访问到它
覆盖原型:
function QQUsers (QQNo_, QQAge_, QQMark_) {
this.QQNo = QQNo_;//QQ号
this.QQAge = QQAge_;//Q龄
this.QQMark = QQMark_;//QQ标签
}
//方法栈--执行方法时的栈区
QQUsers.prototype.commonfriends = ['骑驴看海', '大漠上的英雄', '坚实的果子', '小草']
QQUsers.prototype.show = function () {
console.log(`QQ号:${this.QQNo},QQ龄:${this.QQAge},QQ标注:${this.QQMark}`)
console.log(`共同的好友是:${this.commonfriends}`);
}
let QQZhangSan = new QQUsers("37834522", 15, "王阳明传人")
let QQLisi = new QQUsers("30424232", 10, "袁隆平的徒弟")
//QQUsers.prototype.commonfriends.push("大树");
console.log(QQZhangSan.commonfriends);
console.log(QQLisi.commonfriends);
QQUsers.prototype = {
commonfriends: ["abc", "bcd", '骑驴看海']
}
console.log("QQUsers.prototype:", QQUsers.prototype)
console.log("QQZhangSan.commonfriends:", QQZhangSan.commonfriends)
console.log("QQUsers.prototype.commonfriends:", QQUsers.prototype.commonfriends)
// 结果:
// QQUsers.prototype: { commonfriends: [ 'abc', 'bcd', '骑驴看海' ] }
// QQZhangSan.commonfriends: [ '骑驴看海', '大漠上的英雄', '坚实的果子', '小草' ]
// QQUsers.prototype.commonfriends: [ 'abc', 'bcd', '骑驴看海' ]
因为创建对象变量在前,而QQUsers.prototype = { commonfriends: ["abc", "bcd", '骑驴看海'] }改变的是构造函数上的原型对象空间,它会指向新的空间,但是之前创建的对象变量的指向和之前的原型对象空间并没有销毁改变,所以访问的还是之前的值。
思考题:QQZhangSan.__proto__.show()和QQZhangSan.show() 输出的结果完全一样吗? 为什么呢?
QQZhangSan.__proto__.show()
// QQ号:undefined,QQ龄:undefined,QQ标注:undefined
// 共同的好友是:骑驴看海,大漠上的英雄,坚实的果子,小草
QQZhangSan.show()
// QQ号:37834522,QQ龄:15,QQ标注:王阳明传人
// 共同的好友是:骑驴看海,大漠上的英雄,坚实的果子,小草
QQZhangSan.__proto__.show() 执行的是构造函数原型上初始的show()方法,QQZhangSan.show() 执行对象变量个性化传参之后的show()方法。