likes
comments
collection
share

从头创建一个"郎朗家族"2——带你深入了解原型

作者站长头像
站长
· 阅读数 18

上期通过构造函数,我们已经能够高效的创造一个又一个坚果对象,最终创建了一个坚果大军;还可以通过给构造函数传入参数,来给每个新创建的对象自定义一些例如颜色、形状的属性。通过上期的学习,相信你已经了解了构造函数和包装类了,我们先来看看最后留的这道面试题:

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'吧,我们运行一下代码看看。

从头创建一个"郎朗家族"2——带你深入了解原型

输出了'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   删除了原型上的属性,实例对象无法访问到

从头创建一个"郎朗家族"2——带你深入了解原型

从上面的代码可以看出,这个显式具有的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
评论
请登录