对原型的引入
在每一个JavaScript对象里都会内置一个特殊的属性[[Prototype]],这个属性在几乎所有对象创建的时候都会被赋予一个非空值。假设我们开始创建了这样的一个对象:
1 | var obj = { |
但是如果在obj中没有name这样的一个属性,很明显我们就无法访问这个了。如果还有需要,就要引入原型链。比如下面的这个例子:
1 | var obj = { |
在这个例子里面虽然obj1并没有设置name这个属性,但是通过创建的时候通过Object.create这个方法我们将他的原型设置成了obj,从而之后js在寻找的时候,发现obj1没有这个属性,就会去他的原型链上查找。直到把原型链遍历完为止。
prototype和constructor
一个例子
刚刚提到,在我们建立一个对象的时候,会默认生成他的一个原型prototype,当我们实例化对象的时候,这个实例化的对象中有一个属性__proto__
会指向原型prototype,原型中有一个属性叫做constructor ,他会指向创建这个对象的函数。比如我们来看下面的这个例子:
1 | function Person() { |
我们用一张图来表示上述过程:
分析
首先我们利用function创建了一个Person类,这个类中name是直接赋值得到,而height用了this声明。这个类创建好后,会为我们自动创建一个原型Person.prototype,这个原型中只有一个属性constructor,它是指向Person类的构造函数,所以如果我们输出Person.prototype.constructor,会输出[Function: Person]。如果我们试图去访问Person.prototype.height,将会输出undefined。之后我们又在他的属性里面添加了一个age属性。
之后我们创建了一个实例化的对象p1,创建实例化对象的时候系统会自动为我们生成一个属性__proto___
,指向new这个实例的构造函数的原型对象,这里就是Person.prototype。当我们访问name的时候,由于name虽然是Person的一个属性,但是并没有用this声明,所以p1并不拥有这样的一个属性,即使到他的原型中也不存在,所以就会是undefined。当访问height搭的时候,由于在实例化对象创建的时候就有,所以能够正常输出。访问age的时候,虽然p1中并没有这样的一个属性,但是js会向上寻找他的原型Person.prototype,这里有这样的一个属性age,就会将其输出。
过程总结
所以对这几个重要的概念进行梳理:
prototype:构造函数中的属性,指向该构造函数的原型对象。
constructor:原型对象中的属性,指向该原型对象的构造函数
__proto__
:实例中的属性,指向new这个实例的构造函数的原型对象
在我们var p1 = new Person()的时候,实际发生了下面的过程:
1.var p1 = new Object(); //此时p1.proto = Object Prototype
2.p1.
__proto__
= Person.prototype;3.Person.call(p1);//使用新对象p1调用函数Person,将this作用域给p1
原型链
如果在上述例子中,我们将代码做下面的处理,会发现一些不一样的结果:
1 | function Person() { |
这里,我们将Person.prototype.age = ‘18’改为用对象的形式单独写出,但会发现,输出结果中Person.prototype.constructor变成了[Function: Object],说明现在,Person.prototype.constructor指向已经不再是Person了。这是由于如果我们将一个Person.prototype单独附一个对象,实际上他的constructor已经指向了Object的构造函数,他还多了一个__proto__
指向了Object.prototype。整个原型链如下所示:
但如果这个时候,我们有需求将construct指向Person,那么这时候就需要用到下面的这种定义方式:
1 | Object.defineProperty(Person.prototype,"constructor",{ |
instanceof
在java中,通过instanceof可以检查左边的类是否和右边的类属于同一个子类。在JavaScript中,这种判别机制则是判断左右两个类是否在同一个原型链上。因此,我们可以通过instanceof来判断原型链上的关系。如s instanceof Person可以判断Person是否在s的原型链上。
类的继承
实现类的继承有以下几种方式,但是有的会有些意想不到的缺陷:
组合继承
通过结合原型链和构造函数,我们可以实现组合继承。通过构造函数实现每个类的特有属性,通过原型链是他们拥有共同的方法,实现方法的复用。
1 | function Person(name){ |
这种方式有下面几个缺点:
1.会导致两次调用超类的构造函数,一次在Person.call,另一次在Student.prototype = new Person()。
2.两次调用构造函数会导致Person的原型上有name和age,在stu实例上也有name和age。
原型式继承
1 | function Person(name){ |
这种方式是通过Object.create来实现构建原型链,其中Object.create(参数1,参数2); 参数1用作新对象的原型对象,参数2为新对象定义额外属性的对象。值得注意的是,这里有下面几种常见的错误写法:
Student.prototype = Person.prototype
这里,Student.prototype不会单独创建一个新的内存,而是直接引用了Person.prototype,如果在这个时候我们改了Student.prototype的相关属性,Person.prototype也会被直接修改,故不能这样做。
第二种错误是Student.prototype = new Person(),这里,Student.prototype使用了Person的构造函数,后期会产生一些副作用,如果改变Person中的状态,会影响Student后代。
不过在ES6之后我们可以把Student.prototype = Object.create(Person.prototype)改写成Object.setPrototypeOf(Student.prototype,Person.prototype),注意这里要对Student.prototype.constructor进行校正。
寄生组合式继承
这个方式是针对第一种方式的缺点进行的改造,实际上就是先获取父类的相关属性,再定义一个函数专门用于实现继承并修正的作用,具体实现如下所示:
1 | //寄生组合式继承 |