【Javascript】Prototype源码浅析——Class部分(一)之类

说明:

在javascript中,由于缺乏传统面向对象语言的继承机制,类与继承是一个比较复杂的概念。
因此本段解析中,不就javascript中的原型、类、继承、封装进行深入探讨。
需要深入了解的可以去参考大牛的文章,另推荐几本书《javascript高级程序设计》、《javascript语言精髓与编程实践》和《javascript设计模式》,多读几遍就会对javascript的原型有深入的了解。

所以本文只就Prototype远源码涉及到的部分进行解析。

到Class这一步,需要说一下前面没有说到的单体模式,通过前面的几个部分的观察,会发现整个Prototype部分,大部分对象的方法扩展采用的都是类似下面的形式:

1
2
3
4
5
6
7
var obj = (function(){
var variable ;
function method(){}
return {
method:method
}
})();

这就是单体模式。

不过这么做有什么好处呢?

单体简单的理解就是一个对象。比如:

1
2
3
4
5
6
7
var obj = {
name : 'xesam',
say : function(){
console.log(this.name);
}
};
obj.say();

此时,obj有个作用就是可以提供一个命名空间,并且组织了一部分变量和方法。

不够这样的一个去缺点就是name完全暴露在外面,可以随意修改,如果name对我们来说是私有的,那么这么样就无法满足要求了。所以鉴于JS中奇特的作用域限制,我们自然得依靠函数来帮忙,于是修改如下:

1
2
3
4
5
6
7
var obj = function(){
var name = 'xesam';
function say(){
console.log(name);
}
};
obj.say();

此时,name变成一个局部变量,外界就无法访问了,不过此时obj.say()也会报错,因为obj已经不是一个对象(这里的对象是指普通对象,而非函数这种对象)了,因此我们再让函数自执行,返回我们需要的对象:

1
2
3
4
5
6
7
8
9
10
var obj = (function(){
var name = 'xesam';
function say(){
console.log(name);
}
return {
say : say
}
})();
obj.say();

于是就得到了类似Prototype中的形式,这么做的好处大概也就出来了,变量和方法的组织,数据的封装和隐藏。两外,由于匿名函数只执行(实例化)了一次,所以也不会带来内存的问题。如果愿意,也可以这么样来模拟JS中的Math对象。

现在回到Prototype的Class部分,我们来一步步实现Prototype中的Class部分。

对于最常见的类的声明,就是创建一个构造函数,然后扩展其原型。比如:

1
2
3
4
5
6
7
8
function Person(name){
this.name = name;
}
Person.prototype.say = function(){
console.log('hello ' + this.name);
}
var xesam = new Person('xesam');
xesam.say();

上面定义构造函数和添加方法分开了,现在我们把他们打包到一起。运用刚才说故偶的单体模式,我们先定义一个对象,用作命名空间:

1
var Class = {}

现在需要提供的是一个初始化变量和添加方法的功能。

写之前我们先得规定一下最终形式的调用方法,我们还是尊重Prototype的形式,并且以上面的Person类为例,声明Person类的形式为:

1
2
3
4
5
6
7
8
9
10
var Person = Class.create({
initialize:function(name){
this.name = name;
},
say : function(){
console.log('hello ' + this.name);
}
});
var xesam =new Person('xesam');
xesam.say();

如上所示,Class.create的参数是一个对象,其中initialize包含的是初始化部分(相当于普通面向对象的构造函数),其他部分(say)则是需要添加的方法

【说明:initialize是必须的,其他则可选】

在实现之前,我们先准备一个工具函数$A,作用就是获得一个对象的数组形式(转化为一个数组):

1
2
3
4
5
6
7
function $A(iterable) {
if (!iterable) return [];
if ('toArray' in Object(iterable)) return iterable.toArray();
var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
}

这个方法挺简单的,无需多说。

下面我们可以获得一个最简单的Class.create实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Class = {};
Class.create = function(source){
return function(){
for(var i in source){
this[i] = function(){
source[i].apply(this,$A(arguments));
}
//如果你还记得Function部分的内容,这里可以写成this[i] = source[i].bind();
}
source.initialize.apply(this,arguments);
}
}

var Person = Class.create({
initialize:function(name){
this.name = name;
},
say : function(){
console.log('hello ' + this.name);
}
});
var xesam =new Person('xesam');
xesam.say();

上面create返回的是一个匿名函数,我们还可以采用另一种方式,先声明一个函数,处理这个函数之后再返回:

1
2
3
4
5
6
7
8
9
Class.create = function(source){
var kclass = function(){
this.initialize.apply(this,arguments);
};
for(var i in source){
kclass.prototype[i] = source[i];
}
return kclass;
};

