原型、原型链、继承

190 阅读9分钟

一切都是对象

JavaScript 中有一句著名的话——“一切皆对象”,理解这句话的关键在于怎么理解“对象”。

当然,也不是所有都是对象,值类型就不是对象。

    console.log(typeof 123);          //number
    console.log(typeof "abc");        //string
    console.log(typeof true)          //boolean
    console.log(typeof null);         //object
    console.log(typeof undefined);    //undefined
    console.log(typeof [1,2,3,4]);    //object
    console.log(typeof {name:"jim"}); //object
    console.log(typeof function(){}); //function

判断一个变量是不是对象非常简单。值类型的类型判断用typeof,引用类型的类型判断用instanceof

这里要说一个比较特殊的存在——null

基本类型(基本数值、基本数据类型)是指非 对象 并且无方法的数据。在 JavaScript 中,共有6种基本数据类型:string,number,boolean,null,undefined,symbol (ECMAScript 2015新增)。

上面是MDN中的一段原话,我们可以清楚的看到null属于基本类型。但是typeof null返回的结果却是object?这是为什么呢?

其实这是一个历史遗漏bug,

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0,typeof null就错误的返回了"object"。

在ECMA6中,曾经有提案为历史平凡,将type null的值纠正为null,但最后提案被拒了。 理由是历史遗留代码太多,不想得罪人,不如继续将错就错当和事老。

    var fn = function () { };
    console.log(fn instanceof Object);  // true

说了这么多,那么对象到底是啥?

在 JavaScript 中,对象是拥有属性和方法的数据。

通俗的讲,对象就是若干属性的集合,方法也是一种属性。因为它的属性表示为键值对的形式。

    var obj = {
        a: 1,
        b: function(x){
            console.log(this.a + x)
        },
        c: {
            name: '小明',
            year: 1994
        }
    }

上面有一段代码是这么写的:

    var fn = function () { };
    console.log(fn instanceof Object);  // true

由此我们可以得到函数就是对象的一种,但是函数却不像数组那样仅仅是一种包含和被包含的关系,函数和对象之间的关系比较复杂。先看一个小例子:

    function Fn() {
        this.name = '小明';
        this.year = 1994;
    }
    var fn1 = new Fn();  //{name: "小明", year: 1994}

上面的这个例子很简单,它能说明:对象可以通过函数来创建

但是,如果我说对象都是通过函数创建的,肯定有人疑惑:我们平时创建对象并没有用到函数啊?如下:

    var obj = { a: 10, b: 20 };
    var arr = [5, 'x', true];

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

    //var obj = { a: 10, b: 20 };
    //var arr = [5, 'x', true];
    
    var obj = new Object();
    obj.a = 10;
    obj.b = 20;
    
    var arr = new Array();
    arr[0] = 5;
    arr[1] = 'x';
    arr[2] = true;

而其中的 Object 和 Array 都是函数:

    console.log(typeof (Object));  // function
    console.log(typeof (Array));   // function

现在是不是糊涂了?对象是函数创建的,而函数却又是一种对象!天哪!函数和对象到底是什么关系啊?别着急!揭开这个谜底,还得先去了解一下另一位老朋友——prototype原型。

每个函数都有一个属性:prototype ——> 原型

每个函数都有一个属性叫做prototype。这个prototype的属性值是一个对象,默认的只有一个叫做constructor的属性,指向这个函数本身。

如上图,Func是一个函数,右侧的方框就是它的原型。

原型既然作为对象,肯定可以自定义许多属性。我们先来看看Object的prototype里面,就有好几个其他属性。

Object原型里面是不是有很多似曾相识的方法?你也可以在自己自定义的方法的prototype中新增自己的属性

    function Fn() {};
    Fn.prototype.name = '小明';
    Fn.prototype.getYear = function () {
        return 1994;
    };

这样,函数的原型就变成了下图这样:

那么,这样做有何用呢?

    function Fn() { }
    Fn.prototype.name = '小明';
    Fn.prototype.getYear = function () {
        return 1994;
    };
    
    var person = new Fn();
    console.log(person.name);       // 小明
    console.log(person.getYear());  // 1994

上面的函数即说明,person对象是从Fn函数new出来的,这样person对象就可以调用Fn.prototype中的属性。

因为每个对象都有一个隐藏的属性:__proto__,这个属性引用了创建这个对象的函数的prototype。即:fn.__proto__ === Fn.prototype

对象的隐式原型:__proto__

使用__proto__是有争议的,也不鼓励使用它。因为它从来没有被包括在EcmaScript语言规范中,但是现代浏览器都实现了它。

上面截图来看,obj.__proto__和Object.prototype的属性一样!这么巧?

答案就是一样。

obj这个对象本质上是被Object函数创建的,因此obj.__proto__=== Object.prototype。我们可以用一个图来表示。

即,每个对象都有一个__proto__属性,指向创建该对象的函数的prototype。

那么问题来了:上图中的“Object.prototype”也是一个对象,它的__proto__指向哪里?

自定义函数的prototype本质上就是和 var obj = {}是一样的,都是被Object创建,所以它的__proto__指向的就是Object.prototype。

但是Object.prototype确实一个特例:它的__proto__指向的是null

函数也是一种对象,函数也有__proto__吗?——当然有,请看下面分析。

函数也是被创建出来的。谁创建了函数呢?

——Function。注意这个大写的“F”。

    // 函数创建的第一种写法
    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。

Function.prototype指向的对象,它的__proto__是不是也指向Object.prototype?

继承关系:instanceof

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

这个时候就需要用到instanceof。

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

上面代码中我们看到:f1这个对象是被Foo创建,但是“f1 instanceof Object”为什么是true呢?

至于为什么过会儿再说,先把instanceof判断的规则告诉大家。根据以上代码看下图:

Instanceof运算符的第一个变量是一个对象,暂时称为A,如上面代码中的f1;第二个变量一般是一个函数,暂时称为B,如上面代码中的Foo。

Instanceof的判断队则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。

按照以上规则,大家看看“ f1 instanceof Object ”这句代码是不是true? 根据上图很容易就能看出来,就是true。

前面我们贴了很多图片,其实那些图片是可以联合成一个整体的,即:

看这个图片,千万不要嫌烦,必须一条线一条线挨着分析。如果前面你看的比较仔细,再结合刚才咱们介绍的instanceof的概念,相信能看懂这个图片的内容。

Instanceof这样设计,就是为了表达一种继承关系,或者原型链的结构。

原型链继承

与java/C#中的不同,javascript中的继承都是通过原型链来体现的。

	function Foo(){}
	var fn = new Foo();
	
	fn.a = 10;
	
	Foo.prototype.a = 100;
	Foo.prototype.b = 200;

	console.log( fn.a );  // 10
	console.log( fn.b );  // 200

以上代码中,fn是Foo函数new出来的对象,fn.a是fn对象的基本属性,fn.b是怎么来的呢?

答案是从Foo.prototype得来,因为fn.__proto__指向的是Foo.prototype

由此我们得出:

访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。

上图中,访问fn.b时,fn的基本属性中没有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中的方法。

那怎么还有hasOwnProperty呢?

那是Function.prototype继承自Object.prototype的方法。有疑问可以看看上面讲instanceof时候那个大图,看看Function.prototype.__proto__是否指向Object.prototype。

原型、原型链,大家都明白了吗?

结语

基本上原型、原型链、继承简单的知识都讲完了,不知道你们有没有明白。当然这里面其实还以很多的内容没有讲到比如最最最容易绕晕的this指针等,由于篇幅较长,将再后面的文章讲到。