在基于类的语言中,对象是类的实例,并且类可以从另一个类继承。JavaScript是一门基于原型的语言,这意味着对象直接从其他对象继承。
JavaScript提供了一套极为丰富的代码重用模式,能够实现继承的方法有很多。在了解这些方法之前,首先要对"对象"这个概念具备一定认知
对象
在JS里,万物皆对象。而__proto__是每个对象都有的属性。
__proto__属性都是由一个对象指向一个对象,它的作用是当访问一个对象的属性不存在时,就会去它的__proto__属性所指向的那个对象里找,如果仍然不存在这个属性,则继续寻找,直到顶端为null时结束,这样形成的一条链即所谓的原型链。
let a={name:'haha'};
let b={};
b.__proto__=a;
console.log(b.name);
在js中,有多种生成对象的方式。不同方式生成的对象其proto指向也不同,下面给出三种最常用的生成对象的方式以及各自的proto指向
var a={};
var result = a.__proto__===Object.prototype;
console.log(result);
function A(){};
var a=new A();
var result=a.__proto__===A.prototype;
console.log(result);
var a1={}
var a2=Object.create(a1);
var result = a2.__proto__===a1;
console.log(result);
实例和原型
在对js中的’对象’具备一定的认知之后,接下来需要了解的就是实例和原型的概念了,不论ES5还是ES6,都有原型属性和实例属性的概念,但是ES5的表述更加直观,因此我们首先以ES5为例,简要介绍什么是原型属性,什么是实例属性
function A() {
this.x = 10;
this.printX = function () {
console.log(this.x);
}
}
A.prototype.y = -10;
A.prototype.printY = function () {
console.log(this.y);
}
let a = new A();
console.log(Object.getOwnPropertyNames(a));
console.log(Object.getOwnPropertyNames(A.prototype));
a.printX();
a.printY();
let aa = new A();
aa.__proto__.y = -20;
a.printY();
ES6加入了类的概念,反而模糊了实例和原型的界限,但这种区别依旧存在
class A{
constructor() {
this.aaa = 'aaa';
}
foo() {
console.log('foo');
}
bar = () => {
}
}
class B extends A{
constructor() {
super();
}
}
class C extends B{
constructor() {
super();
}
fun() {
super.foo();
}
}
let a = new A();
let b = new B();
let c = new C();
console.log(Object.getOwnPropertyNames(A.prototype));
console.log(Object.getOwnPropertyNames(a));
console.log(Object.getOwnPropertyNames(B.prototype));
console.log(Object.getOwnPropertyNames(b));
console.log(Object.getOwnPropertyNames(C.prototype));
console.log(Object.getOwnPropertyNames(c));
c.foo();
c.fun();
箭头函数
这里对箭头函数做个简单介绍,重点对比箭头函数和绑定this的区别
class A{
constructor() {
this.x = 100;
this.bar = this.bar.bind(this);
}
foo() {
}
bar() {
}
temp = () => {
}
}
let a = new A();
console.log(Object.getOwnPropertyNames(A.prototype));
console.log(Object.getOwnPropertyNames(a));
在上面的例子中,class A里面一共定义了3个方法,其中foo和bar被放在了原型属性中,而箭头函数temp被视为实例属性。这里我们对bar方法进行了绑定this的操作,通过日志可以发现,bar方法和temp一样,也存在于实例a当中,换句话说,绑定this的操作使得bar方法能够同时存在于原型属性和实例属性当中
下面举一个绑定this的具体使用环境
class A {
constructor() {
this.x = 100;
}
foo() {
let temp = {
x: 6,
print: this.printX
}
temp.print();
}
bar() {
this.printX();
}
printX() {
console.log(this.x);
}
}
let a = new A();
a.printX();
a.bar();
a.foo();
通过上面的例子可以看出,当foo方法没有进行绑定this时,其this的指向会和我们所预期的不一致。这里使用绑定this或箭头函数都可以解决这个问题。
继承的实现
其实上面的内容已经对继承有了部分介绍,这里重点比较ES5和ES6在继承使用上面的差别
ES6中采用了大家更容易理解的class来实现继承,但实际上class只是构造函数的语法糖而已,class本身也是一个函数
class Person{};
let result = typeof Person ==='function';
console.log(result);
ES5中继承的实现(方式之一:原型链继承)
function Father(){
this.name='father';
}
function Child(){
this.id='001';
}
Child.prototype=new Father();
let child=new Child();
console.log(child.name);
console.log(child.id);
如果把构造函数比做类的话,Child类通过将prototype属性设置为Father类的实例来实现继承。
ES6中继承的实现
class Father{
constructor(){
this.name='father';
}
}
class Child extends Father{
constructor(){
super();
this.id='001';
}
}
let child=new Child();
console.log(child.name);
console.log(child.id);
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
原型链继承(同上)
function Father(name){
this.name=name;
this.printName=function(){
console.log(this.name);
}
}
Father.prototype.age=40;
function Child(){
this.name='child';
}
Child.prototype=new Father('father');
let result = Child.prototype.__proto__===Father.prototype;
console.log(result);
let child = new Child();
console.log(child);
console.log(child.age);
console.log(child instanceof Father);
console.log(child instanceof Child);
console.log(child.__proto__);
子类的prototype由于传入了父类的实例,使得子类实例继承了父类的全部属性(构造函数属性和prototype中的属性),如果子类实例child想要添加新的属性或者重写原有属性,则需要在子类构造函数Child中定义。
缺点:1、新实例无法向父类构造函数传参。 2、继承单一,没有实现多继承。 3、原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
let child2 = new Child();
child.__proto__.age=10;
console.log(child2.age);
构造函数继承
function Father(name){
this.name=name;
this.printName=function(){
console.log(this.name);
}
}
Father.prototype.age=40;
function Child(){
Father.call(this);
this.name='child';
}
let child = new Child();
console.log(child);
console.log(child.age);
console.log(child instanceof Father);
console.log(child instanceof Child);
将父类构造函数中的属性直接添加给子类的构造函数,实现了多继承,当生成子类实例时,该实例包含了所有父类构造函数中定义的全部属性
缺点:1、只能继承父类构造函数的属性,父类的原型属性无法获取(因为从子类prototype到父类prototype的原型链不存在。 2、无法实现父类构造函数的复用,每次创建子类实例都要重新调用父类的构造函数。这导致每个新实例都有父类构造函数的属性副本,比较臃肿。
组合继承
function Father(name){
this.name=name;
this.printName=function(){
console.log(this.name);
}
}
Father.prototype.age=40;
function Child(name){
Father.call(this,name);
}
Child.prototype=new Father('father');
let child = new Child('child');
console.log(child);
console.log(child.age);
console.log(child instanceof Father);
console.log(child instanceof Child);
组合继承结合了前面两种模式的特点,但是调用了两次父类的构造函数,造成了内存上的损耗
寄生组合继承
function Father(name){
this.name=name;
this.printName=function(){
console.log(this.name);
}
}
Father.prototype.age=40;
Father.prototype.printAge=function(){
console.log(this.age);
}
function content(obj){
function Foo(){};
Foo.prototype=obj;
return new Foo();
}
let father=content(Father.prototype)
function Child(){
Father.call(this,'child');
}
Child.prototype=father;
Child.prototype.constructor=Child;
let child=new Child();
console.log(child);
console.log(child.age);
console.log(child instanceof Father);
console.log(child instanceof Child);
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法和属性,避免了组合继承的缺点
|