这里先前的实现其实差不多,而且和最初的Person比较吻合。

分析上面的实现,你会发现定义完成之后,除了去改动初始定义,就没办法扩展了。因此我们在打包了之后再将具体实现分开,将Class分为初始化和添加方法两个部分,并使用刚才说过的单体模式来组织:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Class = (function(){
function create(source){
function kclass(){
this.initialize.apply(this,arguments);
};
addMethods.call(kclass,source);//注意这里的调用
return kclass;
}
function addMethods(source){
for(var property in source){
this.prototype[property] = source[property];
}
}
return {
create : create,
addMethods : addMethods
}
})();

这么处理过之后,addMethods方法分离出来了,但是现在还是在Class对象上,并不会添加到我们新创建的Person类上面,所以我们将addMethods添加到实现中的kclass上面。

1
kclass.addMethods = addMethods;//(代码4-1)

注意,这里有个关于扩展性的问题。如果我们的Class并不是只有addMethods一个方法,而且有addMethods1,addMethods2,addMethods3···方法,那么我们总不能按照这个顺序一并写下去吧,所以我们可以用一个对象(比如叫Methods)来把这些方法都组织起来。所以代码4-1的形式变成:

1
kclass.addMethods = Methods.addMethods;

继续分析,create的参数可能是多个需要添加的方法,因此不能用一个形参source来限定死了,采用内置的arguments对象来替代。所以Class的基本骨架就出来了:

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
var Class = (function(){
function create(){
var properties = $A(arguments);
function kclass(){
this.initialize.apply(this,arguments);//实例化的时候,这里自动调用了initialize方法
};
kclass.addMethods = Class.Methods.addMethods;
//在Prototype中由于有一个Object方法,所以这里调用的是Object.extend(Class.Methods)方法
for(var i = 0; i < properties.length; i++){
kclass.addMethods(properties[i]);
}
if (!kclass.prototype.initialize){//这里是一个小检测,避免因找不到initialize而报错
kclass.prototype.initialize = function(){};
}
return kclass;
}
function addMethods(source){
for(var property in source){
this.prototype[property] = source[property];
}
}
return {
create : create,
Methods : { //注意这里的改动
addMethods : addMethods
}
}
})();

到此,我们有了一个Class对象(可以创建类和添加方法)和一个Person类(有name属性和say方法)。

【Javascript】Prototype源码浅析——Object部分(三)之有关JSON

对JSON的操作主要是解析JSON字符串为一个对象和将一个对象转换成JSON字符串。

网上搜一下JSON解析,就会发现一堆文章和方法,当然这篇文章主要不是讨论JSON的解析,不过也可以大致回顾一下:

  1. eval。eval(‘(‘ + jsonStr + ‘)’);加个括号是为了强制表达式运算,不然直接eval(‘{}’),具体原因可以去翻看一下《JS语言精髓与编程实践》
  2. Function
  3. 另外有一个JSON解析的库,这个可以自己去下载
  4. 除了IE6,7之外,现代浏览器基本都内置了JSON解析器

【说明,prototype本身也实现了一个evalJSON方法,不过也脱离不了上面几种情况】

实例如下(没有用JSON解析库):

1
2
3
4
5
6
7
(function(){
var obj = "{\"name\":\"xesam\"}";
console.log('eval:',eval('(' + obj + ')'));
console.log('Function',(new Function('return' + obj))());
console.log('JSON:',JSON.parse(obj));
})();
//结果:{name:'xesam'}

现在将上面的过程反过来,将一个对象编码为一个JSON字符串。同样,如果浏览器带有原生的JSON方法,有一个与parse对应的方法stringify

1
2
3
4
(function(){
var obj = {name:'xesam'};
console.log('obj:',JSON.stringify(obj));//'{"name":"xesam"}'
})();

这种操作就不像解析那样有那么多方法,因此需要自己来写。

写之前,先来探讨一下目标对象可能出现的情况,然后把它转换成字符串的形式。

1
2
3
4
5
6
7
8
var object_1 = null;
var object_2 = undefined;
var object_3 = 12;
var object_4 = '12';
var object_5 = true;
var object_6 = [1,2,3];
var object_7 = {name :'xesam'};
var object_8 = {name : function(){}};

先硬性规定一下,null、undefined、true、false返回对应的’null’、’undefined’、’true’、’false’。数字直接量和字符串直接量返回对应的数值。

