我对javascript对象的理解

前言

JavaScript这门语言除了基本类型都是对象,能够说JavaScript核心就是对象,所以理解JavaScript对象及其种种特性相当重要,这是内功。本文介绍了我对es5对象,原型, 原型链,以及继承的理解javascript

注意(这篇文章特别长)这篇文章仅仅是我我的对于JavaScript对象的理解,并非教程。这篇文章写于我刚了解js对象以后。文章确定有错误之处,还望读者费心指出,在下方评论便可^-^html

什么是JavaScript对象

var person = {   //person就是对象,对象都有各类属性,每一个属性又都对应着本身的值
    //键值对形式
    name: "Mofan",//能够包含字符串
    age: 20,//数字
    parents: [  //数组
        "Daddy",
        "Mami",
    ]
    sayName: function(){  //函数
        console.log(this.name);
    },
    features: {   //对象
        height: "178cm",
        weight: "60kg",
    }
}复制代码

js里除了基本类型外全部事物都是对象:java

  • 函数是对象function sayName(){} ——sayName是函数对象web

  • 数组是对象var arr = new Array() ——arr是数组对象编程

为何JavaScript要这么设计呢?我以为首先这样一来,统一了数据结构,使JavaScript成为一门编程风格很是自由化的脚本语言:不管定义什么变量,通通var;其次,JavaScript对象都有属性和方法,函数数组都是对象,调用引用就会很是灵活方便;再者,为了构建原型链?数组

建立对象的几种方式

  • Object()模式使用对象字面量:var obj={...}就像上面那样或者使用原生构造函数Object():浏览器

var person = new Object();
    person.name = "Mofan";
    person.sayName = function(){
        console.log(this.name);
        };
    console.log(person.name);//Mofan
    obj.sayName();//Mofan
    复制代码
  • 利用函数做用域使用自定义构造函数模式模仿类(构造器模式):数据结构

function Person(name,age){
        this.name = name;
        this.age = age;
        this.print = function(){
            console.log(this.name + this.age)
            };
    }
    var person = new Person("Mofan",19);
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19复制代码
  • 原型模式:wordpress

function Person(){}
    //能够这样写
    /*Person.prototype.name = "Mofan"; Person.prototype.age = 19; Person.prototype.print = function(){ console.log(this.name+this.age); }*/
    //推荐下面这样写,但两种方式不能混用!由于下面这种方式实际上重写了
    //Person原型对象,若是二者混用,后面赋值方式会覆盖前面赋值方式
    Person.prototype = {
        name:"Mofan",
        age:19,
        print:function(){
            console.log(this.name+this.age);
        }
    }
    var person = new Person();
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19复制代码
  • 组合构造函数模式和原型模式:函数

function Person(name,age){
        //这里面初始化属性
        this.name = name;
        this.age = age;
        ...
    }
    Person.prototype = {
        //这里面定义公有方法
        print:function(){
            console.log(this.name+this.age);
        },
        ...
    }
    var person = new Person("Mofan",19);
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19复制代码
  • 动态建立原型模式:

function Person(name,age){
        //初始化属性
        this.name = name;
        this.age = age;
        //在建立第一个对象(第一次被调用)时定义全部公有方法,之后再也不调用
        if(typeof this.print !="function"){
            Person.prototype.print =function(){
                    console.log(this.name+this.age);
                };
            Person.prototype.introduction=function(){
                    console.log("Hi!I'm "+this.name+",I'm "+this.age);
                };
                //若是采用对象字面量对原型添加方法的话,第一次建立的对象将不会有这些方法
            };
            
        
    }
    var person = new Person("Mofan",19);
    person.print();//Mofan19
    person.introduction();//Hi!I'm Mofan,I'm 19复制代码

还有一些模式用的场景比较少

这些模式的应用场景

怎么会有这么多的建立模式?实际上是由于js语言太灵活了,所以前辈们总结出这几种建立方式以应对不一样的场景,它们各有利弊。

  • 第一种方式,使用字面量或者使用构造函数Object()经常使用于建立普通对象存储数据等。它们的原型都是Object,彼此之间没有什么关联。事实上,下面建立方式都是同样的:

var o1 = {};//字面量的表现形式
    var o2 = new Object;
    var o3 = new Object();
    var o4 = new Object(null);
    var o5 = new Object(undefined);
    var o6 = Object.create(Object.prototype);//等价于 var o = {};//即以 Object.prototype 对象为一个原型模板,新建一个以这个原型模板为原型的对象复制代码

  • 第二种方式,利用函数做用域模仿类,这样就能够在建立对象时传参了,能够建立不一样属性值得对象,实现对象定制。不过print方法也定义在了构造函数里面,若是要把它当作公有方法的话,这样每new一个对象,都会有这个方法,太浪费内存了。能够这样修改一下构造器模式:

