当前位置: 移动技术网 > IT编程>开发语言>JavaScript > js原型链和vue构造函数

js原型链和vue构造函数

2018年10月25日  | 移动技术网IT编程  | 我要评论

这篇文章首先会讲到原型链以及原型链的一些概念,然后会通过分析vue的源码,来看一下vue的构造函数是如何被创建的,now we go!

 

 

一、什么是原型链?

 

简单回顾下构造函数,原型和实例的关系:
     每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针.
 
然鹅,在js对象里有这么一个规则:
  如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性.
 
少废话,先来看个例子:
  
    function fun1 () {
        this.win = "skt"
    }
    fun1.prototype.getval = function () {
        return this.win
    }
    function fun2 () {
        this.other_win = "rng"
    }
    fun2.prototype = new fun1 ()
    fun2.prototype.getotherval = function () {
        return this.other_win
    }
    let instance = new fun2()
    console.log(instance.getval())  //skt
在上述例子中,有一个很有意思的操作,我们让原型对象指向了另一个类型的实例,即: constructor1.property = instance2
 
那么他是怎么找到instance.getval()的?这中间又发生了什么?
 
  1).首先会在instance1内部属性中找一遍;
  2).接着会在instance1.__proto__(constructor1.prototype)中找一遍,而constructor1.prototype 实际上是instance2, 也就是说在instance2中寻找该属性;
  3).如果instance2中还是没有,此时程序不会灰心,它会继续在instance2.__proto__(constructor2.prototype)中寻找...直至object的原型对象
 
    搜索轨迹: instance1--> instance2 --> constructor2.prototype…-->object.prototype

 

这种搜索的轨迹,形似一条长链, 又因prototype在这个游戏规则中充当链接的作用,于是我们把这种实例与原型的链条称作 原型链
 
 
二、prototype 和 __proto__ 都是个啥?
 
1.prototype是函数才有的属性
    let fun = function () {}
    console.log(fun.prototype) // object
    console.log(fun.__proto__) // function
2.__proto__是对象具有的属性,但__proto__不是一个规范的属性,对应的标准属性是 [[prototype]]
    let obj = {}
    console.log(obj.prototype) // underfined
    console.log(obj.__proto__) // object
 
我们可以把__proto__理解为构造器的原型,大多数情况下 __proto__ === constructor.prototype      ( object.create()除外 )
 
 
三、new又是个什么鬼?
 
我们都知道new是一个实例化的过程,那么他是怎么实例化的?下面我们来看一个简单的例子:
 
    function fun() {
        this.team = "rng"
    }
    let f = new fun()
    console.log(f.team) // rng

上述代码中,我们通过new命令实例化了一个叫fun的函数并赋值给f,这个新生成的实例对象f从构造函数fun中得到了team属性,其实构造函数内部的this,就代表了新生成的实例对象,所以我们打印f.team的值就取到了rng这个值

这又是哪门子原理?答案如下?

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。这也是为什么构造函数叫"构造函数"的原因,就是操作一个空对象(即this对象),将其“构造”为所需要的样子。

 

如果我不加new呢?

    function fun() {
        this.team = "rng"
    }
    let f = fun()
    console.log(f) // undefined
    console.log(team) // rng

我们可以看出上面打印f为undefined,而team却有值,这又是为什么?

 

其实在这种情况下,构造函数就变成了普通的函数,而且不会被实例.而此时的this指向了全局,team就变成了全局变量,因此我们取到了值

 

四、 __proto__指向哪?

  说到__proto__的指向问题,还得取决于该对象创建时的实现方式.

  辣么,到底有那些实现方式?

1.字面量方式
    let obj = {}
    console.log(obj.__proto__)  // object
    console.log(obj.__proto__ === obj.constructor.prototype) // true 证明用字面量创建的函数,他的__proto__ 等于 该对象构造器的原型

2.构造器方式

    function func () {}
    let a = new func()
    console.log(a.__proto__) // object
    console.log(a.__proto__ === a.constructor.prototype) // true 
3.object.create()方式
    let obj1 = {name:"rng"}
    let obj2 = object.create(obj1)
    console.log(obj2.__proto__) //{name: "rng"}
    console.log(obj2.__proto__ === obj2.constructor.prototype) // false

  注: object.create(prototype, descriptors) 创建一个具有指定原型且可选择性地包含指定属性的对象

 

五、如何确定原型和实例的关系?

  想要确定原型和实例的关系,坦率的讲,有两种方式:  instance  和  isprototype() 
 
1.instanceof
 
  我们用这个操作符来测试实例(instance)与原型链中出现过的构造函数,如果出现过则返回true,反之则为false
 
  来来来,我们来测试一下:
    function fun1 () {
        this.laji = "uzi"
    }
    function fun2 () {
        this.strong = "faker"
    }
    fun2.prototype = new fun1()

    let fun2 = new fun2 ()

    console.log(fun2 instanceof fun1)    // true
    console.log(fun2 instanceof fun2)    // true
    console.log(fun2 instanceof object)  // true