下面主要是区分出Array类型和Function,主要操作就是遍历每一项,碰到数组或者一般对象就依次展开,所以可以得到一个初始的版本:

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
46
47
48
49
50
51
52
(function(){
function object2Str(object){
if(arguments.length != 0 && typeof object == 'undefined'){
return 'undefined';
}
switch (object) {
case null : return 'null';
case true : return 'true';
case false: return 'false';
}
var type = Object.prototype.toString.call(object);
switch(type){
case '[object Number]' : return String(object);
case '[object String]' : return String(object);
case '[object Array]' : {
var ret = [];
for(var i = 0,len = object.length; i < len; i++){
ret.push(object2Str(object[i]));
}
return '[' + ret.join(',') + ']';
}
case '[object Object]' :{
var ret = [];
for(var key in object){
if(object.hasOwnProperty(key)){
if(typeof object2Str(object[key]) != 'undefined'){
ret.push(String(key) + ':' + object2Str(object[key]));
}
}
}
return '{' + ret.join(',') + '}';
}
}
}
console.log(object2Str(null));
console.log(object2Str(undefined));
console.log(object2Str(true));
console.log(object2Str(12));
console.log(object2Str('34'));
console.log(object2Str({name : function(){}}));
var object = [1,2,3,4,[5,6,7,8,9],6];
console.log(object2Str(object));
var object = {
name:'xesam',
age : 24,
books:{
key_1 : 'book_1',
key_2 : 'book_2'
}
};
console.log(object2Str(object));
})();

看这个实现,{name:function(){}} 这个并没有显式的提到,因为在默认情况下

1
if(typeof object2Str(object[key]) != 'undefined')

这一步就被处理了。

基本的框架得到了,我们的实现中还包括了 undefined 的处理,Prototype 的实现中并没有这一步,需要注意。

下面要做的就是一些修缮工作,继续补充:

在上面的例子中,我们忽略了

1
var number_1 = 12 

1
var number_2 = new Number(12)

的区别,我们先填补这两者之间的区别。

于是在处理每一个值之前先直接调用valueOf获得变量的原始值。于是上面的例子变为:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
(function(){
function object2Str(object){
if(arguments.length != 0 && typeof object == 'undefined'){
return 'undefined';
}
var type = Object.prototype.toString.call(object);
switch (type) {
case '[object Number]' :
case '[object String]' :
case '[object Boolean]':
object = object.valueOf();
}
switch (object) {
case null : return 'null';
case true : return 'true';
case false: return 'false';
}
type = typeof object;
switch(type){
case 'number' : return String(object);
case 'string' : return String(object);
case 'object' : {
var ret = [];
if(Object.prototype.toString.call(object) == '[object Array]'){
for(var i = 0,len = object.length; i < len; i++){
ret.push(object2Str(object[i]));
}
return '[' + ret.join(',') + ']';
}else if(Object.prototype.toString.call(object) == '[object Function]'){
return '{}';
}else{
for(var key in object){
if(object.hasOwnProperty(key)){
if(typeof object2Str(object[key]) != 'undefined'){
ret.push(String(key) + ':' + object2Str(object[key]));
}
}
}
return '{' + ret.join(',') + '}';
}
}
}
}
console.log(object2Str(null));
console.log(object2Str(undefined));
console.log(object2Str(true));
console.log(object2Str(12));
console.log(object2Str('34'));
console.log(object2Str({name : function(){}}));
var object = [1,2,3,4,[5,6,7,8,9],6];
console.log(object2Str(object));
var object = {
name:'xesam',
age : 24,
books:{
key_1 : 'book_1',
key_2 : 'book_2'
}
};
console.log(object2Str(object));
})();

【说明:上面的实现中并未转义”,/,\和还有一些控制符在Prototype中,调用的是string.inspect这个方法来实现的,不过这一部分在后面的String部分,所以暂且这么实现,了解即可】

现在回到Prototype中,在Prototype里面,相似的方法叫做Str【这个函数是大写开头,说明是一个局部函数,另一个大写开头的是Type,这两个方法并未对外公开】

对比 object2Str 和 Str,除了上面说的特殊字符转义之外,Str的实现还多了两个参数就是key和stack。

这个 stack 的作用是什么呢?为了避免循环使用导致栈溢出。

比如我有一个对象:

1
var obj = {name : this}。

去调用Prototype里面的Str方法,就会报错(typeError),如果直接调用object2Str就会报栈溢出。所以这是一个必要的检测。

