重温——对象与原型

数据类型

一切引用类型都是对象。我们都知道JS的数据类型有String、Number、Boolean、Undefined,ES6新增Symbol的基本数据类型、Null、对象(Object)。其中,前面5种属于简单的值类型,不是对象。Null和Object(包括函数、数组、对象)等都是对象,它们都是引用类型。判断一个变量是不是对象非常简单。值类型的类型判断用typeof,引用类型的类型判断用instanceof。

1
2
3
console.log(typeof true);  // boolean
var fn = function () { };
console.log(fn instanceof Object); // true

函数和对象的关系

先看一个例子:

1
2
3
4
5
function Fn() {
this.name = 'cchroot';
this.year = 1994;
}
var fn1 = new Fn();

这个例子可以说明:对象可以通过函数来创建。
但是我们经常看到这样的写法:

1
2
var obj = { a: 1, b: 2 };
var arr = [5, '我', false];

其实这是一种“快捷写法”,在编程语言中,一般叫做“语法糖”。
它的本质是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
 var obj = new Object();
obj.a = 1;
obj.b = 2;

var arr = new Array();
arr[0] = 5;
arr[1] = '我';
arr[2] = false;

//其中Object 和 Array 都是函数:
console.log(typeof (Object)); // function
console.log(typeof (Array)); // function

即,可以说明:对象都是通过函数来创建的。

prototype原型

每个函数都有一个属性叫做prototype。这个prototype的属性值是一个对象(对象是属性的集合),默认的只有一个叫做constructor的属性,指向这个函数本身。原型既然作为对象,属性的集合,不可能就只有一个constructor属性,还可以自定义的增加许多属性。例如Object对象:

这里写图片描述

隐式原型

每个函数function都有一个prototype,即原型;每个对象都有一个__proto__,可成为隐式原型,指向创建该对象的函数的prototype。Object.__proto__和Object.prototype的属性一样:

这里写图片描述

其中,Object.prototype是一个特例——它的__proto__指向的是null

这里写图片描述

然后,函数也是一种对象,函数也有__proto__吗?答案是:对的,也有:函数也不是从石头缝里蹦出来的,函数也是被创建出来的。谁创建了函数呢?——Function——注意这个大写的“F”。
例如:

1
2
3
4
5
6
7
function fn(x,y){
return x + y;
}
console.log(fn(10,20)); //30

var fn1 = new Function("x","y","return x + y");
console.log(fn1(10,20)); //30

以上代码中,第一种方式是比较传统的函数创建方式,第二种是用new Functoin创建。但是并不推荐用第二种方式创建函数。

根据上面说的一句话——对象的proto指向的是创建它的函数的prototype,就会出现:Object.proto === Function.prototype:

这里写图片描述

上图中,很明显的标出了:自定义函数Foo.proto指向Function.prototype,Object.proto指向Function.prototype,唉,怎么还有一个……Function.proto指向Function.prototype?这不成了循环引用了?

对!是一个环形结构。

其实稍微想一下就明白了。Function也是一个函数,函数是一种对象,也有proto属性。既然是函数,那么它一定是被Function创建。所以——Function是被自身创建的。所以它的proto指向了自身的Prototype。

instanceof

对于值类型,你可以通过typeof判断,string/number/boolean都很清楚,但是typeof在判断到引用类型的时候,返回值只有object/function,你不知道它到底是一个object对象,还是数组,还是new Number等等。这个时候就需要用到instanceof:
例如:

1
2
3
4
function Foo(){};
var f1 = new Foo();
console.log(f1 instanceof Foo);//true
console.log(f1 instanceof Object)//true

上面的代码中,f1这个对象是被Foo创建,所有第一个输出为true,至于
f1 instanceof Object 为什么为true,那是因为:
Instanceof的判断队则是:沿着A的proto这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。

这里写图片描述

大家根据规则照着图看看“ f1 instanceof Object ”这句代码是不是true? 根据上图很容易就能看出来,就是true。
最后贴一张完整的原型链图:

这里写图片描述

至于Instanceof这样设计,到底有什么用?到底instanceof想表达什么呢?答案就是:
instanceof表示的就是一种继承关系,或者原型链的结构

原型链与继承

在javascript中的继承是通过原型链来体现的:

1
2
3
4
5
6
7
function Foo(){};
var f1 = new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
console.log(f1.a); //10
console.log(f1.b); //200

以上代码中,f1是Foo函数new出来的对象,f1.a是f1对象的基本属性,f1.b是怎么来的呢?——从Foo.prototype得来,因为f1.__proto__指向的是Foo.prototype,所以:
访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。

这里写图片描述

上图中,访问f1.b时,f1的基本属性中没有b,于是沿着__proto__找到了Foo.prototype.b。

那么我们在实际应用中如何区分一个属性到底是基本的还是从原型中找到的呢?大家可能都知道答案了——hasOwnProperty,特别是在for…in…循环中,一定要注意。

这里写图片描述

你可能又会问: f1的这个hasOwnProperty方法是从哪里来的? f1本身没有,Foo.prototype中也没有,哪儿来的?
好问题。
它是从Object.prototype中来的,请看图:

这里写图片描述

对象的原型链是沿着__proto__这条线走的,因此在查找f1.hasOwnProperty属性时,就会顺着原型链一直查找到Object.prototype。

由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法。这就是所谓的“继承”。

我们都知道每个函数都有call,apply方法,都有length,arguments,caller等属性。为什么每个函数都有?这肯定是“继承”的。函数由Function函数创建,因此继承的Function.prototype中的方法。
所以也可以说:JS里面的继承是通过原型链来实现的

原型非常具有灵活性

首先,对象属性可以随时改动
对象或者函数,刚开始new出来之后,可能啥属性都没有。但是你可以这会儿加一个,过一会儿在加两个,非常灵活。

在jQuery的源码中,对象被创建时什么属性都没有,都是代码一步一步执行时,一个一个加上的。

这里写图片描述

其次,如果继承的方法不合适,可以做出修改。例如我们可以对Object和Array的toString()方法进行修改,修改成我们想要的方法,但是我们一般不会去修改原型链上本来有的基本方法,倒是经常可以为自己的特殊需求添加自定义一些函数:例如,自己去修改prototype.toString()方法:

1
2
3
4
5
6
function Foo(){};
var f1 = new Foo();
Foo.prototype.toString() = function(){
return "cchroot";
}
console.log(f1.toString());//cchroot

最后,如果感觉当前缺少你要用的方法,可以自己去创建。

如果你要添加内置方法的原型属性,最好做一步判断,如果该属性不存在,则添加。如果本来就存在,就没必要再添加了。

完~



注:本文是当时查看王福朋前辈的博客:深入理解javascript原型和闭包 系列文字做的笔记,以及自己的一些理解、见解和,以上如果有不理解,可以访问前辈的文章链接进行查看。