由于原型链的关系,我们可以说fun2是一个对象object,fun1或是fun2中任何一个类型的实例,所以这三个结果都返回了true

 
2.isprototype()
 
  这个方法同样,只要是原型链中出现过的原型,该方法就会返回true,用法如下
 
    console.log(fun1.prototype.isprototypeof(fun2))  // true
    console.log(fun2.prototype.isprototypeof(fun2))  // true
    console.log(object.prototype.isprototypeof(fun2))// true

 

六、原型链的问题

 

  什么?原型链还有问题?买了佛冷,why?让我们来看一下:

  问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

  问题二:在创建子类型时,不能向超类型的构造函数中传递参数.

 

七、如何解决原型链问题?

 

1.借用构造函数,也叫经典继承
 
  基本思想: 在子类型构造函数的内部调用超类型构造函数
 
  函数只是在特定环境中执行的代码的对象,因此通过使用 apply() 和 call() 方法也可以在(将来)新创建的对象上执行构造函数
 
看例子:

 

    function father () {
        this.team = ["letme","mlxg"]
    }
    function son () {
        father.call(this)
    }
    let son = new son()
    son.team.push("uzi")
    console.log(son.team)        //  ["letme", "mlxg", "uzi"]
    let little_son = new son()   
    console.log(little_son.team) //  ["letme", "mlxg"]
我们可以看出,借用构造函数一举解决了原型链的两大问题:
  其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;
  其二, 子类型创建时也能够向父类型传递参数.
但是还还还有一个问题,如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题:
  方法都在构造函数中定义, 因此函数复用也就不可用了.而且超类型(如father)中定义的方法,对子类型而言也是不可见的. so,借用构造函数的技术也很少单独使用.
 
2.组合继承

  组合继承, 有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者优点的一种继承模式.

  基本思想: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.

  这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性.
 
接着看例子:
    function father (team) {
        this.team = team
        this.people = ["mlxg","letme"]
    }
    father.prototype.sayteam = function () {
        return console.log(this.team)
    }
    function son (team,age) {
        this.age = age
        father.call(this,team)
    }

    son.prototype = new father()
    son.prototype.sayage = function () {
        return console.log(this.age)
    }
    let son = new son("faker",8)
    son.people.push("uzi")
    console.log(son.people)   // ["mlxg", "letme", "uzi"]
    son.sayage()              //8
    son.sayteam()             // faker

    let little_son = new son("bang",3)
    console.log(little_son.people)  // ["mlxg", "letme"] 
    little_son.sayage()             // 3
    little_son.sayteam()            // bang

  我们可以看出,组合继承既保证了引用类型不再被所有实例所共享,也能够让子类型创建时向父类型传参,同时,原型中的方法又能够被复用,可以说是避免了原型链中的两大问题以及借用构造函数的缺陷,因此他也是js中最常用的继承方式,而且

instanceof 和 isprototypeof( )也能用于识别基于组合继承创建的对象.
 
3.原型继承
 
  基本思想: 借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型
 
  绳么意思?
 
  比如我们在fun()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.
 
 
        function fun(o){
            function f(){}
            f.prototype = o;
            return new f();
        }