所以我们仿写最终的版本是:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
(function(){
function object2Str(object,stack){
stack = stack || [];
if(arguments.length != 0 && typeof object == 'undefined'){
return 'undefined';
}
var type = Object.prototype.toString.call(object);
switch (type) {
case '[object Number]' :
case '[object String]' :
case '[object Boolean]':
object = object.valueOf();
}
switch (object) {
case null : return 'null';
case true : return 'true';
case false: return 'false';
}
type = typeof object;
switch(type){
case 'number' : return String(object);
case 'string' : return String(object);
case 'object' : {
for (var i = 0, length = stack.length; i < length; i++) {
if (stack[i] === object){
console.log('error');
throw new TypeError();
}
}
stack.push(object);

var ret = [];
if(Object.prototype.toString.call(object) == '[object Array]'){
for(var i = 0,len = object.length; i < len; i++){
ret.push(object2Str(object[i]));
}
ret = '[' + ret.join(',') + ']';
}else{
for(var key in object){
if(object.hasOwnProperty(key)){
if(typeof object2Str(object[key],stack) != 'undefined'){
ret.push(String(key) + ':' + object2Str(object[key],stack));
}
}
}
ret ='{' + ret.join(',') + '}';
}
stack.pop();
return ret;
}
}
}
console.log(object2Str(null));
console.log(object2Str(undefined));
console.log(object2Str(true));
console.log(object2Str(12));
console.log(object2Str('34'));
console.log(object2Str({name : function(){}}));
var object = [1,2,3,4,[5,6,7,8,9],6];
console.log(object2Str(object));
var object = {
name:'xesam',
age : 24,
books:{
key_1 : 'book_1',
key_2 : 'book_2'
}
};
console.log(object2Str(object));
var object = {
name : this
}
console.log(object2Str(object));
})();

另外,在Object中,Str是为toJSON来服务的,因此这个调用方法也是特别处理了的,其参数形式是Str(key, holder, stack)

由于在Prototype的Object中是实现了一个keys方法和一个values方法,所以在类似下面的代码中

1
2
3
4
5
6
7
for(var key in object){
if(object.hasOwnProperty(key)){
if(typeof object2Str(object[key]) != 'undefined'){
ret.push(String(key) + ':' + object2Str(object[key]));
}
}
}

直接使用了keys函数,Prototype中的 具体实现是:

1
2
3
4
5
6
7
var keys = Object.keys(value);
for (var i = 0, length = keys.length; i < length; i++) {
var key = keys[i], str = Str(key, value, stack);
if (typeof str !== "undefined") {
partial.push(key.inspect(true)+ ':' + str);
}
}

所以这么一来,toJSON的调用形式也变成了:

1
2
3
function toJSON(value) {
return Str('', { '': value }, []);
}

如果按照我们在例子中的写法,可以直接是

1
2
3
function toJSON(value) {
return object2Str(value, []);
}

说完Str,另一个用的多的就是inspect方法,顾名思义啊·检查作用,可以用来调试。

一个最简单的实现:

1
2
3
4
5
(function(){
function inspect(object){
return object.toString();
}
})();

null和undefined没有toString方法,这里顺便说明一下,typeof null == ‘object’,但是null又没有toString方法,因此不能人为null是一个简单的object

所以这个讨论在前面已经讨论过了,一次跳过。其他就是一些检测,优化的机制。就不算重点了。

至于keys(values)两个方法是很简单的,就是抄袭python,遍历一个对象的key(value),然后装到一个数组里返回。

【toHTML,isHash,toQueryString】的具体实现跟后面关联比较紧,因此在这里就暂时不说。

【Javascript】Prototype源码浅析——Object部分(二)之类型检测

这里我不关心javascript里面各种类型是怎么定义的,唯一要指出的是

1
2
3
var str_1 = 'xesam';
var str_2 = new String('xesam');
console.log(str_1 === str_2);

其中str_1和str_2不是同一个东西,先前看到群里面有个人提到类似的一个问题,所以才想起来的。

检测一个变量的类型,最常见的就是typeof操作符。typeof的返回值是一个字符串,一般只能返回如下几个结果:

number,boolean,string,function,object,undefined

但是显然要是我们想区分Date,RegExp这样的具体类,typeof就没有办法了,于是统一归结到object里面。

先看一下Prototype里面的一些定义:

  NULL_TYPE = 'Null',
  UNDEFINED_TYPE = 'Undefined',
  BOOLEAN_TYPE = 'Boolean',
  NUMBER_TYPE = 'Number',
  STRING_TYPE = 'String',
  OBJECT_TYPE = 'Object',
  FUNCTION_CLASS = '[object Function]',
  BOOLEAN_CLASS = '[object Boolean]',
  NUMBER_CLASS = '[object Number]',
  STRING_CLASS = '[object String]',
  ARRAY_CLASS = '[object Array]',
  DATE_CLASS = '[object Date]',
  

【这些变量的值是作为规定的结果返回,避免人为错误】