//构造器方法2
    function print(){      //定义一个全局的 Function 对象,把要公有的方法拿出来
         console.log(this.name + this.age);
    }
    
    function Person(name,age){
        this.name = name;
        this.age = age;
   
        this.print = print.bind(this);//每一个 Person 对象共享同一个print 方法版本(方法有本身的做用域,不用担忧变量被共享)
    }
    var person = new Person("Mofan",19);
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19
    复制代码

然而这样看起来很乱,也谈不上类的封装性。仍是使用原型吧

  • 第三种方式,纯原型模式,不论是属性仍是方法都添加到原型里面去了,这样作好处是很省内存,可是应用范围就少了,更多的对象 内部的属性是须要定制的,并且一旦更改原型,全部这个原型实例都会跟着改变。所以能够结合构造函数方式来实现对对象的定制,因而就有了第四种方式——组合构造函数模式与原型模式,能够定制的放在构造器里,共有的放在原型里,这也符合构造器和原型的特性。“这是es5中使用最普遍、认同度最高的建立自定义类型的方法”---《JavaScript高级程序设计》第三版

  • 第五种方式,动态原型模式,出现这种方式是由于有些面向对象开发人员习惯了类构造函数,因而对这种独立出来的构造函数和原型感到困惑和不习惯。因而,就出现了把定义原型也写进构造函数里的动态原型模式。上面在动态原型模式程序里面讲“若是采用对象字面量对原型添加方法的话,第一次建立的对象将不会有这些方法”这是由于在if语句执行之前,第一个对象已经被建立了,而后执行if里面的语句,若是采用对象字面量给原型赋值,就会致使原型在实例建立以后被重写,建立的第一个实例就会失去与原型的连接,也就没有原型里的方法了。不过之后建立的对象就可使用原型里的方法了,由于它们都是原型被修改后建立的。

原型是什么

在JavaScript中,原型就是一个对象,不必把原型和其余对象区别对待,只是经过它能够实现对象之间属性的继承。任何一个对象也能够成为原型。之因此常常说对象的原型,实际上就是想找对象继承的上一级对象。对象与原型的称呼是相对的,也就是说,一个对象,它称呼继承的上一级对象为原型,它本身也能够称做原型链下一级对象的原型。

一个对象内部的[[Prototype]]属性生来就被建立,它指向继承的上一级对象,称为原型。函数对象内部的prototype属性也是生来就被建立(只有函数对象有prototype属性),它指向函数的原型对象(不是函数的原型!)。当使用var instance = new Class();这样每new一个函数(函数被当作构造函数来使用)建立实例时,JavaScript就会把这个原型的引用赋值给实例的原型属性,因而实例内部的[[Prototype]]属性就指向了函数的原型对象,也就是prototype属性。

原型真正意义上指的是一个对象内部的[[Prototype]]属性,而不是函数对象内部的prototype属性,这二者之间没有关系!对于一个对象内部的[[Prototype]]属性,不一样浏览器有不一样的实现:

var a = {}; 
 
     //Firefox 3.6+ and Chrome 5+ 
     Object.getPrototypeOf(a); //[object Object] 
     
     //Firefox 3.6+, Chrome 5+ and Safari 4+ 
    a.__proto__; //[object Object] 
     
     //all browsers 
     a.constructor.prototype; //[object Object]复制代码

之因此函数对象内部存在prototype属性,而且能够用这个属性建立一个原型,是由于这样以来,每new一个这样的函数(函数被当作构造函数来使用)建立实例,JavaScript就会把这个原型的引用赋值给实例的原型属性,这样以来,在原型中定义的方法等都会被全部实例共用,并且,一旦原型中的某个属性被定义,就会被全部实例所继承(就像上面的例子)。这种操做在性能和维护方面其意义是不言自明的。这也正是构造函数存在的意义(JavaScript并无定义构造函数,更没有区分构造函数和普通函数,是开发人员约定俗成)。下面是一些例子:

var a = {}    //一个普通的对象
    function fun(){}   //一个普通的函数
    //普通对象没有prototype属性
    console.log(a.prototype);//undefined
    console.log(a.__proto__===Object.prototype);//true
    
    //只有函数对象有prototype属性
    console.log(fun.prototype);//Object
    console.log(fun.__proto__===Function.prototype);//trueconsole.log(fun.prototype.__proto__===Object.prototype);//true
    console.log(fun.__proto__.__proto__===Object.prototype);//true
    console.log(Function.prototype.__proto__===Object.prototype);//true
    console.log(Object.prototype.__proto__);//null复制代码

