JS原型和原型链最详解(附图)

发布于:2023-02-09 ⋅ 阅读:(1167) ⋅ 点赞:(1)

目录

一、自定义构造函数

二、调用构造函数

三、构造函数、原型对象和实例之间的关系

四、isPrototypeOf()

五、Object.getPrototypeOf() 

六、Object.setPrototypeOf()

七、Object.create()

八、原型链

九、总结


进入原型讲解之前,我们需要知道两个概念

1、首先,构造函数是一个对象(函数就是在特定上下文中执行代码的简单对象)!

2、其次,原型对象本身也是对象!

原型和原型对象的区别:

我们有时候会见到这么一段描述:每个对象都有原型,对象的原型指向原型对象

其实,他们之间的关系就是key:value的关系,原型是key,value是原型对象

所以,我们才会说原型指向原型对象,我们不必过分纠结两者的关系,当我们看见什么指向什么时,大都可以理解为key:value的关系

一、自定义构造函数

每个函数都会创建一个 prototype 属性,这个属性的值就是原型对象,所以可通过.prototype获取原型对象

那么这个原型对象从何而来,我们接着往下看...

function Person() {}

// 获取原型对象
console.log(Person.prototype) // Person的原型对象
console.log(Object.prototype) // Object的原型对象

1. 原型对象默认会获得constructor属性,指回与之关联的构造函数(constructor: ƒ Person()),

    其他方法则继承自Object(至于为什么继承自Object,我们先暂且搁置,后面自会豁然开朗)

  • constructor是一个不可枚举属性,可通过以下方法查看
let observe = Object.getOwnPropertyDescriptors(Person.prototype)
console.log(observe)
  • 所以你会发现constructor是一个灰色的属性

2. 你会在原型对象下看到[[Prototype]]: Object,这个就是Object的原型对象

3. 总的来说,可以理解为 [[Prototype]] 就是构造函数的原型对象,可通过构造函数的prototype属性获取(即prototype指向原型对象[[Prototype]]

图一


二、 调用构造函数

调用构造函数,即new之后,js都做了哪些事?

具体过程可阅读工厂模式与构造函数模式的区别最后部分

let person1 = new Person()
// 给原型对象添加其他属性和方法
Person.prototype.name = 'jack'
Person.prototype.sayName = () => {
        console.log(this.name)
    }
console.log(person1)

图二

刚开始我们就说到,原型对象也是对象,即然如此,那原型对象也应该有原型对象

如果这么理解,那岂不是儿子的爸爸的爸爸的爸爸的······是谁? 

为了解决这个疑问,我们来看看爸爸是谁:

1. 正常的原型链都会终止于 Object 的原型对象,Object 原型对象的原型是null(Object.prototype.__proto__)

2. Firefox、Safari 和 Chrome会在每个原型对象上暴露__proto__属性,通过这个属性可以   访问原型对象的原型对象(因为原型对象本身也是对象)

console.log(Object.prototype)

 

 图三

到这里我们就知道了,原来爸爸是我 😁


 三、构造函数、原型对象和实例之间的关系

1. 构造函数、原型对象和实例是 3 个完全不同的对象

console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true 

2. 实例通过__proto__链接到原型对象,即指向隐藏特性 [[Prototype]]

如果不理解,可以对比构造函数:

  构造函数是通过 prototype 属性获取的原型对象[[Prototype]],prototype:[[Prototype]]

  实例则是通过 __proto__ 属性获取的原型对象[[Prototype]], __proto__:[[Prototype]]

  实例.__proto__ 就相当于 构造函数.prototype

  • 同一个构造函数创建的两个实例共享同一个原型对象

let person2 = new Person()

console.log(person1.__proto__ === person2.__proto__); // true

// constructor 可以用于标识对象类型
console.log(person1 instanceof Person); // true

// person1 之所以也被认为是Object的实例,是因为所有自定义对象都继承自Object
console.log(person1 instanceof Object); // true

console.log(Person.prototype instanceof Object); // true

3. 构造函数通过 prototype 属性指向 原型对象

4. 结论:实例与构造函数没有直接联系,与原型对象有直接联系

console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.constructor === Person); // true
console.log(Person.prototype.constructor === Person); // true

ps:如果不理解这个图,建议对比我们之前打印的结果(图一和图二),或许对你有更好的帮助

 关系图


四、isPrototypeOf()

1. 不是所有实现都对外暴露了[[Prototype]],但可以使用 isPrototypeOf()方法确定两个对象之间关系

2. 通过原型对象调用 isPrototypeOf()方法检查了person1,这个实例内部的constructor指向Person.prototype(即Person的原型对象),所以返回true

console.log(Person.prototype.isPrototypeOf(person1)); // true

ps:不理解请看上面的关系图


五、Object.getPrototypeOf() 

返回参数的内部特性[[Prototype]]的值,即取得一个对象的原型

console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person1));


六、Object.setPrototypeOf()

1. 向实例的私有特性 [[Prototype]] 写入一个新值,这样就可以重写一个对象的原型继承关系

2. 由于同一个构造函数创建的多个实例共享同一个原型对象,所以可能会严重影响代码性能,  会涉及所有访问了[[Prototype]]的对象的代码

let biped = {
    numLegs: 2
};
let person = {
    name: 'jack'
};
Object.setPrototypeOf(person, biped);
console.log(person)
console.log(person.name); // jack
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true


 七、Object.create()

为避免使用 Object.setPrototypeOf()可能造成的性能下降,可以通过 Object.create()来创建一个新对象,同时为其指定原型

let biped1 = {
    numLegs: 2
};
let persons = Object.create(biped1);
persons.name = 'jack';
console.log(persons)
console.log(persons.name); // jack
console.log(persons.numLegs); // 2
console.log(Object.getPrototypeOf(persons) === biped1); // true

八、原型链

原型链并没有我们想象的那么复杂,他就是一个找爸爸的过程

在前面我们就提到,每个对象都有原型对象,原型对象又有原型对象,......

当然,我们不可能永无休止的找爸爸,当我们找到null时,也就是Object 原型对象的原型时

我们就停下来了

爸爸 => 爸爸 => 爸爸 => ······ => null

这种原型层层连接起来就构成了我们所说的 原型链


九、总结

每个函数都会创建一个 prototype 属性,这个属性是一个对象

现在,我们回到第一部分抛出的两个问题:

这个对象从何而来?为什么说所有自定义对象都继承自Object?

通过了解调用构造函数(new)执行的操作后(如果不清楚,可参考 工厂模式与构造函数模式的区别最后部分),我们可以知道:

实际上,这个对象就是调用构造函数(new)时创建的  对象的 原型,

即prototype:[[Prototype]]

这也就是  所有自定义对象都继承自Object  的来源。

本文含有隐藏内容,请 开通VIP 后查看