回到变量检测上,我们看个实例:

1
2
3
4
5
6
7
8
var array_1 = [null,undefined,10,NaN,'10',true,function(){}];
var array_2 = [new Function(),new String(),new Number(),new Boolean(true),{},[],/x/,new Error(),new Date()];
var varArray = array_1.concat(array_2);
(function(){
for(var i in varArray){
console.log(typeof varArray[i]);
}
})();

下面是结果:

object
undefined
number
number
string
boolean
function
function
object
object
object
object
object
object
object
object

需要重点注意的几个是:

null --> object
NaN --> number

带有new操作符的除了Function之外,全部都是object。

我们按照Prototype的形式,包装一下这个过程,作为常规的过程,null和undefined这两个特别的我们先处理掉:

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
(function(){
//先定义一下类型的表示字符串
var typeMap = {
'number' : 'Number',
'boolean' : 'Boolean',
'string' : 'String',
'function' : 'Function',
'object' : 'Object',
'undefined': 'Undefined',
'null' : 'Null'
};
function type(obj){
switch(obj){ //检测null和undefined
case null:{
return typeMap['null'];
}
case undefined:{
return typeMap['undefined'];
}
}
var objType = typeof obj;
switch(objType){
case 'number' : {
return typeMap['number'];
}
case 'boolean' : {
return typeMap['boolean'];
}
case 'string' : {
return typeMap['string'];
}
case 'function' : {
return typeMap['function'];
}
case 'object' : {
return typeMap['object'];
}
}
}
for(var i in varArray){
console.log(type(varArray[i]));
}
})();

【说明:这里和Prototype里面有个不一致的地方,Prototype里面并没有用typeof来检测函数,我这里用了,注意就行】

或者上面的例子再简单一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
(function(){
function type(obj){
return obj === null ? 'Null' :
obj === undefined ? 'Undefined' :
typeof obj === 'number' ? 'Number':
typeof obj === 'boolean' ? 'Boolean':
typeof obj === 'string' ? 'String':
typeof obj === 'function' ? 'Function': 'Object';
}
for(var i in varArray){
console.log(type(varArray[i]));
}
})();

运行结果:

Object
Undefined
Number
Number
String
Boolean
Function
Function
Object
Object
Object
Object
Object
Object
Object
Object

现在我们分离出来了“基本类型”【我这里基本类型是指的typeof可以检测出来的类型,不是指的js的基本类型,大牛不要喷我···】

接下来就是分离object那个里面的类型了。方法提供两个:

  1. 转化为字符串形式
  2. 检测原型

第一种方法:

先将获得对象的字符串表示形式,获得字符串形式的方法有几种:

1
2
3
4
var obj = 'xesam';
obj = ''+obj;
obj = String(obj);
obj = obj.toString();

对于 alert 和 console 这样的输出方法,输出对象的时候,都调用了对象的toString()方法。按理来说,这样没什么问题,但是Date,RegExp,Array等都是重写了从Object继承过来的toString的方法的,因此到底返回什么就无法预测了。

所以要调用,必须使用最原始的Object.prototype.toString。

使用最原始的Object.prototype.toString的局限就是无法判断自定义的类,除非自定义类重写了自己的toString方法,但是重写自定义类的toString方法之后又会引起枚举(in)的问题(Object第一部分提到过),所以这个需要仔细考虑一下。

具体的实现如下:

1
2
3
4
5
6
7
8
9
(function(){
function objectType(obj){
obj = Object.prototype.toString.call(obj);
return obj;
}
for(var i in varArray_2){
console.log(objectType(varArray_2[i]));
}
})();

运行结果:

[object Function]
[object String]
[object Number]
[object Boolean]
[object Object]
[object Array]
[object RegExp]
[object Error]
[object Date]

第二种方法就是调用instanceof操作符

比如:

1
console.log([] instanceof Array);

这就是 variable instanceof constructor 的形式。

或者直接判断constructor。

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function(){
function objectType(obj){
obj = obj.constructor;
return obj === Array ? '[object Array]':
obj === Boolean ? '[object Boolean]':
obj === Date ? '[object Date]':
obj === RegExp ? '[object RegExp]':
obj === String ? '[object String]':
obj === Function? '[object Function]':
obj === Error ? '[object Error]':
obj === Number ? '[object Number]':'[object Object]'
}
for(var i in varArray_2){
console.log(objectType(varArray_2[i]));
}
})();

运行结果:

[object Function]
[object String]
[object Number]
[object Boolean]
[object Object]
[object Array]
[object RegExp]
[object Error]
[object Date]

这个对于自定义的类,也可以获得相应的结果,只是稍微变通一下代码而已。