从头创建一个"郎朗家族"2——带你深入了解原型
上期通过构造函数,我们已经能够高效的创造一个又一个坚果对象,最终创建了一个坚果大军;还可以通过给构造函数传入参数,来给每个新创建的对象自定义一些例如颜色、形状的属性。通过上期的学习,相信你已经了解了构造函数和包装类了,我们先来看看最后留的这道面试题:
var str = 'abc'
str += 1
var test = typeof(str)
if (test.length == 6) {
test.sign = 'typeof 的返回结果是string'
}
console.log(test.sign);
问输出了什么?是'typeof 的返回结果是string'吗?别着急,我们一步一步来看:
首先 var str = 'abc'; str += 1 会发生类型转换,数字 1 的类型转换成字符串 '1',与'abc'拼接为'abc1'; 此时typeof(str)当然还是'string',test.length == 6 判断也是成立的,执行test.sign = 'typeof 的返回结 果是string',再把它输出出来,应该是'typeof 的返回结果是string'吧,我们运行一下代码看看。
输出了'undefined',访问了一个不存在的属性?噢,还记得我们前面聊到的包装类的执行过程嘛,test是原始值字符串'string' ,给它添加属性'sign' 并不会成功,sign属性被临时包装对象给添加了,但是这个临时对象被丢弃了,所以sign属性并没有被添加给 test, 而是被delete掉了,所以访问不到。
OK,让我们继续加强我们的坚果大军。上期评论区有同学还不太清楚显式原型和隐式原型的区别,我们先来讲讲原型是什么。这个原型可不是现出原形的那个"原形",原型是函数上的一个属性,它定义了构造函数创建的对象的公共祖先。 构造函数new出来的对象会隐式继承到构造函数原型上的属性。有点抽象,举个栗子:
function Nut() {
this.maxHealth = 4000;
this.health = 4000;
this.type = "plant";
}
console.log(Nut.prototype); // {} 说明原型也是一个对象
Nut.prototype.cost = 50; // 既然是对象,我们尝试往里面加一些属性看看
let n1 = new Nut()
console.log(n1); // Nut { maxHealth: 4000, health: 4000, type: 'plant' },没有显示出cost属性
console.log(n1.cost); // 50 能访问到cost属性
// 试试往原型里加一个函数体
Nut.prototype.defend = function() {
console.log("I'm bited!");
this.health -= 100;
if (this.health <= 0) {
console.log("Nut has been destroyed!");
this.isDestroyed = true;
}
};
n1.defend(); // I'm bited! 函数能够调用
console.log(n1.health) // 3900 血量确实减少了
还是那个构造函数,我们打印这个原型看看,输出了个"{ }",说明原型也是一个对象,既然是对象,我们就可以往里面添加属性和方法,我们先添加一个cost属性,再创建一个实例对象,打印这个对象,发现上面并没有cost属性,但我们却可以访问到它;同样,当我们往函数原型上加一个函数体,我们也可以调用到实例对象上的函数,说明实例对象中并不是没有这些属性和函数,只是隐式具有着,我们看不到它们。
function Nut(color,shape) {
this.maxHealth = 4000;
this.health = 4000;
this.type = "plant";
this.cost = 50;
this.color = color;
this.shape = shape
}
let n1 = new Nut('green','square');
let n2 = new Nut('purple','round');
// ...成千上万个实例对象
我们注意到,每次创建一个新实例对象都调用一次构造函数,假如我们要创建成千上万个对象,每个对象又有成千上万个属性,而其中大多都是固定的,固定的参数每次都要执行一遍,造成大量性能的浪费,能不能优化一下? 我们可以用上刚刚认识到的函数原型,优化一下模具。
// 我们把固定的属性拿出来 放在构造函数原型上 实例对象可以隐式继承到 不用重复执行了 效率得到了提高
Nut.prototype.maxHealth = 4000;
Nut.prototype.Health = 4000;
Nut.prototype.type = "plant";
Nut.prototype.cost = 50;
// 我们只需要执行需要自定义的属性代码
function Nut(color,shape) {
this.color = color; // 定制颜色
this.shape = shape; // 定制形状
}
let n1 = new Nut('green','square');
let n2 = new Nut('purple','round');
console.log(n1);
console.log(n2); // Nut { color: 'purple', shape: 'round' } 原型中的属性隐式存在
console.log(n1.color); // green 原型中的属性隐式存在 可以访问
通过把一些固定的属性写在构造函数原型上,我们可以避免很多重复执行的代码,优化代码的性能。
Nut.prototype.cost = 50 // 实例对象会隐式具有的属性cost
function Car() {
this.type = "nut"
}
let nut = new Nut();
// 改造成一个僵尸坚果墙
nut.type = "zombie"
console.log(nut); // Nut { type = "zombie" } 成功了
// 无良商家涨价了
nut.cost = 200
console.log(nut.cost); // 200 那么是不是函数原型中定义的属性也能改呢?
console.log(nut); // Nut { type = "zombie",cost = 200 } 隐式具有的cost变成了显式具有?
nut.nickname = '尕娃' // 实例对象无法给原型新增属性
Nut.prototype.nickname = '五香蛋'
console.log(nut.nickname); // 尕娃 优先访问到实例对象上定义的属性
delete nut.nickname // 删除实例对象中的属性
console.log(nut.nickname) // 五香蛋 还是能访问到隐式继承到的属性,实例对象无法删除原型上的属性
delete Nut.prototype.nickname // 删除原型上的属性
console.log(nut.nickname) // undefined 删除了原型上的属性,实例对象无法访问到
从上面的代码可以看出,这个显式具有的cost和隐式具有的并不是同一个,如果能修改就不会多一个显式具有的属性了。
原来 nut.cost = 200 并不是修改了cost属性,而是往实例对象中加了个cost属性,隐式属性还在,但访问不到了。 js引擎在查找属性时,会先查找对象显式具有的属性,找不到再查找对象的隐式原型(proto)。实例对象不能修改隐式具有的属性,只能添加新的显式具有的属性。
我们再来看看构造器constructor属性:
function Nut() {
}
// let nut = new Nut()
// console.log(nut.constructor); // 构造器 输出[Function: Nut] 就是构造函数Nut函数 constructor 隐式具有的属性 由原型继承而来 记录该对象是由谁创建的
function Dave() {}
// 修改原型对象 让car的构造器指向Bus
Nut.prototype = {
constructor : Dave
}
let nut = new Nut()
console.log(nut.constructor); // [Function: Dave] 修改了constructor的指向 找不到是谁创建的car了,变成了戴夫
实例对象中存在constructor属性,它指向构造这个实例对象的构造函数,constructor的指向也可以被修改,修改后就找不到实例对象是由谁创建的了。
// 构造函数体上的属性
function Nut() {
this.type = "plant";
}
// 构造函数原型上的属性
Nut.prototype.defend = function() {
console.log("I'm bited!");
}
let n = new Nut();
console.log(n.__proto__); // 在浏览器中打开,看控制台 结果如下 // n.__proto__ 对象的原型 隐式原型
// {}
// constructor: ƒ Nut()
// [[Prototype]]: Object
console.log(Nut.prototype); // 函数原型 显式原型
// {}
// constructor: ƒ Nut()
// [[Prototype]]: Object
// 完全一样 n.__proto__ === Nut.prototype 构造函数的原型等于实例对象的原型 显式原型和隐式原型完全一样
// 函数原型的constructor和实例对象的constructor相同 说明实例对象的constructor是从函数原型上继承来的
console.log(n); // 对象用自己来接收构造函数的函数体上的属性
n.defend() // 可访问构造函数原型上的属性
// Nut
// type : "plant"
// [[Prototype]]: Object 对象的原型还是个对象
// say: ƒ () 对象用自己的原型来接收构造函数原型上的属性
// constructor: ƒ Nut()
// [[Prototype]]: Object
// 访问属性时,先访问对象本身,没有则去原型上找
在浏览器中查看一个对象的隐式原型和它的构造函数的显式原型,会发现构造函数的显式原型等于实例对象的隐式原型 ,实例对象的constructor是从函数原型上继承来的。对象用自己来接收构造函数的函数体上的属性,用自己的原型来接收构造函数原型上的属性。
我们已经知道,当访问一个实例对象的属性时,会先查找对象显式具有的属性,如果找不到,会再去对象的隐式原型,即构造函数的显式原型上查找,那么如果构造函数的显式原型上也找不到,又会去构造函数显式原型的隐式原型上找, 构造函数显式原型的隐式原型是什么呢?在浏览器中运行下面的代码:
function Nut() {
}
let n = new Person()
console.log(n)
console.log(Nut.prototype);
console.log(Nut.prototype._proto_)
console.log(Nut.prototype._proto_._proto_)
我们会发现以下规律:
n.proto === Nut.prototype; // 对象p的隐式原型 === 构造函数的显式原型
Nut.prototype.proto === Object.prototype; // 内置的构造函数 所有的对象最根本都是由这个函数创建的
Object.prototype.proto === null; // 到这一步就不能再往上翻了
这就是原型链,就像是翻族谱一样去查找原型上的属性,只要没有找到,就一层一层往上,直到null,一个人——父亲——爷爷——祖宗——女娲,翻到了女娲,女娲是谁创建的? 谁也不知道,到这就封顶了,返回null。这个“女娲”,就是JS中的Object.prototype,所有对象的构造函数的显式原型,没有隐式原型了。
所以,当面试时被问到:“所有的对象都有原型吗?”你应该回答:不,Object.create(null) 没有原型(创建一个新对象,让新对象隐式继承null的属性),通过这种方式创建的对象不会继承Object.prototype
上的任何属性或方法。
另外,补充一点,如果想要实例对象能继承到构造函数上的方法,就把方法写在构造函数的原型上;如果不想所有实例都继承,只想让构造函数能访问,就把方法写在构造函数上即可。
// Fn.prototype.say = function() { // 写在了原型上,所有由Fn创建的实例都会继承,但我们并不想所有实例都继承,只想让Fn能访问
// }
function Fn() {
console.log('hello')
}
Fn.say = function() {} // 写在了构造函数Fn上,只有Fn能访问
let f = new Fn();
Fn.say() // hello
f.say() // TypeError: f.say is not a function
了解了原型,我们就能通过原型来实现继承,完成杂交的效果,下期我们来通过继承来把我们的普通坚果杂交成更强的火炬坚果、磁铁坚果、生瓜蛋子、冰冻坚果。
转载自:https://juejin.cn/post/7395868215610720268