js原型和原型链

对原型的引入

​ 在每一个JavaScript对象里都会内置一个特殊的属性[[Prototype]],这个属性在几乎所有对象创建的时候都会被赋予一个非空值。假设我们开始创建了这样的一个对象:

1
2
3
4
var obj = {
name:'liming'
}
console.log(obj.name)

​ 但是如果在obj中没有name这样的一个属性,很明显我们就无法访问这个了。如果还有需要,就要引入原型链。比如下面的这个例子:

1
2
3
4
5
var obj = {
name:'liming'
}
var obj1 = Object.create(obj)
console.log(obj1.name)

​ 在这个例子里面虽然obj1并没有设置name这个属性,但是通过创建的时候通过Object.create这个方法我们将他的原型设置成了obj,从而之后js在寻找的时候,发现obj1没有这个属性,就会去他的原型链上查找。直到把原型链遍历完为止。

prototype和constructor

一个例子

​ 刚刚提到,在我们建立一个对象的时候,会默认生成他的一个原型prototype,当我们实例化对象的时候,这个实例化的对象中有一个属性__proto__会指向原型prototype,原型中有一个属性叫做constructor ,他会指向创建这个对象的函数。比如我们来看下面的这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {
name = 'Person';
this.height = '160cm';
}
//在其原型对象中添加age属性
Person.prototype.age = '18';
console.log(Person.prototype.name)
console.log(Person.prototype.constructor)
//Person的实例p1
var p1 = new Person();
console.log(p1.age)
console.log(p1.name)
console.log(p1.__proto__)

​ 我们用一张图来表示上述过程:

你想要输入的替代文字

分析

​ 首先我们利用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
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person() {
name = 'Person';
this.height = '160cm';
}
Person.prototype = {
age:18
}
console.log(Person.prototype.name)
console.log(Person.prototype.constructor)
//Person的实例p1
var p1 = new Person();
console.log(p1.age)
console.log(p1.name)
console.log(p1.__proto__)

​ 这里,我们将Person.prototype.age = ‘18’改为用对象的形式单独写出,但会发现,输出结果中Person.prototype.constructor变成了[Function: Object],说明现在,Person.prototype.constructor指向已经不再是Person了。这是由于如果我们将一个Person.prototype单独附一个对象,实际上他的constructor已经指向了Object的构造函数,他还多了一个__proto__指向了Object.prototype。整个原型链如下所示:

你想要输入的替代文字

​ 但如果这个时候,我们有需求将construct指向Person,那么这时候就需要用到下面的这种定义方式:

1
2
3
4
5
6
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
writable:true,
configurable:true,
value:Person
})

instanceof

​ 在java中,通过instanceof可以检查左边的类是否和右边的类属于同一个子类。在JavaScript中,这种判别机制则是判断左右两个类是否在同一个原型链上。因此,我们可以通过instanceof来判断原型链上的关系。如s instanceof Person可以判断Person是否在s的原型链上。

类的继承

​ 实现类的继承有以下几种方式,但是有的会有些意想不到的缺陷:

组合继承

​ 通过结合原型链和构造函数,我们可以实现组合继承。通过构造函数实现每个类的特有属性,通过原型链是他们拥有共同的方法,实现方法的复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name){
this.name = name;
this.age = 18
}
Person.prototype.sayName = function(){
return this.name;
}
function Student(name,num){
Person.call(this,name)
this.num = num;
}
Student.prototype = new Person();
console.log(Student.prototype.constructor)
Student.prototype.constructor = Person
var stu1 = new Student('liming',12)

​ 这种方式有下面几个缺点:

​ 1.会导致两次调用超类的构造函数,一次在Person.call,另一次在Student.prototype = new Person()。

​ 2.两次调用构造函数会导致Person的原型上有name和age,在stu实例上也有name和age。

原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name){
this.name = name;
this.age = 18
}
Person.prototype.sayName = function(){
return this.name;
}
function Student(name,num){
Person.call(this,name)
this.num = num
}
Student.prototype = Object.create(Person.prototype)
console.log(Student.prototype.constructor)
var s = new Student('liming',16)
console.log(s.age)
console.log(s.sayName())

​ 这种方式是通过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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//寄生组合式继承
function inheritProto(Sub, Super){
//根据Super的原型创建一个新的对象proto
var proto = Object(Super.prototype);
//增强新对象,为其赋construtor值
proto.constructor = Sub;
//将新对象赋值给子类的原型。
Sub.prototype = proto;
}
//使用
//Super中定义属性name
function Super(name){
this.name = name;
this.color = ['red','green'];
}
//Super的原型中定义方法
Super.prototype.sayname = function(){
return this.name;
}
function Sub(name, age){
//通过构造函数的方式继承Super的属性,只在此处调用一次Super构造函数
Super.call(this, name);
//定义自己的属性
this.age = age;
}
//调用函数,实现继承。代替之前的Sub.prototype = new Super();语句,防止Super构造函数调用两次
inheritProto(Sub,Super);
var s = new Sub('liming',18)
console.log(s.sayname())
0%