精华内容
下载资源
问答
  • 重新认识java(一) ---- 万物皆对象

    万次阅读 多人点赞 2016-11-27 21:39:37
    如果你现实中没有对象,至少你在java世界里会有茫茫多的对象,听起来是不是很激动呢?

    如果你现实中没有对象,至少你在java世界里会有茫茫多的对象,听起来是不是很激动呢?

    对象,引用,类与现实世界

    现实世界里有许许多多的生物,非生物,跑的跳的飞的,过去的现在的未来的,令人眼花缭乱。我们编程的目的,就是解决现实生活中的问题。所以不可避免的我们要和现实世界中各种奇怪的东西打交道。

    在现实世界里,你新认识了一个朋友,你知道他长什么样,知道了他的名字年龄,地址。知道他喜欢干什么有什么特长。你想用java语言描述一下这个人,你应该怎么做呢?

    这个时候,就有了类的概念。每一个类对应现实世界中的某一事物。比如现实世界中有人。那么我们就创建一个关于“人”的类。

    每一个人都有名字,都有地址等等个人信息。那么我们就在“人”的类里面添加这些属性。

    每一个人都会吃,会走路,那么我们就在“人”的类里面添加吃和走的方法。

    当这个世界又迎来了一个新生命,我们就可以“new”一个“人”,“new”出来的就叫”对象“。

    每一个人一出生,父母就会给他取个名字。在程序里,我们需要用一种方式来操作这个“对象”,于是,就出现了引用。我们通过引用来操作对象,设置对象的属性,操作对象的方法。

    这就是最基本的面向对象。

    现实世界的事物】 —抽象—> 【 】—new—>【对象 】<—控制— 【引用

    从创建一个对象开始

    创建对象的前提是先得有一个类。我们先自己创建一个person类。

    //Person类
    public class Person {
        private String name;
        private int age;
    
        public void eat(){
            System.out.println("i am eating");
        }
    }

    创建一个person对象。

        Person p = new Person();

    怎么理解这句简单的代码呢?

    • new Person :一个Person类型的对象
    • () : 这个括号相当于调用了person的无参构造方法
    • p : Person对象的引用

    有的人会认为p就是new出来的Person对象。这是错误的理解,p只是一个Person对象的引用而已。那么问题来了,什么是引用?什么又是对象呢?这个要从内存说起。

    创建对象的过程

    java大体上会把内存分为四块区域:堆,栈,静态区,常量区。

    • 堆 : 位于RAM中,用于存放所有的java对象。
    • 栈 : 位于RAM中,引用就存在于栈中。
    • 静态区 : 位于RAM中,被static修饰符修饰的变量会被放在这里
    • 常量区 :位于ROM中, 很明显,放常量的。

    事实上,我们不需要关心java的对象,变量到底存在了哪里,因为jvm会帮我们处理好这些。但是理解了这些,有助于提高我们的水平。

    当执行这句代码的时候。

    Person p = new Person();

    首先,会在堆中开辟一块空间存放这个新来的Person对象。然后,会创建一个引用p,存放在栈中,这个引用p指向Person对象(事实上是,p的值就是Person对象的内存地址)。

    这样,我们通过访问p,然后得到了Person的内存地址,进而找到了Person对象。

    然后又有了这样一句代码:

    Person p2 = p;

    这句代码的含义是:
    创建了一个新的引用,保存在栈中,引用的地址也指向Person的地址。这个时候,你通过p2来改变Person对象的状态,也会改变p的结果。因为它们指向同一个对象。(String除外,之后会专门讲String)

    此时,内存中是这样的:

    这里写图片描述

    用一种很通俗的方式来讲解一下引用和对象。

    大家都应该用过windows吧。win有一个神奇的东西叫做快捷方式。我们桌面的图标大部分都是快捷方式。它并不是我们安装在电脑上的应用的可执行文件(不是.exe文件),那么为什么点击它可以打开应用程序呢?这个我不用讲了把。

    我们的对象和引用就和快捷方式和它连接的文件一样。

    我们不直接对文件进行操作,而是通过快捷方式来进行操作。快捷方式不能独立存在,同样,引用也不能独立存在(你可以只创建一个引用,但是当你要使用它的时候必须得给它赋值,否则它将毫无用处)。

    一个文件可以有多个快捷方式,同样一个对象也可以有多个引用。而一个引用只能同时对应一个对象。

    在java里,“=”不能被看成是一个赋值语句,它不是在把一个对象赋给另外一个对象,它的执行过程实质上是将右边对象的地址传给了左边的引用,使得左边的引用指向了右边的对象。java表面上看起来没有指针,但它的引用其实质就是一个指针。在java里,“=”语句不应该被翻译成赋值语句,因为它所执行的确实不是一个简单的赋值过程,而是一个传地址的过程,被译成赋值语句会造成很多误解,译得不准确。

    特例:基本数据类型

    为什么会有特例呢?因为用new操作符创建的对象会存在堆里,二在堆里开辟空间等行为效率较操作栈要低。而我们平时写代码的时候会经常创建一些“小变量”,比如int i = 1;如果每次都用Interger来new一个,效率不是很高而且浪费内存。

    所以针对这些情况,java提供了“基本数据类型”,基本数据类型一共有八种,每一个基本数据类型存放在栈中,而他们的值存放在常量区中。

    举个例子:

    int i = 2;
    int j = 2;

    我们需要知道的是,在常量区中,相同的常量只会存在一个。当执行第一句代码时。先查找常量区中有没有2,没有,则开辟一个空间存放2,然后在栈中存入一个变量i,让i指向2;

    执行第二句的时候,查找发现2已经存在了,所以就不开辟新空间了。直接在栈中保存一个新变量j,让j指向2;

    当然,java堆每一个基本数据类型都提供了对应的包装类。我们依旧可以用new操作符来创建我们想要的变量。

    Integer i = new Integer(1);
    Integer j = new Integer(1);

    但是,用new操作符创建的对象是不同的,也就是说,此时,i和j指向不同的内存地址。因为每次调用new操作符,都会在堆开辟新的空间。

    当然,说到基本数据类型,不得不提一下java的经典设计。

    先看一段代码:

    这里写图片描述

    为什么一个是true一个是false呢?

    我就不讲了,应该都知道吧。我就贴一个Integer的源码(jdk1.8)吧。

    这里写图片描述

    Integer 类的内部定义了一个内部类,缓存了从-128到127的所有数字,所以,你懂得。

    又一个特例 :String

    String是一个特殊的类,因为它被final修饰符所修饰,是一个不可改变的类。当然,看过java源码后你会发现,基本类型的各个包装类也被final所修饰。这里以String为例。

    我们来看这样一个例子

    这里写图片描述

    执行第一句 : 常量区开辟空间存放“abc”,s1存放在栈中指向“abc”

    执行第二句,s2 也指向 “abc”,

    执行第三句,因为“abc”已经存在,所以直接指向它。

    所以三个变量指向同一块内存地址,结果都为true。

    当s1内容改变的时候。这个时候,常量区开辟新的空间存放“bcd”,s1指向“bcd”,而s2和s3指向“abc”所以只有s2和s3相等。

    这种情况下,s1,s2,s3都是字符串常量,类似于基本数据类型。(如果执行的是s1 = “abc”,那么结果会都是true)

    我们再看一个例子:

    这里写图片描述

    执行第一行代码: 在堆里分配空间存放String对象,在常量区开辟空间存放常量“abc”,String对象指向常量,s1指向该对象。

    执行第二行代码:s2指向上一步new出来的string对象。

    执行第三行代码: 在堆里分配新的空间存放String对象,新对象指向常量“abc”,s3指向该对象。

    到这里,很明显,s1和s2指向的是同一个对象

    接着就很诡异了,我们让s1 依旧= “abc”,但是结果s1和s2指向的地址不同了。

    怎么回事呢?这就是String类的特殊之处了,new出来的String不再是上面的字符串常量,而是字符串对象。

    由于String类是不可改变的,所以String对象也是不可改变的,我们每次给String赋值都相当于执行了一次new String(),然后让变量指向这个新对象,而不是在原来的对象上修改。

    当然,java还提供了StringBuffer类,这个是可以在原对象上做修改的。如果你需要修改原对象,那么请使用StringBuffer类。

    值传递和引用传递的战争

    java是值传递还是引用传递的呢?毫无疑问,java是值传递的。那么什么又叫值传递和引用传递呢?

    我们先来看一个例子:

    这里写图片描述

    这是一个很经典的例子,我们希望调用了swap函数以后,a和b的值可以互换,但是事实上并没有。为什么会这样呢?

    这就是因为java是值传递的。也就是说,我们在调用一个需要传递参数的函数时,传递给函数的参数并不是我们传进去的参数本身,而是它的副本。说起来比较拗口,但是其实原理很简单。我们可以这样理解:

    一个有形参的函数,当别的函数调用它的时候,必须要传递数据。
    比如swap函数,别的函数要调用swap就必须传两个整数过来。

    这个时候,有一个函数按耐不住寂寞,扔了两个整数过来,但是,swap函数有洁癖,它不喜欢用别人的东西,于是它把传过来的参数复制了一份,然后对复制的数据修修改改,而别人传过来的参数动根本没动。

    所以,当swap函数执行完毕之后,交换了的数据只是swap自己复制的那一份,而原来的数据没变。

    也可以理解为别的函数把数据传递给了swap函数的形参,最后改变的只是形参而实参没变,所以不会起到任何效果。

    我们再来看一个复杂一点的例子(Person类添加了get,set方法):

    这里写图片描述

    可以看到,我们把p1传进去,它并没有被替换成新的对象。因为change函数操作的不是p1这个引用本身,而是这个引用的一个副本。

    你依然可以理解为,主函数将p1复制了一份然后变成了chagne函数的形参,最终指向新Person对象的是那个副本引用,而实参p1并没有改变。

    再来看一个例子:

    这里写图片描述

    这次为什么就改变了呢?分析一下。

    首先,new了一个Person对象,暂且叫他小明吧。然后p1指向小明。

    小明10岁了,随着时间的推移,小明的年龄要变了,调用了一下changgeAge方法,把小明的引用传了进去。

    传递的过程中,changgeAge也有洁癖,于是复制了一份小明的引用,这个副本也指向小明。

    然后changgeAge通过自己的副本引用,改变了小明的年龄。

    由于是小明这个对象被改变了,所以所有小明的引用调用方法得到的年龄都会改变

    所以就变了。

    最后简单的总结一下。

    java的传值过程,其实传的是副本,不管是变量还是引用。所以,不要期待把变量传递给一个函数来改变变量本身。

    对象的强引用,软引用,弱引用和虚引用

    Java中是JVM负责内存的分配和回收,这样虽然使用方便,程序不用再像使用c那样操心内存,但同时也是它的缺点(不够灵活)。为了解决内存操作不灵活这个问题,可以采用软引用等方法。

    先介绍一下这四种引用:

    • 强引用

      以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

    • 软引用(SoftReference)

      如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

      软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

    • 弱引用(WeakReference)

      如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

      弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    • 虚引用(PhantomReference)

      “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

      虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

    在实际开发中,弱引用和虚引用不常用,用得比较多的是软引用,因为它可以加速jvm的回收。

    软引用的使用方式:

    这里写图片描述

    关于软引用,我之后会单独写一篇文章,所以这里先一笔带过。

    对象的复制

    java除了用new来创建对象,还可以通过clone来复制对象。

    那么这两种方式有什么相同和不同呢?

    • new

    new操作符的本意是分配内存。程序执行到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。


    • clone

    clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
    如何利用clone的方式来得到一个对象呢?

    看代码:

    这里写图片描述

    对Person类做了一些修改

    看实现代码:

    这里写图片描述

    这样就得到了一个和原来一样的新对象。

    深复制和浅复制

    但是,细心并且善于思考的人可能一经发现了一个问题。

    age是一个基本数据类型,支架clone没什么问题,但是name可是一个String类型的啊。我们clone后的对象里的name和原来对象的name是不是指向同一个字符串常量呢?

    做个试验:

    这里写图片描述

    果然,是同一个对象。如果你不能理解,那么看这个图。

    这里写图片描述

    其实如果只是String还好,因为String的不可变性,当你随便修改一个值的时候,他们就会指向不同的地址了,但是除了String,其他都是可变的。这就危险了。

    上面的这种情况,就是浅克隆。这种方式在你的属性列表中有其他对象的引用的时候其实是很危险的。所以,我们需要深克隆。也就是说我们需要将这个对象里的对象也clone一份。怎么做呢?

    在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。

    //使用该工具类的对象必须要实现 Serializable 接口,否则是没有办法实现克隆的。
    public class CloneUtils {
    
        public static <T extends Serializable> T clone(T   obj){
            T cloneObj = null;
            try {
                //写入字节流
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                ObjectOutputStream obs = new   ObjectOutputStream(out);
                obs.writeObject(obj);
                obs.close();
    
                //分配内存,写入原始对象,生成新对象
                ByteArrayInputStream ios = new  ByteArrayInputStream(out.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(ios);
                //返回生成的新对象
                cloneObj = (T) ois.readObject();
                ois.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return cloneObj;
        }
    }

    使用该工具类的对象只要实现 Serializable 接口就可实现对象的克隆,无须继承 Cloneable 接口实现 clone() 方法。

    测试一下:

    这里写图片描述

    很完美

    这个时候,Person类实现了Serializable接口

    是否使用复制,深复制还是浅复制看情况来使用。

    关于序列化与反序列化以后会讲。


    这篇文章到这里就暂时告一段落了,后续有补充的话我会继续补充,有错误的话,我也会及时改正。欢迎大家提出问题。

    博客同步更新在http://blog.improvecfan.cn
    事例代码放在github:https://github.com/CleverFan/JavaImprove

    展开全文
  • 面向对象编程 Go 语言的面向对象编程(OOP)非常简洁而优雅。说它简洁,简介之处在于,它没有了OOP中很多概念,比如:继承、虚函数、构造函数和析构函数、隐藏的this指针等等。说它优雅,是它的面向对象(OOP)是语言...

    原文转自:http://www.ituring.com.cn/article/1339

    面向对象编程

    Go 语言的面向对象编程(OOP)非常简洁而优雅。说它简洁,简介之处在于,它没有了OOP中很多概念,比如:继承、虚函数、构造函数和析构函数、隐藏的this指针等等。说它优雅,是它的面向对象(OOP)是语言类型系统(type system)中的天然的一部分。整个类型系统通过接口(interface)串联,浑然一体。

    类型系统(type system)

    很少有编程类的书籍谈及类型系统(type system)这个话题。但实际上类型系统是整个语言的支撑,至关重要。

    类型系统(type system)是指一个语言的类型体系图。在整个类型体系图中,包含这些内容:

     基本类型。如byte、int、bool、float等等。
     复合类型。如数组(array)、结构体(struct)、指针(pointer)等。
     Any类型。即可以指向任意对象的类型。
     值语义和引用语义。
     面向对象。即所有具备面向对象特征(比如有成员方法)的类型。
     接口(interface)。

    类型系统(type system)描述的是这些内容在一个语言中如何被关联。比如我们聊聊Java的类型系统: 在Java语言中,存在两套完全独立的类型系统,一套是值类型系统,主要是基本类型,如byte、int、boolean、char、double、String等,这些类型基于值语义。一套是以Object类型为根的对象类型系统,这些类型可以定义成员变量、成员方法、可以有虚函数。这些类型基于引用语义,只允许new出来(只允许在堆上)。只有对象类型系统中的实例可以被Any类型引用。Any类型就是整个对象类型系统的根 —— Object类型。值类型想要被Any类型引用,需要装箱(Boxing)过程,比如int类型需要装箱成为Integer类型。只有对象类型系统中的类型才可以实现接口(方法是让该类型从要实现的接口继承)。

    在Go语言中,多数类型都是值语义,并且都可以有方法。在需要的时候,你可以给任何类型(包括内置类型)“增加”新方法。实现某个接口(interface)无需从该接口继承(事实上Go语言并没有继承语法),而只需要实现该接口要求的所有方法。任何类型都可以被Any类型引用。Any类型就是空接口,亦即 interface{}。 让我们一一道来。

    给类型增加方法

    在Go语言中,你可以给任意类型(包括内置类型,但指针类型除外)增加方法,例如:

    type Integer int
    
    func (a Integer) Less(b Integer) bool {
        return a < b
    }

    在这个例子中,我们定义了一个新类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法:Less。如此,你就可以让整型看起来像个类那样用:

    func main() {
        var a Integer = 1
        if a.Less(2) {
            fmt.Println(a, "Less 2")
        }
    }

    在学其他语言的时候,很多初学者对面向对象感到很神秘。我在给初学者介绍面向对象的时候,经常说到“面向对象只是一个语法糖”。以上代码用面向过程的方式来写是这样的:

    type Integer int
    
    func Integer_Less(a Integer, b Integer) bool {
    return a < b
    }
    
    func main() {
    var a Integer = 1
    if Integer_Less(a, 2) {
        fmt.Println(a, "Less 2")
    }
    }

    在Go语言中,面向对象的神秘面纱被剥得一干二净。对比这两段代码:

        func (a Integer) Less(b Integer) bool {  // 面向对象
        return a < b
    }
    
    func Integer_Less(a Integer, b Integer) bool {  // 面向过程
        return a < b
    }
    
    a.Less(2)  // 面向对象
    Integer_Less(a, 2)  // 面向过程

    你可以看出,面向对象只是换了一种语法形式来表达。在Go语言中没有隐藏的this指针。这句话的含义是:

    第一,方法施加的目标(也就是“对象”)显式传递,没有被隐藏起来。
    第二,方法施加的目标(也就是“对象”)不需要非得是指针,也不用非得叫this。

    我们对比Java语言的代码:

    class Integer {
        private int val;
        public boolean Less(Integer b) {
            return this.val < b.val;
        }
    }

    这段Java代码初学者会比较难懂,主要是因为Integer类的Less方法隐藏了第一个参数Integer* this。如果将其翻译成C代码,会更清晰:

    struct Integer {
        int val;
    };
    
    bool Integer_Less(Integer* this, Integer* b) {
        return this->val < b->val;
    }

    在Go语言中的面向对象最为直观,也无需支付额外的成本。如果要求对象必须以指针传递,这有时会是个额外成本,因为对象有时很小(比如4个字节),用指针传递并不划算。

    只有在你需要修改对象的时候,才必须用指针。它不是Go语言的约束,而是一种自然约束。举个例子:

    func (a *Integer) Add(b Integer) {
        *a += b
    }

    这里为Integer类型增加了Add方法。由于Add方法需要修改对象的值,所以需要用指针引用。调用如下:

    func main() {
        var a Integer = 1
    a.Add(2)
        fmt.Println("a =", a)
    }

    运行该程序得到的结果是:a = 3。如果你不用指针:

    func (a Integer) Add(b Integer) {
        a += b
    }

    运行程序得到的结果是:a = 1,也就是维持原来的值。究其原因,是因为Go和C语言一样,类型都是基于值传递。要想修改变量的值,只能传递指针。

    值语义和引用语义

    值语义和引用语义的差别在于赋值:

    b = a
    b.Modify()

    如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型。

    多数Go语言中的类型,包括:

     基本类型。如byte、int、bool、float32、float64、string等等。
     复合类型。如数组(array)、结构体(struct)、指针(pointer)等。

    都基于值语义。Go语言中类型的值语义表现得非常彻底。我们这么说是因为数组(array)。如果你学习过C语言,你会知道C语言中的数组(array)比较特别。通过函数传递一个数组的时候基于引用语义,但是在结构体中定义数组变量的时候是值语义(表现在结构体赋值的时候,该数组会被完整地拷贝一份新的副本)。

    Go语言中的数组(array)和基本类型没有区别,是很纯粹的值类型。例如:

    var a = [3]int{1, 2, 3}
    var b = a
    b[1]++
    fmt.Println(a, b)

    程序运行结果:[1 2 3] [1 3 3]。这表明b = a赋值语句是数组内容的完整拷贝。要想表达引用,需要用指针:

    var a = [3]int{1, 2, 3}
    var b = &a
    b[1]++
    fmt.Println(a, *b)

    程序运行结果:[1 3 3] [1 3 3]。这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int类型。

    Go语言中有4个类型比较特别,看起来像引用类型:

     切片(slice):指向数组(array)的一个区间。
     字典(map):极其常见的数据结构,提供key-value查询能力。
     通道(chan):执行体(goroutine)间通讯设施。
     接口(interface):对一组满足某个契约的类型的抽象。

    但是这并不影响我们将Go语言类型是值语义的本质。我们一个个来看这些类型:

    切片(slice)本质上是range,你可以大致将 []T 表示为:

    type slice struct {
        first *T
        last *T
        end *T
    }

    因为切片(slice)内部是一系列的指针,所以可以改变所指向的数组(array)的元素并不奇怪。slice类型本身的赋值仍然是值语义。

    字典(map)本质上是一个字典指针,你可以大致将map[K]V表示为:

    type Map_K_V struct {
        ...
    }
    
    type map[K]V struct {
        impl *Map_K_V
    }

    基于指针(pointer),我们完全可以自定义一个引用类型,如:

    type IntegerRef struct { impl *int }

    通道(chan)和字典(map)类似,本质上是一个指针。为什么将他们设计为是引用类型而不是统一的值类型,是因为完整拷贝一个通道(chan)或字典(map)并是常规需求。

    同样,接口(interface)具备引用语义,是因为内部维持了两个指针。示意为:

    type interface struct {
        data *void
        itab *Itab
    }

    接口在Go语言中的地位非常重要。关于接口(interface)内部实现细节,后面在高阶话题中,我们再细细剖析。

    结构体(struct)

    Go语言的结构体(struct)和其它语言的类(class)有同等的地位。但Go语言放弃了包括继承在内的大量OOP特性,只保留了组合(compose)这个最基础的特性。

    组合(compose)甚至不能算OOP的特性。因为连C语言这样的过程式编程语言中,也有结构体(struct),也有组合(compose)。组合只是形成复合类型的基础。

    上面我们说到,所有的Go语言的类型(指针类型除外)都是可以有自己的方法。在这个背景下,Go语言的结构体(struct)它只是很普通的复合类型,平淡无奇。例如我们要定义一个矩形类型:

    type Rect struct {
        x, y float64
        width, height float64
    }

    然后我们定义方法Area来计算矩形的面积:

    func (r *Rect) Area() float64 {
    return r.width * r.height
    }

    初始化

    定义了Rect类型后,我们如何创建并初始化Rect类型的对象实例?有如下方法:

    rect1 := new(Rect)
    rect2 := &Rect{}
    rect3 := &Rect{0, 0, 100, 200}
    rect4 := &Rect{width: 100, height: 200}

    在Go语言中,未显式进行初始化的变量,都会初始化为该类型的零值(例如对于bool类型的零值为false,对于int类型零值为0,对于string类型零值为空字符串)。

    构造函数?不需要。在Go语言中你只需要定义一个普通的函数,只是通常以NewXXX来命名,表示“构造函数”:

    func NewRect(x, y, width, height float64) *Rect {
        return &Rect{x, y, width, height}
    }

    这一切非常自然,没有任何突兀之处。

    匿名组合

    确切地说,Go语言也提供了继承,但是采用了组合的文法,我们称之为匿名组合:

    type Base struct {
        ...
    }
    
    func (base *Base) Foo() { ... }
    func (base *Base) Bar() { ... }
    
    type Foo struct {
        Base
        ...
    }
    
    func (foo *Foo) Bar() {
        foo.Base.Bar()
        ...
    }

    以上代码定义了一个Base类(实现了Foo、Bar两个成员方法),然后定义了一个Foo类,从 Base“继承”并实现了改写了Bar方法,该方法实现时先调用了基类的Bar方法。

    在“派生类”Foo没有改写“基类”Base的成员方法时,相应的方法就被“继承”。例如在上面的例子中,调用foo.Foo() 和调用foo.Base.Foo() 效果一致。

    区别于其他语言,Go语言很清晰地告诉你类的内存布局是怎么样的。在Go语言中你还可以随心所欲地修改内存布局,如:

    type Foo struct {
    ...
        Base
    }

    这段代码从语义上来说,和上面给例子并无不同,但内存布局发生了改变。“基类”Base的数据被放在了“派生类”Foo 的最后。

    另外,在Go语言中你还可以以指针方式从一个类“派生”:

    type Foo struct {
        *Base
        ...
    }

    这段Go代码仍然有“派生”的效果,只是Foo创建实例的时候,需要外部提供一个Base类实例的指针。C++ 中其实也有类似的功能,那就是虚基类。但是虚基类是非常让人难以理解的特性,普遍上来说 C++ 的开发者都会遗忘这个特性。

    成员的可访问性

    Go语言对关键字的增加非常吝啬。在Go语言中没有private、protected、public这样的关键字。要想某个符号可被其他包(package)访问,需要将该符号定义为大写字母开头。如:

    type Rect struct {
        X, Y float64
        Width, Height float64
    }

    这样,Rect类型的成员变量就全部被public了。成员方法遵循同样的规则,例如:

    func (r *Rect) area() float64 {
        return r.Width * r.Height
    }

    这样,Rect的area方法只能在该类型所在的包(package)内使用。

    需要强调的一点是,Go语言中符号的可访问性是包(package)一级的,而不是类一级的。尽管area是Rect的内部方法,但是在同一个包中的其他类型可以访问到它。这样的可访问性控制很粗旷,很特别,但是非常实用。如果Go语言符号的可访问性是类一级的,少不了还要加上friend这样的关键字,以表示两个类是朋友关系,可以访问其中的私有成员。

    接口(interface)

    Rob Pike曾经说,如果只能选择一个Go语言的特性移植到其他语言中,他会选择接口。

    接口(interface)在Go语言有着至关重要的地位。如果说goroutine和channel 是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代,成为一道极为亮丽的风景;那么接口(interface)是Go语言整个类型系统(type system)的基石,让Go语言在基础编程哲学的探索上,达到史无先例的高度。

    我曾在多个场合说,Go语言在编程哲学上是变革派,而不是改良派。这不是因为Go语言有 goroutine和channel,而更重要的是因为Go语言的类型系统,因为Go语言的接口。因为有接口,才让Go语言的编程哲学变得完美。

    Go 语言的接口(interface)不单单只是接口。

    为什么这么说?让我们细细道来。

    其他语言(C++/Java/C#)的接口

    Go语言的接口,并不是你之前在其他语言(C++/Java/C#等)中接触到的接口。

    在Go语言之前的接口(interface),主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:

    interface IFoo {
        void Bar();
    }
    
    class Foo implements IFoo { // Java 文法
        ...
    }
    
    class Foo : public IFoo { // C++ 文法
    ...
    }
    
    IFoo* foo = new Foo;

    哪怕另外存在一个一模一样的接口,只是名字不同叫IFoo2(名字一样但是在不同的名字空间下,也是名字不同),上面的类Foo只实现了IFoo,但没有实现IFoo2。

    这类接口(interface),我们称之为侵入式的接口。“侵入式”的主要表现在于实现类需要明确声明自己实现了某个接口。

    这种强制性的接口继承,是面向对象编程(OOP)思想发展过程中的一个重大失误。我之所以这样讲,是因为它从根本上是违背事物的因果关系的。

    让我们从契约的形成过程谈起。设想我们现在要实现一个简单搜索引擎(SE)。该搜索引擎需要依赖两个模块,一个是哈希表(HT),一个是HTML分析器(HtmlParser)。

    搜索引擎的实现者认为,SE对哈希表(HT)的依赖是确定性的,所以他不并认为需要在SE和HT之间定义接口,而是直接import(或者include)的方式使用了HT;而模块SE对HtmlParser的依赖是不确定的,未来可能需要有WordParser、PdfParser等模块来替代HtmlParser,以达到不同的业务要求。为此,他定义了SE和HtmlParser之间的接口,在模块SE中通过接口调用方式间接引用模块HtmlParser。

    应当注意到,接口(interface)的需求方是搜索引擎(SE)。只有SE才知道接口应该定义成什么样子才比更为合理。但是接口的实现方是HtmlParser。基于模块设计的单向依赖原则,模块HtmlParser实现自身的业务时,不应该关心某个具体使用方的要求。HtmlParser在实现的时候,甚至还不知道未来有一天SE会用上它。 要求模块HtmlParser知道所有它的需求方的需要的接口,并提前声明实现了这些接口是不合理的。同样的道理发生在搜索引擎(SE)自己身上。SE并不能够预计未来会有哪些需求方需要用到自己,并且实现他们所要求的接口。

    这个问题在标准库的提供来说,变得更加突出。比如我们实现了File类(这里我们用Go语言的文法来描述要实现的方法,请忽略文法上的细节),它有这些方法:

    Read(buf []byte) (n int, err error)
    Write(buf []byte) (n int, err error)
    Seek(off int64, whence int) (pos int64, err error)
    Close() error

    那么,到底是应该定义一个IFile接口,还是应该定义一系列的IReader, IWriter, ISeeker, ICloser接口,然后让File从他们继承好呢?脱离了实际的用户场景,讨论这两个设计哪个更好并无意义。问题在于,实现File类的时候,我怎么知道外部会如何用它呢?

    正因为这种不合理的设计,使得Java、C# 的类库每个类实现的时候都需要纠结:

     问题1:我提供哪些接口好呢?
     问题2:如果两个类实现了相同的接口,应该把接口放到哪个包好呢?

    非侵入式接口

    在Go语言中,一个类只需要实现了接口要求的所有函数,那么我们就说这个类实现了该接口。例如:

    type File struct {
        ...
    }
    
    func (f *File) Read(buf []byte) (n int, err error)
    func (f *File) Write(buf []byte) (n int, err error)
    func (f *File) Seek(off int64, whence int) (pos int64, err error)
    func (f *File) Close() error

    这里我们定义了一个File类,并实现有Read,Write,Seek,Close等方法。设想我们有如下接口:

    type IFile interface {
        Read(buf []byte) (n int, err error)
        Write(buf []byte) (n int, err error)
        Seek(off int64, whence int) (pos int64, err error)
        Close() error
    }
    
    type IReader interface {
        Read(buf []byte) (n int, err error)
    }
    
    type IWriter interface {
        Write(buf []byte) (n int, err error)
    }
    
    type ICloser interface {
        Close() error
    }

    尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类实现了这些接口,可以进行赋值:

    var file1 IFile = new(File)
    var file2 IReader = new(File)
    var file3 IWriter = new(File)
    var file4 ICloser = new(File)

    Go语言的非侵入式接口,看似只是做了很小的文法调整,但实则影响深远。

    其一,Go语言的标准库,再也不需要绘制类库的继承树图。你一定见过不少C++、Java、C# 类库的继承树图。这里给个Java继承树图:

    http://docs.oracle.com/javase/1.4.2/docs/api/overview-tree.html

    在Go中,类的继承树并无意义。你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。

    其二,实现类的时候,只需要关心自己应该提供哪些方法。不用再纠结接口需要拆得多细才合理。接口是由使用方按需定义,而不用事前规划。

    其三,不用为了实现一个接口而import一个包,目的仅仅是引用其中的某个interface的定义,这是不被推荐的。因为多引用一个外部的package,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。

    接口赋值

    接口(interface)的赋值在Go语言中分为如下2种情况讨论:

     将对象实例赋值给接口
     将接口赋值给另一个接口

    先讨论将某种类型的对象实例赋值给接口。这要求该对象实例实现了接口要求的所有方法。例如,在之前我们有实作过一个Integer类型,如下:

    type Integer int
    
    func (a Integer) Less(b Integer) bool {
        return a < b
    }
    
    func (a *Integer) Add(b Integer) {
        *a += b
    }

    相应地,我们定义接口LessAdder,如下:

    type LessAdder interface {
        Less(b Integer) bool
        Add(b Integer)
    }

    现在有个问题:假设我们定义一个Integer类型的对象实例,怎么其赋值给LessAdder接口呢?应该用下面的语句(1),还是语句(2)呢?

    var a Integer = 1
    var b LessAdder = &a     ... (1)
    var b LessAdder = a      ... (2)

    答案是应该用语句(1)。原因在于,Go语言可以根据

    func (a Integer) Less(b Integer) bool

    这个函数自动生成一个新的Less方法:

    func (a *Integer) Less(b Integer) bool {
        return (*a).Less(b)
    }

    这样,类型 *Integer就既存在Less方法,也存在Add方法,满足LessAdder接口。而从另一方面来说,根据

    func (a *Integer) Add(b Integer)

    这个函数无法自动生成

    func (a Integer) Add(b Integer) {
        (&a).Add(b)
    }

    因为 (&a).Add改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。故此,Go语言不会自动为其生成该函数。因此,类型Integer只存在Less方法,缺少Add方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。

    为了进一步证明以上的推理,我们不妨再定义一个Lesser接口,如下:

    type Lesser interface {
        Less(b Integer) bool
    }

    然后我们定义一个Integer类型的对象实例,将其赋值给Lesser接口:

    var a Integer = 1
    var b1 Lesser = &a     ... (1)
    var b2 Lesser = a      ... (2)

    正如如我们所料的那样,语句(1)和语句(2)均可以编译通过。

    我们再来讨论另一种情形:将接口赋值给另一个接口。在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么他们就是等同的,可以相互赋值。例如:

    package one
    
    type ReadWriter interface {
        Read(buf []byte) (n int, err error)
        Write(buf []byte) (n int, err error)
    }
    
    package two
    
    type IStream interface {
        Write(buf []byte) (n int, err error)
        Read(buf []byte) (n int, err error)
    }

    这里我们定义了两个接口,一个叫 one.ReadWriter,一个叫 two.IStream。两者都定义了Read、Write方法,只是定义的次序相反。one.ReadWriter先定义了Read再定义Write,而two.IStream反之。

    在Go语言中,这两个接口实际上并无区别。因为:

     任何实现了one.ReadWriter接口的类,均实现了two.IStream。
     任何one.ReadWriter接口对象可赋值给two.IStream,反之亦然。
     在任何地方使用one.ReadWriter接口,和使用two.IStream并无差异。

    以下这些代码可编译通过:

    var file1 two.IStream = new(File)
    var file2 one.ReadWriter = file1
    var file3 two.IStream = file2

    接口赋并不要求两个接口必须等价。如果接口A方法列表是接口B方法列表的子集,那么接口B可以赋值给接口A。例如假设我们有Writer接口:

    type Writer interface {
        Write(buf []byte) (n int, err error)
    }

    我们可以将上面的one.ReadWriter、two.IStream接口的实例赋值给Writer接口:

    var file1 two.IStream = new(File)
    var file4 Writer = file1

    但是反过来并不成立:

    var file1 Writer = new(File)
    var file5 two.IStream = file1 // 编译不能通过!

    这段代码无法编译通过。原因是显然的:file1并没有Read方法。

    接口查询

    有办法让上面Writer接口转换为two.IStream接口么?有。那就是我们即将讨论的接口查询语法。代码如下:

    var file1 Writer = ...
    if file5, ok := file1.(two.IStream); ok {
        ...
    }

    这个if语句的含义是:file1接口指向的对象实例是否实现了two.IStream接口呢?如果实现了,则... 接口查询是否成功,要在运行期才能够确定。它不像接口赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。

    在Windows下做过开发的人,通常都接触过COM,知道COM也有一个接口查询(QueryInterface)。是的,Go语言的接口查询和COM的接口查询(QueryInterface)非常类似,都可以通过对象(组件)的某个接口来查询对象实现的其他接口。当然Go语言的接口查询优雅很多。在Go语言中,对象是否满足某个接口、通过某个接口查询其他接口,这一切都是完全自动完成的。

    让语言内置接口查询,这是一件非常了不起的事情。在COM中实现QueryInterface的过程非常繁复,但QueryInterface是COM体系的根本。COM书籍对QueryInterface的介绍,往往从类似下面这样一段问话开始,它在Go语言中同样适用:

    \> 你会飞吗?    // IFly
    \> 不会。
    > 你会游泳吗?    // ISwim
    > 会。
    > 你会叫么? // IShout
    > 会。
    > ...

    随着问题深入,你从开始对对象(组件)一无所知(在Go语言中是interface{},在COM中是IUnknown),到逐步有了深入的了解。

    但是你最终能够完全了解对象么?COM说不能,你只能无限逼近,但永远不能完全了解一个组件。Go语言说:你能。

    在Go语言中,你可以向接口询问,它指向的对象是否是某个类型,例子如下:

    var file1 Writer = ...
    if file6, ok := file1.(*File); ok {
        ...
    }

    这个if语句的含义是:file1接口指向的对象实例是否是 *File 类型呢?如果是的,则...

    你可以认为查询接口所指向的对象是否是某个类型,只是接口查询的一个特例。接口是对一组类型的公共特性的抽象。所以查询接口与查询具体类型的区别,好比是下面这两句问话的区别:

    > 你是医生吗?
    > 是。
    > 你是某某某?
    > 是。

    第一句问话查的是一个群体,是查询接口;而第二句问话已经到了具体的个体,是查询具体类型。

    在C++/Java/C# 等语言中,也有一些类似的动态查询能力,比如查询一个对象的类型是否是继承自某个类型(基类查询),或者是否实现了某个接口(接口派生查询)。但是他们的动态查询与Go的动态查询很不一样。

    > 你是医生吗?

    对于这个问题,基类查询看起来像是在这么问:“你老爸是医生吗?”;接口派生查询则看起来像是这么问:“你有医师执照吗?”;在Go语言中,则是先确定满足什么样的条件才是医生,比如技能要求有哪些,然后才是按条件一一拷问,确认是否满足条件,只要满足了你就是医生,不关心你是否有医师执照,或者是小国执照不被天朝承认。

    类型查询

    在Go语言中,你还可以更加直接了当地询问接口指向的对象实例的类型。例如:

    var v1 interface{} = ...
    switch v := v1.(type) {
    case int: // 现在v的类型是int
    case string: // 现在v的类型是string
    ...
    }

    就像现实生活中物种多得数不清一样,语言中的类型也多的数不清。所以类型查询并不经常被使用。它更多看起来是个补充,需要配合接口查询使用。例如:

    type Stringer interface {
        String() string
    }
    
    func Println(args ...interface{}) {
        for _, arg := range args { 
        switch v := v1.(type) {
        case int: // 现在v的类型是int
        case string: // 现在v的类型是string
    default:
            if v, ok := arg.(Stringer); ok { // 现在v的类型是Stringer
                val := v.String()
                ...
            } else {
                ...
            }
        }
    }

    Go语言标准库的Println当然比这个例子要复杂很多。我们这里摘取其中的关键部分进行分析。对于内置类型,Println采用穷举法来,针对每个类型分别转换为字符串进行打印。对于更一般的情况,首先确定该类型是否实现了String()方法,如果实现了则用String()方法转换为字符串进行打印。否则,Println利用反射(reflect)遍历对象的所有成员变量进行打印。

    是的,利用反射(reflect)也可以进行类型查询,详细可参阅reflect.TypeOf方法相关文档。在后文高阶话题中我们也会探讨有关“反射(reflect)”的话题。

    Any类型

    由于Go语言中任何对象实例都满足空接口interface{},故此interface{}看起来像是可以指向任何对象的Any类型。如下:

    var v1 interface{} = 1      // 将int类型赋值给interface{}
    var v2 interface{} = "abc"  // 将string类型赋值给interface{}
    var v3 interface{} = &v2    // 将*interface{}类型赋值给interface{}
    var v4 interface{} = struct{ X int }{1}
    var v5 interface{} = &struct{ X int }{1}

    当一个函数可以接受任意的对象实例时,我们会将其声明为interface{}。最典型的例子是标准库fmt中PrintXXX系列的函数。例如:

    func Printf(fmt string, args ...interface{})
    func Println(args ...interface{})
    ...

    前面我们已经简单分析过Println的实现,也已经展示过interface{}的用法。总结来说,interface{} 类似于COM中的IUnknown,我们刚开始对其一无所知,但我们可以通过接口查询和类型查询逐步了解它。

    总结

    我们说,Go 语言的接口(interface)不单单只是接口。在其他语言中,接口仅仅作为组件间的契约存在。从这个层面讲,Go语言接口的重要突破是,其接口是非侵入式的,把其他语言接口的副作用消除了。

    但是Go语言的接口不仅仅是契约作用。它是Go语言类型系统(type system)的纲。这表现在:

     接口查询:通过接口你可以查询接口所指向的对象是否实现了另外的接口。
     类型查询:通过接口你可以查询接口所指向的对象的具体类型。
     Any类型:在Go语言中interface{}可指向任意的对象实例。

    展开全文
  • 怎么设计实体和怎么划分限界上下文同样重要。实体的概念就是要保证通用语言的完整性。领域驱动让设计实体的关注点从数据的属性和表的关联转化到了富有行为的领域概念上。 实体是具有可变性的,这是一个和值对象...

    实体

    引言

    在领域驱动设计里,实体的设计可以说是通用语言的核心,也是最开始在模型划分中需要考虑的。怎么样设计实体和怎么样划分限界上下文同样重要。实体的概念就是要保证通用语言的完整性。领域驱动让设计实体的关注点从数据的属性和表的关联转化到了富有行为的领域概念上。

    实体是具有可变性的,这是一个和值对象比较明显的区分,也即实体是可以持续得变化,持续得修改,并且具有唯一的标识。在设计实体的时候需要跳出CRUD的设计思维。把关注重点从数据模型设计转移到实体模型上。实体是能够表达什么概念,具有哪些行为,领域范围是哪些。实体的唯一标识是用来区分实体的,在实体的整个生命周期中这个唯一标识都是不变的。

    设计实体

    实体设计中,需要先确定实体的唯一标识。在Java的实体设计中,可以借助框架来实现唯一标识。这里先不讨论具体实现细节。设计唯一标识其实可以有多种方式。

    1. 用户输入唯一标识,程序再根据输入生成可识别的数值和符号,这种方式不方便修改生成规则,而且也会存在输入冲突。
    2. 应用程序生成,比如java自带的UUID生成器。apache的Commons的id生成组件。
    3. 持久化机制,DB的序列值Sequence或者自增主键。
    4. 其他上下文提供唯一标识,比如本地上下文也有一个本地的User,然后用全局的比如登录系统的用户id作为本地User实体的主键,这种需要谨慎使用,尽量保证系统的自治性。
    5. 委派标识。很多情况下,唯一标识都是和领域概念无关的,客户端也无需关注这个标识。这时候就可以用一些ORM工具来处理,写一个抽象父类,来专门做id生成。其他子类的实体只需要关注自己领域的模型和行为即刻。
      具体实现可以用Hibernate或者Jpa这些工具。如下是一个用jpa生成自增主键的父类。
    package com.lijingyao.bookrent.entity;
    
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.MappedSuperclass;
    
    /**
     * Created by lijingyao on 15/12/20 13:14.
     */
    @MappedSuperclass
    public abstract class LayerSuperType {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    
        protected Long getId() {
            return id;
        }
    
        protected void setId(Long id) {
            this.id = id;
        }
    }
    

    实体就可以不用关注id生成,如下,是一个代表用户的实体。

    package com.lijingyao.bookrent.entity;
    
    import com.sun.istack.internal.NotNull;
    
    import java.util.Calendar;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Index;
    import javax.persistence.Table;
    
    /**
     * Created by lijingyao on 15/12/20 12:48.
     */
    @Entity
    @Table(
            name = "br_user",
            indexes = {@Index(name = "IDX_USER_NAME", columnList = "name", unique = false)})
    public class User extends LayerSuperType {
    
        @NotNull
        @Column(name = "name")
        private String name;
    
    
        @Column(name = "utc_create")
        private Calendar utcCreate;
    
        @Column(name = "utc_modified")
        private Calendar utcModified;
    
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Calendar getUtcCreate() {
            return utcCreate;
        }
    
        public void setUtcCreate(Calendar utcCreate) {
            this.utcCreate = utcCreate;
        }
    
        public Calendar getUtcModified() {
            return utcModified;
        }
    
        public void setUtcModified(Calendar utcModified) {
            this.utcModified = utcModified;
        }
    }
    

    定义实体

    定义实体需要先理解了领域的通用语言。因为实体需要在表达完整的通用语言基础上再对实体的属性进行定义,然后还需定义实体的唯一标识。
    在定义实体时还需要清楚,哪些行为是属于这个实体的,哪些职责是本实体应该具备的。在实体的属性验证过程也应该回归实体。调用验证的过程也不一定是到了持久化这一步之后才进行,前置验证一样可以把验证行为回归到实体。
    现在一些流行的开源框架也支持了多种实体验证方式,比如可以用JPA的注解来进行验证,这种是属于延迟验证,但回归到实体的本质和行为上,验证本身也是实体行为的一环。

    值对象

    概念和特性

    值对象值不变的对象,也就是说有特定含义的表达。所以值对象没有唯一标识,也作为反映通用语言的一种方式,就像领域驱动中的一个部件。值对象相对实体概念上更加简单,但关键点是一个领域概念是设计成实体还是值对象。
    在设计选择的时候可以针对值对象的特征进行比较:
    1. 值对象是用来度量或者描述了领域中的一件东西。
    2. 可作为不变量(不变性)
    3. 将不同的相关属性组合成一个概念整体
    4. 可以和其他值对象进行相等比较(属性相等就是同一个对象)
    5. 不会对协作对象造成副作用(无副作用)
    其实,在整理值对象的概念时,往往会在实体上设计很多值对象,在java中最常见的就是用枚举来表示和实现。枚举的不变性可以方便表达很多概念。值对象的设计是为了内聚得表达通用语言的一个概念。在领域建模的时候,对于一些不需要唯一标识的概念就可以设计成值对象。比如我们有一个租书一同,现在描述这样一个概念,对于一个实体“书”,如果有一组这样的概念“文学书”约定文学书最多只能借出1个月,科技类的书最多可以租借十天。这里的文学书是一个不变的概念,那么对于书籍的分类就可以设计成一组值对象。对于值对象的管理和生成最好利用工厂方法或者Builder的设计模式。构造函数就初始化好值对象有利于概念的整体性。
    对于第五点的无副作用行为,其实结合Java8的lambda表达式,或者说函数式编程能更好的理解,函数式编程就是无副作用的,因为它不会改变对象内部的状态,同样,值对象也不会改变其他实体的状态,只是用来输出一组概念。

    在上下文中集成

    集成值对象要保持最小化集成和最少职责。如果值对象需要依赖上游上下文的聚合。假设还是刚才的租书的场景,书籍管理系统是在一个上下文中,出租书籍上下文中需要表示一个如下概念:书籍类别(bookType)+ 出租记录(record)可以定义一个“XX类Top10 租售书籍”。这个概念就可以设计成一个值对象-BestRent。这里的BestRent并不包含Book中的type属性。而是通过bookType 这样一个自己拥有的属性来表明一个书的类型。如下是一个简单的值对象:

    package com.lijingyao.bookrent.vo;
    
    /**
     * Created by lijingyao on 15/12/26 15:00.
     */
    public class BestRent {
    
        /**
         * The type of a book.category see {@link BookType}
         */
        private String bookType;
    
        /**
         * the rent number of one book.
         */
        private Integer topNum;
    
        public BestRent() {
        }
    
        public BestRent(String bookType, Integer topNum) {
            this.bookType = bookType;
            this.topNum = topNum;
        }
    
        public BestRent topNScience(Integer topNum) {
            if (null == topNum) {
                throw new IllegalArgumentException("topNum may not be null.");
            }
            return new BestRent(BookType.SCIENCE.name(), topNum);
        }
    
        public BestRent top10Science() {
            return new BestRent(BookType.SCIENCE.name(), 10);
        }
    
        public void setBookType(String bookType) {
            this.bookType = bookType;
        }
    
        public void setTopNum(Integer topNum) {
            this.topNum = topNum;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
    
            BestRent bestRent = (BestRent) o;
    
            if (bookType != null ? !bookType.equals(bestRent.bookType) : bestRent.bookType != null) return false;
            return !(topNum != null ? !topNum.equals(bestRent.topNum) : bestRent.topNum != null);
    
        }
    
        @Override
        public int hashCode() {
            int result = bookType != null ? bookType.hashCode() : 0;
            result = 31 * result + (topNum != null ? topNum.hashCode() : 0);
            return result;
        }
    }
    

    通过值对象就不需要关注Book领域的业务和BookType了。这个值对象保持了无副作用性,因为它不会改变任何领域对象的状态,也不需要唯一标识。一个固定的name和一个固定的bookType就可以定义一个不变的概念,比如”自然科学类的Top10”就可以通过top10Science方法来获得。
    同样需要注意到,值对象最好实现equals和hashCode方法。对于一组概念描述的值对象来说,只要其表达完整概念的属性值相同,值对象就是相同的。这就能保证top10Science和topNScience(10)需要返回相同的对象。这就是值对象的不变性。

    标准类型(Standard Type)

    标准类型是标识某些事物的描述对象的表示方式。比如表示货币类型,标准类型可以用RMB,JPY,AID,USD等货币类型。系统中建立标准类型可以统一通信标准,防止有人拼写错误,或者用非标准的描述对象(比如临时的String对象)来表示标准概念。
    对于标准类型,在定义概念的上下文中可以建模成实体,在消费方可以建模成值对象。如果消费方的上下文没有必要维护一个描述类型对象的生命周期,那么就可以将其建模成值对象。或者为了方便维护,将标准类型放到单独的限界上下文中。消费方只需要关注标准类型中自己需要用到的属性就可以了。这也是为了尽量最小化集成。
    对于有限集合的标准类型可以建模成枚举类。后面会介绍一种结合策略模式的方式去维护值对象的行为。

    策略模式实现值对象设计

    在构建一个值对象的时候,需要考虑到不变性的概念,可以隐藏对象本身的Setters方法。只有通过构造函数才能使用委派给自己的属性进行设置。
    现在考虑对于上文所描述的书籍类型进行值对象设计。这里的书籍类型是一种有限集合的值对象,所以我们可以用Java的Enum来设计。
    这里简单说一下,使用策略模式是因为不同的值对象,或者说有限集合的值对象对于某种固定的行为操作(比如本例中的获取TopN的广告语)有着不同的实现。并且结合Java8的lambda表达式,就可以更好地定义我们的StrategyHandler。值对象的不同Handler,就可以定义成不同的Function,从而简化代码实现。
    这里假设有一个获取图书广告语的RESTful接口。资源的Controller如下

    package com.lijingyao.bookrent.controllers;
    
    import com.lijingyao.bookrent.service.RentService;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    
    @RestController
    @RequestMapping("/books")
    public class BookResource {
    
        @Autowired
        private RentService rentService;
    
        @RequestMapping(value = "/{type}/adv", method = {RequestMethod.GET})
        public ResponseEntity getBestSellBookAdv(@PathVariable("type") String type, @RequestParam("num") Integer num) {
            return new ResponseEntity(rentService.getBookAdvByType(type, num), HttpStatus.OK);
        }
    
    }
    

    controller直接通过service获取资源数据。如果存在不同类别的书籍,那么service内部就需要有一个switch 来实现,最简单粗暴的方式就是:

    
    public String getBookAdvByType(String type, Integer num) {
            if (type.equals("SCIENCE")) {
                return "自然科学 图书的畅销Top :" + num.toString();
            }else if(type.equals("NOVEL")){
                return "小说 图书的畅销Top :" + num.toString();
            }else{
                ...
            }
            return "没有找到此类别图书广告语.";
        }   
    

    这种方式可以发现,对于以后扩展书籍类型的话,需要修改Service的实现,也就不符合开放闭合原则。如果借助枚举类来管理书籍类型,再用函数式编程处理广告语产生的行为。那么Service的代码就可以写成:

       package com.lijingyao.bookrent.service;
    
    import com.lijingyao.bookrent.service.vo.BookType;
    
    import org.springframework.stereotype.Service;
    
    /**
     * Created by lijingyao on 15/12/26 14:37.
     */
    @Service
    public class RentService {
    
    
        public String getBookAdvByTypeStrategy(String type, Integer num) {
            BookType bookType = BookType.valueOf(type);
            if (null == bookType) {
                return "There is no advertise of this kind of book.";
            }
            return bookType.bestRentOf(num);
        }
    
    
    }
    

    行为bookType.bestRentOf 的实现交给了具体的策略。策略的Handler,也就是枚举类如下:

      package com.lijingyao.bookrent.service.vo;
    
    import java.util.function.Function;
    
    /**
     * Created by lijingyao on 15/12/20 13:43.
     */
    public enum BookType {
    
    
        NOVEL((topNum) -> BestRentUtils.advOfBestRent("小说", topNum)),
        SCIENCE(((topNum) -> BestRentUtils.advOfBestRent("自然科学", topNum))),
        TECHNOLOGY(((topNum) -> BestRentUtils.advOfBestRent("科学技术", topNum)));
    
        private Function<Integer, String> strategy;
    
        public String bestRentOf(Integer topNum) {
            return strategy.apply(topNum);
        }
    
    
        BookType(Function<Integer, String> strategys) {
            this.strategy = strategys;
        }
    
    }
    
    

    启动Springboot 输入:http://localhost:8080/books/NOVEL/adv?num=20
    就可以看到结果。”小说 图书的畅销Top :20”。这里的处理只是简单的字符串拼接,在具体的上下文中,对于值对象会有更为复杂的计算逻辑。那么就可以把每个单独的又有共性的策略抽取出来,每一个值对象都会有自己的Strategy类来封装内部的实现。如果要扩展策略,也只要新增一个策略实现,以及在枚举中添加一组类型即可。

    结语

    无论是值对象还是实体,在设计持久化的时候,需要先根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。这是一种DDD的思维方式,所以要尽量避免数据模型从领域模型中泄露给客户端。下一篇文章会总结下领域服务和应用服务。

    展开全文
  • JS原生对象与内置对象

    千次阅读 2018-03-05 04:27:38
    一、JS的对象和类型 JS中的所有事物都是对象,包括但不限于字符串、数值、数组、函数等等,还包括自定义对象。 在红宝书中,将JS分为五种基本类型:null、undefined、number、string、boolean和一种复杂类型:...

    一、JS的对象和类型

          JS中的所有事物都是对象,包括但不限于字符串、数值、数组、函数等等,还包括自定义对象。

         在红宝书中,将JS分为五种基本类型:null、undefined、number、string、boolean和一种复杂类型:object。但是在《JavaScript语言精髓与编程实践》认为是6种:undefined、number、string、boolean、object和function,它的依据是typeof的结果只有6种(仅含ES自身,不包括宿主对象),其中null的类型也是object.

            ES6新增了一种 Symbol 类型,它是一种不可变的类型,表示独一无二的值。一般作为对象属性的标识符使用。ES6 之前的属性名都是字符串类型,会造成属性名的重写覆盖。ES6 提出 Symbol 类型后,JS的基本类型即增加到了 7 种。

    var  s1 = Symbol();
    typeof  s1;  // "symbol"
     
    var  s2 = Symbol( 'andy' );
    s2;  // Symbol(andy)
    var  a = 1, b =  '2' , c =  true , d, e =  null , f =  function (){}
     
    typeof  a ===  'number' // true
    typeof  b ===  'string' ;     // true
    typeof  c ===  'boolean' // true
    typeof  d ===  'undefined' // true
    typeof  e ===  'object' // true
    typeof  f ===  'function' // true        

    • null 返回值是 object ,因为解析器把 null 识别为了一个空值的对象。
    • 正则表达式 /^\w*$/ 返回值是 object ,在一些老的浏览器返回值是 function。
    • function 不属于数据类型,为什么用 typeof 会返回? 
      js 中函数也是对象,function 可以看成是 object 的一个子集。
    • alert(typeof ii) 中的变量 ii 并没有定义,怎么也有返回值? 
      js 中规定未定义的变量使用 typeof 识别数据类型时也为 undefined 。
            ===严格比较,不会进行自动转换,要求进行比较的操作数必须类型一致,不一致时返回flase, =="将先做类型转换。简而言之就是 "==" 只要求值相等;   "===" 要求值和类型都相等。

    下面是七种基本类型的基本使用场景和方法:(引用链接: http://blog.csdn.net/baidu_32262373/article/details/52615158)

    Undefiend 类型 
    1. Undefined 类型只有一个值,即: undefined 。

    2.当用 var 声明一个变量但未初始化值时,此时 js 默认会给这个变量赋值,这个值是 undefined 。

    var  message;                 // 此时只声明了变量,未初始化,默认值为  undefined
    
    alert(message);               // 返回结果  undefined

    3.为什么引入这个数据类型

    首先我们得弄清一个概念 —— 去考试了但是考了0分 / 没有去考场,考卷上没有分数。

    这个数据类型的创建就是为了解决这个问题。

    我们声明了一个变量,只是没有给它赋值而已,它是一个实实在在的存在的东西。 
    和我们直接使用一个凭空出现的,尚未声明的不存在的变量是两个不同的概念。

    var message;        // 声明了变量  message
    
    alert(message);     // 返回结果  undefined,这个变量已经声明过了,只是没有初始化值
    alert(apple);       // 返回结果  控制台直接报错。
    //(打开F12,选择 控制台或者Console 选项卡会看见红色报错信息)

    4.总结

    • 当控制台报错 : apple is not defined 时,应该迅速定位问题:我的 js 代码中没有定义这个变量,我需要考虑是否要声明这个变量。
    • Undefined 类型的出现是为了区分:尚未初始化值的变量和尚未定义的变量。


    Null 类型

    1.Null 类型只有一个值,即: null 。

    null 是空对象指针,对象里没有内容,即: null={ };   (注:js 中一对华括弧就是一个对象)

    2.Null 和 Undefined 有什么关系?

    Undefined 可以看成是 Null 的一个子集,下面的例子可以证实这一点:(了解有这回事,没多大卵用,知道 Null 和 Undefined 是两回事即可)
    alert(null == undefined);        // 返回结果   true
    虽然 Undefined 和 Null 有关系而且很相似,为什么又要同时引入这两个概念呢?看下面。

    3.Null 的用处?

    如果一个变量被声明出来并且明确了在接下来的操作中会接收一个对象,那么就该显式的为该变量赋值 null 。
    
    在下面的操作中只需要判断这个变量是否等于 null 就可以知道这个变量到底有没有储存一个对象。
    var obj = null;    // 定义了变量 obj ,并且已经明确这个变量下面就是用来接收对象
    
    // ... obj 变量接收一个对象(对象即:键值对)
    obj = {key1: 'value1', key2: 'value2'};
    
    if (obj != null) {
        // 判断 obj 如果不为 null,就说明此时已经储存了对象
    }

    4.总结

    简单的说:

    • undefined 的出现是为了区分 声明了但未初始化的变量和未定义变量之间的区别;
    • null 的出现是为了更加方便的对对象进行处理。


    Boolean 类型

    1.Boolean 类型有两个值: true 和 false 。

    2.常见误区:

    • true 不一定等于数字 1,false 不一定等于数字 0 ;
    • 由于 js 中区分大小写,因此 True 和 False 都不是 Boolean值。

    3.类型转换

    除 Boolean 类型外还有五种数据类型,都可以转换为 Boolean 类型。有两种方式:

    1)使用 Boolean() 方法

    var message = 'This is information';
    var asBoolean = Boolean(message);alert(asBoolean); // 返回结果 true
    • 2) 不适用任何方法,自带转换,常见于控制语句分支条件
    var info = 'i am string';
    
    if (info) {           
    // 这里的if(info)已经默认调用了Boolean()转换为了true,所以才会执行下面的 alert语句
         alert(1);        // 返回结果  1
    }

    4.其它四种数据类型转化为Boolean类型(不包括Null类型)

    数据类型转换为true的值转换为false的值
    Booleantruefalse
    String任何非空字符串”(空字符串)
    Number任何非零数字值(包括无穷大)0和NaN
    Undefined不适用undefined
    Object任何对象null


    Number 类型

    1.进制数

    十进制:js中默认的进制数
    八进制:第一位必须是零,然后是0-7的数字组成
    十六进制:前两位必须是0x,然后是0-9及A-F(字母不区分大小写)

    例:

    var num1 = 10;    // 十进制
    var num2 = 070;   // 八进制的56
    var num3 = 079;   // 十进制,因为有数字超过了7,这里是79
    var num4 = 0x1f;  // 十六进制的31

    注:

    八进制在严格模式下("use strict")是无效的,会导致js报错,避免使用。

    2.浮点值(即小数) 
    看个例子:

    var num = 0.1 + 0.2;
    alert(num);  // 返回结果:0.30000000000000000004

    上面例子表达的就是js的浮点型数据在计算时容易丢失精度,这一点并不仅在javascript存在,建议处理这方面问题使用专用的数字处理类,比如Java里的BigDecima类来处理。这里略过。

    3.数字的范围 
    js中数值的范围是有效位数的,基本上够我们使用,我们仅需要知道以下几个知识点:

    • 使用 Number.MIN_VALUE(或Number.NEGATIVE_INFINITY) 表示js中的最小值
    • 使用 Number.MAX_VALUE(或Number.POSITIVE_INFINITY) 表示js中的最大值
    • 如果某个值超出js的范围,就会转换为无穷大 Infinity 或 -Infinity
    • 如果某次计算返回结果中有Infinity,该值无法参与下一次计算
    • 使用 isFinite() 方法确认一个数值是否在js有效数值返回内

      如:

    var num = Number.MAX_VALUE + Number.MAX_VALUE;
    alert(isFinite(num));    // 返回 false,无穷大加无穷大超出js数值范围

    3.NaN (not a number) 
    问:为什么要引入这个东西? 
    答:在其他语言中,比如java中 1/0 由于0作为分母就会报错,但在js中就不会,直接返回NaN。

    NaN的含义是本该返回数值的操作未返回数值,返回了NaN就不会抛出异常影响语句流畅性。

    掌握:isNaN()的使用?判断一个值是不是数值,不是数值返回true,是数值返回false

    如:

    alert(isNaN(NaN));      // true 不是数值
    alert(isNan(10));       // false  是数值
    alert(isNan(false));    // false  自动转换为数值 0
    alert(isNan('10'));     // false  自动转换为数值 10
    alert(isNan(hahah));    // true 不是数值

    在上面的例子中,会发现有些值被自动的转换为了数值。这就和Boolean类型中的分支条件语句也会将所有数据类型自动转换Boolean一样。

    if('hello'){    // 这里的'hello'被转换为了Boolean类型的true值
        alert('i am print');
    }

    4.类型转换

    就像Boolean类型有个Boolean()方法可以在某些情形下自动将其它数据类型转换为Boolean类型;
    Number也提供了Number()方法在某些情形将其它数据类型转换为Number类型。
    数据类型返回结果情形
    null返回0-
    undefined返回0-
    Booleantrue返回1,false返回0-
    String‘123’返回 123,’011’转换为 11字符串中只包含数字,省略前导0
     ‘1.1’返回 1.1,’02.2’返回 2.2字符串只包含小数,省略前导0
     ‘0xf’返回 15转换为十进制再返回
     ”返回 0空字符串转换为0
     -除字符串的其它情形返回NaN
    Object待定调用valueOf()方法再参照上述规则

    parseInt()方法介绍:只能将字符串转换为数值型

    如:

    var num1 = Number('123abc');   // NaN
    var num2 = Number('123abc');   // 123

    问:为什么有Number()了还需要parseInt()方法? 
    答:比如上面的例子中使用Number()方法就不能返回数值,因为它要求字符串必须全部数值才行,而parseInt的解析字符串机制让它在某些情况下优于Number。

    parseInt()处理机制: 
    首先解析字符串的第一个字符,如果不是数值直接返回,如果是数字,再接着往下解析,直到遇到不是数字的字符时停止,返回前面解析过的值。

    使用parseInt()的好习惯是加上进制数。第二个参数定义转换的进制数。 
    如:

    var num1 = parseInt('AF', 16);  // 175
    var num2 = parseInt('AF');      // NaN

    上面例子中num1会返回数值,因为定义为了16进制数,而num2只是被当做普通字符串解析。



    String 类型 
    1.类型转换

    toString() 方法

    • 几乎每个值都有toString()方法
    • null和undefined没有toString()方法
    • 数值、布尔值直接加引号转换为字符
    • 对象的返回结果加引号转换为字符
    • 对数值使用toString()时,可以加进制数限制结果

      如:

    var num = 10;
    alert(num.toString(2));     // '1010'
    alert(num.toString(8));     // '12'

    String() 方法 
    由于toString()方法不能对null和undefined进行转换,那么当我们不确定这个值是不是null或undefined时如何转换呢?String()可以对任何值进行转换。 
    如:

    var aa = null;
    var bb;
    String(aa);     // 'null'
    String(bb);     // 'undefined'

    Object 类型(引用链接:http://luopq.com/2016/02/28/Object-in-Javascript/)

    Object是在javascript中一个被我们经常使用的类型,而且JS中的所有对象都是继承自Object对象的。虽说我们平时只是简单地使用了Object对象来存储数据,并没有使用到太多其他功能,但是Object对象其实包含了很多很有用的属性和方法,尤其是ES5增加的方法,因此,本文将从最基本的介绍开始,详细说明了Object的常用方法和应用。

    基础介绍

    创建对象

    首先我们都知道,对象就是一组相似数据和功能的集合,我们就是用它来模拟我们现实世界中的对象的。那在Javascript中,创建对象的方式通常有两种方式:构造函数和对象字面量。

    new构造函数法

    ?
    1
    2
    3
    var person = new Object();
    person.name = "狼狼的蓝胖子" ;
    person.age = 25;

    这种方式使用new关键字,接着跟上Object构造函数,再来给对象实例动态添加上不同的属性。这种方式相对来说比较繁琐,一般推荐使用对象字面量来创建对象。

    对象字面量

    对象字面量很好理解,使用key/value的形式直接创建对象,简洁方便。

    ?
    1
    2
    3
    4
    var person = {
       name: “狼狼的蓝胖子”,
       age: 25
    };

    这种方式直接通过花括号将对象的属性包起来,使用key/value的方式创建对象属性,每个属性之间用逗号隔开。
    注意:如果是最后一个属性,后面就不要加逗号,因为在一些旧的浏览器下会报错。

    对象实例的属性和方法

    不管通过哪种方式创建了对象实例后,该实例都会拥有下面的属性和方法,下面将会一一说明。

    constructor属性

    constructor属性是保存当前对象的构造函数,前面的例子中,constructor保存的就是Object方法。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    var obj1 = new Object();
    obj1.id = "obj1" ;
    var obj2 = {
       "id" : "obj2"
    };
      
    console.log(obj1.constructor); //function Object(){}
    console.log(obj2.constructor); //function Object(){}

    hasOwnProperty(propertyName)方法

    hasOwnProperty方法接收一个字符串参数,该参数表示属性名称,用来判断该属性是否在当前对象实例中,而不是在对象的原型链中。我们来看看下面这个例子:

    ?
    1
    2
    3
    var arr = [];   
    console.log(arr.hasOwnProperty( "length" )); //true
    console.log(arr.hasOwnProperty( "hasOwnProperty" )); //false

    在这个例子中,首先通过定义了一个数组对象的实例arr,我们知道数组对象实际是通过原型链继承了Object对象,然后拥有自己的一些属性,我们通过hasOwnProperty方法判断length是arr自己的属性,而hasOwnProperty是在原型链上的属性。
    hasOwnProperty方法可以和for..in结合起来获取对象自己的key。

    isPrototypeOf(Object)方法

    isPrototype方法接收一个对象,用来判断当前对象是否在传入的参数对象的原型链上,说起来有点抽象,我们来看看代码。

    ?
    1
    2
    3
    function MyObject() {}
    var obj = new MyObject();
    console.log(Object.prototype.isPrototypeOf(obj));

    我们知道MyObject是继承自Object对象的,而在JS中,继承是通过prototype来实现的,所以Object的prototype必定在MyObject对象实例的原型链上。

    propertyIsEnumerable(prototypeName)方法

    prototypeIsEnumerable用来判断给定的属性是否可以被for..in语句给枚举出来。看下面代码:

    ?
    1
    2
    3
    4
    5
    6
    var obj = {
    name: "objName"
    }
    for ( var i in obj) {
    console.log(i);
    }

    执行这段代码输出字符串“name”,这就说明通过for…in语句可以得到obj的name这个属性,但是我们知道,obj的属性还有很多,比如constructor,比如hasOwnPrototype等等,但是它们没有被输出,说明这些属性不能被for…in给枚举出来,可以通过propertyIsEnumerable方法来得到。

    复制代码代码如下:

    console.log(obj.propertyIsEnumerable("constructor"));//false

    判断“constructor”是否可以被枚举,输出false说明无法被枚举出来。

    toLocaleString()方法

    toLocalString方法返回对象的字符串表示,和代码的执行环境有关。

    ?
    1
    2
    3
    4
    5
    var obj = {};
    console.log(obj.toLocaleString()); //[object Object]
     
    var date = new Date();
    console.log(date.toLocaleString()); //2016/2/28 下午1:39:27

    toString()方法

    toString用来返回对象的字符串表示。

    ?
    1
    2
    3
    4
    5
    var obj = {};
    console.log(obj.toString()); //[object Object]
         
    var date = new Date();
    console.log(date.toString()); //Sun Feb 28 2016 13:40:36 GMT+0800 (中国标准时间)

    valueOf()方法

    valueOf方法返回对象的原始值,可能是字符串、数值或bool值等,看具体的对象。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = {
       name: "obj"
    };
    console.log(obj.valueOf()); //Object {name: "obj"}
     
    var arr = [1];
    console.log(arr.valueOf()); //[1]
     
    var date = new Date();
    console.log(date.valueOf()); //1456638436303

    如代码所示,三个不同的对象实例调用valueOf返回不同的数据。

    属性的类型

    在Javascript中,属性有两种类型,分别是数据属性和访问器属性,我们来看看这两种属性具体是什么东西。

    数据属性

    数据属性我们可以理解为我们平时定义对象时赋予的属性,它可以进行读和写。但是,ES5中定义了一些特性,这些特性是用来描述属性的各种特征,特性是内部值,不能直接访问到。特性通过用两对方括号表示,比如[[Enumerable]]。属性的特性会有一些默认值,要修改特性的默认值,必须使用ES5定义的新方法Object.defineProperty方法来修改。
    数据属性有4个描述其特征的特性,下面将依次说明每一个特性:
    (1)[[Configurable]]:该特性表示是否可以通过delete操作符来删除属性,默认值是true。

    ?
    1
    2
    3
    4
    5
    var obj = {};
    obj.name = "myname" ;
         
    delete obj.name;
    console.log(obj.name); //undefined

    这段代码很明显,通过delete删除了obj的name属性后,我们再访问name属性就访问不到了。
    我们通过Object.defineProperty方法来修改[[Configurable]]特性。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    var obj = {};
    obj.name = "myname" ;
    Object.defineProperty(obj, "name" , {
       configurable: false
    })       
     
    delete obj.name;
    console.log(obj.name); //myname

    通过将configurable特性设置成false之后,delete就无法删除name属性了,如果在严格模式下,使用delete去删除就会报错。

    (2)[[Enumerable]]:表示是否能够通过for…in语句来枚举出属性,默认是true
    我们来看看前面的例子:

    ?
    1
    2
    3
    4
    5
    6
    var obj = {
       name: "objName"
    }
    for ( var i in obj) {
       console.log(i); //name
    }

    这段代码只输出了name属性,我们来将constructor属性的[[Enumerable]]设置为true试试。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var obj = {
         name: "objName"
    }
    Object.defineProperty(obj, "constructor" , {
       enumerable: true
    })
     
    for ( var i in obj) {
       console.log(i); //name,constructor
    }
    console.log(obj.propertyIsEnumerable( "constructor" )); //true

    这段代码中,for…in循环得到了name和constructor两个属性,而通过propertyIsEnumerable方法来判断constructor也返回了true。

    (3)[[Writable]]:表示属性值是否可以修改,默认为true
    如果[[Writable]]被设置成false,尝试修改时将没有效果,在严格模式下会报错

    (4)[[Value]]:表示属性的值,默认为undefined

    我们通过一个简单的例子来看看这两个特性:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var obj = {
       name: "name"
    };
    console.log(obj.name); //name   
     
    Object.defineProperty(obj, "name" , {
       value: "newValue" ,
       writable: false
    })
    console.log(obj.name); //newValue
     
    obj.name = "oldValue" ;
    console.log(obj.name); //newValue

    我们首先定义了obj对象的name属性值为“name”,然后通过defineProperty方法来修改值,并且将其设置为不可修改的。接着我们再修改name属性的值,可以发现修改无效。
    如果我们通过defineProperty来修改name属性的值,是否可以修改呢?答案是可以的:

    ?
    1
    2
    3
    4
    Object.defineProperty(obj, "name" , {
       value: "oldValue"
    })
    console.log(obj.name); //oldValue

    访问器属性

    访问器属性有点类似于C#中的属性,和数据属性的区别在于,它没有数据属性的[[Writable]]和[[Value]]两个特性,而是拥有一对getter和setter函数。
    [[Get]]:读取属性时调用的函数,默认是undefined
    [[Set]]:设置属性时调用的函数,默认是undefined
    getter和setter是一个很有用的东西,假设有两个属性,其中第二个属性值会随着第一个属性值的变化而变化。这种场景在我们平时的编码中起始是非常常见的。在之前的做法中,我们往往要去手动修改第二个属性的值,那现在我们就可以通过get和set函数来解决这个问题。看下面这个例子:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var person = {
       age: 10
    }
     
    Object.defineProperty(person, "type" , {
       get: function () {
         if (person.age > 17) {
           return "成人" ;
         }
         return "小孩" ;
       }
    })
     
    console.log(person.type); //小孩
     
    person.age = 18;
    console.log(person.type); //成人

    通过修改age的值,type的值也会相应的修改,这样我们就不用再手动的去修改type的值了。
    下面这种方式也是可以实现同样的效果:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var person = {
       _age: 10,
       type: "小孩"
    }
     
    Object.defineProperty(person, "age" , {
       get: function () {
         return this ._age;
       },
       set: function (newValue) {
         this ._age = newValue;
         this .type = newValue > 17 ? "成人" : "小孩" ;
       }
    })
    console.log(person.type);
     
    person.age = 18;
    console.log(person.type);

    关于访问器属性,有几点要注意:
    1、严格模式下,必须同时设置get和set
    2、非严格模式下,可以只设置其中一个,如果只设置get,则属性是只读的,如果只设置set,属性则无法读取
    3、Object.defineProperty是ES5中的新方法,IE9(IE8部分实现,只有dom对象才支持)以下浏览器不支持,一些旧的浏览器可以通过非标准方法defineGetter()和defineSetter()来设置,这里就不说明了,有兴趣的同学可以查找相关资料。

    特性操作的相关方法

    ES5提供了一些读取或操作属性特性的方法,前面用到的Object.defineProperty就是其中之一。我总结了一些比较常用的方法如下:

    (1)Object.defineProperty
    定义一个对象的属性,这个方法前面我们已经用到多次,简单说说其用法。

    复制代码代码如下:

    Object.defineProperty(obj,propName,descriptor);

    defineProperty有点类似于定于在Object上的静态方法,通过Object直接调用,它接收3个参数:
    obj:需要定义属性的对象
    propNane:需要被定义的属性名称
    defineProperty:属性描述符,包含一些属性的特性定义
    例子如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    var obj = {};
    Object.defineProperty(obj, "name" , {
       value: "name" ,
       configurable: true ,
       writable: true ,
       enumerable: true
    });

    (2)Object.defineProperties
    和defineProperty类似,是用来定义对象属性的,不同的是它可以用来同时定义多个属性,我们通过命名也可以看出来,用法如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var obj = {};
    Object.defineProperty(obj, {
       "name" : {
         value: "name" ,
         configurable: true ,
         writable: true ,
         enumerable: true
       },
       "age" : {
         value: 20
       }
    });

    (3)Object.getOwnPropertyDescriptor
    ES5中还提供了一个读取特性值的方法,该方法接收对象及其属性名作为两个参数,返回一个对象,根据属性类型的不同,返回对象会包含不同的值。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var person = {
       _age: 10,
       type: "小孩"
    }
    Object.defineProperty(person, "age" , {
       get: function () {
         return this ._age;
       },
       set: function (newValue) {
         this ._age = newValue;
         this .type = newValue > 17 ? "成人" : "小孩" ;
       }
    })
     
    console.log(Object.getOwnPropertyDescriptor(person, "type" )); //Object {value: "成人", writable: true, enumerable: true, configurable: true}
    console.log(Object.getOwnPropertyDescriptor(person, "age" )); //Object {enumerable: false, configurable: false, get: function(),set: function ()}

    Object的方法

    在ES5中,Object对象上新增了一批方法,这些方法可以直接通过Object进行访问,前面用到的defineProperty就是新增的方法之一。除此之外还有很多方法,我将其总结归纳如下:

    对象创建型方法

    Object.create(proto, [propertiesObject])

    在前面我们提到,创建一个对象有两种方法:构造函数和对象字面量。
    这两种方法有一个缺点就是:如果要创建多个对象,写起来很繁琐,所以后来就有了一种创建自定义构造函数的方法来创建对象,如下所示:

    ?
    1
    2
    3
    4
    5
    6
    function Person(name, age) {
       this .name = name;
       this .age = age;
    }
     
    var person = new Person( "Jack" , 15);

    这种方式可以很方便的创建多个同样的对象,也是目前比较常用的方法。

    ES5提供的Object.create方法也是一个创建对象的方法,这个方法允许为创建的对象选择原型对象,不需要定义一个构造函数。用法如下:

    ?
    1
    2
    3
    4
    5
    6
    var obj = Object.create(Object.prototype, {
       name: {
         value: "Jack"
       }
    })
    console.log(obj.name); //Jack

    这个方法接收的第一个参数作为被创建对象的原型,第二个参数是对象的属性。注意:在这个例子中,name属性是无法被修改的,因为它没有设置writable特性,默认则为false。
    个人看法:Object.create这种创建对象的方式略显繁琐,除非是需要修改属性的特性,否则不建议使用这种方式创建对象。

    属性获取型方法

    Object.keys

    Object.keys是用来获取给定对象的所有可枚举的自身属性的属性名,它返回一个数组。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Parent() {
       this .lastName = "Black"
    }
    function Child(firstName) {
       this .firstName = firstName;
    }
    Child.prototype = new Parent();
     
    var son = new Child( "Jack" );
    console.log(Object.keys(son)); //["firstName"]

    代码中返回了firstName,并没有返回从prototype继承而来的lastName和不可枚举的相关属性。
    在一些旧的浏览器中,我们可以使用hasOwnProperty和for…in来达到类似的效果。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Object.keys = Object.keys ||
       function (obj) {
         var keys = [];
         for ( var key in obj) {
           if (obj.hasOwnProperty(key)) {
             keys.push(key);
           }
         }
         return keys;
       }
    Object.getOwnPropertyNames()

    getOwnPropertyNames用来获取对象自身的所有属性,包括可枚举和不可枚举的所有属性,如下所示:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Parent() {
       this .lastName = "Black"
    }
    function Child(firstName) {
       this .firstName = firstName;
    }
    Child.prototype = new Parent();
     
    var son = new Child( "Jack" );
    Object.defineProperty(son, "age" , {
       enumerable: false
    })
    console.log(Object.keys(son)); //["firstName"]
    console.log(Object.getOwnPropertyNames(son)); //["firstName", "age"]


    我们定义给son对象定义了一个不可枚举的属性age,然后通过keys和getOwnPropertyNames两个方法来获取属性列表,能明显看出了两者区别。

    属性特性型方法

    这个主要是前面提到的三个方法:defineProperty,defineProperties和getOwnPropertyDescriptor三个方法

    对象限制型方法

    ES5中提供了一系列限制对象被修改的方法,用来防止被某些对象被无意间修改导致的错误。每种限制类型包含一个判断方法和一个设置方法。

    阻止对象扩展

    Object.preventExtensions()用来限制对象的扩展,设置之后,对象将无法添加新属性,用法如下:

    复制代码代码如下:

    Object.preventExtensions(obj);

    该方法接收一个要被设置成无法扩展的对象作为参数,需要注意两点:
    1、对象的属性不可用扩展,但是已存在的属性可以被删除
    2、无法添加新属性指的是无法在自身上添加属性,如果是在对象的原型上,还是可以添加属性的。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(name) {
       this .name = name;
    }
    var person = new Person( "Jack" );
    Object.preventExtensions(person);
     
    delete person.name;
    console.log(person.name); //undefined
     
    Person.prototype.age = 15;
    console.log(person.age); //15

    Object.isExtensible方法用来判断一个对象是否可扩展,默认情况是true

    将对象密封

    Object.seal可以密封一个对象并返回被密封的对象。
    密封对象无法添加或删除已有属性,也无法修改属性的enumerable,writable,configurable,但是可以修改属性值。

    ?
    1
    2
    3
    4
    5
    6
    7
    function Person(name) {
       this .name = name;
    }
    var person = new Person( "Jack" );
    Object.seal(person);
    delete person.name;
    console.log(person.name); //Jack

    将对象密封后,使用delete删除对象属性,还是可以访问得到属性。

    通过Object.isSealed可以用来判断一个对象是否被密封了。

    冻结对象

    Object.freeze方法用来冻结一个对象,被冻结的对象将无法添加,修改,删除属性值,也无法修改属性的特性值,即这个对象无法被修改。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(name) {
       this .name = name;
    }
    var person = new Person( "Jack" );
    Object.freeze(person);
     
    delete person.name;
    console.log(person.name); //Jack
     
    Person.prototype.age = 15;
    console.log(person.age); //15

    分析上面的代码我们可以发现,被冻结的对象无法删除自身的属性,但是通过其原型对象还是可以新增属性的。

    通过Object.isFrozen可以用来判断一个对象是否被冻结了。

    可以发现:这三个限制对象的方法的限制程度是依次上升的。

    总结

    Object虽说是一个我们平时开发中最经常用到的对象,但是它的很多功能还没有被我们挖掘出来。本文首先介绍了Object的基本使用,接着介绍了一些比较少使用到的属性特性,最后分析了一些比较常用的方法,尤其是ES5中提供的新方法。欢迎大家交流!!


    Symbol类型(引用链接:http://blog.csdn.net/OLiver_web/article/details/54090289)

    ES6 中的 Symbol 也是一种数据类型,但是不是字符串,也不是对象,而是一种新的数据类型:第七种数据类型。

    下面我们来看一个场景,也许 Symbol 能派上用场。
    一个布尔值引出的问题

    有时,把一些属于其他对象的数据暂存在另一个对象中是非常方便的。例如,假设你正在编写一个 JS 库,使用 CSS 中的 transition 来让一个 DOM 元素在屏幕上飞奔,你已经知道不能同时将多个 transition 应用在同一个 div 上,否则将使得动画非常不美观,你也确实有办法来解决这个问题,但是首先你需要知道该 div 是否已经在移动中。

    怎么解决这个问题呢?

    其中一个方法是使用浏览器提供的 API 来探测元素是否处于动画状态,但杀鸡焉用牛刀,在将元素设置为移动时,你的库就知道了该元素正在移动。

    你真正需要的是一种机制来跟踪哪些元素正在移动,你可以将正在移动的元素保存在一个数组中,每次要为一个元素设置动画时,首先检查一下这个元素是否已经在这个列表中。

    啊哈,但是如果你的数组非常庞大,即便是这样的线性搜索也会产生性能问题。

    那么,你真正想做的就是直接在元素上设置一个标志:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if (element.isMoving) {
      smoothAnimations(element);
    }
    element.isMoving = true ;
     
      
    if (element.isMoving) {
      smoothAnimations(element);
    }
    element.isMoving = true ;

    这也有一些潜在的问题,不得不承认这样一个事实:还有其他代码也可能操作该 ODM 元素。

    •     在其他代码中,你创建的属性会被 for-in 或 Object.keys() 枚举出来;
    •     在其他一些库中也许已经使用了同样的方式(在元素上设置了相同的属性),那么这将和你的代码发生冲突,产生不可预计的结果;
    •     其他一些库可能在将来会使用同样的方式,这也会与你的代码发生冲突;
    •     标准委员会可能会为每个元素添加一个 .isMoving() 原生方法,那么你的代码就彻底不能工作了。

    当然,对于最后三个问题,你可以选择一个无意义的不会有人会使用到的字符串:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
      smoothAnimations(element);
    }
    element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true ;
      
    if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
      smoothAnimations(element);
    }
    element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true ;

    这似乎太不靠谱了,看了让人眼睛痛。

    你还可以用加密算法来生成一个几乎唯一的字符串:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // get 1024 Unicode characters of gibberish
    var isMoving = SecureRandom.generateName();
     
    ...
     
    if (element[isMoving]) {
      smoothAnimations(element);
    }
    element[isMoving] = true ;
      
    // get 1024 Unicode characters of gibberish
    var isMoving = SecureRandom.generateName();
      
    ...
      
    if (element[isMoving]) {
      smoothAnimations(element);
    }
    element[isMoving] = true ;

    object[name] 语法允许我们将任何字符串作为属性名,代码能正常工作,冲突几乎是不可能了,代码看起来也美观多了。

    但是,这回导致糟糕的调试体验,每次使用 console.log() 打印出包含该属性的元素时,你回看到一个庞大的垃圾字符串,并且如果还不止一个这样的属性呢?每次刷新后属性名都发生了变化,怎么样使这些属性看起来更加直观呢?

    为什么这么难?我们只是为了保存一个小小的标志位。
    用 Symbol 来解决问题

    Symbol 值可以由程序创建,并可以作为属性名,而且不用担心属性名冲突。

    ?
    1
    2
    3
    var mySymbol = Symbol();
     
    var mySymbol = Symbol();

    调用 Symbol() 方法将创建一个新的 Symbol 类型的值,并且该值不与其它任何值相等。

    与数字和字符串一样,Symbol 类型的值也可以作为对象的属性名,正是由于它不与任何其它值相等,对应的属性也不会发生冲突:

    ?
    1
    2
    3
    4
    5
    obj[mySymbol] = "ok!" ; // guaranteed not to collide
    console.log(obj[mySymbol]); // ok!
      
    obj[mySymbol] = "ok!" ; // guaranteed not to collide
    console.log(obj[mySymbol]); // ok!

    下面是使用 Symbol 来解决上面的问题:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // create a unique symbol
    var isMoving = Symbol( "isMoving" );
     
    ...
     
    if (element[isMoving]) {
      smoothAnimations(element);
    }
    element[isMoving] = true ;
      
    // create a unique symbol
    var isMoving = Symbol( "isMoving" );
      
    ...
      
    if (element[isMoving]) {
      smoothAnimations(element);
    }
    element[isMoving] = true ;

    上面代码需要注意几点:

    •     方法 Symbol("isMoving") 中的 "isMoving" 字符串被称为 Symbol 的描述信息,这对调试非常有帮助。可以通过 console.log(isMoving) 打印出来,或通过 isMoving.toString() 将 isMoving 转换为字符串时,或在一些错误信息中显示出来。
    •     element[isMoving] 访问的是 symbol-keyed 属性,除了属性名是 Symbol 类型的值之外,与其它属性都一样。
    •     和数组一样,symbol-keyed 属性不能通过 . 操作符来访问,必须使用方括号的方式。
    •     操作 symbol-keyed 属性也非常方便,通过上面代码我们已经知道如何获取和设置 element[isMoving] 的值,我们还可以这样使用:if (isMoving in element) 或 delete element[isMoving]。
    •     另一方面,只有在 isMoving 的作用域范围内才可以使用上述代码,这可以实现弱封装机制:在一个模块内创建一些 Symbol,只有在该模块内部的对象才能使用,而不用担心与其它模块的代码发生冲突。

    由于 Symbol 的设计初衷是为了避免冲突,当遍历 JavaScript 对象时,并不会枚举到以 Symbol 作为建的属性,比如,for-in 循环只会遍历到以字符串作为键的属性,Object.keys(obj)和 Object.getOwnPropertyNames(obj) 也一样,但这并不意味着 Symbol 为键的属性是不可枚举的:使用 Object.getOwnPropertySymbols(obj) 这个新方法可以枚举出来,还有 Reflect.ownKeys(obj) 这个新方法可以返回对象中所有字符串和 Symbol 键。(我将在后面的文章中详细介绍 Reflect 这个新特性。)

    库和框架的设计者将会发现很多 Symbol 的用途,稍后我们将看到,JavaScript 语言本身也对其有广泛的应用。
    Symbol 究竟是什么呢

    ?
    1
    2
    3
    4
    5
    > typeof Symbol()
    "symbol"
      
    > typeof Symbol()
    "symbol"

    Symbol 是完全不一样的东西。一旦创建后就不可更改,不能对它们设置属性(如果在严格模式下尝试这样做,你将得到一个 TypeError)。它们可以作为属性名,这时它们和字符串的属性名没有什么区别。

    另一方面,每个 Symbol 都是独一无二的,不与其它 Symbol 重复(即便是使用相同的 Symbol 描述创建),创建一个 Symbol 就跟创建一个对象一样方便。

    ES6 中的 Symbol 与传统语言(如 Lisp 和 Ruby)中的 Symbol 中的类似,但并不是完全照搬到 JavaScript 中。在 Lisp 中,所有标识符都是 Symbol;在 JavaScript 中,标识符和大多数属性仍然是字符串,Symbol 只是提供了一个额外的选择。

    值得注意的是:与其它类型不同的是,Symbol 不能自动被转换为字符串,当尝试将一个 Symbol 强制转换为字符串时,将返回一个 TypeError。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    > var sym = Symbol( "<3" );
    > "your symbol is " + sym
    // TypeError: can't convert symbol to string
    > `your symbol is ${sym}`
    // TypeError: can't convert symbol to string
      
    > var sym = Symbol( "<3" );
    > "your symbol is " + sym
    // TypeError: can't convert symbol to string
    > `your symbol is ${sym}`
    // TypeError: can't convert symbol to string

    应该避免这样的强制转换,应该使用 String(sym) 或 sym.toString() 来转换。
    获取 Symbol 的三种方法

    1.     Symbol() 每次调用时都返回一个唯一的 Symbol。
    2.     Symbol.for(string) 从 Symbol 注册表中返回相应的 Symbol,与上个方法不同的是,Symbol 注册表中的 Symbol 是共享的。也就是说,如果你调用 Symbol.for("cat") 三次,都将返回相同的 Symbol。当不同页面或同一页面不同模块需要共享 Symbol 时,注册表就非常有用。
    3.     Symbol.iterator 返回语言预定义的一些 Symbol,每个都有其特殊的用途。

    如果你仍不确定 Symbol 是否有用,那么接下来的内容将非常有趣,因为我将为你演示 Symbol 的实际应用。
    Symbol 在 ES6 规范中的应用

    我们已经知道可以使用 Symbol 来避免代码冲突。之前在介绍 iterator 时,我们还解析了 for (var item of myArray) 内部是以调用 myArray[Symbol.iterator]() 开始的,当时我提到这个方法可以使用 myArray.iterator() 来代替,但是使用 Symbol 的后向兼容性更好。

    在 ES6 中还有一些地方使用到了 Symbol。(这些特性还没有在 FireFox 中实现。)

    •     使 instanceof 可扩展。在 ES6 中,object instanceof constructor 表达式被标准化为构造函数的一个方法:constructor[Symbol.hasInstance](object),这意味着它是可扩展的。
    •     消除新特性和旧代码之间的冲突。
    •     支持新类型的字符串匹配。在 ES5 中,调用 str.match(myObject) 时,首先会尝试将 myObject 转换为 RegExp 对象。在 ES6 中,首先将检查 myObject 中是否有 myObject[Symbol.match](str) 方法,在所有正则表达式工作的地方都可以提供一个自定义的字符串解析方法。

    二、JS的原生对象和内置对象


    1、原生对象(new后产生的对象)

    native object 

    object in an ECMAScript implementation whose semantics are fully defined by this specification rather than by the host environment.

    原生对象:独立于宿主环境的ECMAScript 实现提供的对象

    NOTE Standard native objects are defined in this specification. Some native objects are built-in; others may be constructed during the course of execution of an ECMAScript program.

    注:一些原生对象是内置对象,另外一些原生对象在执行ECMAScript程序的过程中生成。
    原生对象包括:
    ObjectFunctionArrayStringBooleanNumberDateRegExpErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError、ActiveXObject(服务器方面)、Enumerator(集合遍历类)、RegExp(正则表达式)

    2、内置对象(不需要new)

    built-in object 

    object supplied by an ECMAScript implementation, independent of the host environment, that is present at the start of the execution of an ECMAScript program.

    内置对象:由ECMAScript实现提供的对象,独立于宿主环境,在一个脚本程序执行的开始处。 

    NOTE Standard built-in objects are defined in this specification, and an ECMAScript implementation may specify and define others. Every built-in object is a native object. A built-in constructor is a built-in object that is also a constructor.

    注:每个内置对象(built-in object)都是原生对象(Native Object),一个内置的构造函数是一个内置的对象,也是一个构造函数。

    来源:http://es5.github.io/#x4.3.7

    ECMA-262 只定义了两个新的内置对象,即 Global 和 Math (它们也是原生对象,根据定义,每个内置对象都是原生对象)。

    内置对象包括:

    global、ObjectFunctionArrayStringBooleanNumberMathDateRegExpJSONError对象(Error,   EvalError, RangeError, ReferenceError,   SyntaxError, TypeErrorURIError

    math对象:http://www.w3school.com.cn/jsref/jsref_obj_math.asp

    Math 对象并不像 Date 和 String 那样是对象的类,因此没有构造函数 Math(),像 Math.sin() 这样的函数只是函数,不是某个对象的方法。您无需创建它,通过把 Math 作为对象使用就可以调用其所有属性和方法。

    global对象:http://www.w3school.com.cn/js/jsref_obj_global.asp

    全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。全局对象不是任何对象的属性,所以它没有名称。

    在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。但通常不必用这种方式引用全局对象,因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

    我们也可以修改内置对象的原型(引用链接https://segmentfault.com/a/1190000002634958)

    if (!Array.prototype.forEach) { 
      Array.prototype.forEach = function(fn){ 
        for ( var i = 0; i < this.length; i++ ) { 
          fn( this[i], i, this ); 
        } 
      }; 
    } 
     
    ["a", "b", "c"].forEach(function(value, index, array){ 
      assert( value, "Is in position " + index + " out of " + (array.length - 1) ); 
    });
    

    以上代码将输出:

    PASS Is in position 0 out of 2
    PASS Is in position 1 out of 2
    PASS Is in position 2 out of 2  
    

    注意:扩展原型是很危险的:

    Object.prototype.keys = function(){ 
      var keys = []; 
      for ( var i in this ) 
        keys.push( i ); 
      return keys; 
    }; 
     
    var obj = { a: 1, b: 2, c: 3 }; 
     
    assert( obj.keys().length == 3, "We should only have 3 properties." ); 
     
    delete Object.prototype.keys;
    

    输出: FAIL We should only have 3 properties.

    如果不是有特殊需要而去扩展原生对象和原型(prototype)的做法是不好的

    //不要这样做 
    Array.prototype.map = function() { 
        // code 
    }; 

    除非这样做是值得的,例如,向一些旧的浏览器中添加一些ECMAScript5中的方法。 
    在这种情况下,我们一般这样做:

     <script type="text/javascript">
        if (!Array.prototype.map) {
            Array.prototype.map = function() {
                //code
            };
        }
    </script>

    三、对象的创建(引用链接https://segmentfault.com/a/1190000002634958)

    JavaScript 支持四种类型的对象:内部对象、生成的对象、宿主给出的对象(如 Internet 浏览器中的 window 和 document)以及 ActiveX 对象(外部组件)。

    Microsoft Jscript 提供了 11 个内部(或“内置”)对象。它们是Array、Boolean、Date、Function、Global、Math、Number、Object、RegExp、Error 以及 String 对象。每一个对象有相关的方法和属性,

    JavaScript中对象的创建有以下几种方式:

    1)使用内置对象
    (2)使用JSON符号
    (3)自定义对象构造
    

    一、使用内置对象

    JavaScript可用的内置对象可分为两种:
    
    1,JavaScript语言原生对象(语言级对象),如StringObjectFunction等;
    2,JavaScript运行期的宿主对象(环境宿主级对象),如windowdocument、body等。

    内置对象列表

    Array
    Boolean
    Date
    Error
    EvalError
    Function
    Infinity
    JSON
    Map
    Math
    NaN
    Number
    Object
    ParallelArray
    Promise
    Proxy
    RegExp
    Set
    String
    Symbol
    SyntaxError
    Uint32Array
    WeakSet
    decodeURI
    decodeURIComponent()
    encodeURI()
    encodeURIComponent()
    escape()已废弃
    eval()
    isFinite()
    isNaN()
    null
    parseFloat
    parseInt
    undefined
    

    以上资料来源于:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects#.E5.9F.BA.E6.9C.AC.E5.AF.B9.E8.B1.A1

    自定义对象构造

    创建高级对象构造有两种方式:使用“this”关键字构造、使用原型prototype构造

    Date对象

    get系列方法

    getDate() 从 Date 对象返回一个月中的某一天 (1 ~ 31)。
    getDay() 从 Date 对象返回一周中的某一天 (0 ~ 6)。
    getMonth() 从 Date 对象返回月份 (0 ~ 11)。
    getFullYear() 从 Date 对象以四位数字返回年份。注意不要使用getYear()。
    getHours() 返回 Date 对象的小时 (0 ~ 23)。
    getMinutes() 返回 Date 对象的分钟 (0 ~ 59)。
    getSeconds() 返回 Date 对象的秒数 (0 ~ 59)。
    getMilliseconds() 返回 Date 对象的毫秒(0 ~ 999)。
    getTime() 返回 197011 日至今的毫秒数。
    getTimezoneOffset() 返回本地时间与格林威治标准时间 (GMT) 的分钟差。
    getUTCDate() 根据世界时从 Date 对象返回月中的一天 (1 ~ 31)。
    getUTCDay() 根据世界时从 Date 对象返回周中的一天 (0 ~ 6)。
    getUTCMonth() 根据世界时从 Date 对象返回月份 (0 ~ 11)。
    getUTCFullYear() 根据世界时从 Date 对象返回四位数的年份。
    getUTCHours() 根据世界时返回 Date 对象的小时 (0 ~ 23)。
    getUTCMinutes() 根据世界时返回 Date 对象的分钟 (0 ~ 59)。
    getUTCSeconds() 根据世界时返回 Date 对象的秒钟 (0 ~ 59)。
    getUTCMilliseconds() 根据世界时返回 Date 对象的毫秒(0 ~ 999)。
    

    set系列方法

    setDate() 设置 Date 对象中月的某一天 (1 ~ 31)。
    setMonth() 设置 Date 对象中月份 (0 ~ 11)。
    setFullYear() 设置 Date 对象中的年份(四位数字)。注意不要使用setYear()方法。
    setHours() 设置 Date 对象中的小时 (0 ~ 23)。
    setMinutes() 设置 Date 对象中的分钟 (0 ~ 59)。
    setSeconds() 设置 Date 对象中的秒钟 (0 ~ 59)。
    setMilliseconds() 设置 Date 对象中的毫秒 (0 ~ 999)。
    setTime() 以毫秒设置 Date 对象。
    setUTCDate() 根据世界时设置 Date 对象中月份的一天 (1 ~ 31)。
    setUTCMonth() 根据世界时设置 Date 对象中的月份 (0 ~ 11)。
    setUTCFullYear() 根据世界时设置 Date 对象中的年份(四位数字)。
    setUTCHours() 根据世界时设置 Date 对象中的小时 (0 ~ 23)。
    setUTCMinutes() 根据世界时设置 Date 对象中的分钟 (0 ~ 59)。
    setUTCSeconds() 根据世界时设置 Date 对象中的秒钟 (0 ~ 59)。
    setUTCMilliseconds() 根据世界时设置 Date 对象中的毫秒 (0 ~ 999)。
    

    toString系列方法

    toString() 把 Date 对象转换为字符串,toString()总是返回一个用美式英语表达的字符串。
    toTimeString() 把 Date 对象的时间部分转换为字符串。
    toDateString() 把 Date 对象的日期部分转换为字符串。
    toUTCString() 根据世界时,把 Date 对象转换为字符串。
    toLocaleString() 根据本地时间格式,把 Date 对象转换为字符串。
    toLocaleTimeString() 根据本地时间格式,把 Date 对象的时间部分转换为字符串。
    toLocaleDateString() 根据本地时间格式,把 Date 对象的日期部分转换为字符串。
    

    Array对象

    属性

    constructor
        指定创建对象原型的函数。
    
    index
        如果数组是通过正则表达式匹配创建的,比配是字符串的下标索引为0.
    
    input
        如果数组是通过正则表达式匹配创建的,返回原始的字符串。
    length长度
        返回数组中元素个数.
    
    prototype
        允许为所有对象附加属性.
    

    方法

    这些方法可以改变数组自身:

    pop
        移除数组的最后一个元素,返回值是被删除的元素。
    
    push
        在数组的末尾添加一个或者多个元素,返回值是新的数组的长度。
    
    reverse
        颠倒数组中元素的顺序,原先第一个元素现在变成最后一个,同样原先的最后一个元素变成了现在的第一个,也就是数组的索引发生了变化。
    
    shift
        删除数组的第一个元素,返回值是删除的元素。
    
    sort
        对数组中的元素进行排序。
    
    splice
        添加或删除数组中的一个或多个元素。
    
    unshift
        添加一个或者多个元素在数组的开头,返回值是新的数组的长度。

    Accessor 方法

    这些过程不改变数组自身

    concat
        返回一个包含此数组和其他数组和/或值的结合的新数组
    indexOf
        返回第一个与给定参数相等的数组元素的索引,没有找到则返回-1join
        将所有的数组元素连接成一个字符串。
    
    lastIndexOf
        返回在数组中搜索到的与给定参数相等的元素的最后(最大)索引。
    
    slice
        返回数组中的一段。
    
    toSource
        Returns an array literal representing the specified array; you can use this value to create a new array. Overrides the Object.toSource method.
    
    toString
        返回代表该数组及其元素的字符,重写Object.toString 过程.
    
    valueOf
        Returns the primitive value of the array. Overrides the Object.valueOf method.
    

    循环(迭代)过程

    filter
        对数组中的每一个元素调用参数中指定的过滤函数,并将对于过滤函数返回值为true的那些数组元素集合为新的数组返回。
    
    forEach
        对数组的每一个元素依次调用参数中指定的函数。
    
    every
        如果数组中每一个元素都满足参数中提供的测试函数,则返回真。
    
    map
        Creates a new array with the results of calling a provided function on every element in this array.
    
    some
        如果数组中至少有一个元素满足参数函数的测试,则返回true

    总结:

    改变原数组的方法:pop()、push()、reverse()、shift()、sort()、splice()、unshift()
    
    不改变原数组的方法:concat()、join()、slice()、toString()
    
    

    Boolean

    属性

    Boolean.length
        长度属性,值为1.
    Boolean.prototype
        代表Boolean构造器的原型.   
    

    方法

    Boolean.prototype.toSource() 
    Boolean.prototype.toString()
    Boolean.prototype.valueOf()
    

    String 对象

    属性

    String.length
    String.prototype
    

    方法:

    charAt() 方法可返回指定位置的字符。stringObject.charAt(index)
    
    charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 – 65535 之间的整数。 stringObject.charCodeAt(index)
    
    concat() 方法用于连接两个或多个字符串。
    stringObject.concat(stringX, stringX, …, stringX)
       
    indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。
    stringObject.indexOf(searchvalue, fromindex)
    
    lastIndexOf() 方法可返回一个指定的字符串值最后出现的位置,在一个字符串中的指定位置从后向前搜索。
    
    localeCompare():用本地特定的顺序来比较两个字符串stringObject.localeCompare(target)
    
    match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
    该方法类似 indexOf()lastIndexOf(),但是它返回指定的值,而不是字符串的位置。
    stringObject.match(regexp)
    
    
    replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。stringObject.replace(regexp/substr, replacement)
    
    search() 方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。
    stringObject.search(regexp)
    
    slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
    stringObject.slice(start, end)
    
    substring()不推荐使用,建议使用slice()替代。
    
    substr()不推荐使用,建议使用slice()替代。
    
    toLocaleLowerCase()不推荐使用,只在土耳其语等少数语种中有用,建议使用toLowerCase()替代。
    
    toLocaleUpperCase()不推荐使用,只在土耳其语等少数语种中有用,建议使用toUpperCase()替代。
    
    toLowerCase() 方法用于把字符串转换为小写。
    
    toUpperCase() 方法用于把字符串转换为大写。
    

    注意:String 对象的方法 slice()、substring() 和 substr() 都可返回字符串的指定部分。强烈建议在所有场合都使用 slice()方法。

    RegExp对象

    属性

     RegExp.prototype.constructor
     创建该正则对象的构造函数。
     
     RegExp.prototype.global
     是否开启全局匹配,也就是匹配目标字符串中所有可能的匹配项,而不是只进行第一次匹配。
    
     RegExp.prototype.ignoreCase
     在匹配字符串时是否要忽略字符的大小写。
    
     RegExp.prototype.lastIndex
     下次匹配开始的字符串索引位置。
    
     RegExp.prototype.multiline
     是否开启多行模式匹配(影响 ^ 和 $ 的行为)
    
     RegExp.prototype.source
     正则对象的源模式文本。
    

    方法

     RegExp.prototype.exec()
        在目标字符串中执行一次正则匹配操作。
     RegExp.prototype.test()
        测试当前正则是否能匹配目标