__proto__和prototype

1,984 阅读9分钟

前言

该系列文章将带你全面理解js对象和原型链,并用es5去实现类以及认识es6中class的美妙。该系列一共有3篇文章:

  • __proto__prototype来深入理解JS对象和原型链
  • javascript实现类与继承
  • 初始ES6class

此篇为第一篇__proto__prototype来深入理解JS对象和原型链

prototype__proto__

何为原型

引用《JavaScript权威指南》的一段描述

Every JavaScript object has a second JavaScript object (or null ,
but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.

翻译过来就是:每一个JS对象一定关联着另外一个JS对象(也许是null,但是它一定是唯一的)。这个另外的对象就是所谓的原型对象。每个对象从它的原型对象中继承属性和方法。

如果你是初学者,这句话肯定很绕吧(不过它确实描述得很精辟)。没关系,你只需要先把握以下两点就好了:

  • 在JS里,万物皆对象。方法(Function)是一个对象,方法的原型(Function.prototype)是对象。

  • JS有三种构造对象的方式

    • 通过对象字面量

      var person1 = {
          name: 'Jzin',
          sex: 'male'
      }
      
    • 通过构造函数

      function Person(name, sex) {
          this.name = name;
          this.sex = sex;
      }
      var person1 = new Person('Jzin', 'male');
      

      所谓构造函数就是:可以通过它来new出一个对象实例的函数。通常构造函数里都用了this,因为这样子才会给调用它的对象绑定属性。

    • 由函数Object.create构造

      var person1 = {
          name: 'Jzin',
          sex: 'male'
      }
      var person2 = Object.create(person1);
      

    这三种方法的异同到后面还会继续分析,现在你只需掌握如何构建对象就好啦。

以上两点就是这小结你要掌握的东西:

  • 万物皆对象的思想
  • 如何构造对象

至此,我还没介绍什么是原型,不过没关系,我们先看看原型的分类,慢慢你就会理解了。

原型的分类

JS的原型分成两类:隐式原型和显示原型

显式原型(explicit prototype property)

当你创建一个函数时,JS会为这个函数(别忘了:JS一切皆对象)自动添加prototype属性,这个属性的值是一个对象,也就是原型对象(即函数名.prototype)。这个对象并不是空对象,它拥有constructor属性,属性值就是原函数。当然,你也可以自己在原型对象中添加你需要的属性(即函数名.prototype.属性名=属性值).

那么原型对象(prototype)的作用是什么呢?可以用原型对象来实现继承,即通过函数构造出来的实例可以直接访问其构造函数的原型对象中的属性。可能有点绕,但是读到后面你就会理解啦。

需要注意的是:

  • 显式原型(prototype)只有函数才拥有。我们后面讲的隐式原型则是所有对象都有。
  • 通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性。
隐式原型( implicit prototype link)

JavaScript中任意对象都有一个内置属性[[prototype]],在ES5之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过__proto__来访问。现在,所谓的隐式原型就是__proto__ 了。

  • 隐式原型的指向

    隐式原型指向创建这个对象的函数的prototypeObject.create函数构造出来的实例有点例为,后面会说明。其实也不是例为,只是它经过了一定的封装)。看下面的例子

    function person(name) {
        this.name = name;
    }
    person.prototype.class = 'Human';
    var person1 = new person('Jzin');
    
    console.log(person1.__proto__); //person { class: 'Human' }
    console.log(person.__proto__);  //[Function]
    

    person1__proto__很容易理解:它是由person方法构造的实例,它的__proto自然就是person.prototype

    person__proto__呢?其实每一个方法的构造方法都是Function方法,也就是所有方法的__proto__都是Function.prototype。如果现在还不理解也没关系,后面会有一副图帮你理解。

  • 隐式原型的作用

    • 构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj这个对象中的x属性时,如果在obj中找不到,那么就会沿着__proto__依次查找。这也是protorype可以实现继承的原因。
    • 可以用来判断一个对象(L)是否是某个函数(R)的实例:只需判断L.__proto__.__proto__ ..... === R.prototype这个是否为真就行了。这也是instanceof运算符的原理。后面会讲到。

一张图带你形象理解__proto__prototype

先上图,如果图片显示不了可以点击这里:传送门

我们来理解一下这幅图:

  1. 构造函数Foo()
    • 构造函数Foo的原型属性prototype指向了它的原型对象Foo.prototype。原型对象Foo.protoype中有默认属性constructor指向了原函数Foo
    • 构造函数Foo创建的实例f2,f1__proto__指向了其构造函数的原型对象Foo.prototype,所以Foo的所有实例都可以共享其原型对象的属性。
    • 构造函数Foo其实是Function函数创建的实例对象,所以它的__proto__就是Function函数的原型对象Function.prototype
    • 构造函数Foo的原型对象其实是Object函数创建的实例对象,所以它的__proto__就是Object函数的原型对象Object.prototype
  2. Funtion函数
    • 你所写的所有函数,其实都是Function函数构造的实例对象。所以所有函数的__proto__都指向Fucntion.prototype
    • Function函数对象是由它本身创建(姑且可以这么理解),所以Function.__proto__d等Function_prototype
    • Function函数的原型对象其实是Object函数创建的实例对象,所以它的__proto__就是Object函数的原型对象Object.prototype
  3. Object函数
    • Object函数其实是Function函数创建的实例对象,所以它的__proto__就是Function函数的原型对象Function.prototype
    • 需要注意的是:Object.prototype__proto__是指向null的!!!