let obj = {arr:[11,22] fun(obj).arr.push(33) console.log(fun(obj).arr) // [11,22,33]
  在这个例子中,可以作为另一个对象基础的是obj对象,于是我们把它传入到fun()函数中,然后该函数就会返回一个新对象. 这个新对象将arr作为原型,因此它的原型中就包含引用类型值属性. 然后我们向该属性中又增加了一个元素,所以我们能够将它打印出来
 
       *在原型继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式一样.
4.寄生式继承
 
  基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象
    function fun(o){
        function f(){}
        f.prototype = o;
        return new f();
    }
    let obj = {a:[11,22]}

    function createanother(z) {
        // 通过调用函数创建一个新对象
        var clone = fun(z);
        clone.sayhi = function () {
            alert("hi");
        }

        return clone;
    }

    createanother(obj)
 

  上面的例子中,我们把obj传入createanother()函数中,返回的新对象clone不仅拥有了该属性,而且还被增强了,拥有了sayhi()方法;

 

  等一下,这里要注意: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.

 

5.寄生组合式继承

  前面讲过,组合继承是 javascript 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部. 寄生组合式继承就是为了降低调用父类构造函数的开销而诞生的
 
  基本思想:不必为了指定子类型的原型而调用超类型的构造函数
 
    function inheritprototype(subtype, supertype) {
        var prototype = object.create(supertype.prototype);    //创建对象
        prototype.constructor = subtype;                       //增强对象
        subtype.prototype = prototype;                         //指定对象
    }


    function father(name) {
        this.name = name;
        this.colors = ["red", "blue", "green"];
    }
    father.prototype.sayname = function () {
        console.log(this.name);
    }

    function son(name, age) {
        father.call(this, name);

        this.age = age;
    }
    inheritprototype(son, father)
    son.prototype.sayage = function () {
        console.log(this.age);
    }

    var instance = new son("uzi", 3);
    instance.sayname(); //uzi
    instance.sayage();  //3

   inheritprototype函数接收两个参数:子类型构造函数和超类型构造函数。

    1. 创建超类型原型的副本。

    2. 为创建的副本添加constructor属性,弥补因重写原型而失去的默认的constructor属性

    3. 将新创建的对象(即副本)赋值给子类型的原型

  inheritprototype的高效率体现在它没有调用superclass构造函数,因此避免了在subclass.prototype上面创建不必要多余的属性. 同时,原型链还能保持不变,可以说是相当奈斯


  由于寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法.
 
八.vue构造函数

我们在使用的vue的时候,经常会用new操作符去将他实例化,这说明vue也是一个构造函数,那么他是如何被创建的呢?我怀着无比激动的心情clone了vue的源码,仔细研究了一番

vue源码地址

我首先找到了src/core/instance/index.js文件,打开一看,惊了


在第八行代码中,创建了一个vue的函数,这不就是vue的构造函数么,而且在12行的警告中我更加肯定了,他说:vue是一个构造函数,应该使用“new”关键字调用
然后他在下面,他分别在
  initmixin()
  statemixin()
  eventsmixin()
  lifecyclemixin()
  rendermixin()
 
 这五个方法中讲vue作为形参传入,最后将vue导出.
那么这五个方法是干什么的呢?我们先来看看initmixin()方法,打开./init.js文件,找到该方法


其他的代码我们先不管,我们就看该方法的前几行,他在vue的原型中注入了_init方法,这个方法有点眼熟,我们好像在哪见过,对,就是刚才的index.js文件中

 

这个this_init(options)看上去像是一个内部初始化的一个方法,而option应该就是初始化时的一些配置项了,在vue被实例化的时候,this._init()方法就会执行

 

接下来,我们来看一下./state.js文件,找到statemixin方法

 

我们们先看最后两行,他使用object.defineproperty方法里面传了三个参数:vue的原型,$data和$props以及datadef和propsdef,object.defineproperty顾名思义就是在对象里定义原型,所以这个方法应该就是将$data和$props注入vue的原型中.这两个属性的定义分别写在了 datadef 以及 propsdef 这两个对象里,我们来看一下这两个对象的定义,首先是 get :

   从中我们可以看出,datadef.get方法返回了this._data,也就是说,$data 属性实际上代理的是 _data 这个实例属性,而同理,$props 代理的是 _props 这个实例属性。然后有一个是否为生产环境的判断,如果不是生产环境的话,就为 $data 和 $props 这两个属性设置一下 set,给了你一个提示,避免替换实例根$data。' + "使用嵌套数据属性",实际上就是想告诉你:别碰我.所以说,$data和$rop都是只读属性.

  然后,他又向vue的原型里注入了$set啊,$delete啊还有$watch,这些你在看vue的api的时候应该见过吧

 

下面,我们看下一个方法eventsmixin .打开./events.js文件

不难发现,eventsmixin方法在vue的原型中分别写入了$on,$once,$off和$emit这四个方法

 

下一个是lifecyclemixin方法,打开./lifecycle.js文件找到该方法,这个方法在vue.prototype上添加了三个方法:_update,$forceupdate以及$distroy

 

 

最后一个就是 rendermixin 方法了,它在 render.js 文件中

 

 

这个方法的一开始以 vue.prototype为参数调用了 installrenderhelpers 函数,我们可以从这个文件开始看到这个函数来自./render-helpers/index.js 文件,打开这个文件找到 installrenderhelpers 函数:

 

  以上代码就是 installrenderhelpers 函数的源码,可以发现,这个函数的作用就是在 vue.prototype 上添加一系列方法,这些方法貌似render函数被创建时需要用到的一些方法.

  rendermixin 方法在执行完 installrenderhelpers 函数之后,又在 vue.prototype 上添加了两个方法,分别是:$nexttick 和 _render,至此,instance/index.js 文件中的代码就运行完毕了(所谓的运行,是指执行 npm run dev命令时构建的运行)。

我们大致可以得出一个结论:vue官方通把vue的一些方法和属性做了分类,通过不同*mixin方法,将这些方法挂载到了vue原型中,而每个 *mixin 方法的作用其实就是包装 vue.prototype,这就是vue构造函数被创建的过程.

 

以上理解可能有的不到位或者错误的地方,欢迎留言指正,3q.

 

 

 

 

 

 



 
 
 
 
 

 

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网