原型和原型链 | 理解原型和原型层级

原型和原型链是JavaScript中很重要的一块知识点,因为实例、构造函数、原型函数之前关系,有时候会被绕晕,遂计划进行一波总结~

回顾

  1. 只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)

  2. 默认情况下,所有的原型的对象自动获得一个名为constructor的属性,指回与之关联的构造函数

  3. 原型对象上包含由构造函数的实例共享的属性和方法

  4. 在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object

  5. 每次调用构造函数创建一个新实例,这个实例内部[[prototype]]指针就会被赋值为构造函数的原型对象,一般我们通过__proto__进行访问对象的原型

实例 构造函数 原型 三者之间的关系

根据之前回顾的知识,若设Person为构造函数,Person.prototype即为原型对象,p=new Person()为构造函数创建的实例,不难可以手绘此图~

根据上图,不难理解:实例与构造函数原型之间有直接的联系,而实例与构造函数之间没有直接联系。

1. 原型行为

下面根据一个例子来说明三者之间的关系

(1)构造函数和原型链循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 建立构造函数Person
function Person() {};

// 根据回顾1:只要创建一个函数,就会有原型对象
console.log(Person.prototype);
/*
{
constructor: f Person
[[prototype]]: Object
}
*/

// 根据回顾2:默认情况下,所有的原型的对象自动获得一个名为``constructor``的属性,指回与之关联的构造函数
console.log(Person.prototype.constructor === Person); // true

/*
结合回顾1&&2,不难得到构造函数和原型之间的循环引用关系

Person <————> Person.prototype
*/

(2)原型链终止于Object原型对象

1
2
3
4
5
6
7
8
/*
正常的原型链都会终止于Object的原型对象
Object原型的原型是null
这个在图中已经展示了,下面通过代码进行验证
*/
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true

(3)实例访问原型对象

1
2
3
4
5
6
7
8
/*
实例通过new构造函数进行创建
根据回顾5:实例可通过__proto__访问到构造函数的原型

*/

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

(4)原型操作方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
instaceof的原理是:检测构造函数的原型是否存在于实例对象的原型链上
*/
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true

/*
isPrototyeoOf() 用于检测两个对象之间的原型关系
*/
console.log(Person.prototype.isPrototypeOf(person1)); // true

/*
Object.getPrototypeOf() 用于获取对象的原型
*/
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true

/*
Object.setPrototypeOf() 用于获取对象的原型
*/
let person = {
name: 'Katrina',
};

Object.setPrototypeOf(person1, person);
console.log(Object.getPrototypeOf(person1) === person); // true

/*
Object.setPrototype()会严重影响代码性能,
[Mozilla在文档里说,在所有浏览器和JavaScript 引擎中,修改继承关系的影响都是微妙且深远的。这种影响并不仅是执行Object.setPrototypeOf()
语句那么简单,而是会涉及所有访问了那些修改过[[Prototype]]的对象的代码]
所以可以通过Object.create()来创建一个新对象,同时为其指定原型
*/
let person = {
name: 'Katrina',
};

let person2 = Object.create(person);
console.log(Object.getPrototypeOf(person2) === person); // true

上述关系均可抽象为下图:

2. 原型层级

(1)对象属性的查找机制

对象属性的查找机制:在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值,如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。如果直接原型中没有找到,就会去原型的原型进行寻找,直到找到Object.prototype的原型,指向null为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {};

Person.prototype.name = 'Katrina';
Person.prototype.age = 18;
Person.prototype.sayName = function() {
console.log(`Hi, my name is ${this.name}.`);
};

let p1 = new Person();
let p2 = new Person();
p1.name = 'y2d';
console.log(p1.name); // y2d
console.log(p2.name); // Katrina

从上面这个例子不难理解对象属性的查找机制,在p1对象中含有name属性,打印结果为y2d,而p2对象中不含name属性,就沿着指针去对原型寻找name属性并打印出Katrina,同时也可以说明如果实例对象有和原型对象相同的属性,实例对象的属性会遮盖住原型对象的属性,会屏蔽对原型同名属性的访问。

(2)方法

  • delete删除属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person() {};

Person.prototype.name = 'Katrina';
Person.prototype.age = 18;
Person.prototype.sayName = function() {
console.log(`Hi, my name is ${this.name}.`);
};

let p1 = new Person();
let p2 = new Person();
p1.name = 'y2d';
console.log(p1.name); // y2d
console.log(p2.name); // Katrina

delete p1.name;
console.log(p1.name); // Katrina
  • hasOwnProperty() 方法用于确定某个属性是实例的属性还是原型的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person() {};

Person.prototype.name = 'Katrina';
Person.prototype.age = 18;
Person.prototype.sayName = function() {
console.log(`Hi, my name is ${this.name}.`);
};

let p1 = new Person();

p1.gender = 'female';

console.log(p1.hasOwnProperty('gender')); // true
console.log(p1.hasOwnProperty('name')); // false

hasOwnProperty() 方法可用于改进for…in枚举对象属性时会枚举原型链上的可枚举属性的缺陷:循环语句 | for-in&&for-of的区别

知识补充

手写new

手写instanceof

手写Object.create

参考