精华内容
下载资源
问答
  • Shell中的循环结构

    千次阅读 2019-06-30 22:07:00
    与其他的程序设计语言一样,for循环都是初学者在学习循环结构时的入门课程。for循环通常用于遍历整个对象或者数字列表。按照循环条件的不同,for循环语句可以分为带列表的for循环、不带列表的for循环以及类C风格的...

    步进循环语句for

    for循环是最简单,也是最常用的循环语句。for循环通常用于遍历整个对象或者数字列表。按照循环条件的不同,for循环语句可以分为带列表的for循环、不带列表的for循环以及类C风格的for循环。

    带列表的for循环语句

    带列表的for循环通常用于将一组语句执行已知的次数,其基本语法如下:

    for variable in {list}
    do
       statement1
       statement2
       ...
    done
    

    在上面的语法中,variable称为循环变量,list是一个列表,可以是一系列的数字或者字符串,元素之间使用空格隔开。do和done之间的所有的语句称为循环体,即循环结构中重复执行的语句。for循环体的执行次数与list中元素的个数有关。在带列表的for语句的执行时,Shell会将in关键字后面的list列表的第1个元素的值赋给变量variable,然后执行循环体;当循环体中的语句执行完毕之后,Shell会将列表中的第2元素的值赋给变量variable,然后再次执行循环体。当list列表中的所有的元素都被访问后,for循环结构终止,程序将继续执行done语句后面的其他的语句。

    【例1】演示将数字列表作为循环条件列表的for语句的使用方法

    #! /bin/bash
    
    #for循环开始
    for var in 1 2 3 4 5 6 7 8
    do
       #依次输出列表中的数字
       echo "the number is $var"
    done
    

    运行以上程序:

    [root@localhost ~]# ./test.sh
    the number is 1
    the number is 2
    the number is 3
    the number is 4
    the number is 5
    the number is 6
    the number is 7
    the number is 8
    

    【例2】本例将【例1】进行改造,使用了简单的书写方法

    #! /bin/bash
    
    #使用省略的写法表示某个范围
    for var in {1..8}
    do
       echo "the number is $var"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    the number is 1
    the number is 2
    the number is 3
    the number is 4
    the number is 5
    the number is 6
    the number is 7
    the number is 8
    

    Shell允许用户指定for语句的步长。当用户需要另外指定步长时,其基本语法如下:

    for varibale in {start..end..step}
    do
       statement1
       statement2
       ...
    done
    

    【例3】演示通过for循环,并配合步长来计算100以内奇数的和

    #! /bin/bash
    #定义变量,并赋初值为0
    sum=0;
    #for循环开始,设置起始数值为1,结束数值为100,步长为2
    for i in {1..100..2}
    do
       #将数累加
       let "sum+=i"
    done
    echo "the sum is $sum"
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    the sum is 2500
    

    【例4】本例将1周中7天的名字作为列表条件,依次输出每天的名称

    #! /bin/bash
    #for循环开始
    for day in {Mon Tue Wed Thu Fri Sat Sun}
    do
       #输出循环变量的值
       echo "$day"
    done
    

    运行以上程序:

    [root@localhost ~]# ./test.sh
    {Mon
    Tue
    Wed
    Thu
    Fri
    Sat
    Sun}
    

    【例5】通过ls命令的输出结果作为for循环的执行条件

    #! /bin/bash
    
    #使用ls命令的执行结果作为列表
    for file in $(ls)
    do
       #输出每个文件名
       echo "$file"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    aaa
    anaconda-ks.cfg
    Desktop
    Documents
    Downloads
    initial-setup-ks.cfg
    Music
    Pictures
    Public
    Templates
    test.sh
    Videos
    

    【例6】使用通配符“*”作为条件列表

    #! /bin/bash
    #使用通配符作为列表条件
    for file in *
    do
       echo "$file"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    aaa
    anaconda-ks.cfg
    Desktop
    Documents
    Downloads
    initial-setup-ks.cfg
    Music
    Pictures
    Public
    Templates
    test.sh
    Videos
    

    【例7】说明如何使用for循环逐个处理脚本的参数值

    #! /bin/bash
    #输出所有的参数
    echo "$*"
    #将参数列表作为条件
    for arg in $*
    do
       #依次输出各个参数值
       echo "${arg}"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh a b c d de f
    a b c d de f
    a
    b
    c
    d
    de
    f
    

    不带列表的for循环语句

    在某些特殊情况下,for循环的条件列表可以完全省略,称为不带列表的for循环语句。如果没有为for循环提供条件列表,Shell将从命令行获取条件列表。不带列表的for循环语句的一般语法如下:

    for variable
    do
       statement1
       statement2
       ...
    done
    

    由于系统变量$@同样可以获取所有的参数,所以以上的语法等价于以下语法:

    for variable in $@
    do
       statement1
       statement2
       ...
    done
    

    也同样等价于以下语法:

    for variable in $*
    do
       statement1
       statement2
       ...
    done
    

    【例8】演示不带列表的for循环语句的使用方法

    #! /bin/bash
    #不带条件列表
    for arg
    do
       #输出每个参数
       echo "$arg"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh a b c d de f 
    a
    b
    c
    d
    de
    f
    

    类C风格的for循环语句

    类C风格的for循环语句的基本语法如下:

    for ((expression1;expression2;expression3))
    do
       statement1;
       statement2;
       ...
    done
    

    在上面的语法中,for循环语句的执行条件被2个圆括号包括起来。执行条件分为3个部分,由2个分号隔开,第1部分expression1通常时条件变量初始化的语句;第2部分expression2是决定是否执行for循环的条件。当expression2的值为0时,执行整个循环体;当expression2的值为非0时,退出for循环体。第3部分,即表达式expression3通常用来改变条件变量的值,例如递增或者递减等。
    【例9】演示类C风格的for循环语句的使用方法

    #! /bin/bash
    #for循环开始
    for (( i=1;i<5;i++))
    do
       #输出循环变量i的值
       echo "$i"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    1
    2
    3
    4
    

    使用for循环语句处理数组

    使用for循环遍历数组非常方便。针对数组,Shell专门提供了一种特殊语法的for循环语句,其基本语法如下:

    for variable in ${array[*]}
    do
       statement1
       statement2
       ...
    done
    

    其中,变量variable是循环变量,in关键字后面的部分表示要遍历的数组,其中array表示数组的名称。在遍历数组的过程中,for循环语句会将每个数组元素的值赋给循环变量variable。因此,用户可以在循环体中对每个数组元素进行相应的操作。
    【例10】演示通过for循环来遍历数组

    #! /bin/bash
    #定义数组
    array=(Monday Tuesday Wednesday Thursday Friday Saturday Sunday)
    #通过for循环遍历数组元素
    for day in ${array[*]}
    do
       #输出每个数组元素的值
       echo $day
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
    

    until循环语句

    until循环语句同样也存在于多种程序设计语言中。顾名思义,until语句的作用时将循环体重复执行,直到某个条件成立为止。恰当地使用until语句,可以收到事半功倍地效果。

    until语句的基本语法

    until循环语句的功能是不断地重复执行循环体中的语句,直至某个条件成立。until语句的基本语法如下:

    until expression
    do
       statement1
       statement2
       ...
    done
    

    在上面的语法中,expression是一个条件表达式。当该表达式的值不为0时,将执行do和done之间的语句;当expression的值为0时,将退出until循环结构,继续执行done语句后面的其它的语句。
    【例11】演示until语句的使用方法

    #! /bin/bash
    #定义循环变量i
    i=1
    #当i的值小于9时执行循环
    until [[ "$i" -gt 9 ]]
    do
       #计算i的平方
       let "square=i*i"
       #输出i的平方
       echo "$i*$i=$square"
       #循环变量加1
       let "i=i+1"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    1*1=1
    2*2=4
    3*3=9
    4*4=16
    5*5=25
    6*6=36
    7*7=49
    8*8=64
    9*9=81
    

    利用until语句批量增加用户

    until :这个结构在循环的顶部判断条件,并且如果条件一直为 false 那就一直循环下去.(与 while 相反)。注意: until 循环的判断在循环的顶部,这与某些编程语言是不同的. 与 for 循环一样,如果想把 do 和条件放在一行里,就使用";
    【例】演示基础的until语句

    [root@foundation0 ~]# cat test.sh 
    #!/bin/bash 
    END_CONDITION=end 
    until [ "$var1" = "$END_CONDITION" ] 
    # 在循环的顶部判断条件. 
    do 
    echo "Input variable #1 " 
     echo "($END_CONDITION to exit)" 
     read var1 
     echo "variable #1 = $var1" 
     echo 
     done 
     exit 0 
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    ./test.sh: line 3: [: missing `]'
    Input variable #1
    (end to exit)
    8
    variable #1 = 8
    

    【例12】演示如何通过循环语句来批量地增加用户,并设置用户的初始密码

    #! /bin/bash
    #定义变量i
    i=1
    #一直循环到变量i的值为21
    until [ "$i" -eq 21 ]
    do
       #执行useradd命令添加用户
       useradd user$i
       #修改用户密码
       echo "password" | passwd --stdin user$i > /dev/null
       #循环变量自增
       let "i++"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    [root@localhost ~]# more /etc/passwd
    ........
    user1:x:1001:1001::/home/user1:/bin/bash
    user2:x:1002:1002::/home/user2:/bin/bash
    user3:x:1003:1003::/home/user3:/bin/bash
    ......
    user12:x:1012:1012::/home/user12:/bin/bash
    user13:x:1013:1013::/home/user13:/bin/bash
    user14:x:1014:1014::/home/user14:/bin/bash
    .......
    user20:x:1020:1020::/home/user20:/bin/bash
    

    【例13】介绍如何通过循环删除系统用户

    #! /bin/bash
    i=1
    until [ "$i" -eq 21 ]
    do
       #依次删除用户
       userdel -r user$i
       let "i++"
    done
    

    运行以上程序

    【例12】中创建的用户被删除
    

    while循环语句

    while循环是另外一种常见的循环结构。使用while循环结构,可以使得用户重复执行一系列的操作,直到某个条件的发生。这听起来好像跟until循环非常相似,但是与until语句相比,while语句有着较大的区别。

    while语句的基本语法

    while循环语句的基本语法如下:

    while expression
    do
    	statement1
    	statement2
    	...
    done
    

    在上面的语法中,expression表示while循环体执行时需要满足的条件。虽然可以使用任意合法的Shell命令,但是,通常情况下,expression代表一个测试表达式。与其他的循环结构一样,do和done这2个关键字之间的语句构成了循环体。

    通过计数器控制while循环结构

    所谓计数器,实际上就是指一个循环变量,当该变量的值在某个范围内时,执行循环体;当超过该范围时,终止循环。
    【例14】使用while循环来输出1~9的平方

    #! /bin/bash
    #定义循环变量
    i=1
    #while循环开始
    while [[ "$i" -lt 10 ]]
    do
       #计算平方
       let "square=i*i"
       #输出平方
       echo "$i*$i=$square"
       #循环 变量自增
       let "i=i+1"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    1*1=1
    2*2=4
    3*3=9
    4*4=16
    5*5=25
    6*6=36
    7*7=49
    8*8=64
    9*9=81
    

    【例15】使用while循环来输出变量值

    #!/bin/bash 
    echo 
    #等价于: 
    while [ "$var1" != "end" ] # while test "$var1" != "end" 
    do 
    echo "Input variable #1 (end to exit) " 
    read var1 # 为什么不使用'read $var1'? 
    echo "variable #1 = $var1" # 因为包含"#"字符,所以需要"" 
    # 如果输入为'end',那么就在这里 echo. 
    # 不在这里判断结束,在循环顶判断. 
    echo 
    done  
    exit 0 
    
    [root@localhost ~]# ./test.sh
    
    Input variable #1 (end to exit) 
    test
    variable #1 = test
    
    Input variable #1 (end to exit) 
    test2
    variable #1 = test2
    
    Input variable #1 (end to exit) 
    test3
    variable #1 = test3
    
    Input variable #1 (end to exit) 
    end
    variable #1 = end
    
    [root@localhost ~]# 
    
    

    通过计数器控制while循环结构

    一个 while 循环可以有多个判断条件,但是只有最后一个才能决定是否退出循环.然而这需要一种有点不同的循环语法:
    【例16】多条件的 while 循环

    #!/bin/bash 
    var1=unset 
    previous=$var1
    
    while echo "previous-variable = $previous" 
            echo 
            previous=$var1 
            [ "$var1" != end ] 
    do
    echo "Input variable #1 (end to exit) " 
    read var1
    echo "variable #1 = $var1" 
    done
    exit 0
    
    [root@localhost ~]# ./test.sh
    previous-variable = unset
    
    Input variable #1 (end to exit) 
    test1
    variable #1 = test1
    previous-variable = unset
    
    Input variable #1 (end to exit) 
    test1
    variable #1 = test1
    previous-variable = test1
    
    Input variable #1 (end to exit) 
    test
    variable #1 = test
    previous-variable = test1
    
    Input variable #1 (end to exit) 
    end
    variable #1 = end
    previous-variable = test
    
    [root@localhost ~]# 
    
    

    通过计数器控制while循环结构

    与 for 循环一样,while 循环也可通过(())来使用 C 风格语法.
    【例17】C风格的 while 循环

    [root@foundation0 ~]# cat test.sh 
    #!/bin/bash
    LIMIT=10
    ((a = 1)) # a=1 
    # 双圆括号允许赋值两边的空格,就像 C 语言一样. 
    while (( a <= LIMIT )) # 双圆括号, 变量前边没有"$". 
    do 
    echo -n "$a " 
    ((a += 1)) # let "a+=1" 
    # Yes, 看到了吧. 
    # 双圆括号允许像 C 风格的语法一样增加变量的值. 
    done 
    echo 
    # 现在,C 程序员可以在 Bash 中找到回家的感觉了吧. 
    exit 0 
    

    运行以上程序:

    [root@localhost ~]# ./test.sh
    1 2 3 4 5 6 7 8 9 10 
    
    
    注意:while 循环的 stdin 可以用<来重定向到文件. 
     whild 循环的 stdin 支持管道. 
    

    通过结束标记控制while循环结构

    在某些情况下,用户可能不知道while循环会执行多少次。此时,用户就无法使用计数器来控制while循环。为了处理这种情况,用户可以在程序中设置一个特殊的标记值,当该标记值出现时,终止while循环。这种特殊的标识值称为结束标记。
    【例18】演示如何通过结束标记来控制while循环

    #! /bin/bash
    #提示用户输入数字
    echo "Please enter a number between 1 and 10.Enter 0 to exit."
    #读取用户输入的数字
    read var
    #while循环开始
    while [[ "$var" != 0 ]]
    do
       #提示用户输入数字太小
       if [ "$var" -lt 5 ]
       then
          echo "Too small. Try again."
          read var
       #提示用户输入数字太大
       elif [ "$var" -gt 5 ]
       then
          echo "Too big. Try again."
          read var;
        else
           echo "Congratulation! You are right."
           exit 0;
        fi
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    Please enter a number between 1 and 10.Enter 0 to exit.
    7
    Too big.Try again.
    3
    Too amall.Try again.
    5
    Congratulation!You are right.
    

    理解while语句与until语句的区别

    语法结构非常相似,都是将循环条件放在了语句的后面:

    while expression
    do
    ...
    done
    until语句的语法结构如下:
    unitl expression
    do
    ...
    done
    

    但是,在while语句中,当expression的值为0时才执行循环体中的语句,当expression的值为非0值时,将退出循环体;在until语句中,当expression的值为非0时,执行循环体中的语句,当expression的值为0时,将退出循环结构。因此,用户在使用这2种语句时,一定要注意区分这个关键的地方。
    在执行机制方面,这2个语句是相同的,即首先会判断expression的值,当该表达式的值符合要求时,才执行循环体中的语句;否则,都不会执行循环体。

    嵌套循环

    在程序设计语言中,嵌套的循环也是一种非常常见的结构。Shell同样也支持嵌套循环。通过嵌套循环,可以完成更复杂的功能。本节将介绍Shell中嵌套循环的使用方法。嵌套循环就是在一个循环中还有一个循环,内部循环在外部循环体中.在外部循环的每次执行过程中都会触发内部循环,直到内部循环执行结束.外部循环执行了多少次,内部循环就完成多少次.当然,不论是外部循环或内部循环的 break 语句都会打断处理过程.
    【例19】使用2层循环打印出乘法表

    #! /bin/bash
    
    #外层循环
    for ((i=1;i<=9;i++))
    do
       	#内层循环
       	for ((j=1;j<=i;j++))
       	do
          		#计算2个数的乘积
          		let "product=i*j"
          		#输出乘积
          		printf "$i*$j=$product"
          		#输出空格分隔符
          		if [[ "$product" -gt 9 ]]
          		then
             		printf "   "
          		else
             		printf "    "
          		fi
       	done
       	echo
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    1*1=1    
    2*1=2    2*2=4    
    3*1=3    3*2=6    3*3=9    
    4*1=4    4*2=8    4*3=12   4*4=16   
    5*1=5    5*2=10   5*3=15   5*4=20   5*5=25   
    6*1=6    6*2=12   6*3=18   6*4=24   6*5=30   6*6=36   
    7*1=7    7*2=14   7*3=21   7*4=28   7*5=35   7*6=42   7*7=49   
    8*1=8    8*2=16   8*3=24   8*4=32   8*5=40   8*6=48   8*7=56   8*8=64   
    9*1=9    9*2=18   9*3=27   9*4=36   9*5=45   9*6=54   9*7=63   9*8=72   9*9=81  
    

    利用break和continue语句控制循环

    在Shell中的循环结构中,还有2个语句非常有用,即break和continue语句。前者用于立即从循环中退出;而后者则用来跳过循环体中的某些语句,继续执行下一次循环。本节将详细介绍这2个语句的使用方法。

    利用break语句控制循环

    break语句的作用是立即跳出某个循环结构。break语句可以用在for、while或者until等循环语句的循环体中。
    【例20】输出5以内的乘法表

    #! /bin/bash
    for ((i=1;i<=9;i++))
    do
       for ((j=1;j<=i;j++))
       do
          let "product=i*j"
          printf "$i*$j=$product"
          if [[ "$product" -gt 9 ]]
          then
             printf "   "
          else
             printf "    "
          fi
       done
       echo
       #当变量i的值为5时,退出循环
       if [[ "$i" -eq 5 ]]
       then
          break;
       fi
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    1*1=1    
    2*1=2    2*2=4    
    3*1=3    3*2=6    3*3=9    
    4*1=4    4*2=8    4*3=12   4*4=16   
    5*1=5    5*2=10   5*3=15   5*4=20   5*5=25 
    

    【例21】本例将【例20】中的break语句移到了内层循环中

    #! /bin/bash
    for ((i=1;i<=9;i++))
    do
       for ((j=1;j<=i;j++))
    do
          let "product=i*j"
          printf "$i*$j=$product"
          if [[    "$product" -gt 9 ]]
          then
             printf "   "
          else
             printf "    "
          fi
          #退出循环
          if [[ "$j" -eq 5 ]]
          then
             break
          fi
       done
       echo
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    1*1=1    
    2*1=2    2*2=4    
    3*1=3    3*2=6    3*3=9    
    4*1=4    4*2=8    4*3=12   4*4=16   
    5*1=5    5*2=10   5*3=15   5*4=20   5*5=25   
    6*1=6    6*2=12   6*3=18   6*4=24   6*5=30   
    7*1=7    7*2=14   7*3=21   7*4=28   7*5=35   
    8*1=8    8*2=16   8*3=24   8*4=32   8*5=40   
    9*1=9    9*2=18   9*3=27   9*4=36   9*5=45  
    

    【例22】通过break语句跳出指定的层数

    #! /bin/bash
    for ((i=1;i<=9;i++))
    do
       for ((j=1;j<=i;j++))
       do
          let "product=i*j"
          printf "$i*$j=$product"
          if [[ "$product" -gt 9 ]]
          then
             printf "   "
          else
             printf "    "
          fi
          if [[ "$j" -eq 5 ]]
          then
             #增加参数2
             break 2
          fi
       done
       echo
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    1*1=1    
    2*1=2    2*2=4    
    3*1=3    3*2=6    3*3=9    
    4*1=4    4*2=8    4*3=12   4*4=16   
    5*1=5    5*2=10   5*3=15   5*4=20   5*5=25   [root@localhost ~]# 
    

    利用continue语句控制循环

    前面介绍的break语句是退出循环体。而continue语句则比较有趣,它的作用不是退出循环体。而是跳过当前循环体中该语句后面的语句,重新从循环语句开始的位置执行。
    【例23】演示continue语句的使用方法

    #! /bin/bash
    for var in {1..10}
    do
       #如果当前数字为奇数
       if [[ "$var%2" -eq 1 ]]
       then
          #跳过后面的语句
          continue
       fi
       echo "$var"
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    2
    4
    6
    8
    10
    

    分析break语句和continue语句的区别

    正如前面所讲,break语句和continue语句都可以位于各种循环体内,用于控制当前的循环流程。但是,break语句是直接退出当前的循环结构,转向执行循环体后面的语句;而continue语句则只是跳过当前循环体中continue语句后面的语句,转向当前循环体的起始位置,重新执行下一次循环,并没有退出当前的循环结构。这是这两者的最本质的区别。
    另外, 没有参数的break语句和continue语句都只是影响到本层的循环流程,如果想要影响多层循环,则可以附加数字参数。
    【例24】演示在双层嵌套循环中,break语句对于流程的影响

    #!/bin/sh
    #外层循环
    for i in a b c d
    do
       echo -n "$i "
       #内层循环
       for j in `seq 10`
       do
          if [ $j -eq 5 ];then
             break
          fi
          echo -n "$j "
       done
       echo
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    a 1 2 3 4 
    b 1 2 3 4 
    c 1 2 3 4 
    d 1 2 3 4
    

    【例25】演示通过break语句跳出指定的层数

    #!/bin/sh
    for i in a b c d
    do
       echo -n "$i "
       for j in `seq 10`
       do
          if [ $j -eq 5 ];then
             #指定跳出层数2
             break 2
          fi
          echo -n "$j "
       done
       echo
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    a 1 2 3 4 [root@localhost ~]# 
    

    【例26】演示通过break语句跳出指定的层数

    [root@foundation0 ~]# cat test.sh 
    #!/bin/bash 
    # break-levels.sh: 退出循环. 
    # "break N" 退出 N 层循环. 
    for outerloop in 1 2 3 4 5 
    do 
    	echo -n "Group $outerloop: " 
    # -------------------------------------------------------- 
    	for innerloop in 1 2 3 4 5 
    	do 
    	echo -n "$innerloop " 
    		if [ "$innerloop" -eq 3 ] 
    			then 
    			break  # 试试 break 2 来看看发生什么. 
    # (内部循环和外部循环都被退出了.) 
    		fi 
    	done 
    # -------------------------------------------------------- 
    	echo 
    done 
    echo 
    exit 0 
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    Group 1: 1 2 3 
    Group 2: 1 2 3 
    Group 3: 1 2 3 
    Group 4: 1 2 3 
    Group 5: 1 2 3 
    
    

    【例27】演示通过break语句跳出指定的层数

    [root@foundation0 ~]# cat test.sh 
    #!/bin/bash 
    # break-levels.sh: 退出循环. 
    # "break N" 退出 N 层循环. 
    for outerloop in 1 2 3 4 5 
    do 
    	echo -n "Group $outerloop: " 
    # -------------------------------------------------------- 
    	for innerloop in 1 2 3 4 5 
    	do 
    	echo -n "$innerloop " 
    		if [ "$innerloop" -eq 3 ] 
    			then 
    			break 2 # 试试 break 2 来看看发生什么. 
    # (内部循环和外部循环都被退出了.) 
    		fi 
    	done 
    # -------------------------------------------------------- 
    	echo 
    done 
    echo 
    exit 0 
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    Group 1: 1 2 3 
    
    continue语句

    continue 命令也可以带一个参数.一个不带参数的 continue 命令只去掉本次循环的剩余代码 .而 continue N 将会把 N 层循环剩余的代码都去掉,但是循环的次数不变.
    【例28】演示通过continue语句跳出指定的层数

    #!/bin/bash 
    # "continue N" 命令, 将让 N 层的循环全部被 continue. 
    for outer in I II III IV V # 外部循环
    do
    echo; echo -n "Group $outer: " 
    # -------------------------------------------------------------------- 
    for inner in 1 2 3 4 5 6 7 8 9 10 # 内部循环
    do
    if [ "$inner" -eq 7 ]
    then
    continue 2 # continue 2 层, 也就是到 outer 循环上. 
    fi
    echo -n "$inner " # 7 8 9 10 将不会被 echo 
    done
    # 如果在此处添加 echo 的话,当然也不会输出. 
    done
    echo; echo 
    exit 0
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    
    Group I:123456
    Group II:123456
    Group III:123456
    Group IV:123456
    Group V:123456
    
    

    【例29】通过continue语句跳过当前循环后面的语句

    #!/bin/bash
    for i in a b c d
    do
       echo -n "$i "
       for j in `seq 10`
       do
          if [ $j -eq 5 ];then
             #跳过后面的语句
             continue
          fi
          echo -n "$j "
       done
       echo
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    a1 2 3 4 6 7 8 9 10 
    b1 2 3 4 6 7 8 9 10 
    c1 2 3 4 6 7 8 9 10 
    d1 2 3 4 6 7 8 9 10 
    

    【例30】演示含有参数的continue语句的使用方法

    #!/bin/sh
    for i in a b c d
    do
       echo -n "$i "
       for j in `seq 10`
       do
          if [ $j -eq 5 ];then
             #使用含有数字参数的continue语句
             continue 2
          fi
          echo -n "$j "
       done
       echo
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    a1 2 3 4 b1 2 3 4 c1 2 3 4 d1 2 3 4 [root@localhost ~]#
    

    【例31】演示break和continue的区别:

    [root@foundation0 ~]# cat test.sh 
    #!/bin/bash 
    LIMIT=19 # 上限
    echo 
    echo "Printing Numbers 1 through 20 (but not 3 and 11)." 
    a=0 
    while [ $a -le "$LIMIT" ] 
    do 
    a=$(($a+1)) 
    if [ "$a" -eq 3 ] || [ "$a" -eq 11 ] # 除了 3 和 11. 
    then 
    continue # 跳过本次循环剩下的语句. 
    fi 
    echo -n "$a " # 在$a 等于 3 和 11 的时候,这句将不会执行. 
    done 
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    
    Printing Numbers 1 through 20 (but not 3 and 11).
    1 2 4 5 6 7 8 9 10 12 13 14 15 16 17 18 19 20 [root@localhost ~]# 
    

    【例32】演示break和continue的区别:

    # 练习: 
    # 为什么循环会打印出 20? 
    # echo; echo 
    # echo Printing Numbers 1 through 20, but something happens after 2. 
    #########################################################
    ### 
    # Same loop, but substituting 'break' for 'continue'. 
    # 同样的循环, 但是用'break'来代替'continue'. 
    # a=0 
    # while [ "$a" -le "$LIMIT" ] 
    # do 
    # a=$(($a+1)) 
    # if [ "$a" -gt 2 ] 
    # then 
    # break # 将会跳出整个循环. 
    # fi 
    # echo -n "$a " 
    # done 
    # echo; echo; echo 
    # exit 0 
     #break 命令可以带一个参数.一个不带参数的 break 循环只能退出最内层的循环,而 break N 
    #可以退出 N 层循环. 
    
    #---------------------------------------------------------------------------------------#
    #!/bin/bash 
    
    LIMIT=19 # 上限
    
    echo 
    echo "Printing Numbers 1 through 20 (but not 3 and 11)." 
    a=0
    while [ $a -le "$LIMIT" ] 
    do
    a=$(($a+1))
    if [ "$a" -eq 3 ] || [ "$a" -eq 11 ] # 除了 3 和 11. 
    then
    break # 跳过本次循环剩下的语句. 
    fi
    echo -n "$a " # 在$a 等于 3 和 11 的时候,这句将不会执行. 
    done
    

    运行以上程序

    [root@localhost ~]# ./test.sh
    
    Printing Numbers 1 through 20 (but not 3 and 11).
    1 2 [root@localhost ~]# 
    
    展开全文
  • 结构化方法和面向对象方法的比较

    千次阅读 2018-10-31 16:50:00
    结构化方法和面向对象方法的比较 翁松秀 北京航空航天大学  摘要:编程之精髓在于编程思想,而不同的编程方法有不同的编程思想。结构化程序设计方法一直以来都是编程人员基本的编程方法,而近年来流行的面向对象...

    结构化方法和面向对象方法的比较

    翁松秀

    北京航空航天大学

        摘要:编程之精髓在于编程思想,而不同的编程方法有不同的编程思想。结构化程序设计方法一直以来都是编程人员基本的编程方法,而近年来流行的面向对象方法也备受人们青睐。文章通过介绍编程世界的这两种主要编程方法,结构化方法和面向对象方法的分析、设计和应用。并在最后对两种方法进行比较分析,比较结构化方法和面向对象方法编程思想的异同和两种方法的应用。

    关键词:结构化方法;面向对象方法;编程思想;再抽象;再模块化;

       前言

      结构化方法和面向对象方法是当前软件开发最常用的方法。基于“瀑布模型”的软件开发方法中,以软件架构为核心,采用结构化设计和分析方法将软件开发的生命周期划分为制定计划、需求分析、软件设计、程序编写、软件测试和运行维护六个基本阶段。而在通过迭代式和增量式进行开发的敏捷软件开发方法中,主要采用面向对象的设计和分析方法。结构化方法与面向对象方法的使用,与具体的开发环境和开发方法有关,如传统软件工程中更适合用结构化方法,而敏捷软件开发方法中面向对象开发方法更受青睐。

      1   结构化方法

      结构化编程思想由艾慈格·迪杰斯特拉在1976年在《GOTO陈述有害论》中提出,1975年赖瑞·康斯坦丁和爱德华·尤登提出结构化设计,1978年汤姆·迪马克和爱德华·尤登等在1975年提出结构化分析,后来逐渐被完善,形成现在软件开发方法中主要采用的结构化方法。结构化方法从功能上对系统进行划分,将系统按功能划分成各个模块,将子模块编写成子程序,通过子程序之间的结构和层次之间的耦合来解决复杂的问题。结构化方法按软件周期可划分为结构化分析、结构化设计和结构化实现,这里主要讨论SA和SD。

      1.1 结构化分析(Structured Analysis)

      结构化分析是通过数据来对需求进行分析,功能模块之间通过数据进行联系,采用的建模技术有:数据流图DFD(Data Flow Diagram)、ERD(Entity Relation Diagram)和数据字典DD(Data Dictionary)等。数据在结构化分析方法中举足轻重,就相当于血管之于人体,系统的各个模块通过数据的传递进行沟通和联系,导致了功能模块的强耦合性,模块之间的耦合性降低了结构化程序的可重用性和可维护性。主要分析步骤如下:

      ①  分析当前的情况,做出反映当前物理模型的DFD;

      ②  推导出等价的逻辑模型的DFD;

      ③  设计新的逻辑系统,生成数据字典和基元描述;

      ④  建立人机接口,提出可供选择的目标系统物理模型的DFD;

      1.2 结构化设计(Structured Design)

      结构化设计阶段将分析阶段得到的目标系统物理模型的DFD表示的具体信息转化成程序结构的设计描述,过渡成软件结构。在这个过程中采用的建模技术有:系统结构图SSD(System Struct Diagram)。这个过程分两步完成,第一步是从分析得到的结果出发,构造一个设计方案,决定系统功能模块的结构。第二步进行详细设计,确定每个功能模块的内部控制结构和算法,最终产生每个功能模块的程序流程图。具体步骤如下:

      ①   评审和细化数据流图;

      ②   确定数据流图的类型;

      ③   把数据流图映射到软件模块结构;

      ④   基于数据流图逐层分解高层模块,设计中下层模块;

      ⑤   对模块的控制结构和算法进行优化;

      ⑥   描述模块接口;

      1.3 结构化方法的应用

      结构化方法对需求变化的适应能力比较弱,功能的改变甚至会牵一发而动全身,但由于结构化分析和设计对系统进行完整的架构和具体功能的分析以及实现的各个阶段严谨的测试,所以结构化程序可靠性高。基于结构化分析、设计和实现中的这些主要优点和缺点,结构化比较适合于像操作系统、实时处理系统等这样以功能为主的系统。比如绝大多数操作系统都是以结构化语言C语言写的。

      2   面向对象方法

      面向对象OO(Object Oriented)方法把面向对象的思想应用于软件开发,将系统中的数据和相关操作进行分类和封装,抽象成一种新的数据结构——类,并在系统的实现过程中实例化,实例化的对象与客观实体有直接对应关系,通过对象的属性和行为,以及对象之间的关系来解决实际问题。面向对象方法吸收了结构化的基本思想和优点,在数据抽象的基础上添加对操作的抽象,使面向对象程序具有高重用性。面向对象方法其实就是结构化的再抽象和再模块化。

      2.1 面向对象分析(Object Oriented Analysis)

      面向对象分析阶段通过分析,确定问题空间和解空间中所有对象及其属性,进而确定每个对象的操作,即对事务的处理能力,然后通过对象之间的行为来确定对象之间的关系。具体步骤如下:

      ①   确定问题域和解空间;

      ②   区分类和对象;

      ③   区分整体对象以及组成部分,确定类的关系以及内部结构;

      ④   定义类的数据(属性)和操作(处理事务的行为);

      ⑤   确定附加的系统约束

      2.2 面向对象设计(Object Oriented Design)

      面向对象设计阶段主要对分析阶段的结果进行进一步的规范化整理,以便后面的具体实现。OOA阶段和OOD阶段没有明显的划分界限,两者相互衔接,最能体现出这种关系的是近年来出现的新的软件生命周期模型——喷泉模型。如图(1)

                                                                                               图(1)

      面向对象设计阶段的主要步骤如下:

      ①   改进和完善分析阶段对系统的分析结果;

      ②   设计交互过程和用户接口;

      ③   设计任务管理、子系统以及任务之间的协调方式;

      ④   设计全局资源,确定任务或者子系统的资源分配;

      ⑤   设计对象;

      2.3 面向对象方法的应用

      面向对象方法从问题的模型开始,模拟人对事物的认识对事物进行分类,进而识别对象,通过整理对象的内部结构和对象之间的关系不断细化问题,它的本质就是敏捷开发方法中的迭代和递增。开发过程通过不断地对功能进行反复的迭代,使系统的功能不断完善。所以面向对象方法适用于SCRUM、XP(极限编程)、CRYSTAL(水晶编程)、PDD(特性驱动开发)等敏捷软件开发。

      3   结构化方法和面向对象方法的比较

      3.1 编程思想的比较

      软件开发的过程就是人们使用各种计算机语言将现实世界的问题翻译到计算机世界,可描述为图(2):

    图(2)

      结构化方法的编程思想是自上而下,逐步求精,按系统的功能进行模块化设计,将一个复杂和完整的系统按功能分解成小的模块,模块内由顺序、分支和循环等基本控制结构组成,各模块的功能由子程序进行实现。如图(3)

    图(3)

      而面向对象方法通过模拟人类日常的逻辑思维中经常采用的思想方法和原则,将系统中的事物进行抽象、分类、继承和封装。通过抽象出来的类之间的行为关系来确定整个系统的联系,并在系统中实例化需要的对象,在系统的设计中尽可能地运用人类的思维方式。与结构化相比面向对象更符合人们对事物和解决问题的过程和思维方法。如图(4)

    图(4)

      3.2 应用的比较分析

      结构化开发方法开发前期对系统的功能需求和非功能需求进行完整的架构,使软件具有高可靠性。开发过程自上而下,按照严谨的软件开发生命周期:计划、分析、设计、编码、测试和维护逐个阶段进行开发,各个阶段相互衔接,每一个阶段都要通过严格的审查,测试通过才能进入下一个阶段,保证了软件的高质量。所以比较适合于像操作系统、实时处理系统等这样以功能为主的系统。而由于面向对象方法迭代和递增的本质,所以面向对象方法更适合于在敏捷软件开发中使用。如SCRUM、XP(极限编程)、CRYSTAL(水晶编程)、PDD(特性驱动开发)等。

    参考文献:

    [1]雷西玲,谢天保,何文娟. 面向对象方法与结构化方法的比较. 2001

    [2]邵维忠,杨芙清. 面向对象的系统分析. 1998

    [3]蔡希尧. 面向对象技术. 2001

    [4]翁松秀. 敏捷软件开发与传统软件工程概述比较. 2016

    [5]张莉,裘国永. 结构化方法与面向对象方法的比较分析. 陕西师范大学学报. 2001

     

    https://www.cnblogs.com/maya389069192/p/6166238.html

    展开全文
  • 四、数据结构对象和数组 原文:Data Structures: Objects and Arrays 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了《JavaScript 编程精解(第 2 版)》 On two occasions I...

    四、数据结构:对象和数组

    原文:Data Structures: Objects and Arrays

    译者:飞龙

    协议:CC BY-NC-SA 4.0

    自豪地采用谷歌翻译

    部分参考了《JavaScript 编程精解(第 2 版)》

    On two occasions I have been asked, ‘Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?’ […] I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question.

    Charles Babbage,《Passages from the Life of a Philosopher》(1864)

    数字,布尔和字符串是构建数据结构的原子。 不过,许多类型的信息都需要多个原子。 对象允许我们将值(包括其他对象)放到一起,来构建更复杂的结构。

    我们迄今为止构建的程序,受到一个事实的限制,它们仅在简单数据类型上运行。 本章将介绍基本的数据结构。 到最后,你会知道足够多的东西,开始编写有用的程序。

    本章将着手于一个或多或少的实际编程示例,当概念适用于手头问题时引入它们。 示例代码通常基于本文前面介绍的函数和绑定。

    松鼠人

    一般在晚上八点到十点之间,雅克就会变身成为一只毛茸茸的松鼠,尾巴上的毛十分浓密。

    一方面,雅克非常高兴他没有变成经典的狼人。 与变成狼相比,变成松鼠的确会产生更少的问题。 他不必担心偶然吃掉邻居(那会很尴尬),而是担心被邻居的猫吃掉。 他在橡木树冠上的一个薄薄的树枝上醒来,赤身裸体并迷失方向。在这两次偶然之后,他在晚上锁上了房间的门窗,并在地板上放了几个核桃,来使自己忙起来。

    这就解决了猫和树的问题。 但雅克宁愿完全摆脱他的状况。 不规律发生的变身使他怀疑,它们可能会由某种东西触发。 有一段时间,他相信只有在他靠近橡树的日子里才会发生。 但是避开橡树不能阻止这个问题。

    雅克切换到了更科学的方法,开始每天记录他在某一天所做的每件事,以及他是否变身。 有了这些数据,他希望能够缩小触发变身的条件。

    他需要的第一个东西,是存储这些信息的数据结构。

    数据集

    为了处理大量的数字数据,我们首先必须找到一种方法,将其在我们的机器内存中表示。 举例来说,我们想要表示一组数字 2, 3, 5, 7 和 11。

    我们可以用字符串来创建 - 毕竟,字符串可以有任意长度,所以我们可以把大量数据放入它们中,并使用"2 3 5 7 11"作为我们的表示。 但这很笨拙。 你必须以某种方式提取数字,并将它们转换回数字才能访问它们。

    幸运的是,JavaScript提供了一种数据类型,专门用于存储一系列的值。我们将这种数据类型称为数组,将一连串的值写在方括号当中,值之间使用逗号(,)分隔。

    let listOfNumbers = [2, 3, 5, 7, 11];
    console.log(listOfNumbers[2]);
    // → 5
    console.log(listOfNumbers[0]);
    // → 2
    console.log(listOfNumbers[2 - 1]);
    // → 3

    我们同样使用方括号来获取数组当中的值。在表达式后紧跟一对方括号,并在方括号中填写表达式,这将会在左侧表达式里查找方括号中给定的索引所对应的值,并返回结果。

    数组的第一个索引是零,而不是一。 所以第一个元素用listOfNumbers[0]获取。 基于零的计数在技术上有着悠久的传统,并且在某些方面意义很大,但需要一些时间来习惯。 将索引看作要跳过的项目数量,从数组的开头计数。

    属性

    在之前的章节中,我们已经看到了一些可疑的表达式,例如myString.length(获取字符串的长度)和Math.max(最大值函数)。 这些表达式可以访问某个值的属性。 在第一个中,我们访问myString中的length属性。 第二个中,我们访问Math对象(它是数学相关常量和函数的集合)中的名为max的属性。

    在 JavaScript 中,几乎所有的值都有属性。但nullundefined没有。如果你尝试访问nullundefined的属性,会得到一个错误提示。

    null.length;
    // → TypeError: null has no properties

    在JavaScript中访问属性的两种主要方式是点(.)和方括号([])。 value.xvalue [x]都可以访问value属性,但不一定是同一个属性。 区别在于如何解释x。 使用点时,点后面的单词是该属性的字面名称。 使用方括号时,会求解括号内的表达式来获取属性名称。 鉴于value.x获取value的名为x的属性,value [x]尝试求解表达式x,并将结果转换为字符串作为属性名称。

    所以如果你知道你感兴趣的属性叫做color,那么你会写value.color。 如果你想提取属性由绑定i中保存的值命名,你可以写value [i]。 属性名称是字符串。 它们可以是任何字符串,但点符号仅适用于看起来像有效绑定名的名称。 所以如果你想访问名为2John Doe的属性,你必须使用方括号:value[2]value["John Doe"]

    数组中的元素以数组属性的形式存储,使用数字作为属性名称。 因为你不能用点号来表示数字,并且通常想要使用一个保存索引的绑定,所以你必须使用括号来表达它们。

    数组的length属性告诉我们它有多少个元素。 这个属性名是一个有效的绑定名,我们事先知道它的名字,所以为了得到一个数组的长度,通常写array.length,因为它比array["length"]更容易编写。

    方法

    length属性之外,字符串和数组对象都包含一些持有函数值的属性。

    let doh = "Doh";
    console.log(typeof doh.toUpperCase);
    // → function
    console.log(doh.toUpperCase());
    // → DOH

    每个字符串都有toUpperCase属性。 调用时,它将返回所有字母转换为大写字符串的副本。 另外还有toLowerCase

    有趣的是,虽然我们没有在调用toUpperCase时传递任何参数,但该函数访问了字符串"Doh",即被调用的属性所属的值。我们会在第 6 章中阐述这其中的原理。

    我们通常将包含函数的属性称为某个值的方法。比如说,toUpperCase是字符串的一个方法。

    此示例演示了两种方法,可用于操作数组:

    let sequence = [1, 2, 3];
    sequence.push(4);
    sequence.push(5);
    console.log(sequence);
    // → [1, 2, 3, 4, 5]
    console.log(sequence.pop());
    // → 5
    console.log(sequence);
    // → [1, 2, 3, 4]

    push方法将值添加到数组的末尾,而pop方法则相反,删除数组中的最后一个值并将其返回。

    这些有点愚蠢的名字是栈的传统术语。 编程中的栈是一种数据结构,它允许你将值推入并按相反顺序再次弹出,最后添加的内容首先被移除。 这些在编程中很常见 - 你可能还记得前一章中的函数调用栈,它是同一个想法的实例。

    对象

    回到松鼠人的示例。 一组每日的日志条目可以表示为一个数组。 但是这些条目并不仅仅由一个数字或一个字符串组成 - 每个条目需要存储一系列活动和一个布尔值,表明雅克是否变成了松鼠。 理想情况下,我们希望将它们组合成一个值,然后将这些分组的值放入日志条目的数组中。

    对象类型的值是任意的属性集合。 创建对象的一种方法是使用大括号作为表达式。

    let day1 = {
      squirrel: false,
      events: ["work", "touched tree", "pizza", "running"]
    };
    console.log(day1.squirrel);
    // → false
    console.log(day1.wolf);
    // → undefined
    day1.wolf = false;
    console.log(day1.wolf);
    // → false

    大括号内有一列用逗号分隔的属性。 每个属性都有一个名字,后跟一个冒号和一个值。 当一个对象写为多行时,像这个例子那样,对它进行缩进有助于提高可读性。 名称不是有效绑定名称或有效数字的属性必须加引号。

    let descriptions = {
      work: "Went to work",
      "touched tree": "Touched a tree"
    };

    这意味着大括号在 JavaScript 中有两个含义。 在语句的开头,他们起始了一个语句块。 在任何其他位置,他们描述一个对象。 幸运的是,语句很少以花括号对象开始,因此这两者之间的不明确性并不是什么大问题。

    读取一个不存在的属性就会产生undefined

    我们可以使用=运算符来给一个属性表达式赋值。如果该属性已经存在,那么这项操作就会替换原有的值。如果该属性不存在,则会在目标对象中新建一个属性。

    简要回顾我们的绑定的触手模型 - 属性绑定也类似。 他们捕获值,但其他绑定和属性可能会持有这些相同的值。 你可以将对象想象成有任意数量触手的章鱼,每个触手上都有一个名字的纹身。

    delete运算符切断章鱼的触手。 这是一个一元运算符,当应用于对象属性时,将从对象中删除指定的属性。 这不是一件常见的事情,但它是可能的。

    let anObject = {left: 1, right: 2};
    console.log(anObject.left);
    // → 1
    delete anObject.left;
    console.log(anObject.left);
    // → undefined
    console.log("left" in anObject);
    // → false
    console.log("right" in anObject);
    // → true

    当应用于字符串和对象时,二元in运算符会告诉你该对象是否具有名称为它的属性。 将属性设置为undefined,和实际删除它的区别在于,在第一种情况下,对象仍然具有属性(它只是没有有意义的值),而在第二种情况下属性不再存在,in会返回false

    为了找出对象具有的属性,可以使用Object.keys函数。 你给它一个对象,它返回一个字符串数组 - 对象的属性名称。

    console.log(Object.keys({x: 0, y: 0, z: 2}));
    // → ["x", "y", "z"]

    Object.assign函数可以将一个对象的所有属性复制到另一个对象中。

    let objectA = {a: 1, b: 2};
    bject.assign(objectA, {b: 3, c: 4});
    console.log(objectA);
    // → {a: 1, b: 3, c: 4}

    然后,数组只是一种对象,专门用于存储对象序列。 如果你求解typeof [],它会产生object。 你可以看到它们是长而平坦的章鱼,它们的触手整齐排列,并以数字标记。

    我们将雅克的日记表示为对象数组。

    let journal = [
      {events: ["work", "touched tree", "pizza",
                "running", "television"],
       squirrel: false},
      {events: ["work", "ice cream", "cauliflower",
                "lasagna", "touched tree", "brushed teeth"],
       squirrel: false},
      {events: ["weekend", "cycling", "break", "peanuts",
                "beer"],
       squirrel: true},
      /* and so on... */
    ];

    可变性

    我们现在即将开始真正的编程。 首先还有一个理论要理解。

    我们看到对象值可以修改。 千米你的章节讨论的值的类型(如数字,字符串和布尔值)都是不可变的 – 这些类型的值不可能修改。 你可以将它们组合起来并从它们派生新的值,但是当你采用特定的字符串值时,该值将始终保持不变。 里面的文字不能改变。 如果你有一个包含"cat"的字符串,其他代码不可能修改你的字符串中的一个字符,来使它变成"rat"

    对象的工作方式不同。你可以更改其属性,使单个对象值在不同时间具有不同的内容。

    当我们有两个数字,120 和 120 时,我们可以将它们看作完全相同的数字,不管它们是否指向相同的物理位。 使用对象时,拥有同一个对象的两个引用,和拥有包含相同属性的两个不同的对象,是有区别的。 考虑下面的代码:

    let object1 = {value: 10};
    let object2 = object1;
    let object3 = {value: 10};
    
    console.log(object1 == object2);
    // → true
    console.log(object1 == object3);
    // → false
    
    object1.value = 15;
    console.log(object2.value);
    // → 15
    console.log(object3.value);
    // → 10

    object1object2绑定持有相同对象,这就是为什么改变object1会改变object2的值。 据说他们具有相同的身份。 绑定object3指向一个不同的对象,它最初包含的属性与object1相同,但过着单独的生活。

    绑定可以是可变的或不变的,但这与它们的值的行为方式是分开的。 即使数值不变,你也可以使用let绑定来跟踪一个变化的数字,通过修改绑定所指向的值。与之类似,虽然对象的const绑定本身不可改变,并且始终指向相同对象,该对象的内容可能会改变。

    const score = {visitors: 0, home: 0};
    // This is okay
    score.visitors = 1;
    // This isn't allowed
    score = {visitors: 1, home: 1};

    当你用 JavaScript 的==运算符比较对象时,它按照身份进行比较:仅当两个对象的值严格相同时才产生true。 比较不同的对象会返回false,即使它们属性相同。 JavaScript 中没有内置的“深层”比较操作,它按照内容比较对象,但可以自己编写它(这是本章末尾的一个练习)。

    松鼠人的记录

    于是,雅克开始了他的 JavaScript 之旅,并搭建了用于保存每天记录的一套开发环境。

    let journal = [];
    
    function addEntry(events, squirrel) {
      journal.push({events, squirrel});
    }

    请注意添加到日记中的对象看起来有点奇怪。 它不像events:events那样声明属性,只是提供属性名称。 这是一个简写,意思一样 - 如果大括号中的属性名后面没有值,它的值来自相同名称的绑定。

    那么,在每天晚上十点 – 或者有时候是下一天的早晨,从它的书架顶部爬下来之后 – 雅克记录了这一天。

    addEntry(["work", "touched tree", "pizza", "running",
              "television"], false);
    addEntry(["work", "ice cream", "cauliflower", "lasagna",
              "touched tree", "brushed teeth"], false);
    addEntry(["weekend", "cycling", "break", "peanuts",
              "beer"], true);

    一旦他有了足够的数据点,他打算使用统计学来找出哪些事件可能与变成松鼠有关。

    关联性是统计绑定之间的独立性的度量。 统计绑定与编程绑定不完全相同。 在统计学中,你通常会有一组度量,并且每个绑定都根据每个度量来测量。 绑定之间的相关性通常表示为从 -1 到 1 的值。 相关性为零意味着绑定不相关。 相关性为一表明两者完全相关 - 如果你知道一个,你也知道另一个。 负一意味着它们是完全相关的,但它们是相反的 - 当一个是真的时,另一个是假的。

    为了计算两个布尔绑定之间的相关性度量,我们可以使用 phi 系数(ϕ)。 这是一个公式,输入为一个频率表格,包含观测绑定的不同组合的次数。 公式的输出是 -1 和 1 之间的数字。

    我们可以将吃比萨的事件放在这样的频率表中,每个数字表示我们的度量中的组合的出现次数。

    如果我们将那个表格称为n,我们可以用下列公式自己算ϕ

    (如果你现在把这本书放下,专注于十年级数学课的可怕的再现,坚持住!我不打算用无休止的神秘符号折磨你 - 现在只有这一个公式。我们所做的就是把它变成 JavaScript。)

    符号n01表明, 第一个绑定(松鼠)为假(0)时,第二个绑定(披萨)为真(1)。 在披萨表中,n01是 9。

    n1表示所有度量之和,其中第一个绑定为true,在示例表中为 5。 同样,n0表示所有度量之和,其中第二个绑定为假。

    因此,我们以比萨表为例,除法线上方的部分(被除数)为1×76–9×4=40,而除法线下面的部分(除数)则是10×80×5×85的平方根,也就是√340000。计算结果为ϕ≈0.069,这个结果很小,因此吃比萨对是否变身成松鼠显然没有太大影响。

    计算关联性

    我们可以用包含 4 个元素的数组([76,9,4,1])来表示一张 2 乘 2 的表格。我们也可以使用其他表示方式,比如包含两个数组的数组,每个子数组又包含两个元素([[76,9],[4,1]])。也可以使用一个对象,它包含一些属性,名为"11""01"。但是,一维数组更为简单,也容易进行操作。我们可以将数组索引看成包含两个二进制位的数字,左边的(高位)数字表示绑定“是否变成松鼠”,右边的(低位)数字表示事件绑定。例如,若二进制数字为 10,表示雅克变成了松鼠,但事件并未发生(比如说吃比萨)。这种情况发生了 4 次。由于二进制数字 10 的十进制是 2,因此我们将其存储到数组中索引为 2 的位置上。

    下面这个函数用于计算数组的系数ϕ

    function phi(table) {
      return (table[3] * table[0] - table[2] * table[1]) /
        Math.sqrt((table[2] + table[3]) *
                  (table[0] + table[1]) *
                  (table[1] + table[3]) *
                  (table[0] + table[2]));
    }
    
    console.log(phi([76, 9, 4, 1]));
    // → 0.068599434

    这将ϕ公式直接翻译成 JavaScript。 Math.sqrt是平方根函数,由标准 JavaScript 环境中的Math对象提供。 我们必须在表格中添加两个字段来获取字段,例如n1因为行和或者列和不直接存储在我们的数据结构中。

    雅克花了三个月的时间记录日志。在本章的代码沙箱(http://eloquentjavascript.net/code/)的下载文件中,用JOURNAL绑定存储了该结果数据集合。

    若要从这篇记录中提取出某个特定事件的 2 乘 2 表格,我们首先需要循环遍历整个记录,并计算出与变身成松鼠相关事件发生的次数。

    function hasEvent(event, entry) {
      return entry.events.indexOf(event) != -1;
    }
    
    function tableFor(event, journal) {
      let table = [0, 0, 0, 0];
      for (let i = 0; i < journal.length; i++) {
        let entry = journal[i], index = 0;
        if (entry.events.includes(event)) index += 1;
        if (entry.squirrel) index += 2;
        table[index] += 1;
      }
      return table;
    }
    
    console.log(tableFor("pizza", JOURNAL));
    // → [76, 9, 4, 1]

    数组拥有includes方法,检查给定值是否存在于数组中。 该函数使用它来确定,对于某一天,感兴趣的事件名称是否在事件列表中。

    tableFor中的循环体通过检查列表是否包含它感兴趣的特定事件,以及该事件是否与松鼠事件一起发生,来计算每个日记条目在表格中的哪个盒子。 然后循环对表中的正确盒子加一。

    我们现在有了我们计算个体相关性的所需工具。 剩下的唯一一步,就是为记录的每种类型的事件找到关联,看看是否有什么明显之处。

    数组循环

    tableFor函数中,有一个这样的循环:

    for (let i = 0; i < JOURNAL.length; i++) {
      let entry = JOURNAL[i];
      // Do something with entry
    }

    这种循环在经典的 JavaScript 中很常见 - 遍历数组,一次一个元素会很常见,为此,你需要在数组长度上维护一个计数器,并依次选取每个元素。

    在现代 JavaScript 中有一个更简单的方法来编写这样的循环。

    for (let entry of JOURNAL) {
      console.log(`${entry.events.length} events.`);
    }

    for循环看起来像这样,在绑定定义之后用of这个词时,它会遍历of之后的给定值的元素。 这不仅适用于数组,而且适用于字符串和其他数据结构。 我们将在第 6 章中讨论它的工作原理。

    分析结果

    我们需要计算数据集中发生的每种类型事件的相关性。 为此,我们首先需要寻找每种类型的事件。

    function journalEvents(journal) {
      let events = [];
      for (let entry of journal) {
        for (let event of entry.events) {
          if (!events.includes(event)) {
            events.push(event);
          }
        }
      }
      return events;
    }
    
    console.log(journalEvents(JOURNAL));
    // → ["carrot", "exercise", "weekend", "bread", …]

    通过遍历所有事件,并将那些不在里面的事件添加到events数组中,该函数收集每种事件。

    使用它,我们可以看到所有的相关性。

    for (let event of journalEvents(JOURNAL)) {
      console.log(event + ":", phi(tableFor(event, JOURNAL)));
    }
    // → carrot:   0.0140970969
    // → exercise: 0.0685994341
    // → weekend:  0.1371988681
    // → bread:   -0.0757554019
    // → pudding: -0.0648203724
    // and so on...

    绝大多数相关系数都趋近于 0。显然,摄入胡萝卜、面包或布丁并不会导致变身成松鼠。但是似乎在周末变身成松鼠的概率更高。让我们过滤结果,来仅仅显示大于 0.1 或小于 -0.1 的相关性。

    for (let event of journalEvents(JOURNAL)) {
      let correlation = phi(tableFor(event, JOURNAL));
      if (correlation > 0.1 || correlation < -0.1) {
        console.log(event + ":", correlation);
      }
    }
    // → weekend:        0.1371988681
    // → brushed teeth: -0.3805211953
    // → candy:          0.1296407447
    // → work:          -0.1371988681
    // → spaghetti:      0.2425356250
    // → reading:        0.1106828054
    // → peanuts:        0.5902679812

    啊哈!这里有两个因素,其相关性明显强于其他因素。 吃花生对变成松鼠的几率有强烈的积极影响,而刷牙有显着的负面影响。

    这太有意思了。让我们再仔细看看这些数据。

    for (let entry of JOURNAL) {
      if (entry.events.includes("peanuts") &&
         !entry.events.includes("brushed teeth")) {
         entry.events.push("peanut teeth");
      }
    }
    console.log(phi(tableFor("peanut teeth", JOURNAL)));
    // → 1

    这是一个强有力的结果。 这种现象正好发生在雅克吃花生并且没有刷牙时。 如果他只是不注意口腔卫生,他从来没有注意到他的病痛。

    知道这些之后,雅克完全停止吃花生,发现他的变身消失了。

    几年来,雅克过得越来越好。 但是在某个时候他失去了工作。 因为他生活在一个糟糕的国家,没有工作就意味着没有医疗服务,所以他被迫在一个马戏团就业,在那里他扮演的是不可思议的松鼠人,在每场演出前都用花生酱塞满了它的嘴。

    数组详解

    在完成本章之前,我想向你介绍几个对象相关的概念。 我将首先介绍一些通常实用的数组方法。

    我们在本章的前面已经了解了pushpop方法,分别用于在数组末尾添加或删除元素。相应地,在数组的开头添加或删除元素的方法分别是unshiftshift

    let todoList = [];
    function remember(task) {
      todoList.push(task);
    }
    function getTask() {
      return todoList.shift();
    }
    function rememberUrgently(task) {
      todoList.unshift(task);
    }

    这个程序管理任务队列。 你通过调用remember("groceries"),将任务添加到队列的末尾,并且当你准备好执行某些操作时,可以调用getTask()从队列中获取(并删除)第一个项目。 rememberUrgently函数也添加任务,但将其添加到队列的前面而不是队列的后面。

    有一个与indexOf方法类似的方法,叫lastIndexOf,只不过indexOf从数组第一个元素向后搜索,而lastIndexOf从最后一个元素向前搜索。

    console.log([1, 2, 3, 2, 1].indexOf(2));
    // → 1
    console.log([1, 2, 3, 2, 1].lastIndexOf(2));
    // → 3

    indexOflastIndexOf方法都有一个可选参数,可以用来指定搜索的起始位置。

    另一个基本方法是slice,该方法接受一个起始索引和一个结束索引,然后返回数组中两个索引范围内的元素。起始索引元素包含在返回结果中,但结束索引元素不会包含在返回结果中。

    console.log([0, 1, 2, 3, 4].slice(2, 4));
    // → [2, 3]
    console.log([0, 1, 2, 3, 4].slice(2));
    // → [2, 3, 4]

    如果没有指定结束索引,slice会返回从起始位置之后的所有元素。你也可以省略起始索引来复制整个数组。

    concat方法可用于将数组粘在一起,来创建一个新数组,类似于+运算符对字符串所做的操作。

    以下示例展示了concatslice的作用。 它接受一个数组和一个索引,然后它返回一个新数组,该数组是原数组的副本,并且删除了给定索引处的元素:

    function remove(array, index) {
      return array.slice(0, index)
        .concat(array.slice(index + 1));
    }
    console.log(remove(["a", "b", "c", "d", "e"], 2));
    // → ["a", "b", "d", "e"]

    如果你将concat传递给一个不是数组的参数,该值将被添加到新数组中,就像它是单个元素的数组一样。

    字符串及其属性

    我们可以调用字符串的lengthtoUpperCase这样的属性,但不能向字符串中添加任何新的属性。

    let kim = "Kim";
    kim.age = 88;
    console.log(kim.age);
    // → undefined

    字符串、数字和布尔类型的值并不是对象,因此当你向这些值中添加属性时 JavaScript 并不会报错,但实际上你并没有将这些属性添加进去。前面说过,这些值是不变的,不能改变。

    但这些类型包含一些内置属性。每个字符串中包含了若干方法供我们使用,最有用的方法可能就是sliceindexOf了,它们的功能与数组中的同名方法类似。

    console.log("coconuts".slice(4, 7));
    // → nut
    console.log("coconut".indexOf("u"));
    // → 5

    一个区别是,字符串的indexOf可以搜索包含多个字符的字符串,而相应的数组方法仅查找单个元素。

    console.log("one two three".indexOf("ee"));
    // → 11

    trim方法用于删除字符串中开头和结尾的空白符号(空格、换行符和制表符等符号)。

    console.log("  okay \n ".trim());
    // → okay

    上一章中的zeroPad函数也作为方法存在。 它被称为padStart,接受所需的长度和填充字符作为参数。

    console.log(String(6).padStart(3, "0"));
    // → 006

    你可以使用split,在另一个字符串的每个出现位置分割一个字符串,然后再用join把它连接在一起。

    let sentence = "Secretarybirds specialize in stomping";
    let words = sentence.split(" ");
    console.log(words);
    // → ["Secretarybirds", "specialize", "in", "stomping"]
    console.log(words.join(". "));
    // → Secretarybirds. specialize. in. stomping

    可以用repeat方法重复一个字符串,该方法创建一个新字符串,包含原始字符串的多个副本,并将其粘在一起。

    console.log("LA".repeat(3));
    // → LALALA

    我们已经看到了字符串类型的length属性。 访问字符串中的单个字符,看起来像访问数组元素(有一个警告,我们将在第 5 章中讨论)。

    let string = "abc";
    console.log(string.length);
    // → 3
    console.log(string[1]);
    // → b

    剩余参数

    一个函数可以接受任意数量的参数。 例如,Math.max计算提供给它的参数的最大值。

    为了编写这样一个函数,你需要在函数的最后一个参数之前放三个点,如下所示:

    function max(...numbers) {
      let result = -Infinity;
      for (let number of numbers) {
        if (number > result) result = number;
      }
      return result;
    }
    console.log(max(4, 1, 9, -2));
    // → 9

    当这样的函数被调用时,剩余参数绑定一个数组,包含所有其它参数。 如果之前有其他参数,它们的值不是该数组的一部分。 当它是唯一的参数时,如max中那样,它将保存所有参数。

    你可以使用类似的三点表示法,来使用参数数组调用函数。

    let numbers = [5, 1, 7];
    console.log(max(...numbers));
    // → 7

    这在函数调用中“展开”数组,并将其元素传递为单独的参数。 像`max(9, …numbers, 2)’那样,可以包含像这样的数组以及其他参数。

    方括号的数组表示法,同样允许三点运算符将另一个数组展开到新数组中:

    let words = ["never", "fully"];
    console.log(["will", ...words, "understand"]);
    // → ["will", "never", "fully", "understand"]

    Math对象

    正如我们所看到的那样,Math对象中包含了许多与数字相关的工具函数,比如Math.max(求最大值)、Math.min(求最小值)和Math.sqrt(求平方根)。

    Math对象被用作一个容器来分组一堆相关的功能。 只有一个Math对象,它作为一个值几乎没有用处。 相反,它提供了一个命名空间,使所有这些函数和值不必是全局绑定。

    过多的全局绑定会“污染”命名空间。全局绑定越多,就越有可能一不小心把某些绑定的值覆盖掉。比如,我们可能想在程序中使用名为max的绑定,由于 JavaScript 将内置的max函数安全地放置在Math对象中,因此不必担心max的值会被覆盖。

    当你去定义一个已经被使用的绑定名的时候,对于很多编程语言来说,都会阻止你这么做,至少会对这种行为发出警告。但是 JavaScript 不会,因此要小心这些陷阱。

    让我们来继续了解Math对象。如果需要做三角运算,Math对象可以帮助到你,它包含cos(余弦)、sin(正弦)、tan(正切)和各自的反函数(acosasinatan)。Math.PI则表示数字π,或至少是 JavaScript 中的数字近似值。在传统的程序设计当中,常量均以大写来标注。

    function randomPointOnCircle(radius) {
      let angle = Math.random() * 2 * Math.PI;
      return {x: radius * Math.cos(angle),
              y: radius * Math.sin(angle)};
    }
    console.log(randomPointOnCircle(2));
    // → {x: 0.3667, y: 1.966}

    如果你对正弦或余弦不大熟悉,不必担心。我们会在第 13 章用到它们时,再做进一步解释。

    在上面的示例代码中使用了Math.random。每次调用该函数时,会返回一个伪随机数,范围在 0(包括)到 1(不包括)之间。

    console.log(Math.random());
    // → 0.36993729369714856
    console.log(Math.random());
    // → 0.727367032552138
    console.log(Math.random());
    // → 0.40180766698904335

    虽然计算机是确定性的机器,但如果给定相同的输入,它们总是以相同的方式作出反应 - 让它们产生随机显示的数字是可能的。 为此,机器会维护一些隐藏的值,并且每当你请求一个新的随机数时,它都会对该隐藏值执行复杂的计算来创建一个新值。 它存储一个新值并返回从中派生的一些数字。 这样,它可以以随机的方式产生新的,难以预测的数字。

    如果我们想获取一个随机的整数而非小数,可以使用Math.floor(向下取整到与当前数字最接近的整数)来处理Math.random的结果。

    console.log(Math.floor(Math.random() * 10));
    // → 2

    将随机数乘以 10 可以得到一个在 0 到 10 之间的数字。由于Math.floor是向下取整,因此该函数会等概率地取到 0 到 9 中的任何一个数字。

    还有两个函数,分别是Math.ceil(向上取整)和Math.round(四舍五入)。以及Math.abs,它取数字的绝对值,这意味着它反转了负值,但保留了正值。

    解构

    让我们暂时回顾phi函数:

    function phi(table) {
      return (table[3] * table[0] - table[2] * table[1]) /
        Math.sqrt((table[2] + table[3]) *
                  (table[0] + table[1]) *
                  (table[1] + table[3]) *
                  (table[0] + table[2]));
    }

    这个函数难以阅读的原因之一,是我们有一个指向数组的绑定,但我们更愿意拥有数组的元素的绑定,即let n00 = table [0]以及其他。 幸运的是,有一种简洁的方法可以在 JavaScript 中执行此操作。

    function phi([n00, n01, n10, n11]) {
      return (n11 * n00 - n10 * n01) /
        Math.sqrt((n10 + n11) * (n00 + n01) *
                  (n01 + n11) * (n00 + n10));
    }

    这也适用于由letvarconst创建的绑定。 如果你知道要绑定的值是一个数组,则可以使用方括号来“向内查看”该值,并绑定其内容。

    类似的技巧适用于对象,使用大括号代替方括号。

    let {name} = {name: "Faraji", age: 23};
    console.log(name);
    // → Faraji

    请注意,如果尝试解构nullundefined,则会出现错误,就像直接尝试访问这些值的属性一样。

    JSON

    因为属性只是捕获了它们的值,而不是包含它们,对象和数组在计算机的内存中储存为字节序列,存放它们的内容的地址(内存中的位置)。 因此,包含另一个数组的数组,(至少)由两个内存区域组成,一个用于内部数组,另一个用于外部数组,(除了其它东西之外)其中包含表示内部数组位置的二进制数。

    如果你想稍后将数据保存到文件中,或者通过网络将其发送到另一台计算机,则必须以某种方式,将这些内存地址的线团转换为可以存储或发送的描述。 我想你应该把你的整个计算机内存,连同你感兴趣的值的地址一起发送,但这似乎并不是最好的方法。

    我们可以做的是序列化数据。 这意味着它被转换为扁平的描述。 流行的序列化格式称为 JSON(发音为“Jason”),它代表 JavaScript Object Notation(JavaScript 对象表示法)。 它被广泛用作 Web 上的数据存储和通信格式,即使在 JavaScript 以外的语言中也是如此。

    JSON 看起来像 JavaScript 的数组和对象的表示方式,但有一些限制。 所有属性名都必须用双引号括起来,并且只允许使用简单的数据表达式 - 没有函数调用,绑定或任何涉及实际计算的内容。 JSON 中不允许注释。

    表示为 JSON 数据时,日记条目可能看起来像这样

    {
      "squirrel": false,
      "events": ["work", "touched tree", "pizza", "running"]
    }

    JavaScript 为我们提供了函数JSON.stringifyJSON.parse,来将数据转换为这种格式,以及从这种格式转换。 第一个函数接受 JavaScript 值并返回 JSON 编码的字符串。 第二个函数接受这样的字符串并将其转换为它编码的值。

    let string = JSON.stringify({squirrel: false,
                                 events: ["weekend"]});
    console.log(string);
    // → {"squirrel":false,"events":["weekend"]}
    console.log(JSON.parse(string).events);
    // → ["weekend"]

    本章小结

    对象和数组(一种特殊对象)可以将几个值组合起来形成一个新的值。理论上说,我们可以将一组相关的元素打包成一个对象,并通过这个对象来访问这些元素,以避免管理那些支离破碎的元素。

    在 JavaScript 中,除了nullundefined以外,绝大多数的值都含有属性。我们可以用value.propvalue["prop"]来访问属性。对象使用名称来定义和存储一定数量的属性。另外,数组中通常会包含不同数量的值,并使用数字(从 0 开始)作为这些值的属性。

    在数组中有一些具名属性,比如length和一些方法。方法是作为属性存在的函数,常常作用于其所属的值。

    你可以使用特殊类型的for循环for (let element of array)来迭代数组。

    习题

    范围的和

    在本书的前言中,提到过一种很好的计算固定范围内数字之和的方法:

    console.log(sum(range(1, 10)));

    编写一个range函数,接受两个参数:startend,然后返回包含startend(包括end)之间的所有数字。

    接着,编写一个sum函数,接受一个数字数组,并返回所有数字之和。运行示例程序,检查一下结果是不是 55。

    附加题是修改range函数,接受第 3 个可选参数,指定构建数组时的步长(step)。如果没有指定步长,构建数组时,每步增长 1,和旧函数行为一致。调用函数range(1, 10, 2),应该返回[1, 3, 5, 7, 9]。另外确保步数值为负数时也可以正常工作,因此range(5, 2, -1)应该产生[5, 4, 3, 2]

    // Your code here.
    
    console.log(range(1, 10));
    // → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    console.log(range(5, 2, -1));
    // → [5, 4, 3, 2]
    console.log(sum(range(1, 10)));
    // → 55

    逆转数组

    数组有一个reverse方法,它可以逆转数组中元素的次序。在本题中,编写两个函数,reverseArrayreverseArrayInPlace。第一个函数reverseArray接受一个数组作为参数,返回一个新数组,并逆转新数组中的元素次序。第二个函数reverseArrayInPlace与第一个函数的功能相同,但是直接将数组作为参数进行修改来,逆转数组中的元素次序。两者都不能使用标准的reverse方法。

    回想一下,在上一章中关于副作用和纯函数的讨论,哪个函数的写法的应用场景更广?哪个执行得更快?

    // Your code here.
    
    console.log(reverseArray(["A", "B", "C"]));
    // → ["C", "B", "A"];
    let arrayValue = [1, 2, 3, 4, 5];
    reverseArrayInPlace(arrayValue);
    console.log(arrayValue);
    // → [5, 4, 3, 2, 1]

    实现列表

    对象作为一个值的容器,它可以用来构建各种各样的数据结构。有一种通用的数据结构叫作列表(list)(不要与数组混淆)。列表是一种嵌套对象集合,第一个对象拥有第二个对象的引用,而第二个对象有第三个对象的引用,依此类推。

    let list = {
      value: 1,
      rest: {
        value: 2,
        rest: {
          value: 3,
          rest: null
        }
      }
    };

    最后产生的对象形成了一条链,如下图所示:

    使用列表的一个好处是,它们之间可以共享相同的子列表。举个例子,如果我们新建了两个值:{value: 0,result: list}{value: -1,result: list}(list引用了我们前面定义的绑定)。这是两个独立的列表,但它们之间却共享了同一个数据结构,该数据结构包含列表末尾的三个元素。而且我们前面定义的list仍然是包含三个元素的列表。

    编写一个函数arrayToList,当给定参数[1, 2, 3]时,建立一个和示例相似的数据结构。然后编写一个listToArray函数,将列表转换成数组。再编写一个工具函数prepend,接受一个元素和一个列表,然后创建一个新的列表,将元素添加到输入列表的开头。最后编写一个函数nth,接受一个列表和一个数,并返回列表中指定位置的元素,如果该元素不存在则返回undefined

    如果你觉得这都不是什么难题,那么编写一个递归版本的nth函数。

    // Your code here.
    
    console.log(arrayToList([10, 20]));
    // → {value: 10, rest: {value: 20, rest: null}}
    console.log(listToArray(arrayToList([10, 20, 30])));
    // → [10, 20, 30]
    console.log(prepend(10, prepend(20, null)));
    // → {value: 10, rest: {value: 20, rest: null}}
    console.log(nth(arrayToList([10, 20, 30]), 1));
    // → 20

    深层比较

    ==运算符可以判断对象是否相等。但有些时候,你希望比较的是对象中实际属性的值。

    编写一个函数deepEqual,接受两个参数,若两个对象是同一个值或两个对象中有相同属性,且使用deepEqual比较属性值均返回true时,返回true

    为了弄清楚通过身份(使用===运算符)还是其属性比较两个值,可以使用typeof运算符。如果对两个值使用typeof均返回"object",则说明你应该进行深层比较。但需要考虑一个例外的情况:由于历史原因,typeof null也会返回"object"

    当你需要查看对象的属性来进行比较时,Object.keys函数将非常有用。

    // Your code here.
    
    let obj = {here: {is: "an"}, object: 2};
    console.log(deepEqual(obj, obj));
    // → true
    console.log(deepEqual(obj, {here: 1, object: 2}));
    // → false
    console.log(deepEqual(obj, {here: {is: "an"}, object: 2}));
    // → true
    展开全文
  • 面向对象程序设计

    千次阅读 2020-12-09 15:37:28
    之前复习面向对象的时候整理的,丢出来一起分享一下。因为复习得很赶,只是大致的整理,且大部分图片来自老师的ppt,可能不是很准确。如果要详细了解其中的某个知识点请另外搜索。 但是老师不讲武德啊,明明提纲给了...

    之前复习面向对象的时候整理的,丢出来一起分享一下。因为复习得很赶,只是大致的整理,且大部分图片来自老师的ppt,可能不是很准确。如果要详细了解其中的某个知识点请另外搜索。
    但是老师不讲武德啊,明明提纲给了不按提纲考,还从开卷变成闭卷。大意了,当时就不应该报啊。

    一、面向对象程序设计的基本概念

    1.面向对象的基本思想

    (1)客观事物是由对象组成的,对象是在原事物基础上抽象的结果。
    (2)对象是由属性和操作组成的,其属性反映了对象的数据信息特征,而操作则用来定义改变对象属性状态的各种操作方式。
    (3)对象之间的联系通过消息传递机制来实现。
    (4)对象可以按其属性来归类。
    (5)对象具有封装的特性,可达到软件复用的目的。
    Ps:面向对象的三大特性是"封装、“继承”、“多态”
    五大原则是"单一职责原则"
    “开放封闭原则”:对扩展是开放的,对修改是封闭的
    “里氏替换原则”:子类应当可以替换父类,并能出现在父类能出现的任何位置上
    “依赖倒置原则”:抽象不能依赖于具体,具体应该依赖于抽象
    “接口分离原则”:使用多个专门的接口,而不使用单一的总接口,即类不应该依赖那些它不需要的接口

    2.面向对象方法与面向过程方法的区别

    1.面向过程方法首先关心的是功能,强调以模块(即过程)为中心,采用模块化、自顶向下、逐步求精设计过程,系统是实现模块功能的函数和过程的集合。
    2.面向对象方法是一种运用对象、类、继承、聚合、关联、消息、封装等概念和原则来构造软件系统的开发方法。
    3.面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
    4.面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

    3.类和对象

    类(Class)是面向对象的基础。
    对象(Object)是具有明确语义边界并封装了状态和行为的实体,由一组属性和作用在这组属性上的一组操作构成,是构成软件的一个基本单位。
    类是对具有相同属性和操作的一组对象的统一抽象描述。
    对象是它所属的类的实例。

    4.类和类之间的关系

    类和类之间的关系主要有继承、实现、依赖、关联、聚合和组合、(细化)。
    继承和实现:子类和父类
    依赖:A使用B
    关联:强依赖\引用关系,类A中的某个属性的类型是类B
    聚合:聚合关系表示的是一种整体和部分的关系,聚合是一种较弱的关联关系,部分与整体可相互独立存在。
    组合:也表示一种整体和部分的关系。组合是一种较强的关联关系,部分与整体“共存亡”。
    实现:类指向接口。

    5.类的成员

    就是类里面的所有东西,包括成员变量和成员函数

    6.对象的生命周期

    构造函数到析构函数

    7.可见性(横着记)

    在这里插入图片描述

    8.继承
    继承的本质就是原型链
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    9. 消息通信

    对象只能通过消息进行通信,而不允许在对象之外直接地访问它内部的属性,这是由封装原则引起的。
    消息必须直接发给特定的对象,消息中包含所请求服务的必要信息,且遵守所规定的通信规格说明。一条消息的规格说明至少包括:消息名、入口参数、可能返回的参数。一个对象可以是消息的接受者、发送者和参数。

    10.抽象(定义以及抽象层次)

    抽象是指从事务中舍弃个别的、非本质的特征,而抽取共同的、本质特征的思维方式。在面向对象方法中、对象是对现实世界中事务的抽象,类是对对象的抽象,一般类是对特殊类的抽象。

    11.多态性

    多态性是指一般类和特殊类可以有相同的属性或操作,但这些属性或操作,但这些属性和操作具有不同的含义,即具有不同的数据类型或表现出不同的行为。针对同一个消息,不同的对象响应所做出的行为是不同的。

    二、UML

    作用+画法 用例图、类图、序列图、活动图、状态图
    在这里插入图片描述

    1.类图:

    定义:描述了系统中各类对象以及它们之间的静态关系。
    在这里插入图片描述

    作用:
    吴小竹:显示了信息的结构,同时描述了系统的行为。是定义其他图的基础
    1.描述了系统的类的集合,类的属性和类之间的关系,可以简化人们对系统的理解。
    2.类图为系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
    3.作为最常用的UML图,显示出类、接口以及它们之间的静态结构和关系。
    4.类图图示了所构建系统的所有实体、实体的内部结构以及实体之间的关系。
    5.它是构建其他设计模型的基础,也是面向对象编程的起点和依据。

    2.用例图:

    定义:一幅由参与者、用况以及这些元素之间的关系组成的图。
    主要的作用:(1)描述了待开发系统的功能需求;(2)将系统看作黑盒,从外部执行者的角度来理解系统;(3)驱动了需求分析之后各阶段的开发工作,不仅在开发过程中保证了系统所有功能的实现,而且被用于验证和检测所开发的系统,从而影响到开发工作的各个阶段和 UML 的各个模型。

    3.序列图(顺序图):

    定义:是一种详细描述对象之间以及对象与参与者之间交互的图,由一组相互协作的对象或参与者实例以及它们之间发送的消息组成,强调消息之间的顺序。
    作用:1.把用例表达的需求,转化为进一步、更加正式层次的精细表达。2.用例常常被细化为一个或者更多的序列图。同时序列图更有效地描述如何分配各个类的职责以及各类具有相应职责的原因。
    序列图是由对象、生命线、激活、消息、分支与从属流等元素构成的
    生命线(Lifeline)是一条垂直的虚线,用来表示序列图中的对象在一段时间内的存在激活。
    激活(Activation)表示一个对象直接或通过从属操作完成操作的过程。在UML图中通过一个窄长的矩形来表示,矩形的高度表示对象存在的过程。
    在这里插入图片描述

    4.活动图:

    定义:是描述动作、动作的执行顺序以及动作的输入与输出的图,由动作结点和边组成
    动作:是可执行的基本功能单元,用以描述系统中的状态转换或活动,是原子性的和实时的。
    活动:是一组相互协作的动作构成的行为单元,在执行中可被事务中断。
    作用: (1)描述复杂过程的算法。(2)活动图对用例描述尤其有用,它可建模用例的工作流,显示用例内部和用例之间的路径。它可以说明用例的实例是如何执行动作以及如何改变对象状态。(3)活动图对理解业务处理过程十分有用。活动图可以画出工作流用以描述业务,有利于与领域专家进行交流。(4)描述一个操作执行过程中所完成的工作。说明角色、工作流、组织和对象是如何工作的。发送信号接收信号
    在这里插入图片描述

    5.状态图:

    定义:描述了一个对象在其生命周期内因响应事件所经历的状态序列以及这些事件所做出的反应。主要由状态和状态间的转移构成。
    事件的定义:指时间上和空间上可以定位并具有实际意义、值得注意的所发生的事情。
    UML中把事件分为
    信号事件:一个对象对一个信号实例的接受,特征标记放在由它触发的转移上。
    调用事件:对操作的调用的接受,导致一个调用事件,由接受事件的对象实现事件。
    改变事件:用布尔表达式描述的指派条件变为真。
    状态的定义:对象在其生命周期内满足特定条件,进行特定活动或等待特定事件的状况。
    在这里插入图片描述

    三、面向对象设计方法(具体不懂的可以去菜鸟教程看)

    要解决的问题、结构(uml类图)、优缺点
    开-闭原则:要求一个系统的设计能够允许系统在无需修改的情况下,扩展其功能。

    简单工厂

    由一个工厂对象决定创建出哪一种产品类的实例。
    解决的问题:
    专门负责将大量有共同接口的类实例化。
    结构:
    在这里插入图片描述

    优点:
    1.客户端则可以免除直接创建产品对象的责任,而仅仅负责“消费”产品。
    2.对于消费者角色来说,任何时候需要某种产品,只需要向工厂角色(下订单)请求即可,而无需知道产品创建细节。
    3.实现了客户端类与产品类的解耦。
    缺点:对“开-闭”原则的支持不够,因为如果有新的产品加入到系统中去,就需要修改工厂类,将必要的逻辑加入到工厂类中。

    &工厂方法

    工厂方法模式将定义一个创建产品对象的工厂接口,将实际创建功能放到具体工厂子类中。

    解决的问题:

    对简单工厂模式加以改进,使得新增产品时依旧符合开放-封闭原则。

    类图:

    在这里插入图片描述

    Creator:工厂 ConcreteCreator:具体工厂 Product:产品 ConcreteCreator:抽象产品

    序列图:

    在这里插入图片描述

    工厂模式和简单工厂模式的区别:

    工厂方法模式的核心是一个抽象工厂类,而简单工厂模式把核心放在一个具体类上。
    工厂方法模式可以允许很多具体工厂类从抽象工厂类中将创建行为继承下来,从而可以成为多个简单工厂模式的综合,进而推广了简单工厂模式。
    工厂方法模式退化后可以变得很像简单工厂模式。如果确定一个系统只需要一个具体工厂类,把抽象工厂类合并到具体的工厂类中去。将工厂方法改成为静态方法,这时候就得到了简单工厂模式。

    优点:

    如果系统需要加入一个新的产品,那么所需要的就是向系统中加入一个这个产品类以及它所对应的工厂类。没有必要修改客户端,也没有必要修改抽象工厂角色或者其他已有的具体工厂角色。对于增加新的产品类而言,这个系统完全支持“开-闭”原则。

    &单例模式

    用法即优点:

    单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。

    解决的问题:

    用于资源管理器对多个设备的管理。在一个系统要求一个类只有一个实例时才应当使用单例模式。反过来说,如果一个类可以有几个实例共存,那么就没有必要使用单例类。

    类图:

    在这里插入图片描述

    饿汉单例:

    public class EagerSingleton
    {
    private static final EagerSingleton m_instance =new EagerSingleton();
    //私有的默认构造子
    private EagerSingleton() { }
    //静态工厂方法
    public static EagerSingleton getInstance()
    {
    return m_instance;//对方调用时实例化
    }
    }
    

    类图:
    在这里插入图片描述

    懒汉式单例:

    与饿汉式单例类相同之处是,类的构造函数是私有的。与饿汉式单例类不同的是,懒汉式单例类在第一次被引用时将自己实例化。
    类图:
    在这里插入图片描述

    package com.javapatterns.singleton.demos;
    public class LazySingleton
    {
       private static LazySingleton  m_instance = null;
    //私有的默认构造子,保证外界无法直接实例化
       private LazySingleton() { }
    //静态工厂方法,返还此类的惟一实例
       synchronized public static LazySingleton getInstance()
       {
          if (m_instance == null)//第一次引用就实例化
          {
             m_instance = new LazySingleton();
          }
          return m_instance;
       }
    }
    

    &克隆模式(原型模式)

    通过给出一个原型对象来指明所要创建的对象类型,然后用复制这个原型对象的办法创建出更多的同类型对象。

    类图:

    在这里插入图片描述

    public class Client
    {
       private Prototype prototype;
       public void operation(Prototype example)
       {
          Prototype p=(Prototype)example.clone();
       }
    }
    抽象原型角色声明了一个clone()方法
    pubilc interface Prototype  extends Cloneable
    {   
       Object clone();
    }
    具体原型角色实现clone()方法
    public class ConcretePrototype implements Prototype/** 克隆方法*/
    {   public Object clone()
       {try 
            {  return new ConcretePrototype(); }
            catch( CloneNotSupportedException e)
            {  return null; }
       }
    }
    

    在这里插入图片描述

    客户端(Client)角色:向原型管理器提出存储或提取对象的请求。
    抽象原型(Prototype)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体原型类所需的接口。
    具体原型(Concrete Prototype)角色:被复制的对象。此角色需要实现抽象的原型角色所要求的接口。
    原型管理器(Prototype Manager)角色:管理记录每一个被创建的对象。
    优点:1.简化了对象的创建过程。2.在有大对象或多个对象的复制时,可提高系统性能。
    缺点:1.每个类必须额外增加一个克隆方法。2.深复制的实现较复杂。

    &适配器模式

    分为类的适配器模式(类的结构模式)和对象的适配器模式(对象的结构模式)

    需要用到的情况:

    (1)系统需要使用现有的类,而此类的接口不符合系统的需要。
    (2)想要建立一个可以重复使用的类.用于一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有很复杂的接口。

    1.类的适配器模式:

    把一个类的接口变换成客户端所需要的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
    被适配的类的API转换成为目标类的API,其静态结构图如下图所示。

    结构图:

    在这里插入图片描述

    目标(Target)角色:这就是所期待得到的接口。
    源(Adaptee)角色:现有需要适配的接口。
    适配器(Adapter)角色:适配器类是本模式的核心,把源接口转换成目标接口。不可以是接口,而必须是具体类。

    public interface Target
    {
         void sampleOperation1();  //源类的方法
         void sampleOperation2();  //源类没有的方法
    } 
    public class Adaptee
    {
        public void sampleOperation1() { }
    }
    public class Adapter extends Adaptee implements Target
    {
        /** 由于源类没有方法sampleOperation2,因此适配器类补上这个方法 */
        public void sampleOperation2()
        {
            // Write your code here
        }
    }
    

    2.对象的适配器模式

    对象的适配器模式不是使用继承关系连接到adaptee类,而是使用委派关系连接到Adaptee类。

    结构:

    在这里插入图片描述

    Adapter的源代码如下:

    public class Adapter implements Target
    {    private Adaptee adaptee;
        public Adapter(Adaptee adaptee)
        {      super();
                this.adaptee = adaptee;
        }
        public void sampleOperation1()
        {        adaptee.sampleOperation1();
        }
        public void sampleOperation2()
        {        // Write your code here
        }
    }
    

    &合成模式(对象的结构模式)

    1.合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。
    2.合成模式可以使客户端将整体对象与部分对象同等看待。

    需要用到的情况:

    (1)需要描述对象的部分和整体的等级结构
    (2)需要客户端忽略掉整体对象和部分对象的区别。
    抽象构件(Component)角色:这是一个抽象角色,给参加组合的对象规定一个接口。这个角色给出共有的接口及其默认行为。
    树叶构件(Leaf)角色:代表参加组合的树叶对象。没有下级的子对象。定义出参加组合的原始对象的行为。
    树枝构件(Composite)角色:代表参加组合的所有包含子对象的对象,并给出树枝构件对象的行为。

    两种形式

    1.透明方式
    透明式的合成模式要求所有的具体构件类,不论是树枝还是树叶,均符合一个固定的接口。
    在这里插入图片描述

    优点:所有的构件类都有相同的接口 缺点:不够安全
    2.安全方式
    安全式的合成模式要求管理聚集的方法只出现在树枝构件类中,而不出现在树叶构件类中。
    在这里插入图片描述

    优点:很安全 缺点:不够透明,树叶类和合成类将具有不同的接口
    eg:安全模式

     public abstract class Graphics
    {
        public abstract void draw();
    }
    public class Picture extends Graphics
    {
       private Vector list=new Vector(10);
       public void draw()
       {
          for (int i=0;i< list.size();i++)
    {
             Graphics g=(Graphics)list.get(i);
             g.draw();
          }
       }
    public void add(Graphics g)
    {
          list.add(g);
        }
       public void remove(Graphics g)
    {/*删除一个子构件 */
          list.remove(g);
       }
       public void getChild(int i)
    {/* 返还一个子构件 */
          return (Graphics)list.get(i);
       }
    }
    public class Line extends Graphics
    {
    public void draw()
    {
          //write your code 
        }
    }
    

    在这里插入图片描述

    透明模式

    abstract public class Graphics
    {
       public abstract void draw();
       public void add(Graphics g);/* 聚集管理方法,增加一个子构件 */
       public void remove(Graphics g);/* 聚集管理方法,删除一个子构件 */
       public Graphics getChild(int i);/* 返还一个子构件 */
    }
    package com.javapatterns.composite.drawingtransparent;
    import java.util.Vector;
    public class Picture extends Graphics
    {
       private Vector list=new Vector(10);
       public void draw()
       {
          for (int i=0;i< list.size();i++)
          {
             Graphics g=(Graphics)list.get(i);
             g.draw();
          }
       }
       public void add(Graphics g){
          list.add(g);
       }
       public void remove(Graphics g){
          list.remove(g);
       }
       public Graphics getChild(int i){
          return (Graphics)list.get(i);
       }
    }
    public class Line extends Graphics
    {
        public void draw(){//do nothing }
       public void add(Graphics g){//do nothing }
       public void remove(Graphics g){//do nothing }
       public Graphics getChild(int i){//do nothing }
    }
    

    在这里插入图片描述

    &代理模式(对象的结构模式)

    代理(Proxy)模式给某一个对象提供一个代理,并由代理对象控制对原对象的引用
    在这里插入图片描述

    抽象主体角色(Subject):声明了真实主体和代理主体的共同接口
    代理主体(Proxy)角色:内部含有对真实主体的引用;供一个与真实主体角色相同的接口;负责在需要的时候创建真实主体对象(和删除真实主体对象),在将客户端调用传递给真实的主体之前或之后,都要执行某个操作
    真实主体角色(RealSubject)角色:定义了代理角色所代表的真实对象。

    Package com.javapatterns.proxy;
    Abstract public class Subject
    {
          abctract public void request();//抽象请求方法
    }
    Public class RealSubject extends Subject
    { 
       public RealSubject(){}
       public void request(){//具体请求方法}
    }
    Public class ProxySubject extends Subject
    {
       private RealSubject realSubject;
       public ProxySubject(){}
       public void request()//实现请求方法
       {  preRequest();//请求先先进行的操作
          if (realSubject==null)
          	realSubject=new RealSubject(); 
          realSubject.request();
          postRequest();//请求后进行的操作
       }
      private void preRequest()//请求前的操作
       { 
            //something you want to do before requesting
       }
       private void postRequest()//请求后的操作
       {
             //something you want to do before requesting 
       }
    }
    

    &装饰模式/包装模式(对象的结构模式)

    以对客户端透明的方式扩展对象的功能。是继承关系的一种替代方案。
    在这里插入图片描述

    抽象构件(Component)角色:给出一个接口,以规范准备接收附加责任(扩展功能)的对象。
    具体构件(Concrete Component)角色:定义一个准备接收附加责任的类。
    装饰(Decorator)角色:持有一个构件的实例,并定义与抽象构件接口一致的接口。
    具体装饰(Concrete Decorator)角色:负责给构件对象“贴上”附加的责任。

    Public interface Component
    {
          void sampleOperation();//某业务逻辑方法
    }
    Public class Decorator  implements Component
    {
    	private Component component;
     	public Decorator(Component component)
    	{
    		this.component = component;
    	}
      	public void sampleOperation()/** 业务方法,委派给构件*/
    	{
    		component.sampleOperation();
    	}
    }
    Public class ConcreteComponent implements Component
    {
    	public ConcreteComponent()  {//Write your code here}
    	public void sampleOperation()  {//Write your code here}
    }
    Public class ConcreteDecorator  extends Decorator
    {
    	public void sampleOperation()//进一步增强功能
    	{	super.sampleOperation();	}
    }
    

    优点:

    1.可以提供比继承更多的灵活性.
    2.通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同的行为的组合.

    缺点:

    1.产生出较多的对象;
    2.比继承更易出错;

    &观察者模式

    一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
    解决的问题:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
    在这里插入图片描述

    抽象主题(Subject)角色:把所有对观察者对象的引用保存在一个聚集里;提供接口,可以增加和删除观察者对象。
    抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。这个接口叫做更新接口。
    具体主题(Concrete Subject)角色:将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。负责实现对观察者引用的聚集的管理方法。
    具体观察者(Concrete Observer)角色:实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题 的状态相协调。

    所有具体主题管理聚集的方法都相同,因此可以将它移到抽象主题中.
    第二种实现方案和第一种实现方案的主要区别就是代表存储观察者对象的聚集连线是从抽象主题到抽象观察者.
    在这里插入图片描述

    优点

    1.观察者模式在被观察者和观察者之间建立一个抽象的耦合。2.观察者模式支持广播通信。
    缺点
    1.如果一个被观察者有很多的观察者,将所有的观察者都通知到会花费很多时间。
    2.如果在被观察者之间有循环依赖,被观察者会触发它们之间进行循环调用,导致系统崩溃。
    3.虽然观察者随时知道所观察的对象发生了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎样发生变化的。

    &命令模式/行动模式/事物模式

    请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。请求的一方和接收的一方独立开来。
    在这里插入图片描述

    客户端(Client)角色:创建了一个具体命令(ConcreteCommand)对象并确定其接收者。
    命令角色(Command):声明了一个给所有具体命令类的抽象接口。
    具体命令(ConcreteCommand)角色:定义一个接收者和行为之间的弱耦合;实现execute()方法,负责调用接收者的相应操作。execute()方法通常叫做执行方法。
    请求者(Invoker)角色:负责发出一个请求。
    接收者(Receiver)角色:任何一个类都可以成为接收者;实施和执行请求的方法叫做行动方法。

    public class Invoker
    {   private Command command;
        public Invoker(Command command)
        { 
            this.command=command;
         }
       /* 行动方法 */
       public void action()
       {
          command.execute();
       }
    }
    
    public class Receiver
    {
       public Receiver()
       {   }
       public void action()
       {
          System.out.println(“Action has been taken.);
       }
    }
    
    
    
    
    
    public interface Command
    {   void execute(); }
    
    Public class ConcreteCommand implements Command
    {
       private Receiver receiver;
       public ConcreteCommand(Receiver receiver)
       {
          this.receiver=receiver;
       }
       public void execute()
       {
          receiver.action();//调用接受者的接受方法
       }
    }
    
    public class Client
    {   public static void main(String[] args)
        {   Receiver receiver=new Receiver();
            Command command=new ConcreteCommand(receiver);
            Invoker invoker=new Invoker(command);
            invoker.action();//发出请求
        }
    }
    

    在这里插入图片描述

    优点:

    1.命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
    2.命令类与其他任何别的类一样,可以修改和扩展。
    3.可以把命令对象聚合在一起,合成为合成命令。
    4.由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很容易。

    缺点:

    1使用命令模式会导致某些系统有过多的具体命令类。

    &模板方法模式

    模板方法模式是所有模式中最为常见的几个模式之一。模板方法模式是基于继承的代码复用的基本技术,模板方法模式的结构和用法也是面向对象设计的核心。
    模板方法模式为设计抽象类和实现具体子类的设计师之间的协作提供了可能:一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。
    解决的问题:一些方法通用,却在每一个子类都重新写了这一方法。
    在这里插入图片描述

    意图:

    定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

    主要解决:

    一些方法通用,却在每一个子类都重新写了这一方法。

    优点:

    1、封装不变部分,扩展可变部分。
    2、提取公共代码,便于维护。
    3、行为由父类控制,子类实现。(即好莱坞原则)

    缺点:

    每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
    在这里插入图片描述

    策略模式

    针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得他们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
    何时使用:
    1.如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为。
    2.一个系统需要动态地在几种算法中选择一种。
    3.一个系统的算法使用的数据不可以让客户端知道。
    4.如果一个对象有很多的行为,
    在这里插入图片描述
    在这里插入图片描述

    环境(Context)角色:表示策略的使用环境,它持有一个Strategy对象的引用。
    抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
    具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

    优点:

    1、算法可以自由切换。
    2、避免使用多重条件判断。
    3、扩展性良好。

    缺点:

    1、策略类会增多。
    2、所有策略类都需要对外暴露。
    (ppt无)
    java语法:
    继承:extends class A extends B接口:implements eg: class A implements B
    浅复制:
    被复制对象的所有变量都含有与原对象相同的值,而所有的对其它对象的引用都仍然指向原来的对象。浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
    深复制:
    被复制对象的所有变量都含有与原对象相同的值,除去那些引用其它对象的变量。那些引用其它对象的变量将指向被复制过的新对象。深复制把要复制的对象所引用的对象都复制了一遍,而这种对被引用到的对象的复制叫做间接复制。

    展开全文
  • js常用对象及解构赋值

    千次阅读 2019-10-30 10:29:53
    基本类型的包装对象 “包装对象”对于每种基本类型调用都是不同的,如String, Number, Boolean 和 Symbol。因此,他们提供了不同的方法。基本类型 null 和 undefined 是个例外。他们没有相应的“包装对象”,也没有...
  • 六、对象的秘密 原文:The Secret Life of Objects 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了《JavaScript 编程精解(第 2 版)》 抽象数据类型是通过编写一种特殊的程序...
  • 该插件通过设置适当的帧,沿指定方向旋转主要对象来自动执行精灵动画渲染,并将渲染的帧保存在以下文件夹结构中:save_path / animation_folder_name / direction_folder_name(如果有多个启用的方向)/ frame_name...
  • VFS对象数据结构

    千次阅读 2010-06-02 12:41:00
      每个VFS对象都存放在一个恰当的数据结构中,其中包括对象的属性和指向对象方法表的指针。内核可以动态地修改对象的方法,因此可以为对象建立专用的行为。   所有的VFS对象都放在include/...
  • 专升本数据结构复习

    千次阅读 多人点赞 2021-03-03 13:48:51
    数据结构知识点总汇 主要参考书目: ...① 数据结构(逻辑结构)其4类基本结构:集合、线性结构、树形结构、图状结构 和 网状结构。 ② 物理结构(存储结构)其4种存储结构:顺序存储结构、链式存储结构、索引存储
  • MFC文档/视图结构体系及SDI回顾(1)

    千次阅读 2016-10-17 21:29:10
    在早期的MFC1.0版本,应用程序具有两个主要的控件:代表应用程序自身的应用程序对象和代表应用程序窗口的窗口对象。应用程序对象的主要任务在于创建窗口,反过来窗口再处理消息。 通过引入文档视图体系结构,可以...
  • JSON的全称是”JavaScript Object Notation”,意思是JavaScript对象表示法,它是一种基于文本,独立于语言的轻量级数据交换格式。 XML也是一种数据交换格式,为什么没有选择XML呢?因为XML虽然可以作为跨平台的...
  • 数据结构知识整理

    万次阅读 多人点赞 2018-07-30 18:50:47
    1.数据结构:是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。 2.数据结构涵盖的内容: 3.基本概念和术语: 数据:对客观事物的符号表示,在计算机科学中是指所有能...
  • UML2面向对象分析与设计期末复习知识点

    千次阅读 多人点赞 2020-12-31 16:10:00
    – UML 是对描述面向对象的系统分析和设计工作所用符号进行标准化尝试的一种语言 – 其目的是建立一套不依赖于完成设计所用方法的符号 – UML 的开发意图是用于所有面向对象的开发方法、生命循环阶段、应用程序 ...
  • 创建包含N个空对象的数组

    千次阅读 2019-01-21 01:17:00
    在给对象设置属性时, 如果对象不存在很容易报错. 有些场景, 在对对象数组处理时, 设置对象属性前判断对象是否存在. 与其这样, 还不如直接初始化为空对象数组. 本文主要涉及到一些容易忽略的知识点: Array....
  • 严蔚敏数据结构课后参考答案

    万次阅读 多人点赞 2019-11-09 22:46:44
    1.简述下列概念:数据、数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、抽象数据类型。 答案: 数据:是客观事物的符号表示,指所有能输入到计算机中并被计算机程序处理的符号的总称。如数学计算中用...
  • 西安理工大学计算机考研数据结构863整理总结

    千次阅读 多人点赞 2020-07-27 16:19:56
    1)了解数据元素、数据结构、抽象数据类型、存储结构等概念;了解算法概念及算法设计的基本要求 ; 2)掌握算法分析方法、语句的频度和估算时间复杂度、空间复杂度分析方法。 考查要点 1.数据结构的研究内容 包括...
  • VBA FSO对象模型详解

    千次阅读 2018-06-27 15:43:11
    循环迭代系统中所有驱动器,或者使用 Drives 集合的 Item 方法读取某个 Drive 对象(代表系统中的某个驱动器)。 例如,下面的代码创建计算机驱动盘清单: Sub DrivesList(   ) Dim fs As Object Dim ...
  • 约瑟夫(Josephus)问题的求解——利用循环链表 1. 约瑟夫问题的提法 约瑟夫问题(约瑟夫环)是一个数学的应用问题。 已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围,从编号为k的人开始报数,数到m的那个人...
  • 数据结构名词解释以及简答

    万次阅读 多人点赞 2020-05-20 23:28:52
    数据结构:是相互之间存在一种或多种特定关系的数据元素的集合,是计算机存储和数据组织的方式,它分为三个方面,即数据的逻辑结构,数据的物理结构,数据的操作。 数据项:是数据不可分割的最小单位,用它可以...
  • 循环链表(Circular Linked List)

    千次阅读 2016-05-03 00:42:13
    循环链表(Circular Linked List) 1. 循环链表的概念 1.1 循环链表的定义 循环链表是另一种形式的表示线性表的链表。 1.2 循环链表的结点结构 循环链表的结点包括两个部分:数据域和...循环链表的结点结构示意图:
  • 寻求数据模型的实质是分析问题,从中提取出计算机处理的对象,并找出这些对象之间的关系,然后用数学的语言加以描述。 关键:分析问题中所用到的数据是如何组织的,研究数据之间存在什么样的关系。 由于数据必须在...
  • 【数据结构】之排序算法综合篇

    千次阅读 2019-07-14 15:36:35
    博主声明: 转载请在开头附加本文链接及作者... 数据结构课程的内容基本忘光了,所以又拾起数据结构与算法的书本,重新学习了一边排序算法的原理和实现,在经过几个晚上的努力,终于把这几种排序算法给搞明白了,下...
  • 数据结构分类及八种常见数据结构

    千次阅读 2020-05-09 11:04:00
    一.数据结构分类 数据的逻辑结构 ...链式存储结构:使用若干地址分散的存储单元存储数据元素,逻辑上相邻的数据元素在物理位置上不一定相邻,数据元素之间的关系需要采用附加信息特别指定。c语言采
  • 2018山西专升本数据结构知识点总结

    万次阅读 多人点赞 2018-06-29 19:41:36
    2018山西专升本数据结构知识点总结
  • Xcode与C++之游戏开发: 游戏对象

    千次阅读 2019-03-07 01:28:30
    游戏对象(game object),指的是游戏中任何需要更新和...表示游戏对象存在不同的方法,有的采取层次结构,有的采用组合,也有更复杂的设计模式。但不管是哪种表示方法,游戏都需要某种方式来跟踪和更新这些游戏对象
  • 30 个重要数据结构和算法完整介绍(建议收藏保存)

    万次阅读 多人点赞 2021-06-07 08:02:06
    数据结构和算法 (DSA)通常被认为是一个令人生畏的话题——一种常见的误解。它们是技术领域最具创新性概念的基础,对于工作/实习申请者和有经验的程序员的职业发展都至关重要。话虽如此,我决定在CSDN新星计划挑战...
  • 《数据结构C语言版》——绪论

    千次阅读 2020-02-10 23:04:01
    《数据结构C语言版》——绪论 一.基本概念和术语 1.数据,数据元素, 数据项,数据对象 数据:是客观事物的符号表示,是所有能够输入到计算机中并被计算机程序处理的符号的总称。 数据元素:是数据的基本单位,数据...
  • 数据结构与算法常见笔试题

    万次阅读 多人点赞 2017-09-10 11:03:57
    1.数据结构与算法常见笔试题   第一章 数据结构与算法 一.算法的基本概念 计算机解题的过程实际上是在实施某种算法,这种算法称为计算机算法。 1.算法的基本特征:可行性,确定性,有穷性,拥有足够的情报。 2....
  • java克隆对象clone()的用法和作用

    万次阅读 2015-12-09 10:02:09
    内容摘要若需修改一个对象,同时不想改变调用者的对象,就要制作该对象的一个本地副本。这也是本地副本最常见的一种用途。若决定制作一个本地副本,只需简单地使用clone()方法即可。Clone是“克隆”的意思,即制作...
  • 数据结构笔记(参考王道考研系列)

    千次阅读 多人点赞 2020-07-22 23:40:52
    数据结构在学什么? 如何用程序代码把现实世界的问题信息化 如何用计算机高效地处理这些信息从而创造价值 信息化世界 人类文明 人类文明 注解 农业革命 学会农耕,从“动物”到“人类” 工业革命 出现...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 64,845
精华内容 25,938
关键字:

循环结构的附加对象