本文主要复习原型和原型链 ,希望能够对其有更深的认识,此外并会向外延申,探讨JavaScript中的继承方式!
构造函数和方法
在谈原型和原型链之前,我们首先聊一聊构造函数和方法的区别!
我认为这是一个理论概念上的不同,我们由浅入深逐步理解:
- 在传统的面向对象编程的Java中,我们如何创建一个对象(以下所涉及的对象均指面向对象编程中对象的含义)?
- 写一个类,确定其成员变量、构造函数,并为其添加方法。
- 然后
new
出来,我们就会得到一个该类的对象。
- 但是在JavaScript中,早期是没有
class
关键字的,那我们是如何创建一个对象的呢?- 使用函数(当然不可以是箭头函数),利用
this
关键字为函数中的成员变量以及方法进行初始化。 - 然后
new
出来,我们就能得到一个构造函数为该函数的对象。
- 使用函数(当然不可以是箭头函数),利用
js
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log(this);
console.log('Hello I am ' + this.name);
}
}
因此在JavaScript中,构造函数和方法应该区分开来:
- '构造函数'的概念可以理解为一个类,在你执行'构造函数'时(
new
)来实例化一个对象。 - 方法的概念应该就是处理某些业务逻辑的函数。
再想一想:
- 在Java中一个类写好了,他就放在那里,你不
new
他,他就只是一个文件、一串代码。当你真正实例化他的时候,他在在内存中开辟一块空间来存储一个实例。(这里的表达并不准确,并不是文件/代码,也会被初始化赋予一些信息)- 在JavaScript中其实也差不多,如上所述的代码,我不
new
他是什么?了解V8的同学就能做出回答,尽管对整个函数进行了解析和编译(生成了作用域等信息)并存储在堆内存中了,但是他并没有被调用,他就是静静的放在那而已。
- 在JavaScript中其实也差不多,如上所述的代码,我不
因为早期Javascript设计之初就是宣称面向对象,但是由于各种原因并不像Java那样强类型,有明确的类概念。为了解决这个问题,JavaScript的设计师引入了原型和原型链的概念来解决这个问题。
原型和原型链
原型 :prototype
是一个对象,他存储了当前这个实例对象的构造函数;prototype
只存在于函数中
console.log(Person.prototype)
==>
根据这个规定,我们可以知道Person === Person.prototype.constructor
;
原型链 :是一个已经被构造的对象中可以逐步找到构造该对象以及构造该对象的对象...这样的一条链路
这个链条是哪来的?在JavaScript中,每个对象都有__proto__
这个属性,这个属性也是一个对象 ,存放的就是当前对象原型 ===> 实例对象的__proto__ === 构造函数的prototype
js
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log(this);
console.log('Hello I am ' + this.name);
}
}
Person.prototype.live = true;
const p = new Person('ys', 18);
console.log(p, p.__proto__ === Person.prototype, Person.prototype);
上述代码中我们使用Person.prototype.live = true;
在构造函数Person的原型上增添了一个属性live
。 那么我们就应该在Person的实例p上,通过__proto__
找到这个属性,结果也是如此:
我们再向下分析一层:
框住的这个是什么? 他也是通过__proto__
获取的,那么他一定是谁
的原型,之前我们说过原型prototype
是一个对象,那么他就应该是对象(Object)的__proto__
;
答案也确实如此:p.__proto__.__proto__ === Object.prototype
总结一下:
- 原型
prototype
是函数的一个属性,其中包括了这个函数的构造函数以及这个函数所拥有的属性和方法 __proto__
是对象的一个属性,它指向该对象的原型,由一层一层的__proto
所连接的就是原型链
继承
思考一下原型链有什么作用呢?我们可以通过原型链逐步找到这个对象构造函数的方法/属性。有点继承 的意思。实际确实如此,自从ES6之后,JS引入了class
关键字,能够让我们更好的进行面向对象编程:
js
class Person {
name;
age;
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log('Hi I am ' + this.name);
}
}
class Student extends Person {
grade;
constructor(name, age, grade) {
super(name, age);
this.grade = grade;
}
study() {
console.log(this.name + " is study in grade " + this.grade);
}
}
对了,要提一句的是,每个类中的方法都将存在于这个类的prototype
上;
执行如下代码得到结果:
js
const me = new Student('ys', 18, 3);
console.log(Student.prototype, me);
Student.prototype
是Student的原型,包含方法以及构造函数
js
console.log(Student.prototype.__proto__ === Person.prototype); //true
console.log(me.__proto__ === Student.prototype); //true
//原型链体现在此处,父类的方法/属性可以在__proto__组成的链上找到!
console.log(me.__proto__.__proto__ === Person.prototype) //true
至此原型和原型链我们应该已经了解的差不多了,你应该已经可以自己进行一些分析了。
扩展
除了上述所说的以外,还有两个关注点:
Function
和Object
;- Javascript中继承的方法;
Function
和 Object
JS中有这样两个函数:Function Object
我们常说JS中一切都是对象,并且我们在打印出一个对象时总是可以一直展开,这些都是为什么呢?
打印结果是两个函数,说明他们肯定都有prototype
。
我们使用console.dir()
将Function
和 Object
都打开打印出来看看:
神奇的事情发生了: 他俩都有prototype
和__proto__
,在这里我们不能再利用之前说的prototype
都在函数上,__proto__
都在对象上了,因为他俩已经足够底层,再遵从这些规定就不能满足设计需求了,因此我们应该特殊的看待他们。
直接看图
Function.prototype === Function.__proto__ === Object.prototype
说实话,这里我还没有很是理解,感觉还是有点抽象,他这样设计的意义是什么,欢迎大家在评论区讨论。
下图是JS部分内建对象的引用:
Javascript中继承的方法
经过上面的学习,我们实际上可以发现:JS中的继承实际上就是子类的原型的__proto__
指向父类的原型
但是在JS中又拥有很多种继承方式:
- 原型链继承
- 原型式继承
- 组合继承
- 构造函数继承
- 寄生式继承
- 寄生组合继承
拥有这么多方法是因为可能有些情况我们不希望继承的属性被子类修改。有时候又不需要继承方法,有时候要共享方法/属性....
这些看到我都头大了,要我说class
就够了,真到什么时候需要进行性能优化再想这些吧。
但是如果面试要问那真没办法,不过我觉得思想是最重要的,JS中的继承就是前面说的prototype
,我们只要想办法把他维护好应该就能达到预期的继承效果了。
如果有必要,我后面会将这里完善🤩