原型算是JS中的一个重要知识点,也是难点。我觉得,对于陌生的事物,人本能的会恐惧和排斥。对于学习也一样,如果你熟悉了,会觉得很容易,但对于复杂的未知的东西,会有种恐惧心里。以前学习JS,觉得原型继承啥的特别难,现在发现其实没有那么难,至于以后是什么感觉,那是以后的事了。

####对象和函数
通过instanceof可以判断引用类型的是什么类型的值。instanceof是通过原型链实现的,在这里先不详细讲,以后有机会在另写一篇文章介绍。通过这篇文章对原型的介绍,理解原型之后,就会明白怎么回事了。通过instanceof,我们可以知道函数是一种对象。

1
2
3
function a() {
}
a instanceof Object; //=> true

JavaScript中没有静态语言那种真正意义上的类,JavaScript中的类是由构造函数和原型实现的。我们要用构造函数来创造新的对象实例,对象实例可以从原型中继承属性和方法。在JavaScript中:

函数是一种特殊的对象。
通过构造函数能初始化新的对象实例。

通过构造函数可以创建对象实例。我们可以自定义构造函数,也可以直接用内置的原生构造函数。比如:Object,Date,Array等。通过构造函数调用生成新的对象。

1
2
const obj = new Object() //初始化一个新对象
const arr = new Array() //初始化新数组

我们自定义的构造函数,函数名习惯上是要大写,这是为了表明,这是一个构造函数,你可以通过我创建对象。

1
2
3
4
5
6
7
8
9
function Location(x, y) {
this.x = x;
this.y = y;
area: function() {
return this.x * this.y;
};
}
const loc = new Location(1, 2)
loc.area() // =>2

每次调用Location,Location都会把函数体初始化成一个对象。注意,并没有return。

如果构造函数里有return,这就分两种情况:

1.return返回的是一个对象,那么return返回的这个对象就是我通过调用构造函数得到的新建对象。原来以函数体初始化的对象被抛弃了。
2.return返回的是一个简单类型值。那么return相当于不存在过一样。新建的对象还是刚才函数体初始化的那个。

通过以下例子就可以简单说明什么意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Loc(x) {
this.x = x
}
const l = new Loc(2);// => l是:{ x : 2 }

//加了return之后
function Loc1(x) {
this.x = x;
return { y: 1};
}
const l1 = new Loc1(2)// => l1是:{ y: 1 }

//加了return之后,返回基本类型值
function Loc1(x) {
this.x = x;
return 1;
}
const l1 = new Loc1(2)// => l1是:{ x: 2 }

####从浏览器中的__proto属性说起
首先,咱们打开浏览器控制台,输入:a = { x: 2 },然后按回车。你会发现除了对象本身的属性x外,还有一个**proto**属性,这个**proto
指向一个对象。那这个**proto__为啥无缘无故出现在你的对象里呢?

每个对象里都有一个内置的[[prototype]]属性,这个属性指向对象的原型。用两个方块号表示这个属性是内置隐藏的,我们不能直接用代码.prototype获取。

浏览器实现的__proto__属性,会引用[[prototype]]属性。但它并不是一个标准属性。它其实是浏览器实现的。[[prototype]]会指向构造函数的prototype属性,那个属性就是原型对象。比如通过Object创建一个新对象,那么Object.prototype就是新对象的原型对象(可以简称原型)。

####原型链
原型对象本身也有原型属性[[prototype]](浏览器里是__proto__),它也指向它的原型对象,可以从原型对象继承属性和方法。而原型的原型也可能还有原型对象。这样就构成了一条继承链,叫做原型链。在末端的对象,可以继承和共享整条原型链上的属性和方法。对象的toString()方法就继承自Object.prototype。所有对象都有toString()方法,就是因为实例对象可以共享原型链上的属性和方法。

1
2
3
4
5
6
7
function Exam() {
}
Exam.prototype.x = 2;
const ex = new Exam()
console.log(ex.x); // => 2,继承来的x
ex.x = 1;
console.log(ex.x) // => 1,自身同名属性会覆盖掉继承属性。

对于原型对象的属性,只能继承来用。不能修改,如果自身有同名属性,会覆盖原型对象里的属性。

上面例子的原型链是:

ex => Exam.prototype => Object.prototype => null

所有对象都继承自Object.prototype, 所有对象都有原型,处于最顶端的Object.prototype也有原型,只不过是null而已(就是没有了??)。

####Object.create()
ES5开始就有了一个Object.create()方法,这个方法传入一个对象(null也行),创建一个新对象。传入的对象就是新对象的原型对象。

1
2
3
4
const obj = Object.create({x:11})
console.log(obj.x) // =>11, 继承自原型对象

const obj2 = Object.create(null)//这个对象的toString()等各种继承自Object.prototype的属性都没了。因为null是它的原型,而null啥也没有。原型链就是:obj2 => null

除了__prototype

虽然浏览器实现了proto__属性,但它始终不是标准。ES5中可以通过标准的方法Object.getPrototypeOf()获取属性[[prototype]]:

1
Object.getPrototypeOf(obj)   //获得obj的原型对象

ES6中还有一个setPrototype(obj)方法可以设置对象的原型。传入的参数就是原型。
也可以通过isPrototypeOf()判断一个对象是不是继承自另一个对象。

1
2
3
const p = {x:1}
const o = Object.create(p)
p.isPrototypeOf(o) //true, o 继承自p

参考文献

《JavaScript权威指南》
《JavaScript高级程序设计》
《深入理解ES6》