一、基本概念
1.什么是原型
这里会设计到两个概念:构造函数和原型对象。
构造函数:在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。构造函数首字母一般大写。
原型对象:初始化一个构造函数,它有一个 prototype 属性,该属性值指向的就是构造函数的原型对象
function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
比如上文的Person.prototype
指向的就是Person
的原型
当然了,只有构造函数才有 prototype 这个属性,其他对象是没有的
我们来看看原型的定义:
原型本质是一个对象,原型对象是为了给其他实例对象提供共享属性的
2.访问原型对象
首先通过构造函数初始化一个实例对象
const person1 = new Person('高启强', 38)
console.log(person1);
左边红色框表示调用的哪个,右边是对象实例。
可以看到 person1 只具有构造函数中初始化的两个默认属性 name 和 age
上面说到构造函数可以通过prototype
属性访问到原型对象,那么这个构造函数的实例对象如何访问到这个原型对象呢?
我们可以使用__proto__
属性
console.log(person1.__proto__ === Person.prototype)
看一下控制台打印:
我们可以看到这两个是完全等价的
__proto__
最初是由各个浏览器厂商独立实现的 hack 方法,目前已收入到ES6规范中,可以放心使用
当然,更推荐的还是使用 ES6 规定的获取原型的方法Object.getPrototypeOf()
console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
反过来,如何通过原型对象访问到构造函数呢?原型对象有个 constructor 属性:
console.log(Person.prototype.constructor === Person) // true
每个原型对象都有一个 constructor 属性指向关联的构造函数
3.共享属性
如果我们访问一个构造函数没有定义过的属性会怎么样?
console.log(person1.work) // undefined
当然是 undefined 了,没定义就没有,我们找不到它。
那我们尝试在原型对象上定义它,再次记住原型对象的作用:给实例对象提供共享属性
Person.prototype.work= '杀鱼'
OK,我们尝试在实例对象上访问这个属性
console.log(person1.work) // 杀鱼
看一下控制台打印:
成功了!
那么 person1 会有什么变化吗?打印下看看
console.log(person1) // Person {name: 'Jason', age: 21}
person1 是没有 work属性,因为它并没有在 person1 或者构造函数中定义过
那 person1.work
的值哪来的?答案已经呼之欲出
从 person1 的原型对象上拿到的
person1.work=== person1.__proto__.work
所以我们可以得出第一个结论,当找不到对象的某个属性时,它会继续往这个对象的原型对象上找
我们再用这个构造函数初始化一个对象
const person2 = new Person('高启盛', 22)
看一看这个 person2 长啥样
console.log(person2) // Person {name: '高启盛', age: 22}
person2 同样也没有 work 属性
console.log(person2.name +"的工作是"+ person2.work)
看一下控制台打印:
可以看到我们声明的第二个人物同样拿到了work这个属性,说明虽然没有这个属性,但是也能访问到原型对象上的共享属性 work,简单说只要是这个对象new出来的都可以使用它。
console.log(person2.__proto__.work=== person2.work) // true
没错,这俩个就是完全等价的!
我们通过 person2 改变这个绑定在原型上的属性值试试,注意看我这里改变的是person2原型上的work变成了白金瀚的老板,打印出来person1的work也变成白金瀚的老板。也就是原型对象上的work属性是全部实例对象所共享的,一有俱有,一改俱改
这样我们应该就理解了,原型对象是用来提供共享属性这句话了
person2.__proto__.work = '白金瀚老板'
console.log("现在"+person1.name +"的工作是"+ person1.work)
看一下控制台打印:
二、原型链
1.访问对象的某个属性当找不到时?
我们知道了访问对象的某个属性当找不到时,会往它的原型对象上找,如果原型对象上也找不到呢?
首先看看 person1 原型对象目前长啥样
console.log(person1.__proto__)
看一下控制台打印:
我们应该可以猜到,原型对象也是对象,在原型对象上没找到,那就继续往上去找!
我们来看看原型的原型是什么
console.log(person1.__proto__.__proto__)
看一下控制台打印:
这一眼看过去,好像也不知道是什么,但你是否记得上面提到过的:
每个原型对象都有一个 constructor 属性指向关联的构造函数
我们访问原型的原型的 constructor 属性试试
console.log(person1.__proto__.__proto__.constructor) // ƒ Object()
没想到吧,实例对象原型的原型就是 Object 的原型对象,其实Object是原型的顶层原型对象
因为是顶层原型对象,它的__proto__指向null。
console.log(person1.__proto__.__proto__.__proto__) // null
借用冴羽老师的一张图就清楚其中的关系了:
图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
简单来说就是,找 person1 对象某个属性没找到,就通过__proto__
访问原型一层一层地找,直到找到这个属性或者到 null 位置结束,相互关联的原型就构成了原型链。这样是不是也不难理解。
三.扩展
通过上面的知识我们知道了,构造函数 Person 的prototype
也被称为显式原型,实例对象 person 的__proto__
也被称为隐式原型
即构造函数的显式原型和实例对象的隐式原型是同一个对象
function Person() {
}
const person = new Person()
Person.prototype === person.__proto__ // true
那么构造函数的隐式原型__proto__
指向什么呢?
指向Function
的原型,即
Person.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
大家有时间可以看看这篇文章:https://juejin.cn/post/7064188728704499726
里面这位老哥有更详细的解读,本文如果有错误的话也欢迎大家来指正。
总结
- 原型对象的本质其实就是一个对象
- 原型的作用是给其他对象提供共享属性,即用来实现基于原型的继承与属性的共享
- 相互关联的原型组成的链状结构就是原型链
- 原型链的顶级原型对象是Object,且最终一定指向
null