相信你通过这幅图,已经对原型有自己的理解了,我们来总结一下:

  • 对象有__proto__属性,指向该对象的构造函数的原型对象。
  • 方法除了有__proto__属性,还有prototype属性,prototype指向该方法的原型对象。

深入理解__proto__的指向

相信经过上面的介绍,你已经能很好地掌握__proto__的指向了。本节通过一些实际的例子让你更加深入地理解__proto__的指向。

在一开始,我们了解了构造对象的三种方式:(1)对象字面量的方式 (2)new的方式 (3)ES5中的Object.create()。其实,这三种方式在我看来都是一种的,即通过new来构建。为什么这么说呢?我们来仔细分析分析:

  1. 通过字面量构造对象

    var person1 = {
        name: 'Jzin',
        sex: 'male'
    }
    

    其实这种方式只是为了开发人员更方便创建对象的一个语法糖(语法糖:顾名思义,就是很甜的糖,经过代码封装,让语法更加人性化,实际的内部实现是一样的)。

    上面也就等价于:

    var person1 = new Object();
    person1.name = 'Jzin';
    person1.sex = 'male';
    

    person1Object函数构造的对象,所以person1.__ptoto__就指向Object.prototype

    也就是说,通过对象字面量构造出来的对象,其__proto__都是指向Object.prototype

  2. 通过构造函数

    function Person(name, sex) {
        this.name = name;
        this.sex = sex;
    }
    var person1 = new Person('Jzin', 'male');
    

    通过new操作符调用的函数就是构造函数。由构造函数构造的对象,其__proto__指向其构造函数的原型对象。

    在本例中,person1.__proto__就指向Person.prototype

  3. 由函数Object.create构造

    var person1 = {
        name: 'Jzin',
        sex: 'male'
    }
    var person2 = Object.create(person1);
    

    由函数Object.create(obj)构造出来的对象,其隐式原型有点特殊:指向obj.prototype。在本例中,person2.__proto__指向person1

    这是为什么呢?我们来分析一下。在没有Object.create函数的日子里,为了实现这一功能,我们需要这样子做:

    Object.create = function(p) {
        function F(){}
        F.prototype = p;
        return new F();
    }
    var f = Object.create(p);
    

    这样子也就是实现了其功能,分析如下:

    // 以下是用于验证的伪代码
    var f = new F();	//var f = Object.create(p);
    // 于是有
    f.__proto__ === F.prototype	//true
    // 又因为
    F.prototype === p;	//true
    // 所以
    f.__proto__ === o	//true
    

    因此由Object.create(p)创建出来的对象它的隐式原型指向p。

通过上面的分析,相信你对原型又进一步理解啦。我们再来几题玩玩。

  1. 构造函数的显式原型的隐式原型

    • 内建对象(built-in object)的的隐式原型

      比如Array()Array.prototype.__proto__指向什么?

      Array.prototype.__proto__ === Object.prototype //true
      

      比如Function()Function.prototype.__proto__指向什么?

      Function.prototype.__proto__ === Object.prototype //true
      

      根据上面那幅图,这些也很简单啦。

  2. 自定义对象

    • 默认情况下

      function Foo(){}
      var foo = new Foo()
      Foo.prototype.__proto__ === Object.prototype //true 
      foo.prototype.__proto__ === Foo.prototype //true 
      

      理由,就不必解释了吧

    • 其他情况

      1. function Bar(){}
        function Foo(){}
        //这时我们想让Foo继承Bar
        Foo.prototype = new Bar()
        
        Foo.prototype.__proto__ === Bar.prototype //true
        console.log(Foo.prototype.constructor);	//[Function: Bar]
        
      2. function Foo(){}
        //我们不想让Foo继承谁,但是我们要自己重新定义Foo.prototype
        Foo.prototype = {
          a:10,
          b:-10
        }
        //这种方式就是用了对象字面量的方式来创建一个对象,根据前文所述 
        Foo.prototype.__proto__ === Object.prototype
        console.log(Foo.prototype.constructor);	//[Function: Object]
        

      注意:以上两种情况都等于完全重写了Foo.prototype,所以Foo.prototype.constructor也跟着改变了,于是constructor这个属性和原来的构造函数Foo也就切断了联系。

instanceof

instanceof的左值一般是一个对象,右值一般是一个构造函数,用来判断左值是否是右值的实例。instanceof操作符的内部实现机制和隐式原型、显式原型有直接的关系,它的内部实现原理是这样的:

//设 L instanceof R 
//通过判断
 L.__proto__.__proto__ ..... === R.prototype ?
//最终返回true or false

也就是沿着L的__proto__一直寻找到原型链末端,直到等于R.prototype为止。知道了这个也就知道为什么以下这些奇怪的表达式为什么会得到相应的值了

Function instanceof Function //true
Function instanceof Object // true 
Object instanceof Function // true 
Object instanceof Object // true
Number instanceof Number //false
Number instanceof Function //true
Number instanceof Object //true

你发现没有:这就是原型链啊!!!

L1.__proto__指向R1.prototype

R1.prototype.__proto__指向R2.prototype

...

Rn.prototype.__proto__指向Object.prototype

Object.prototype.__proto__指向null

这样子就把原型串起来啦,也就是实现了继承。也就是为什么所有对象都要toString方法,因为这个方法在Object.prototype上面啊啊啊啊。

总结

至此,相信你已经完全理解原型和原型链了。当然,只是理解不实践是没用的。在下一篇,我们将利用原型来实现类与继承。