# 原型
原型到底是什么?为什么会有原型、原型链呢?原型链有什么作用?
# 由来
在很早之前,我们的网页是很简陋的,也没有交互作用。即使用户填写的表单里的密码格式错误,也只能提交请求,由服务器端判断。如果不正确,那么用户就需要把表单的内容重填一遍。所以浏览器公司——网景,很需要一种能和网页互动的脚本语言。网景公司让工程师 Brendan Eich 负责开发这种新语言。网景公司做出决策,未来的网页脚本语言必须"看上去与 Java 足够相似",但是比 Java 简单,使得非专业的网页作者也能很快上手。这个决策实际上将 Perl、Python、Tcl、Scheme 等非面向对象编程的语言都排除在外了。但 Brendan Eich 的主要方向和兴趣是函数式编程,他对 Java 一点兴趣也没有。为了应付公司安排的任务,他只用 10 天时间就把 Javascript 设计出来了。
他在设计的时候,觉得这种语言只要能够完成一些简单操作就够了,没必要设计得很复杂。受到 Java 的影响,Javascript 里都是对象。如果真的是一种简易的脚本语言,其实不需要有 “继承” 机制。但是,Javascript 里面都是对象,必须有一种机制,将所有对象联系起来。所以 Brendan Eich 最后还是设计了 “继承”。但是,他不打算引入 “类”(class) 的概念,因为一旦有了 “类” ,Javascript 就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。所以便借鉴 Self 语言,使用基于 原型(prototype) 的继承机制。
所以,Javascript 语言实际上是两种语言风格的混合产物——(简化的)函数式编程 加上 (简化的)面向对象编程。这是由 Brendan Eich (函数式编程) 与 网景公司(面向对象编程) 共同决定的。
参考
上面内容摘选以下文章的一部分,建议读者完整阅读以下文章:
# 类与对象
面向对象编程的语言,最重要的就是对象、类、继承等概念。对象是某个类的实例。我们可以拿 Java 来简单举例:
class People {
// 名
public String givenName;
// 姓
public String surname;
// 构造函数
public People(String givenName, String surname) {
this.givenName = givenName;
this.surname = surname;
}
}
// 实例化->对象
People p1 = new People("Y", "XP");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在没有类概念的 Javascript,如何表达面向对象呢?Brendan Eich 想到 C++ 和 Java 使用命令时,都会调用 “类” 的构造函数。他就做了一个简化的设计,在 Javascript 语言中,new
命令后面跟的不是 “类”,而是 构造函数。比如:
// 构造函数
function People(surname, givenName) {
this.surname = surname;
this.givenName = givenName;
}
// 实例化->对象
var p1 = new People("Y", "XP");
2
3
4
5
6
7
8
# 显式原型
对象不仅有自己的属性,其实还有一系列共享方法或属性。例如:
class People {
// 名
public String givenName;
// 姓
public String surname;
// 构造函数
public People(String givenName, String surname) {
this.givenName = givenName;
this.surname = surname;
}
// 英文名
public String getNameEN() {
return this.givenName + ' ' + this.surname;
}
// 中文名
public String getNameZH() {
return this.surname + this.givenName;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
提示
这里暂时不讲解 Java 静态属性和方法,与之相对应的 Javascript 也是。
如果 Javascript 还是按照下面这种方式来实现上述功能,那么每次实例化一个对象,就会创建一次getNameEN
、getNameZH
方法,这会浪费大量内存。好比一间宿舍里,只需要一台洗衣机,而不是每位舍友单独配一台洗衣机。如果一间宿舍有 10 个人住,就需要 10 台洗衣机,那么宿舍岂不是挤爆了。
// 构造函数
function People(surname, givenName) {
this.surname = surname;
this.givenName = givenName;
this.getNameEN = function() {
return this.givenName + " " + this.surname;
};
this.getNameZH = function() {
return this.surname + this.givenName;
};
}
// 实例
var p1 = new People("Y", "XP1");
var p2 = new People("Y", "XP2");
// 是否共享? -> 内存地址是否一致?
console.log(p1.getNameEN === p2.getNameEN); // false
console.log(p1.getNameZH === p2.getNameZH); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如何才能使实例化出的对象共享一个方法或属性?于是 Brendan Eich 就引入了 prototype 机制,为函数设置一个prototype
属性,这个属性值是一个对象 (显式原型对象),可以把共享的方法或属性放在这个对象里面。例如:
// 构造函数
function People(surname, givenName) {
this.surname = surname;
this.givenName = givenName;
}
People.prototype.getNameEN = function() {
return this.givenName + " " + this.surname;
};
People.prototype.getNameZH = function() {
return this.surname + this.givenName;
};
// 实例
var p1 = new People("Y", "XP1");
var p2 = new People("Y", "XP2");
// 是否共享? -> 内存地址是否一致?
console.log(p1.getNameEN === p2.getNameEN); // true
console.log(p1.getNameZH === p2.getNameZH); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这样就能做到所有实例化出的对象就能共享一个方法了,共享属性同理。我们也可以打印出原型对象看看:
// 显式原型对象
console.log(People.prototype);
2
结果如下:
你会发现多了个constructor
和__proto__
两个属性,其实constructor
指向的是构造函数。我们可以画个图:
至于__proto__
这个属性,你看到后面就知道了,所以图中就不画具体指向了,避免思路混乱。
总结: 构造函数 的prototype
(显式原型) 会指向一个对象 (原型对象),该对象包含共享方法和属性,同时对象中的constructor
指向 构造函数。
注意
可以说每个函数都有prototype
,但还有一种特殊情况除外,就是通过Function.prototype.bind
方法构造出来的函数,它是没有prototype
属性的。可以查看规范 (opens new window)的 15.3.5.2 小结。
# 隐式原型
思考: 实例化出的对象又是如何访问到这些共享方法和属性的呢?
可以先打印实例化出的对象console.log(p1)
,看看里面都有什么,打印结果如下:
只有givenName
和surname
属性,没有getNameEN
、getNameZH
方法。但是有个__proto__
的属性,我们可以看看__proto__
到底有什么内容。展开如下:
很明显有了getNameEN
、getNameZH
这两个方法,当我们使用p1.getNameEN()
时,会在自己身上查找该方法,如果找不到,就会到__proto__
属性里查找。
思考: 在显式原型的例子中,p1.getNameEN === p2.getNameEN
是成立的,这意味着p1.__proto__.getNameEN
和p2.__proto__.getNameEN
是同一个函数,难道p1.__proto__
与p2.__proto__
的值是相同的吗?如果相同,那这个值又是从哪来的?
如果你仔细与构造函数的prototype
(显式原型)的结构图对比,就会发现两者很相似,我们可以猜想它俩是同一个值,并且运行以下代码进行验证:
console.log(p1.__proto__ === People.prototype); // true
console.log(p2.__proto__ === People.prototype); // true
2
可以发现,实例化出的对象的__proto__
(隐式原型) 等于构造函数的prototype
。我们可以用图来表示一下,如下:
总结: 某个构造函数实例化出的对象的__proto__
(隐式原型) 会指向这个构造函数的prototype
(显式原型)。
思考: 那么图中 “?” 的地方又是什么意思呢?是否也是这关系呢,是某个构造函数实例化出的?
# 原型链
开头说到 Javascript 里面都是对象,需要有某种机制,需要把所有对象联系起来。而 Brendan Eich 是借鉴 Self 语言,使用基于 原型(prototype) 的继承机制。其实根据上面的结论,我们也可以推断Function
、Object
等对象之间的联系。
先看看大致的整体,Function
、Object
肯定有原型对象和构造函数。可以根据上述内容,做出下图:
思考: 一切皆对象,那么Function
的原型对象,是从哪个构造函数实例化出来的呢?如果我换个方式,那么就很好答出来了。
// prototype 从哪个构造函数实例化出来的呢?
var prototype = {
constructor: function() {},
call: function() {},
apply: function() {},
};
// 可以再简化成如下
// p 从哪个构造函数实例化出来的呢?
var p = {};
2
3
4
5
6
7
8
9
10
很明显,原型对象就是Object
构造函数实例化出来的,那么原型对象就有__proto__
并且指向Object.prototype
。可以完善一下图表:
思考
为什么我这里只画了Functino
的原型对象的__proto__
,同理不是可以得出:Object
的原型对象的__proto__
也是Object
构造函数实例化出来的吗?这个答案在下方。
思考: 构造函数是一个函数,那么函数又是谁哪个构造函数实例化出来的呢?感觉有点绕,换个说话:{}
的母亲是Object
,那么function(){}
的母亲是谁呢?
解答: 其实根据上面隐式原型的总结,可以得知函数是Function
实例化出来的,构造函数也有一个隐式原型__proto__
指向显式原型Function.prototype
。可以完善一下图表:
疑问: 那么Object
的原型对象,不应该也有一个__proto__
指向自己吗?对象是由Object
实例化出来的。
解答: 如果是这样的话,就会陷入一个无限套娃现象,所有函数、对象的__proto__.__proto__..
将没有尽头。而且在上面也说过,如果某方法在自己身上找不到,就会向__proto__
查找,这样会让 JS 引擎永远查询下去。如下:
// 如果Object.prototype.__proto__ === Object.prototype
var o = Object.prototype;
while (o.__proto__) {
o = o.__proto__; // 无尽头,死循环
}
var o = Object.prototype;
while (!("functionName" in o) && o.__proto__) {
o = o.__proto__; // 无尽头,死循环
}
if ("functionName" in o) o.functionName();
2
3
4
5
6
7
8
9
10
11
其实在Javascript
规范中,也提到原型链必须是有限长度的,从任一节点出发,经过有限步骤后必须到达一个终点。所以Object.__proto__
最合理的值就是 null
空对象。稍微完善一下图表就是:
思考: 那么String
、Number
、Boolean
又是怎么样的呢?(其实和People
是一致的,所以这里就不重复了)
# new
函数和构造函数又有啥区别呢?为什么函数里没写return
,用了new
后能返回对象呢?例如:
function People(surname, givenName) {
this.surname = surname;
this.givenName = givenName;
}
// ... getNameEN、getNameZH ...
var returnPeople = People("y", "xp");
var newPeople = new People("y", "xp");
console.log("returnPeople:", returnPeople);
console.log("newPeople:", newPeople);
2
3
4
5
6
7
8
9
10
11
注意
如果构造函数返回一个不为空的对象或者函数,那么就以函数的返回值返回,其他情况就返回出的实例。这里就不举例了,可以自己在函数内部写各种类型值试试。
同一个函数,仅仅加了new
,结果就不同了。new
到底做了什么事情?如果熟悉this
并且结合上面的内容,就很容易猜出,new
做了以下内容:
- 先创建一个空对象。
- 将空对象的
__proto__
指向原型对象。 - 再将这个函数内部的
this
指向这个空对象。
用代码实现,如下:
function New(fun) {
// 第一步,创建空对象
var obj = {};
// 第二步,隐式原型指向原型对象(显式原型)
if (fun.prototype) {
obj.__proto__ = fun.prototype;
}
// 去掉参数列表中的第一个fun(构造函数)参数,后面的参数作为构造函数的参数
var parameter = Array.prototype.slice.call(arguments, 1);
// 第三步,将这个函数内部的`this`指向这个空对象。顺便存下返回值,以便后面判断
// 这样 this.xx = yy 就能在空对象里设置键值对了
var result = fun.apply(obj, parameter);
// 函数返回值判断一下,是否返回实例
if (
(typeof result === "object" || typeof result === "function") &&
result !== null
) {
return result;
}
return obj;
}
var NewPeople = New(People, "y", "xp");
console.log(NewPeople);
console.log(NewPeople instanceof People);
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
运行如下图,与原生new
结果一致:
注意
__proto__
的取值和赋值,最好使用Object.getPrototypeOf()
和Object.setPrototypeOf()
,上面只是为了方便理解才写成var o = x.__proto__
,更好的方法是var o = Object.getPrototypeOf(x)
。赋值同理。
# instanceof
我们在判断一个对象是否某个构造函数的时候,常常用instanceof
来判断,例如:
var p1 = new People("Y", "XP");
console.log(p1 instanceof People); // true
2
其实根据对象、隐式原型、显式原型之间的关系,我们也可以做一个简单的判断方法。例如:
function _instanceof(object, F) {
return object.__proto__ === F.prototype;
}
console.log(_instanceof(p1, People)); // true
2
3
4
5
上面只是简单的判断,毕竟少了考虑了 “继承” 的因素,例如:
console.log(p1 instanceof Object); // true
根据原型链,也可以知道这就是根据嵌套的__proto__
来判断。稍微做下修改:
function _instanceof(object, F) {
var p = object;
var prototype = F.prototype;
while ((p = p.__proto__)) {
if (p === prototype) return true;
}
return false;
}
console.log(_instanceof(p1, Object)); // true
console.log(_instanceof(p1, People)); // true
2
3
4
5
6
7
8
9
10
11
上面实现的_instanceof
方法和原生的instanceof
几乎 一致。看看几个奇怪例子:
// 这里就不写 console.log() 了
// 为什么会有以下这些情况呢?
Function instanceof Object; // true
Object instanceof Object; // true
Function instanceof Function; //true
Object instanceof Function; // true
Number instanceof Number; //false
// 随便检查一下,效果是否一致
_instanceof(Function, Object); // true
_instanceof(Object, Object); // true
_instanceof(Function, Function); // true
_instanceof(Object, Function); // true
_instanceof(Number, Number); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
根据原型链和_instanceof
实现原理等上面的内容来思考,就会发现,以上例子输出的结果是理所应当的。其实过程如下:
// 构造函数(函数)的隐式原型(原型对象)的隐式原型 等于 Object构造函数的显式原型
Function.__proto__.__proto__ === Object.prototype; // true
// 构造函数(函数)的隐式原型(原型对象)的隐式原型 等于 Object构造函数的显式原型
Object.__proto__.__proto__ === Object.prototype; // true;
// 构造函数(函数)的隐式原型 等于 Function构造函数的显式原型
Function.__proto__ === Function.prototype; // true
// 构造函数(函数)的隐式原型 等于 Function构造函数的显式原型
Object.__proto__ === Function.prototype; // true
// 构造函数(函数)的隐式原型 不等于 Number构造函数的显式原型
// 构造函数(函数)的隐式原型(原型对象)的隐式原型 不等于 Number构造函数的显式原型
// 构造函数(函数)的隐式原型(原型对象)的隐式原型的隐式对象(null) 不等于 Number构造函数的显式原型
Number....__proto__ !== Number.prototype
2
3
4
5
6
7
8
9
10
11
12
13
相比上面例子,下面的例子才叫奇怪:
"yxp" instanceof String; // false
String("yxp") instanceof String; // false
new String("yxp") instanceof String; // true
1 instanceof Number; // false
Number(1) instanceof Number; // false
new Number(1) instanceof Number;
true instanceof Boolean; // false
Boolean(true) instanceof Boolean; // false
new Boolean(true) instanceof Boolean; // true
[] instanceof Array; // true
Array() instanceof Array; // true
new Array() instanceof Array; // true
{} instanceof Object; // true
Object() instanceof Object; // true
new Object() instanceof Object; // true
function (x, y) {return x + y;} instanceof Function); // true
Function("x", "y", "return x + y") instanceof Function); // true
new Function("x", "y", "return x + y") instanceof Function; // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
基础类型(primitive) 的数据是没有方法和属性的,没有toString
方法,更没有__proto__
属性等等。但实际却可以使用,这是因为 Javascript 内部会将 基础类型(primitive) 的数据包装成 对象类型(object) 数据,也可以说是 引用类型 数据。例如:
// 以 String 举例,Numbner、Boolean 同理
// "yxp" 没有任何方法和属性
// 表面上可以使用
"yxp".toString();
// 实际上内部转化
new String("yxp").toString();
2
3
4
5
6
这种自动转化成对象类型的机制,就是我说实现的_instanceof
方法 几乎 与原生instanceof
一样的理由。如下:
// _instanceof 方法与原生 instanceof 的差异
"yxp" instanceof String; // false
String("yxp") instanceof String; // false
new String("yxp") instanceof String; // true
_instanceof("yxp", String); // true
_instanceof(String("yxp"), String); // true
_instanceof(new String("yxp"), String); // true
// 假设 object 是 String 基础类型数据,Numbner、Boolean 同理
function _instanceof(object, F) {
var p = object;
var prototype = F.prototype;
// p.xx -> object.xx -> new String(object).xx
while ((p = p.__proto__)) {
if (p === prototype) return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以加深一下理解:
// 基础类型字符串(字符串字面量)
var literalString = "yxp";
// 函数返回值(字符串字面量)
var returnString = String("yxp");
// 字符串对象(引用类型数据)
var newString = new String("yxp");
console.log(literalString); // "yxp"
console.log(returnString); // "yxp"
console.log(newString); // String{"yxp"}
console.log(literalString === returnString); // true
console.log(literalString === newString); // false
console.log(returnString === newString); // false
// 数组字面量(数组对象)
var literalArray = [];
// 函数返回值(数组对象)
var returnlArray = Array();
// 数组对象(引用类型数据)
var newArray = new Array();
console.log(literalArray);
console.log(returnlArray);
console.log(newArray);
// 看上去都是[],但是记住这是三个不同的内存地址,不要被上面搞蒙了
console.log(literalArray === returnlArray); // false
console.log(literalArray === newArray); // false
console.log(returnlArray === newArray); // false
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
运行结果如下:
# 实现继承
在上面我们讲到了原型链,如果在自己找不到对应的方法或属性,便会从__proto__
查询下去。这个是不是有点像继承?可以调用父原型上的方法和属性。除了继承原型上的方法和属性,其实还需要继承父类构造函数。那么我们也可以利用原型来实现继承。
后续
篇幅似乎有点长,“几种手动实现继承的方式”就等到后面讲解,后面也会将该篇内容重新划分一下。