前言

JavaScript这门语言除了基本类型都是对象,可以说JavaScript核心就是对象,因此理解JavaScript对象及其种种特性至关重要,这是内功。本文介绍了我对es5对象,原型, 原型链,以及继承的理解

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

什么是JavaScript对象

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

js里除了基本类型外所有事物都是对象:

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

  • 数组是对象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);//Mofanobj.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);//Mofan19person.print();//Mofan19复制代码
  • 原型模式:

    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);//Mofan19person.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);//Mofan19person.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();//Mofan19person.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一个对象,都会有这个方法,太浪费内存了。可以这样修改一下构造器模式:

    //构造器方法2function 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);//Mofan19person.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);//undefinedconsole.log(a.__proto__===Object.prototype);//true//只有函数对象有prototype属性console.log(fun.prototype);//Objectconsole.log(fun.__proto__===Function.prototype);//true
​console.log(fun.prototype.__proto__===Object.prototype);//trueconsole.log(fun.__proto__.__proto__===Object.prototype);//trueconsole.log(Function.prototype.__proto__===Object.prototype);//trueconsole.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);//undefinedconsole.log(person1.__proto__===Person.prototype);//trueconsole.log(person1.__proto__.__proto__===Object.prototype);//trueconsole.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);//superconsole.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);//10console.log(instance2.superValue);["big", "large"]console.log(instance2.id);//11console.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);//10console.log(instance2.superValue);["big", "large"]console.log(instance2.id);//11console.log(instance1.getSuperValue());["big", "large", "super"]console.log(instance1.getSubValue());//subconsole.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);//10console.log(instance2.superValue);//["big", "large"]console.log(instance2.id);//11console.log(instance1.getSuperValue());//["big", "large", "super"]console.log(instance1.getSubValue());//subconsole.log(instance2.getSuperValue());//["big", "large"]console.log(instance2.getSubValue());//sub-sub复制代码

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

    console.log(SubClass.prototype.__proto__===SuperClass.prototype);//tureconsole.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/…

我对javascript对象的理解相关推荐

  1. JavaScript对象的理解

    在JavaScript语言中,可用于创建和使用的对象共有三类,分别为:本地对象.内置对象和宿主对象. (更好的阅读体验,请移步我的个人博客) 一,本地对象(非静态对象,需要手动创建(new)才能使用) ...

  2. 【javascript】深入理解对象

    为什么80%的码农都做不了架构师?>>>    今天学习的主题是 JavaScript对象. 要创建一个JavaScript对象大家应该都觉得很简单,直接写上一行 var obj = ...

  3. 如何更好地理解Javascript对象的自有属性和原型继承属性

    Javascript对象具有"自有属性"(own property),也有一些属性是从原型对象继承而来的.为了更好地理解对象自有属性和继承属性下面的示例深入解释了属性的访问和设置细 ...

  4. javascript之异步操作理解---回调函数,async,await以及promise对象

    javascript之异步操作理解---回调函数,async,await以及promise对象 概述 概述 写在前面:虽然平时做项目,但是发现自己写的代码还是很烂.最近接触了一个对性能要求比较高的项目 ...

  5. JavaScript对象理解

    1.对象 javascript对象:有助于组织信息 对象仅仅是一种特殊的数据类型?⒂涤幸幌盗械氖粜院头椒ā?lt;br /> 访问对象的属性:对象名.属性名 对象的方法调用:对象名.方法名() ...

  6. javaScript 对象添加属性和创建js对象的方式(以及理解:“无法给构造函数添加新的属性“)

    1.javaScript 对象想要添加属性,非常简单 (1)直接添加,使用语法:objectName.propertyName 添加属性. 举例: var person = new Object(); ...

  7. 后处理程序文件大小的变量_【每日一题】(17题)面试官问:JS中事件流,事件处理程序,事件对象的理解?...

    关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 2020,实「鼠」不易 2021,「牛」转乾坤 风劲潮涌当扬帆,任重道远须奋蹄! 一.前言 2020.12.23 立 ...

  8. JavaScript对象

    对象 javaScript中的对象,和其它编程语言中的对象一样,可以比照现实生活中的对象来理解.在JavaScript中,一个对象可以是一个单独拥有属性和类型的实体.和杯子做一下比较,一个杯子是一个对 ...

  9. 《JavaScript启示录》——第1章 JavaScript对象 1.1创建对象

    本节书摘来自异步社区<JavaScript启示录>一书中的第1章,第1.1节,作者:[美]Cody Lindley著,更多章节内容可以访问云栖社区"异步社区"公众号查看 ...

最新文章

  1. Hibernate关联关系映射实例速查
  2. 同软件多个线程设置不同ip_软件测试如何自学?收下这份《2020千锋性能测试入门视频教程》...
  3. Java中TreeMap和TreeSet的底层实现
  4. BZOJ:4820: [Sdoi2017]硬币游戏BZOJ:1444: [Jsoi2009]有趣的游戏(高斯消元求概率)
  5. mysql序列号生成_超详细的mysql数据库GTID介绍—概念、优缺点、原理、生命周期等
  6. 关于Chrome Devtools你可能有所不知的几个技巧
  7. 服务器tomcat/mysql的一些有关命令
  8. Y 组合子详解 (The Y Combinator)
  9. 追剪算法C语言,基于PLC的追剪控制系统设计.doc
  10. 【渝粤教育】电大中专混凝土结构_1作业 题库
  11. 通过股票数据接口如何看懂Level-2行情?
  12. 技术分享 | gh-ost 在线 ddl 变更工具​
  13. Python爬取虎牙主播图片
  14. 大数据信息资料采集:阿里巴巴1688电商网站货源产品信息采集
  15. 计算机毕业设计、课程设计之 [含论文+开题+任务书+中期检查+ppt+源码等]S2SH+mysql城市公交管理系统
  16. jeesite使用说明
  17. linux网络流量统计,linux下网络流量监控统计
  18. 华为Cloud BU总裁郑叶来:云服务低价竞争会回归理性
  19. 干货|4个全网最实用的OCR图片文字识别软件合集
  20. 公司笔试题练习AWS

热门文章

  1. easyui-treegrid移除树节点出错
  2. LINUX下c语言调用math.h库函数的注意事项
  3. 动效设计的物理法则:动画的一切皆在于时间点和空间幅度(转)
  4. linux字符cdev和Inode的关系
  5. Hibernate和Mysql5.1以上版本创建表出错 type=InnDB
  6. 【javascript】深入理解对象
  7. [svc]gns3模拟器及探讨几个bgp问题
  8. Workaround for 1701 Cannot truncate a table referenced in a foreign key constraint using doctrine:
  9. Android开发资料学习(转载/链接)
  10. MySQL------MySQL与SQLServer数据类型的转换