当执行console.log(fun.prototype);输出为能够看到,每建立一个函数,就会建立prototype属性,这个属性指向函数的原型对象(不是函数的原型),而且这个原型对象会自动得到constructor属性,这个属性是指向prototype属性所在函数的指针。而__proto__属性是每一个对象都有的。

接着上面再看:

function Person(){}//构造函数,约定首字母大写
    var person1 = new Person();//person1为Person的实例console.log(person1.prototype);//undefined
    console.log(person1.__proto__===Person.prototype);//true
    console.log(person1.__proto__.__proto__===Object.prototype);//true
    console.log(person1.constructor);//function Person(){}
    
    //函数Person是Function构造函数的实例
    console.log(Person.__proto__===Function.prototype);//true
    //Person的原型对象是构造函数Object的实例
    console.log(Person.prototype.__proto__===Object.prototype);//true复制代码

person1和上面那个普通的对象a有区别,它是构造函数Person的实例。前面讲过:

当使用var instance = new Class();这样每new一个函数(函数被当作构造函数来使用)建立实例时,JavaScript就会把这个原型的引用赋值给实例的原型属性,因而实例内部的[[Prototype]]属性就指向了函数的原型对象,也就是prototype属性。

所以person1内部的[[Prototype]]属性就指向了Person的原型对象,而后Person的原型对象内部的[[Prototype]]属性再指向Object.prototype,至关于在原型链中加了一个对象。经过这种操做,person1就有了构造函数的原型对象里的方法。

另外,上面代码console.log(person1.constructor);//function Person(){}中,person1内部并无constructor属性,它只是顺着原型链往上找,在person1.__proto__里面找到的。

能够用下面这张图理清原型、构造函数、实例之间的关系:

继承

JavaScript并无继承这一现有的机制,但能够利用函数、原型、原型链模仿。下面是三种继承方式:

类式继承

