再来说Javascript中的原型

2016-12-22

toString是从哪来的

先来看一段代码

1
2
var obj = {};
console.log(obj.toString()); // 结果是:"[object Object]"

这段代码很简单,一个对象调用了toString方法,但是如果我问你为什么可以调toString方法?toString是从哪来的?

对于大多学过几天js的人知道,因为obj是继承了Object对象,而toString是Object对象的方法,所以obj可以调toString

那么很明显,这个回答是正确的错误答案,因为关键字没有说到:原型

原型是什么?

首先,原型是一个普通的对象。

为什么说是普通,说明一定有不普通的对象,比如function类型的对象,它就属于不太普通的对象,这个后面说

在js的世界里,一个普通对象是怎么样

  1. 对象上可以定义方法或属性
  2. 对象一定有一个原型
  3. 对象的原型上的方法和属性,对象本身可以通过.语法获取,就像调用自己的方法和属性一样
  4. 对象的原型是指向其构造函数的prototye属性的指针(引用)
  5. 对象本身的方法或属性会覆盖掉原型上的方法或属性

一个对象一定有一个原型,而原型又是一个对象,那么原型上就会有原型,这个原型依旧是对象,而这个对象依旧有原型,这样不停往上追溯,直到js的根对象Object.prototype,这个就叫原型链

实际上每个对象都有一个存放原型的内部属性 [[prototype]](实际上看不到,ecma的标准里是这么描述的), 而在浏览器环境中,提供了一个叫__proto__的属性去访问它,下面就通过__proto__去看看原型链是是怎么链起来的

先来看一个空对象的原型是怎样的

1
2
3
var obj = {}; // 一个空对象,等同于 new Object()

obj.__proto__ === Object.prototype; // true

obj.__proto__是obj对象原型,Object是obj对象的构造函数,
对象的原型是指向其构造函数的prototye属性的指针,所以结果是true

1
obj.__proto__.__proto__ === null;		// true

Object.prototype是最基本的原型,但是它仍然是个对象,所以,它还是有原型的,它的原型一个空对象null,js就是这么定义的,这里不必深究

上面这个例子因为构造函数就是Object,所以直接就到Object.prototype,没有表现出原型链的样子,下面进入正题

原型链

通常我们使用的对象并不是使用Object这个构造函数生成的,而是我们自己定义的构造函数,构造函数其实就是函数,而函数其实也是对象,就是前面说的那个不太普通的对象。

函数对象上拥有一个特殊的属性prototype,其实前面已经有写到(Object.prototype),它就是用来定义存放原型的地方

js中,因为没有类的概念,所以是构造函数承担类的功能,去定义对象的属性和方法,去实现继承,下面的代码是三个构造函数继承的关系

Tips继承了Dialog,

Dialog继承了Modal,

Modal并没有说明继承了什么,实际上就是继承了Object,通过这种具有继承关系的构造函数,我们可以清晰的看出原型链的形成。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Modal 是弹窗,有宽,高和内容
* Dialog 是对话框, 有宽高,内容 还有一个按钮
* Tips 是一个提示对话框, 有宽高,内容, 按钮,还有一个标题栏
*/

function Modal(height, width, content) {
this.height = height;
this.width = width;
this.content = content;
}

Modal.prototype.open = function () {
console.log('width:' + this.width + ' ,height:' + this.height + ' ,content:' + this.content);
};

function Dialog(height, width, content, button) {
Modal.apply(this, arguments);
this.button = button;
}
Dialog.prototype = Object.create(Modal.prototype);
Dialog.prototype.constructor = Dialog;
Dialog.prototype.showButton = function () {
console.log('button:' + this.button)
};

function Tips(height, width, content, button, title) {
Dialog.apply(this, arguments);
this.title = title;
}
Tips.prototype = Object.create(Dialog.prototype);
Tips.prototype.constructor = Tips;
Tips.prototype.showTitle = function () {
console.log('title:' + this.title)
};

/* ================================================================================ */

var tips = new Tips(100,200,'233333','ok','warn');

console.log(tips.__proto__ === Tips.prototype); // true
console.log(tips.__proto__.__proto__ === Dialog.prototype); // true
console.log(tips.__proto__.__proto__.__proto__ === Modal.prototype); // true
console.log(tips.__proto__.__proto__.__proto__.__proto__ === Object.prototype); // true
console.log(tips.__proto__.__proto__.__proto__.__proto__.__proto__ === null); // true

tips对象的原型就是其构造函数Tips的属性prototype定义了一个对象,这个对象定义了constructor指向Tips,定义了showTitle方法;

同时通过create方法继承了构造函数Dialog的原型(Dialog.prototype),得到了showButton方法;

Dialog又继承了Modal的原型(Modal.prototype),所以得到open方法;

Modal继承Object的原型(Object.prototype),所以得到下面列一个Object原型上的属性和方法

  • __defineGetter__
  • __defineSetter__
  • hasOwnProperty
  • __lookupGetter__
  • __lookupSetter__
  • propertyIsEnumerable
  • constructor
  • toString
  • toLocaleString
  • valueOf
  • isPrototypeOf
  • __proto__

由于对象的原型上的方法和属性,对象本身可以通过.语法获取,所以

1
2
3
4
tips.open();	// width:200 ,height:100 ,content:233333
tips.showButton(); // button:ok
tips.showTitle(); // title:warn
tips.toString(); // [object Object]

优先级问题

如果在Tips上定义的属性和原型链上的重复?

1
2
3
Tips.prototype.toString = function() { console.log('new toString')}

tips.toString(); // new toString

原型链底下的会覆盖掉原型链顶上的的属性或方法。这也是很合理的

优先级最高的则是对象本身的的属性,即对象本身的方法或属性会覆盖掉原型上的方法或属性

当一个对象调用一个属性或方法时,js解释器会先在当前对象上扫描是否有该属性,如果没有,才会到这个对象的原型上找,如果,还是没有,就到原型的原型上找,最后,直至Object.prototype,还是没有的话,就返回undefined