在典型的OOP语言中(如java),都存在类的实例,类是对象的模板,对象就是类的实例。在ES6之前的JS中,没有类的概念,ES6之后引入了 class 关键字用来声明一个类。而在ES6之前,对象不是基于类创建的,而是用构造函数来定义对象。本篇文章带你好好理理构造函数、对象实例和原型对象之间的关系。
对象原型
先来回顾下如何创建对象
创建对象可以通过以下三种方式:
- 利用对象字面量
let obj1 = {}
复制代码
- 利用 new Object()
let obj2 = new Object()
复制代码
- 利用构造函数
function Person(name, age) {
this.name = name
this.age = age
}
let per = new Person('Steven', 22)
复制代码
有了对象之后,我们都知道在对象里面可以定义属性和方法,但其实JS天生就规定了一个隐藏的属性__proto__
,它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”。
下面我们来看看它
let user = {
name: 'Steven'
}
console.log(user);
复制代码
定义了一个 user 对象,添加一个 name 属性, 值为 ‘Steven’,输出看看
可以看到 user 对象 里面不仅有 name 属性, 还有一个__proto__
属性指向了一个Object 。
而我们前面说了,这个属性要么为 null,要么就是对另一个对象的引用,被引用的对象称为此对象的原型。
好,现在我们知道了对象有隐藏的 __proto__
属性会指向原型或null,那么它究竟有什么作用呢?
JS规定:当我们从一个 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这种行为被称为“原型继承”。
意思就是说使用__proto__
可以实现继承,来看个例子
let animal = {
eat: true
}
let cat = {
jump: true
}
cat.__proto__ = animal
// 这两个属性都可以读取到
console.log(cat.jump); // ture
console.log(cat.eat); // true
复制代码
这里我们将 animal 设置为 cat 的原型,当我们想读取 cat.eat
时,首先,它会在 cat 对象里面查找该属性,在 cat 里面没有找到,于是 JavaScript 会顺着 __proto__
的引用,在 animal 中查找。
在这儿我们可以说 animal 是 cat 的原型,或者说 cat 的原型是从 animal 继承而来的。
因此,如果 animal 有许多有用的属性和方法,那么它们将自动地变为在 cat 中可用。这种属性被称为“继承”。
原型链可以很长:
let animal = {
eat: true
}
let cat = {
jump: true,
__proto__: animal
}
let tomcat = {
hide: true,
__proto__: cat
}
console.log(tomcat.eat); // true
复制代码
可以看到 tomcat 能访问到 eat 属性
原型对象prototype
在之前我们说过,通过 new 一个构造函数可以创建一个对象,那么 new 操作到底干了什么?
new 操作符干了什么
function Person(name) {
this.name = name
}
let p = new Person()
复制代码
new
操作符实际上干了以下三件事:
let p = {}
p.__proto__ = Person.prototype
Person.call(p)
复制代码
- 创建了一个空对象p
- 将这个空对象的__proto__属性指向了Person函数对象的prototype属性
- 将Person函数对象的this指针替换成p,然后再调用Person函数
也就是说我们使用new
操作符的效果就等价于上面三行代码实现的效果
这里出现了函数的prototype,那么prototype是啥呢?
prototype
JS规定:每个函数都有 “prototype” 属性,即使我们没有提供它。
默认的 “prototype” 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。
什么意思呢,我们拿下面的函数来测试一下
function Person(name) {
this.name = name
this.say = function() {
console.log('hhhh');
}
}
let p = new Person()
console.log(Person.prototype);
复制代码
输出结果:
可以看到 Person.prototype 返回的是一个对象,对象里面有 constructor
属性,这个属性指向了 Person 函数自身。 constructor
主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
好,我们来验证一下:
console.log(Person.prototype.constructor == Person); // true
// 没有问题
复制代码
知道了prototype之后,那prototype有啥用呢?
假如现在我再通过new Person()去实例一个新的对象,新的对象也会有say方法,由于函数属于复杂数据类型,这样内存就又会开辟空间去存储这个函数,这样就会造成内存浪费。而如果我们把那些不变的方法,直接定义在prototype原型上,这样所有对象的实例就可以共享这些方法,这样就可以避免内存浪费的问题。
Person.prototype.play = function() {
console.log('写文章真好玩!');
}
复制代码
上面直接在原型对象上定义一个play方法,对象实例可以直接访问到
p.play() // 写文章真好玩!
复制代码
那为什么可以访问到呢?
在前面我们说了,使用 new 操作符创建对象实例的时候,会将这个空对象的__proto__属性指向Person函数对象的prototype属性。也就是说 p.__proto__ == Person.prototype
,我们再来验证一下:
console.log(p.__proto__ == Person.prototype); // true
复制代码
可以看到结果为true,所以对象实例 p 可以通过自带的隐藏属性__proto__
找到 Person.prototype,从而访问里面的方法!
构造函数、对象实例、原型对象,三者之间的关系:
我感觉可以理解为一家三口:构造函数是母亲,原型对象是父亲,对象实例看成孩子。父母相互吸引,妈妈生了你,你没钱花了,先看看妈妈有没有,没有就默认找你爸要钱。
原型链
前面已经提到过这个概念了,下面我们再来梳理一下
function Person(name) {
this.name = name
this.say = function() {
console.log('hhhh');
}
}
let p = new Person()
console.log(p.__proto__ == Person.prototype); //true
console.log(Person.prototype.__proto__ == Object.prototype); // true
console.log(Object.prototype.__proto__); // null
复制代码
通过 __protp__
一层层往上找,发现最后为null,这也就是前面所说的JS天生就规定了一个隐藏的属性__proto__
,它要么为 null,要么就是对另一个对象的引用。
JavaScript的成员查找机制
①当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
②如果没有就查找它的原型(也就是__proto__ 指向的prototype原型对象)。
③如果还没有就查找原型对象的原型( Object的原型对象)。
④依此类推一直找到Object为止 ( null)。
另外在现代JS中,通过__proto__
查找原型已经过时了,JS官方建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf
来取代 __proto__
去 get/set 原型。
在我们阅读一些源码的时候就能经常见到这个两个方法。
总结
- 所有的对象都有一个隐藏的
__proto__
属性 - 构造函数声明就会给一个原型对象
- 构造函数通过
prototype
访问原型对象. - 对象实例可以直接访问原型对象的方法和属性.
周末花了大半天时间磕磕绊绊写的总结,新手上路,如有错误,欢迎指出,觉得有用就点个赞吧~