//父类
    function SuperClass(){
        this.superValue = "super";
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子类
    function SubClass(){
        this.subValue = "sub";
    }
    //类式继承,将父类实例赋值给子类原型,子类原型和子类实例能够访问到父类原型上以及从父类构造函数中复制的属性和方法
    SubClass.prototype = new SuperClass();
    //为子类添加方法
    SubClass.prototype.getSubValue = function(){
        return this.subValue;
    }
    
    //使用
    var instance = new SubClass();
    console.log(instance.getSuperValue);//super
    console.log(instance.getSubValue);//sub复制代码

这种继承方式有很明显的两个缺点:

  • 实例化子类时没法向父类构造函数传参

  • 若是父类中的共有属性有引用类型,就会在子类中被全部实例所共用,那么任何一个子类的实例更改这个引用类型就会影响其余子类实例,可使用构造函数继承方式解决这一问题

构造函数继承

//父类
    function SuperClass(id){
        this.superValue = ["big","large"];//引用类型
        this.id = id;
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子类
    function SubClass(id){
        SuperClass.call(this,id);//调用父类构造函数并传参
        this.subValue = "sub";
    }
     var instance1 = new SubClass(10);//能够向父类传参
     var instance2 = new SubClass(11);
     
    instance1.superValue.push("super");
    console.log(instance1.superValue);//["big", "large", "super"]
    console.log(instance1.id);//10
    console.log(instance2.superValue);["big", "large"]
    console.log(instance2.id);//11
    console.log(instance1.getSuperValue());//error复制代码

这种方式是解决了类式继承的缺点,不过在代码的最后一行你也看到了,没有涉及父类原型,所以违背了代码复用的原则。因此组合它们:

组合继承

function SuperClass(id){
        this.superValue = ["big","large"];//引用类型
        this.id = id;
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子类
    function SubClass(id,subValue){
        SuperClass.call(this,id);//调用父类构造函数并传参
        this.subValue = subValue;
    }
     SubClass.prototype = new SuperClass();
      SubClass.prototype.getSubValue = function(){
        return this.subValue;
    }
    
     var instance1 = new SubClass(10,"sub");//能够向父类传参
     var instance2 = new SubClass(11,"sub-sub");
​
    instance1.superValue.push("super");
    console.log(instance1.superValue);//["big", "large", "super"]
    console.log(instance1.id);//10
    console.log(instance2.superValue);["big", "large"]
    console.log(instance2.id);//11
    console.log(instance1.getSuperValue());["big", "large", "super"]
    console.log(instance1.getSubValue());//sub
    console.log(instance2.getSuperValue());//["big", "large"]
    console.log(instance2.getSubValue());//sub-sub复制代码

嗯,比较完美了,可是有一点,父类构造函数被调用了两次,这就致使第二次调用也就是建立实例时重写了原型属性,原型和实例都有这些属性,显然性能并很差。先来看看克罗克福德的寄生式继承:

function object(o){
        function F(){};
        F.prototype = o;
        return new F();
   }
    function createAnnther(original){
        var clone = object(original);
        clone.sayName = function(){
            console.log(this.name);
        }
        return clone;
   }
    var person = {
        name:"Mofan",
        friends:["xiaoM","Alice","Neo"],
   };
    var anotherPerson = createAnnther(person);
    anotherPerson.sayName();//"Mofan"
}复制代码

就是让一个已有对象变成新对象的原型,而后再在createAnother函数里增强。你也看到了,person就是一个普通对象,因此这种寄生式继承适合于根据已有对象建立一个增强版的对象,在主要考虑经过已有对象来继承而不是构造函数的状况下,这种方式的确很方便。但缺点也是明显的,createAnother函数不能复用,我若是想给另一个新建立的对象定义其余方法,还得再写一个函数。仔细观察一下,其实寄生模式就是把原型给了新对象,对象再增强。

等等,写到这个地方,我脑子有点乱,让咱们回到原点:继承的目的是什么?应该继承父类哪些东西?我以为取决于咱们想要父类的什么,我想要父类所有的共有属性(原型里)而且能够自定义继承的父类私有属性(构造函数里)!前面那么多模式它们的缺点主要是由于这个:

SubClass.prototype = new SuperClass();复制代码

那为何要写这一句呢?是只想要继承父类的原型吗?若是是为何不这么写:

SubClass.prototype = SuperClass.prototype;复制代码

这样写是能够继承父类原型,可是风险极大:SuperClass.prototype属性它是一个指针,指向SuperClass的原型,若是把这个指针赋给子类prototype属性,那么子类prototype也会指向父类原型。对SubClass.prototype任何更改,就是对父类原型的更改,这显然是不行的。

寄生组合式继承

但出发点没错,能够换种继承方式,看看上面的寄生式继承里的object()函数,若是把父类原型做为参数,它返回的对象实现了对父类原型的继承,没有调用父类构造函数,也不会对父类原型产生影响,堪称完美。

function object(o){
        function F(){};
        F.prototype = o;
        return new F();
   }
    function inheritPrototype(subType,superType){
        var proto = object(superType.prototype);
        proto.constructor = subType;//矫正一下construcor属性
        subType.prototype = proto;
   }
​
   function SuperClass(id){
        this.superValue = ["big","large"];//引用类型
        this.id = id;
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子类
    function SubClass(id,subValue){
        SuperClass.call(this,id);//调用父类构造函数并传参
        this.subValue = subValue;
    }
   inheritPrototype(SubClass,SuperClass);//继承父类原型
    SubClass.prototype.getSubValue = function(){
        return this.subValue;
    }
    var instance1 = new SubClass(10,"sub");//能够向父类传参
     var instance2 = new SubClass(11,"sub-sub");
​
    instance1.superValue.push("super");
    console.log(instance1.superValue);//["big", "large", "super"]
    console.log(instance1.id);//10
    console.log(instance2.superValue);//["big", "large"]
    console.log(instance2.id);//11
    console.log(instance1.getSuperValue());//["big", "large", "super"]
    console.log(instance1.getSubValue());//sub
    console.log(instance2.getSuperValue());//["big", "large"]
    console.log(instance2.getSubValue());//sub-sub复制代码

解决了组合继承的问题,只调用了一次父类构造函数,并且还能保持原型链不变,为何这么说,看对寄生组合的测试:

console.log(SubClass.prototype.__proto__===SuperClass.prototype);//ture
    console.log(SubClass.prototype.hasOwnProperty("getSuperValue"));//false复制代码

所以,这是引用类型最理想的继承方式。

总结

建立用于继承的对象最理想的方式是组合构造函数模式和原型模式(或者动态原型模式),就是让可定义的私有属性放在构造函数里,共有的放在原型里;继承最理想的方式是寄生式组合,就是让子类的原型的[[prototype]]属性指向父类原型,而后在子类构造函数里调用父类构造函数实现自定义继承的父类属性。

JavaScript对象总有一些让我困惑的地方,不过我还会继续探索。我在此先把我了解的记录下来,与各位共勉。错误的地方请费心指出,我将感谢您的批评指正。

本文为做者原创,转载请注明本文连接,做者保留权利。

参考文献:[1] www.cnblogs.com/chuaWeb/p/5…[2] www.cnblogs.com/xjser/p/496…[3] javascriptweblog.wordpress.com/2010/06/07/…

相关文章
相关标签/搜索