前言
别再背__proto__
和prototype
,直接理解就完了。
- 一开始了解到隐式原型和显示原型,以为是两种东西,后来才知道是一样的。
- 两个都是属性,只是属性名不同,属性值是相同的,属性值保存的都是原型对象的引用。
看完下面内容,你就明白了。
一、原型对象
1. 三者关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlMKEayz-1651567516522)(C:\Users\USER\Desktop\前端面试\D博客文章\image\原型链-2.PNG)]
这张图中有三个主体,分别是实例对象、构造函数和原型对象。主要是理解这三者的关系。
- 构造函数&原型对象:当代码执行过程创建了函数A,创建这个函数的原型对象B,放在这个函数的
__prototype__
属性上。原型对象B的constructor
指向该函数A。 - 实例对象&构造函数:对函数A进行new操作,这个函数A就是构造函数,而
new操作
就是为了生成的对象C。 - 实例对象&原型链:在上面new操作的过程中,将函数A的原型对象复制到对象C的
__proto__
属性上,(对象复制的是引用),所以对象C的__proto__
属性指向和构造函数指向的是同一个原型对象。
以下同代码配合说明一下:
function A (){...}// 当执行到这段代码时,会创建原型对象B,就会有A.__prototype__ = B
const C = new A() // 对A执行new操作,产生对象C,而且会有C.__proto__ = B
从上面可以看出,三者当中,对象C和构造函数A没有直接联系。只跟变量A有关系。
但是new操作做了什么?对象C的属性生成跟构造函数A有什么关系?
2. new操作
前面提到的new操作,其实并不神秘
内部其实是
- 生成一个空对象,将构造函数A的原型对象赋值到这个空对象的
__proto__
属性上。 - 然后将构造函数作为该对象的方法执行。执行构造函数过程中为该对象添加属性和方法。
代码说明一下:
// 模拟new操作
function myNew(constructor, arg) {
// 1. 创建一个新对象
const obj = {};
// 2. 为新对象添加属性__proto__,将该属性链接至构造函数的原型对象
obj.__proto__ = constructor.prototype;
// 3. 执行构造函数,this被绑定在新对象上,为对象
const res = constructor.call(obj, ...arg);
// 4. 确保返回一个对象
return res instanceof Object ? res : obj;
}
继续展开call方法
function myNew(constructor, arg) {
// 1. 创建一个新对象
const obj = {};
// 2. 为新对象添加属性__proto__,将该属性链接至构造函数的原型对象
obj.__proto__ = constructor.prototype;
// 3. 执行构造函数,this被绑定在新对象上,为对象
// const res = constructor.call(obj, ...arg);
obj.fn = constructor;
const res = obj.fn();
delete context['fn']
// 4. 确保返回一个对象
return res instanceof Object ? res : obj;
}
- 其实是将构造函数作为的方法执行,为obj添加属性和方法。
到这里可能还不够清晰,执行构造函数怎么给对象添加属性
那么举个例子分析一下:
function Student (){
this.name = '张三';
this.age = '33';
}
const newStudent = new Student()
console.log(newStudent.name) // "张三"
// 也就是构造函数中的this.xxx会变成实例的属性和方法
构造函数的本质:
- 函数本身不是构造函数,当你在普通函数调用前加上new关键字后,就会把这个函数调用变成一个“构造函数调用”。
- 在
JavaScript
中对于“构造函数”最准确的解释是,所有带new的函数调用。
3. 小结
- 每个函数都有一个公有的不可枚举的
prototype
属性,指向原型对象。(注意,不包括箭头函数,全文其他位置也一样。) - 原型对象自动获得一个
constructor
的属性,指回与之关联的构造函数。 - 对函数执行new操作,就会创建一个新实例,这个实例的内部
__proto__
属性就会被赋值为构造函数的原型对象。
这也就是上面那副关系图。
二、原型链
理解了原型,理解原型链就简单多了。
1. 理解原型链
如果原型对象C也有自己原型对象,那么就像这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kjEY20ht-1651567516530)(C:\Users\USER\Desktop\前端面试\D博客文章\image\原型链-1.PNG)]
图中右边的原型对象也又constructor属性指向构造函数。(两者成对存在)
如果一层一层往下连下去,就形成了原型链。
2. [[Prototype]]属性
- JavaScript的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。在浏览器中可以通过
__proto__
访问到。 - 所有对象在创建是,[[Prototype]]属性都被赋予一个非空的值。
[[Prototype]]的引用有什么用?
- 当你试图引用对象的属性时,会触发[[Get]]操作。
- 对于默认的[[Get]]操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。
- 如果无法在对象本身找到需要的属性,就会继续访问对象的[[Prototype]]链。
(该讨论在ES6的proxy中不适用)
3. 原型链的顶层
到哪里是尽头呢?
- 所有普通的prototype链都会指向内置的
Object.prototype
。
为什么是指向Object.prototype
呢?
比如对象a的创建过程:
// 字面量创建对象
const a = {
tip: '我是对象a'
}
// 构造函数创建对象
const a = new Object() // Object是内置函数,这里a继承了Object().prototype
a.tip = '我是对象a'
- 对象字面量是对象定义的简写形式,目的是为了简化包含大量属性的对象的创建。
- 除了
Object()
,JS中还有其他内置函数,比如:Array(),String(),Math()等,由此产生不同的实例对象:数组、字符串等。
这些其实是new操作的内容。
4. 原型属性共享
前面已经说过,原型对象中的属性可以被实例对象访问到,这里原型的作用。那么对于原型链来说,也是一样。
(1)原理
其实,也就是对象属性查找机制。
- 在通过对象访问属性时,会按照这个属性的名称开始搜索。
- 如果在这个实例上发现了给定的名称,则返回该名称对应的值。
- 如果没有找到这个属性,则搜索会进入原型对象,然后在原型对象上找到属性后,再返回对应的值。如果没找到,则会继承搜索实例的原型,直到
Object.__prototype__
。
(2)判断是不是自身属性
因为对象可以访问到原型上的属性,所以有些时候需要判断是自身属性还是原型上的属性。
- 使用
hasOwnProperty()
来检查对象自身中是否含有该属性 - 使用
in
操作符检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
如果用 in 返回为true,hasOwnProperty()
返回false,则说明属性在原型链上。
5. 原型链的问题
(1)引用值问题
原型中包含的引用值会在所有实例间共享。
- 就是说,一个实例对象修改了原型上的引用类型,其他实例访问这个属性的时候,只能获取到改变后的。
- 但,如果属性值是原始类型,实例对象会在自身添加该属性,不会修改原型上的。其他实例获取到的属性只不会改变。
(2)无法传参
- 子类型在实例化时不能给父类型的构造函数传参。
- 由于前面这两个问题,导致原型链基本不会被单独使用。
6. 原型的动态性
- 因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
- 这个在前面在属性共享也有类似说明(一个修改影响到其他的),不过这里可以只有单个实例。
举个例子
let friend = new Person();
Person.prototype.sayHi = function() { // 直接修改原型对象
console.log("hi");
};
friend.sayHi(); // "hi",没问题! // 执行了原型对象上新增的属性
其实原型对象本身就是一个对象,原型对象改变了,通过引用获取到的自然也是改变了的。
7. 小结
- 原型链机制建立起对象与对象之间的联系。在对象自身找不到的属性,会到原型链上查找。
- 原型链的顶层是
Object.prototype
,对象属性查找到这里结束。 - 原型链的属性共享,修改引用类型的属性值会影响到其他实例。