瑝琦的博客

基于 jekyll 的 Github Pages 个人博客网站,技术的学习、总结、分享与提升

SSH协议中隧道与代理的用法详解

SSH 协议是 Linux 系统中使用较为频繁的协议之一,通常用于远程管理主机或服务器,默认使用 22 端口,可类比 Windows 系统中的 telnet(23 端口),这里要介绍的是 ssh 除了远程连接外的另一强大特性,即隧道加密与多种场景下代理功能的实现。 前置条件 为了理解更轻松,最大程度上简化网络拓扑,后续都只用两台机器做测试,IP 与主机名对应关系如下;更复杂的网络结构和多接口场景可举一反三延申,要用作某些特殊用途,则自行YY。 192.168.111.128 --> Kali 192.168.111.131 --> Centos 在验证 IP 地址和多级代理的场景时,会额外用到网关机器:192.168.111.1。 然后再多了解一下三个 ssh 的命令行参数,后面会用到: -N 建立连接后不远程执行命令,也没有交互shell,通常用于端口转发的场景。 -f 建立连接后会在后台运行进程,不占用前台窗口。 -c 传输数据时对数据进行压缩,压缩算法和 gzip 的一样,但不适用于高速网络环境,会降低连接速度。 -v 打印更详细的连接过程信息。 本地转发(-L) 原理 本地转发即使用 ssh -L 参数,先看一下官方解释(man ssh): -L [bind_address:]port:host:hostport -L [bind_address:]port:remote_socket -L local_socket:host:hostport -L local_socket:remote_socket Specifies that connections to the given TCP port or Unix socket on the local (client) host are to be forwarded to the given host and port, or Unix socket, on the remote side. This works by allocating a socket to listen to either a TCP port on the local side, optionally bound to the specified bind_address, or to a Unix socket. Whenever a connection is made to the local port or socket, the connection is for‐ warded over the secure channel, and a connection is made to either host port hostport, or the Unix socket remote_socket, from the remote machine. Port forwardings can also be specified in the configuration file. Only the superuser can forward privi‐ leged ports. IPv6 addresses can be specified by enclosing the address in square brackets. By default, the local port is bound in accordance with the GatewayPorts setting. However, an explicit bind_address may be used to bind the connection to a specific address. The bind_address of “localhost” indicates that the listening port be bound for local use only, while an empty address or ‘*’ indicates that the port should be available from all interfaces. 翻译成人话,通俗讲就是,使用该参数执行 ssh 连接后,会在本机开启一个指定监听端口1,然后绑定到远程机器的指定接口(IP)的指定端口2,用另一个程序再访问本机的指定端口1,流量就会转发到远程机器的指定端口2,相当于直接访问远程机器的端口2;可以简单的理解为 “流量从本地转发到远程机器”。 参数值([bind_address:]port:host:hostport)也有规律,从左到右写是本地到远程的顺序:本地地址(bind_address])的端口(port)转发到远程地址(host)的端口(hostport),本地接口地址可省略,默认为 127.0.0.1。除了接口和端口也可以使用 Unix socket 建立连接,把对应的位置换成 socket 地址即可。 实验 上面的转发流程有点绕,用实验来理解下实际效果,假设场景为 Kali(192.168.111.128)去连接 Centos(192.168.111.131);那么首先确保 Centos 开启 22 端口,然后用 python 给它在 8000 端口开启一个简单的 http 服务: 试着先本地访问下(Centos 具有第二个接口:192.168.122.1): ok 没问题,接着去另一台机器 Kali(192.168.111.128) 上先测试连一下 Centos 的 ssh: 证明连接是通的,接下来直接在 Kali 上使用本地转发连接 Centos(为了方便已提前给两台机器配置了 ssh 公钥连接,避免输入密码),执行参数为: ssh -NL 1080:192.168.122.1:8000 root@192.168.111.131 然后执行后会在 Kali 本地监听 1080,再用 curl 访问一下这个端口,返回数据就是 Centos 的 192.168.122.1 接口上的 8000 端口所对应的内容: Centos 这边也记录到了相应的连接日志,本地转发成功: 远程转发(-R) 原理 远程转发的执行参数是 ssh -R,官方的解释是: -R [bind_address:]port:host:hostport -R [bind_address:]port:local_socket -R remote_socket:host:hostport -R remote_socket:local_socket -R [bind_address:]port Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the local side. This works by allocating a socket to listen to either a TCP port or to a Unix socket on the remote side. Whenever a connection is made to this port or Unix socket, the connection is forwarded over the secure channel, and a connection is made from the local machine to either an explicit destination specified by host port hostport, or local_socket, or, if no explicit destination was specified, ssh will act as a SOCKS 4/5 proxy and forward connections to the destinations requested by the remote SOCKS client. Port forwardings can also be specified in the configuration file. Privileged ports can be forwarded only when logging in as root on the remote machine. IPv6 addresses can be specified by enclosing the address in square brackets. By default, TCP listening sockets on the server will be bound to the loopback interface only. This may be overridden by specifying a bind_address. An empty bind_address, or the address ‘*’, indicates that the remote socket should listen on all interfaces. Specifying a remote bind_address will only succeed if the server's GatewayPorts option is enabled (see sshd_config(5)). If the port argument is ‘0’, the listen port will be dynamically allocated on the server and reported to the client at run time. When used together with -O forward, the allocated port will be printed to the standard output. 通俗讲就是,执行远程转发命令后,会在远程机器开启监听一个指定端口1,绑定到本地的指定端口2,所有访问远程机器端口1 的流量都会被转发到本地的端口2 上,相当于直接访问本地的端口2,也可以简单的理解为 “流量从远程转发到本地机器”; 可以注意到这里的转发流向其实是和上面的本地转发是反的,所以参数值([bind_address:]port:host:hostport)的规律也反了过来,从左到右写是:远程机器的接口([bind_address])上的端口(port)转发到本地接口(host)的端口(hostport)上去。远程接口地址(IP)可以省略,默认 127.0.0.1,socket 连接同理。 这里有个值得注意的地方,可以看到参数值那块,相比于前面的本地转发多了个 [bind_address:]port 值,也就是说同时省略了本地接口地址和端口值,远程接口也可按需省略,相当于可以只写一个远程端口值,这样其实会建立一个反向的 socks5 代理,即远程机器的那个端口可以被当成一个 socks4 或 socks5 代理端口,代理流量都会被引导至本地机器,这个后面也有对应实验验证。 实验 下面同样使用 Kali(192.168.111.128)通过远程转发去连接 Centos(192.168.111.131),不过这次是在 Kali 上用 python 启一个简单的 http 服务: 本地连接没问题,接下来在 Kali 上开启远程转发,参数为: ssh -NR 1080:127.0.0.1:8000 root@192.168.111.131 这会在远程机器(Centos, 192.168.111.131)上开启一个新的监听端口 1080,再使用 curl 对其访问,就会得到 Kali(192.168.111.128)上端口 8000 的返回内容: Kali 这边也有了相应的访问记录,至此远程转发成功: 反向 socks 代理 然后测试 -R 参数值只指定一个远程端口的效果,前面讲这会让本地机器成为一台 socks 代理服务器,下面为了测试代理效果,就需要网关机器(192.168.111.1)上场了,在网关上启一个简单的 php 服务,以获取访问者的真实 IP 地址,代码如下: <?php echo $_SERVER['REMOTE_ADDR'] . PHP_EOL; 访问路径是:http://192.168.111.1/get-ip.php,先分别用 Kali 和 Centos 访问试下: ok,IP 是对的,然后在 Kali(192.168.111.128)上启动反向 socks 代理服务: ssh -NR 1080 root@192.168.111.131 去 Centos(192.168.111.131)上访问一下看看代理(使用 curl 的 -x 参数指定代理服务器)效果: 至此反向 socks 代理成功。 远程接口地址问题 在实际使用远程转发的时候,可能会遇到一个小坑,就是 -R 的参数值虽然可以任意指定远程机器的接口地址,但实际上 ssh 默认都只会在 127.0.0.1 接口地址上开监听端口,也就是说假如这台远程机器是台公网服务器的话,新监听的端口是没法在公网上访问到的,只能这台机器本地访问,这是一个 sshd 的默认配置导致的,它一般在 /etc/ssh/sshd_config 文件中,GatewayPorts 这个配置项,默认是 no,改成 yes 即可,如果没有这一行就手动添加: 然后重启 sshd 服务,再用远程转发连接时就会在所有接口(即 0.0.0.0)上监听新端口了,但这样也只能在所有接口上开启端口了,无论连接方如何设置远程接口,所以需斟酌使用。 动态转发(-D) 原理 动态转发的执行参数是 ssh -D,官方解释是: -D [bind_address:]port Specifies a local “dynamic” application-level port forwarding. This works by allocating a socket to listen to port on the local side, optionally bound to the specified bind_address. Whenever a connection is made to this port, the connection is forwarded over the secure channel, and the application protocol is then used to determine where to connect to from the remote machine. Currently the SOCKS4 and SOCKS5 protocols are supported, and ssh will act as a SOCKS server. Only root can forward privileged ports. Dynamic port forwardings can also be specified in the configuration file. IPv6 addresses can be specified by enclosing the address in square brackets. Only the superuser can forward privileged ports. By default, the local port is bound in accordance with the GatewayPorts set‐ ting. However, an explicit bind_address may be used to bind the connection to a specific address. The bind_address of “localhost” indicates that the listening port be bound for local use only, while an empty address or ‘*’ indicates that the port should be available from all interfaces. 这应该是实际使用中较多的一种用途,即 socks 代理,动态转发是在本地监听一个指定端口,应用程序将 socks 代理端口设置为这个端口后,任何连接流量都会通过这个端口,经由 ssh 隧道转发到远程机器代为发送,由于不再受限于连接端和被连接端的端口与接口,所以称为动态。 参数值([bind_address:]port)为要在本地机器监听的接口地址与端口,不写接口地址则默认为 127.0.0.1,在所有接口监听则写 0.0.0.0。 实验 还是在 Kali(192.168.111.128)上执行操作,监听本地 1080 端口,将 Centos(192.168.111.131)作为 socks 代理服务器: ssh -ND 1080 root@192.168.111.131 尝试用 curl 测试代理效果,可以看到 socks5 代理已生效,IP 地址为 Centos(192.168.111.131)的: Kali 访问 Centos 的其他接口(192.168.122.1)的 http 服务(8000 端口)也没问题: 多级代理(-J) 原理 实现多级代理需要用到 ssh -J 参数,即设置 Jump host,可理解为跳板,官方解释为: -J destination Connect to the target host by first making a ssh connection to the jump host described by destination and then establishing a TCP forwarding to the ultimate destination from there. Multiple jump hops may be specified separated by comma characters. This is a shortcut to specify a ProxyJump configuration di‐ rective. Note that configuration directives supplied on the command-line generally apply to the desti‐ nation host and not any specified jump hosts. Use ~/.ssh/config to specify configuration for jump hosts. 大意就是连接 ssh 的过程中,可以指定(多个)跳板机,实现流量的一级一级转发,每一级跳板的系统都只有上一级的访问记录,达到一定的隐匿作用。参数值 destination 的语法与普通 ssh 连接对象的语法一致,指定多个跳板则将多个节点值以逗号分隔。 实验 为了呈现多级节点的效果,这里需再次用到网关机器(192.168.111.1),用其通过 ssh 连接 Centos(192.168.111.131),并以 Kali(192.168.111.128)作为跳板机,直接在网关上执行: ssh root@192.168.111.131 -J root@192.168.111.128 可以看到最终连接到的 Centos 机器的访问记录是跳板机 Kali 的 IP 地址: 再测试一下指定多个跳板: ssh root@192.168.111.131 -J root@192.168.111.128,root@192.168.111.131 这里为了简化网络拓扑,所以把 Centos 自身也设置为一个跳板,那么经过的两个跳转节点就是 .128 和 .131,整个流量流转过程可以简化为: *.111.1 --> *.111.128 --> *.111.131 --> *.111.131 将 -J 参数与前面提到的几种转发与代理参数结合,就实现了多级代理功能。

2022/7/17
articleCard.readMore

后记:菠菜站点的攻克之旅

故事发生在上次事件(https://knightyun.github.io/2021/09/04/exploit-take-down-swindle-website)之后,算是作为一个收尾,但也算是另一个开始 (⌐■_■),顺便记录下相关操作; 前情提要 上回故事说到,骗子服务器的最高权限虽然已经拿到,但这也只是技术层面的掌控,想要立案,需要提供尽量多的人员相关信息,如手机、银行卡等,但这些目前都并未采集到(前面虽然提到了某次源码有个银行账户,但后面发现那只是个测试号,百度出来一堆在用的…),所以还需要通过一些额外的手段去获取有用的信息; 信息收集 宝塔后台 首先想到的就是之前一直留着没进去的宝塔面板后台,里面应该会有些登录信息之类,但并没有得到登录密码,但这也并没有太大影响,因为现在可以直接访问宝塔的数据库文件(panel/data/default.db, sqlite数据库文件),所以直接进去备份个账户然后设置个密码,防止把正常账户挤下去: 清理下日志,然后就是愉快的登录进去 ㄟ( ▔, ▔ )ㄏ: 首先看到的就是账户名,想是管理员的手机号,这里看不全,去设置里面瞅瞅: 这里也是中间四位打了星号,从源码里也看不出,但这些也都是纸老虎,因为随后审查发现一处接口请求数据,返回信息里是完整的手机号,微信搜了下也有这个这么个账户: 但其真实性未知,多半只是个幌子,先记着吧; 新起点 在之后某个时间点准备继续收集信息的时候,发现其域名甚至IP都无法再访问了,后面几天试了也都不行,感觉可能是收割完一波然受卷款跑路了;自然,除了一些信息,之前获取的所有权限,都化作泡影了;也是在这之后,警察蜀黍竟主动联系了过来(没有喝茶,俺是良民 $_$),由于想着再碰碰运气看看,结果有趣的事再次发生了,访问之前那个IP展示出了这么个页面: 好吧得承认,那一瞬间确实差点信了这标题和图标,还浪费了三秒考虑渗透它的正当性,仔细想想也知道,真要是那家银行,怎么会把服务器放到有前科的IP身上,页面内容也说不过去,然后简单注册了个账号登录进去: 漏洞挖掘 端口服务 =_= 好吧,来都来了,就是个顺手的事;下面的分析也印证了上面的猜想,这也算是个IP反查域名 的小技巧,因为正常的工具比如 nslookup, dig 只能从域名到IP进行解析(某些有 ptr 的除外),但是遇到这种有使用 https 的站点,如果没有限制IP直接访问的话,能够正常进入页面,并且在浏览器左上角点击协议名还能查看所使用的证书,正常的证书的“颁发给”的值就是站点的域名,这里显然不是,应该是个临时或者测试证书: 然后反过来对域名分析一波: 这里发现通过公共DNS解析出来的IP,和前面用的好像对不上号,细想下,应该就是使用了 CDN服务,这些IP对应的服务器都是提供服务的三方机构的,渗透没太大意义,并且也不容易,这里辛亏是直接通过源站IP进入的,不然要通过域名和CDN检索出源站还是另一项头疼的活儿; 那么有了域名就来扫一波子域名,获取潜在的关联站: 还真有不少,先记着备用,随后从前台页面的返回数据里分析,发现这是一台使用 php 的 Linux 服务器,和之前的 Windows IIS 服务器不同,看来应该是域名被释放或转卖了,那么就从新扫波端口吧: 还是发现了一些熟悉的身影,依旧先后台跑着密码先吧,按以往的经验,遇到精明的主也许有漏网之鱼,所以还需要贴心的整套全端口扫描服务: 还确实有,看协议大概能猜到是什么服务,挨个试试,发现一个是 ssh 登录,一个宝塔后台: 宝塔依然有登录次数校验,爆破无望,只能先搁一边; 后台目录 这次都还没来得及用上目录爆破,手打了几个就盲猜了出来后台路径,倒也省工夫勒: 双赢?不存在的,只会是单方面的 ╮(╯_╰)╭,简单分析了会儿页面,这里就暂时不用祭出神器了,直接用 wfuzz 跑一波账户密码: 先放后台跑着,去其他地方转转;一会儿后就看到了结果,哟西!进去瞅瞅: 麻雀虽小五脏俱全,然后也有一些预料中的有意思的东西: 提现那个就不说了,能否成功全看管理员心情,下面那个看不太明白也没关系,可能道理大家也都懂,反正就那么个意思,你的命运我掌控,你的风险我操纵 (¬‿¬); Get webshell 后面花了点时间,分析了下页面找到一处可利用的漏洞,然后传个小玩意上去: 搞定,又到了亮剑的时刻: 往上遍历目录发现确实不简单,出现了之前的某些域名,难道是小站群,后面花了好一段时间才稍微搞懂他们的架构,多个二级域名指向当前IP,并拥有几个不同的CDN地址,然后某个二级域名又指向另一台服务器IP,当前IP的服务器呢又包含了另一个一级域名对应的二级域名,这几个站点又几乎在共享同一套代码,=_= 真够复杂的,难道是业务拓展导致的计划不周么,不过着也不重要,对应到服务器就行; 然后就是访问 /etc/passwd 文件查看用户,因为这个文件在 Linux 中是所有用户可访问: 都是些默认账户,由于当前是 www 账户,所以是没有访问 /etc/shadow 文件的权限的,这个文件记录的是系统所有账户的密码的 hash 值,所以后面步骤就是提权了; 在浏览系统目录的时候,发现了 phpMyAdmin 的登录地址,难怪之前没扫出,应该是宝塔里配置过,生成随机的复杂路径来校验访问入口: 账户和密码就简单了,可通过某些手段获取,只是可惜对方并不是配置的 root 账户,而是一个子账户,用站点命名,应该是服务器有多站点的缘故,权限也不高,先登录进去看下: 内部包含的是站点前后台的一些数据,先找下有用的信息: 有个表存的管理员的信息,额这个表名(bulamao)……难道又要开始考验我对博大的中国文化熟悉程度了吗,我认输了,有知道的小伙伴可以评论下;表里面有串密码的哈希值,先拿去查一下碰碰运气: 居然还真有,小本本记下(后面确实为进入其他站点提供了依据); 小插曲 浏览目录的时候也发现了一些精致的小玩意: 地方不大,还挺热闹,对面都是大佬惹不起,似乎还有好几拨人,让它们安静的躺着,相爱相杀吧,俺啥都没看见 (x_x); disable_functions 绕过 本来是准备打开虚拟终端愉快的研究如何提权的,结果开幕雷击: 不用说,多半一些系统执行函数被禁用了,就是 php 配置项中的 disable_functions 值,用于限制能在 php 脚本中执行系统命令的一些函数,当然也存在一些漏洞导致的绕过方法,途径不少,节约时间,就不挨个手工测试了,直接用现有的集成插件: 看到 putenv 被禁用心就凉了半截,光这就能劝退大部分绕过方式了,不过还是得试试,因为还剩一个方法可用(php-fpm),通过查看系统配置文件发现,fpm 模块使用的 socket 通信方式,配置一下然后启动: 最后提示都是成功,检查其生成的动态链接库文件也是成功上传的,但终端始终无法开启,一直提示返回数据为空,起始以为是插件用的某个函数也被 php 给禁用了导致返回为空,也找到不其他可利用的漏洞,为此还卡了大半周的时间,后来准备查查资料,手动把利用方法实现一遍; 其实该方法的原理大致就是:php 是一门动态语言,但 nginx 是无法处理这些的,所以中间还有个 fastcgi 协议在牵线搭桥,可类比 HTTP 协议,nginx 将接受到的客户端请求转换成 fastcgi 协议格式的数据,而 php 模块中的 php-fpm 就是用来处理这些 fastcgi 协议数据的,然后再传给 php 解释器去处理,完成后结果数据又以之前同样的路径返回到浏览器客户端;所以一般在 Linux 服务器上启动 php 程序,都会启动一个叫 php-fpm 的服务,一般会监听本机的 9000 端口,或者套接字文件,nginx 的配置文件 fastcgi 访问地址也配成这个端口或文件,这些都是为了完成上述通信过程; 这里面可利用的点就是,绕过通往 nginx 的请求,直接与 php-fpm 服务沟通,理想情况就是由于配置失误导致 9000 监听在了外网接口而不是本机接口,当然这种情况也是极少数,但这也并不意味着监听本机就无法利用了,在 php 程序文件可写的前提下,可以在程序中通过 curl 接口向服务器本机 9000 端口发起请求(或 stream_socket_client 发起套接字文件通信请求),并且是模仿 fastcgi 客户端发送对应格式的数据,这样就能实现绕过 nginx 直接与 php-fpm 沟通;这种操作还有另一种说法,叫 SSRF(Server-Side Request Forgery),即服务端请求伪造,通过服务器去实现访问客户端正常不能访问到的内网资源;当然还有一个和他名字很像的手段: CSRF(Cross-Site Request Forgery,跨站请求伪造),只不过这个是盗用其他客户端的登录凭证; 可能这里还有个问题,这样绕了一圈去建立通信,最后不是还是会通过 php-fpm 吗,这样配置的函数限制依然存在,其实不然,直接和 php-fpm 沟通的话,它是支持修改 php 配置的,就是 fastcgi 协议中的 PHP_VALUE, PHP_ADMIN_VALUE 这两个参数,比如可以设置这两个配置: "PHP_VALUE": "auto_prepend_file = php://input" "PHP_ADMIN_VALUE": "allow_url_include = On" 这会导致执行 php 程序之前包含 HTTP 中 POST 的数据,实现任意代码执行的目的,但即使这样也还是不行,因为这里的任意代码执行依然逃不开 php 配置文件的控制,所以就还需要更进一层,可以利用 extension 这个环境变量,设置执行脚本是要引入的动态链接库文件(Linux 下是 .so,Windows 下是 .dll): "PHP_VALUE": "extension = /xxx/xxx.so" 这就需要有任意文件上传权限,不过都开始研究限制绕过了,这点权限是肯定有的,然后就是编译构造自己的 .so 文件,并向其中添加要执行的系统命令,这样链接库文件在被引入的时候就会执行预定的命令,同时也不受 php 配置文件的限制;这个sao操作也是在研究那个插件代码时发现的 (¬‿¬),同时也通过抓包找到了之前一直返回空数据的原因: 原来插件一直在站点根目录读取配置的后门程序,之前为了掩人耳目是塞到了一个隐蔽的深层目录,所以一直获取不到数据返回空,也算是自己的配置失误,就是这里需要配置为后门程序所在目录: 远程指令执行 目前虽然可以通过插件绕过 disable_functions 并正常使用虚拟终端,但后续并不打算这么做,插件的机理其实就是通过包含 .so 时执行里面插入的指令,查看生成的 .so 文件可以看到插入了这样一条命令: 其实就是利用远程指令执行运行了另一个 php 服务,自定义了端口,并且 -n 参数是不使用 php 配置文件的意思,这样就实现了绕过 disable_functions,方便让其他程序畅通无阻的运行虚拟终端,手段确实有趣,不过这里就大可不必了,都能执行命令了还要虚拟终端干啥,另外经过测试也发现这样连接有一定时效性,大概一分钟左右就会断开连接,原因未知,所以为了后续更愉快的玩耍,直接上 msf payload,生成 elf-so 格式的文件后上传站点,然后把自定义的 fastcgi 客户端(https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75)参数改下,让包含的 .so 文件为我们的 payload 文件以使其运行: 运行后就会得到构造后的 fastcgi 协议数据(TCP数据流),让服务器 php 发送它就好,所以还需要服务端写个 php 程序来配合发送,这是使用使用套接字文件通信时的文件内容: <?php ini_set("display_errors", "On"); error_reporting(E_ALL); $fp = stream_socket_client("unix:///tmp/php-cgi-56.sock", $errno, $errstr, 30); $url = $_GET['url']; if (!$fp) { echo "Errno: " . "$errstr ($errno)<br />\n"; } else { try { fwrite($fp, base64_decode($url)); var_dump(fread($fp, 8192)); } catch (Exception $e) { print_r($e); } fclose($fp); } 如果是监听本地 9000 端口,可以使用 fsockopen 协议来发送 fastcgi 协议数据,具体就是: <?php ini_set("display_errors", "On"); error_reporting(E_ALL); $fp = fsockopen("127.0.0.1", 9000, $errno, $errstr, 30); $url = $_GET['url']; if (!$fp) { echo "Error: $errstr ($errno)<br />\n"; } else { try { fwrite($fp, base64_decode($_GET['url'])); var_dump(fread($fp, 8192)); } catch (Exception $e) { print_r($e); } fclose($fp); } 然后打开 msfconsole 开启反向连接监听: 然后本地用浏览器访问一下服务器中的发送 fastcgi 的 php 程序,并凭借上要发送的数据: 正常情况页面会持续加载中,然后 msf 这边就会收到连接请求并进入 meterpreter shell,然后看一下系统信息,nice,直接进入 shell 进行下一步操作: 因为之前说这里的 php 建立的连接只有不到一分钟时间,.so 文件执行的指令也不例外,所以这里就到了争分夺秒考验手速的时刻了,先断开连接待会重来,然后这次需要准备第二个 payload 传到同目录下,为了节省时间可以在第一次连接前以后台任务运行(run -j)meterpreter 反向监听,这样就不会浪费时间再去切换 module 和设置 payload 再运行了: 在网页请求后建立第一个 payload 连接,然后迅速进入 shell 执行一个 nohup 后台任务来运行刚才上传的第二个 payload: 不出意外第二个 payload 的监听任务就会建立连接,并且是持续的长连接,第一个连接关掉也无所谓,后面就能愉快地进行其他操作了 (ง •_•)ง Linux 提权(Privilege Escalation) 前面虽然已花了不少时间,但还没到达最关键的一步,现在才是重头戏,并且也不会太轻松,毕竟这是 Linux 系统,不同于 windows,版本和补丁数量不是特别高的话,提权漏洞一抓一把(这方面也稍微印证了 Linux 系统较于 Windows 更安全),少归少,不至于没有,只能逐个尝试; Sudo Baron Samedit(cve-2021-3156) 记得今年(2021)年初正好报了一个 Linux sudo 程序的提权漏洞,正好试下: 感觉有戏,下个 exp 上传手动跑下试试: 最后似乎不行,再用 msf exploit 试试: 等了十几分钟,最后依然失败,这条路应该是行不通了; Local Exploit Suggester 这里使用一下这款本地提权建议工具,它会自动获取相关系统信息并提供一些可利用的漏洞建议: 可以看到列举出了不少存在可能性的 exp,再去挨个尝试下,结果,也全部无效,心又凉了小半截; Suid 提权 简单说 suid 是一种权限,它运行具有该权限的文件执行时,能以该文件所有者的权限执行,例如具有所有用户读写执行的文件权限是 rwxrwxrwx,那么它再具有 suid 权限就会是 rwsrwxrwx (第三位是 s),权限码就是 4777,这里可利用的地方就是,假如某个文件所有者是 root 账户,并且其他用户可执行,那么其他用户在执行时,就间接有了 root 权限; 例如利用 find 提权执行 whoami 就是: find some-file -exec whoami 可以先查找一下全系统就有该权限特征的文件: find / -user root -perm -4000 -type f 2>/dev/null 虽然有不少但是没有找到可利用的,常见的可利用程序和利用参数大致有(执行高权限的文件或查看高权限文件内容): find cp nmap vim vi bash more less nano zip tar mv man chmod ash awk python perl tcpdump date time cpulimit crontab 任务 利用定时任务(cron)漏洞也是一种思路,假如存在会被定时执行的脚本文件,并且该文件又可写,就可以让其到达时间后执行任意命令;不过检查了一遍,都没有找到可利用的文件; 宝塔 CSRF 在一度没辙的时候,把方向转向了之前备份下载的站点源码,分析的时候发现有个日志输出目录,看着输出日志信息都比较详细,就尝试着全局搜一下请求数据之类的,看有没有账户密码或 cookies 数据,结果就找到了几处宝塔相关的登录凭证: 但这些凭证的时间都很靠前,应该都失效了,而且在用自己的宝塔服务测试时发现,出了会话 cookie 时效为 2 小时外,软件似乎也对 csrf 这类漏洞做了限制,比如某个账户在一台电脑登录后,直接拿到浏览器存储的 cookie 信息然后放到另一台电脑中去伪造访问,不仅伪造的机子无法登录,原登录的机子也会自动退出登录;不过在分析时发现了另外一处 cookie 信息,里面似乎包含了宝塔面板的一些信息: 拿去解码分析下,发现有个隐藏四位的手机号,应该是面板的登录账户名,先留着也许后面有用: mysql 提权 目前来看这条路希望不大,前面提到不同站点使用不同的 mysql 子账户,且分配的权限较低,连 dumpfile 这样的指令也执行不了,更不用说 UDF 这类提权操作了,也对 root 账户进行过密码爆破,都没有收获;想起之前不是拿到一个宝塔的手机号嘛,说不准会用手机作为账户密码,虽然隐藏四位,但无关紧要,总共才一万种可能,直接拿 crunch 生成一个字典然后拿去 hydra 跑一下 root 密码: 虽然跑完也没花多少时间,但结果证明一个也不是,猜想失败; Dirty Cow 后续没更多思路时,查了会资料,准备再试下经典的 Dirty Cow 提权漏洞: 然后尴尬的事就发生了,执行了一段时间 exp 后,服务器就突然报错断开了,然后 IP 就一直超时再也连不上了,这个 Kernel panic ,也就是 Linux 系统的致命错误,如果没记错的话无限近似于 Windows 系统中断或蓝屏(难道不经意间让 exp 升级为 DOS 了……),当然也可能是运气不好对方又关站跑路了,那么就先这样吧,指不定后面谁又接盘了 (T_T); C段嗅探 鉴于这次攻克过于棘手,所以过程中曾对机器的所在网段进行过扫描,然后不小心拿下一台业务类似的,和这次的目标在同一网关管辖下,所以先后台嗅探着数据吧,后面有时间再看看收获; 总结 惯例的总结时间,这个就没说啥的了,如果是诈骗是被动诱捕,那么这个就是主动投敌了,不要过分相信别人私下写的代码再呈现给你看的东西,即使它看起来是那样的真实,你所看到的只不过是对方想让你看到的罢了,对方既是规则的执行者也是制定者以及违背者。

2021/12/6
articleCard.readMore

记一次拿下网络诈骗者站点的全过程与套路分析

这是一则漫长又跌宕起伏的故事,小伙伴们请随意就坐,自备茶点;全文包含信息收集与攻克的详细全过程,以及对该类型诈骗思路的分析拆解,以提高防范意识; 0x00 梦的开始 那是一个阳光明媚的晌午,日常的搬砖过程中收到一封公司邮件, 看到这熟悉的措辞,又瞄了一眼下面的附件内容,熟悉的气息扑面而来,就顺手保存了下来; 随即管理员立马发现了不对劲,追发邮件说员工账号被盗用,不要轻信邮件内容,原始邮件也被标为垃圾邮件(上次的类似邮件删的太突然,事情还没开始就结束了,这次总跑不掉了( ̄_, ̄ ),作为当代好青年,五星好市民,是时候发扬一下活雷锋精神了); 而这张图片,就成了一切梦开始的地方…… 0x01 信息收集 0x001 审查域名 起始信息非常有限,开局一张图,剧情全靠猜,不过这个入口也足够了,先拿出家伙解析下二维码中的信息: 没有额外的数据,只有一串网页链接,看着这域名名称,嘴角微微上扬;先去解析一下域名: 到写文为止已经不能解析该域名了,整顿的倒挺快,不过好在之前有解析备份,域名万变不离其 IP,并且也没有发现使用 CDN,流量全部通往源站;顺手查了一下,是香港的服务器: 然后 whois 一下,搜集相关信息: 不出意外,又是用的三方注册机构,没有额外的有用信息,不过这个注册时间挺有意思,本月的,骗子同志动作还蛮快的;接下来只能去对方网站瞅瞅; 又是西部数码,看来有些备受青睐,网站提供隐私保护机制,注册信息不对外公开,暂时也获取不到有用信息; 0x002 审查 IP 现在唯一的线索就是之前解析的那个 IP 了,一步一步来,先 nmap 扫一波端口服务先,收集更多信息: 嗯还行,看见了几个熟悉的身影,继续走流程,分别跑下默认脚本分析下端口服务信息: 没有探测倒匿名 ftp,http 支持 TRACE,没有设 httponly,有执行 XSS 的机会,小本本先记下;然后老规矩,默认字典定向爆破一轮先,尝试还是要有的,万一那啥了呢: 剩余端口也都试了,意料之中,没啥收获,看来基本的密码强度意识还是有的;另外之前的扫描扫描中有出现 8888 这个端口,记得这个是服务器管理工具宝塔面板的默认后台入口,访问一下试试: 有入口校验,起码证明确实是用的宝塔面板,不过这个爆破应该是不可能去爆破的,记得入口 url 后缀默认大概是 8 位任意大小写字母与数字的组合,就是 62 的 8 次方,大约两百万亿,就挺秃然的,先放在后面再说吧,接下来,继续转战其他方向; 0x003 审查页面 来都来了,既然扫码是跳转页面链接,并且端口也开放了 80 和 443,当然要打开网页访问一下康康,同时开启开发者工具,看看有啥小动作: 哎呀,还识别机型,这靶向用户还挺明确,那就切成移动端看看: emm……怎么说呢,有那味儿了,咋一看还真看不出来,模仿的还挺全(不过胆也确实大,政府网站都搞),然后看了下接口返回的头信息,发现使用的 Windows IIS 7.5 + ASP.NET 的服务: 这个先记着,后面漏洞挖掘用得着,后面试了一圈发现页面都是空壳,只有那个办理入口的弹窗能跳转,跳转页面是: 描述的还挺全,好让大家都能对号入座,这里点下立即申请: 然后就开始了个人信息收集的一条龙服务,先是姓名和身份证号,另外,注意看旁边显示加载的 PNG 头图的名称,额难道这是开发者的疯狂暗示??这里随便输入条信息进去看看: 居然还有校验,打个断点看看源码逻辑: 一个号码校验都整的这么齐全,还真是费心了,不过前端的校验都是纸老虎,这里也不用什么偏方了,直接用开发者工具的源码编辑覆盖功能,直接给校验函数返回 true: 然后校验通过,进入下一个步骤: 这里是要收集银行卡号和密码以及绑定的手机号,唉,意图很明显,哪有打款转账还要对方密码的操作,这里也准备随便填下,银行卡校验用同样的方法绕过一下,不过在其中一个加载的脚本文件中发现了有意思的东西: 开发者连源码中的调试数据都不删除,调的阿里支付的接口,正好借对方调试账号用一下下(再次证明了作为开发者,生产环境中代码移除注释的重要性=_=): 然后进入了下一个页面,再次收集姓名和证件号,以及银行卡余额(这里应该是对用户真实情况进行摸底以及其他未知操作),下意识查了自己的余额,唉果然这里连撒谎的勇气都没有 T_T,马上填完进入下一步吧: 然后页面会一直加载不断刷新,再无其他跳转,应该是给诈骗者提供操作的时间,那么网页的相关操作就暂时告一段落,大致了解了一些操作步骤,接下来探索一下其他方向; 0x02 漏洞挖掘 0x001 SQL 注入 信息收集差不多了,现在来逐个击破,先从最熟悉的网页端入手,前面审查页面时有不少提交表单和输入框,这些都是潜在的攻破点;挖掘技术哪家强,先祭出神器 Burp,拦截一下之前提交银行卡号密码的表单数据: 然后对字段值进行简单注入尝试,检查报错信息: 没反应,应该是有基础校验,再换个: 有反应,似乎看到了希望,虽然返回乱码了,应该是对方程序处理问题,不过看句式像是 SQL 报错,又接连试了几个,也是同样的返回,那么剩下的冗杂工作,就交给工具进行吧,掏出 sqlmap 跑一波: 后面换了几轮参数也没成功,应该是过滤机制比较周到,然后在测试另一个页面时,才发现了那段报错信息的原本含义: 嗯还是太年轻,会错意了,应该是程序识别到了字段值中的 SQL 关键字;另外回想起之前扫描到服务可能隐含 TRACE 相关漏洞,测试了下,应该是服务端暂未支持: 然后又想了想,针对密码字段进行数据库表字段设计时,应该会考虑其字符位数低的特点,减少空间占用,因为这是银行卡密码,都知道是 6 位数字,这里传个大数看看有没有惊喜: 尴尬,是有惊无喜,应该是没有特别处理,直接反馈为服务端错误;后面又陆续换了几个页面,测试下来都没太大收获,场面又一度陷入僵局,只能又暂时转移战场; 0x002 Metasploit 渗透 终于到 Metasploit 出场了,蓄势待发, 先搜一下 IIS 的已知漏洞: 有不少,那就先调几个条件匹配的试一波,这里只放个示例,就不一一展示了: 然后就是其他几个端口、服务,挨个测试一遍,也没有什么突破,看来补丁都打的挺全;目前又暂时陷入了死胡同,虽然 msf 还有不少模块可以利用,不过暂时不准备继续深入试探了,因为想起来还有另外一件重要的事情还没做; 0x003 站点目录枚举 站点目录扫描,这件重要的事怎么能少得了,可供选择的工具很多,如 dirbuster 等,这里我们使用 Burp Suite 的 Engagement tools 里的 Discovery content 工具,进行目录爆破: 其内置的大量字典已经足够使用了,不过涉及网络请求,这个过程也是异常漫长,不过可以后台跑着,不影响做其他事,这里就直接贴一个扫描结果: 直呼好家伙!不扫不知道,出来吓一跳,居然错过了这么多隐藏入口,小本本记下先,后续挨个探索,不过呢视线情不自禁地锁定到了名为 upload.asp 这个文件上,开发者这么明显的暗示就无需我多言了(ㄟ( ▔, ▔ )ㄏ); 直接访问没什么返回数据,难道是方法不对?换成 POST 表单文件数据再试试: 看来这样上传也没用,也许还要额外的校验参数之类的,上面不是刚扫了一堆没见过的页面嘛,现在回过头去挨个分析下页面源码看看,也许会有收获: 果然,在其中一个页面中,发现了调用这个上传接口的表单,是个隐藏元素,结合页面内容,应该是用于收集用户上传的某些证件信息,身份证照片之类的,然后看了对应的 js 源码,果然存在一些校验和接口参数: 这里去分析调用这些函数再上传文件太费劲了,这不是个隐藏表单嘛,直接改下代码通过 UI 操作多轻松 (¬‿¬): 样子虽然简陋了点,管他的,能跑起来就行了,上传个文件试试: 然后再访问看看效果: 好家伙,大写的激动!嘴角再度微微上扬,不过先冷静下,再试试有没有文件类型校验,服务是 ASP.NET 的,那就简单传个 asp 程序试试,下面的代码会在页面输出站点运行服务的名称: <% response.write(Request.ServerVariables("SERVER_SOFTWARE")) %> 然后上传上去看看: 这……俺还能说什么呢,此时无声胜有声 $_$ ,不过这并不是终点,这只是一个良好的起点,一切才刚刚开始 (¬‿¬); 0x004 意外收获 其实网站目录中还有一个很让人在意,就是叫 jieliuzi 这个目录,虽然摸透了对象开发者喜欢汉语拼音命名的习惯,但是这个含义始终未能参透,连蒙带猜外加输入法都未得其解,甚至后面进去一探究竟后没没想明白 =_=,中华文化真是博大精深;不管了,看看页面访问结果: 是个登录页,十分简洁,而且这其实是个 PC 站点页,骗子在某些方面也挺有情调的,这里不作展示是由于整图过于刺激(怎么说呢,额,这登录框真白),怕过不了审;另外,注意一下这个网页最上面的标题名,第一反应就是应该不会是简单的字面意思,听着也不像啥好词儿,为此专门去百度了一下: em……又长见识了,原来诈骗也能是一门学问,再结合最开始那张二维码的来源(企业邮件),看来是这么个意思了,大家平时也都注意警惕一下; 随后试了下登录,没有验证码或超时超次数验证这些,并且也没挖掘到可利用的 SQL 注入点,这种情况就该 Burp 的 Intruder 出场了,后台登录名一般都是 admin,密码就祭出 rockyou.txt 跑一波先: 这也是一个比较漫长的过程,不过一会儿之后再去看的时候就发现了变化: 需要留心的就是这里的返回数据长度,因为正常讲大部分试的密码都是错误的,服务器响应的数据大小也都是一样的,突然出现不一样的长度多半就出现了转机,这里看来是识别出了 sql 语句,和之前一样报错,然后看最上面那条: 整个列表中就这一条最特别,看返回也是 302 重定向,看来应该是密码对了跳转进页面了,然后看这密码,也是够随意的,看着简单,猜的时候谁又能想到呢,登录进去看看: ……,唉,怎么说呢,应该是触目惊心吧,虽然数量没有一些媒体平台时不时报道那些的那么夸张,但也不是一个小数目了,而且每一处记录的银行卡账号、密码、身份证号、手机号、验证码、IP位置这些,都是近乎真实的;我不是正义的制裁者,也没有多少执行正义的力量,最多就热心的帮他们删个库再跑路,个人的力量太小了,所以这些还是都后面交给警察蜀黍处理吧,正义可能会迟到,但不会缺席。 0x03 获取权限 0x001 Get webshell 暂时先抛开题外话,上面进行到了文件上传这一步,文件上传可以意味着很多事情,可执行文件的上传便可以实现获取服务器的系统操作权限,当然这里的网页应用程序的相关权限一版很低,提权的事就是后话了;当前任务是拿下网站操作权限,即 webshell,继续走流程,先上传个简单的 asp 版一句话木马: <% execute(request("pass")) %> 然后就该 亮剑 了: 搞定,进去康康: 文件相关操作: 数据库相关操作(对应之前后台页面的展示数据): 命令行终端相关操作: 甚至获取 Windows 系统信息: 好了,这不啥都有了,还要啥自行车,但也不能止步于此,虽然现在拥有了对这整个站点的操作权限,包括文件及数据库的增删改查等,但也不至于为了正义用爱发电,每日蹲点删库删站,我们的目标还在山的那边 (ง •_•)ง 0x002 Backdoor 俗话讲,要生于忧患,虽然现在入口打通了,但是凡事都要留一手(我更喜欢留多手),防止木马那天被管理员察觉并清理就尴尬了,所以找几个隐蔽的位置,再上传几个,比如平时几乎不被注意的 css 或图片等静态资源文件夹,js 库文件等,文件命名上也可以花点心思,模仿已有文件或者配置文件之类的,使人有一种一眼看上去是正常文件的幻觉(+_+); 当然,还可以利用一种技术,简单讲就是 Windows 中我们熟知的隐藏文件,一般一些软件或系统的配置文件会通过这样的方式隐藏,防止普通用户误删改,查看也能简单,像下面这样勾选一些就都出来了: 所以我们这里可以通过 shell 命令把指定文件隐藏掉,使得其不会被轻易发现: 0x003 Get shell 目前我们获取到的还只是 webshell,虽然可以模拟执行终端命令,但是这些都是通过上传的 asp 后门程序执行的,就是说每步操作都会发起 http 请求,防止被平台记录,需要获取系统 shell,即上传系统可执行文件(如 .exe 文件,就是俗称的木马),至于如何生成,就需要 metasploit 再次登场了; 这里又涉及一些东西,getshell 的木马一般有两种,正向连接和反向连接,基础的认知可能是将程序上传至机器,然后运行就会后台监听某个端口,等待外部的连接,连上后就可以通过两端的交互达到控制系统,当然前提是系统防火墙没开或者允许该端口放行,而通常服务器都会开启防火墙并过滤端口,只放行指定的几个; 所以后面出现了第二种类型,反向连接,或者叫反弹 shell,简单讲就是第一种类型反转一下,不是目标机器等待我们连,而是我们外部的机器等待目标机器来连接,这解决了防火墙端口的限制,因为防火墙一般不会特意去限制出站端口,当然这种类型又增加了额外的限制,首先我们需要一台具有公网 IP 的机器,像平常这种连着 WiFi, 深埋于 n 层路由下的设备,基本可以放弃了(使用端口转发也需要运营商提供公网 IP),另外就是需要目前机器可访问公网,因为有些服务器考虑安全因为是在内外下的,用反向代理等方式传输流量,无法访问外网; 由于不清楚对方防火墙情况,先用反向连接看看,手边暂时没有公网机器,就先用 ngrok 转发一下端口流量,先启动服务: 从之前收集的数据中得知,对方机器是 x64 Windows 系统,那么这里就直接用 msfvenom 生成对应的 payload(暂时先不考虑加密加壳这些,传上去看看是否被杀毒再说): reverse_tcp 是反向连接,bind_tcp 是正向连接,连接地址填 ngrok 提供的链接,端口填链接对应的,然后生成执行文件,最后上传到对方站点某个隐蔽的目录下: 然后在 msf 中启动监听程序,等待对方连接,地址填本地,端口填之前 ngrok 中设定的本地端口,使用的 pyaload 要和之前 msfvenom 中配置的一致,不然会连接失败;完成后,一旦目标上的程序启动,首先就会访问 ngrok 域名和对应端口,ngrok 服务再把传输数据转发到本地的监听端口中,数据就通过这一层中介进行双向传输: 然后在 webshell 中执行它: 运行成功,这是切换回 msf 一会后就会有所反应,表示连接成功: 现在再查看 ngrok 服务就回显示有一条连接,表示可以进行通信了: 现在连接成功进入的操作界面是 meterpreter,这是 metasploit 提供的有许多拓展功能的集成终端,类似于 cmd、shell,但是功能更强大,可以执行切换目录、获取系统信息、操作进程、上传下载文件、进一步渗透等操作,甚至在拿下 system 权限后,还能解锁控制鼠标键盘、屏幕截图或实时预览等高级功能,这里先查询下基础信息: 和之前的虚拟终端一样,当前操作用户是 IIS,当然这里想用 cmd 终端操作也是可以的,执行下 shell 命令进入系统 shell,操作和命令与系统命令提示符没有两样: 初次进入可能会出现中文字符乱码情况,因为 cmd 默认使用的字符集问题,手动切换成 UTF-8 的 65001 就行了,可能唯一不太方便的地方,就是没有集成历史记录的功能,要重试上个命令得全部重打一遍,毕竟是网络转发数据,不应要求太多: 0x004 Get system 如上面所讲,目前获取到的只是普通账户的 shell,虽然有一定的终端操作权限,但还是远远不够的,会有诸多限制,比如无法操作一些服务、增删系统账户、访问系统目录等等,那么接下来的操作就是 shell 提权了,或者叫 getsystem,这也是 meterpreter 中的一条命令,功能和描述一样,会尝试多种漏洞进行权限提升以获取 SYSTEM 权限,只不过试了下似乎都没有成功,只能另寻他法: 这里值得说的是,可能有小伙伴会有疑惑,认知中 Windows 系统中权限最高的不应该是超级管理员 Administrator 吗,为什么一直在强调获取 SYSTEM 账户权限,其实严格来讲,Administrator 账户虽然成为管理员,其实权限并不是最高的,也有很多它做不了的事,举个简单的例子就是删除系统账户 Guest: 以及其他一些系统层面的事,administrator 也做不到,而这些高层次的操作都由 SYSTEM 账户来完成,虽然在账户设置里面从没见过它,但它是确确实实存在的: 就像网站服务其实也有个对应的 www 账户一样,应该没人曾经登录过它吧; 前面讲获取 system 权限使用 getsystem 不奏效,那么就继续试探一下其他路子,也就是令牌窃取,令牌指的登录令牌,类似登录网站要用的 cookies 或者 token,而 Windows 中存在两种令牌:Delegation Tokens(授权令牌) 和 Impersonation Tokens(模拟令牌),前者用于交互式的登录,如直接或使用远程桌面,输入账号密码登录进系统,而后者用于非交互式的会话中,访问网络驱动或其他域登录脚本程序; 这里要利用的正是模拟令牌,窃取的过程有些类似网站里的 cookies 窃取,由于令牌在重启系统之前,会一直被保留,所以有账户登陆过的话,其令牌有机会被冒充使用,而且有授权令牌登录的账户注销的话,令牌也会转化为模拟令牌,同时其原本的相关权力都会保留; 这里接下来就先使用 incognito(伪装、隐瞒的意思,偷盗者总是会先隐藏起来嘛)插件,进行令牌盗取,再用 list_token 命令查询当前能获取的令牌情况(数量因当前的 shell 权限而定): 这里可以看到可盗取令牌的账户,要窃取的话就使用 impersonate_token 命令: 虽然可以窃取成功,但试了下这个账户的权限也不高,尝试性的试了下 system 账户也失败: 所以只能再尝试其他提权漏洞了,这里就先用 bg 命令把当前 meterpreter 会话切换至后台,回到 msf 界面,使用一个已知的 Rotten Potato 提权漏洞: 需要的唯一参数是会话 id,正好可以利用刚才的 meterpreter 会话,然后还需要设置一个 payload,这里之前有了解过,目前机器可以访问公网,而且虽然有使用防火墙过滤端口,但是似乎一直开着这么一群端口: 这是远程 RPC 需要使用的一系列端口,所以盲猜机器会开放 49150-49160 这么一个范围的端口,那么就见缝插针,选一个没被占用的用于开启监听,这样就能直接使用正向连接了,而不用麻烦的再次使用反向连接: 地址填目标机器的地址,这样运行后就能自动连接到目标机器的监听端口上,这里先运行一下试试能不能成功: 哟西!竟然比想象中的顺利,就不多解释了,检查一下权限和令牌先: Nice,成过窃取到了 system 账户的令牌,以对方身份登录成过,接下来就可以试试权限了: 进入 shell 后账户确实变成了 SYSTEM,也能增加或删除账户了,那么至此,成过拿下系统最高权限,成功 getsystem; 0x005 权限测试 回想起前面有说过,高权限可以获取屏幕截图或者实式预览,那么这里来就看一下下: 成功截图到了指定文件,打开看看: 虽然分辨率有点低,但大致是个登录界面的模样,然后实时预览看看,由于这个要启动图形服务,就在虚拟机里跑一下: 大晚上的,应该没啥人登录,,不过光是预览还不够,现在是可以直接远程登录进去的,只是需要额外的一些操作,先建立一个账户并分配管理员权限(即加入 Administrators 用户组),名字同样可以取得有一定迷惑性: 不过光是这样还仍没有远程登录的权限,需要将用户分配进远程桌面用户组才具有登录权限: 然后这里本来是要开启远程桌面服务的(TermService),结果查询发现是已经开启的,然后一路追踪到了服务对应的进程,进程对应的端口: 居然默认端口从 3386 改成了 10086,有意思,之前端口扫描默认常用端口,难怪没有扫出来,既然现在用户和密码都有了,登录进去看看: 唉,都舍不得掏钱整台配置好一点的,一点都不懂得投资 ㄟ( ▔, ▔ )ㄏ,没意思,擦擦屁股溜了溜了(~ ̄▽ ̄)~; 0x005 System Backdoor 老规矩,任何阶段都需要留一手,并且现在 system 权限,因此只要留下后门那么再次连接就直接具有 system 权限,那么话不多说直接上传,文件名同样可以取得有一定迷惑性: 然后配置程序开机启动: 当然,要想再留一手就可以再创建个系统服务,只是生成的 payload 要用 service 类型的,不然会启动失败: 然后上传后在目标机器创建服务,配置开机自启,也可以再额外配置启动失败后自动重启服务,或者最近调用执行其他程序,最后再手动启动一次服务: 至此,大部分流程基本结束,拥有系统权限可以做更多的拓展渗透,这些都是后话; 0x04 套路分析 0x001 作案工具 目前为止一共收集到两个后台,一个宝塔面板和一个捕鲸系统后台,宝塔之前是由于有登录校验,没有获取到登录入口,现在就轻松了,用宝塔自带命令 bt default 查看一下默认入口和账号信息: 额……者用户名难道是要暗示自己输得起?待会用这个账号和密码登进去看看,另外在查看目录时,又发现了有意思的部分: 这里还列出了不少站点,难道现在诈骗也搞分布式作案了 ( $ _ $ ),或者微诈骗?算了后面有机会再去一锅端了,先登录进去看看: 看了默认密码被换调了,虽然现在有权限能直接改掉密码,为了不打草惊蛇,就先放一放,反正进去也都是一些可视化服务器配置,毕竟现在服务器的系统权限都攥在手上呢,不必着急,主要先分析下另外一套系统,那才是骗子们的主要作案工具; 可以观察页面存在两列功能栏,分别罗列着骗子能远程执行的操作: 两列功能类似,只有个别区别,这里贴个图对比一下,感觉功能 2 应该是 1 的升级版,多加了几个功能,不过后面发现并不是这么简单,二者都能使用,这样是为了方便骗子连环套取用户信息: 0x002 诈骗流程 最前面的捕鲸邮件这些就不说了,就是邮箱盗号后广发邮件撒网罢了,现在主要来看下上面的一堆功能项,为了行骗过程操作方便,大部分功能应该都是字面意思,还记得最开始审查页面的时候,最后是停在了一个加载页上面,那么应该就是衔接这里的操作的,不同的功能项会触发不同的返回结果: 这里空讲可能没什么概念,功能汇总有十几二十 个,就挨个给大家实际演示一遍,从骗子的视角过一遍大致就能体会了,还是用之前的表单数据提交,然后这部触发不同的功能后回去看结果: “未通过” 未通过就是上面的加载页面,是默认状态,也是处理不通过返回之前的中转页面; “通过–杀它” 标记通过后会跳转提交验证码的页面,这里获取验证码只是个幌子,真实情况是骗子在向银行发起请求获取验证码,然后骗取用户提交: “验证码超时失效,请返回重新获取(不显示金额)” 如果你这边操作慢了,导致骗子那边输入超时,就会提示让你重新操作获取提交一次(想的真周到=_=): “通过=【显示余额】” 只是不是有提供余额信息嘛,这里就提供用户确认再次骗取二维码(让人觉得像是银行正常查询出来的): “余额查询” 如果骗子那边执行什么操作核实到金额不对时,就是提示用户重新填写正确金额: “取款密码错误” 如果发现取款密码不对,也会重新让用户提供: “手机号码错误” 然后是手机号: “银行卡号错误” 银行卡号: “身份证号错误” 姓名和身份证号: “审核通过” 然后是审核通过,让你耐心等待结果(等待骗子把钱套走跑路 $_$): “不支持,请更换名下其他银行认证” 如果发现某些银行卡不太好利用,就会提示更换: “换【信用卡】” 换信用卡: “换【储蓄卡】” 换储蓄卡,反正对方为了达到目的,会反复榨干你的每一处积蓄,比大多数服务业都贴心…… “有效期和后三位/错误” 然后这个是信用卡相关的信息,和密码一样重要,也要注意不要轻易透露出去: “【建设银行】==获取授权码” 使用建设银行应该是需要额外的授权码,骗子也会想办法搞到手: “【网银密码】” 如果存在网银,也会骗取登录密码进行远程套现: “请保持银行卡内余额大于5000验证” 下面的就有意思了,如果骗子觉得你卡内金额太少(穷),就会提示你再多搞点过来: “请保持银行卡内余额大于10000验证” 或者骗子心情好,来个狮子大开口: “验证码超时失效,请返回重新获取(显示余额)” 然后这是验证码超时提示金额,和之前差不多: “只支持工、平、浦、招、光储蓄卡” 这里本来应该是提示支持指定银行,不过……看来天下开发者总是有不同的环境,类似的处境,唉,同情那位仁兄3秒: “下载 apk 拦截马” 这里也一样,本来该骗用户下载安卓木马的,但似乎也还没准备好: “联系在线客服” 一样: “信息不一致,请返回从新填写正确的预留信息” 察觉用户输入信息不一致,重新填写: “扣款–打枪” 这里也有意思,基本就是骗子已经从用户手上套现了,为了安抚民心,整这一大段话提示用户这是流程的基本操作,以及后续会返还之类的(信你个gui): “【先回复打字,再设置此功能键】” 然后这是最后一项了,大致就是骗子自己编话术,然后返回提示用户: 0x05 结语 正道的光可以迟到,但不能缺席,由于目前所收集的诈骗团伙相关信息还并不充分,后续会想办法陆陆续续收集,再交由警方处理 最后呢,其实也并没有什么额外要说的,因为防骗或安全意识这些大道理,国家政府再到媒体个人,每天都在强调,稍微留心一下就行了,从这整个过程看来(攻克过程不重要,可以忽略),贬义的讲,骗子考虑业务场景还挺周到的,也有不少地方在利用人的弱点,就比如之前图中那些接近官方的环境和话语,大家还是得注意分辨; 然后就是最最重要的,因为那封邮件是一切的开端,注意提高账户密保和安全性是一方面,比如加个登录二次验证什么的,另一方面就是不要轻信任何信息,涉及链接或二维码这些的,即使来自身边熟悉的人,因为他们可能也只是受害者链上的一个节点而已。 另外,额外宣传一波 (¬‿¬),国家出台的反诈中心APP,除了风险检测与线上举报外,更有相当多的真实案例公布更新,可下载备用。

2021/9/4
articleCard.readMore

CSS flex 盒子在 Chrome 和 Safari 中的行为差异

问题 最近的开发中有遇到一个页面样式的兼容性问题,大致是使用 flex 布局的两个嵌套弹性盒子,在 Chrome 和 Safari 中对一些特殊情况的处理行为不一致,从而产生了测试 bug; 复现 下面将问题简化为了一个 demo 模型,一个定高 300px 的 flex 盒子 A(红色边框),嵌套了另一个高度被子元素(绿色块)撑开的 flex 盒子 B(蓝色边框),其中盒子 B 为垂直伸缩 flex-direction: column;,Footer 元素(绿色边框)高度固定 100px,背景半透明;代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Demo</title> <style> .flex-box-a { display: flex; width: 300px; height: 300px; flex-direction: column; border: 2px solid red; } .flex-box-b { display: flex; width: 200px; height: auto; border: 2px solid blue; } .inner-green-box { width: 100px; height: 100px; background: green; } .inner-aqua-box { width: 100px; height: 100px; background: aqua; } .footer-box { width: 200px; height: 100px; border: 2px solid lime; background: skyblue; opacity: .5; } .bottom-brown-box { width: 304px; height: 100px; color: white; background: burlywood; } </style> </head> <body> Flex Box A - height: 300px; <div class="flex-box-a"> Flex Box B - height: auto; <div class="flex-box-b"> <div class="inner-green-box">height: 100px;</div> <div class="inner-aqua-box"></div> </div> <div class="footer-box">Footer - height: 100px;</div> </div> <div class="bottom-brown-box"> height: 100px; </div> </body> </html> 效果: 然后增加绿色块子元素的高度,可以发现在 Chrome 中 flex 盒子 B 的高度会被该子元素撑开,Footer 元素高度被压缩,而 Safari 中的盒子 B 的高度并没有被子元素撑开,而是超出部分被隐藏在了 Footer 下层,同时 Footer 元素也被少量压缩: 当绿色块子元素高度超过最外层 flex 盒子 A 的高度时,Chrome 中的内层 flex 盒子 B 继续被撑开,Footer 元素直接脱离外层 flex 盒子,而 Safari 中只有子元素超出外层 flex 盒子,Footer 依然只被少量压缩: 解决 最终解决问题的方式是放开外层 flex 盒子 A 的高度限制,这样无论在 Chrome 还是 Safari 中 flex 盒子都能被自由撑开,行为表现一致: 因此开发中遇到类似定高与弹性并存的情况,需要合理设计布局,避免不必要的兼容问题;

2021/7/5
articleCard.readMore

React 组件性能优化之 PureComponent 的使用

在 React 类组件中,如果状态(state)发生变化,便会触发组件的重新渲染(执行 render 方法),并且是包括所有子组件在内的全部重渲染,无论某些子组件是否有用到 state 中的值;但有些时候部分子组件计算或渲染工作量较大,并且只做一些情况单一的展示工作,那么在更新状态时对其的渲染,便是额外的性能负担,所以需要寻求一些优化手段; shouldComponentUpdate shouldComponentUpdate 是 React 的生命周期函数之一,它会在每次渲染(render)之前被调用,并且根据该函数的返回值(true/false)来决定是否调用渲染函数(return true 触发渲染,return false 阻止渲染),但是组件的首次渲染或者调用 forceUpdate() 方法时不会触发调用 shouldComponentUpdate 方法;该生命周期函数的默认行为是在每次 state 发生变化时触发重新渲染,如果自行声明该函数会覆盖这一默认行为,需要自行判断 state 的变化以决定是否重新渲染; shouldComponentUpdate 方法接收两个传参:(nextProps, nextState),分别表示变化后的 props(组件的参数) 和 state(组件的状态); class MyComponent extends React.Component { state = { count: 0 }; shouldComponentUpdate(nextProps, nextState) { // 无需手动更新 state 值,组件会自动更新 // this.setState({ ...nextState }); if (nextState.count <= 3) { // count 值大于 3 后,组件便不再更新 return true; } else { return false; } } render() { const { count } = this.state; return ( <button onClick={() => this.setState({ count: count + 1 })}> {count} </button> ); } } PureComponent React.PureComponent 类似于我们常用的 React.Component,区别在于 PureComponent 的内置 shouldComponentUpdate 逻辑,它会同时对 props 和 state 的变化前和变化后的值进行浅对比,如果都没发生变化则会跳过重渲染,相当于多了一层 props 对比;下面通过一个简单的例子来对比这两种组件的效果差异; 效果对比 假设有一个计数器,点击按钮增加计数,并用两种组件渲染计数值: class Counter extends React.Component { state = { count: 0 }; render() { const { count } = this.state; return ( <div style=> <div>count: {count}</div> <CountText count={count > 2 ? count : 0} /> <ConstText count={count > 2 ? count : 0} /> <button onClick={() => this.setState({ count: count + 1 })}>Add</button> </div> ); } } // 普通组件 class CountText extends React.Component { render() { const { count } = this.props; console.log('normal rendered', count); return <div>normal: {count}</div>; } } // “纯”组件 class ConstText extends React.PureComponent { render() { const { count } = this.props; console.log('pure rendered', count); return <div>pure: {count}</div>; } } 初次渲染的效果图与输出如下,页面初始化时普通组件与纯组件都会进行一次渲染: 第一次和第二次点击按钮后,可以看到都只有普通组件触发了渲染,即使组件每次接收的 props 值 count 都没有发生变化: 在第三次点击按钮后,由于 props 的传入值发生了改变,因此纯组件也触发了重渲染,页面内容正常更新: 子组件更新问题 可以看到 PureComponent 确实可以在传入 props 值没有变化时避免重新渲染,在一些场景下优化性能,但是这也是使用 PureComponent 的一个前提,即需要组件在相同 props 传入值的情况下总会有相同的渲染内容,也就是纯组件中 Pure 的含义所在,它有些类似纯函数的定义(传入相同的参数执行后,总会得到相同的返回值); 从另一个方面来说,就是 PureComponent 跳过渲染时,它的所有子组件也会跳过渲染,即使子组件应被更新,所以需要保证纯组件的所有子组件也都是纯组件;举个例子,下面的纯组件包含一个展示当前时间的子组件: class Counter extends React.Component { state = { count: 0 }; render() { const { count } = this.state; return ( <div style=> <div>count: {count}</div> <ConstText count={count > 2 ? count : 0} /> <button onClick={() => this.setState({ count: count + 1 })}>Add</button> </div> ); } } // “纯”组件 class ConstText extends React.PureComponent { render() { const { count } = this.props; const d = new Date(); const time = `${d.getHours()}: ${d.getMinutes()}: ${d.getSeconds()}`; console.log('pure rendered', count); return ( <div> pure: {count} <ConstChild time={time} /> </div> ); } } // 展示时间的子组件 class ConstChild extends React.Component { render() { const { time } = this.props; console.log('child rendered', time); return <div>{time}</div> } } 页面初始化时: 前两次点击按钮后: 此时纯组件和其子组件都未触发更新,在第三次点击后,才同时触发更新: 浅对比问题 最开始有提到 PureComponent 是对 props 的变化前后的值进行浅对比来决定是否重渲染组件,实际上就是对每个 props 值进行基本的值对比,如果值类型是复杂类型,如引用类型(对象),并不会深入遍历每个属性的变化,下面改造一下上面的示例,让传入纯组件的 props 变成一个引用对象: class Counter extends React.Component { state = { count: 0 }; obj = { num: [0] }; handleAdd() { const newCount = this.state.count + 1; this.setState({ count: newCount }); this.obj.num[0] = newCount; } render() { const { count } = this.state; console.log('Counter rendered', count, JSON.stringify(this.obj)); return ( <div style=> <div>count: {count}</div> <ConstText count={this.obj} /> <button onClick={() => this.handleAdd()}>Add</button> </div> ); } } class ConstText extends React.PureComponent { render() { const { count: { num: [count] } } = this.props; console.log('pure rendered', count); return <div>pure: {count}</div>; } } 首次初始化后的结果: 依次点击三次后的结果: 可以看到其实每次传入纯组件的 props 的实际值都有发生变化,但是由于是引用类型,所以组件并没有识别到这一变化,永远跳过了组件更新;因此如果遇到复杂数据结构的情况,尽量使用 state,因为 state 由于自身的特性和规则,每次变化后的值都是一个全新的值; 当然,还有一种特殊情况,如果在组件中使用了 Render prop 或类似的形式,即 props 的值是一个返回某个值的函数,如: <Counter count={() => 3} /> 那么即使每次函数实际执行的值都是相同的,也都会触发渲染,因为这个函数本身每次都会被判断为一个新值,使得性能优化失效; React.memo React.memo 是一个类似 PureComponent 的高阶组件,只不过它用于函数组件,而 PureComponent 用于类(class)组件,但二者的实际展示与优化效果是一致的,下面是两种组件形式的写法: // 类组件 class ConstText1 extends React.PureComponent { render() { const { count } = this.props; console.log('pure rendered', count); return <div>pure: {count}</div>; } } // 函数组件 const ConstText2 = React.memo(function(props) { const { count } = props; console.log('const rendered', count); return <div>{count}</div> });

2021/5/9
articleCard.readMore

TypeScript 之泛型

背景 泛型用于创建可复用的支持多种类型的组件,比如不仅能支持当前的类型,还能支持未来的类型,为大型系统的构建提供一定灵活性,泛有广泛、多种的意思,即泛型可实现对多种类型的支持;泛型是一种已有的概念,除了 TypeScript,同样也存在于其他多种语言中; 先举一个基本的例子,ts 中实现一个加法运算的函数,可以是这样的: function addFn(arg1: number, arg2: number): number { return arg1 + arg2; } addFn(1, 2); 如果后期功能拓展,需要上述函数也具备拼接字符串的功能,即: function addFn(arg1: string, arg2: string): string { return arg1 + arg2; } addFn('a', 'b'); 但是这样的申明并不与上面已有的申明兼容,即使使用联合类型处理也比较复杂,但是它们的处理逻辑却是一样的,只是类型值换了,重写一个新函数也不符合拓展的初衷,所以需要寻求其他方法; 函数重载 在 ts 中重载是为一个函数提供多个类型定义的操作,使得函数可以根据传参的不同而拥有不同的返回类型;这样,我们就能轻松实现之前例子的拓展需求: function addFn(arg1: string, arg2: string): string; function addFn(arg1: number, arg2: number): number; function addFn(arg1: any, arg2: any): any { // 上面一行只是函数的实现签名,为了兼容上面两个重载签名,不能被直接调用, // 同时它也并不算作一个重载,真正的重载签名只有最上面的两个 return arg1 + arg2; } // 以下代码都能通过类型检查 addFn(1, 2); addFn('a', 'b'); // 而没在重载定义中的类型会报错 // addFn(true, true); // Error 这个例子中,该申明函数与正常函数的区别是: 在函数申明的上方又叠加了几个申明表达式,包裹参数类型和返回类型,末尾以分号结束,每一个申明便是一个重载; 然后是在函数区块中写处理逻辑,同时包含进上面的几种参数与返回类型的情况; 最后调用时就可以传入不同重载所对应的传参类型,并且能通过类型检查,而不在重载定义中的类型则不会通过类型检查; 函数在调用时,ts 会在申明的函数重载中自上而下查找第一个匹配的重载签名,最后一个函数签名称为“实现签名”,并不会被调用; 使用重载虽然实现了同时能计算数字和拼接字符串的需求,但是这种写法还是有些复杂,因为参数类型与返回类型具有一定规律性;因此还可以继续寻求更简便的方式; 类型变量 在 ts 中,可以使用泛型来解决上述需求,具体的方式是使用类型变量,顾名思义,ts 包含一个庞大的类型处理系统,有各种使用类型的情景,为了应对一些场景,就需要类型值有像变量一样的变化性,支持赋值与取值操作; 先看一下使用类型变量的具体写法: function addFn<T>(arg1: T, arg2: T): T { return arg1 + arg2; } addFn<number>(1, 2); addFn<string>('1', '2'); addFn('a', 'b'); // 调用时也可以省略类型赋值 这里的写法就比写重载的形式简便了许多;示例中出现了 <T> 这个标识,其中 T 表示类型变量,<> 表示对类型变量的申明,即申明时使用 <> 设置变量,调用时再使用 <> 进行赋值,这样所有用到变量 T 的地方都会被替换为传入的类型值;这里可以发现,泛型就像类型系统中一个针对类型的函数,类型参数就是形参; 调用时可以省略类型赋值的操作是因为上面的场景中 ts 可以利用类型推断机制自动判断出 T 的实际类型值(number); 由于 T 表示任意类型,所以不能直接访问某些属性或方法: function fn<T>(arg: T): T { // return arg.toString(); // Error,因为并不是所有类型都有该方法 return arg; } 如果是复合类型,则可以使用某些固有属性: function fn<T>(arg: T[]): string { return arg.toString(); // 普通数组类型都具有该方法 } 类型变量也可以使用其他字母或者单词(通常使用 T),并且可以同时定义多个变量: function fn<M, My, other>(arg: M): M { let one: My; let two: other; return arg; } // 存在多个类型变量时,需要依次赋值类型 fn<string, number, boolean>('abc'); 泛型接口 除了函数,泛型也可以在接口中使用,例如: interface IGeneric { <T>(arg: T): T; } let fn: IGeneric = function(arg) { return arg; // 和接口申明不一致会报错 // return arg + ''; } 或者是针对整个接口的泛型: interface IGeneric<T> { a: T; b: T[]; c(arg: T): T; } const obj: IGeneric<number> = { a: 123, b: [1, 2, 3], c: (arg) => arg + 1 }; 泛型类 针对类定义类型变量时,类的所有非静态成员都可以使用该变量: class CS<T> { constructor(public attr: T) {} fn(): T { return this.attr; } // 静态成员不能使用泛型类型 // static a: T = ''; // Error } // 实例化时传入类型值 const cs = new CS<number>(123); cs.fn(); // 123 泛型约束 在一些场景中,可能并不期望类型变量的值太泛,而是需要具有某些属性或方法,这时就可以使用 extends 关键字对类型加一些约束,这和类中的继承用途也有些类似;例如: interface IObj { length: number; } // 将参数约束为具有 length 属性的任意类型 function fn<T extends IObj>(arg: T): number { return arg.length; } fn('abc'); // 3 // 报错,因为数字没有 length 属性 // fn(123); // Error 结合其他 ts 特性,也能表示一些特殊情形,比如下例表示函数第二个参数,需要是第一个参数对象的属性名之一,keyof 关键字为索引查询操作符,keyof T 表示 T 的所有属性构成的联合类型: function fn<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } fn({ a: 1 }, 'a'); // 报错,因为第一个参数对象中并没有一个叫 'b' 的属性 // fn({ a: 1 }, 'b'); // Error

2021/4/18
articleCard.readMore

TypeScript 中类型 any,void,unknown,never之间的区别

TypeScript 拓展了 JavaScript 的基本类型与语言特性,为了覆盖类型检查的情景,衍生出了一些额外的类型,其中 any, unknown, void, never 这几个类型所适用的情形容易使人混淆,下面通过举例进行一下区分; any 这应该是 typescript 中最开始就会接触到的类型,顾名思义:任意类型,这也是 ts 中不写类型申明时的默认类型,即不作任何约束,编译时会跳过对其的类型检查, let val1: any; val1 = 'abc'; val1 = 123; val1 = true; const arry: any[] = [123, 'abc', true, null]; void void 表示无任何类型,正好与 any 相反,没有类型,如果是函数则应没有返回值或者返回 undefined,和 C 等语言中的无返回值函数申明类似: function voidFn(): void {} function voidFn1(): void { return undefined; } function voidFn2(): void { return; } function voidFn3(): void { return 1; } // Error 变量也可以申明为 void 类型,只不过只能给这个变量分配 undefined, null 和 void 类型的值(如果 ts 配置文件中设置了 "strictNullChecks": false,那么分配 null 类型的值也会报错): let val1: void; let val2: null = null; let val3: undefined = undefined; let val4: void; val1 = val2; // "strictNullChecks": false 时报错 val1 = val3; val1 = val4; unknown 顾名思义,unknown 表示未知类型,是 typescript 3.0 中引入的新类型,即写代码的时候还不清楚会得到怎样的数据类型,如服务器接口返回的数据,JSON.parse() 返回的结果等;该类型相当于 any,可以理解为官网指定的替代 any 类型的安全版本(因为不提倡直接使用 any 类型); 它能被赋值为任何类型,但不能被赋值给除了 any 和 unknown 之外的其他类型,同时,不允许执行 unknown 类型变量的方法(any 可以),举个例子: let uk: unknown = 'abc'; uk = 123; uk = true; uk.toString(); // Error let valAny: any = 'abc'; valAny.toString(); // 'abc' let uk1: unknown = uk; let uk2: any = uk; let uk2: string = uk; // Error never never 同样顾名思义,表示永不存在的值的类型,是 typescript 2.0 中引入的新类型,概念有点绕,什么情况下变量会永远不存在值呢?因为通常情况下,变量一旦申明了,就会被分配值,即使没有特别指定,也会被初始化为 undefined,同样一个函数即使有个写返回值,也会默认返回 undefined,也不是真正的不存在返回值: let foo; console.log(typeof foo); // 'undefined' function bar() {}; console.log(typeof bar()); // 'undefined' 其实确实有一些情形,值会永不存在,比如,从程序运行的维度讲,如果一个函数执行时抛出了异常,那么这个函数变永远不会有值了(因为抛出异常会直接中断程序运行,这样程序就运行不到返回值那一步了,即具有不可达的终点,也就永不存在返回了): function err(msg: string): never { throw new Error(msg); } // 有机会到达终点的函数也算存在返回值,编译会报错 function err1(): never { // Error if (Math.random() > 0.5) { throw new Error('message'); } } 还有一种极端情况也比较类似,就是函数中执行无限循环的代码(死循环),这样也同样使得程序永远无法运行到函数返回值那一步,永不存在返回: function loopForever(): never { while (true) {}; } 变量也可以直接申明为 never 类型,让它永不存在值,其实就是意思就是永远不能给它赋值,否则就会报错,这样就可以形成一种保护机制; let ne: never; ne = 123; // Error 另外,never 是所有类型的子类型,意思就是它可以赋值给任何类型(前提是配置里 "strictNullChecks": false,否则检查不通过); let num: number = 123; let ne: never; num = ne; 同时也没有任何类型是 never 的子类型,除了 never 自身,即除了 never 任何类型都不能赋值给 never 类型的变量(如果前提是 "strictNullChecks": true,never 也不能赋值给 never); let ne1: never; let ne2: never; ne1 = ne2; // any 也不能分配给 never let any1: any = 123; ne1 = any1; // Error

2021/4/3
articleCard.readMore

Git submodule 知识总结

概念 先引用 git 的官方定义描述: A submodule is a repository embedded inside another repository. The submodule has its own history; the repository it is embedded in is called a superproject. 子模块(submodule)是一个内嵌在其他 git 仓库(父工程)中的 git 仓库,子模块有自己的 git 记录。 通常,如果一个仓库存在子模块,父工程目录下的 .git/modules/ 目录中会存在一个 git 目录,子模块的仓库目录会存在于父工程的仓库目录中,并且子模块的仓库目录中也会存在一个 .git 目录; 使用场景: 想要在一个工程中使用另一个工程,但是那个工程包含了单独的提交记录,submodule 就可以实现在一个工程中引入另一个工程,同时保留二者的提交记录并且区分开来;目前 submodule 还能实现单独开发子工程,并且不会影响父工程,父工程可以在需要的时候更新子模块的版本; 想要把一个工程拆分成多个仓库并进行集中管理,这可以用来实现 git 当前的限制,实现更细粒度的访问,解决当仓库过于庞大时所出现的传输量大、提交记录冗杂、权限分设等问题; 使用 新增子模块 向一个项目中添加子模块: git submodule add https://github.com/yyy/xxx.git 之后会 clone 该子模块对应的远程项目文件到本地父项目目录下的同名文件夹中(./xxx/),父项目下也会多一个叫 .gitmodules 的文件,内容大致为: [submodule "xxx"] path = xxx url = git@github.com:yyy/xxx.git 如果存在多个子模块,则会继续向该文件中追加与上面相同格式的内容; 同时父项目下的 .git 目录中也会新增 /modules/xxx/ 目录,里面的内容对应子模块仓库中原有的 .git 目录中的文件,此时虽然子模块目录下的 .git 依然存在,但是已经由一个文件夹变成了文件,内容为: gitdir: ../.git/modules/xxx 即指向了父项目的 .git/modules/xxx 目录;如果运行 git config --list 查看项目的配置,也会发现多了类似下面两行的内容: submodule.xxx.url=git@github.com:yyy/xxx.git submodule.xxx.active=true 如果修改 submodule.xxx.url 的值,则会覆盖 .gitmodules 文件中对应的 url 值; 查看子模块 查看当前项目下的子模块: git submodule 或者 git submodule status 输出: 70c316ecb7c41a5bdf8a37ff93bf866d3b903388 xxx (heads/master) 如果将父项目推送到远程仓库(如 Github),在网页浏览该项目时子模块所在的目录会多一个类似 @70c316e 的后缀,即上面查看子模块命令输出内容的 hash 值的前面部分,点击这个目录会跳转到这个子模块对应的仓库地址(另一个 url); 如果执行: git submodule deinit 删除了子模块,则再次查看时输出会是这样的: -70c316ecb7c41a5bdf8a37ff93bf866d3b903388 xxx 拉取子模块 如果要 clone 一个项目,并且包含其子模块的文件,则需要给 git clone 命令最后加上 --recurse-submodules 或者 --recursive 参数(否则只会下载一个空的子模块文件): git clone https://github.com/yyy/xxx.git --recursive 当然,克隆时忘记了加这个参数,后续也有办法去拉取子模块的文件,首先执行: git submodule init 这会初始化子模块相关配置,比如自动在 config 中加入下面两行内容: submodule.xxx.url=git@github.com:yyy/xxx.git submodule.xxx.active=true 然后执行: git submodule update 就可以拉取到子模块仓库中的文件了,也可以将这两步命令合并为一步: git submodule update --init 要拉取所有层层嵌套的子模块,则执行: git submodule update --init --recursive 之前 clone 时加参数不过是自动执行初始化配置并拉取子模块(甚至嵌套的子模块)中的文件罢了; 命令默认拉取主分支(master),想要修改这个默认拉取分支可以修改 .gitmodules 文件中子模块对应的 branch 值,或者执行: git config submodule.xxx.branch dev 或者执行同时将配置写入文件,这样其他人拉取父项目也会获取该配置: git config -f .gitmodules submodule.xxx.branch dev 更新子模块 拉取更新 获取子模块仓库的最新提交,同步远程分支的变更,可以直接在子模块目录下执行: git pull 或者在父目录下执行: git submodule update --remote 这里给 git submodule update 加上 --remote 是为了直接从子模块的当前分支的远程追踪分支获取最新变更,不加则是默认从父项目的 SHA-1 记录中获取变更;当有多个子模块时,该命令默认拉取所有子模块的变更,指定更新子模块 xxx 需要执行: git submodule update --remote xxx 如果将修改子模块的相关变更推送到父项目的远程,其他人拉取代码时,只用 git pull 的话只会把子模块的相关修改拉取到父项目,具体变更并不会更新到子模块中,在父项目里执行: git diff --submodule Submodule xxx a6e2962..70c316e (rewind): < add file 注意子模块提交记录中前的 < 符号,表示变更未更新到子模块文件夹里,所以更新子模块变更需要执行: git submodule update --init --recursive 或者直接在父项目拉取时同时更新子模块(需要子模块已经 init,否则仍然拉取不到文件): git pull --recurse-submodules 分支切换 更新完子模块(git submodule update)后,虽然会将文件变更同步到子模块目录下,但是此时子模块并没有处于任何已有分支下,去子模块目录下检查一下分支就会发现: git branch -vv * (HEAD detached at 16d1b6b) 16d1b6b mod file master 16d1b6b [origin/master] mod file 当前分支并不是 master,而是一个 detached 状态的编号分支,官方文档称为“游离的 HEAD”,虽然可以提交,但是并没有本地分支跟踪这些更改,意味着下次更新子模块就会丢失这些更改; 所以在子模块下开始开发前,需要先切换到某个已有分支或者创建新的分支,比如进入主分支: git checkout master 分支合并 除了默认的分支同步更新操作,也可以执行其他类型的分支更新行为,比如 merge,rebase 等;如将父项目中记录的子模块最新变更(分支是 submodule.xxx.branch 中配置的,默认主分支 master)merge 到子模块的当前分支中,则执行: git submodule update --remote --merge rebase 到子模块当前分支则执行: git submodule update --remote --rebase 注意事项 如果其他人修改了子模块的内容并提交了记录,父项目也提交并推送了远程仓库,但是子模块没有推送其对应的远程仓库, 那么其他人拉取父项目代码变更时没有问题,但是更新子模块时就会遇到下面的问题: fatal: remote error: upload-pack: not our ref 16d1b6b94e3245f3a7fb4f43e5b6f44b14027fbb Fetched in submodule path 'xxx', but it did not contain 16d1b6b94e3245f3a7fb4f43e5b6f44b14027fbb. Direct fetching of that commit failed. 即由于其他人没有及时将子模块的提交 push 的子模块的远程仓库,我们本地父项目有了关于子模块最新的变更,但是在子模块的仓库中却找不到,就报错了,让对方在子模块下 push 一下这边再重新更新就行了; 为了避免制造这一不必要的麻烦,可以把在父项目中推送远程的命令替换为: git push --recurse-submodules=check 这样如果子模块(与父项目记录的对应分支)存在未 push 的提交,就会报错,并且子模块有推送失败的,父项目也会推送失败;需要在推送父项目时自动推送未推送的子模块,则执行: git push --recurse-submodules=on-demand 觉得每次手输太麻烦,就直接将其写入配置: git config push.recurseSubmodules check 如果父项目中子模块的仓库地址(submodule.xxx.url)被其他协作者修改了,那么我们再更新子模块时就可能遇到问题,需要执行: git submodule sync --recursive 同步完 url,然后再重新初始化更新: git submodule update --int --recursive 删除子模块 在确认移除子模块前,需要先将其取消注册(unregister),即删除该子模块相关的配置文件(git config),比如要移除子模块 xxx,则执行: git submodule deinit xxx 然后子模块的相关配置会被删除(.gitmodules 和 .git/modules/xxx 中的配置会保留),子模块对应的目录也会被清空(子模块目录本身会保留),再运行 git submodule status 查看子模块则会输出: -70c316ecb7c41a5bdf8a37ff93bf866d3b903388 xxx 前缀 - 表示该子模块已经被取消注册,可理解为暂时移除,想必官方这样做也是给我们提供反悔的余地,因为想要恢复刚才删除的子模块,重新执行 git submodule update --init xxx 就能重新初始子模块并拉取文件; 由于还有一些配置文件仍然被保留,所以想要彻底删除的话,需要继续手动删除这里配置文件,即: 删除子模块对应的目录 xxx; 删除 .gitmoduls 中子模块 xxx 对应的区块配置; 删除 .git/modules/ 目录下的子模块目录 xxx; 删除子模块的缓存:git rm --cached xxx; 然后再执行 git submodule 就没有任何输出了,清除完毕; 子模块与父项目的联系 父项目和子模块有着分开的 git 仓库,所以可以分别在父项目和子模块的目录下使用 git 命令,操作的也是各自的仓库,比如分别在父项目和子模块中执行 git branch -a 或者 git remote -v 的输出结果是不同的; 虽然二者有个分开的仓库与提交记录,但是又是关联起来的(这正是 submodule 所做的工作),举个例子,在子模块目录 xxx/ 下新增一个文件 test.txt,然后在子模块目录中执行 git satus 会输出: Untracked files: (use "git add <file>..." to include in what will be committed) xxx/test.txt 此时在父项目下执行 git status 输出的是: Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) (commit or discard the untracked or modified content in submodules) modified: xxx (untracked content) 即提示需要先在子模块下提交修改记录; 然后子模块下提交记录,执行: git add . git commit -m "add file" 这时再分别运行 git status,子模块的输出是: Your branch is ahead of 'origin/master' by 1 commit. (use "git push" to publish your local commits) nothing to commit, working tree clean 而父项目的输出是: Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: xxx (new commits) 提示子模块中有了新的提交(new commits); 假如再把子模块下的这个 test.txt 文件删除,则子模块的状态是: Changes not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) deleted: xxx/test.txt 但是父项目的状态依然是: Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: xxx (new commits) 子模块撤销刚才的删除操作,将新增文件的记录 git push 到远程(这会推送到子模块自己的远程仓库),此时子模块的工作区状态是清空状态,但是父项目的依旧是: Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: xxx (new commits) 所以,父项目与子模块的关联便是,父模块只是单纯的识别子模块的总体变化,而不会在意具体是新增、修改还是删除,甚至修改已经提交推送到子模块所属的远程仓库,只是将这些调整统一识别为 modified 状态,然后需要提交并推送到自己(父项目)所属的远程仓库; 在父项目中使用 git diff 可以查看当前的变更,会输出: diff --git a/xxx b/xxx index 70c316e..a6e2962 160000 --- a/xxx +++ b/xxx @@ -1 +1 @@ -Subproject commit 70c316ecb7c41a5bdf8a37ff93bf866d3b903388 +Subproject commit a6e29629904538e8f70694df607617084d2659ca 如果想要查看具体子模块的变动,可以执行: git diff --submodule Submodule xxx 70c316e..a6e2962: > add file 输出会列出当前子模块的所有变动的提交日志;也可以直接日志中关联的子模块提交记录,执行: git log -p --submodule commit 909a721e3755affb7620316b44df8fbc1b3488f2 (HEAD -> master) Author: ****** Date: ****** mod submodule Submodule xxx 70c316e..a6e2962: > add file 其他 父项目从含有子模块的分支切换到没有子模块的分支时,默认会保留子模块对应的目录,所以这使得切换过去时本地会保留关于子模块的修改记录,显然这不太合理,所以从包含子模块的分支切换到 xxx 时,需要这样执行: git checkout xxx --recurse-submodules 当父项目存在许多子模块时,有时需要对多个子模块执行相同的操作,这时就可以使用 foreach 功能,比如批量存储: git submodule foreach 'git stash' 或者在每个子模块中新建切换分支: git submodule foreach 'git checkout -b new'

2021/3/21
articleCard.readMore

Git 仓库中文件名大小写问题

首先,Windows 下 git 默认配置是对文件/文件夹名称的大小写不敏感: git config --get core.ignorecase # true 这就导致了一些时候的难以预料问题的产生,针对这个配置,先引用一下官方帮助文档的原话: The default is false, except git-clone or git-init will probe and set core.ignoreCase true if appropriate when the repository is created. 即 git 默认对大小写敏感,但是会在仓库克隆或初始化时,根据当前系统来设置是否忽略大小写,比如 Windows 下会设置为 true,即不敏感,而 Linux 中不会忽略;相信有不少开发者的项目开发与协同工作都是在 Windows 系统下进行的,下面就列出 git 的这种机制会导致的问题与解决思路; 规范重命名 如果分支上直接在编辑器或资源管理器上修改项目中的文件名(只变更大小写),本地虽然可以调试通过,但是 git 并不会识别和记录这个修改,所以下一次提交推送时并不会带上这个重命名修改,远程仓库中这个文件名还是保持不变; 因此,如果检出其他分支或者其他协作者拉取代码,项目就会报错,因为一个本地文件的名称如果由小写变成了大写,使用这个文件的代码部分也改成了大写,推送到远程后,远程的这个文件依然是小写,但远程上使用该文件的代码却成功变成了大写,那边启动项目就多半会提示文件不存在了; 对于这种情况 git 提供了一种规范的做法,使用 git mv 命令: git mv test.txt TEST.txt 以此来实现对文件的重命名,同时 git 也会将其识别为 Rename 的变更类型,然后正常提交推送就能同步到远程仓库了;如果是重命名文件夹,由于 Windows 下对文件夹的大小写也不敏感(-_-),所以直接使用上面的方法会失败: git mv test-dir TEST-DIR # Rename from 'test-dir' to 'Test-dir/test-dir' failed. 这里就只有迂回一下,先把文件夹命名成其他名称,然后再命名为大写就行了: git mv test-dir tmp git mv tmp TEST-DIR 修改配置 可以选择直接修改 git 配置为不忽略大小写: git config core.ignorecase false 然后直接在资源管理器或编辑器中修改文件名大小写,git 就会识别到了,而且是被识别为 untracked 类型的变更,这依然是 Windows 下对文件名大小写不敏感导致的(=_=),如果直接推送到远程的话,那么远程仓库就会同时存在大小写两个版本的文件(github/gitlab 服务器通常都是 Linux 系统),为后期维护添加隐患,本地在分支间切换时也可能出现以下报错: error: The following untracked working tree files would be overwritten by checkout: test.txt Please move or remove them before you switch branches. Aborting 这种情况下依然需要使用一些迂回的办法,就是先把要重命名的文件改成其他临时名称,提交一次(git commit),然后再把临时名称改成想要的名称,再提交一次,最后推送到远程,这样本地和远程都只保留下一个文件了; # rename test.txt --> tmp git add . git commit -m "..." # rename tmp --> TEST.TXT git add . git commit -m "..." git push 所以不管是 Windows 还是其他系统下,还是不要轻易修改 git 默认的 core.ignorecase 配置,使用规范的 git mv 做法就好;

2021/1/18
articleCard.readMore

JavaScript 经典设计模式

设计模式(Design Pattern)是一套被反复使用的代码设计经验总结,一个模式可以是一套可复用的方案,或者一个解决某一类问题的模板;使用固定的模式(解决方式)解决软件开发中的某些问题,利于代码的可理解性与可靠性,比如,比起设计一套标新立异的房屋结构,沿用大部分的现有设计结构更容易取得大部分人的信任与接受;下面是一些应用于 JavaScript 中的常用设计模式,以及以 JavaScript 实现的经典设计模式,包含示例与说明; 构造器模式 在构造器模式中,通过声明构造器函数,再使用 new 关键字可获取一个新实例对象;构造函数可以接收参数,并在实例化时传参赋值,也可以在构造函数的 prototype 中添加公共属性或方法,使得所有实例对象都能访问; function Person(name) { this.type = 'Human'; this.name = name; } Person.prototype.getName = function() { return this.name; } Person.prototype.getType = function() { return this.type; } var knight = new Person('Knight'); console.log(knight); // { type: "Human", name: "Knight" } console.log(knight.getName()); // "Knight" var cloud = new Person(); console.log(cloud.getType()); // "Human" 这是 ES5 原生的写法,在 ES6 中可以使用类的概念进行改造: class Person { // 类的构造器 constructor(name) { this.name = name; this.type = 'Human'; } // 实例的公共方法 getName() { return this.name; } getType() { return this.type; } } const knight = new Person('Knight'); console.log(knight); // { type: "Human", name: "Knight" } console.log(knight.getName()); // "Knight" 模块化模式 模块是健壮的应用程序里不可或缺的一部分,模块化能助于系统各单元的合理分工,并维持清晰有组织的联系;模块可以提供私有和公共的封装方法,现代实现模块化的方式有多种,如 AMD, CommonJS, ES Module 等; 一个极简的模块可以用一个对象字面量表示: var module = { a: 1, getA: function() { return this.a; } } var a = 2; console.log(module.getA()); // 1 console.log(module.a); // 1 console.log(a); // 2 模块中的属性都可看作维护的“私有”变量,如 module.a 的值并不与全局的 a 冲突,但是这也仅仅是防止了模块内外的变量命名冲突,其实还是能进行全局访问或修改,准确讲上面的模块的属性都为“公共”属性; 但很多时候又确实需要维护一些仅模块内部可访问的私有属性或变量,下面便是一种使用闭包特性实现的常用手法: var module = (function() { var a = 1; this.b = 2; return { getA: function() { return a; }, setA: function(n) { a = n; } } })(); console.log(module.getA()); // 1 console.log(module.a); // undefined console.log(module.b); // undefined module.setA(3); console.log(module.getA()); // 3 即模块通过某些方式,暴露一个公共的接口,以提供外部访问或修改模块内部某些属性或变量的途径; 暴露模块模式 暴露模块,即在模块化的基础上,实现对私有或公有成员的改名,甚至成员之间的相互访问: var module = (function(){ var a = 1; function privateFn() { return 2; } function publicFn() { var r = privateFn(); return r; } return { b: a, renameFn: publicFn } })(); console.log(module.b); // 1 console.log(module.renameFn()); // 2 这样做可以实现将模块内部的私有属性,以更加规范化或标准化的方式展现(暴露给外部),增强可读性; 单例模式 单例模式(Singleton Pattern)中,单例是指类只有一个实例对象,实现思路可以是返回一个实例对象时,如果未初始化则初始化后返回,已经初始化则返回对它的引用; 单例类有且只有一个自己的实例,并能提供给作用域中所有其他对象访问; var Singleton = (function(){ var instance; function init() { var r = Math.random(); return { r: r } } return { // 单例模式的方法,只实例化一次 getR: function() { if (!instance) instance = init(); return instance; }, // 非单例模式的方法,每次都实例化 _getR: function() { return init(); } } })(); // 下面的输出值随机,不过两次输出相同 console.log(Singleton.getR().r); // 0.9703895249107328 console.log(Singleton.getR().r); // 0.9703895249107328 // 两次输出不同 console.log(Singleton._getR().r); // 0.49340212997805155 console.log(Singleton._getR().r); // 0.06939910709339148 直接的对象定义(对象字面量)是最简单的单例模式,因为利用变量声明的不可重复特性,保证了对象的唯一性与作用域可访问性; var obj = { name: 'Knight', age: 22, getName() { return this.name; } } 观察者模式 观察者模式(Observer Pattern)由被观察者和观察者组成,观察者可以是多个,被观察者维护着多个观察者,如添加或删除观察者;当被观察着数据变化时,会通过广播的方式通知维护的每一个观察者(即调用观察者提供的回调函数); 简单实现: // 被观察者 class Source { // 维护的一组观察者 observers = []; // 获取观察者 getObserver(idx) { if (idx) { return this.observers; } else { return this.observers[idx]; } } // 添加观察者 addObserver(...obs) { Array.prototype.push.apply(this.observers, obs); } // 删除指定观察者 deleteObserver(idx) { this.observers.splice(idx, 1); } // 发布通知(广播)的函数 notify(data) { this.observers.forEach(x => { // 执行观察者提供的回调,实现通知数据的变化 x.callback.call(null, data); console.log('Notified ' + x.name); }) } } // 观察者 class Observer { constructor(name) { this.name = name; } callback(data) { console.log(data); } } // 定义两个观察者,接口要和被观察者里的一直 const observer1 = new Observer('observer1'); const observer2 = new Observer('observer2'); // 注册观察者 const source = new Source(); source.addObserver(observer1, observer2); // 发布通知 source.notify('I am changed.'); // 随后会输出以下内容,表示通知到了被观察者们 // I am changed. // Notified observer1 // I am changed. // Notified observer2 发布/订阅模式 发布/订阅模式是在观察者模式的一种变体,区别在于,观察者模式每个观察者想要接收通知需要到被观察者上面去注册,而发布/订阅模式的原理是发布者(可以理解为之前的被观察者)将信息发送到指定的频道(channel)上,如果订阅者(理解为观察者)对某个频道感兴趣,就可以订阅该频道以接收消息,不感兴趣后还可以取消订阅(整个过程和平时订阅杂志蛮像的);所以其相比于观察者模式的优点便是减小了信息发布者和接收者的依赖性和耦合度; 简单实现: // 发布者 class Publisher { // 定义一些固定的频道供订阅者订阅 // 每个频道都维护着多个订阅者,数组长度即订阅者数量 channels = { 'Game': [], 'Movie': [], 'Music': [] } // 每个订阅者的唯一标识符 subId = 0; // 订阅者实现订阅的函数 // 参数包括要订阅的频道和回调函数 subscribe(channel, callback) { if (this.channels[channel] !== undefined) { // 某频道的每个订阅者包含的信息, // 包括一个唯一标识符和一个回调函数 this.channels[channel].push({ subId: this.subId++, callback }); } else { console.log('Channel not found!'); } } // 为订阅者取消订阅 unsubscribe(id) { let found = false; for (let k in this.channels) { if (found) { break; } else { for (let i = 0; i < this.channels[k].length; i++) { if (this.channels[k][i][subId] === id) { this.channels[k].splice(i, 1); found = true; break; } } } } } // 为指定频道发布通知 publish(channel, data) { if (this.channels[channel] !== undefined) { this.channels[channel].forEach(x => { x.callback.call(null, data); }); } else { console.log('Channel not found!'); } } // 添加消息发布频道 addChannel(channel) { if (this.channels[channel] !== undefined) { console.log('Channel existed!'); } else { this.channels[chanel] = []; } } // 删除指定频道 deleteChannel(channel) { if (this.channels[channel] !== undefined) { delete this.channels[channel]; } else { console.log('Channel not exist!'); } } } const publisher = new Publisher(); // 用户订阅频道 // 用户 1 publisher.subscribe('Game', function(data) { console.log(data); }); // 用户 2 publisher.subscribe('Music', function(data) { console.log(data); }); // 发布订阅 publisher.publish('Music', 'Add a new music.'); // Add a new music. (用户 2 收到消息) publisher.publish('Game', 'Add a new game.'); // Add a new game. (用户 1 收到消息) 中介者模式 中介者模式(Mediator Pattern),中介者的定义是,一个在谈判或冲突解决过程中起辅助作用的中立方,如生活中的房屋中介,可以减少普通用户购房过程的一些繁杂操作;机场的航站塔,接受总部指令,控制具体那些飞机可以起飞、降落;软件中的中介者用于减少组件间的大量直接通信,转而通过某个中心点来控制,可以实现松耦合,同时提高其复用性; 尝试实现一个简单的聊天室,不同用户发送消息,中介者(消息处理器)统一处理信息(如显示在公屏上); // 定义用户及其行为 class User { constructor(name) { this.name = name; } sendMessage(msg, to) { MessageMediator.sendMessage(this.name, msg, to); } } // 聊天室 class ChatRoom { // 模拟在公屏上展示消息 static showMessage(msg) { console.log(msg); } } // 消息处理器 class MessageMediator { // 统一处理不同用户发送的消息 static sendMessage(user, msg, to) { let toUser = ''; if (to) { toUser = `@${to};`; } ChatRoom.showMessage(`[${user}]: ${toUser} ${msg}`); } } const jack = new User('Jack'); const bob = new User('Bob'); // 模拟发送消息 jack.sendMessage('Hello everyone!'); // [Jack]: Hello everyone! bob.sendMessage('Welcome! Jack.', 'Jack'); // [Bob]: @Jack; Welcome! Jack. 咋一看是一个简单的功能用了大量代码来实现,但是当用户量相当大,逻辑更复杂时,就能较好的把用户与聊天室的联系松开,通过中间件处理两边的复杂逻辑并做好二者的衔接工作,实现一个中介的价值; 原型模式 原型模式(Prototype Pattern)核心为继承,即能够基于一个对象或类为模板,创建另一个对象或类,使新对象与原型对象拥有一些共同属性或方法;该模式创建重复对象,可以一定程度上提升性能; JavaScript 中基于一个对象为原型创建另一个对象常用的方法是 Object.create(),方法第一个参数是原型(prototype)对象,第二个参数是新建对象的一些属性描述: const protoObj = { a: 1, getA: function() { return this.a; } } const newObj = Object.create(protoObj, { b: { value: 2 }, getB: { value: function() { return this.b; } } }); console.log(newObj.a); // 1 console.log(newObj.getA()); // 1 console.log(newObj.b); // 2 console.log(newObj.getB()); // 2 console.log(newObj.__proto__); // { a: 1, getA: f () } protoObj.a = 3; console.log(newObj.a); // 3 newObj.a = 4; console.log(protoObj.a); // 3 或者模拟一下实现: function createObj(proto, obj) { function F() {}; F.prototype = proto; return new F(); } const protoObj = { a: 1 }; const newObj = createObj(protoObj); newObj.b = 2; console.log(newObj.a); // 1 console.log(newObj.b); // 2 console.log(newObj.__proto__); // { a: 1 } protoObj.a = 3; console.log(newObj.a); // 3 newObj.a = 4; console.log(protoObj.a); // 3 以上例子最后的执行结果都表明,新建对象的原型是对原型对象的一个引用,并且是单向引用,即原型对象会影响新对象,新对象无法影响到原型对象,这就可以在实现克隆或继承对象的功能时,减少内存的开销,提升性能; 类的原型继承就比较简单了: class A { a = 1 } class B extends A { b = 2; } const b = new B(); console.log(b); // { a: 1, b: 2 } 由于 JavaScript 不像 Java 一样有类的机制,所以上述写法也只是 ES6 的新特性,方便书写代码,底层实现和 Object.create() 相差无几; 命令模式 命令模式(Command Pattern)是将一系列方法封装在一个对象中,对方法的调用请求以命令的形式传递给这个对象,然后由该对象来寻找处理该命令的合适方法;是一种数据驱动的模式;将方法的实现与调用解耦,并实现方法调用的参数化,能便于后期的逻辑调整; 考虑下面一种购买商品的简单场景: const itemObj = { buy: function(item) { console.log('You have bought the ' + item); }, sell: function(item) { console.log('You have sold the ' + item); } } itemObj.buy('phone'); // You have bought the phone 这样 itemObj 既是内部方法的声明者也是调用者,如果后期一些内部方法的逻辑调整,比如方法名或传参,那么后面所有调用这些方法的地方都得要改一遍,这种强耦合对于开发是很不利的,所以需要实现一个方法,专门用于接受一些调用命令,再自行去对应地调用合适的方法执行逻辑;比如: const itemObj = { buy: function(item) { console.log('You have bought the ' + item); }, sell: function(item) { console.log('You have sold the ' + item); }, execute: function(command, content) { switch (command) { case 'buy': return this.buy(content); case 'sell': return this.buy(content); default: return; } } } itemObj.execute('buy', 'phone'); // You have bought the phone 这种对方法或属性进行实现与调用的松耦合操作,有些类似一些面向对象语言中的抽象类(abstract class)和接口(interface)特性,即将实现与调用分离; 外观模式 外观模式(Facade Pattern),概括就是隐藏系统的复杂性,对外封装提供一个更易于使用和访问的接口;所谓外观便是,好看好用就行,管它内部经络骨骼有多复杂和不堪,比如 JQuery,能曾经一度流行也和其对一些方法和兼容性处理逻辑的封装有着很大关系,比如 $() 可直接进行多种类型的元素选择,不用再去写冗长的 getElementById() 或者 getElementsByClassName() 等方法; 比如要实现一个简单的字符串转十六进制码的方法,可以这样封装: class StringConvertor { static toHex(str) { const strArr = str.split(''); const hexArr = strArr.map(char => { return char.charCodeAt().toString(16); }); return hexArr.join(''); } } const hex = StringConvertor.toHex('Hello world!'); console.log(hex); // "48656c6c6f20776f726c6421" 当然使用这种模式也需要考虑性能等因素,需要权衡实现需求与性能消耗之间,是否值得加上并使用这一层抽象逻辑,否则这种外观就成了“花瓶”,中看不中用;比如使用但从速度上说,$('#id') 是肯定比不上 getElementById('id') 的,当然为了整体的使用体验愿意牺牲这些性能也可以完全忽略;而对于一些复杂逻辑的实现,难以找到更好的替代选项,那么提供这一层封装多方面来讲都是有益的; 工厂模式 工厂模式(Factory Pattern),提供了一种对象的创建方式,一个工厂能提供一个公共接口用于对象创建,接口内部封装好对象的创建逻辑而不会暴露出来,通常用于逻辑复杂的对象生成场景,如根据输入获得不同类型的对象; 例如一个简单的生产汽车的工厂示例: // 生产 A 类汽车的类 class CarA { constructor(option) { this.carAttr = option; } getCarAttr() { return this.carAttr; } } // 生产 B 类汽车的类 class CarB { constructor(option) { this.carAttr = option; } getCarAttr() { return this.carAttr; } } // 生产不同汽车的工厂类 class CarFactory { static createCar(option) { const { type, name, color } = option; if (type === 'A') { return new CarA({ name, color }); } else if (type === 'B') { return new CarB({ name, color }); } else { return {}; } } } const car1 = CarFactory.createCar({ type: 'A', name: 'car1', color: 'red' }); const car2 = CarFactory.createCar({ type: 'B', name: 'car2', color: 'black' }); const car1Attr = car1.getCarAttr(); const car2Attr = car2.getCarAttr(); console.log(car1Attr); // {name: "car1", color: "red"} console.log(car2Attr); // {name: "car2", color: "black"} 当然,像上面这类简单场景,使用工厂模式实现对象创建这一需求,被弄得有些复杂了,所以使用时需要斟酌建立在复杂需求上的设计必要性与该模式所引入的复杂性之间的利害关系; 抽象工厂模式 抽象工厂模式(Abstract Factory Pattern),即对上面提到的工厂模式中的工厂的抽象,可以理解为一个超级工厂,或者创建工厂的工厂,相当于工厂的上一级,和抽象类的概念有些类似,工厂是生产某些产品的具体工厂,抽象工厂则创建具有有些相同特征的一类工厂; 沿用之前的汽车工厂的例子,汽车工厂可以生产一辆辆具体属性的某系列汽车,那么汽车抽象工厂,则可以建造不同品牌的汽车工厂,比如创建了福特汽车工厂,可以用于生产福特某系列的汽车,创建奔驰汽车工厂,可以生产奔驰一系列的汽车,示例如下: // 抽象汽车工厂 class AbstractCarFactory { static createFactory(brand) { if (brand === 'Benz') { return new BenzFactory(); } else if (brand === 'Ford') { return new FordFactory(); } else { return {}; } } } // 奔驰汽车工厂 class BenzFactory { createCar(option) { const { series, name, color } = option; if (series === 'v1') { return new BenzV1({ name, color }); } else if (series === 'v2') { return new BenzV2({ name, color }); } else { return {}; } } } // 奔驰 v1 系列汽车 class BenzV1 { constructor(option) { this.carAttr = option; } getAttr() { return this.carAttr; } } // 奔驰 v2 系列汽车 class BenzV2 { constructor(option) { this.carAttr = option; } getAttr() { return this.carAttr; } } // 福特汽车工厂 class FordFactory { // ... } // 创建汽车工厂 const benzFactory = AbstractCarFactory.createFactory('Benz'); // 生产具体系列汽车 const benzV1 = benzFactory.createCar({ series: 'v1', name: 'Benz V1', color: 'red' }); const benzV2 = benzFactory.createCar({ series: 'v2', name: 'Benz V2', color: 'black' }); const benzV1Attr = benzV1.getAttr(); const benzV2Attr = benzV2.getAttr(); console.log(benzV1Attr); // {name: "Benz V1", color: "red"} console.log(benzV2Attr); // {name: "Benz V2", color: "black"} 由于抽象工厂是建立在工厂模式之上,所以复杂度会比之前的工厂模式更高一级,所以同样需要考虑使用该模式引入的复杂性是否是系统设计的必要; 混入模式 混入模式(Mixin Pattern),即对象实现属性的继承,功能的复用与拓展,其实就是继承这一概念的特性,该模式可以简单理解为将某一特定对象的属性方法混入多个对象中;对象中有一个原型,可以从原型中继承额外的属性,而原型又能从其它对象继承而来;在类中,子类B 继承自超类A,则类B 的实例就拥有了所有类A 的方法,还有类B 中额外定义的方法,以及重载的类A的方法; 下面是一个使用 Mixin 实现对象继承的示例,这是原始(ES5)的一种写法: var mixin = { firstName: 'Knight', lastName: 'Huang', } function Person() { this.hobby = 'Games'; } // 将 mixin 的属性继承给 Person,实际就是将 mixin 的所有属性, // 拷贝到 Person.prototype 这个对象的 __proto__ 属性中,即原型链继承; Person.prototype = mixin; var knight = new Person(); console.log(knight.firstName, knight.lastName, knight.hobby); // Knight Huang Games 当然更通用和规范的写法(ES5)是: // 被继承的超类 function Mixin() { this.firstName = 'Knight'; this.lastName = 'Huang'; } function Person() { // 调用超类的构造函数,继承其属性 Mixin.call(this); this.hobby = 'Games'; } // 还需要继承超类的原型链属性 Person.prototype = Object.create(Mixin.prototype); var knight = new Person(); console.log(knight); // {firstName: "Knight", lastName: "Huang", hobby: "Games"} 更直观和方便的新语法(ES6)写法是: class Mixin { constructor() { this.firstName = 'Knight'; this.lastName = 'Huang'; } } class Person extends Mixin { constructor() { // 调用超类构造函数,继承属性 super(); this.hobby = 'Games'; } } const knight = new Person(); console.log(knight); // {firstName: "Knight", lastName: "Huang", hobby: "Games"} 装饰器模式 装饰器模式(Decorator Pattern),是一种类似混入模式的用于提升复用性的模式,其主要功能是向现有的类或对象动态添加新功能(属性/方法),并且不改变,也不用关心该类或对象原有的结构;顾名思义,现实世界的大部分装饰物,都是起着美化原有物的作用,并且基本都不会改变其结构,比如给房间添加个挂件什么的,还不至于把整个房子拆了重建,那就不是装饰而是改造了; 装饰器模式解决的问题也很明显,假如需要给某对象添加一些新特性,按传统思维就是使用继承,但是新功能的量很大,或者在不断递增时,使用继承再获取对象实例的开销(子类量)就相当大了;下面是一个车辆装饰器的简单示例: class Car { constructor(option) { this.brand = option.brand; this.name = option.name; } } class CarDecorator { constructor(car) { this.car = car; // 添加额外功能 this.addColor('blue'); // ... } getAttr() { return this.car; } // 为汽车添加涂色 addColor(color) { this.car.color = color; } // ... 其它需要装饰的功能 } const benz = new Car({ brand: 'Benz', name: 'My car' }); const newBenz = new CarDecorator(benz); const newBenzAttr = newBenz.getAttr(); console.log(newBenzAttr); // {brand: "Benz", name: "My car", color: "blue"} 这里所谓的不改变原有结构进行装饰,其实就是在类的实例对象中动态增加功能,而该类的其它实例不会收到影响; 享元模式 享元模式(Flyweight Pattern),是一种减少对象创建数量,提倡数据共享,减少重复数据以优化性能(如内存占用)的结构化解决方案;“Flyweight”这个单词源自拳击比赛中“轻量级”选手一词,该模式的命名者也是如此取其含义,就是使应用程序更轻量级,“享元”也可以理解为共享元数据; 享元模式下一个重要概念是区分内部状态和外部状态,内部状态即需要维护的共享元,供外部共享,且不会因外部变化而改变;内部状态不被共享,会随外部环境变化而改变;享元工厂应维护一个内部状态,并能够接受或操作外部状态,同时需要将二者分离开来,相互的影响会引起系统的混乱; 以下是一个汽车工厂的例子,需要实现零件共享(如轮胎,车门,车窗…),生产组装车辆时可以使用现有零组件,零件库里没有找到再进行单独制造,然后放入零件库中供后续使用; // 单个零件类 class CarComponent { constructor(option) { const { name, color } = option; this.name = name; this.color = color; } } // 汽车零件享元工厂 class FlyweightFactory { constructor() { // 内部状态,供接口调用访问 this.components = {}; } // 获取零件 getComponent(option) { const { name, color } = option; let component = this.components[name]; // 存在该零件则直接返回,否则重新生产 if (!component) { component = new CarComponent({name, color}); this.components[name] = component; } return component; } // 返回所有已有零件 getAllComponents() { return this.components; } } // 汽车工厂 class CarFactory { // 通过组装零件生产汽车 static createCar(components) { // 外部状态,与共享的内部状态分离 const carAttr = {}; carAttr.door = components.getComponent({ name: 'door', color: 'blue' }); carAttr.window = components.getComponent({ name: 'window', color: 'transparent' }); carAttr.tire = components.getComponent({ name: 'tire', color: 'black' }); return carAttr; } } // 先实例化享元工厂,建立共享零件库 const components = new FlyweightFactory(); // 组建汽车 const car1 = CarFactory.createCar(components); const car2 = CarFactory.createCar(components); console.log(car1); // { // "door": {"name": "door", "color": "blue"}, // "window": {"name": "window", "color": "transparent"}, // "tire": {"name": "tire", "color": "black"} // } console.log(car2); // { // "door": {"name": "door", "color": "blue"}, // "window": {"name": "window", "color": "transparent"}, // "tire": {"name": "tire", "color": "black"} // } const allComponents = components.getAllComponents(); console.log(allComponents); // { // "door": {"name": "door", "color": "blue"}, // "window": {"name": "window", "color": "transparent"}, // "tire": {"name": "tire", "color": "black"} // } MVC 模式 MVC 模式即:模型(Model),视图(View),控制器(Controller),实现了将业务数据模型从用户视图中分离,通过控制器处理数据逻辑与页面交互逻辑;用户在视图上触发某一动作(点击、拖拽…)后,会通知处理该交互的控制器更新模型中的数据,模型更新后会再通知其观察者(视图),然后视图层收到通知便会自行做出相应更新;整体结构有些类似这样: 产生交互 更新数据 View ---------> Controller ----------> Model ^ | |_______________________________________| 更新页面通知 一个极简的计数器的示例: // 视图 class CounterView { // 初始化视图 constructor(text) { const body = document.querySelector("body"); const elCounter = document.createElement("div"); const elDisplay = document.createElement("div"); const elButton = document.createElement("button"); elDisplay.innerText = `Current count: ${text}`; elButton.innerText = "Add count"; elCounter.appendChild(elDisplay); elCounter.appendChild(elButton); body.appendChild(elCounter); // 提供接口供控制器操作 this.elDisplay = elDisplay; this.elButton = elButton; } // 更新视图自身内容的逻辑 updateView(text) { this.elDisplay.innerText = `Current count: ${text}`; } } // 数据模型 class CounterModel { constructor(initValue) { this.count = initValue; // 维护观察者 this.observers = []; } // 获取数据 getCount() { return this.count; } // 改变数据 setCount(value, view) { this.count = value; // 通知观察者们更新 this.observers.forEach(view => { view.updateView(value); }); } // 添加观察者 addObserver(view) { this.observers.push(view); } } // 控制器 class CounterController { constructor(model, view) { this.model = model; this.view = view; // 给视图绑定事件监听 view.elButton.addEventListener( "click", this.clickCallback.bind(this) ); } // 视图事件回调 clickCallback() { const count = this.model.getCount(); // 触发模型数据更新 this.model.setCount(count + 1, this.view); } } // 建立视图 const counterView = new CounterView(0); // 建立模型 const counterModel = new CounterModel(0); // 添加视图为模型的观察者 counterModel.addObserver(counterView); // 建立控制器 const counterController = new CounterController( counterModel, counterView ); // Current count: 0 // 模拟用户交互 counterView.elButton.click(); // Current count: 1 MVP 模式 MVP 模式(模型-Model,视图-View,展示器-Presenter)是 MVC 模式的一个衍生模式,不同于 MVC 中的 Controller,可能更侧重于处理视图层对模型层的操作逻辑,MVP 会更专注于提升展现逻辑,即 Presenter 的功能,通常它是一个包含视图层逻辑的组件,处理用户的交互请求,然后对模型中的数据进行读写等操作,数据更新后又将数据稍做处理,格式化展现到视图中,也因此 MVP 模式的的视图层几乎不再包含任何逻辑,通常被称作被动视图(Passive View); 这种模式下展示器更像一个中介者,解耦视图与模型的相互关联,因此这也更易于进行单元测试,比如对指定视图状态的模拟;MVP 模式的整体结构有些类似下图: 交互回调 更新数据 View ------------> Controller ------------> Model <------------ <------------ 更新页面 更新通知 沿用上面 MVC 模式的计数器例子,MVP 模式下可以改造成这样: // 视图 class CounterView { // 初始化视图 constructor(text) { const body = document.querySelector("body"); const elCounter = document.createElement("div"); const elDisplay = document.createElement("div"); const elButton = document.createElement("button"); elDisplay.innerText = `Current count: ${text}`; elButton.innerText = "Add count"; elCounter.appendChild(elDisplay); elCounter.appendChild(elButton); body.appendChild(elCounter); // 提供接口供控制器操作 this.elDisplay = elDisplay; this.elButton = elButton; } } // 数据模型 class CounterModel { constructor(initValue) { this.count = initValue; } // 获取数据 getCount() { return this.count; } // 改变数据 setCount(value, callback) { this.count = value; // 数据更新后执行的回调,这里就是通知展示器 callback(value); } } // 控制器 class CounterController { constructor(model, view) { this.model = model; this.view = view; // 给视图绑定事件监听 view.elButton.addEventListener( "click", this.clickCallback.bind(this) ); } // 更新视图 updateView(value) { this.view.elDisplay.innerText = `Current count: ${value}`; } // 更新模型 updateModel(value, callback) { this.model.setCount(value, callback); } // 视图事件回调 clickCallback() { const count = this.model.getCount(); // 触发模型更新 this.updateModel( count + 1, this.updateView.bind(this) ); } } // 建立视图 const counterView = new CounterView(0); // 建立模型 const counterModel = new CounterModel(0); // 建立控制器 const counterController = new CounterController( counterModel, counterView ); // Current count: 0 // 模拟用户交互 counterView.elButton.click(); // Current count: 1 MVVM 模式 MVVM 模式:(模型-Model,视图-View,视图模型-ViewModel)是一种基于 MVC 和 MVP 的衍生模式,它试图将视图层(用户界面,UI)从业务逻辑中更清晰地分析出来,使得 UI 和逻辑层的开发工作可以同时进行,UI 交互设计师负责绑定 View 与 ViewModel,专注于用户体验,而业务逻辑开发者负责维护 Model 与 ViewModel 的联系,专注于业务需求;MVVM 模式所呈现的结构类似下图: 双向绑定 改变数据 View <-----------> ViewModel ----------> Model <---------- 数据展示 MVVM 模式最初由微软在 WPF(Windows Presentation Foundation,.NET 图形系统)中推出使用,视图层(View)与 MVC 和 MVP 中的视图一样,即用户所看见的页面结构和布局,能展示模型层的数据并接受用户的交互操作,只不过不再是被动视图,而是包含数据绑定与事件指令,是一种主动视图; 视图模型层(ViewModel)是 MVVM 与 MVC 和 MVP 形成区别的地方,它和 MVP 中的 Presenter 有些类似,包含状态与逻辑信息,负责沟通数据模型层与视图层,把数据信息转换并展示到视图层,同时也能将视图层的指令传递到模型层,或者对输入数据进行验证;视图通常使用一类标记语言或者约定的标记语法,实现与视图模型层的数据双向绑定,从而达到同步更新; 流行的 Vue 框架,就是典型的 MVVM 模式,下面通过 Vue 的语法,依然以前面的计数器为例进行简单说明: index.html: <!DOCTYPE html> <html> <head> <title>Counter</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script> </head> <body> <div id="app"> <div>Current count: 8</div> <button v-on:click="addCount">Add count</button> </div> <script src="./index.js"></script> </body> </html> 这里的 HTML 代码就可以看作视图层(View),由于框架层提供的特性,所以它相比于传统 HTML 代码有一些区别,就是使用了模板语法,能够直接在元素中使用数据变量(8),以及绑定事件(v-on:click="addCount"),而这些都是之前所说的 MVVM 模式中视图层的特性; index.js: new Vue({ el: '#app', data: { count: 0 }, methods: { addCount: function() { const count = this.count; this.count = count + 1; } } }) 这里的 JS 逻辑代码可以看作视图模型层(ViewModel),包含对数据的展示处理,以及视图的指令(事件)监听处理,当然,data 属性可以看作是模型层(Model),但一般大型应用而言,真正意义的数据模型通常是位于服务端,页面对数据的增删改查通过接口进行,客户端则一般只对数据进行临时性存储; MVVM 与 MVC 以及 MVP 的区别之处在于,ViewModel 不用像 Controller 一样不做干预,将 Model 直接暴露给 View,也不像 Presenter 一样,需要去引用 View,视图模型只是让视图将展示绑定到自己的属性上,再将模型中的数据处理后暴露给视图;当然能够轻松的使用 MVVM 带来的各种松耦合的便利,都离不开隐藏在框架之下的复杂性的支持,比如如何具体实现视图层与视图模型层的双向绑定,指令监听等,所以使用时必然需要考虑这层复杂逻辑带来的性能损耗是否在预期范围内;

2021/1/17
articleCard.readMore

Redux 主要知识学习总结

概念 Redux 作为一个状态管理器,可以应用于多种 web 技术或框架中,React 只是其中之一;Redux 的特点在于,多个页面或组件使用同一个状态(store,用于管理应用的 state),可以实现各模块或组件之间的数据共享,应用的任何部分都能进行状态修改,避免了传统的组件间深层次传值问题; 使用 创建状态(store) Redux.createStore() 方法用于创建一个 store,其接收 reducer 作为第一个参数; reducer 为一个自定义函数,接收 state 作为第一个参数,同时返回一个值作为新的 state; reduce 有缩减,减少的意思,可以理解为一个缩减器,不断将新得到的状态覆盖原状态,以实现 store 的单一状态更新,其名字也是根据 Array 的 .reduce() 方法而来的; import Redux from 'redux'; // 为 state 设置默认值 const reducer = (state = 1, action) => { return state; } const store = Redux.createStore(reducer); createStore() 方法还接收第二个参数 initialState,作为 state 的初始化值,即下面两种写法效果相同: import Redux from 'redux'; const store1 = Redux.createStore((state = 1) => { return state; }); const store2 = Redux.createStore((state) => { return state; }, 1); const state1 = store1.getState(); const state2 = store2.getState(); console.log(state1); // 1 console.log(state2); // 1 读取状态 getState(),是所创建的 store 对象的一个方法,用于获取创建的状态; const store = Redux.createStore( (state = 1) => state; ); const state = store.getState(); console.log(state); // 1 改变状态(action) 触发更新 state 的更新需要通过触发 action 来实现,actoin 是前面的 reducer 函数接收的第二个参数,一个 action 是一个包含操作信息的对象,同时也可以携带要传递的额外数据; 触发 action 使用 dispatch 实现,dispatch 是 store 对象的一个方法,其接收参数为 action 对象,是更新状态的唯一途径; 这里之所以多加一层 action,而不直接修改状态,是为了追踪某一状态为何更新,或者调试时进行操作复现等目的,而 action 中的 type 就相当于为了被追踪而留下的痕迹; const store = Redux.createStore( (state=1, action) => { if (action.type === 'myAction') { return action.myData; } else { return state; } } ); const action = { type: 'myAction', // type 属性为必填项 myData: 'myContent.', // 自定义携带数据 } store.dispatch(action); console.log(store.getState()); // "myContent." 在模块较多的复杂应用中,为了辨识操作,方便理解,通常 type 的格式会定义为 模块/操作 的形式,模块一般和对应的 reducer 相关,例如: const todoReducer = (state, action) => state; const userReducer = (state, action) => state; const addTodo = { type: 'todo/add' }; const renameUser = { type: 'user/rename' }; 响应更新 更新 state 的 action 被触发了,还需要定义一些操作对其进行响应,在 action 触发时执行,即指定如何更新 state; 这里更新 state 的逻辑写在之前创建 store 时传入的 reducer 函数中,由于 Redux 中的 state 是只读的(并未强制,但需自行在代码中遵守),所以 reducer 每次返回的 state 都是新的; const myState = { num: 0 } const myReducer = (state=myState, action) => { if (action.type === 'add') { return { num: state.num + 1 }; } else { return state; // 非指定状态需要考虑返回原状态 } } const store = Redux.createStore(myReducer); const myAction = { type: 'add' } store.dispatch(myAction); console.log(store.getState().num); // 1 Redux 并未强制 reducer 中的 state 为只读的,其实是可以对其进行修改,例如: const defaultState = { num: 0 }; const store = Redux.createStore( (state=defaultState, action) => { if (action.type === 'add') { state.num += 1; return state; } else { return state; } } ); console.log(store.getState()); // { num: 0 } store.dispatch({ type: 'add'}); console.log(store.getState()); // { num: 1 } 但官方并不建议这么做,这有可能导致页面数据得不到及时更新的 bug,所以需要开发者考虑自行维护其不可变性(Immutability),这也能实现更好的状态追踪,问题追溯等开发体验,如官网提到的一项叫 time traveling debugging 技术;并且,Redux 官网对该框架的介绍也是 Redux is a predictable state container,即具有预见性的状态管理器; 订阅状态 subscribe() 是 store 对象的方法之一,它接收一个函数作为参数,用于设置监听器以订阅状态的更新,即指定 state 更新时应该做什么; const store = Redux.createStore((state=0, action) => { if (action.type === 'add') { return state + 1; } else { return state; } }); store.subscribe(() => { // 指定每次更新状态就打印当前值 console.log('dispatch', store.getState()); }) store.dispatch({ type: 'add' }); // 'dispatch' 1 store.dispatch({ type: 'add' }); // 'dispatch' 2 拓展 状态合成 虽然 Redux 为了管理方便而设置单一的 store 对所有 state 进行统一管理,但是状态量的增长会使得书写变得复杂,所以 Redux 对象提供了一个 combineReducers() 方法,将所有声明的分工不同(不同组件、页面或子应用)的 reducer 合并为一个总的 reducer; 该方法接收一个对象作为参数,不同的属性名用于标识不同作用的 reducer,以及状态更新后从 store 中取回状态值,属性值为声明的 reducer 函数; const calcReducer = (state=1, action) => { switch (action.type) { case 'add': return state + 1; case 'minus': return state - 1; default: return state; } } const countReducer = (state=0, action) => { if (action.type === 'add') return state + 1; else return state; } const rootReducer = Redux.combineReducers({ calc: calcReducer, count: countReducer, }); const store = Redux.createStore(rootReducer); console.log(store.getState()); // { calc: 1, count: 0, } store.dispatch({ type: 'add' }); console.log(store.getState()); // { calc: 2, count: 1 } combineReducers() 参数对象中指定的属性名用于存储该 reducer 的所有状态值; Enhancer Redux.createStore() 方法还可以接收第三个参数 enhancer,用于自定义 store 的功能或强化其能力(例如魔改),比如改变 dispatch(), getState(), subscribe() 等方法的默认行为; enhancer 参数为一个自定义函数,其接收 Redux.createStore 这个方法作为参数,并返回一个新的 createStore 方法; 下面是一个为 dispatch 添加功能的简单示例: const myReducer = (state=1, action) => { if (action.type === 'add') { return state + 1; } else { return state; } } // enhancer 接收一个参数,即 Redux.createStore 这个方法, // 用于执行创建 store 的默认行为; const myEnhancer = (createStore) => { // enhancer 需要返回一个函数,其参数与 Redux.createStore 的相同, // 可以理解为返回另一个新的 createStore 函数; return (reducer, initialState, enhancer) => { // 需要执行一次 Redux.createStore 的默认行为,并获取 store const store = createStore(reducer, initialState, enhancer); // 修改 store 中的默认 dispatch 方法 store.dispatch = (action) => { // 新加的功能 console.log('dispatched.'); // 最后仍需执行一次 dispatch 的默认行为 return store.dispatch(action); } // 修改默认的 getState 方法 store.getState = () => { return store.getState() + 1; } // 返回的新 createStore 方法还需要返回一个对象,即整个 store 对象; return store; } } } const store = Redux.createStore(myReducer, undefined, myEnhancer); store.dispatch({ type: 'add' }); // "dispatched." console.log(store.getState()); // 3 需要同时使用多个 enhancer 时,需要进行合成,可以使用 Redux.compose() 方法: const enhancers = Redux.compose(enhancer1, enhancer2); // 可以传入多个参数作 enhancer const store = Redux.createStore(myReducer, undefined, enhancers); Middleware 大多数时候,我们只希望自定义 dispatcch 方法的逻辑,所以官方专门提供了一个叫 middleware 的特性,翻译过来就是中间件,即在触发 action 和调用 reducer 执行响应之间,给用户提供一个可操作空间,如用于日志记录,问题报告,或者处理异步操作等; middleware 是一个自定义函数,其接收一个对象作为参数,该参数对象有两个方法,分别是 dispatch 和 getState,逻辑都与 store 对象中的两个同名方法相同; middleware 函数还需要返回另一个函数作为包装(自定义)后的 dispatch 方法,由于逻辑层次较多,下面会通过代码说明; Redux 中内置了一个叫做 applyMiddleware 的 enhancer 方法,用于添加 middleware,它可以接收多个参数以传入多个 middleware; 具体实现通过举例说明: const myReducer = (state, action) => { if (action.type === 'add') { return state + 1; } else { return state; } } // 自定义的中间件函数 const myMiddleware1 = ({ dispatch, getState }) => { // 中间件需要返回一个函数,即新的 dispatch 逻辑, // 该函数又接收一个参数 next,用于执行下一个 middleware, // 当然如果有下一个中间件就执行,没有了就执行原始的 dispatch, // 其实这个参数 next 也就是原始的那个 store.dispatch 方法; return (next) => { // 该函数也需要返回一个函数,用于处理 action, // 接收一个 acction 作参数,即 store 触发的 action; return (action) => { // 自定义逻辑 console.log('mid 1', getState()); // 这个函数还需要返回一个函数,即用之前的 next 方法 // 将 action 传递给下一个 middleware 继续处理; return next(action); } } } // 也可以使用简写方式 const myMiddleware2 = ({ getState }) => next => action => { console.log('mid 2', getState()); const result = next(action); console.log('mid 2 new', getState()); return result; } // 使用中间件 const myEnhancer = Redux.applyMiddleware(myMiddleware1, myMiddleware2); const store = Redux.createStore(myReducer, 1, myEnhancer); store.dispatch({ type: 'add' }); // "mid 1" 1 // "mid 2" 1 // "mid 2 new" 2 console.log(store.getState()); // 2 总结一下整个执行过程就是: 用户调用了 store.dispatch() 触发 action; Redux 按 applyMiddleware() 方法中参数的传入顺序,挨个执行自定义的 middleware 逻辑; 然后再调用原始的 store.dispatch() 方法触发 action; 最终执行 reducer 中的逻辑; 整个过程有些类似函数的链式调用: dispatch -> middleware1 -> middleware2 ... -> dispatch -> reducer 此外,由于 middleware 的执行逻辑,其特性还包括对 action 中数据的修改、中断甚至彻底停止 action,的触发,例如上例中最后不返回 next(action),那么整个过程执行完第一个 middleware 就结束了,state 也不会发生预期的改变; 处理异步逻辑 Redux 内部并不知道如何处理异步逻辑,只会同步的触发 action,然后调用 reducer 更新 state,所以任何异步逻辑需要我们在外部自己实现;而 Redux 的宗旨是 recuder 不要有任何副作用,最好是一个纯函数,即不要有多余的外部联系,如控制台打印,异步请求等; 而 middleware 就是 Redux 专为副作用逻辑需求而设计的,这里以异步操作为例用代码进行简单实现: const reducer = (state, action) => { if (action.type === 'add') { return state + 1; } else if (action.type === 'asyncAdd') { return action.data; } else { return state; } } const asyncMiddleware = _store => next => action => { if (action.type === 'asyncAdd') { setTimeout(() => { action.data = 'some data.'; next(action); }, 2000); } else { next(action); } } const enhancer = Redux.applyMiddleware(asyncMiddleware); const store = Redux.createStore(reducer, 0, enhancer); store.dispatch({ type: 'add' }); console.log(store.getState()); // 1 store.dispatch({ type: 'asyncAdd' }); console.log(store.getState()); // 1 setTimeout(() => { console.log(store.getState()); }, 2000); // 2 秒后输出: // some data. 结果显示异步操作获取的数据,可以成功被 reducer 拿到并实现相应的逻辑,所以把 setTimeout 换成 Ajax 请求也同样可以从服务器获取到数据,然后传递给 Redux 进行下一步处理; 由于上面的异步逻辑的原生写法不太方便,Redux 官方就提供了一款 redux-thunk 工具,封装好了一个 middleware,应用之后就可以将 action 声明为一个函数(以前是一个对象),其接收 dispatch 和 getState 两个参数;具体用法如下: import Redux from 'redux'; import ReduxThunk from 'redux-thunk'; const reducer = (state, action) => { if (action.type === 'add') { return state + 1; } else if (action.type === 'asyncAdd') { return action.data; } else { return state; } } // 直接应用该工具 const middlewareEnhancer = Redux.applyMiddleware(ReduxThunk.default); const store = Redux.createStore(reducer, 1, middlewareEnhancer); // 这里 action 声明为函数,处理异步逻辑 const asyncAction = (dispatch, getState) => { console.log('old state:', getState()); setTimeout(() => { dispatch({ type: 'asyncAdd', data: 'some data.' }); }, 2000); } store.dispatch(asyncAction); // "old state:" 1 setTimeout(() => { console.log(store.getState()); }, 2000); // 2 秒后输出: // "some data." 需要注意的是,一些教程上(包括 Redux 官网)介绍 Redux Thunk 的用法时,仍然使用的 Redux.applyMiddleware(ReduxThunk) 写法,这是该工具 1.x 版本的写法,现在 2.x 版本需要加上 .default,即 Redux.applyMiddleware(ReduxThunk.default),不然程序会出现问题;

2020/11/29
articleCard.readMore

记一次 React 组件无法更新状态值的问题分析与解决

问题 React 组件中通过直接声明的元素变量(jsx 写法),在访问 state 中指定的状态值时,如果状态发生改变,使用状态值的元素内容无法得到相应更新; 下面的例子中,直接在 class 组件中声明元素变量 myDiv,并且需要访问 this.state 中的数据,最终对状态值进行展示,按钮用于改变状态值: import React from 'react'; class App extends React.Component { state = { msg: 'hello', }; myDiv = <div>{this.state.msg}</div>; handleClick() { this.setState({ msg: 'world', }) } render() { return ( <> <div>{this.myDiv}</div> <button onClick={() => this.handleClick()}> change </button> </> ); } } export default App; 按理说点击按钮后,状态发生改变(this.state.msg),页面的值会发生相应更新,但是页面内容并未发生相应改变,这其实是一个微小的细节问题,下面对其进行展开分析; 分析 上例中,在组件中直接声明了值为元素的一个变量 myDiv,并且其内容调用了状态值(this.state.msg),其实该变量在声明时状态内容直接被赋予了 this.state.msg 的当前值,并非想象中的引用值,然后状态改变(this.setState())时,React 会重新调用组件的 render() 方法,重新渲染组件内容,但是此时该变量中的状态值仍是之前被赋予的字面值,不会再访问一次当前的 state,所以其值最终也就不会发生相应的改变; 并且在一般的写法中,涉及访问状态的逻辑(如 {this.state.msg})一般都写在整个 render() 方法之中,这样每次状态的改变导致 render 方法重新执行,使得重新执行获取状态的逻辑,就能每次都访问到最新的状态值了;但有时又很难避免在复用组件时在 render 方法以外的地方访问 state,并期望数据被动态改变,这里就需要寻求其他解决方案; 解决方案 方法一 我们可以使用 getter 方式来声明变量,getter/setter 方法是声明对象属性的一种方式,可以实现该对象指定属性的属性值的访问控制(getter)以及修改控制(setter),下面是一个简单的使用示例: var obj = { num: 1, get a() { return this.num; } set b(n) { this.num = n; } } console.log(obj.a); // 1 obj.b = 2; console.log(obj.a); // 2 getter 声明的属性的特点是,每次调用对象的该属性,都会执行一次 getter 函数内的逻辑,然后返回 return 的值;所以如果把之前组件中的 myDiv 属性以 get 方式进行声明,这样每一次状态改变后,render() 方法重新执行,然后就会涉及对该变量的访问,导致重新执行 getter 方法中的逻辑,最后就能访问到改变后的状态值(this.state.msg),页面也就相应地更新了; 改造后的组件代码: import React from 'react'; class App extends React.Component { state = { msg: 'hello', }; get myDiv() { return <div>{this.state.msg}</div>; } handleClick() { this.setState({ msg: 'world', }) } render() { return ( <> <div>{this.myDiv}</div> <button onClick={() => this.handleClick()}> change </button> </> ); } } export default App; 方法二 类似使用 getter 的思路,为了让每次状态改变,用到状态的变量也发生相应的更新,另一种方法就是将变量 myDiv 声明为函数类型,同样也能使每次获取变量时都重新执行一次获取状态的逻辑,以获取最新状态值,改造后代码如下: import React from 'react'; class App extends React.Component { state = { msg: 'hello', }; myDiv = () => <div>{this.state.msg}</div>; // 或者是: // myDiv() { return <div>{this.state.msg}</div> }; handleClick() { this.setState({ msg: 'world', }) } render() { return ( <> <div>{this.myDiv()}</div> <button onClick={() => this.handleClick()}> change </button> </> ); } } export default App; 不同之处就是每次调用变量 myDiv 时需要使用函数式调用(后面加一对括号),为了方便多处调用,显然方法一更有优势;

2020/9/10
articleCard.readMore

React 组件间传值的几种情形

父级传向子级 这应该是最常见的一种场景,通过在子组件上写 props,将数据从父组件中传递到子组件,子组件再从 this.props 中获取相应的值,这样可以根据传入值的不同返回不同的状态,即实现组件的复用;例如: import React from 'react'; // 父组件 class Parent extends React.Component { message = 'Hello world!'; render() { return ( <Child myProp={this.message} /> ); } } // 子组件 class Child extends React.Component { message = this.props.myProp; render() { // Hello world! return ( <div>{this.message}</div> ); } } export default Parent; 多层传值 上述方法只用于单层数据传递,即父级传向子级,如果子级又存在子级,甚至向下递推,那么父组件要传值给后代组件,就要逐层向下传递,类似下面的情况: import React from 'react'; class Parent extends React.Component { render() { return <Child myProp="hello" /> } } class Child extends React.Component { render() { return <Grandchild myProp1={this.props.myProp} /> } } class Grandchild extends React.Component { render() { // hello return <div>{this.props.myProp1}</div> } } export default Parent; 如果层数再多一些就是书写噩梦了,所以 React 提供了 context 机制,解决了深层传值的问题,现在来改造上面的代码: import Reac from 'react'; // 首先需要创建一个自定义 context // 该方法接收一个参数作为 context 的默认值 const myContext = React.createContext(); // 获取包裹组件,用于包裹需要应用 context 的组件 const { Provider } = myContext; class Parent extends React.Component { value = { message: 'hello', }; render() { // 通过包裹器的 value 属性向下传递指定值 return ( <Provider value={this.value}> <Child /> </Provider> ); } } class Child extends React.Component { render() { return <Grandchild /> } } class Grandchild extends React.Component { // 在需要获取祖代传递的 context 值的后代组件中, // 声明 contextType 静态属性,值为之前创建的 context; static contextType = myContext; render() { // 最后使用 this.context 就能获取到之前 // 在 Provider 中传入的 value 值; return <div>{this.context.message}</div> // hello } } export default Parent; 需要注意的是,由于提供方和调用方需要使用同一个使用 React.createContext() 创建的 context,所以如果父组件和要调用 context 的子组件不在同一个文件中的话,则需要考虑通过 export 和 import 来实现引用,但是这样组件间的耦合度又增加了一层,React 官方建议使用 组合 方式取代上述的 继承 方式,下面再次对上述代码进行改造: import React from 'react'; class Parent extends React.Component { value = { message: 'hello', }; render() { return ( <Child> {this.value.message} </Child> ); } } class Child extends React.Component { render() { // this.props.children 会指向父组件在子组件中嵌入的数据或组件 return ( <div>{this.props.children}</div> ); } } export default Parent; 子级传向父级 React 中似乎没有提供子级向父级直接传值,类似 props 的方法或途径,可以通过一些间接手段实现,开发中常见的处理方式就是子组件调用父组件通过 props 传入的处理函数,对需要传递的值进行处理;例如: import React from 'react'; class Parent extends React.Component { state = { message: '', }; handleMsg(msg) { this.setState({ message: msg, }); } render() { // Hello return ( <> {this.state.message} <Child onMsg={(msg) => this.handleMsg(msg)} /> </> ); } } class Child extends React.Component { handleClick() { this.props.onMsg('Hello'); } render() { return ( <button onClick={() => this.handleClick()} >Clike me</button> ); } } export default Parent; 简单梳理一下流程: 父组件提前声明数据处理逻辑,该方法接收传入的值,然后进行相应处理; 父组件将该方法通过 props 传递给子组件; 子组件触发一些行为,得到了将要传递给父组件的值; 子组件通过 this.props 调用父组件传入的处理函数,并将要传递的值作为该函数的参数; 处理函数开始执行,由于其是在父组件的作用域中声明的,所以也能访问父组件中的一些数据,比如 state,相当于在父组件中处理子组件传入的数据; 处理函数更新 state 状态值,随后其他访问该 state 的地方也会随即更新; 同级间传递 状态提升 这是 React 官网提到的一个概念,即多个组件都在重复使用同一个状态值,可以将这个值 提升 至父组件中保存,相当于数据复用;当然如果想实现一个子组件向另一个子组件传值,也可以通过父组件这层“媒介”;下面举例说明: import React from 'react'; class Parent extends React.Component { state = { msg: 'hello', } handleChangeMsg(msg) { this.setState({ msg: msg, }); } render() { return (<> <ChildLabel msg={this.state.msg} /> <ChildButton onChangeMsg={(msg) => this.handleChangeMsg(msg)} /> </>); } } class ChildLabel extends React.Component { render() { return <span>{this.props.msg}</span> } } class ChildButton extends React.Component { render() { return ( <button onClick={() => this.props.onChangeMsg('world')} >Change</button> ) } } export default Parent; 这样在页面中点击子组件 <ChildButton /> 中的按钮时,子组件 <ChildLabel /> 中的文本便会发生相应变化,成功获取传入的值;这里的操作相当于子组件先向父组件传值,使得父组件状态变化,然后使用该状态的另一子组件就会相应地变化; Refs Refs 是 React 提供的另一种机制,React 中典型数据流是通过 props 传递,而 refs 则相当于提供了直接操纵组件或 DOM 元素的一种途径,强制修改元素;因此官网也建议避免过度使用 refs,防止应用变得难以理解或“失控”;下面通过举例简单说明其用法: import React from 'react'; class Parent extends React.Component { // 首先需要声明 ref myRef = React.createRef(); handleClick(msg) { // 通过 ref 的 current 属性实现对该元素的引用, // 然后就能想操作正常 DOM 一样实现控制; this.myRef.current.innerText = msg; } render() { // 对需要被引用的元素使用 ref 属性,值为之前所创建的 ref return (<> <span ref={this.myRef}>hello</span> <button onClick={() => this.handleClick('world')} >Change</button> </>); } } export default Parent; 在页面上点击按钮后,前面的文本同样会发生改变,即 DOM 元素的元素属性 innerText 值被成功修改,如需使用其他原生属性或方法同理; Refs 转发 虽然 Refs 提供了直接访问组件或元素的途径,但是它却访问不了组件中的组件,这是 React 层故意为之,隐藏组件实现细节与渲染结果,防止组件的 DOM 结构被过度依赖;但有一些特殊情况下确实需要访问组件内部的组件的话,React 也提供了另外一种机制,即 Refs 转发(Refs Forwarding);顾名思义,组件 A 不能直接使用 ref 访问组件 B 中的组件 C,但是可以通过组件 B 转发 ref 给组件 C,这里改造一下上面 Refs 中的例子,我们在 <Parent /> 和 <span> 之间再加一层组件,再实现对其的操作: class Parent extends React.Component { myRef = React.createRef(); handleClick(msg) { this.myRef.current.innerText = msg; } render() { // 对子组件正常使用 ref return ( <Child ref={this.myRef} handleClick={this.handleClick.bind(this)} /> ); } } // 使用 React.forwardRef 方法转发 ref 给下一层组件 const Child = React.forwardRef((props, ref) => { return (<> <span ref={ref}>hello</span> <button onClick={() => props.handleClick('world')} >Change</button> </>); }); Refs 转发需要使用 React.forwardRef() 方法创造组件,该方法接收一个回调函数做为参数,该回调函数接收两个入参,第一个是传进组件的 props,第二个是传进组件的的 ref,通过内部逻辑决定 ref 再转发给谁,回调函数的返回值是最终生成的组件;页面加载组件后,点击按钮,就能像直接使用 ref 一样改变展示的文本值了;

2020/9/3
articleCard.readMore

提升 Linux 终端命令敲写效率的快捷键参考

移动 快捷键 描述 Ctrl + B 光标向前移动一个字符(Backward) Ctrl + F 向后移动一个字符(Forward) Alt + B/Ctrl + → 向前移动一个单词 Alt + F/Ctrl + ← 向后移动一个单词 Ctrl + A/Home 移动到行首(Ahead) Ctrl + E/End 移到行尾(End) Ctrl + XX 行首与当前光标之间切换位置(方便在两处修改) Ctrl + T 交换光标处与左边一个字符位置(Transform) Alt + T 交换光标处和左边一个单词位置 编辑 快捷键 描述 Ctrl + L 清屏(不包含输入为运行的命令) Ctrl + Shift + C 复制(到系统粘贴板) Ctrl + Shift + V 粘贴(来自系统粘贴板) Alt + . 粘帖上次命令的最后一个参数 Ctrl + H/Backspace 向左删除一个字符 Ctrl + D/Del 向右删除一个字符(Delete) Ctrl + W/Alt + Backspace 向左剪切一个单词(到终端粘贴板) Ctrl + Del/Alt + D 向右剪切一个单词 Ctrl + U 剪切光标左边所有内容(不含光标处) Ctrl + K 剪切光标处以及右边所有内容 Ctrl + Y 粘贴(来自终端粘贴板) Alt + Y 粘贴(按下 Ctrl+Y 后,由新到旧选择历史剪切内容粘贴) Alt + C 光标处往后一个单词首字母大写(capital) Alt + U 光标处往后一个单词字母全大写(Upper case) Alt + L 光标处往后一个单词字母全小写(Lower case) 控制 快捷键 描述 Ctrl + M/Ctrl + O 运行命令(类似 Enter 键) Ctrl + C 终止命令(未执行的输入命令会清除,已运行则中止进程) Ctrl + D 退出终端(无未执行命令时) Ctrl + Z 挂起命令(切换到后台继续执行) Ctrl + S 阻止屏幕输出(当前在运行持续输出的命令时,如 watch 命令) Ctrl + Q 允许屏幕输出(阻止后恢复输出) Ctrl + P/↑ 切换上一条命令 Ctrl + N/↓ 切换下一条命令 Shift + PageUp 向上翻页(历史记录) Shift + PageDown 向下翻页 Ctrl + R 搜索历史记录(输入后立即在输入栏显示匹配项) Ctrl + G 退出历史搜索(按 Esc 退出会保留匹配项) 特殊命令 以下命令可以避免重复输入冗长的命令; 快捷键 描述 !! 获取上一条命令(会输出所执行的完整命令及其结果) !xx 获取最近一条以 xx 开头的命令(xx 为任意字符) !xx:p 类似 !xx,只是不会执行,只输出完整命令 !$ 获取上一条命令的最后一个参数 !$:p 输出上一条命令的最后一个参数(不执行) !* 获取上一条命令的所有参数 !*:p 输出上一条命令的所有参数(不执行) ^xy 执行上一条命令中删除了 xy (任意字符)后剩下的内容 ^ab^xy 执行上一条命令中 ab 替换为 xy 后得到的内容(只替换第一个匹配项) 参考链接: https://linuxtoy.org/archives/bash-shortcuts.html https://github.com/hokein/Wiki/wiki/Bash-Shell%E5%B8%B8%E7%94%A8%E5%BF%AB%E6%8D%B7%E9%94%AE

2020/5/24
articleCard.readMore

提升开发效率的 Chrome 开发者工具快捷键参考

本文只介绍 Chrome 一些主要和常用的快捷键,在其它浏览器的开发者工具中可类比,大部分快捷键作用都相同,只有个别差异。 全局(所有面板) 功能 快捷键 描述 打开开发者工具 F12 / Ctrl + Shift + I 普通打开 打开 Console Ctrl + Shift + J 打开工具并进入 Console 面板 检查元素 Ctrl + Shift + C 进入 Elements 面板后开始选取页面元素 开发者工具设置 ?/F1 开发者工具的所有相关设置 切换上一个面板 Ctrl + [ 向左切换一个面板 切换下一个面板 Ctrl + ] 向右切换一个面板 切换工具停靠位置 Ctrl + Shift + D 即整个工具相对于浏览器的停靠位置,具体在悬浮、左侧、右侧、底部哪两个间切换,取决于最近两次操作 切换设备模式 Ctrl + Shift + M 普通模式和响应式设备编辑模式间切换 切换抽屉工具栏 Esc 切换显示工具底部的弹出式工具栏,如 Console 等 刷新网页 F5/Ctrl + R 普通刷新页面,会读取 memory cache 和 disk cache 强制刷新 Ctrl + F5/Ctrl + Shift + R 禁用缓存刷新,类似于勾选 disable cache 后刷新 当前面板中搜索 Ctrl + F 当前面板中搜索文本,如 Elements, Console 等 所有资源中搜索 Ctrl + Shift + F 在当前网页加载的所有资源(Sources)中搜索文本 按文件名搜索 Ctrl + O/Ctrl + P 同样搜索所有资源,只是搜索对象限制在文件名称上 放大字体 Ctrl + + 放大整个开发者工具的字体与图形(不包括网页) 缩小字体 Ctrl + - 缩小开发者工具字体与图形的尺寸 恢复字体大小 Ctrl + 0 将开发者工具的字体与图形尺寸恢复默认大小 Elements(元素)面板 功能 快捷键 描述 撤消更改 Ctrl + Z 撤销对 HTML 内容的修改 重做更改 Ctrl + Y 恢复撤销前的修改 展开节点 →(右方向键) 展开当前折叠的 HTML 元素节点 折叠节点 ←(左方向键) 折叠当前展开的 HTML 元素节点 递归展开节点 Alt + → 递归地展开当前折叠的 HTML 元素节点 递归折叠节点 Alt + ← 递归地折叠当前展开的 HTML 元素节点 编辑节点属性 Enter 编辑所在元素节点的属性值(第一个属性的) 编辑下个属性 Tab 按下 Enter 编辑属性后,按 Tab 可以依次切换到后面的属性 编辑上个属性 Shift + Tab 切换到当前元素节点的上一个属性值进行编辑 隐藏节点 H 在页面中隐藏当前元素节点,实质是设置 visibility: hidden !important; 样式 编辑 HTML F2 以 HTML 的形式编辑当前元素节点内容,类似于右键菜单的 Edit as HTML 编辑源样式 Ctrl + 点击 Style(样式)栏中,Ctrl + 鼠标点击 CSS 规则(选择器、属性或属性值)时,会跳转到源文件的相应位置 切换颜色类型 Shift + 点击 Shift + 点击样式颜色值左边的取色框时,色值类型会在 #, rgb(), hls() 之间切换 增/减数值 ↑/↓(上、下方向键) 单次增/减大小为 1 单位 0.1 倍增/减值 Alt + ↑/Alt + ↓ 单次增/减 0.1 倍单位值 10 倍增/减值 Shift + ↑/Shift + ↓ 单次增/减 10 倍单位值 100 倍增/减值 Ctrl + ↑/Ctrl + ↓ 单次增/减 100 倍单位值 Console(控制台)面板 功能 快捷键 描述 清空输出 Ctrl + L 清空控制台的历史输出 聚焦输入 Ctrl + `(反引号) 把输入焦点聚焦到控制台输入框中,其它面板中使用则打开 Console 抽屉栏 接受建议 Enter/→ 接受代码提示的第一个建议项 切换命令 ↑/↓ 切换上/下一条历史输入命令 运行代码 Enter 回车运行输入的代码 换行输入 Shift + Enter 由于直接回车是运行代码,所以想换行输入需要加 Shift Sources(资源)面板 功能 快捷键 描述 执行脚本 F8/Ctrl + \ 切换执行或暂停脚本 单步执行 F10/Ctrl + ' (Debugger 栏中 )单步调试脚本 单步进入 F11/Ctrl + ; 单步进入调试(遇到函数就跳入) 单步跳出 Shift + F11/Ctrl + Shift + ; 单步跳出调试(跳出当前函数) 下个调用帧 Ctrl + .(英文句号) 调试时切换到调用栈(Call Stack 栏)中当前帧的下面一帧 上个调用帧 Ctrl + ,(英文逗号) 调试时切换到当前调用的帧上面一帧(当前为栈顶则忽略) 输出变量值 Ctrl + Shift + E 在控制台输出编辑器中所选中的变量的值 观察变量值 Ctrl + Shift + A 将编辑器中选中的变量值添加到 Watch 栏中 切换断点 Ctrl + B 当焦点位于代码编辑器中时,用于切换当前行是否设置为断点 切换启用断点 Ctrl + Shift + B 切换当前行的断点是否启用(不会删除断点) 切换启用全部断点 Ctrl + F8 切换所有设置的断点是否启用 编辑断点 Ctrl + Alt + B 编辑当前行的断点类型(Breakpoint, Conditional Breakpoint, Logpoint) 代码编辑器(Sources 面板中) 功能 快捷键 描述 切换注释 Ctrl + / 为当前行或选中内容添加/取消注释 保存修改 Ctrl + S 保存对文件的修改(需要先启用侧栏的 Overrides 功能) 保存所有更改 Ctrl + Alt + S 保存编辑器打开的所有文件,同样需要启用 Overrides 转到匹配括号 Ctrl + M 跳转到与当前光标位置的括号匹配的另一个括号处 添加匹配项 Ctrl + D 跳转并添加选择下一个与选择文本匹配的文本 撤销匹配项 Ctrl + U 返回并撤销选择一个匹配项,与 Ctrl + D 相反 转到行 Ctrl + G 跳转到指定行数 转到成员 Ctrl + Shift + O 跳转到当前文件中的指定成员函数 上个编辑处 Alt + - 跳转到上一个光标位于的编辑位置 下个编辑处 Alt + + 跳转到下一个编辑位置 关闭活动标签 Alt + W 关闭当前编辑的标签(文件) 增/减CSS值 Alt + ↑/Alt +↓ 编辑 CSS 源文件时,可对当前数值增/减 1 一个单位 10 倍增/减CSS值 Alt + PageUp↑/Alt + PageDown CSS 中对当前数值增/减 10 一个单位

2020/5/23
articleCard.readMore

记一次Windows电脑开机登录后黑屏的问题分析与排查

AMD 显卡为了防止因为频率太高导致系统不稳定。所以在 AMD 显卡上推出了一个 ULPS 功能,就是用户无操作的时候自动降频,休眠,然后用于节电。想法是好的,但是有人用了导致黑屏。所以出了一个关闭此功能的工具,它可以用于检测这个功能的开关状态,并直接关掉。 不过这个问题能在我的 Intel 中出现也是很迷;另外文章还有提到: > - ULPS是休眠状态 ,降低非主卡的频率和电压的以节省电能,缺点就是可能会导致性能的损失和一些交火不稳定。 > - 经常用电池的不建议关闭ULPS,因为关闭后显卡一直工作在独显状态。 细想以前似乎从未动过这个功能,这么冒然改好像有点简单粗暴,之后还可能会得不偿失,所以这个方案暂时存着,先找找其它方面的问题; ## 自行排查 ### 系统服务 就像之前说的,搜索问题有时候并不能得到有效的解决方案,但是某些回复的解决手段或者思路是可以起到一定程度的启发作用的,比如某一条大致说的是排查系统服务的问题,确实,之前分析时把问题定位在系统层面,排查过了启动项,但是 **服务** 这一块还没测试,所以先打开 `msconfig`,`Win+R` 后输入: ![msconfig-open.png](https://i.loli.net/2020/05/20/yec8OnPfowgRaqF.png) 然后进入服务模块: ![msconfig-service.png](https://i.loli.net/2020/05/20/KgP5yZrG4AYoabQ.png) 这里列出的就是系统中的所有服务项,前面打勾代表已启用,否则是禁用,这里的思路就是先都禁用了,然后重启如果正常则挨个启用排查是哪一项服务的问题,当然这样工作量有点大,全部禁用也可能会出现额外的问题,所以可以先试试系统自带的诊断启动,会加载一些基本服务和设备,就是点击上图顶部最左侧的 **常规** 模块,然后选择 **诊断启动**: ![msconfig-diagnose-boot.png](https://i.loli.net/2020/05/20/L3IlZ2dJMxRBubN.png) 点确定或应用后重启系统,这次就愉快又快速的进入系统桌面了(证明禁用不必要的服务确实能提高开机速度),不过也会发现某些模块无法使用,比如喇叭和屏幕亮度,甚至提示某些系统错误,很正常,因为只启用了基本服务,其它的系统服务和模块就没有加载,不过不影响问题排查就行了; ### 系统日志 当然,排查问题怎么少的了日志分析,可以起到一定辅助作用,于是这时就想起了 Windows 自家的法宝 **事件查看器**(由于平时也基本也怎么用过),平时用惯了 Linux 命令行分析日志,突然一切可视化了还不太习惯,先打开熟悉一下操作再说: ![event-win-search.png](https://i.loli.net/2020/05/20/rckZ8LMRA7YO6t9.png) 点开 Windows 日志: ![eventlog-windows-log.png](https://i.loli.net/2020/05/20/VNi7eWsDgdE84GA.png) 再看看包含事件的几项: ![eventlog-app.png](https://i.loli.net/2020/05/20/lYFzVDdJ62CHBvj.png) ![eventlog-security.png](https://i.loli.net/2020/05/20/KWTXPaz51S7picD.png) 这里由于分析问题可能是出在系统层面上,所以先关注 Windows 相关的事件,应用程序的暂且不管(其实也是因为点开它后发现应用数量有些庞大,不好找落脚点 ╮(╯▽╰)╭ ),然后就是挨个进到每一项中,点击右侧操作栏的**清除日志**按钮把日志分别清空: ![eventlog-clear.png](https://i.loli.net/2020/05/20/e2TqM3rIoicgZRx.png) 这样如果后续操作时问题复现了,就可以较精确的定位了; ### 服务排查 然后可以把事件查看器关闭,再次打开 `msconfig`,选择诊断启动,再切到服务模块,可以看到大部分服务都没有被勾选了,然后我们点一下“服务”这个表头,让项目按名称顺序排列,方便后续操作: ![msconfig-sort-by-name.png](https://i.loli.net/2020/05/20/HZ8UNyD95i6QRor.png) 然后就是重点的排除环节了,这里大致数了一下,有 400 个左右的服务项,如果挨个勾选再重启检查的话,可能也就写不出这篇文章了,所以需要找一个高效的办法,之前搜索问题时也受到一位小伙伴的启发,可以使用 **二分查找** 法进行排除,这本来是算法中的一种解决方案,没想到被这样给实际应用了(~ ̄▽ ̄)~,这里通俗讲就是先勾选一半的服务项目,比如从第一条开始,一直勾选直到右侧滚动条运动到大概中点的位置(好像工作量也不小,看手速咯),前面已经对服务名称进行过排序,所以这里前半部分服务大致是字母开头是 `A - P` 的服务项: ![msconfig-check-a-p.png](https://i.loli.net/2020/05/20/NxIhDsUypc31LbE.png) 重启系统后正常进入桌面,证明问题不在勾选的前半部分服务项中,可以排除掉,接下来我们再把剩下的没有勾选的服务项,勾选它们的前半部分,也就是说现在还有总量的最后四分之一部分没有被勾选,这样排除确实挺快,然后就是清除全部 Windows 日志,重启,再重复这些工作,直到问题复现(登录黑屏); 于是乎在进行到 `W` 字母开头的服务项排查时,登录终于黑屏了,虽然有些幸灾乐祸,但是却代表定位到问题了;然后就是继续二分,缩小范围,最终定位如下图所示: ![msconfig-uncheck-web-account.png](https://i.loli.net/2020/05/20/mZPgpqOarJB6sWu.png) 也就是说罪魁祸首是这个名为“web 账户管理器”的服务项,看制造商应该是一项系统服务,并且之前搜索时看到有几位小伙伴定位的服务项是“App Readiness”,所以这个会因不同系统环境而不同,不应该一概而论冒然禁用;当然把它禁用后问题就**解决**了,没有像之前一样修改注册表,但是再次本着职业精神(no zuo no die),就继续分析一下问题的具体原因; ### 日志分析 #### 日志概览 每一次统计的系统日志就在这时候发挥作用了,因为每一次重启前都清除了日志,所以每次记录的也就是当前排查项的事件,下面看一下记录的日志情况: ![eventlog-app-apperr.png](https://i.loli.net/2020/05/20/7qgA6YcxHBOaDLZ.png) ![black-sys-errlog.png](https://i.loli.net/2020/05/20/PJanBDdlA4kv5ZE.png) 分别查看不同事件,可以 显示详细信息: ![black-sys-err-dhcp.png](https://i.loli.net/2020/05/20/CoM5NGHnkl8tgTi.png) ![black-sys-err-scm-ops.png](https://i.loli.net/2020/05/20/1tLIeDmEuXxSrbW.png) #### 日志筛选 可以看到即使单次记录的日志量也是很庞大的,所以现在可以使用事件查看器的**日志筛选**功能了,即点击右侧操作栏的**筛选当前日志**按钮,会弹出筛选设置窗口: ![eventlog-filter.png](https://i.loli.net/2020/05/20/Whd6uxaFLikn57f.png) 首先是记录时间,即指定事件的起始和结束时间点,可以在开机和桌面显示后分别记录一个时间,然后选择这个时间区间就能进一步缩小范围; ![eventlog-filter-time.png](https://i.loli.net/2020/05/20/DLo1dfYI8UyHezx.png) 然后是时间组别,浏览也会发现事件主要分为信息、警告和错误,这里我们只用关心**错误**类型的事件,勾上后下面的项目暂时不用关,点确定; ![eventlog-filter-config.png](https://i.loli.net/2020/05/20/NkmrXzZR5lKGdpe.png) 下面就是筛选结果,可以看到错误信息还挺多, ![eventlog-filter-done.png](https://i.loli.net/2020/05/20/vqGgIDwxbWpL6lT.png) 对比黑屏时产生的错误日志,可以发现“应用程序”项的错误在正常进入桌面时也有发生,所以可以暂时排除这一项,而“安全”这一项,都是信息类,并没有错误类事件,所以也排除,最后就只剩“系统”这一项中的错误日志存在差异,存在差异的事件包括名为 `Service Control Manager` 和 `DistributedCOM` 的事件“来源”中; ![eventlog-app-err-of-black.png](https://i.loli.net/2020/05/20/41OykxKat78FhLi.png) ![eventlog-sys-err-of-black.png](https://i.loli.net/2020/05/20/1Lkr5Uwgcjodvl3.png) #### 对比分析 那么我们就来对比一下“系统”中产生的错误日志的差异,只是事件查看器似乎没有内置日志对比的功能,所以只能使用较为原始的办法,先选中想要分析的事件: ![eventlog-select-event.png](https://i.loli.net/2020/05/20/oBW3fsAN7KrJZO4.png) 再点击右侧的**保存选择的事件**按钮,保存事件日志文件到任意位置: ![eventlog-save-selected-log.png](https://i.loli.net/2020/05/20/7kif9mCnGgQD2NX.png) 像这样分别记录和保存发生黑屏问题和未发生问题时的事件,然后点击“打开保存的日志”,就能导入两个日志文件就行下一步分析了: ![eventlog-open-saved-log.png](https://i.loli.net/2020/05/20/NzOyr49GwTdnQvg.png) 另外发现每个事件似乎都对应着一个唯一的 **事件 ID** 值,可以通过这个把两个日志文件重复的地方剔除,这就要使用筛选功能里的事件 ID 排除选项了: ![Inkedeventlog-filter-exclude-id.png](https://i.loli.net/2020/05/20/e7Crcf4RqUGz6i1.jpg) 填入重复的事件 ID,用逗号隔开,前面加负号 `-` 表示排除该 ID 的事件,不加表示包括,筛选结果如下: ![eventlog-filter-compare-result.png](https://i.loli.net/2020/05/20/s4mTkPF37lWdDEL.png) 两个错误事件相同,从下方信息栏中没有发现特别有用的信息,只有一行主要信息: ``` 服务器 {784E29F4-5EBE-4279-9948-1E8FE941646D} 没有在要求的超时时间内向 DCOM 注册。 ``` 那么接下来分析一下这串注册值,`Win+R` 输入 `wmic` 运行,进入 **wmic** 管理界面,然后运行: ```sh dcomapp where "appid='{74'" get appid,name ``` 以上命令是查询开头类似 `{784E29F4-5EBE-4279-9948-1E8FE941646D}` 的 DCOM 服务,得到结果如下: ![wmic-dcomapp-query.png](https://i.loli.net/2020/05/20/3IOJitc1AXCrnRk.png) 浏览后发现里面没有和上面相同的 ID 值,所以这条线索断了,试试其它的; #### 进程追踪 点一下“详细信息”,再向下浏览,发现了触发该事件的进程信息,其中比较重要的就是进程 ID(ProcessID),也就是常说的 PID,这里为 `1140`,先记下来; ![eventlog-sys-compared-err-pid.png](https://i.loli.net/2020/05/20/Ek7Or2Ui3MwvhNL.png) 然后 `Ctrl+Shift+Esc` 打开任务管理器,点一下 **PID** 栏(没有就在表头右键单击,然后勾选上),让它按数字升序排列,找到之前记录的 pid 值(1140): ![eventlog-err-pid-with-tasklist.png](https://i.loli.net/2020/05/20/mCSs7kdBhu2xXOD.png) 这时就能看到运行该进程的命令行信息了(同样要是没有这一列就右键点击勾选),发现运行的程序是 `C:\Windows\system32\svchost.exe`,这是一个系统程序,很多服务都会调用它,需要关注的是后面的参数,出现了 `RPCSS` 这个关键字,看着很熟悉,好像是和远程相关的,搜索后网上说这是一个与 `135` 端口相关的服务,那么我们就 `Win+R` 输入 `cmd` 打开命令提示符,查看一下这个端口信息: ![cmd-netstat-rpcss.png](https://i.loli.net/2020/05/20/a4Fj1Jzonr3c5fN.png) 果然存在关联,那么这个 `RPCSS` 应该是一个服务,所以接下来用 `sc` 命令查询一下这个服务: ![cmd-sc-rpcss.png](https://i.loli.net/2020/05/20/kPT8q6sgMxtSjwC.png) 确实是一个服务,这里主要是获取 `DISPLAY_NAME` 这个值,即 `Remote Procedure Call`,然后打开服务管理工具(`Win+R` 后输入 `services.msc`),找到这个服务项: ![service-rpcss.png](https://i.loli.net/2020/05/20/ZdMJRHNU1QyuG38.png) 双击进去,看一下依赖关系,确实是一项系统基础服务,许多重要的服务和模块都依赖于它,还不能直接冒然禁用: ![service-rpcss-depend.png](https://i.loli.net/2020/05/20/fgk4eRA3tKdjvZp.png) 到这里所有分析工作就结束了。 # 后记 下面是之后重新收集的黑屏时的错误事件(启用全部服务),这次就只剩一处错误日志了,也与上面分析筛选结果一致: ![eventlog-in-black-less-err-warn.png](https://i.loli.net/2020/05/20/uTV34l8KHfGkJOc.png) 然而,当再次禁用之前的问题服务项时,信息量就剧增了: ![eventlog-no-black-more-err-warn.png](https://i.loli.net/2020/05/20/qPZIgbeGtcEQnay.png) 原因也很明显,禁用一项比较关键的服务项,并且其依赖项还比较多时,就难免发生连环事故,虽然暂时解决了目前的问题,但是对于轻微强迫症的作者来说,多少看着还是有些不安(饮鸩止渴?)但是后面有趣的事情又发生了,在某一次启用全部服务(包括之前确定为问题源的“web 账户管理器”服务)重启进入系统后,竟然意外地**没有黑屏**,而是和平时一样正常进入桌面,后面又试了几次都正常……难道是这一天的 n 顿操作猛如虎和无数次重启再次感化 CPU?看来 Windows 系统永远是个谜,bug 轻轻走了又正如它轻轻的来,不带走一片云彩,算了不玩了,收工。 --> 起因 一阳光明媚的晌午,本人心情愉悦地翻开笔记本,一如既往地摁下开机键后,略过了主板开机动画,熬过了 Windows 登录(win10 系统)的魔力转圈圈,最终却没能等来那昔日熟悉的桌面与亲切的图标们,直接映入眼帘的是下图: 嗯,就这样盯着它,10s…30s…1min…时间安静的流淌,内心也慢慢掀起了波澜,身经百战的心灵意识到不好的事情要发生了;Nice,人在家中坐,bug 天上来,不过黑屏给了我黑色的眼,我将用它来寻找问题。 问题探索 首先,调整好心态,冷静就有希望,慌乱就会败北(或者是像本人一样曾被无数 bug 折磨后的生死看淡?)问题总有会一些办法可以进行解决;然后就是寻找突破口了,这时下意识的晃了晃鼠标,然后熟悉的小光标出现了!但是还是背景一片黑,不过在这无边的黑暗中,这光标也算闪烁着唯一又弱小的希望的光芒;然后又是试探性的按了一下键盘的 windows 键,然后画风一变: 按 win+a也有反应,打开了侧边栏,证明系统已经加载完毕,按键都有作用,只是无法显示,于是一顿操作打开了个应用(盲开),等待数秒后没有反应,仍是一片黑,再次按下 win 键又确实看见了它已被打开,鼠标挪到任务栏位置看一下: 再开个应用,尝试使用 alt+tab 组合键切换应用: 看来能够正常启动应用,然后尝试点开了任务栏一个应用(资源管理器),把鼠标挪到应用的任务栏缩略图后,出现了下面一幕: 咦这不是我那亲切的桌面嘛,居然以这种方式出现了,果然有戏,接下来再进一步发掘;然而 就在这时,桌面奇迹般的亮了,一切恢复如初,就像风不曾吹过,雨不曾下过,似一切都未曾发生过,难道是这般执著感化了 CPU ?开个玩笑,刚才没有执行特使的操作,应该是某种超时时间过了,桌面出现响应,不过看了看时间,算一下时间差大概有 3 分钟左右,果然这就是神奇的相对论,转瞬的时间有时可以变得很漫长; 不过事情不会这样结束,接下来又是习惯性地重启了电脑,看一下问题是否会再现,一顿操作和等待后,电脑开机…登录…转圈圈…然后果然又是黑屏!无边的黑暗再次席卷覆盖整个显示屏,不过这一次就要想办法将其撕破了; 问题分析 根据前面的经历,这里黑屏应该也要持续 3 分钟左右,甚至更多,那么就不能干等着,于是开始盯着深邃的屏幕陷入沉思:问题出在 Windows 系统登录后(该系统设置了开机自动登录 Windows 账户),就是系统的 BOOT 引导已经结束,这样就排除了常见的开机黑屏现象,即按下 电源键后一直黑没多余反应那种,这就通常是硬件方面的问题,比如内存条接触不良等原因,目前就基本排除了这些原因;既然是系统启动后,并且执行完了登录操作,而没能正常显示桌面,那么问题就缩小(好像也不怎么小哈..)到了软件层面,比如系统服务,驱动,启动项等等; 等等,启动项和桌面,好像想到了什么,因为一直在用一款桌面整理软件,从而避免脏乱差的视觉环境,同时就是设置开机启动,最近软件也更新了一下,难道是这个原因造成的桌面显示 bug ?不知不觉间桌面已经恢复显示了,于是按下 Ctrl+Shift+Esc 组合键调出任务管理器,点下 启动 栏,然后禁用该程序开机启动: 随后马不停蹄地重启了电脑,然后,事实证明事情似乎没有想象中的简单,依然是熟悉的黑屏,到这里也没有特别的好招了,因为一般给别人解决问题时首先就是问最近干了啥,可能会发现线索,不过本人最近用计算机干的事情似乎有点多,系统到用户层面的各种,服务器、虚拟机、数据库等等,一时也想不出什么线索(甚至觉得盯着屏幕呼吸都是一种错 -_-),所以准备向搜索引擎寻求帮助或者找找启发; 问题排查 搜索解决方案 一搜还确实有不少小伙伴有类似的经历,排除硬件故障无法开机的,有说更新驱动的,还不少,这种回答就一笑略过吧,这种方案很普遍也是有原因的,排除不愿相信硬件损坏的显示,就可以把大部分问题推到在软硬件之间打交道的驱动程序上了,其在过去确实能解决大部分问题,不过各厂商也都更新了这么多年了,驱动层面的问题现在应该很少了,而且本人电脑里的各个驱动都一直保持在最新状态,这个也排除了; 另外也有提到取消 Windows 的快速启动功能的,也就是下面的步骤: 不过经测试无效,所以排除; 继续浏览,不出所料,处理和解决 Windows 大部分故障或问题的场景,几乎都能见到 注册表 的身影,不过确实注册表这东西和 Windows 系统关系相当紧密,你在 Windows 中执行的大部分可见甚至不可见的操作,几乎都一项注册表项值与之关联;关于注册表的解决方案中基本都提到修改同一个地方(大部分是让下载或者新建一个注册表文件,然后双击导入系统,其实大可不必这样复杂): HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0000 HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0001 即在这两项下都增加(不存在的话)一个名为 EnableULPS 的键,值为 0,类型为 REG_DWORD,就是如下图这样: 不明觉厉,先重启试试……还真解决了!!但是由于职业精神,问题被莫名其妙的解决了还是有些不甘心,于是就继续深入分析下,上面的注册表操作其实就是把 EnableULPS 这个熟悉赋值为了 0,根据字面意思全部翻译过来就是禁用了 ULPS 这个功能,再搜索得知,其全称 Ultra Low Power State(超低功率状态),这个似乎是 AMD 中的一个功能,下面是引用片段: AMD 显卡为了防止因为频率太高导致系统不稳定。所以在 AMD 显卡上推出了一个 ULPS 功能,就是用户无操作的时候自动降频,休眠,然后用于节电。想法是好的,但是有人用了导致黑屏。所以出了一个关闭此功能的工具,它可以用于检测这个功能的开关状态,并直接关掉。 不过这个问题能在我的 Intel 中出现也是很迷;另外文章还有提到: ULPS是休眠状态 ,降低非主卡的频率和电压的以节省电能,缺点就是可能会导致性能的损失和一些交火不稳定。 经常用电池的不建议关闭ULPS,因为关闭后显卡一直工作在独显状态。 细想以前似乎从未动过这个功能,这么冒然改好像有点简单粗暴,之后还可能会得不偿失,所以这个方案暂时存着,先找找其它方面的问题; 自行排查 系统服务 就像之前说的,搜索问题有时候并不能得到有效的解决方案,但是某些回复的解决手段或者思路是可以起到一定程度的启发作用的,比如某一条大致说的是排查系统服务的问题,确实,之前分析时把问题定位在系统层面,排查过了启动项,但是 服务 这一块还没测试,所以先打开 msconfig,Win+R 后输入: 然后进入服务模块: 这里列出的就是系统中的所有服务项,前面打勾代表已启用,否则是禁用,这里的思路就是先都禁用了,然后重启如果正常则挨个启用排查是哪一项服务的问题,当然这样工作量有点大,全部禁用也可能会出现额外的问题,所以可以先试试系统自带的诊断启动,会加载一些基本服务和设备,就是点击上图顶部最左侧的 常规 模块,然后选择 诊断启动: 点确定或应用后重启系统,这次就愉快又快速的进入系统桌面了(证明禁用不必要的服务确实能提高开机速度),不过也会发现某些模块无法使用,比如喇叭和屏幕亮度,甚至提示某些系统错误,很正常,因为只启用了基本服务,其它的系统服务和模块就没有加载,不过不影响问题排查就行了; 系统日志 当然,排查问题怎么少的了日志分析,可以起到一定辅助作用,于是这时就想起了 Windows 自家的法宝 事件查看器(由于平时也基本也怎么用过),平时用惯了 Linux 命令行分析日志,突然一切可视化了还不太习惯,先打开熟悉一下操作再说: 点开 Windows 日志: 再看看包含事件的几项: 这里由于分析问题可能是出在系统层面上,所以先关注 Windows 相关的事件,应用程序的暂且不管(其实也是因为点开它后发现应用数量有些庞大,不好找落脚点 ╮(╯▽╰)╭ ),然后就是挨个进到每一项中,点击右侧操作栏的清除日志按钮把日志分别清空: 这样如果后续操作时问题复现了,就可以较精确的定位了; 服务排查 然后可以把事件查看器关闭,再次打开 msconfig,选择诊断启动,再切到服务模块,可以看到大部分服务都没有被勾选了,然后我们点一下“服务”这个表头,让项目按名称顺序排列,方便后续操作: 然后就是重点的排除环节了,这里大致数了一下,有 400 个左右的服务项,如果挨个勾选再重启检查的话,可能也就写不出这篇文章了,所以需要找一个高效的办法,之前搜索问题时也受到一位小伙伴的启发,可以使用 二分查找 法进行排除,这本来是算法中的一种解决方案,没想到被这样给实际应用了(~ ̄▽ ̄)~,这里通俗讲就是先勾选一半的服务项目,比如从第一条开始,一直勾选直到右侧滚动条运动到大概中点的位置(好像工作量也不小,看手速咯),前面已经对服务名称进行过排序,所以这里前半部分服务大致是字母开头是 A - P 的服务项: 重启系统后正常进入桌面,证明问题不在勾选的前半部分服务项中,可以排除掉,接下来我们再把剩下的没有勾选的服务项,勾选它们的前半部分,也就是说现在还有总量的最后四分之一部分没有被勾选,这样排除确实挺快,然后就是清除全部 Windows 日志,重启,再重复这些工作,直到问题复现(登录黑屏); 于是乎在进行到 W 字母开头的服务项排查时,登录终于黑屏了,虽然有些幸灾乐祸,但是却代表定位到问题了;然后就是继续二分,缩小范围,最终定位如下图所示: 也就是说罪魁祸首是这个名为“web 账户管理器”的服务项,看制造商应该是一项系统服务,并且之前搜索时看到有几位小伙伴定位的服务项是“App Readiness”,所以这个会因不同系统环境而不同,不应该一概而论冒然禁用;当然把它禁用后问题就解决了,没有像之前一样修改注册表,但是再次本着职业精神(no zuo no die),就继续分析一下问题的具体原因; 日志分析 日志概览 每一次统计的系统日志就在这时候发挥作用了,因为每一次重启前都清除了日志,所以每次记录的也就是当前排查项的事件,下面看一下记录的日志情况: 分别查看不同事件,可以 显示详细信息: 日志筛选 可以看到即使单次记录的日志量也是很庞大的,所以现在可以使用事件查看器的日志筛选功能了,即点击右侧操作栏的筛选当前日志按钮,会弹出筛选设置窗口: 首先是记录时间,即指定事件的起始和结束时间点,可以在开机和桌面显示后分别记录一个时间,然后选择这个时间区间就能进一步缩小范围; 然后是时间组别,浏览也会发现事件主要分为信息、警告和错误,这里我们只用关心错误类型的事件,勾上后下面的项目暂时不用关,点确定; 下面就是筛选结果,可以看到错误信息还挺多, 对比黑屏时产生的错误日志,可以发现“应用程序”项的错误在正常进入桌面时也有发生,所以可以暂时排除这一项,而“安全”这一项,都是信息类,并没有错误类事件,所以也排除,最后就只剩“系统”这一项中的错误日志存在差异,存在差异的事件包括名为 Service Control Manager 和 DistributedCOM 的事件“来源”中; 对比分析 那么我们就来对比一下“系统”中产生的错误日志的差异,只是事件查看器似乎没有内置日志对比的功能,所以只能使用较为原始的办法,先选中想要分析的事件: 再点击右侧的保存选择的事件按钮,保存事件日志文件到任意位置: 像这样分别记录和保存发生黑屏问题和未发生问题时的事件,然后点击“打开保存的日志”,就能导入两个日志文件就行下一步分析了: 另外发现每个事件似乎都对应着一个唯一的 事件 ID 值,可以通过这个把两个日志文件重复的地方剔除,这就要使用筛选功能里的事件 ID 排除选项了: 填入重复的事件 ID,用逗号隔开,前面加负号 - 表示排除该 ID 的事件,不加表示包括,筛选结果如下: 两个错误事件相同,从下方信息栏中没有发现特别有用的信息,只有一行主要信息: 服务器 {784E29F4-5EBE-4279-9948-1E8FE941646D} 没有在要求的超时时间内向 DCOM 注册。 那么接下来分析一下这串注册值,Win+R 输入 wmic 运行,进入 wmic 管理界面,然后运行: dcomapp where "appid<='{79' and appid>='{74'" get appid,name 以上命令是查询开头类似 {784E29F4-5EBE-4279-9948-1E8FE941646D} 的 DCOM 服务,得到结果如下: 浏览后发现里面没有和上面相同的 ID 值,所以这条线索断了,试试其它的; 进程追踪 点一下“详细信息”,再向下浏览,发现了触发该事件的进程信息,其中比较重要的就是进程 ID(ProcessID),也就是常说的 PID,这里为 1140,先记下来; 然后 Ctrl+Shift+Esc 打开任务管理器,点一下 PID 栏(没有就在表头右键单击,然后勾选上),让它按数字升序排列,找到之前记录的 pid 值(1140): 这时就能看到运行该进程的命令行信息了(同样要是没有这一列就右键点击勾选),发现运行的程序是 C:\Windows\system32\svchost.exe,这是一个系统程序,很多服务都会调用它,需要关注的是后面的参数,出现了 RPCSS 这个关键字,看着很熟悉,好像是和远程相关的,搜索后网上说这是一个与 135 端口相关的服务,那么我们就 Win+R 输入 cmd 打开命令提示符,查看一下这个端口信息: 果然存在关联,那么这个 RPCSS 应该是一个服务,所以接下来用 sc 命令查询一下这个服务: 确实是一个服务,这里主要是获取 DISPLAY_NAME 这个值,即 Remote Procedure Call,然后打开服务管理工具(Win+R 后输入 services.msc),找到这个服务项: 双击进去,看一下依赖关系,确实是一项系统基础服务,许多重要的服务和模块都依赖于它,还不能直接冒然禁用: 到这里所有分析工作就结束了。 后记 下面是之后重新收集的黑屏时的错误事件(启用全部服务),这次就只剩一处错误日志了,也与上面分析筛选结果一致: 然而,当再次禁用之前的问题服务项时,信息量就剧增了: 原因也很明显,禁用一项比较关键的服务项,并且其依赖项还比较多时,就难免发生连环事故,虽然暂时解决了目前的问题,但是对于轻微强迫症的作者来说,多少看着还是有些不安(饮鸩止渴?)但是后面有趣的事情又发生了,在某一次启用全部服务(包括之前确定为问题源的“web 账户管理器”服务)重启进入系统后,竟然意外地没有黑屏,而是和平时一样正常进入桌面,后面又试了几次都正常……难道是这一天的 n 顿操作猛如虎和无数次重启再次感化 CPU?看来 Windows 系统永远是个谜,bug 轻轻走了又正如它轻轻的来,不带走一片云彩,算了不玩了,收工。

2020/5/20
articleCard.readMore

Kali Linux系统设置中文语言环境

主流 Linux 系统安装之后,默认使用的语言环境基本都是英语,所以不管时图形桌面的菜单标题,还是终端的一些输出提示,都是展示的英文,对于觉得阅读英语不太友好的同胞们,第一件事可能就是设置一个友好的汉化环境,下面以 Kali Linux 这个发行版的系统为例,介绍一些设置中文语言环境的方法; 图形界面 如果安装的系统有图形界面,那么操作就简单了;一般系统在安装时就会提供语言选项,不过安装好后也能进行修改,一般可以在系统设置的区域与语言模块中找到相关的设置,例如下图: 至于其他 Linux 系统,路径应该类似,或者可以在设置里面搜索一下;修改成功需要重新登录生效,这个修改是永久性的; 终端命令 没有图形界面或者在设置里面没有找到的情况下,可以尝试使用终端命令进行修改; 安装中文环境包 切换中文环境之前,需要安装中文语言环境包,直接运行以下命令: sudo dpkg-reconfigure locales # 上面的运行不成功可以尝试下面这条命令: sudo dpkg-reconfigure --force locales 如果提示类似 locales 未找到这样的信息,那么就先运行 apt install locales 执行安装,然后再运行上面的命令,顺利的话会弹出一个对话框,选择需要安装的语言,因为中文包是 z 开头的,列表又是按字母顺序排列,所以使用方向箭头向下浏览到靠近底端位置,应该能看见这样一个选项: [ ] zh_CN.UTF-8 UTF-8 然后把焦点移动到它上面,按下空格键选择,选中的话方括号中会出现星号,再按回车进入下一步,新的对话框会提示选择系统默认的语言设置,这时依然把焦点移动到和上面一样的选项上,最后按回车确认,代码运行完毕后如果没有报错,就安装完成了; 切换中文环境 接下来开始切换语言环境,先运行一下 locale 命令查看当前的语言环境,应该会得到以下输出(目前还是英语环境): LANG=en_US.UTF-8 LANGUAGE= LC_CTYPE="en_US.UTF-8" LC_NUMERIC="en_US.UTF-8" LC_TIME="en_US.UTF-8" LC_COLLATE="en_US.UTF-8" LC_MONETARY="en_US.UTF-8" LC_MESSAGES="en_US.UTF-8" LC_PAPER="en_US.UTF-8" LC_NAME="en_US.UTF-8" LC_ADDRESS="en_US.UTF-8" LC_TELEPHONE="en_US.UTF-8" LC_MEASUREMENT="en_US.UTF-8" LC_IDENTIFICATION="en_US.UTF-8" LC_ALL= 其实这些也都代表这当前系统的对应环境变量值,例如再运行 echo $LANG 就会得到输出 en_US.UTF-8,其他类似,所以我们修改的重点也是围绕其中的几个变量展开的;由于环境变量的特性,也可以再细分为两种方法; 临时性修改 直接在终端里输入以下命令的话,就能对当前语言的环境变量进行修改: export LANG=zh_CN.UTF-8 注:这里只用修改 LANG 这个变量就行了,后面的其它以 LC 开头的变量值会自动改变; 然后再运行 locale 看一下效果: LANG=zh_CN.UTF-8 LANGUAGE= LC_CTYPE="zh_CN.UTF-8" LC_NUMERIC="zh_CN.UTF-8" LC_TIME="zh_CN.UTF-8" LC_COLLATE="zh_CN.UTF-8" LC_MONETARY="zh_CN.UTF-8" LC_MESSAGES="zh_CN.UTF-8" LC_PAPER="zh_CN.UTF-8" LC_NAME="zh_CN.UTF-8" LC_ADDRESS="zh_CN.UTF-8" LC_TELEPHONE="zh_CN.UTF-8" LC_MEASUREMENT="zh_CN.UTF-8" LC_IDENTIFICATION="zh_CN.UTF-8" LC_ALL= 确实修改成功了,不过这条命令是临时性的,关闭当前终端或重启后就失效了,而且也仅限于当前终端内输出指定的语言,只适用于临时查看某个语言的输出内容的场景; 永久性修改 要永久性的修改语言环境,其实也就是永久性的修改 LANG 这个环境变量的值,要实现它就直接在 ~/.bashrc 这个文件中末尾追加下面一行内容: export LANG=zh_CN.UTF-8 这样就对当前用户设置了中文语言环境,如果需要应用到系统所有用户的话,就追加到 /etc/profile 这个文件中;最后重启一下就设置成功了; 其它修改方法 一些教程中会提到修改 /etc/default/locale 这个文件的内容,其实上面的安装中文环境这一节中,最后一步其实就是向这个文件中写入以下内容: LANG=zh_CN.UTF-8 不过经测试,这样修改后重启并没有改变当前语言环境,不过要是替换成下面的内容就能修改成功: LANGUAGE=zh_CN.UTF-8 另外下面任意一条命令都能实现设置 /etc/default/loale 这个文件的内容: sudo localectl set-locale LANGUAGE=zh_CN.UTF-8 sudo update-lcoale LANGUAGE=zh_CN.UTF-8 修改后重启运行 locale 得到的输出是这样的: LANG=en_US.UTF-8 LANGUAGE=zh_CN.UTF-8 LC_CTYPE="en_US.UTF-8" LC_NUMERIC="en_US.UTF-8" LC_TIME="en_US.UTF-8" LC_COLLATE="en_US.UTF-8" LC_MONETARY="en_US.UTF-8" LC_MESSAGES="en_US.UTF-8" LC_PAPER="en_US.UTF-8" LC_NAME="en_US.UTF-8" LC_ADDRESS="en_US.UTF-8" LC_TELEPHONE="en_US.UTF-8" LC_MEASUREMENT="en_US.UTF-8" LC_IDENTIFICATION="en_US.UTF-8" LC_ALL= 即最终只修改了 LANGUAGE 这一处的变量值,所以这个方法不怎么推荐;另外也有一些教程提到修改 /etc/sysconfig/i18n 或 /etc/local.conf 这两个文件的内容,这应该是其它 Linux 发型版本中的,至少我在 Kali 系统里面没有找到这两个文件; 安装中文字体包 修改好中文环境重启后,还有可能出现的一种常见情况就是,终端或者菜单标题之类的地方出现乱码,也有可能出现先刚安装完系统后;其实原因也很简单,就是虽然环境被设置成了中文,但是当前系统中缺乏相应的中文字体,所以识别错误就用一些乱码来占位了; 解决方法就是安装一些中文字体包,比如可以运行: sudo apt install fonts-wqy-microhei 安装完成后,按需重启,应该就能看见友好又熟悉的内容了 ^_^;

2020/4/15
articleCard.readMore

关于浮点数的剪不断理还乱

二进制小数 谈浮点数前,先了解一些基础知识;对于整数的十进制与二进制转换不难,原理也简单就不再赘述,假如现在要进行转换的十进制数字是带有小数点的,转换方法就会稍微有点不一样了;为了使说明更浅显易懂,先来探究一下十进制小数中的奥秘; 以十进制数 3.125 为例,小数点以左为整数部分,右为小数部分,它也可以被更具体的拆解为以下形式: 3x10^0 + 1x10^-1 + 2x10^-2 + 5x10^-3 这也是十进制数的特点,基数为 10,所以每一位都是与 10 的相应次方的乘积,指数也是有规律的,可以看成以小数点为对称轴,向左就是 10 的 0、1、2、3……次方递增,向右则是 10 的 -1、-2、-3……次方递减,如果换一种形式,那么在运算方面也具有对称性(乘法与除法互为逆运算),就是下面的形式: 3x10^0 + 1/10 + 2/10^2 + 5/10^3 这样,理解二进制的小数部分就容易了,已经知道整数十进制转二进制其实就是连续除以 2 取余数,那么根据上面的规律,小数部分看出逆运算,就可以总结为 连续乘 2 取整数,举例说明: 3.125(十进制) 3 / 2 = 1 --> 1 余 1 --> 1 0.125 x 2 = 0.25 --> 0 0.25 x 2 = 0.5 --> 0 0.5 x 2 = 1 --> 1 ==> 11.001(二进制) 11.001 就是 3.125 的二进制小数形式,同时它也可以做如下拆解: 1x2^1 + 1x2^0 + 0x2^-1 + 0x2^-2 + 0x2^-3 = 2 + 1 + 0 + 0 + 0.125 = 3.125 可以看到也是与十进制形式相对应的,只是基数从 10 换成了 2 而已; 定点数 当然,都知道机器码都是一堆 0 和 1 组成的数据,并没有小数点这个专门的符号,要表示数字 3125 还好说,但是 3.125 中间多出的这一点要怎么解决呢……一个很直接的方法就是把小数点应该在的位置焊死 -_-,比如 32 位的系统,规定小数点在 16 位和 17 位之间,那么根据 3.125 的二进制是 11.001,在 32 中的表现就是: 0000 0000 0000 0011 0010 0000 0000 0000 这就是 定点数 的规则干的事情,看着挺好,要是多用几次系统空间可能就 hold 不住了,不好之处明显就是花掉当前一半的数量级取表述小数部分,当然可以少划分几位给小数,但对小数好像不太公平,万一精度要求高呢,反过来呢对整数又不公平了,手心手背都是肉,得想办法解决才行; 浮点数 于是乎,浮点数横空出世;其实上述问题平时肯定都遇到过,比如记个帐,昨日净收入 31200000000 元(@_@),肯定不想写这一堆 0 对不对(如果是真的咱愿意写 100 遍……),一般都会写 312 亿或者 3.12x10^10 元,重点就在后面这种科学计数表示法,人为了省力省纸省笔墨弄了个科学计数法,处理器也自然用上了浮点数,省空间;具体原理与表示方法后面会讲; 以 C语言为代表,其中就有浮点这一数据类型,比如单精度浮点 float(32 位),双精度浮点 double(64 位)等,可能平时也就直接声明和使用较多,没太关注底层实现的算法,其实也不太复杂,套用一种规则而已,下面开始介绍; 标准 目前关于浮点运算与表示,使用广泛的应该就是 IEEE 754(二进制浮点数算术标准)了,其主要内容如下: v = (-1)^s * 2^e * m v: 浮点数具体值 s: 符号位,即正负号,0 为正,1 为负 m: 有效数,也叫尾数,可以类比科学计数法前面的有效数字 另外还有一个小数位 f, m = 1 + f e: 指数位,即 2 的多少次方 该标准包含了多中位数,以 32 位为例: (1) (8) (23) : 位数 0 00000011 0010000000000000000000 s e f 总结就是: 第一位为符号位 s; 后 8 位为指数位 e; 最后 23 位为小数位 f; 64 位的规则是: 第一位是符号位 s; 后 11 位是指数 e; 最后 52 位是有效数字 m; 知道了这些还不能立刻套用上面的公式经行转换,还需要了解接下来的一些规定; 指数偏移值 浮点的二进制表示中指数位 e 的计算值(即转换成十进制后的值),要在实际指数值(十进制)的基础上加上一个 偏移值,标准中规定偏移值为 2^(e-1) - 1,如 32 为中 e 是 8 位,所以偏移值为 127,64 位的就为 1023;所以 32 位指数实际值的范围为 -126 ~ 127; 举个例,指数 e 的实际值为 -3,那么 32 位中加上偏移值就是 -3 + 127 = 124,换算成二进制的 8 位指数位就是 01111100; 这种指数位偏移后的指数值,又叫做 阶码,因为科学计数法中的指数是有正负之分的,所以实际指数值加上一个正的适中偏移值,就可以使得浮点表示法中的指数位为无符号的整型(就是变成正整数),利于浮点数的比较大小,就是可以直接从浮点的二进制表示中,由高位向低位逐位进行比较(如果是负数二进制比较大小要复杂一点)。 表示方式 具体的表示方式会根据不同的情况而不同,主要有以下几种情况; 规约形式 即 e 的 8 位数字不是全部为 0 或者 1,此时 m = 1 + f,由于小数部分 f 的值在 0 到 1 之间,所以有效数 m 的值在 1 到 2 之间; 非规约形式 这种形式 e 的 8 位全部为 0,小数位 f 值不为 0,用于表示非常接近 0 的数,此时不再是 m = 1 + f,而是 m = f,即 m 值在 0 到 1 之间;实际上所有非规约的浮点数比规约浮点数更接近于 0; 零值 指数位 e 全为 0 的同时,小数部分(f)为 0,用来表示 ±0(正负取决于 s 位的值);且规定最小指数位编码(e = 0 时)的实际值应该取 -126(本来应该是 0 - 127 = -127); 无穷大 如果 e 全为 1,且 m 全为 0,则表示无穷大(Infinity,正负取决于 s 位的值); NaN 如果 e 全为 1,且 m 不全为 0,则表示 NaN(Not a Number,非数值类型); 综合举例 后面的例子都以 32 位为例,其它位数根据标准类推; 先来看一个十进制转浮点,规约形式的例子,比如用之前的十进制数 3.125,转换为 32 位浮点二进制格式(0b 开头的表示二进制数据): 3.125 = 0b11.001 = 0b1.1001 x 2^1 s = 0 e = 1 => 1 + 127 = 128 = 0b10000000 m = ob1.1001 f = m - 1 = 0b0.1001 => 0b10010000000000000000000 ==> 0 10000000 10010000000000000000000 转换的大致流程总结如下: 十进制小数 –> 二进制小数 –> 浮点表示法 –> 二进制浮点 再举一个二进制浮点转十进制小数例子: 1 01111111 00100000000000000000000 s = 1 => -1 e = 0b01111111 = 127 => 127 - 127 = 0 f = 0b0.001 m = 1 + f = 0b1.001 v = -1 x 0b1.001 x 2^0 = 0b-1.001 = -1.125 ==> -1.125 精度 以 32 位单精度浮点数为例,由于分配给有效数字的位数是 23 位,而整数部分默认是 1,它的位置就不用留了,所以小数部分就可以独占 23 位,在加上默认的一个整数位就是 24 位了,同理,64 位双精度浮点数的有效数就是 53 位(52+1),再进行一下算术运算: log(2^24) = 7.22 log(2^53) = 15.95 上面的算式表示二进制下的这么多位数的实际值,对应到十进制中有多少位;结果表明,单精度浮点可以保证 7 位十进制的有效数字,双精度的则可以保证 15 位; “浮”的原因 取名为浮点,那么到底“浮”在了什么地方,与定点数相比的优势又是什么?总的看来,其实其转换操作很类似于十进制中的科学计数法,而科学计数法的出现,就是为了实现能简短地书写较大的数,比如写作 1.0x10^20,就可以避免在 1 后面写那令人抓狂的 20 个 0 了; 同理,二进制中如果用定点数表示小数,那么 32 位的话就最多到 32 位有效数字,而用这种类似科学技术的浮点表示法的话,指数能表示到 100 以上,也就是 100 多位了,相信现在的 100 位系统也是稀有物种了吧;因此,浮点数有效的扩大了能表示的数据范围,科学计数法减少了书写量,浮点表示则是节省了存储空间; 另外也是由于科学计数本身的特性,以及指数偏移值,也就是 阶码 的应用,小数点也就不再像之前一样固定,具体位置会根据指数的大小最终“漂浮”到不同的位置,甚至到那遥远的地方……

2020/3/21
articleCard.readMore

JavaScript之注释规范化(JSDoc)

前言 俗话说,无规矩不成方圆;虽说代码敲出来都是交给编译器解释执行的,只要不存在语法格式错误,排版无论多么反人类都是没有问题的,但是代码除了执行外的另一个广泛用途就是阅读了,翻阅自己过去的代码、理解别人的源码,等等;所以出现了代码风格化,美化外观的同时便于阅读,这就是目前 JSLint 等工具的作用; 当然,除了代码本身外,阅读更多的可能就是代码注释了,注释本身是不会被编译器编译执行的,其作用也是为了留下一些信息,方便更好的理解代码本身;所以,注释的规范化也是一个值得思考的问题;而接下来即将介绍的 JSDoc 就是这样的一款工具; JSDoc 根据其官网(https://jsdoc.app/index.html)的介绍,JSDoc 是一个针对 JavaScript 的 API 文档生成器,类似于 Java 中的 Javadoc 或者 PHP 中的 phpDocumentor;在源代码中添加指定格式的注释,JSDoc 工具便会自动扫描你的代码并生成一个 API 文档网站(在指定目录下生成相关的网页文件); 生成 API 文档只是一方面,其更主要的贡献在于对代码注释格式进行了规范化,你可能没用过,但多半曾经在某个地方的源码中见过类似于下面的注释格式: /** * Returns the sum of a and b * @param {number} a * @param {number} b * @returns {number} */ function sum(a, b) { return a + b; } 使用 工具的使用很简单,首先安装它: npm install -g jsdoc 其次假设在一个名为 doc.js 的文件中书写以下代码: /** * Returns the sum of a and b * @param {number} a * @param {number} b * @returns {number} */ function sum(a, b) { return a + b; } /** * Return the diff fo a and b * @param {number} a * @param {number} b * @returns {number} */ function diff(a, b) { return a - b; } 然后就是在当前目录执行以下命令: jsdoc doc.js 最后就会在当前目录下生成一个名为 out 的目录(也可以另外指定),当前目录内容就会变成像下面这样: ├── doc.js └── out ├── index.html ├── doc.js.html ├── global.html ├── fonts │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ └── OpenSans-Regular-webfont.woff ├── scripts │ ├── linenumber.js │ └── prettify │ ├── Apache-License-2.0.txt │ ├── lang-css.js │ └── prettify.js └── styles ├── jsdoc-default.css ├── prettify-jsdoc.css └── prettify-tomorrow.css 通过浏览器访问这个 out 目录中的相关网页,就会展示类似于下面的页面内容; 主页: 指定函数页: 网页样式模板也可以更换,根据命令行参数修改即可,这里不再探究,下面主要来学习一下它的注释格式; 注释格式 完整的格式介绍请参考官网(https://jsdoc.app/index.html),目前版本是 JSDoc 3,下面只介绍几种常用的标签并配合举例;当然如果嫌手写一堆标签麻烦,现在许多编辑器(比如 VS Code)都提供了相关的插件下载,直接在插件中搜索关键词 jsdoc 就会出现许多,都是带提示或者自动识别当前代码生成的,很方便; 注释符 JSDoc 使用以下格式的注释符来对要添加的标签进行块级包裹: /** * * */ 即星号列垂直对其,第一行使用两个星号,每个星号后要添加一个空格再写内容,比如: /** * 前面留一个空格,再写描述 * 或者多行描述 * @param {number} 关于该参数的描述 */ 行内包裹: /** @function */ @description 也可写作 @desc,描述当前注释对象的详细信息; /** * @function * @description 关于该函数的介绍内容 */ function myFn() {} /** * 也能在这里直接写介绍内容 * @function * @description 如果这里又继续使用标签添加内容,则会覆盖第一行的介绍内容 */ function myFn() {} @file 注释写在文件开头,用于描述当前文件的相关信息;例如: /** * @file 这是一个用于...的文件,包含了...功能 */ // 然后是代码正文... @author 描述当前文件或者代码的作者的相关信息; /** * @author Jack <jack@example.com> */ @copyright 描述当前文件的版权相关信息 /** * @copyright Jack 2020 */ @license 描述当前文件许可证相关信息; /** * @license MIT */ 或者是: /** * @license * Copyright (c) 2015 Example Corporation Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * ... */ @version 描述当前项目的版本号; /** * 这个版本修复了...问题 * @version 1.2.3 */ @since 描述某个功能是从哪个版本开始引入的; /** * 提供了...功能 * @since 1.2.1 */ function newFn() {} @see 类似于“另见”、“详见”的意思,引导至其他位置,也可以使用 @link 引导至某一网络地址; /** * @see fn2 */ function fn1() {} /** * @see {@link http://example.com|some text} */ function fn2() {} @todo 描述接下来准备做的事情; /** * @todo 添加...功能 * @todo 修复...bug */ function myFn() {} @function 与 @func, @method 含义相同,描述一个函数; /** @function */ var myFn = function() {} @type 描述一个变量的类型; /** * 一个对象类型的变量 * @type {object} */ var val1 = {}; /** * 一个字符或者数字类型的变量 * @type {(string|number)} */ var val2; /** * 类型为数字或为空 * @type {?number} */ var val3; /** * 类型为数字或且不能为空 * @type {!number} */ var val4; /** * 一个 MyClass 类的实例数组 * @type {Array.<MyClass>} */ var arr = new MyClass(); /** * 一个字符串的数组 * @type {string[]} */ var arr2 = ['a', 'b', 'c']; /** * 一个包含一个字符串和一个数字类型的对象 * @type {object.<string, number>} */ var obj1 = {a: 'one', b: 2} /** * 指定具体键和类型的对象 * @type {{a: string, b: number}} */ var obj2 = {a: 'one', b: 2} /** * 指定具体键和类型的命名对象 * @type {object} obj3 * @type {string} obj3.a * @type {number} obj3.b */ var obj3 = {a: 'one', b: 2} @param 与 @arg, @argument 含义相同,描述一个函数的参数信息; /** * 标签后跟参数类型,然后是参数名,最后是参数描述 * @param {number} a 这里写变量的描述 * @param {string} b - 或者加上连字符便于阅读 * @param {string} c - 又或者这个参数有一个很长很长很长 * 很长很长很长很长很长非常长的描述,可以这样占用多行 */ function myFn(a, b, c) {} /** * 传入的参数是个对象 * @param {object} option - 传入的对象参数 * @param {string} option.name - 对象的 name 属性 * @param {number} option.age - 对象的 age 属性 */ function myFn(option) { var name = option.name; var age = option.age; } /** * 传入的参数是个字符串组成的数组 * @param {string[]} arr - 传入的对象参数 */ function myFn(arr) { var name = option.name; var age = option.age; } /** * 表示某个参数是可选的 * @param {number} a - 这是必填参数 * @param {number} [b] - 这是可选参数 * @param {number=} c - 可选参数的另一种表示 */ function myFn(a, b, c) {} /** * 表示可选参数的默认值 * @param {number} a * @param {number} [b=3] - 默认值为 3 */ function myFn(a, b) {} /** * 参数类型的各种表示 * @param {number} a - 类型为数字 * @param {number|string} b - 类型为数字或字符串 * @param {?number} c - 类型为数字或者为空(null) * @param {!number} d - 类型为数字且不为空 * @param {*} e - 类型不做限制,即可以为任意类型 */ function myFn(a, b, c, d, e) {} /** * 表示具有任意多个参数的函数 * 下面的函数返回所有传入参数的和 * @param {...number} num - 参数个数任意,但是都是数字类型 */ function sum(num) { var len = arguments.length; var result = 0; for (let i = 0; i < len; i++) { result += arguments[i]; } return result; } @typedef 用于描述自定义的变量类型; /** * 关于自定义类型的描述 * @typedef {(string|number)} myType */ /** * 关于自定义类型的描述 * @type {myType} val - 使用自定义的类型 */ function myFn(val) {} @callback 描述指定函数中作为回调函数的参数信息; /** * 这是关于回调函数的描述 * @callback myCallback * @param {string} aa - 回调函数接受的参数 * @param {number} [bb] - 回调函数接受的另一个可选参数 */ /** * 这是关于函数本身的描述 * @param {string} a * @param {myCallback} callback - 回调函数 */ function myFn(a, callback) {} @returns 或者写作 @return,描述函数的返回值的信息; /** * @param {number} a * @returns {number} 关于返回值的描述 */ function myFn(a) { return a + 1; } /** * @param {number} a * @returns {(number|string)} 返回值可能是数字或字符类型 */ function myFn2(a) { if (a > 1) { return 1; } else { return 'no.'; } } @example 描述指定代码的使用示例; /** * 添加示例代码(格式会被高亮展示) * @param {string} a * @param {string} b * @returns {string} return a concat b. * * @example * console.log(myFn('hello ', 'world!')); * // "hello world!" */ function myFn(a, b) { return a + b; } @class 描述一个 class 类; /** * 关于该类的描述 * @class */ class MyClass {} /** * 或者是一个构造函数 * @class */ function MyClass() {} var ins = new MyClass(); @namespace 描述一个命名空间; /** * 指定一个对象对命名空间 * @namespace */ var MyNamespace = { /** * 表示为 MyNamespace.fn * @returns {*} */ fn: function() {}, /** * 表示为 MyNamespace.a * @type {number} */ a: 1 } /** * 手动指定命名空间 * @namespace MyNamespace */ /** * 一个成员函数,MyNamespace.myFn * @function * @returns {*} * @memberof MyNamespace */ function myFn() {} @member 描述当前类的一个成员; /** * @class */ function MyClass() { /** @member {string} */ this.name = 'knightyun'; /** * 或者一个虚拟的成员 * @member {number} age */ } @memberof 描述成员所属的类; /** * @class */ class MyClass { /** * @constructor * @memberof MyClass */ constructor() {} /* * @param {string} val * @returns {*} * @memberof MyClass */ myFn(val) {} }

2020/3/13
articleCard.readMore

JavaScript与二进制数据的恩怨情仇

编程江湖,终日血雨腥风,论及二进制数据,又有多少豪杰谈笑风生,风生水起,水起船高,高深莫测…… 不扯远了,想必谈到二进制数据,大家联想到的就会是 1010110110001 或者 00000000 11111111 00000101 这样的数据流;而这武林之中,号称三剑客之一的 JavaScript,在其行走江湖之际(日常开发),可能厮杀(处理)最多的类型就是直观的数字、字符串或者对象等;那么与极少露面的隐士(二进制)狭路相逢之时,它又将作何应对(描述与处理二进制数据)呢?是波澜壮阔,还是全身而退,抑或是力挽狂澜,且听本文中分解。 ArrayBuffer 未曾识得英雄面,只缘身在此山中;先来了解第一个概念,ArrayBuffer 表示的是一个原始二进制数据缓冲区(buffer),长度固定,并且内容是只读的;如果需要执行写操作,那么需要使用 类型化数组(TypedArray)或者 数据视图(DataView) 来实现; 知己知彼,方可百战百胜;在 JavaScript 中与二进制数据接触最紧密的可能就是 ArrayBuffer 了,之前讲目的是要描述和操作二进制数据,那么就要把这些数据先存放到某个地方,然后才能对其进行操作,这里的 ArrayBuffer 缓冲区就可以被看成这么一种地方;当然,可能最直观的方式就是将其保存到字符串中,如 "101011011",又或者存入数组,如 [1,0,1,0,1,1,0,1],这样确实是方便人类了,但是机器执行的效率也降低了,因为毕竟字符和数组是另外两种基本类型,并且也不是专门为此设计的;所以,就出现了专门为缓冲数据设计的 ArrayBuffer,并通过结合视图来提供一个访问和操作数据的接口; 语法 实例化 ArrayBuffer 构造函数时,只接受一个参数,就是要创建的 Arraybuffer 的大小,单位是字节,不指定的话默认为 0;同时它也提供了一个实例属性 byteLength(只读),实现对当前 ArrayBuffer 字节值的访问; 举例: var buffer = new ArrayBuffer(3); console.log(buffer.byteLength); // 3 另外,由于 ArrayBuffer 只是负责创建这么一段数据区域,并没有提供初始化赋值的接口,所以这 n 字节的数据都为空,即都置 0; 方法 由于 ArrayBuffer 构造函数本身是用于创建数据缓冲区,并且数据只读,所以提供的属性和方法也只有少数几个; .slice() 用于返回一个新的缓冲区,语法为 .slice(start, end),即以当前缓冲区为母本,从索引为 start 的位置开始,到 end 位置结束(end 位置不包含在内),然后复制并返回这一区段的数据;其用法大致与 Array.prototype.slice() 类似,举例说明: var buffer1 = new ArrayBuffer(5); var buffer2 = buffer1.slice(0, 3); var buffer3 = buffer1.slice(2); var buffer4 = buffer1.slice(1, -1); console.log(buffer2.byteLength); // 3 console.log(buffer3.byteLength); // 3 console.log(buffer4.byteLength); // 3 ArrayBuffer.isView() 该方法用来判断所提供的参数值是否是一种 ArrayBuffer 视图,比如类型化数组(TypedArray)和数据视图(DataView),例如: console.log(ArrayBuffer.isView()); // false console.log(ArrayBuffer.isView([1, 2, 3])); // false console.log(ArrayBuffer.isView(new ArrayBuffer(3))); // false console.log(ArrayBuffer.isView(new Int8Array())); // true console.log(ArrayBuffer.isView(new Uint32Array(3))); // true console.log(ArrayBuffer.isView(new DataView(new ArrayBuffer()))); // true 类型化数组(TypedArray) 概述 工欲善其事必先利其器;前面提到操作 ArrayBuffer 创建的数据缓冲区需要使用视图(view)实现,类型化数组就是这么一个描述二进制数据缓冲区(buffer)的视图(view),这个视图是一个 类数组。另外,不存在 TypedArray() 这个构造函数,它指的是一类数组,因此它有多种实现,即多个类型化数组构造器函数;可以姑且理解为 水果 之于 苹果 和 香蕉,水果指的是一类食物,都知道并不存在名为 水果 的一种具体食物,但是 苹果 和 香蕉 是具体存在的; 有效的类型如下: Int8Array(); // 8 位二进制有符号整数 Uint8Array(); // 8 位无符号整数(超出范围后从另一边界循环) Uint8ClampedArray(); // 8 位无符号整数(超出范围后为边界值) Int16Array(); // 16 位二进制有符号整数 Uint16Array(); // 16 位无符号整数signed Int32Array(); // 32 位二进制有符号整数 Uint32Array(); // 32 位无符号整数 Float32Array(); // 32 位 IEEE 浮点数(7 位有效数字,如 1.1234567) Float64Array(); // 64 位 IEEE 浮点数(16 有效数字,如 1.123...15) BigInt64Array(); // 64 位二进制有符号整数 BigUint64Array(); // 64 位无符号整数 语法: 万法不离其宗,一招一式都有迹可循;后面就都以 Int8Array() 为例进行说明,以下代码展示了可以传入的参数类型: new Int8Array(); // ES2017 中新增 new Int8Array(length); new Int8Array(typedArray); new Int8Array(object); new Int8Array(buffer [, byteOffset [, length]]); 无参数 最好的招式是没有招式;实例化构造函数时不传入任何参数,则返回一个空的类型化数组: var i8 = new Int8Array(); console.log(i8); // Int8Array [] console.log(i8.length); // 0 console.log(i8.byteLength); // 0 console.log(i8.byteOffset); // 0 length 一寸长一寸强;传入一个数字类型的参数,表示申明类型化数组中元素的个数: var i8 = new Int8Array(3); var _i8 = new Int8Array('3'); // 字符串会先被转换成数字 console.log(i8); // Int8Array(3) [0, 0, 0] console.log(_i8); // Int8Array(3) [0, 0, 0] console.log(i8.length); // 3 console.log(i8.byteLength); // 3 console.log(i8.byteOffset); // 0 typedArray 好招不怕效仿;当传入的一个参数同样是一个类型化数组时,则返回一个原类型数组的拷贝(不是引用): var i8 = new Int8Array(3); var _i8 = new Int8Array(i8); console.log(i8 == _i8); // false console.log(i8 === _i8); // false console.log(_i8); // Int8Array(3) [0, 0, 0] object 海纳百川有容乃大;使用该参数时类似于用 TypedArray.prototype.from() 方法创建的类型数组,同时该方法也和 Array.from() 方法类似,即这个 object 参数是一个类数组的对象,或者是可迭代的对象;举例: // 数组 var i81 = new Int8Array([1, 2, 3]); console.log(i81); // Int8Array(3) [1, 2, 3] // 等价的操作 var i81 = Int8Array.from([1, 2, 3]); // Int8Array(3) [1, 2, 3] // 类数组 var i82 = new Int8Array({ 0: 1, 1: 2, 2: 3, length: 3 }); console.log(i82); // Int8Array(3) [1, 2, 3] // 可迭代对象 var i83 = new Int8Array(new Set([1, 2, 3])); console.log(i83); // Int8Array(3) [1, 2, 3] buffer, byteOffset, length 众人拾柴火焰高;该构造函数也支持同时提供三个参数,第一个 buffer 指的是数组缓冲区,是 ArrayBuffer 的实例,同时也是 Int8Array.prototype.buffer 这个属性的值;butyOffset 指的是元素的偏移值,表示从数组中第几个元素开始读取,默认是 0,也就是数组的第一个元素;length 指的是在设置了偏移值后,要读取的元素长度,默认是整个数组的长度;举例说明: var buf = new Int8Array([1,2,3,4,5]).buffer; var i8 = new Int8Array(buf, 1, 2); console.log(i8); // Int8Array(2) [2, 3] 也就是让申明的类型化数组在提供的 buffer 的基础上,从它的索引为 1 的元素(第二个元素)开始读取,然后向后读取 2 个元素;该操作一般用于对缓冲区数据的截取; 类型差异 存在即合理;根据前面的介绍,TypedArray 定义了多种类型,如 Int8Array, Uint8Array, Int16Array 等,这样做也是为了适应不同的应用场景,接下来大致了解一下几个典型的类型化数组之间的区别; 有无符号 以 Int8Array 和 Uint8Array 为例,其实 有符号 的意思是数组中的元素可以存在符号,即可以是负数;因此 无符号 的意思就是元素只能是非负数,举例: var i8 = new Int8Array([1, 2, 3]); var _i8 = new Int8Array([-1, -2, -3]); var ui8 = new Uint8Array([1, 2, 3]); var _ui8 = new Uint8Array([-1, -2, -3]); console.log(i8); // Int8Array(3) [1, 2, 3] console.log(_i8); // Int8Array(3) [-1, -2, -3] console.log(ui8); // Uint8Array(3) [1, 2, 3] console.log(_ui8);// Uint8Array(3) [255, 254, 253] 可以发现有符号类型之处初始化负数元素,而无符号则会对负数进行转换,具体转换方式后面会提到; 元素范围 有无符号的类型数组,除了元素的值的正负区别外,元素的取值范围也有所不同;下面是一份具体的清单: Type Range Int8Array -128 ~ 127 Uint8Array 0 ~ 255 Uint8ClampedArray 0 ~ 255 Int16Array -32768 ~ 32767 Uint16Array 0 ~ 65535 Int32Array -2147483648 ~ 2147483647 Uint32Array 0 ~ 4294967295 Float32Array 1.2×10-38 ~ 3.4×1038 Float64Array 5.0×10-324 ~ 1.8×10308 BigInt64Array -263 ~ 263-1 BigUint64Array 0 ~ 264-1 可以看出,为了顾及有无符号类型的单个元素取值范围区间一样,所以就调整了它们的取值上下限; 字节位数 以有符号类型为例,可以发现有 Int8Array, Int16Array 等几个不同的类型数组,唯一的区别就是他们构造函数名字中间的数字不同,其实这个数字指的是实例化后的类型化数组的单个元素的大小,即多少位,8 就是 8 位,即一字节,16 就是 2 字节,类推;其实,这个数字也反应了类型数组中 BYTES_PER_ELEMENT 这个属性的值,从名字也可以看出代表的是每个元素的字节数;举例说明: var i8 = new Int8Array(3); var i16 = new Int16Array(3); var i32 = new Int32Array(3); console.log(i8.BYTES_PER_ELEMENT); // 1 console.log(i16.BYTES_PER_ELEMENT); // 2 console.log(i32.BYTES_PER_ELEMENT); // 4 console.log(i8.length); // 3 console.log(i16.length); // 3 console.log(i32.length); // 3 console.log(i8.byteLength); // 3 console.log(i16.byteLength); // 6 console.log(i32.byteLength); // 12 另外 byteLength 这个属性其实指的是类型数组总的字节大小,其值等于单个元素字节值乘以元素个数(byteLength = BYTES_PER_ELEMENT x length); Clamped 鹤难隐于鸡群;从前面的清单中可以找到 Uint8ClampedArray 这个独特的类型数组,区别就是中间多了 clamped 这个单词,词典解释的意思是“夹紧,箝位”,具体功能是什么,下面通过代码来解释: var i8 = new Uint8Array([1, 2, 3]); var _i8 = new Uint8Array([-1, -2, -3]); var _i8_ = new Uint8Array([255, 256, 257]); var uic8 = new Uint8ClampedArray([1, 2, 3]) var _uic8 = new Uint8ClampedArray([-1, -2, -3]) var _uic8_ = new Uint8ClampedArray([255, 256, 257]) console.log(i8); // Int8Array(3) [1, 2, 3] console.log(_i8); // Int8Array(3) [255, 254, 253] console.log(_i8_); // Uint8Array(3) [255, 0, 1] console.log(uic8); // Uint8ClampedArray(3) [1, 2, 3] console.log(_uic8); // Uint8ClampedArray(3) [0, 0, 0] console.log(_uic8_);// Uint8Array(3) [255, 255, 255] 不知诸位可否查探出端倪,这里也能解释之前说的无符号类型数组实例化时转换负值的问题;通过分析不难发现,转换方式类似于素组循环取值,就是如果传入的值超过了元素的取值范围的上限或下限之一时,那么超过的部分就会,从范围的另一个界限开始依次向后计数;所以上例中 -1 会被转换为 255,257 会被转换成 1; 而对于 Uint8ClampedArray 这个类型数组,其实差不多也是字面的意思,类似于一个 “夹住” 的操作:超出范围不会发生循环转换,无论超出多少都只会被置为对应的边界值,所以上例中 -1, -2, -3 都被转换为 0,256, 257 则都被转换成了 255; 浮点数 论世间谁主浮沉;仅有的两个浮点类型的类型数组,Float32Array 和 Float64Array,浮点的意思就是元素值可以是小数,因为之前介绍过的都是 int(整数) 类型的;依然来举例说明: var f32 = new Float32Array([1.11, 2.12345678911, -3.33333333333333333333333333]) var f64 = new Float64Array([1.11, 2.12345678911, -3.33333333333333333333333333]) console.log(f32); // Float32Array(3) [1.1100000143051147, 2.1234567165374756, -3.3333332538604736] console.log(f64); // Float64Array(3) [1.11, 2.12345678911, -3.3333333333333335] 从结果来看 32 位浮点类型数组每个元素都保留到小数点后 16 位,而 64 位是最多保留到 16 位,具体的细节就不深究了; 操作元素 欲与二进制数据一决高低,首先肯定是选几样趁手兵器;虽然类型化数组拥有普通数组的大部分方法,比如 every, forEach, slice 等等,但也有自己特有的方法值得说一下,比如 .set() 这个方法; .set() 方法用于把指定数组的(所有)元素添加到当前数组的指定位置,接受的参数为 .set(array[, ofset]),这里的 array 可以是普通数组或类型化数组,offset 指的是偏移值,即从哪个位置开始写入指定数组元素;举例说明: var i8 = new Int8Array(6); var i81 = new Int8Array(6); var i82 = new Int8Array(6); var arr = [1, 2, 3]; var arr1 = [1, 2, 3, 4, 5, 6]; i8.set(arr, 2); console.log(i8); // Int8Array(6) [0, 0, 1, 2, 3, 0] i81.set(arr1, 2); console.log(i81); // Uncaught RangeError: offset is out of bounds i82.set(arr, 6); console.log(i82); // Uncaught RangeError: offset is out of bounds 证明无论是拷贝的数组大小超过原数组,还是偏移值过大使得拷贝结果超过原数组,都会报错提示偏移超过边界,因此使用时需计算准确; 操作缓冲区 箭在弦上,东风将至;前面将 TypedArray 描述为操作 ArrayBuffer 中数据的视图,下面就来看一下具体的操作方法; 数据读取 数据转换 敌不动我不动;使用类型化数组操作 ArrayBuffer 的数据前,需要先获取其中的数据,也就是把 ArrayBuffer 转换为 TypedArray 类型;先来看一下这两种类型互相转换的方法: ArrayBuffer 转换为 TypedArray: var buffer = new ArrayBuffer(5); // 先初始化 5 字节长的区域 var i8 = new Int8Array(buffer); // 再把数据传递进 TypedArray console.log(i8); // Int8Array(5) [0, 0, 0, 0, 0] 这里也可以验证,ArrayBuffer 新创建的区域数据都被置 0 了; TypedArray 转化为 ArrayBuffer: var i8 = new Int8Array(5); var buffer = i8.buffer; console.log(buffer); // ArrayBuffer(5) {} 读取方式 前面讲道,类型化数组有多种不同的实现,比如 1 字节有符号元素的 Int8Array,2 字节的 Int16Array 等;根据 ArrayBuffer 的定义,缓冲区是以 1 字节 为单位进行创建的,所以我们通常读取文本类数据使用 Uint8Array,因为它也正好每个元素的大小为 1 字节,当然,也可以选择用 Uint16Array 来 2 字节地挨个读,其他类型类推; 通过代码来观察一下具体的读取方式: var data = new Uint8Array([1, 2, 3, 4]) var buffer = data.buffer; var ui8 = new Uint8Array(buffer); var ui16 = new Uint16Array(buffer); console.log(ui8); // Uint8Array(4) [1, 2, 3, 4] console.log(ui16); // Uint16Array(2) [513, 1027] 原始数据 data 是 4 字节大小,通过 Uint8Array 就是以 1 字节为单位,所以得到的也是原始的数据 [1, 2, 3, 4],这里由于数据小所以有无符号无影响;而通过 Uint16Array 则是以 2 字节为单位进行读取,所以总的元素长度为 2(2 = 4 / 2),但是其中的单个元素 513, 1027 又分别是如何得到的呢?我们可以通过计算来探究一下: 首先看 1, 2 这两个元素,根据结果它们被读取成为了 513,那么就把这几个元素的二进制数表示出来(缓冲区就是存储的二进制数据): "1": 00000001 "2": 00000010 "12": 00000001 00000010 "513": 00000010 00000001 "21": 00000010 00000001 规律显而易见了,513 这个 2 字节的数据,其实是把 1 和 2 这两个挨着的 1 字节的数据,以 倒序 方式拼接在一起的; 再来看一下 3 和 4 这两个是否也是以同样的方法得出 1027 这个数据的: "3": 00000011 "4": 00000100 "34": 00000011 00000100 "1027": 00000100 00000011 "43": 00000100 00000011 结果不出所料,所以像 Uint32Array 等以多个字节读取数据的类型数组,方法也可以类推; 字节序 另外值得一提的是,上面所说的 倒序 拼接方式,其实有个专业术语,叫做 字节序(Endian),对应这个英文单词应该会感觉似曾相识,例如 Linux 中执行 lscpu 得到的结果中,就会发现它的存在: Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 36 bits physical, 48 bits virtual 字节序,或字节顺序(”Endian”、”endianness” 或 “byte-order”),描述了计算机如何组织字节,组成对应的数字。 这个字节序可以分为: Little Endia(低字节序):低位数据放入存储地址的低位,高位数据放入高位地址; 这种顺序就显得和内存上的存储地址顺序(阅读模式下低位在右,高位在左)保持一致,并且也是一种常见的方式,比如上面的英特尔处理器;只不过对于这种顺序人类阅读时就要反着读了(从右至左),比如上面例子中的数据 12 就是以 21 的顺序读取的,也可以类比这种日期格式:"Sat 07 Mar 2020"; Big Endian(高字节序):低位数据存入高位地址,高位数据放入低位; 这种顺序可能更符合人类的阅读习惯(从左至右),它一般应用在互联网标准的数据结构中,可以类比 "2020-03-07" 这种日期格式; 数据修改 下面通过类型化数组视图来尝试修改一下 ArrayBuffer 缓冲区中的内容: var buffer = new ArrayBuffer(3); var i8 = new Int8Array(buffer); console.log(i8); // Int8Array(3) [0, 0, 0] for (let i = 0; i < i8.length; i++) { i8[i] = 1; } var _i8 = new Int8Array(buffer); // 新建个视图验证是否修改成功 console.log(_i8); // Int8Array(3) [1, 1, 1] 数据拼接 用之前讲过的 .set() 方法来尝试将数据拼接进缓冲区: var buffer = new ArrayBuffer(6); var i80 = new Int8Array(buffer); console.log(i80); // Int8Array(6) [0, 0, 0, 0, 0, 0] var i81 = new Int8Array([1, 2, 3]); var i82 = new Int8Array([4, 5, 6]); i80.set(i81); i80.set(i82, 3); var _i80 = new Int8Array(buffer); // 验证是否修改成功 console.log(_i80); // Int8Array(6) [1, 2, 3, 4, 5, 6] 注意:这里不能使用数组的 .concat() 这个方法来进行元素拼接,因为类型化数组中并没有内置这个方法,不然会报错,如下: var arr1 = [1, 2, 3]; var arr2 = arr1.concat(4, 5, 6); console.log(arr2); // [1, 2, 3, 4, 5, 6] var i81 = new Int8Array([1, 2, 3]); var i82 = i81.concat(4, 5, 6); console.log(i82); // Uncaught TypeError: i81.concat is not a function 同样地,.splice() 这个可以替换元素的方法也不存在于类型化数组中; 数据视图(DataView) 概述 一个好汉三个帮;DataView 是另外一个用于从 ArrayBuffer 缓冲区中读写数据的视图接口,其特点就是考虑了 字节序 的问题,后面会讲; 语法为: new DataView(buffer [, byteOffset [, byteLength]]); 其中 buffer 指传入的数据缓冲区,如 ArrayBuffer;byteOffset 指偏移的字节量,默认第一个字节,byteLength 指要传入的数据的字节长度,默认整个 buffer 的长度;并且这三个参数都可以在实例化后通过相应属性(只读)访问到; var buffer = new Int8Array([1, 2, 3, 4]).buffer; var dv = new DataView(buffer, 1, 2); console.log(dv); // DataView(2) {} console.log(dv.buffer); // ArrayBuffer(4) {} console.log(dv.byteOffset); // 1 console.log(dv.byteLength); // 2 操作数据 DataView 提供了一系列的方法用于操作缓冲区的数据,先简单预览一下: Read Write getInt8() setInt8() getUint8() setUint8() getInt16() setInt16() getUint16() setUint16() getInt32() setInt32() getUint32() setUint32() getFloat32() setFloat32() getFloat64() setFloat64() Read 以 getInt8() 方法为例,可提供一个参数 byteOffset,表示偏移指定字节数,然后读取 1 字节(8 位)数据,默认 为 0(第一字节);而如果是 getInt16() 等用于获取大于 1 字节值以及浮点值的方法,还接受第二个可选参数 littleEndian,就是是否使用 little endian(低字节序,上文有讲)格式来读取数据,传入 true 就表示使用 little endian 格式,传入 false 或者不填,就使用 big endian(高字节序) 格式; var buffer = new Int8Array([1, 2, 3, 4]).buffer; var dv = new DataView(buffer); console.log(dv.getInt8(1)); // 2 console.log(dv.getInt16(0, true)); // 513 console.log(dv.getInt16(0, false)); // 258 console.log(dv.getInt16(0)); // 258 结果为 513 的这一行代码,使用的是 little endian 格式,并且 513 这个值也与之前 TypedArray 中关于 Int16Array 例子的结果一致,证明 TypedArray 默认使用的是 little endian 格式在操作数据缓冲区; Write 以 setInt8() 为例,接受两个参数:setInt8(byteOffset, value),第一个表示偏移字节量,用于定位,第二个则是要设置的具体值,非数字类型会报错;类似地,setInt16 等用于设置超过 1 字节的方法,也提供第三个可选参数 littleEndian,表示是否以 little endian 格式设置; var buffer1 = new ArrayBuffer(2); var buffer2 = new ArrayBuffer(4); var dv1 = new DataView(buffer1); var dv2 = new DataView(buffer2); dv1.setInt8(0, 1); dv1.setInt8(1, 2); var i8 = new Int8Array(dv1.buffer); console.log(i8); // Int8Array(2) [1, 2] dv2.setUint16(0, 513, true); dv2.setUint16(2, 513); var i16 = new Uint16Array(dv2.buffer); console.log(i16); // Int16Array(2) [513, 258] 需要注意的就是,因为 byteOffset 这个参数的单位始终是 1 字节,所以当写入超过一字节的数据时,相应的偏移值也需要增加,就像上例所以展示的一样; 对比 与前文所讲的 TyptedArray 视图接口相比,DataView 视图虽然兼容了不同平台的字节序问题,但是也没有了一些对整段数据进行修改拼接的功能,只能修改单个元素值;另外也不能用构造函数初始赋值,比如下面的情况: console.log(new Int8Array([1, 2, 3])); // Int8Array(3) [1, 2, 3] console.log(new DataView([1, 2, 3])); // Uncaught TypeError: First argument to DataView constructor must be an ArrayBuffer 所以,需要灵活地结合二者使用,以应对复杂的场景;兄弟齐心,其力断金; Blob Blob 构造函数用于描述一个 blob(Binary Large OBject,二进制大对象),即保存原始数据的类文件对象,支持保存 多种类型 的数据(不像 TypedArray,只能使用数字类型),并且数据是只读的,不可修改;另一个基于 Blob 的构造函数 File,就是用来处理用户上传文件的(<input type="file">)数据。 语法: new Blob(array, options); array 指的是一系列类型的数据构成的数组或者类数组,这些数据可以是字符串、ArrayBuffer、DataView、TypedArray、Blob、DOMString 等等;options 则是一个对象,可以包含以下两个属性: { type: "", // 传入的数据的 MIMS 类型,比如 text/plain,默认为空 endings: "" // 如何处理数据中的换行符,比如 \n 和 \r\n,因操作系统而异 // 值为 transparent 或者 native,默认为 transparent // native 表示替换为当前系统的换行符 // transparent 则表示不替换,保持数据内容 } 写入数据 通过几个例子来说明: var blob1 = new Blob([1, 2, 3]); var blob2 = new Blob(['a', 'bc', 'd e']); var blob3 = new Blob(['hello'], {type: 'text/plain'}); var blob4 = new Blob(new Int8Array([4, 5, 6])); var blob5 = new Blob([blob2]); console.log(blob1); // Blob {size: 3, type: ""} console.log(blob2); // Blob {size: 6, type: ""} console.log(blob3); // Blob {size: 5, type: "text/plain"} console.log(blob4); // Blob {size: 3, type: ""} console.log(blob5); // Blob {size: 6, type: ""} 如果参入的参数不是类数组的类型,则会报错: var blob1 = new Blob(123); var blob2 = new Blob('123'); var blob3 = new Blob({foo: 'bar'}); var blob4 = new Blob(true); var blob5 = new Blob(blob1); console.log(blob1); // VM3497:1 Uncaught TypeError: Failed to construct 'Blob': // The provided value cannot be converted to a sequence. console.log(blob2); // VM3497:1 Uncaught TypeError: Failed to construct 'Blob': // The provided value cannot be converted to a sequence. console.log(blob3); // VM3497:1 Uncaught TypeError: Failed to construct 'Blob': // The provided value cannot be converted to a sequence. console.log(blob4); // VM3497:1 Uncaught TypeError: Failed to construct 'Blob': // The provided value cannot be converted to a sequence. console.log(blob5); // VM3497:1 Uncaught TypeError: Failed to construct 'Blob': // The provided value cannot be converted to a sequence. 读取数据 写入 Blob 实例中的数据虽然不能修改,但是还是可以读取的,首先可以获取数据总的大小和类型(只读): var blob = new Blob(['a', 'b', 'c'], {type: 'text/plain'}); console.log(blob.size); // 3 console.log(blob.type); // text/plain .text() 方法用于获取 Blob 中的文本数据,返回值是一个 promise 对象,包含一个 resolved 状态的文本数据,无提供的参数; var blob = new Blob([1, 2, 3]); blob.text().then(data => { console.log(data, typeof data); }); // 123 string .arrayBuffer() 方法也用于获取 Blob 中的数据,并且返回一个 promise,无参数提供,只不过返回的是数据的 ArrayBuffer,即二进制数据缓冲区; var blob1 = new Blob([1, 2, 3]); var blob2 = new Blob(['a', 'b', 'c']); blob1.arrayBuffer().then(data => { console.log(new Uint8Array(data)); }); // Uint8Array(3) [49, 50, 51] blob2.arrayBuffer().then(data => { console.log(new Uint8Array(data)); }); // Uint8Array(3) [97, 98, 99] 计算以下也可以验证,类型数组中的数值确实是对应的原始数据的二进制值。 TextEncoder 临阵磨枪,不快也光;这还是一个处于 实验阶段 的接口,当前的接口将来可能发生改变,并且目前 IE 系列浏览器都还不支持,这里只作简单介绍; 顾名思义,这个构造函数的作用就是负责编码文本,其实就是以指定的编码格式,将传入的文本转换成该数据对应的 类型化数组;实例化时可以提供一个参数,用于编码格式,不过目前默认并且只使用 UTF-8 格式编码,所以可以省略; var encoder = new TextEncoder(); var arr = encoder.encode('abc'); console.log(encoder.encoding); // utf-8 console.log(arr); // Uint8Array(3) [97, 98, 99] 有编码就自然有解码,TextDecode 这个构造函数就与之对应,即将 ArrayBuffer 或者 ArrayBuffer View 类型的数据解码为相应的文本; var ui8 = new Uint8Array([97, 98, 99]); var buffer = ui8.buffer; var decoder = new TextDecoder(); var text1 = decoder.decode(ui8); var text2 = decoder.decode(buffer); console.log(text1); // abc console.log(text2); // abc 这样,除了上面的 Blob,这里的 TextEncoder 也可以用于将文本数据保存为 JavaScript 中的二进制缓冲数据; 处理文件数据 人外有人,天外有天,跨过了这二进制,便是更广阔的天地;说了一系列的关于二进制数据的保存和读写方法,也该谈谈其用武之地了; 要知道 JavaScript 中保存文本字符串什么的用变量就行了,缓冲区、类型数组、Blob 这些接口其实多数是用于处理文件数据相关的,因为它们有着不同的 MIME 类型,比如 .jpg .mp4 .bin 这些后缀的文件,JavaScript 并没有内置一些直接处理这些数据类型的接口(例如 .txt 文档就能可以处理),所以就需要以原生二进制数据的方式来保存或处理,方便用户上传、下载或预览;下面就将介绍一些文件处理相关的接口; File 前面讲到,File 是基于 Blob 的,所以也就继承了它的一些方法;File 用于提供有关文件的信息和内容,语法如下: new File(content, name[, options]); content 指要创建的文件内容,是 ArrayBuffer, View, Blob, DOMString 等类型构成的 数组 或者类数组;name 则是文件的名称或者路径;options 参数可选,包含 type 和 lastModified 两个属性; 举例: var content = new TextEncoder().encode('hello world!'); var file = new File(content, 'test.txt', { type: 'text/plain', // 可选,默认为空 lastModified: Date.now() // 可选,后面是默认值 }); console.log(file.name); // test.txt console.log(file.size); // 12 console.log(file.type); // text/plain console.log(file.lastModified); // 1583638485180 File 构造函数自身并没有自带一些方法,而是继承了 Blob 的方法,例如: var file = new File(['hello world!'], 'text.txt'); // 初始化内容可以直接是字符串,只是需要放在数组中 file.text().then(data => { console.log(data); // hello world! }); file.arrayBuffer().then(data => { var text = new TextDecoder().decode(data); console.log(text); // hello world! }); 其实,一般很少像这样用 File 接口来直接创建一个文件对象,多数是用在用户上传文件等情况,比如在网页中用 <input type="file" /> 标签来上传文档,而用户点击上传后,与文件相关的信息就被包含在了这个 input 标签的节点引用的 files 属性中,这个 files 属性值是一个 FileList 接口的实例,就是包含所有上传文件的数组,其中每个元素都是一个 File 接口的实例; 通过一个简单的 demo 进行说明: <!DOCTYPE HTML> <html> <head></head> <body> <input type="file" class="upload" /> <!-- 如果要上传多个文件,则使用: <input type="file" class="upload" multiple /> --> <input type="submit" value="Upload" onclick="doUpload()" /> <script> var upload = document.querySelector('.upload'); // 用户点击 Upload 按钮后执行 function doUpload() { var file = upload.files[0]; console.log(file); // File {name: "test.txt", lastModified: 1583634142542, lastModifiedDate: // Sun Mar 08 2020 10:22:22 GMT+0800 (中国标准时间), webkitRelativePath: "", size: 12, …} } // 也可以用这种方法获取文件对象, // 这个函数中的代码会在用户完成上传操作就执行,即使没点上传按钮 upload.onchange = function(el) { var file = el.files[0]; // 执行的操作... } </script> </body> </html> FileReader FileReader 是另一个用于读取文件数据的接口,其实例化后的一些方法与 Blob 中的 .text() 和 .arrayBuffer 方法类似,只不过返回的不再是一个 promise 对象,而是一个基于 事件 的接口;FileReader 一般也用于读取用户上传文件的数据; 语法: new FileReader(); // 实例化无须提供任何参数 事件处理 既然是以基于事件,那么就需要一系列处理不同事件的方法,列出如下: .onabort():该事件在读取操作被中断时触发。 .onerror():该事件在读取操作发生错误时触发。 .onload():该事件在读取操作完成时触发。 .onloadstart():该事件在读取操作开始时触发。 .onloadend():该事件在读取操作结束时(要么成功,要么失败)触发。 .onprogress():该事件在读取Blob时触发。 以上事件也可以使用 addEventListener() 方法的相应格式来设置回调函数; 加载状态 因为是基于事件的接口,所以 FileReader 提供了 readyState 这个属性,以不同值代表不同的数据加载状态: 0:数据尚未加载; 1:数据正在加载中; 2:数据加载完成; 数据加载完成后,可以使用 result 这个属性来获取文件内容; 数据加载 readAsText() .readAsText(file[, encoding]) 以文本字符串的形式读取 file (文件对象或者 Blob)中的数据,以 encoding 格式进行编码(默认 utf-8); var file = new File(['abc'], 'test.txt'); var reader = new FileReader(); reader.onloadstart = event => { console.log('loadstart state:', event.target.readyState); } reader.onload = event => { console.log('load state:', event.target.readyState); console.log('result:', event.target.result); } reader.onloadend = event => { console.log('loadend state:', event.target.readyState); } reader.readAsText(file); // loadstart state: 1 // load state: 2 // result: abc // loadend state: 2 readAsArrayBuffer() readAsArrayBuffer(file) 以 ArrayBuffer 的形式读取 file 中(文件或 Blob)的数据; var blob = new Blob(['a', 'b', 'c']); var reader = new FileReader(); // 使用监听器触发效果相同 reader.addEventListener('load', event => { console.log(event.target.result); console.log(new Uint8Array(event.target.result)); }) reader.readAsArrayBuffer(blob); // ArrayBuffer(3) {} // Uint8Array(3) [97, 98, 99] readAsDataURL() readAsDataURL(file) 同样是读取 file 中的数据,只是将文件中的内容以 base64 编码后,放进一个 DataURL 中(内容可以通过 URL 链接直接访问); var file = new File(['abc'], 'test.txt', { type: 'text/plain' }) var reader = new FileReader(); reader.onload = event => { console.log(event.target.result); } reader.readAsDataURL(file); // data:text/plain;base64,YWJj 如果将最后的输出内容粘贴复制进浏览器的地址栏,回车后就能直接看见文本内容; 呈现数据 DataURL Data URL 指的是一种 URL 协议,语法格式为: data:[<mediatype>][;base64],<data> 可以类比常见的 http: 协议,例如上例中的返回值: data:text/plain;base64,YWJj 具体用法如之前所述,输入到浏览器地址栏回车后会直接呈现出原内容,比如上例就是一串文本(文件类型被指定为 text/plain),如果类型 image/png 等图片格式的,则会直接显示该图片; Data URL 除了可以通过浏览器地址栏访问,也可以在 HTML 文档中展示,例如使 <img> 的 src 属性值等于这个 Data URL,这个标签就会展示为相应的图片,同样地,数据指定给 <iframe> 的 src 属性,也可以展示图片或者文本数据,指定给 <video> 标签的 src 则可以展示视频; ObjectURL ObjectURL 使用 URL.createObjectURL() 方法创建,返回结果也是一种类型的 URL,类似于上面的 Data URL,区别在于 ObjectURL 的生命周期与当前网站页面相关,例如 刷新页面 页面后不无法继续访问了; 例如在本地网页控制台中运行下面的代码: var blob = new Blob(['abc']); console.log(URL.createObjectURL(blob)); 则会输出类似下面的内容: blob:http://127.0.0.1:8080/4064e759-231f-466e-a6ef-778505e56d2b 链接临时有效,会展示数据内容刷新页面失效,不过格式基本一致;同样,ObjectURL 也可以用于设置为 <img> 或 <iframe> 的 src 属性,进行单独展示; 需要 注意,createObjectURL() 方法每次调用都会返回一个新的 ObjectURL 对象,即使数据源相同,所以如果调用量较多,可能就会内存剧增,这时需要手动回收,使用的是 revokeObjectURL() 这个方法,示例: var url = URL.createObjectURL(new Blob(['test'])); URL.revokeObjectURL(url); // 完成回收 文件下载 除了使用 <img, <iframe> 等标签对数据进行展示,也可以将文件提供给用户下载,使用的是 <a> 标签,把 DataURL 或者 ObjectURL 指定给它的 href 属性即可,另外还要指定 download 属性值,不然有可能会是跳转到相关页面而不是下载; 一个下载组件的示例: <a href="data:text/plain;base64,YWJj" download="test.txt" type="text/plain">Download</a> download 属性指代下载到用户本地的文件名称,不加后缀则系统自动识别类型,同样 type 属性也是可选的,可用于固定下载文件类型; 上传数据 让用户通过网页上传文件,最重要的当然就是最后的上传阶段了,即把用户选择的文件上传到服务器;下面的例子使用 XMLHttpRequest() 接口来实现数据的上传; var file = new File(['hello world!'], 'hello.txt', { type: 'text/plain' }); // 此处用于模拟用户上传的文件,即有具体的文件名、类型和内容 var xhr = new XMLHttpRequest(); var reader = new FileReader(); // 查看上传进度 xhr.upload.onprogress = event => { if (event.lengthComputable) { console.log('进度:', event.loaded + '/' + event.total); } } // 上传完成的回调 xhr.upload.onload = event => { console.log('upload success.'); } // 上传地址,参数换成实际地址 xhr.open('POST', 'http://localhost/upload/upload.php'); // 服务器没有指定文件类型则自行指定 xhr.overrideMimeType('text/plain'); reader.onload = event => { // 数据读取完毕就开始上传 xhr.send(event.target.result); } reader.readAsText(file); 另外也可以使用 form 表格来上传文件,更加直接: <form action="upload/upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="upload" > <input type="submit" value="Upload"> </form> 需要 注意 的是,上传文件时 必须 加上 enctype="multipart/form-data",不然上传上去的只是一个文件名; 接收数据 投我以木瓜,报之以琼琚;有时也会接收来着服务端的数据,通常就是使用 XMLHttpRequest 来异步获取文本或 JSON 数据,但是它也能用于获取其他类型的数据,只不过需要手动设置 responseType 这个属性进行申明,该属性支持以下几个值: "":默认值,与 text 类型相同; "text":以文本类型响应; "arraybuffer":以 ArrayBuffer 二进制数据响应; "blob":以 Blob 类型数据响应; "json":响应解析为 JSON 对象; "document":解析为 HTML 或 XML 内容; 一个接收数据的实例: var xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; xhr.onload = () => { var buffer = xhr.response; // 可以转换为类型化数组进行数据修改 console.log(new Uint8Array(buffer)); } xhr.open('GET', 'test.png'); xhr.send(); 至此,历经几番交战,刀光剑影,战况激烈空前,难分难解,不下几十回合,能阅读至此处的诸位也都是真正的勇士,敢于面对惨淡的生活,正视淋漓的鲜血……又扯远了,俗话说,物以稀为贵,人以和为贵,JavaScript 剑客与二进制隐士此番交战,不求胜负,若这过程中的原理能被大家理解参透得透彻,也算是名留青史了; 恩怨自了结,情仇终消散,天下没有不散的宴席,暂且就此别过,江湖再见!

2020/3/9
articleCard.readMore

个人博客网站文章添加目录导航

概述 之前有写文章探索如何给个人博客网站添加文章搜索功能,可以方便的通过关键词检索相关文章,现在再来探索一下另一个功能,即给文章添加目录导航;对于篇幅较短的文章,目录的有无影响不大,但是当文章篇幅过长时,一个能提供预览和跳转的目录结构预览,就显得意义重大了,接下来就来一步步将它实现出来; 原理 样式 实现功能必先思考其原理,目录预览其实就是一块内容,包含当前页面不同级别的标题的组合,并结构化的展示出来,首先我们可以参考一些网站的做法,比如 CSDN 的博客文章就有配置目录插件,下面就是我的某篇文章的目录预览图: 它们的目录插件就是右边侧栏的一个按钮,鼠标放上去就会显示一个侧栏,内容就是当前文章的小标题的集合,不同级别的标题对应这不同程度的缩进,并且点击每个标题都会有相应的页面跳转,这也基本是我们常见而熟悉的目录形式,那么我们就以此为参考来实现; 目录获取 想要生成这么一个目录之前,当然是要先获取目录的内容,前面讲过,目录的内容就是当前文章的所有标题的集合,而我们知道,在 HTML 中标题相关的标签是 h1, h2, h3, h4, h5, h6 这几个,所以直接获取它们就行了,比如: // 获取所有的标签名为 h1 的元素 document.querySelectorAll('h1'); // 获取所有的标签名为 h1 - h6 的元素 document.querySelectorAll('h1, h2, h3, h4, h5, h6'); 获取内容是一个数组,包含所有标题节点;接下来的问题就是考虑如何结构化存储,这样便于理解的同时又方便后期的读取,所谓结构化,即目录本身就是一类 树 结构,比如,目录包含多个一级标题,同时某些以及标题可能还有多个二级标题,甚至再向下延伸出三级标题等等,类似下面的结构: ├─ 一级标题 1 │ └─ 二级标题 1 │ └─ 三级标题 1 │ └─ 四级标题 ├─ 一级标题 2 │ └─ 二级标题 1 ├─ 一级标题 3 │ ├─ 二级标题 1 │ ├─ 二级标题 2 │ ├─ 二级标题 3 │ │ ├─ 三级标题 1 │ │ ├─ 三级标题 2 ... ... 理论的做法就是以树结构保存获取的标题,类似于下面这种: [{ node: 'h1Node', // 一级标题 1 对应的节点 child: [{ node: 'h2Node', // 二级标题 1 child: [{ node: 'h3Node', // 三级标题 1 child: [] }] }, { node: 'h2Node', // 二级标题 2 child: [] }] }, { node: 'h1Node', // 一级标题 2 child: [{ node: 'h2Node', // 二级标题 1 child: [] }] }, { node: 'h1Node', // 一级标题 3 child: [] }] 看着还是比较复杂的,耗费的空间也较大,需要递归式获取,最后也要递归式的输出,一般文章目录包含的标题数量也是较少的,所以暂且不用这种结构来保存,可以换一种简单的思路,即我们最后生成该目录时可以选择一行一行递进的输出,即设计如下结构: [ 'h1Node', // 一级标题 1 对应的元素节点 'h2Node', // 二级标题 1 (隶属于一级标题 1) 'h3Node', // 三级标题 1 (隶属于二级标题 1) 'h2Node', // 二级标题 2 (隶属于一级标题 1) 'h1Node', // 一级标题 2 'h2Node', // 二级标题 1 (隶属于一级标题 2) 'h1Node' // 一级标题 3 ] 因为我们只要求最终能输出一列格式化的目录,即挨个依次输出,所以只需以此存储即可,这样占用的空间和复杂度都有所减少; 目录生成 最终展示效果设定为最上面的样图所示,按照之前设计的存储结构,遍历该数组一行一行打印出来即可;关于不用级别的标题应用不同程度的缩进,可以巧妙利用一下元素节点的 nodeName 这个属性,比如元素节点 <h1></h1> 对应的 nodeName 就是 H1,h2 就对应 H2,以此类推,我们就利用该值最后的那个数字,乘以一个固定缩进值,这样级别递增的标题节点也就拥有了递增的缩进值,最后样式部分就可以利用 padding-left 来实现缩进,js 代码的实现思路如下: // node 为标题节点,32 是标题级别增加而多缩进的值 node.style.paddingLeft = node.nodeName.slice(-1) * 32 + 'px'; 对于点击标题跳转到文章对应标题所在位置这个功能,实现也比较简单,设置对应 锚点 即可,也就是标题元素需要设置一个 id 属性值,然后给点击的 <a> 标签的 href 属性也设置为这个 id 值即可,例如: <h1 id="my-h1">一级标题</h1> <a href="my-h1">点我跳转到一级标题</a> 具体实现 第三方库 避免重复造轮子,一些基础的格式化样式就交给第三方库去解决吧,这里使用的是 Materialize 这个库,安装和引用教程去官网:https://materializecss.com 查看; HTML部分 具体参考代码与说明如下: <!-- 引用第三方库 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <!-- 固定于屏幕右下方的一个悬浮按钮 --> <div class="fixed-action-btn"> <a class="btn-floating btn-large blue z-depth-4"> <i class="large material-icons">apps</i> </a> <ul> <!-- 文章目录按钮 --> <li class="category-btn hide"> <a class="sidenav-trigger btn-floating blue lighten-2" data-target="category"> <i class="material-icons">format_list_bulleted</i> </a> </li> <!-- 下面也可以添加其他按钮,如返回文章顶部等--> <li> <a class="btn-floating blue lighten-2" href="javascript: scrollTo(0, 0);"> <i class="material-icons">publish</i> </a> </li> </ul> </div> <!-- 文章目录侧栏 --> <ul id="category" class="hide sidenav grey lighten-4 grey-text text-darken-3"> <li><p class="center-align">目录</p></li> </ul> <!-- 下面的元素中存放文章内容 --> <div id="post-content"> <!-- 文章内容,需要注意的只有,为不同的标题元素设置不同的 id 属性以实现跳转 --> <!-- 以下为示例内容 --> <h1 id="t1">Title 1</h1> <p>Hello World!</p> <p>Hello World!</p> <h2 id="t11">Title 1</h2> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <h2 id="t12">Title 1</h2> <h3 id="t121">Title 1</h3> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <h1 id="t2">Title 2</h1> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <h1 id="t3">Title 3</h1> <p>Hello World!</p> <h2 id="t31">Title 3</h2> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> <p>Hello World!</p> </div> CSS部分 样式部分因人而异,可以自行设计调整,以下为参考: #category li a:before { /* 添加一个折叠符号,为了好看 */ content: "∟"; position: absolute; left: 10px; bottom: 5px; font-size: 12px; } JavaScript部分 该部分就是核心所在了,对应上面的 HTML 和 CSS 部分,实现如下: // 初始化第三方库的插件 M.AutoInit(); document.addEventListener('DOMContentLoaded', function () { var elemCategory = document.querySelector('#category'); M.Sidenav.init(elemCategory, { 'edge': 'right' // right 表示在右侧栏显示,left 则表示在左边显示 }); }); var postContent = document.querySelector('#post-content'); if (postContent) { // 存在文章内容 var categories = postContent.querySelectorAll('h1, h2, h3, h4, h5, h6'); if (categories.length > 0) { // 文章存在标题 var category = document.querySelector('#category'), categoryBtn = document.querySelector('.category-btn'); var li = document.createElement('li'), a = document.createElement('a'); a.className = 'waves-effect'; // 存在目录则显示目录按钮和侧栏 category.classList.remove('hide'); categoryBtn.classList.remove('hide'); categories.forEach(node => { // 每次 cloneNode 取代 createElement // 因为克隆一个元素快于创建一个元素 var _li = li.cloneNode(false), _a = a.cloneNode(false); _a.innerText = node.innerText; // 为标题设置跳转链接 _a.href = '#' + node.id; _li.appendChild(_a); // 为不同级别标题应用不同的缩进 _li.style.paddingLeft = node.nodeName.slice(-1) * 32 + 'px'; category.appendChild(_li); }) } } 效果 最后附上几张本人博客网站实现的最终效果图,也欢迎点击 https://knightyun.github.io 前往访问 ^_^

2020/1/14
articleCard.readMore

JavaScript 数组排序详解

提到 JavaScript 中对数组进行排序操作,可能首先想到的就是 Array.prototype.sort() 这个函数,比如以下场景就比较常见: var arr = [3, 1, 2]; console.log(arr.sort()); // [1, 2, 3] console.log(arr); // sort() 函数会修改原数组 // [1, 2, 3] arr = ['c', 'b','B', 'a','A']; arr.sort(); console.log(arr); // ["A", "B", "a", "b", "c"] 和预想的一样,sort() 函数默认将数组元素升序排列,但是不要被上面的数字数组的排序结果迷惑,该函数并不是按照数字递增的方式排列的,而是按照元素的 ASCII 码或者 Unicode 码进行排序,比如字符 a 对应的 ASCII 码要比字符 b 的小,所以 a 排在 b 前面,同样字符 A 的比字符 a 的小,所以大写字母 A 会排在小写字母 a 前面;考虑以下情景: var arr = [1, 2, 11, 12]; arr.sort(); console.log(arr); // [1, 11, 12, 2] 是不是有些和预想的不一样,这也验证了之前所说,并不是按照数字递增在排序,而是把数组中的数字类型的元素转换成字符,在拆分字符比较单个字符对应的字符码的大小; 比较函数 那么问题就来了,要按照数字递增方式排序,该怎么操作呢?其实这种情况早就被 .sort() 函数考虑到了,只是可能被大家忽略了,就是 .sort() 函数还能接受一个参数,叫做 compareFunction,顾名思义,就是 比较函数,由于该参数是一个函数,所以该函数又能接受两个参数,即比较的值,所以最终就是 .sort(compareFunction(a, b)); 关于这个 比较函数,存在如下规则: 如果 compareFunction(a, b) 返回值小于 0 ,那么 a 会被排列到 b 之前; 如果 compareFunction(a, b) 返回值等于 0 ,那么 a 和 b 的相对位置不变; 备注: ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如 Mozilla 在 2003 年之前的版本); 如果 compareFunction(a, b) 返回值大于 0 ,那么 b 会被排列到 a 之前; compareFunction(a, b) 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。 在使用它之前,先来看看函数里面的参数 a, b 是如何对应数组元素的: var arr = [2, 1, 4, 3]; arr.sort(function(a, b) { console.log(a, b); }) // 1 2 // 4 1 // 3 4 可以发现,由于这里的比较函数没有返回值,所以对数组就没有排序操作,而每一次遍历中,第二个参数 b 对应前一个元素,第一个参数 a 对应后一个元素;当然该函数的具体排序方法就不得而知并且因 JS 引擎而异了; 升序 对数组按照升序方式排序,即小的元素排在前面,大的元素排在后面,假设比较函数当前遍历的元素对为 (2, 1),则 a = 1, b = 2,要想升序就要 a 排到 b 的前面,对应上面的规则,就是需要比较函数的返回值小于 0,由于当前 a - b < 0;所以直接返回一个 a - b 就行了,代码如下: var arr = [2, 1, 3, 11, 12, 11] arr.sort(function(a, b) { return a - b; }) console.log(arr); // [1, 2, 3, 11, 11, 12] 针对上面的代码再来分析下,在每一次遍历比较的两个元素中: 如果后一个元素比前一个元素小,即 a - b < 0,按照规则就是 a 要排到 b 的前面,也就是这两个元素会交换,小的在前,大的在后; 如果后一个元素比前一个元素大,即 a - b > 0,按照规则就是 b 要排到 a 的前面,由于 b 本来就在 a 的前面,所以两元素位置不变; 如果后一个元素与前一个元素相同,即 a - b = 0,按照规则就是 a 和 b 的位置不变,两元素位置同样不变; 最后,数组就变成升序的了; 降序 原理和升序类似,只是思路反过来了,代码如下: var arr = [2, 1, 3, 11, 12, 11] arr.sort(function(a, b) { return b - a; }) console.log(arr); // [12, 11, 11, 3, 2, 1] 同样来分析一下,每一次遍历中: 如果前一个元素比后一个元素小,即 b - a < 0,按照规则就是 a 要排到 b 的前面,也就是这两个元素会交换,大的在前,小的在后; 如果前一个元素比后一个元素大,即 b - a > 0,按照规则就是 b 要排到 a 的前面,由于 b 本来就在 a 的前面,所以两元素位置不变; 如果前一个元素与后一个元素相同,即 b - a = 0,按照规则就是 a 和 b 的位置不变,两元素位置同样不变; 最后,数组也就变成降序了; 反序 这是排序函数一个另一个应用,作用相当于 .reverse() 函数,即让数组中的元素顺序颠倒,实现也很简单,就是利用规则,让每次比较函数的返回值小于 0 就行了,例如: var arr = [2, 1, 4, 3]; arr.sort(function(a, b) { return -1 }); console.log(arr); // [3, 4, 1, 2] 乱序 这也算是一个比较实用的用途了,即将数组中元素的位置和顺序打乱,增加随机性,实现也简单,即利用规则,让比较函数的返回值随机为 > 0, < 0, = 0 这三种情况之一,使得元素是否交换位置具有随机性,也就实现了顺序的打乱,实现代码如下: var arr = [1, 2, 3, 4, 5]; arr.sort((a, b) => { return 0.5 - Math.random(); }); console.log(arr); // [4, 3, 2, 1, 5] arr.sort((a, b) => { return 0.5 - Math.random(); }); console.log(arr); // [5, 3, 1, 2, 4]

2020/1/12
articleCard.readMore

手机端(安卓)安装 Kali Linux 系统详细教程

前言 之前写有文章介绍过关于 Kali Linux 系统安装的详细教程,不过是在电脑端真机或虚拟机中的安装步骤,那么作为平时使用最多并且能方便携带的智能手机端,是否有方法或技术来实现安装呢?还确实有,这也是该文章的主旨。我们都知道安卓系统本身就是 Linux 系统的一个发行版本,所以在技术理论上是有可行性的,并且目前还确实有人实现了,至于苹果手机(iOS系统)端,笔者没做研究,就不做阐述了,有兴趣可以自行了解。那么现在就来介绍一下如何在手机端(安卓系统)进行 Linux 系统(包括但不限于 Kali)的安装。 原理概述 其实实现该技术的原理并不复杂,由于安卓是 Linux 系统的一个分支,所以和其他主流 Linux 操作系统之间有着很大的共同性,比如很多 shell 指令代码都能使用。另外,Linux 命令中有一个叫 chroot 的,大概的作用就是在指定的根目录下运行命令,通俗讲,类似于 Windows 系统中的安全沙箱的概念,即一个安全的隔离环境,这也是该命令的目的所在。因此,既然能在该指定目录下运行一些命令,那么要是把整个操作系统的命令、目录以及文件都装进去,那它不就成了一个嵌入的系统了吗,这里又有些类似虚拟机的概念。 当然,要在手机中执行这些命令就免不了要手机获得 root 权限,即对手机进行 ROOT;不用担心,这只是其中一个办法,本文也会介绍另一种不需要 ROOT 手机的方法。总结一下二者的原理:ROOT 操作相当于直接在手机安卓系统中挂载一个 Linux 系统(例如 Kali),而非 ROOT 方法就有些绕,类似于在安卓系统中通过软件挂载一个能执行一些命令的环境(当然这样会有很多权限限制,毕竟没有 ROOT),然后在这个环境中就可以再挂载想要的 Linux 操作系统了,由于这个系统在一个虚拟的环境中,所以就不存在权限的问题了(得不到真实的权限,给与虚拟的权限还是做得到的→_→,哈哈哈)。通俗讲就是,ROOT 使用的是真机中的虚拟机,而非 ROOT 使用的是真机中的虚拟机中的虚拟机(感觉绕可以再读几遍)。 下面是安装系统的两种方法: ROOT 环境 主要软件 Linux Deploy 有 root 权限的话,安装就比较简单,并且安装的系统使用也比较流畅,毕竟是直接与真机硬件交互的,然后就先介绍一下该方法的主角:Linux Deploy,它是一个安卓软件,图标是一个小企鹅,在各大应用市场应该都能搜得到,找不到的话可以在这里下载: 链接: https://pan.baidu.com/s/16OKQc2ru5E7yOTliDzNuUw 提取码: vi7w VNC Viewer 另外需要一款远程连接软件(这里使用 VNC Viewer)来显示系统界面,因为系统安装到手机中后,一般不会直接把桌面和窗口输出到手机的主屏幕,所以需要远程桌面软件连接到该系统来进行显示,否者就是使用终端连接(如 SSH)的纯命令行操作,VNC Viewer 软件也能在各大市场搜到,或者从下面的链接下载: 链接:https://pan.baidu.com/s/1cjBii8MWSAqsM-9urW658Q 提取码:h7si Terminal 一款安卓的终端软件(非必需),可以在手机上执行一些 Linux 命令,也可以使用它提供的 SSH 连接到之后安装的系统,依然,市场搜不到可以在下面的链接下载: 链接: https://pan.baidu.com/s/1ZOUjbmW0MLjbMunAGecGfA 提取码: 5jhj 下面是 Deploy 软件主界面的截图: 配置 在安装系统前,需要进行一些配置,顺便解释一下各个配置选项,先点击软件主页的配置按钮(上图右下角)进入配置页面: 发行版本,点进去选择想要的 Linux 发行版,这里以 Kali 为例: 架构,点进去会出来这么几个选项: 这里简单解释一下选项中的五种架构,前三个 armel, armhf, arm64 都是 ARM 处理器(常见的手机处理器品牌,类似于电脑的 Intel)的几个版本,区别如下: armel:(arm eabi little endian)也即softfp,用fpu计算,但是传参数用普通寄存器传,这样中断的时候,只需要保存普通寄存器,中断负荷小,但是参数需要转换成浮点的再计算; armhf:(arm hard float)也即hard,用fpu计算,传参数用fpu中的浮点寄存器传,省去了转换性能最好,但是中断负荷高; arm64:64位的arm默认就是hard float的,因此不需要hf的后缀; 至于第四个 i386 是 Intel 的 32 位处理器架构,最后一个 amd64 则是 AMD(一家类似 Intel 的处理器公司)的 64 位处理器架构; 那么要如何判断自己的手机是哪种架构呢?方法很多,可以使用上面提到的 Terminal 软件,通过 cat /proc/cpuinfo 命令查看手机的 CPU 架构信息,如图所示: 这一步查询可能会看到诸如 ARMv8, ARMv7, Cortex-A, AArch64, AArch32 等字眼,简单解释一下,ARMv7 和 ARMv8 是两个 ARM 的版本,v7 版本是 32位 的,v8 是 ARM 公司的第一款 64位 处理器架构,并且从 v7 版本后开始变成了 Cortex 架构,包括 Cortex-A、Cortex-R、C ortex-M 系列,其中的 Cortex-A 系列就是常见的只能手机处理器,另外两个多应用于车载系统,嵌入式微控制器等领域;ARMv8-A 有两种执行状态: AArch64 和 AArch32(后者是为了兼容以前的32bit的程序),AArch64执行A64指令,使用 64bit 的通用寄存器;AArch32 执行 A32/T32 指令,使用 32bit 的通用寄存器; 所以可以简单的理解为 ARMv7 是 32位,ARMv8 是 64位的;如果不想用终端和命令查看,也可以使用软件直接查看,比如著名的 AIDA64,可以在市场搜索下载,或者使用链接: 链接: https://pan.baidu.com/s/1wvUc0VhWyhb-QGbkd9LylQ 提取码: mqby 打开后点击中央处理器模块,内核架构信息里就是要找的架构类型,这里的是 ARMv8 的,如下图: 因此我们在配置里选择 ARM64; 发行版的版本,之前选择 Kali 的话就默认只有一个 kali-rolling 版本,其它系统的话自行决定: 源地址,就是软件从哪里去获取系统镜像等相关文件,这里用默认的就行: 当然官方源的是网站是国外的,速度可能有点慢,可以选择使用国内的源,比如阿里云和中科大等,详细介绍可以参考这篇 文章,比如使用阿里云的就把地址改为以下内容: http://mirrors.aliyun.com/kali/ 安装类型,一般就选择默认的镜像文件类型,方便安全,至于其它类型就是它们字面上的意思,所以可能会和系统出现冲突: 安装路径,就是决定把镜像文件安装到哪,并且以什么名字命名,默认的值使用了环境变量,测试似乎存在问题,所以改一下,比如我们安装到内置存储的根目录,以 kali.img 命名,就是 /sdcard/kali.img,注意镜像文件的后缀 .img 是固定不变的: 镜像大小,默认是 0,即自动分配大小,不过经测试,它似乎没有想象中那么自动 -_-,所以我们就自己设一个值,注意单位是 MB,并且这个值就是安装的系统能用的总存储空间,因此可以稍微设置大一点,具体值随便填,这里的 8096 就是 8GB 左右(1024MB = 1GB): 文件系统,现在最新的是 ext4,是 Linux 下常用的文件系统,类似于 Windows 里的 NTFS 文件系统,或者 U盘使用的 FAT32 文件系统,所以这里选择默认的就好: 用户名,即登录系统的账户名(以 test 为例): 用户密码,上面的账户对应的密码(也以 test 为例): 特权用户,这里填写超级用户 root: 本地化,即系统使用的语言,先使用默认的 C(英语),想要中文可以后期改: DNS、Network trigger、Power trigger 这三项可以不用管,让它们空着; 初始化,启用选项可以不用勾上; 挂载,即挂载 Android 上的资源,类似于一个文件中转站,设置为手机系统中的一个文件夹,这样这个文件夹中的内容 Linux 系统和手机系统都可以访问,也就实现了互传文件的功能,我们可以勾上: 点击 挂载点列表 后进去,点右上角的加号,添加一个挂载点;第一行填写你设置为文件中转站的目录(该文件夹自己事先创建好)的绝对路径,比如该文件夹创建在 SD 卡根目录,名为 share,那就写 /sdcard/share: 点确定后完成创建,返回: SSH,即是否开启 Linux 系统的 ssh 服务,勾上后就可以在手机终端用 ssh 命令连接 Linux 系统,这里我们勾上,然后下面的 SSH 设置 使用默认的不用改: 声音服务,即 Linux 系统的声音在手机上播放,这里我们也勾上,设置用默认的不改: 图形界面,这就是一个比较重要的设置了,因为一旦 Linux 系统在手机上安装好后,不安装图形桌面(类似于 Windows 的桌面)的话,就只能通过 ssh 进行命令行操作系统;勾选启用后,点击下面的 图形子系统 选项,会弹出以下选项: 一般选 VNC,另外两个选项后面介绍: 图形界面设置,对应上面的 VNC 结果,一般也是使用默认的不用改,了解选项含义的可以自行修改: 桌面环境,即图形桌面环境的外观,这里我们选 Xfce: 下面也贴出几个选项对应的桌面外观截图,以供参考: Xterm(无图形桌面,只有 shell): LXDE: Xfce: MATE: 配置结束,这里再提一下上面说过的 图形子系统 的另外两个选项,仅供参考;首先是 X11,熟悉 Linux 的应该了解这个选项,物理机(或虚拟机)安装的 Linux 桌面系统一般都是使用的 X11 的图形服务,选上后进入 图形界面设置 后是以下选项: 不了解的可以按上图配置,另外,使用改选项需要安装图中所说的 Xserver XSDL 这个软件,可以去市场下载,或者使用下面的链接: 链接: https://pan.baidu.com/s/1AuC09-HzT9ZWEwBzV7dRxg 提取码: swx5 安装好后不用特意打开它,Linux 系统启动时它会被自动打开,运行(需等待一段时间,不要点屏幕)截图如下: 如果 图形子系统 选择 framebuffer 的话,图形界面设置 选项如下: 设置也是默认不变,只不过由于这种图形显示技术比较特殊(古老),笔者尝试并未成功,手机还多次卡死 -_-,所以保守派就不要轻易尝试了,自担风险; 配置文件 上面进行的配置会被保存到一个配置文件中,点击 Deploy 软件主界面左上角的菜单按钮,选择第一 配置文件 选项即可对配置文件经行修改,这是默认的配置: 右上角的三个按钮分别是新建、重命名、删除,例如对当前配置文件重命名: 或者新建一个: 使用不同的配置来选择不同的系统,就可以实现一机安装多 linux 系统(手机存储足够前提下); 安装系统 配置完毕,接下来就是安装系统了;首先回到软件主界面,点击右上角的菜单按钮: 然后点击第一个 安装 按钮,然后弹出窗口中点 OK 确定: 随后软件就开始 Linux 系统的安装了,主界面会不断输出一些信息: 操作系统较大,下载安装时间较长,一般用时在半小时左右,网速好的话会快一点;当输出信息如下图的最后一行时,则表示安装完成: 然后下一步本应是点击右上角菜单的配置按钮,但是这样会出现像下面这个的 问题: 也就是输出一堆 skip,这是因为在没有关闭系统的情况下进行配置,会使得系统不能正常配置,所以需要先点击底部菜单的停止按钮(以后出现 skip 输出数量较多时也这样操作): 然后点击确认,最后输出 <<< stop 则表示停止成功: 这里再点击右上角菜单的配置选项,也需要等待一段时间: 配置结束后,点击启动按钮启动系统: 启动成功,都是 done 则表示正常启动,若 skip 较多则尝试停止后再启动(下图的 /dev/shm skip 可以不用管): 进入系统 Linux 系统启动完毕,接下来就是进入图形桌面环境,当然,由于之前开启了 SSH 服务,所以也可以使用之前下载的 Terminal 终端软件进入 shell 环境,这里由于之前选择 VNC 作为图形子系统来配置的,所以这里就用 VNC Viewer 这个软件来进入图形桌面; 打开 VNC Viewer 这个软件,进入主界面: 点击右下角的加号添加要连接的对象,这个软件本来是用于连接所有开有 VNC 服务的服务器的,由于我们把 Linux 系统安装在手机(即本地),所以地址填写本地地址:127.0.0.1,也可以写 localhost,一个意思,下面一行的名字随便写一个: 然后点击 create 创建连接,进入如下界面: 需要注意的只有 view only 这个不要选上,就是字面意思,只读模式;点击 CONNECT 进行连接,会提示输入密码: 密码就是之前在配置里面设置的用户密码,当时设置的是 test(之后记得自己修改密码),填上去,然后把记住密码勾上,再点击右上角的 CONTINUE: 后面出现的安全提示不用管,取消勾选 warn me every time,再点击右上角的 OK 就可以进入系统了: 初次使用软件进入有个教程,跟着做完就行了,忘记了按钮的意思就点右数第二个问号按钮查看就行了,下面再给出几张横屏的特写: 这里再提供一款输入法软件:Hacker’s Keyboard,翻译过来是黑客键盘,听着霸气,其实就是增加了计算机键盘中的 Ctrl, Shift, Tab, Alt 等键,因为常见手机输入法中不存在这几个键,在使用 Kali 系统时可以提供一些便利,自行搜索下载或使用以下链接: 链接: https://pan.baidu.com/s/1iPgSBmNYy9xzHfJg2aoaEg 提取码: ydah 也附上两张特写: 默认是横屏才出现特殊按键,可以自行在设置中修改: 关闭系统 不再使用系统后点右上角的叉号断开连接: 回到主界面也会保存系统的快照,下次直接点击就能进去了: 断开连接不代表系统关闭了,还要回到 Deploy 这个软件,点击底部的停止按钮: 如下图,则表示系统成功关闭了: 下次要启动系统,就点击启动按钮,再用 VNC Viewer 连接就行了,切换不同的 Linux 系统,只需要切换到对应的配置文件就行了,注意 每次切换或修改配置,都需要点击右上角的 配置 按钮进行重新配置,只有在安装新系统时才需要点击 安装 按钮; 至此关于 root 环境下使用 Deploy 安装 Kali 系统的教程就结束了,关于 Deploy 中未提到的其他菜单功能,可以自行探索; 非 ROOT 环境 当然,除去 root 玩家,总会存在那么一些手机厂商,让自家产品百试不得其 ROOT,让技术用户们又爱又恨-_-,所以这里也准备了非 ROOT 环境下的备选方案,当然,比起 ROOT 来说,确实会阉割部分功能,但主要功能都能用,所以建议能 ROOT 就不要退而求其次; 主要软件 Termux 这是该方法的主角,这个软件类似于在手机中搭建一个 Linux 虚拟机,可以执行一些常用的 Linux 命令,与之前说的 Terminal 这个终端软件主要的区别就是该软件可以安装第三方软件包,就是熟悉的 apt 系列命令,很方便,因此功能也较为强大;可以在应用市场搜索下载,或者使用下面的链接: 链接: https://pan.baidu.com/s/1z1blrlhPxUcsbRlmGTkAYw 取码: stye AndroNix 这个软件用于提供常见 Linux 发行版本系统的下载,其实用过后发现就是提供系统的下载链接,然后跳转到 Termux 进行下载和安装,搜索下载或使用链接: 链接: https://pan.baidu.com/s/177VOddzfaCXVoHb_duK5bA 提取码: 7k65 安装系统 首先安装 Termux 软件然后打开,会进入以下界面: 它是一个简单的 shell,可以运行一些常用命令,自行探究; 然后安装并打开 AndroNix 软件,主界面如下: 这里还是以安装 Kali 系统为例,那么我们点击右上那个熟悉的 Kali 系统图标,进入以下界面,点击安装按钮: 按照指示的步骤进行,先点击复制,命令会被复制到系统粘贴板: AndroNix 可能部分机型存在不兼容的情况,这里直接贴出这个软件复制到剪切板的内容,下一个步骤粘贴即可: echo “deb https://termux.mentality.rip/termux-main stable main” > $PREFIX/etc/apt/sources.list && cat $PREFIX/etc/apt/sources.list && pkg update -y && pkg install wget curl proot tar -y && wget https://raw.githubusercontent.com/AndronixApp/AndronixOrigin/master/Installer/Kali/kali-xfce.sh -O kali-xfce.sh && chmod +x kali-xfce.sh && bash kali-xfce.sh 然后我们切换到 Termux 这个软件,长按粘贴刚才复制的代码,内容如下: 然后按下回车后进行系统的下载和安装,同样需要一段时间;完成后,会在当前目录下生成几个新文件,如下图: 目录下的 start-kali.sh 文件就是我们安装的 Kali 系统的启动文件,所以我们输入命令 ./start-kali.sh 启动系统: 到这里我们的系统算是安装成功了; 安装图形桌面 目前安装的 Kali 系统只能 shell 进行访问,接下来安装我们熟悉的图形桌面环境,所以我们手机切换回 AndroNix 这个软件,执行第二步,即择桌面系统,和之前一样,我们点击 XFCE: 这里同样是把代码复制到了系统粘贴板,需要切换到 Termux 并粘贴代码;需要 注意 的是,官方文件似乎没有描述清楚,这里是要再启动后的 Kali 系统环境下粘贴代码以安装图形桌面,也就是运行 ./start-kali.sh 后再粘贴运行代码,而不是在 Termux 的默认环境下粘贴运行,如下图: 安装完后会提示设置连接密码,为了之后使用 VNC Viewer 进行连接: 再次输入以确定: 然后会提示是否设置一个 view-only(只读模式)密码,这里我们不设置,输入 n: 然后就配置完成,要访问 Kali 图形桌面就需要先启动 VNC 服务,启动服务的命令为 vncserver-start,停止的命令为 vncserver-stop,如下图; 会发现这两个命令类似 Deploy 中的启动和停止按钮,只不过它是同时启动系统和 vnc 服务罢了;接下来就输入命令启动 Kali 系统的 VNC 服务,会输出以下信息: 到这里服务就启动成功了,接下来就是和之前一样,使用 VNC Viewer 这个软件来连接图形桌面环境,新建一个连接: 这里也是和之前唯一 不同 之处,即 Deploy 默认开启的 vnc 服务端口是 5900,同时 VNC Viewer 的默认端口也是 5900,所以之前只需要输入 127.0.0.1 就行了,但是 vncserver-start 开启服务的端口是 5901 起步,随开启数量而递增,所以在配置地址时需要指定端口,即地址设为:127.0.0.1:5901; 后面的步骤就和之前大致相同了: 连接密码就是之前设置的密码: 来几张特写: 关闭系统 停止使用系统后,先断开连接: 然后回到 Termux 软件,输入 vncserver-stop 命令停止 vnc 服务,输入 exit 注销登录 Kali系统,再按 CTRL + z 返回 Termux 环境(CTRL 在软件底部菜单栏): 到这里非 ROOT 条件下的 Kali 系统安装教程也结束了,想要安装其它发行版本的 linux 系统可以用类似的方法自行摸索,不做赘述;另外,从上面安装 Kali 系统的命令中也能发现,系统镜像并不是官方源,而是为了适应在非 ROOT 手机中运行的“定制版本”,当然常用功能健在,只是某些部分有所限制,可以之后自行体会;

2020/1/10
articleCard.readMore

JavaScript 类型转换的有趣应用

背景 可以访问这个网站提前预览:https://knightyun.github.io/magic-expression/ 先来看一串代码: (!(~+[])+{})[--[~+''][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]]*~+[]] 也许你在其他地方看见过这种黑科技操作,那么不妨猜一下上面的代码的值等于多少,实在猜不到可以复制它粘贴到浏览器 console 中回车看看; 这里剧透一下,会得到下面的结果(手动解释,仅供娱乐并无其他意思): "sb" 当然,你可能还见过这种形式的 "hello world!" 版本的,即最后输出的字符串是 "hello world!",这里由于它的代码过长就不粘贴出来了,原理和上面类似,我们暂且称它为魔法表达式,下面就以上面的例子还分析一下这窜代码的“魔法”; 类型转换 分析之前先来了解一些基础,在 js 的世界中,存在一种类型转换的机制,大致就是字面意思,下面分别举例说明; Number / String console.log(1 + 1); // 2 console.log('1' + '1'); // "11" console.log(1 + '1'); // "11" console.log('1' + 1); // "11" console.log(1 - 1); // 0 console.log('1' - '1'); // 0 console.log(1 - '1'); // 0 console.log('1' - 1); // 0 console.log('a' - 'b'); // NaN console.log('a' - '1'); // NaN console.log('a' - 1); // NaN // *,/,% 等运算符与 - 运算类似 可以看到,上面的行为可能有点怪异,因为在其他语言中可能会报错,但是在 js 中确实是这样执行的,即静默地尽可能地把运算符两边的类型转换一致再运算; Number / Boolean 当然这种转换不限于字符串和数字类型之间,也包括其他类型,比如很经典的一中转换: console.log(1 == true); // true console.log(1 === true); // false 上面就是把数字类型和 Boolean 类型的值进行比较,第一行的输出结果是因为使用 == 操作符时会自行把 1 转换为 true,所以两边相等(可以理解为执行 Boolean(0) 操作);而 === 操作符除了比较两边的值,还会比较两边类型,二者都相同才判断相等,即没有进行类型转换; 其他类型 其他类型的转换情况: console.log(Boolean([])); // true console.log(Boolean('')); // false console.log(Boolean(String(''))); // false console.log(Boolean(new String(''))); // true console.log(Boolean(' ')); // true console.log(Boolean({})); // true console.log(Boolean(0)); // false console.log(Boolean(Number(0))); // false console.log(Boolean(new Number(0))); // true 应用 下面是该机制的一些实际应用: console.log(+'1', typeof +'1'); // 1 "number" console.log(1 + '', typeof (1 + '')); // 1 "number" console.log('' + 1, typeof ('' + 1)); // 1 "string" console.log(+[], typeof +[]); // 0 "number" console.log(-[], typeof -[]); // -0 "number" console.log(+[1], typeof +[1]); // 1 "number" console.log(+[1,2], typeof +[1,2]); // NaN "number" console.log('' + [], typeof ('' + []), ('' + []).length); // '' "string" 0 console.log([] + '', typeof ('' + []), ('' + []).length); // '' "string" 0 console.log(+{}, typeof +{}); // NaN "number" console.log({} + '', typeof ({} + '')); // [object Object] "string" console.log('' + {}, typeof ('' + {})); // [object Object] "string" 另外,”!” 与 ”~” 运算符也算是 js 中较为常见的,其中 ! 是逻辑运算符,代表非,而 ~ 是位运算符,代表按位取反,它们有以下关系: console.log(!true); // false console.log(!false); // true console.log(!!true); // true console.log(~0); // -1 console.log(~3); // -4 console.log(~~3); // 3 分析 现在开始分析最早提到的那串代码,它的神奇之处就在于整个代码在不包括任何一个字母的情况下输出了字母,代码中都是一些运算符和操作符,代码串挨在一起不利于观察,我们先稍微格式化一下: ( !(~+[]) + {} ) [ --[~+''][+[]] * [~+[]] + ~~!+[] ] + ( {} + [] ) [ [~!+[]] * ~+[] ]; 这里只是拆分美化了一下格式,输出结果不变,然后一行一行进行分析,根据拆分结果,其实整个代码就是两大部分相加; 第一部分 第一部分中,第一行是 (!(~+[]) + {}),也是两部分加和,根据前面的基础,可以得到如下分析结果: (!(~+[]) + {}) ↓ (!(~0) + "[object Object]") ↓ (!-1 + "[object Object]") ↓ (false + "[object Object]") ↓ (false + "[object Object]") ↓ ("false[object Object]") 第一大部分剩下的内容: [ --[~+''] [+[]] * [~+[]] + ~~!+[] ] 分析结果如下: --[~+''][+[]] * [~+[]] + ~~!+[] ↓ ↓ ↓ --[~0][0] * [~0] + ~~!0 ↓ ↓ ↓ --[-1][0] * [-1] + ~~true ↓ ↓ --(-1) ~~1 ↓ ↓ -2 1 => -2 * [-1] + 1 = -2 * -1 + 1 = 3 所以第一大部分的结果是: ("false[object Object]")[3]; // "s" 第二部分 第二大部分也执行类似的分解,第一行内容是 ({} + []),其实也是在拼接字符串,分析如下: ({} + []) ↓ ("[object Object]" + "") ↓ ("[object Object]") 余下内容是: [ [~!+[]] * ~+[] ]; 分析如下: [~!+[]] * ~+[] ↓ ↓ [~!0] * ~0 ↓ ↓ [~true] * -1 ↓ ↓ [~1] * -1 ↓ ↓ [-2] * -1 ↓ -2 * -1 ↓ 2 所以第二大部分结果就是: ("[object Object]")[2]; // "b" 汇总 最后两个大部分内容字符串拼接就是最终结果,这里汇总一下: ( !(~+[]) + {} ) // "false[object Object]" [ --[~+''] [+[]] * // -2 [~+[]] + // -1 ~~!+[] // 1 ] // => ("false[object Object]")[3] => "s" + ({} + []) // [object Object] [ [~!+[]] * // -2 ~+[] // -1 ]; // => ("[object Object]")[2] => "b" // => "s" + "b" = "sb" 费了这么大功夫就得出两个字母,想必这个过程对于理解 js 中的类型转换机制是很有帮助的; 魔力所在 然后就是回顾之前那个问题,为什么整个代码没有出现字母却在结果中出现了,根据上面的分析可以看出,字符串是通过类似以下方式得到的: console.log([] + {}); // "[object Object]" console.log([] + true); // "true" console.log([] + false); // "false" 然后在 js 中字符串也可以通过类似数组的方式获取某个字符: console.log(("[object Objact]")[2]); // "b" // "b" 在字符串中的索引为 2 那么前面提到的输出 hello world! 的代码,其实也是通过类似的方式获取字符然后拼接而成,只是需要思考从哪些格式化输出中获取想要的那个字符而已; 拓展 顺着上面的思路,如果我们想要输出任意指定字符,该如何实现呢?即字符中可能包含 a-z, A-Z, 0-9 中的任何一个字符,甚至是特殊字符; 可能的输出 前面提到输出的关键是存在这个一个标准格式化输出(如 true, false),然后就能从里面扣取字符了,作者目前能想到的标准输出的字符串有如下(可能疏漏): console.log([]+[]); // "" console.log([]+!![]); // "true" console.log([]+![]); // "false" console.log([]+{}); // "[object Object]" console.log([]+!![]-[]); // "NaN" console.log([]+[][+[]]); // "undefined" console.log([]+~~!![]/+[]) // "Infinity" console.log(([]+~[])[~~[]]); // "-" 即使这样,汇总下来的字母也只有: abcdefiIjlnNoOrstuy- 离目标似乎有点远~~,数字就比较好弄了: console.log(+[]); // 0 console.log(+!![]); // 1 console.log(!![]+!![]); // 2(后续的数字可以这样叠加) console.log(-~+!![]); // 2(也可以换个简短的方法) console.log(!![]+!![]+!![]); // 3 console.log(!![]+!![]+!![]+!![]); // 4 console.log(!![]+!![]+!![]+!![]+!![]); // 5 console.log(!![]+!![]+!![]+!![]+!![]+!![]); // 6 console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 7 console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 8 console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 9 更大的数字就可以通过字符拼接或者四则运算获得; 扩大输出范围 目前还是没有获得大部分大写字母和特殊字符 ~!@#$%^&*()_+-=\|[]{};':",./<>?,甚至是汉字或者是其他国字符,等等,说到这里是不是想起了什么?没错就是Unicode(号称万国符来着),首先 js 中使用 Unicode 的形式是 "\uXXXX",后面的 XXXX 是四个十六进制字符,例如: console.log('\u0061'); // "a" // 获取 a 的字符编码 console.log('a'.charCodeAt()); // 97 // 转换为 16 进制 console.log('a'.charCodeAt().toString(16)); // "61" // 0061 = 61 // 汉字 console.log('黄'.charCodeAt().toString(16)); // "9ec4" console.log('\u9ec4'); // "黄" 所以只要能够表示 \, u, a-f, 0-9 这几个字符,就能表示所有 Unicode 字符了!根据前面的总结,其实我们已经能够表示这几个字符了!不过呢,直接拼接 Unicode 的话会出现下面的问题: console.log('\u0061'); // "a" console.log('\u' + '0061'); // SyntaxError: Invalid Unicode escape sequence console.log('\\u' + '0061'); // "\u0061" eval 所以我们不能通过直接拼接 Unicode 字符串来获得能被解析的 Unicode 符的,因此不得不换个思路;既然不能拼接直接 Unicode,那么有么有间接的方法或者 API 呢? 可能会想到使用 eval(): var s = eval('"' + '\\u' + '0061' + '"'); console.log(s); // "a" 虽然成功了,但是目前我们似乎还无法获得 eval 中的字母 v,所以要继续换个方法; Function 其实还有一个函数可以实现类似的功能,它就是 Function(),即构造函数的函数,也是声明函数的另一种方法,举例: var fn = Function('a', 'b', 'return a + b'); var fn2 = Function('return 4'); console.log(fn(1, 2)); // 3 console.log(fn2()); // 4 然后我们就可以像这样拼接 Unicode 了: console.log(Function('return ' + '"\\u' + '0061' + '"')()); // "a" 另外我们需要知道的是:[]['constructor']['constructor'] === Function,所以最后只要构造出这样的字符串就行了: []['constructor']['constructor']('return '+'"'+'\\u0061'+'"')(); 根据前面的基础,我们已经能够获取 constructor, return 里面的所有字母了,这里再把需要用到的字符全部汇总一下: ([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // " "" ([]+/\\\\/)[+!![]]; // "\" []+(+[]); // "0" []+(+!![]); // "1" []+(!![]+!![]); // "2" []+(!![]+!![]+!![]); // "3" []+(!![]+!![]+!![]+!![]); // "4" []+(!![]+!![]+!![]+!![]+!![]); // "5" []+(!![]+!![]+!![]+!![]+!![]+!![]); // "6" []+(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "7" []+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "8" []+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "9" ([]+![])[+!![]]; // "a" ([]+{})[!![]+!![]]; // "b" ([]+{})[!![]+!![]+!![]+!![]+!![]]; // "c" ([]+[][+[]])[!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // "d" ([]+!![])[!![]+!![]+!![]]; // "e" ([]+![])[+[]]; // "f" ([]+[][+[]])[+!![]]; // "n" ([]+{})[+!![]]; // "o" ([]+!![])[+!![]]; // "r" ([]+![])[!![]+!![]+!![]]; // "s" ([]+!![])[+[]]; // "t" ([]+!![])[!![]+!![]; // "u" 结果拼接 最后剩下的就是把想要的输出,转换为 Unicode,再拆分为单个字符对应上面的表达式进行拼接就行了,我们来试一下效果(拿走不谢 :)): var s1 = [][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+!![])+'"')(); console.log(s1); // "I love you!" var s2 = [][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(!![]+!![]+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![])+([]+![])[+[]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(+[])+'"')(); console.log(s2); // "我爱你" 在线转换 这里放一个在线转换的网站: 点我在线转换

2019/10/7
articleCard.readMore

JavaScript 求最小公倍数

最小公倍数(Least Common Multiple) 最小公倍数是中学数学知识中的一个概念,具体定义可以 自行了解,这里只大致解释一下,通常几个正整数会存在许多个倍数,每个倍数除以这几个正整数后都没有余数,而这些倍数之中最小的一个则称为最小公倍数; 一般直接计算最小公倍数较为困难,因此需要用到一个计算公式,即两个数的乘积等于这两个数的最大公约数与最小公倍数的乘积,所以知道两个数的最大公约数或者求最小公倍数,就可以求得另外一个,接下来先来实现最大公约数的求法; 最大公约数(Greatest Common Divisor) 这也是与最小公倍数相似的另一个概念,几个正整数之间一般存在多个约数,即这几个正整数除以这个约数后都没有余数,这些约数中最大的一个称为最大公约数;下面来实现求最大公约数的函数; 最大质因数法 思路很简单,即将两个数分别递减,获取能同时被二者除尽的最大的一个数,即最大公约数: function getGcd(a, b) { for (let i = a; i > 0; i--) { for (let j = b; j > 0; j--) { if (a % i === 0 && b % j === 0 && i === j) { return j; } } } } 辗转相除法 思路是用两个数中的最大项除以最小项,如果能除尽,那么最小项便是这两个数的最大公约数;不能除尽则用最大项除以最小项所得余数,与最小项再进行同样的递归操作,最后得到最大的约数,也就是所谓的辗转相除; function getGcd(a, b) { let max = Math.max(a, b); let min = Math.min(a, b); if (max % min === 0) { return min; } else { return getGcd(max % min, min); } } 求最小公倍数 有了求最大公约数的函数后,再来求最小公倍数就简单了: function getLcm(a, b) { return a * b / getGcd(a, b); }

2019/9/3
articleCard.readMore

JavaScript 中 try, catch, throw 的用法

程序在运行中难免遇到 bug,所以就需要好的调试手段找出问题所在,try, catch, throw 便是 JavaScript 中用来调试并对错误执行相关操作的工具,下面具体介绍其用法; try, catch 基本语法结构: try { // ... // 这里写需要调试的代码段 } catch(error) { // ... // 这里写要对获取的错误信息执行的操作 } 举例: try { // 这里故意写错函数名,为了抛出错误 console.logg('This is an error and will not display'); } catch (e) { console.log(e); // TypeError: console.logg is not a function console.log(e.message); // console.logg is not a function console.log(e.name); // TypeError console.log(e.stack); // TypeError: console.logg is not a function } 上面的错误代码如果直接在正常环境中执行,便会直接在后台输出错误: TypeError: console.loggg is not a function 但是使用 try, catch 结构的话,就可以获取一个包含错误信息的对象,其包含各个部分的错误信息,便于进行一些自定义操作; throw throw 是在上述结构中使用的一个函数,接受一个参数作为输出信息,throw 的作用是中断后面所有语句的执行,包括错误源,但是它前面的语句都会正常执行,它可以用于判断错误的具体位置,例如: try { console.log('This will display.'); throw('My error position.'); // throw 将会中断语句的执行 // 同样故意制造错误 console.logg('This is an error and will not display.'); // 后面是正常语句 console.log('This will not display, either.') } catch (e) { console.log(e); } // This will display. // My error position. 如果错误发生在 throw 语句之前的话,错误便会被正常抛出,而 throw 传递的信息不会被输出,例如: try { console.logg('This is an error and wil not display.'); throw('My error position.'); // 后面的执行同样会被中断 console.log('This will not display, either.') } catch(e) { console.log(e); } // TypeError: console.logg is not a function. 因此,在调试过程中可以结合上面两种情况,一步步找出错误的具体位置;

2019/9/2
articleCard.readMore

JavaScript 变异与非变异数组方法

在 JavaScript 中,存在大量对数组进行操作的方法,它们都有一个特点,就是关于此操作是否会修改原数组,并以此将数组方法分为变异数组方法与非变异数组方法,例如 .pop() 方法便是删除数组的最后一个元素,而 .slice() 方法则是返回一个指定特征的新数组,并不会对原数组进行修改;能有效的区分这两类方法,有助于开发过程中方法选用,避免出现不必要的错误; 下面对一些常见的数组方法进行了整理分类: 非变异数组方法 变异数组方法 .join() .push() .concat() .pop() .slice() .shift() .from() .reverse() .map() .sort() .filter() .splice() .every() .fill() .find()   .findIndex()   .flat()   .flatMap()   .forEach()   .includes()   .indexOf()   .lastIndexOf()   .reduce()   .reduceRight()   .some()   .toString()   .toLocalString()   可以看出改变原数组的变异数组方法只有少数几个,非变异的过多可能记不住,因此可以选择记少不记多的原则,记住那几个特殊的变异方法的,其余的便可归类于非变异的了;

2019/9/2
articleCard.readMore

JavaScript 变量提升(Hoisting)详解

概念 变量提升是 JavaScript 的一种执行机制,大致就是字面意思,将声明的变量提前,但并不是指在编译时改变语句的顺序,而是将变量提前放入内存中,供后续操作,下面通过实例进行分析; 函数申明 在 JavaScript 中,声明一个函数并执行的话,通常会是以下形式: function fn() { console.log('run'); } fn(); // run 上面是正常的思维顺序,但是包括其他一些编程语言在内,通常会使用如下形式: fn(); function fn() { console.log('run'); } // run 这样做在执行上是没用问题的,同时可以在包含大量语句和函数申明的情况下,也可以使用这种特性将普通语句和函数申明分开,提高可读性; 以上情况便是一种常见的提升(Hoisting),即编译时提前将当前执行上下文包含的申明的函数,提前放入内存中,供全文语句执行时调用,为了方便理解而抽象成一种提升行为; 但是如果使用下面的方式申明函数并执行: fn(); var fn = function() { console.log('run'); } // TypeError: fn is not a function 这里就没有像上面一样的结果了,这属于下面将介绍的变量提升行为; 变量申明 当然函数只是一种类型的变量,还存在其他的变量类型,例如考虑以下语句: var a = 1; console.log(a); // 1 逻辑和执行都是正常的,输出结果也是预期的,但是如果变一下顺序: console.log(a); var a = 1; 这种情况,通常可能会认为第一行调用了一个未定义的变量,然后输出 Uncaught ReferenceError: a is not defined 这样的错误,但是呢,并非如此,输出信息如下: undefined 没错,就只有一个单独的 undefined,这种输出情况就类似于以下代码的执行: var a; console.log(a); // undefined 从这里便可以大致分析出,前面的顺序怪异的代码,相当于在编译时提前将后面出现的变量申明提前,然后执行就输出了一个已申明但未 初始化(赋值) 的值,这便是其他类型的变量的提升行为,即在当前执行上下文中,将后面申明的变量提前放入内存,供前面的语句调用; 注意,前面的代码最后一行的语句是 var a = 1,即对变量进行了申明并赋值,但是最后输出仍然是 undefined 而不是 1,证明变量提升行为只会对变量进行申明操作,并不会对其初始化赋值,不管原语句是否有赋值操作; 然后便能解释之前的代码: fn(); var fn = function() { console.log('run'); } // TypeError: fn is not a function 这种情况便是将变量 fn 提升,值为 undefined,所以执行 fn() 语句会提示 fn is not a function 而不是 fn is not defined,与使用关键字 function 申明函数情况不一样; 拓展 return 的限制 另外值得一提的是,我们都知道 return 是函数内代码执行结束的标志,其后代码不会执行,但是提升行为却不受此限制,例如: function fn() { console.log(a); fnn(); return ; var a = 1; function fnn() { console.log('exist.') } } fn(); // undefined // exist. 提升优先级 上面提到两种提升行为,那么它们的优先级顺序是如何的呢?还是通过代码说明: function fn1() { console.log(a); var a = 1; function a(){}; } function fn2() { console.log(a); function a(){}; var a = 1; } fn1(); // f a() {} fn2(); // f a() {} 结果证明函数的提升优先级始终高于普通变量的提升; 提升的执行 再来看一种情况: function fn() { fnn(); var a = 1; function fnn() { console.log(a); } } fn(); // undefined 这里按照正常的逻辑,申明函数 fnn() 之前就已经申明了变量 a,所以会感觉函数 fnn 应该可以访问变量 a,但是最后输出的并不是 1,输出 undefined 说明函数 fnn 并没有访问到赋值后的 a,并且所访问的 a 也触发了提升机制,因为输出的不是 RefferenceError,那么就能大致梳理出提升真 正的执行顺序了: 执行 fn(); 执行 fnn(); 发现前面没有关于函数 fnn() 的申明,于是向后寻找,最后找到了; 执行 console.log(a); 发现 fnn 内部没有 a 的定义,向外一层寻找; a 申明在 fnn() 执行语句以后,所以 a 触发提升,供之前的 console.log 使用; 因此上面的代码相当于是以下面的顺序执行的: function fn() { console.log(a); var a = 1; } fn(); var, let, const的区别 JavaScript 中申明变量的方式以及对应效果如下: a = 0; // 全局变量 var b = 1; // 局部作用域变量(当前上下文) let c = 2; // 块级作用域变量(当前块级上下文) const d = 3; // 常量 作用域 这里解释一下,变量 a 申明时没有带任何关键字,默认其为全局变量;变量 b 申明带有关键字 var,为当前上下文的局部作用域,如果用在全局则为全局变量;变量 c 使用关键字关键字 let,d 使用关键字 const,二者都是ES6中新增的块级作用域申明,只不过 const 申明的是常量,值不可更改; 通过例子看一下它们的区别; var a = '全局'; function fn() { var aa = '局部' console.log(aa); } if (true) { var b = '全局'; let bb = '块级'; const bbb = '块级'; } for (i = 0; i < 1; i++) { var c = '全局' let cc = '块级'; const ccc = '块级'; } console.log(a); // “全局” fn(); // “局部” console.log(aa); // aa is not defined console.log(b, c); // “全局” “全局” console.log(bb, cc); // bb is not defined cc is not defined console.log(bbb, ccc); // bbb is not defined ccc is not defined 可以看出,var 的局部限于全局或者函数内部上下文,而 let 和 const 的块级的意思则是被 块(block) 所包含的上下文,也就是包含在花括号 {} 内部的作用域中,所以也包括函数在内,加上 if, for, while, switch 等情况,且不能被外部作用域访问; 全局变量提升 首先看申明全局变量时的提升行为: console.log(a); a = 0; // ReferenceError: a is not defined 证明不带关键字的申明全局变量,似乎并没有执行变量的提升行为,与以下代码的执行无异: console.log(a); // a 前面未申明 // ReferenceError: a is not defined 局部变量提升 使用关键字 var 申明的情况: function fn() { console.log(aa); var aa = 1; } console.log(a); var a = 1; // undefined fn(); // undefined 前面已解释,不再赘述,只是需要注意下面这种情况: if (false) { var a = 1; } console.log(a); // undefined 正常思维可能会理解 if 条件判断为假所以不会执行内部语句,最后会输出 a is not defined,然而并非如此,仍然将申明的变量执行了提升机制;这里可以简单理解为存在即提升,也就是为了避免以上问题的影响,所以出现了块级变量申明 let 与 const; 块级变量提升 使用 let 与 const 的情况: if (true) { console.log(a); let a = 1; console.log(aa); } // ReferenceError: Cannot access 'a' before initialization // ReferenceError: aa is not defined if (true) { console.log(b); const b = 1; console.log(bb); } // ReferenceError: Cannot access 'b' before initialization // ReferenceError: bb is not defined 可以看出,块级变量申明似乎也执行了类似提升的机制,但是处理却与 var 有区别,这里是直接以错误的形式处理输出,提示该变量未进行初始化,而没有变量的申明语句的情况,则是提示未定义的错误,且 let 与 const 的处理情况一致;

2019/9/2
articleCard.readMore

JavaScript 实现斐波那契数列(Febonacci Array)

斐波那契(Febonacci)数列是一个神奇的数列,在很多地方都有应用,可以自行搜索相关图片体会其魅力,这里不赘述,直接来分析一下如何通过 JavaScript 来实现; 概念 斐波那契数列形式如下: 1 1 2 3 5 8 13 21 34... 规律应该很容易看出来,即从第三项开始,每一项的值等于前两项之和,以此类推下去,至于第一项和第二项的值嘛,不要纠结,就是这样规定的… 斐波那契数 首先来实现一下获取指定项的斐波那契数,即获取该数组中第 n 项的值; 方法一: function getFebNum(n) { if (n == 1 || n == 2) { return 1; } else { return getFebNum(n - 1) + getFebNum(n - 2); } } 方法一使用递归的思路,便于理解代码量也少,但是其算法复杂度较大,当 n 相当大的时候,程序运行也无比复杂; 方法二: function _getFebNum(n) { if (n < 1) return 0; let one = 1, // 初始为第 -2 项 two = 0, // 初始为第 -1 项 three = 0; // 初始为第 1 项 for (let i = 1; i <= n; i++) { three = one + two; one = two; two = three; } return three; } 这种方法算法复杂度就比较小了,只是换了个获取思路,代码量增加也不太容易理解,其中为了缩减代码量,便于递推的进行,把斐波那契数列向后模拟扩展了两项: 项 -2 -1 1 2 3 4 5 … 值 1 0 1 1 2 3 5 … 斐波那契数列 接下来通过代码实现获取指定长度的斐波那契数列: 方法一: function getFebArr(n) { let arr = []; for (let i = 1; i <= n; i++) { arr.push(getFebNum(i)); } return arr; } 这个方法通过挨个获取斐波那契数,最后组成一个斐波那契数列,需要用到前面的 getFebNum 函数; 方法二: function _getFebArr(n) { let arr = []; if (n < 1) return arr; let one = 1, two = 0, three = 0; for (let i = 1; i <= n; i++) { three = one + two; arr.push(three); one = two; two = three; } return arr; } 方法二利用之前 _getFebNum 方法的思路,递推地填充斐波那契数列,降低了算法复杂度;

2019/9/2
articleCard.readMore

JavaScript 稀疏数组

稀疏数组 概念 在一些后端语言中,如 C,数组内的值通常被分配在一系列连续的内存地址上,但是在 js 中,某些数组内存则不是连续的,所谓稀疏,顾名思义,不连续,存在一些空隙; 例如: var arr = new Array(3); console.log(arr); // (3) [empty × 3] 通过以上方法创建数组,其中 Array(3) 中的参数 3 表示数组的长度,这个数组就是稀疏的,控制台输出一般带有 empty 字样,或者像下面这样创建数组: var arr = [1,,2]; console.log(arr); // (3) [1, empty, 2] 因为定义语句中两个逗号之间无字符,没有定义值,同样带有 empty 字样,代表稀疏数组,这里可以把 empty 理解为上面讲到的 空隙; 特点 接下来看一下稀疏数组特殊在什么地方,举个例子说明: var arr1 = [1, 2, 3]; // 正常数组 var arr2 = new Array(3); // 稀疏数组 var arr3 = [1, , 3]; // 稀疏数组 console.log(arr1.length, arr2.length, arr3.length); // 3 3 3 console.log(arr2[0], arr3[1]); // undefined undefined for (var i = 0; i < 3; i++) { console.log(arr1[i], arr2[i], arr3[i]); } // 1 undefined 1 // 2 undefined undefined // 3 undefined 3 arr1.forEach(function(x){ console.log(x); }); // 1 // 2 // 3 arr2.forEach(function(x){ console.log(x); }); // (无输出) arr3.forEach(function(x){ console.log(x); }); // 1 // 3 console.log(0 in arr3, 1 in arr3); // true false 总结一下,创建的稀疏数组,其长度(length)与定义长度值一致;空隙 值可以被单独访问到,并且不是之前出现的 empty 字样,而是 undefined,比如例子中出现 undefined 时都是使用 arr[i] 这样的索引直接访问方式;使用某些数组方法如 forEach() 时,会忽略掉空隙值,只处理正常值,所以也会使得 1 in arr3 值为 false,即数组中不存在该索引; 细想一下,js 这样处理的原因多半是去除不必要的性能开销,当数组相当大时,可以避免处理一些未初始化的值,但这样也同时使得开发中会出现一些问题,所以应尽量避免; 举个例子来查看一下性能如何: console.time('one'); // 密集数组 Array(...Array(1e5)).forEach(function(){ ; }); console.timeEnd('one'); console.time('two'); // 稀疏数组 Array(1e5).forEach(function(){ ; }); console.timeEnd('two'); // one: 26.3759765625ms // two: 5.701171875ms 可以看出在处理较大数组时,稀疏数组确实能降低不少性能开销; 密集数组 概念 与稀疏相对应,则存在密集,定义也就是元素中不存在 空隙 值,其实密集数组基本就是平时常见的正常数组; 例如: var arr1 = [1, 2, 3]; var arr2 = new Array(1, 2, 3); arr2.forEach(function(x){ console.log(x); }); // 1 // 2 // 3 以上都是一些定义密集数组的方法,并且数组中的值都能被正常访问或遍历处理; 区别 运用时需要注意以下情况: var arr1 = [undefined, undefined, undefined]; var arr2 = new Array(3); console.log(arr1[0], arr2[0]); // undefined undefined arr1.forEach(function(x){ console.log(x); }) // undefined // undefined // undefined arr2.forEach(function(x){ console.log(x); }) // (无输出) 即显式的声明值为 undefined 并不代表这个值就是之前提到的空隙值,虽然二者通过索引访问时的值都返回 undefined,但是其根本还是有区别的,显式声明过的是可以被遍历等操作访问的,不会被当成空隙值被忽略; 拓展 通常在很多情况下,我们想要直接声明一个数组并赋予其一些特定的初始值,并且为了避免问题,通常是希望申明为密集数组的,下面就介绍一些常用的方法或技巧: var arr1 = new Array(3).fill(1); console.log(arr1); // [1, 1, 1] var arr2 = Array.fill().map((x, i) => i); console.log(arr2); // [0, 1, 2] var arr3 = Array.apply(null, Array(3)); console.log(arr3); // [undefined, undefined, undefined] // 这样声明的是密集数组,不是稀疏的 var arr4 = new Array(4).join('a').split(''); console.log(arr4); // ['a', 'a', 'a'] // 注意定义数组长度比输出数组大 1 其它更多的方法可以自行类推;

2019/8/2
articleCard.readMore

async 与 await 的用法详解

async 概念 用于声明异步函数,返回值为一个 Promise 对象,它以类似 同步 的方式来写异步方法,语法与声明函数类似,例如: async function fn() { console.log('Hello world!'); } console.log(fn().constructor); // Promise() // 这里证明其返回值为一个 Promise 对象; 返回值 也许这里会有疑问,返回值是 Promise 对象,那么函数本身定义的返回值跑到哪里去了呢?其实,熟悉 Promise 的就知道其异步结果是通过 .then() 或者 .catch() 方法来获取并进行进一步处理的,这样一个道理,定义的异步函数中的返回值会当成 resolve 状态来处理,一般用 .then() 方法处理,而如果定义的异步函数抛出错误,例如变量未定义,则会被当做 reject 状态来处理,一般使用 .catch() 方法来处理; 举例: // 使用 .then() 的情况 async function fn1() { return 'Hello world!'; } fn1().then(function(res) { console.log(res); }); // Hello world! // 使用 .catch() 的情况 async function fn2() { console.log(aaa); // 这里的变量 aaa 未定义,为了制造错误 } fn2().catch(function(error) { console.log(error); }); // ReferenceError: aaa is not defined 假如是既有返回值,又有错误的话,来看看结果如何: async function fn3(){ console.log(aaa); // aaa 依然未定义; return 'Hello world!'; } fn3().then(function(res){ console.log(res); }).catch(function(error){ console.log(error); }); // ReferenceError: aaa is not defined 结果证明只会执行 reject 状态的情况下的语句,忽略了 resolve 时的代码,所以此处值得 注意; await 概念 用法顾名思义,有 等待 的意思,语法为: var value = await myPromise(); 所谓 等待 其实就是指暂停当前 async function 内部语句的执行,等待后面的 myPromise() 处理完返回结果后,继续执行 async function 函数内部的剩余语句;myPromise() 是一个 Promise对象,而自定义的变量 value 则用于获取 Promise 对象返回的 resolve 状态值; 用法 值得 注意 的是,await 必须在 async function 内使用,否则会提示语法错误;如果 await 后面跟的是其他值,则直接返回该值: async function fn() { console.log(1); var result = await new Promise(function(resolve, reject) { setTimeout(function(){ resolve(2); }, 2000); }); console.log(result); console.log(3); console.log(await 4); // 4 会被直接返回 } fn(); // 1 // 2 (2 秒后输出) // 3 // 4 如果不用获取返回值,也可以直接执行语句: async function fn() { console.log(1); await new Promise(function(resolve, reject) { setTimeout(function() { console.log(2); resolve(0); }, 2000); }); console.log(3); } fn(); // 1 // 2 (2 秒后) // 3 返回结果 如之前所说,await 会等到后面的 Promise 返回结果 后才会执行 async 函数后面剩下的语句,也就是说如果 Promise 不返回结果(如 resolve 或 reject),后面的代码就不会执行,例如: async function fn() { console.log(1); await new Promise(function(resolve, reject) { setTimeout(function() { console.log(2); }, 2000); }); console.log(3); } fn(); // 1 // 2 (2 秒后输出,并且后面不会继续输出 3) 这里可以理解为函数会一直等待 await 返回结果(resolve / reject)才会执行剩下的语句,没有返回结果就会一直等下去,也就一直等不到剩下的语句执行了(还挺痴情-_-); 如果 await 后面的 Promise 返回一个 reject 状态的结果的话,则会被当成错误在后台抛出,例如: async function fn() { console.log(1); var result = await new Promise(function(resolve, reject) { setTimeout(function() { reject(2); }, 2000); }); console.log(3); } fn(); // 1 // Uncaught (in promise) 2 (2 秒后输出) 如上,2 秒后会抛出出错误,并且 3 这个数并没有被输出,说明后面的执行也被忽略了; 匿名函数 async 也可以用于申明匿名函数用于不同场景,或者嵌套使用 async 函数,如 await async 的形式,只是要在 await 后面使用 async 形式的函数的话,需要这个函数立即执行且有返回值; let fn = async function() { let a = await (async function() { console.log(1); return 2; })(); console.log(a); async function fn2() { return 3; } console.log(await fn2()); } fn(); // 1 // 2 // 3 另外,await 后面的 Promise 返回的 reject, 也可以被该 async 函数返回的 Promise 对象以 reject 状态获取,例如: async function fn() { console.log(1); var result = await new Promise(function(resolve, reject) { setTimeout(function() { reject(2); }, 2000); }); console.log(3); } fn().catch(function(error) { console.log(error); }); // 1 // 2 (2 秒后输出) 这种情况就不会以错误抛出,直接对异常值进行了处理,并且最后同样没有输出数字 3,即后面的代码依然被忽略了; 注意事项 非 await 部分 async/await 函数以同步的方式书写异步函数确实方便了不少场景,如定义所讲,函数内部遇到 await 会等到返回结果再继续执行下去,也就是说,非 await 部分仍然会以正常的异步或同步方式执行,例如遇到 setTimeout() 就会放入任务队列等待同步语句执行完后再执行; 比如以下情况: async function fn() { console.log(0); await new Promise(resolve => { setTimeout(() => { console.log(1); resolve(); }, 1000); }); setTimeout(() => { console.log(2); }, 0); console.log(3); } fn(); // 0 // 1(2 秒后) // 3 // 2 await 内部 虽然说函数会等待 await 返回结果在继续执行,但是 await 内部的代码也依然按正常的同步和异步执行,例如: async function fn() { console.log(0); setTimeout(() => { console.log(1); }, 0); await new Promise(resolve => { setTimeout(() => { console.log(2); }, 0); console.log(3); setTimeout(() => { console.log(4); resolve(); }, 1000); setTimeout(() => { console.log(5); }, 0); }); setTimeout(() => { console.log(6); }, 0); console.log(7); } fn(); // 0 // 3 // 1 // 2 // 5 // 4(2 秒后) // 7 // 6 上面的代码中返回结果的函数 resolve() 是在 setTimeout() 这个 异步任务 中,所以其被丢到事件队列中等待 2 秒再执行,由于此时 await 还未返回结果,所以还不会去执行 await 以外的代码(输出 7、6),而是先执行同为异步任务、但延时较短的输出 1、2、5 的代码;2 秒后结果返回了,就会继续正常执行 await 以外的同步任务和异步任务了; 但是假如 await 代码内返回结果的函数(resolve() 或 reject())是在 同步任务 中执行的话,情况就有些不一样了,例如: async function fn() { console.log(0); setTimeout(() => { console.log(1); }, 0); await new Promise(resolve => { setTimeout(() => { console.log(2); }, 0); console.log(3); resolve(); console.log(4); setTimeout(() => { console.log(5); }, 0); }); setTimeout(() => { console.log(6); }, 0); console.log(7); } fn(); // 0 // 3 // 4 // 7 // 1 // 2 // 5 // 6 由于同步任务 先于 异步任务执行的机理,在同步任务执行过程中依次输出了 0、3 后,就立即执行了 resolve() 使得 await 得到了返回结果,再往后就继续同步的输出了 4,但是输出 5 的代码是异步任务,与输出 1、2 的代码一并放入任务队列,此时由于 await 返回了结果,所以可以执行 await 以外的代码了,输出 6 是异步任务,于是先输出了同步任务的 7,同步任务都执行完了,最后执行任务队列中的异步任务,按之前进入队列的顺序,就是依次输出 1、2、5、6,所有代码运行结束; 函数嵌套 当 async 函数中嵌套着其他 async 函数时,执行过程可能又有些和预想的不一样,先来看下面的例子: async function fn() { console.log(0); setTimeout(() => { console.log(1); }, 0); (async function() { console.log(2); setTimeout(() => { console.log(3); }, 0); await new Promise(res => setTimeout(res, 1000)) setTimeout(() => { console.log(4); }, 1000); console.log(5); })() console.log(6) } fn(); // 0 // 2 // 6 // 1 // 3 // 5(1 秒后) // 4(再等 1 秒后) 也许会疑惑,不是说 async 函数会等到 await 返回结果后再继续执行吗,为何就先输出 6 了?其实不要混淆概念,确实 async 函数内部是这样干的(3 后 1秒输出 5、4),但 async 函数它自身执行时依然是正常的同步任务执行,也就是虽然内部的 async 函数会等待其 await 返回结果才继续执行后面的代码,但外部的 async 函数可不会等待内部的那个 await,会照常执行(你不是我的菜,天涯何处无芳草╮(╯▽╰)╭); 如果确实需要等待这个嵌套的 async 函数执行完再执行剩下的代码,那么前面加个 await 就行了,原理是也是可行的,因为 async 函数就是返回的一个 Promise 函数,代码如下: async function fn() { console.log(0); setTimeout(() => { console.log(1); }, 0); await (async function() { console.log(2); setTimeout(() => { console.log(3); }, 0); await new Promise(res => setTimeout(res, 1000)) setTimeout(() => { console.log(4); }, 1000); console.log(5); })() console.log(6) } fn(); // 0 // 2 // 1 // 3 // 5(1 秒后) // 6 // 4(再等 1 秒后) 这里也要 注意,假如嵌套的 async 函数中的 await 不返回结果,并且没有在嵌套的 async 函数前面添加 await,那么外部的 async 函数内部剩余的代码也不会执行;

2019/8/2
articleCard.readMore

Linux 中 sudo 免密码

平时在 Linux 中执行一些命令时,可能会遇到 Permission denied 这样的提示,即该用户没有权限; 所以一般会想到在命令最前面加上 sudo 后再执行,然后有可能会提示输入当前用户的密码; 再接下来,如果命令没有正常执行,一般又会提示:user is not in the sudoers file. This incident will be reported.,即当前用户没有出现在这个叫 sudoers 的文件里面,那么这个文件在哪里呢? 一般在这个位置: /etc/sudoers 所以只需要把当前用户添加到这个文件就行了,执行 su 后根据提示输入 root 账户密码,切换到 root 用户,然后用 vi /etc/sudoers 或者 visudo 命令编辑该文件; 在里面可以找到这一行: root ALL=(ALL:ALL) ALL 大致意思是 root 用户具有所有权限,所以可以在下面加入这么一行: user ALL=(ALL:ALL) NOPASSWD:ALL 其中 user 是当前登录的用户名,这样以后使用该账户执行 sudo 就不用输入密码了; 如果存在多个账户,要让这些账户执行 sudo 时都不用输入密码的话,可以添加下面这行: %sudo ALL=(ALL:ALL) NOPASSWD:ALL 保存退出就 OK 了。

2019/6/20
articleCard.readMore

JavaScript 事件循环

运行时(runtime) 一个 JavaScript 运行时包含 栈(stack), 堆(heap), 队列(queue); 栈 (stack) 栈 具有 先进后出 (FILO, First In Last Out) 的特点,有时也叫做 堆栈,可以理解为一个开口向上的容器,先进入的物体压瓶底,后进入的物体一层层向上堆叠,最后取出时,也是一个个拿出来,先拿出最后放进去的,也就是在最上面那个,最后拿出的就是之前第一个放入瓶底的物体;其中容器里的每一个物体叫做 栈帧,理解为动画的每一帧,即最小单元; 动画描述: JavaScript 执行时,每一个调用函数执行时会被压入栈中,称为 压栈,这个函数执行完毕后从栈中弹出,称为 弹栈;即某个物体放入容器一定时间后,再从容器里面取出来,方便为下一次放入物体腾出空间; 例如: function fn1() { console.log('Message 1.'); } console.log('Message 0.'); fn1(); // Message 0. // Message 1. 如果一个函数执行时还会调用第二个函数,那么第一个函数压入栈底后,随后第二个函数便会压在第一个上面,如果还存在第三个、第四个等等,便以此类推向上堆叠,直到最后调用的一个函数执行完之后,在从后往前一次弹出每一个函数; 可以理解为容器放入第一个物体后,本来应该随后取出的,但是这个物体又牵连了第二个物体,所以又继续放入第二个,甚至第三个、第四个等等; 例如: function fn1() { console.log('Message 1.'); } function fn2() { fn1(); console.log('Message 2.'); } function fn3() { fn2(); console.log('Message 3.'); } console.log('Message 0.'); fn3(); // Message 0. // Message 1. // Message 2. // Message 3. // 可以慢慢看几遍捋一下顺序 演示动画: 到这里可能就有问题了,函数能无限调用下去?能无限向栈中压入物体?当然,这个容器是有限制的,例如,在电脑浏览器控制台输入以下代码: (function fn(){fn()})() 其实就是一个递归函数,不断调用自己,并且一直执行下去,那么不出意外,会弹出如下错误提示: 大致意思就是说执行栈发生了溢出,就是不断调用的函数太多了,超过了栈的规定大小; 也可以尝试输入以下代码,看一下使用的浏览器的栈的尺寸: var i = 0; (function fn() { console.log(++i); fn(); })() 回车之后,在浏览器没有卡死的情况下 -_-,n 分钟之后,应该会出现以下错误提示: 最后一个出现的数字应该就是极限了,这里使用的是 Chrome 浏览器,可以看出还是比较大的; 堆 (heap) 堆 在运行期间被用来动态分配内存,比如给变量、对象、数组、字符串等分配特定的内存地址,用以访问,不像栈和队列,它是一个非结构化的区域; 队列 (queue) 队列 具有 先进先出 (FIFO, First In First Out) 的特点,这里就理解为排队取餐的一队人,先到先得,然后从前面先走,后来的排在最后,并且不允许插队; 在 JavaScript 运行时中,队列的结构被应用到了 消息队列 中;前面说到代码执行时,调用函数执行时被压入执行栈 (call stack) 中,并且需要等待该函数彻底执行完后,才能弹出栈,但是假如遇到 setTimeout 这样延时事件,由于 JavaScript 引擎的 单线程 特点,区别于其他语言,因此执行是不会因为延时函数而中断的,此时便会将 setTimeout 延时调用的函数放入 消息队列 中(延时短的早调用,延时长的晚调用,与书写顺序无关),等待当前环境所有压栈、弹栈操作执行完毕,再按照顺序执行队列中的调用函数; 例如: function fn1() { console.log('Message 1.'); } function fn2() { fn1(); setTimeout(function delay1(){ console.log('Message 2.'); }, 0) } function fn3() { fn2(); setTimeout(function delay2(){ console.log('Message 2.5.') }, 1000) console.log('Message 3.') } console.log('Message 0.') fn3(); // Message 0. // Message 1. // Message 3. // Message 2. // Message 2.5. (大约 1 秒后) 动画演示: 这里的结果就明显与之前的例子不同了,根据上面的描述,顺序为: 输出 Message 0.; fn3() 压入栈底; 然后压入 fn2(); 最后压入 fn1(); fn1() 内的语句执行完后,输出 Message 1.; 执行函数 fn2() 的语句; 由于 fn2() 内的 setTimeout() 函数是一个延时函数,所以其调用函数 delay1() 就被放到了消息队列中; 然后执行 fn3() 中的 setTiemout(),其调用函数 delay2() 也被放入了队列中; 由于 delay1() 的延时小于 delay2(),所以 delay2() 被放到了 delay() 的后面,反之颠倒顺序; 输出 fn3() 中的 Message 3.; 此时开始执行消息队列的函数; 先执行 delay1() 输出 Message 2.; 然后执行 delay2() 输出 Message 2.5; 注意,即使 delay1() 的延时为 0,也并不意味着该回调函数会在 0 毫秒后执行,即不会立即执行,由于机制原因,同样会被放入消息队列中,只不过会 比较早执行 而已; 事件循环 (Event Loop) 所谓事件循环,大致就是上诉过程;这里的 事件 指的就是消息队列中的消息,即队列中的调用函数;循环 即不断执行完队列中的消息,并等待是否有新消息到达,进而将其执行的这一循环过程; 宏任务/微任务 由于 JavaScript 的执行是单线程的(浏览器是多线程的),所以为了避免代码执行时遇到一些耗时的操作阻塞后续操作,就有了同步任务和异步任务之分;在 js 执行期间,遇到同步任务就执行入栈操作,遇到异步任务就放入队列中,栈中的同步任务执行完后再执行队列中的异步任务,也就是上面说的事件循环,这样就避免了耗时任务对栈中主线程的阻塞,一般大多数函数和变量声明都是同步任务,异步任务占少数如 setTimeout(), setInterval(), Promise() 等;其中,异步任务又可分为宏任务和微任务; 宏任务常见的有 setTimeout(), setInterval(),微任务常见的便是 Promise() 和 process.nextTick()(nodejs 中使用);之前只提到消息队列,这里为了便于理解可以把宏任务和微任务看成两个分开的队列:宏任务队列与微任务队列; 我们通过以下代码看一下同步任务、宏任务、微任务的执行顺序: console.log('Sync task.'); setTimeout(() => { console.log('Macro task.'); }, 0); Promise.resolve('Micro task.').then(res => { console.log(res); }) console.log('End.'); // Sync task. // End. // Micro task. // Macro task. 接下来我们把宏任务和微任务的顺序换一下,再看一下结果: console.log('Sync task.'); Promise.resolve('Micro task.').then(res => { console.log(res); }) setTimeout(() => { console.log('Macro task.'); }, 0); console.log('End.'); // Sync task. // End. // Micro task. // Macro task. 综合结果证明,任务的执行顺序是: 同步任务 --> 微任务 --> 宏任务 再考虑下面这个较有代表性的稍微复杂点的嵌套情况(答案在代码最右侧,先思考再检查): console.log(0); setTimeout(() => { console.log(1); }, 1000); setTimeout(() => { console.log(2); }, 0); new Promise(resolve => { console.log(3); setTimeout(() => { console.log(4); }, 500); resolve(5); }).then(res => { setTimeout(() => { console.log(6); }, 0) console.log(res); }) // 0 // 3 setTimeout(() => { // 9 console.log(7); // 5 }, 200) // 2 setTimeout(() => { // 8 console.log(8); // 6 }, 0) // 7 // 4 console.log(9); // 1 查看答案后看看结果和预想的是否一致;可能唯一有疑惑的地方就是 3 和 9 的输出顺序,因为前面不是讲同步任务会先于 Promise 微任务执行吗?应该是 9 在 3 前面才对呀?其实不然,并不是在 Promise 函数代码内的都是微任务,因为所谓 微任务 就是一项任务,即指令下达后要做的事情,那么在函数中 要做的事 其实是 .then() 中包裹的代码,而 Promise 函数中包裹的代码(输出 3、4、5)算作 同步任务 的一部分,所以 3 会先于 9 输出; 另外,我们还能注意到,输出 6 的代码被 setTimeout 这个宏任务包裹,但这个宏任务又被 .then() 这个微任务包裹,根据最终的结果,这个宏任务基本还是被正常对待,只是在相同延时的宏任务中被最后执行了,尽管在代码中这个微任务中的宏任务写在了普通宏任务的前面,所以最终 8 会比 6 先输出;

2019/6/20
articleCard.readMore

JavaScript 实现元素全排列

排列 (Permutation / Arrangement) 概念 n 个不同元素中任意选取 m (m <= n) 个元素进行排列,所有排列情况的个数叫做 排列数,其值等于: A = n! / (n - m)! ! 表示数学中的阶乘运算符,可以通过以下函数实现: function factorial(n) { if (n === 0 || n === 1) { return 1; } else if (n < 0) { return null; } else { return n * factorial(n - 1); } } console.log(factorial(4)); // 24 当 n = m 时,称为 全排列,其值等于: A = n! 全排列相当于将所有元素进行排序,得到所有不同顺序情况的个数; 分析 利用阶乘函数,通过上述数学公式只能得到所有情况的个数值,不容易得到具体的每种情况,要获取每种情况的输出值的话需要另寻他法; 用数组举例分析: 全排列: [1, 2, 3] => [ [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1] ] 共 6 种情况 树状图表示: 1 2 3 / \ / \ / \ 2 3 1 3 1 2 | | | | | | 3 2 3 1 2 1 => 6 3 个元素中选取 2 个时:(n = 3, m = 2) [1, 2, 3] => [ [1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2] ] 共 6 种情况 树状图表示: 1 2 3 / \ / \ / \ 2 3 1 3 1 2 => 6 实现 let arr = [1, 2, 3]; /* 参数 a 为输入数组, 元素个数 n 为 a 的长度, 选取个数为 m; */ function permutation(a, m) { // 保存最终输出结果 let result = []; // 定义 m 值默认等于 n,即全排列 let n = a.length; m = m || n; // 定义递归函数保存结果到数组中 // _a 为输入数组, // tmpResult 为保存单个情况结果的数组 function recur(_a, tmpResult = []) { if (tmpResult.length === m) { // 结果达到 m 个时保存结果, // 停止递归并进入下一次遍历 result.push(tmpResult); } else { for (let i = 0; i < _a.length; i++) { // 复制一份输入数组,防止引用值被改变 let tmpA = _a.concat(); // 复制一份保存结果的数组,防止每次遍历相互影响 let _tmpResult = tmpResult.concat(); // 保存当前遍历值 _tmpResult.push(tmpA[i]); // 删除当前遍历值,传递参数进入下一层递归 tmpA.splice(i, 1); recur(tmpA, _tmpResult); } } } // 开始执行递归,然后返回最后结果 recur(a); return result; } console.log(permutation(arr)); // 3 个数全排列: /* [ [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1] ] */ console.log(permutation(arr, 2)); // 3 个数中选取 2 个数排列: /* [ [1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2] ] */ 最终实现函数就是 permutation(a, m),其中参数 a 为输入数组,包含需要排列的所有元素,参数 m 为选取需要排列的个数,默认等于输入数组的长度,即默认全排列,注意 m 不能大于元素个数; 拓展 以上函数输出值为一个二维数组,如果需要便于观察,输出一个一维数组,可以定义一个合并函数: function merge(arr) { return arr.map(x => x.join('')); } let result = merge(permutation([1, 2, 3])); console.log(result); // [123, 132, 213, 231, 312, 321]

2019/5/11
articleCard.readMore

JavaScript 之常见算法排序

冒泡排序 冒泡排序即数组从头到尾,依次比较相邻两数的大小,不符合顺序则交换位置,一直循环直到排序完成。如果是升序排序,那么每一轮的一系列比较和交换之后,最大那个数一定会被排到最后(不信可以动手验证一下),可以理解为冒泡到最后,这样每一轮的最大那个数都冒到最后,所以每一轮需要比较的总数都在减少,直到剩一个数为止,序列就有序了,降序也是同样的道理; // 输入值 _arr 为需要排序的数组,返回一个有序新数组 function bubbleSort(_arr) { var arr = _arr.concat(); var len = arr.length; for (var i = len - 1; i > 0; i--) { for (var j = 0; j < i; j++) { if (arr[j] > arr[j + 1]) { let tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } return arr; } 选择排序 选择排序即从数组第一个数到倒数第二个数,分别与后面的数中的选出的最值(升序就是最小值)进行比较,满足条件(升序就是大于最小值)就交换位置,然后完成排序。这里可以理解为先选择出最小值,然后与前面的数进行比较和交换,就不用像冒泡那样挨个比较和交换了;另外,这里为了交换方便,记录的最值其实是该值在数组中的索引,而不是实际值; function selectSort(_arr) { var arr = _arr.concat(); var len = arr.length; for (var i = 0; i < len - 1; i++) { var minIdx = i; for (var j = i + 1; j < len; j++) { if (arr[j] < arr[minIdx]) minIdx = j; } // 如果当前值已经是最小值,就可以不用交换, // 避免浪费时间 if (minIdx !== i) { var tmp = arr[i]; arr[i] = arr[minIdx]; arr[minIdx] = tmp; } } return arr; } 插入排序 插入排序即从第二个到最后一个数,分别与排在前面的有序序列(第一轮该序列只有一个数,肯定是有序的,之后每一轮结束这个有序序列都会增加)中的每个数进行比较,然后插入合适的位置使其有序,直到最后一个数插入时完成排序; function insertSort(_arr) { var arr = _arr.concat(); var len = arr.length; for (let i = 1; i < len; i++) { // 先将插入值(当前值)备份,方便后续插入操作 let tmp = arr[i]; // 插入值在有序序列中从右向左比较 for (let j = i; j > 0; j--) { // 下面就是“插入”操作的实现: // 如果插入值小于比较值(j-1),则将前面的数向后挪一位, // 这样就可以把被插入的空间留出来了,并且在不断向前移动 if (tmp < arr[j - 1]) { arr[j] = arr[j - 1]; // 如果插入值大于或等于比较值,则把插入值放到这个比较值的后面 // 也就是之前留出来的插入空间 } else { arr[j] = tmp; break; } } } return arr; } 快速排序 快速排序在数组中任选一个数(下面选第一个数)作中间值,然后将余下的数分别与其比较,比中间值小则放到左边,否则放右边,然后再进行递归,将放在左边和右边的数组分别作为新数组进行同样的排序操作,直到数组不能再分,最后将所有排序结果合并;这里快速可以理解为整个操作过程相比于其他方法简单快捷,找好任一个中间值后便将剩下的数挨个放入其左或右,而不用管左右数组是否有序,直到递归完成就整体有序了,至于排序是否快速就要看情况了; // 方法一: function quickSort(arr) { let len = arr.length; if (len < 2) { return arr; } else { let mid = arr[0], // 基准(中间值) left = [], // 放到基准左边的数 right = []; // 放到基准右边的数 for (let i = 1; i < len; i++) { if (arr[i] < mid) { left.push(arr[i]); } else { right.push(arr[i]); } } // 递归分割下去,不能分割时合并左中右数组返回 return quickSort(left). concat(mid). concat(quickSort(right)); } } // 方法二: function quickSort(arr) { let len = arr.length; if (len < 2) { return arr; } else { let midIdx = 0; // 基准值的索引 // 这里执行的就是把值放入基准左边还是右边的操作 for (let i = 1; i < len; i++) { // 由于基准是第一个数,并且是从左向右遍历, // 所以后面的遍历值如果小于基准就先删除再 unshift 到最前面, // 这样就实现了“放到左边”, // 如果大于或等于基准就不用管,也就“放到右边”了; if (arr[i] < arr[midIdx]) { arr.unshift(arr.splice(i, 1)[0]); midIdx++; } } return quickSort(arr.slice(0, midIdx)). concat(arr[midIdx]). concat(quickSort(arr.slice(midIdx + 1))); } } // 经测试方法一比方法二快一些,数组越大相差倍数数量级也越大。 // 显而易见方法一在空间上消耗不少,所以在时间上占优势; 归并排序 归并排序递归地将数组分割为两个部分(左数组与右数组),直到不能再分,然后再定义一个合并函数,负责递归地将两部分合并为一个有序数组作为返回值;合并函数其实会是合并两个有序的数组,合并方法便是分别将两数组第一个数取出(删除)放入返回数组中,至于两个数先放哪一个,可以通过比较大小来确定;所以这里的归并可以理解为递归地合并为一个有序序列; function mergeSort(arr) { if (arr.length < 2) { // 不能再分时返回数组,执行之后的合并操作 return arr; } else { // 将数组分割成两部分 let mid = Math.ceil(arr.length / 2); let left = arr.slice(0, mid); let right = arr.slice(mid); // 递归地合并每次分割的左右数组 return merge(mergeSort(left), mergeSort(right)); } } // 把左右数组合并为一个有序数组的函数 function merge(left, right) { let result = []; let len = left.length + right.length; for (let i = 0; i < len; i++) { // 分割后左数组为空的情况 if (!left[0]) { result.push(right.shift()); // 右数组为空 } else if (!right[0]) { result.push(left.shift()); // 左右数组都不为空 } else { // 较小的元素优先放入 if (left[0] < right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } // 不存在左右数组都为空的情况,因为总循环次数为 len // 所以左右数组都空之前已经停止循环了 } return result; }

2019/5/1
articleCard.readMore

JavaScript 面向对象编程

概述 面向对象编程思想,顾名思义,即模仿现实世界的存在物,一切节对象,拥有各自的特性与行为,如人类,外貌、肤色、身高、体重等是其特征,能吃饭睡觉行走是其行为; 同样,编程中,一个对象拥有 属性(key/property),相当于人类的特征,当然这些属性一般都有 属性值(value),相当于人类特征的具体内容,如黄皮肤、身高 180cm;对象还拥有一类特殊属性,叫 方法(method),也可以理解为一种属性,只不过它的属性值是一个函数,对象的方法就相当于人类的行为,如行走、吃饭、说话,别人叫你名字时你会做出回应等; 构造函数 通常申明一个对象是这样的: var obj = { name: 'knight', // name, age 是对象 obj 的属性 age: 20, greet: function(you) { // greet 是对象 obj 的方法,可以接受参数 console.log('Hello ' + you + ', my name is ' + this.name); } } console.log(obj.name); // 'knight' obj.greet('cloud'); // 'Hello cloud, my name is knight' 面向对象编程则使用 构造函数 创建一个 对象构造器,例如: function Obj() { this.name = 'knight', this.age = 20, this.greet = function(you) { console.log('Hello ' + you + ', my name is ' + this.name); } } 其实就是申明一个函数,只不过函数名通常首字母大写来区分普通函数,函数的作用也与普通函数有所区别;其中的 this 指向当前环境的对象,是一种方便的写法; 对象实例 上面创建了一个对象构造函数,当然这还只是一个函数,不是一个直接能用的对象,从构造函数得到对应的对象,称为创建该构造函数的一个 实例(instance),即一个新对象,使用 new 关键字: function Obj() { this.name = 'knight', this.age = 20, this.greet = function(you) { console.log('Hello ' + you + ', my name is ' + this.name); } } var obj = new Obj(); // 创建对象实例 console.log(obj.name); // "knight" obj.greet('cloud'); // "Hello cloud, my name is knight" new 使用 new 关键字时,其实大致执行了以下操作: // 正常使用 new function Obj(name) { this.name = name; } var obj = new Obj('Knight'); console.log(obj.name); // Knight // 模拟使用 new function Obj(name) { this.name = name; } function _new(fn, name) { var result = {}; // 先创建一个空对象 result.__proto__ = fn.prototype; // 执行原型链连接 var _result = fn.call(result, name); // 执行构造函数方法 if (_result) { // 构造函数有返回值则返回该值 return _result; } else { return result; // 否则返回该对象 } } var obj = _new(Obj, 'Knight'); console.log(obj.name); // Knight instanceof 判断某对象是否是某函数的实例,需要用到 instanceof 操作符,注意是操作符,类似 + - * /,而不是函数,用法如下: var date = new Date(); console.log(date instanceof Date); // true constructor 反过来,想要知道某对象的构造函数,可以使用 constructor 属性,一般对象都有这个属性,其值为该对象的构造函数; function Obj() { this.name = 'knight'; } var obj = new Obj(); console.log(obj.constructor); // function Obj(){this.name='knight'} var date = new Date(); console.log(date.constructor); // function Date(){[native code]} 原型 构造函数创建实例后,函数内部定义的属性和方法也会复制一份到实例中,这类属性称为 own property,对象实例可以通过方法 hasOwnProperty() 检测是否拥有某属性,返回 true / false; function Obj() { this.name = 'knight'; } var obj = new Obj(); console.log(obj.hasOwnProperty('name')); // true 构造函数还能定义一种属性,叫做 prototype(原型),这样所有实例都能获取函数定义的 公共 属性,定义方式如下: function Obj() { this.name = 'knight'; } Obj.prototype.age = 20; // 只能外部定义,不能在函数内部定义; var obj = new Obj(); console.log(obj.age); // 20 注意:prototype 是函数的一个属性,同时对象也有一个隐式的原型,即对象的 __proto__ 属性(前后各有两个下划线)指向其构造函数的 prototype,另外函数也存在 __proto__ 属性指向其构造函数,不过对象是没有 prototype 属性的;通常存在以下关系: function Obj() {}; var obj = new Obj(); console.log(obj.__proto__ === Obj.prototype); // true 其实这就是常说的原型链,一般对象存在以下原型链指向关系: var o = {}; o.__proto__ === Object.prototype; // true,对象是 Object 的实例 Object.__proto__ === Function.prototype; // true,Object 是 Function 的实例,因为 Object 本身就是函数 Function.__proto__ === Function.prototype; // true,Function 是 Function 的实例,Function 也是函数 Function.prototype.__proto__ === Object.prototype; //true,函数的原型其实是个对象 Objec.prototype.__proto__ === null; // true,这是原型链指向的顶端,默认设为 null 当定义较多属性时,可以将 prototype 定义为一个新对象: function Obj() { this.name = 'knight'; } Obj.prototype = { age: 20, hobby: 'game' } var obj = new Obj(); console.log(obj.hobby); // 'game' console.log(obj.constructor); // undefined 如上,将 prototype 定义为新对象时,会 删除 对象的 constructor 值,因为 constructor 属性是定义在构造函数的 prototype 对象中的,所以从新定义 prototype 对象时需要 添加 constructor 属性喝值: function Obj() { this.name = 'knight'; } Obj.prototype = { constructor: Obj, // 只需写出构造函数名即可 age: 20, hobby: 'game' } var obj = new Obj(); console.log(obj.constructor); // function Obj(){this.name='knight'} 这里也可看出,constructor 虽然可以判断对象的构造函数,但是它却是可以被更改的,因此尽量使用 instanceof 判断; 遍历对象属性 常用的遍历方法是 for...in,该方法可以遍历出对象的所有属性,包括原型 prototype 中定义的属性,因为 prototype 中的属性是无法用 .hasOwnProperty() 方法访问的,用法如下: function Obj() { this.name = 'knight'; } Obj.prototype.age = 20; var obj = new Obj(); console.log(obj.hasOwnProperty('age')); // 非 own 属性 for (var i in obj) { console.log(i + ': ' + obj[i]); } /* 'name': 'knight' 'age': 20 */ 继承 多个构造函数需要相同的 prototype 时,避免每个设置一遍,重复代码,可以定义一个公共的 prototype,使用 Object.create() 复用代码: var common = { age: 20 } var obj = Object.create(common); console.log(obj.age); // 20 console.log(obj); // {} 注意 Object.create() 方法是将括号内的对象设置为定义的对象的 __proto__ 属性,所以直接输出 obj 会出现 {},但是却可以访问其值,即该值在它的原型链上; 该方法也可以用来 继承 其他构造函数的 prototype 属性,实现代码复用: function Common() { this.hobby = 'game'; } Common.prototype.age = 20; function Obj() { this.name = 'knight'; } // Obj 继承 Common Obj.prototype = Object.create(Common.prototype); // 相当于 Obj.prototype.__proto__ = Common.prototype // 不要忘记把 constructor 改回来 Obj.prototype.constructor = Obj; var obj = new Obj(); console.log(obj.age); // 20 console.log(obj); // {name: "knight"} console.log(obj.hobby); // undefined 不过该方法存在问题,即子类只能继承父类原型中的属性和方法,父类中的私有属性和方法无法获取; 改进版: function Common(name) { this.age = 20; this.name = name; } function Obj(name) { Common.call(this, name); } Obj.prototype = new Common(); Obj.prototype.constructor = Obj; var obj = new Obj('Knight'); console.log(obj); // {age: 20, name: "Knight"} 这里就解决了之前的问题,子类可以继承父类的私有属性和方法,并且还能进行参数传递,同时也继承了父类的原型中的属性和方法;但是,细看会发现,构造函数其实被执行了两次,因此在实例和实例的原型中会出现相同的属性; 最终版: function Common(name) { this.age = 20; this.name = name; } function Obj(name) { Common.call(this, name); } // 相当于只执行了 obj.prototype = new Common(); 步骤中的原型链连接操作 // 即 obj.prototype.__proto__ = Common.prototype // 而没有执行构造函数 Common() Obj.prototype = Object.create(Common.prototype); Obj.prototype.constructor = Obj; var obj = new Obj('Knight'); console.log(obj); // {age: 20, name: "Knight"} 这种组合方式也基本成为了最理想的继承实现方式;

2019/5/1
articleCard.readMore

ECMAScript 发展简史

ECMAScript,简称 ES,是 JavaScript 的标准版,经历了不同版本的变化; 最初由于网速慢,网页验证表单需要通过服务器完成,因此Netscape(网景)公司决定开发这样一种用于处理验证的客户端语言; 1995年,发布 LiveScript,由于当时 Java 很火,就顺便改名为 JavaScript 蹭热度,与 Netscape Navigator 2 一同发布,成为 JavaScript 1.0; 1996年,JavaScript 1.1 与 Netscape Navigator 3 发布,同时微软也在 IE3 中加入 JScript(避免命名授权问题); 1997年,ECMAScript 成为 JavaScript 的标准版本,标准命名为 ECMA-262,第一版为 ES1.0; 1998年,ES2.0 发布; 1999年,ES3.0 发布,标志其成为一门真正编程语言,成为JavaScript语法基础,即我们所学的 JavaScript; 2000年,ES4.0 发布,新标准几乎是区别于ES3的新语言,所以由于激进未通过,激烈争论了几年。。。 2008年,ES3.1 发布,作为 ES4.0 的替代方案,中止了 ES4.0的发布; 2009年,ES3.1 作为第五版(ES5.0)正式发布; 2011年,ES5.1 发布,成为国际标准; 2015年,ES6 正式成为国际标准,又称 ES2015,也是指 ES5.1 后的下一代JavaScript标准; 总结:JavaScript = ECMAScript(核心) + DOM + BOM;

2019/5/1
articleCard.readMore

JavaScript 之 call,bind,apply 方法及 this 的用法辨析

概述 JavaScript函数中的三个方法.call(), .apply(), .bind(),总体来说主要功能就是改变函数中 this 关键字的指向,因为 this 默认指向当前环境的对象; 例如: var obj = { name: 'Knight', getName: function() { // this 指向 obj 对象 console.log(this.name); } } obj.getName(); // 'Knight' call() .call() 可以用于改变 this 值的指向,例如: this.name = 'Knight'; // 此处 this 指向全局对象 window; var obj = { name: 'Cloud' } function fn() { console.log(this.name); } fn(); // 'Knight' fn.call(obj); // 'Cloud' // 此处指向了 obj 对象,所以 name 变了 也可以传递函数参数,平常调用函数的形式可能是这样: function fn(a, b) { console.log(a + b); } fn(2 + 3); // 5 现在也可以这样调用: function fn(a, b) { console.log(a + n); } fn.call(null, 2, 3); // 5 // 因为函数里没有用到 this, // 所以可以设置为 null apply() .apply() 与 .call() 类似,第一个参数也是用于改变 this 指向,区别就是 apply() 接受的函数参数是一个数组,例如: function fn(a, b) { console.log('Mu name is ' + this.name + (a + b) + ' years old.'); } var obj = { name: 'Knight' } var arr = [2, 3]; fn.apply(obj, arr); // My name is Knight, 5 years old. // 使用 call() 的情况: fn.call(obj, 2, 3); // My name is Knight, 5 years old. bind() .bind() 也与 .call() 类似,改变 this 指向,传递函数参数,区别在于 .bind() 方法结果是创建一个新的 绑定函数,而之前的 .call() 和 .apply() 结果都是 立即执行函数,举例来理解: var obj = { name: 'Knight' } function fn(a, b) { console.log('My name is ' + this.name + (a + b) + ' years old.'); } var fnn = fn.bind(obj, 2, 3); // fn.bind() 是一个函数,不会立即执行 fnn(); // My name is Knight, 5 years old. fn.bind(obj, 2, 3)(); // 这种写法就是立即执行函数了, // 结果与上面一样; this 的困境 考虑以下情况: function fn1() { function fn2() { console.log('fn2: ' + this); } fn2(); console.log('fn1: ' + this); } fn1.call('here'); // 'fn1: here' 结果不会输出 fn2: here,因为函数定义 fn2 里的 this 是一个新的指向,并且未定义,与外部函数 fn1 中的 this 不同; 所以我们通常会进行一下处理: function fn1() { function fn2() { console.log('fn2: ' + this); } // 对 this 进行转存 var that = this; fn2.call(that); console.log('fn1: ' + this); } fn1.call('here'); /* fn2: here fn1: here */ 当然,ES6中的箭头函数解决了上述问题: function fn1() { var fn2 = () => { console.log('fn2: ' + this); } fn2(); fn2.call('there'); console.log('fn1: ' + this); } fn1.call('here'); /* fn2: here fn2: here fn1: here */ 由于 .call() 方法对箭头函数不起作用,所以上面的第二行输出与第一行相同;

2019/5/1
articleCard.readMore

个人博客网站添加文章搜索功能

现在很多网站页面里都有搜索模块,包括在线搜索、站内搜索等等,尤其是博客类网站,文章搜索功能就显得比较重要,现在以个人博客网站为例,详细介绍如何给页面添加搜索功能模块,至于如何搭建个人博客网站,可以参考这篇文章:https://knightyun.github.io/2018/04/01/github-pages-blog; 功能分析 为网页添加搜索模块的第三方网站有不少,实现效果也都不错,既然人家都能通过代码实现此功能,与其过度依赖第三方,何不自己研究一下原理自己实现呢_; 网页内搜索功能无非就是在文本输入框内输入一串想要搜索的关键字符串,点击搜索按钮后查找匹配出站内所有文章的匹配结果进行输出,再附上一个文章的链接;针对这个功能,作者本人第一时间想到的则是前端领域里的 Ajax,利用 XMLHttpRequest() 实现数据的交互,不清楚的可以自行百度快速补习,然后一般博客类网站都有 RSS 订阅功能及其数据页面,不清楚的也可以迅速百度了解一下,其实就是站内某个目录内存在一个叫 feed.xml 或者类似名字的页面,XML格式的,早期用于实现新闻内容订阅的,现在用的也比较广泛,里面存储的就是文章相关的一些数据,因此我们就可以获取这个文件里面的所有内容然后实现搜索匹配; 如果像我一样用 Gihub Pages 搭建的个人博客网站的话,无论用的 jekyll 或者 hexo,一般这个 feed.xml 文件就位于 根目录 下,并且该文件同时记录着某一篇文章所对应的 标题、页面链接、文章内容 等内容,这三个部分后面将用到,xml 内容大致如下: 当然有其他更好的实现方法可以自行参考或者在文末评论,那么接下来便通过这个方法来一步步实现搜素功能; 功能实现 先预览一下页面最终的实现效果: 或者在这里:https://knightyun.github.io/ 预览我的博客的搜索模块的最终实现; HTML部分 即页面样式,组成很简单,即一个文本输入框<input>和一个搜索图标,这里图标可以自行搜索下载一个,或者像下面一样使用在线图标,全部代码如下: 先在<header></header>内部添加以下代码,使用在线图标: <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> 然后在网页内需要添加搜索栏的合适位置添加以下代码,一般放在顶部导航栏: <div class="search"> <i class="material-icons search-icon search-start">search</i> <input type="text" class="search-input" placeholder="Searching..." /> <i class="material-icons search-icon search-clear">clear</i> <div class="search-results"></div> </div> 上面的clear是一个清除输入框内容的图标,search-results是用于输出匹配到的结果的板块; CSS部分 然后来看一下CSS样式代码,仅供参考: .search { position: relative; height: 30px; text-align: right; line-height: 30px; padding-right: 10px; } .search .search-icon { float: right; height: 100%; margin: 0 10px; line-height: 30px; cursor: pointer; user-select: none; } .search .search-input { float: right; width: 30%; height: 30px; line-height: 30px; margin: 0; border: 2px solid #ddd; border-radius: 10px; box-sizing: border-box; } .search .search-clear { display: none; } .search .search-results { display: block; z-index: 1000; position: absolute; top: 30px; right: 50px; width: 50%; max-height: 400px; overflow: auto; text-align: left; border-radius: 5px; background: #ccc; box-shadow: 0 .3rem .5rem #333; } .search .search-results .result-item { background: aqua; color: #000; margin: 5px; padding: 3px; border-radius: 3px; cursor: pointer; } 样式可以自行随意调整,最终感觉好看就OK; JavaScript部分 接下来就是重头戏了,也是实现搜索功能的核心部分,搜索逻辑的实现; 再来大致分析一下逻辑,和实现的思路: 利用XMLHttpRequest()获取站内feed.xml内的所有数据,保存到一个XML DOM对象中; 将XML对象中的文章标题、链接、内容、索引等通过getElementsByTagName()等方法获取并保存到对应数组变量中; 用户在输入框输入查找内容,提交后内容保存到一个字符串类型变量中; 遍历保存文章内容的数组,通过.search()等方法和输入值进行匹配; 匹配成功后得到所有匹配成功的数组元素的索引值,该索引值也是该内容的标题、链接数组对应的索引值; 将最终搜集的文章标题、链接,以及匹配到的内容片段摘取输出到页面; 这里附上最终的 js 实现代码与注释: // 获取搜索框、搜索按钮、清空搜索、结果输出对应的元素 var searchBtn = document.querySelector('.search-start'); var searchClear = document.querySelector('.search-clear'); var searchInput = document.querySelector('.search-input'); var searchResults = document.querySelector('.search-results'); // 申明保存文章的标题、链接、内容的数组变量 var searchValue = '', arrItems = [], arrContents = [], arrLinks = [], arrTitles = [], arrResults = [], indexItem = [], itemLength = 0; var tmpDiv = document.createElement('div'); tmpDiv.className = 'result-item'; // ajax 的兼容写法 var xhr = new XMLHttpRequest() || new ActiveXObject('Microsoft.XMLHTTP'); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { xml = xhr.responseXML; arrItems = xml.getElementsByTagName('item'); itemLength = arrItems.length; // 遍历并保存所有文章对应的标题、链接、内容到对应的数组中 // 同时过滤掉 HTML 标签 for (i = 0; i < itemLength; i++) { arrContents[i] = arrItems[i].getElementsByTagName('description')[0]. childNodes[0].nodeValue.replace(/<.*?>/g, ''); arrLinks[i] = arrItems[i].getElementsByTagName('link')[0]. childNodes[0].nodeValue.replace(/<.*?>/g, ''); arrTitles[i] = arrItems[i].getElementsByTagName('title')[0]. childNodes[0].nodeValue.replace(/<.*?>/g, ''); } } } // 开始获取根目录下 feed.xml 文件内的数据 xhr.open('get', '/feed.xml', true); xhr.send(); searchBtn.onclick = searchConfirm; // 清空按钮点击函数 searchClear.onclick = function(){ searchInput.value = ''; searchResults.style.display = 'none'; searchClear.style.display = 'none'; } // 输入框内容变化后就开始匹配,可以不用点按钮 // 经测试,onkeydown, onchange 等方法效果不太理想, // 存在输入延迟等问题,最后发现触发 input 事件最理想, // 并且可以处理中文输入法拼写的变化 searchInput.oninput = function () { setTimeout(searchConfirm, 0); } searchInput.onfocus = function () { searchResults.style.display = 'block'; } function searchConfirm() { if (searchInput.value == '') { searchResults.style.display = 'none'; searchClear.style.display = 'none'; } else if (searchInput.value.search(/^\s+$/) >= 0) { // 检测输入值全是空白的情况 searchInit(); var itemDiv = tmpDiv.cloneNode(true); itemDiv.innerText = '请输入有效内容...'; searchResults.appendChild(itemDiv); } else { // 合法输入值的情况 searchInit(); searchValue = searchInput.value; // 在标题、内容中查找 searchMatching(arrTitles, arrContents, searchValue); } } // 每次搜索完成后的初始化 function searchInit() { arrResults = []; indexItem = []; searchResults.innerHTML = ''; searchResults.style.display = 'block'; searchClear.style.display = 'block'; } function searchMatching(arr1, arr2, input) { // 忽略输入大小写 input = new RegExp(input, 'i'); // 在所有文章标题、内容中匹配查询值 for (i = 0; i < itemLength; i++) { if (arr1[i].search(input) !== -1 || arr2[i].search(input) !== -1) { // 优先搜索标题 if (arr1[i].search(input) !== -1) { var arr = arr1; } else { var arr = arr2; } indexItem.push(i); // 保存匹配值的索引 var indexContent = arr[i].search(input); // 此时 input 为 RegExp 格式 /input/i,转换为原 input 字符串长度 var l = input.toString().length - 3; var step = 10; // 将匹配到内容的地方进行黄色标记,并包括周围一定数量的文本 arrResults.push(arr[i].slice(indexContent - step, indexContent) + '<mark>' + arr[i].slice(indexContent, indexContent + l) + '</mark>' + arr[i].slice(indexContent + l, indexContent + l + step)); } } // 输出总共匹配到的数目 var totalDiv = tmpDiv.cloneNode(true); totalDiv.innerHTML = '总匹配:<b>' + indexItem.length + '</b> 项'; searchResults.appendChild(totalDiv); // 未匹配到内容的情况 if (indexItem.length == 0) { var itemDiv = tmpDiv.cloneNode(true); itemDiv.innerText = '未匹配到内容...'; searchResults.appendChild(itemDiv); } // 将所有匹配内容进行组合 for (i = 0; i < arrResults.length; i++) { var itemDiv = tmpDiv.cloneNode(true); itemDiv.innerHTML = '<b>《' + arrTitles[indexItem[i]] + '》</b><hr />' + arrResults[i]; itemDiv.setAttribute('onclick', 'changeHref(arrLinks[indexItem[' + i + ']])'); searchResults.appendChild(itemDiv); } } function changeHref(href) { // 在当前页面点开链接的情况 location.href = href; // 在新标签页面打开链接的代码,与上面二者只能取一个,自行决定 // window.open(href); } 可以把上面的代码保存到search-box.js这样的 js 文件中,然后引入到 html 页面里; 看一下最终效果: 或者来我的主页查看:https://knightyun.github.io/

2019/3/4
articleCard.readMore

编程范式之命令式与函数式

很多语言是聚范式/多重范式编程,即支持多在编程范式,如面向对象(Java),面向过程(C语言),泛函(函数式),元程序设计等;以下例子都用 JavaScript 举例; 命令式编程(Imperative) 如命令一般指导程序一步步完成功能,如 for 循环: function myFn(n) { for (i = 0; i < 3; i++) { n++; } console.log(n); } myFn(0); // 3 函数式编程/声明式(Functional/Declarative) 函数式编程特点: 函数式编程是声明式的; 提倡纯函数理念,变量私有,不同于面向对象编程的成员共享; 无副作用,不影响其他外部变量; 有些类似初中数学纯函数f(x)的定义,提供输入值,返回新的输出值,每次提供相同输入值总能返回相同输出值,使线程安全可靠; Array.map(); // 纯函数,输出唯一 Math.random(); // 非纯函数,输出不唯一 函数的大量使用,变量中存储函数,动态创建函数,返回值为函数,函数作为参数传递,等等; // 字符串通过函数储存在变量中 var myStr = function(){return 'Hello World'}; console.log(myStr); // Hello World // 对象属性值储存为函数 var myObj = { name: 'Cloud', getName: function(){ return this.name; } } console.log(myObj.getName()); // Cloud // 动态创建函数,用时调用,用完销毁 console.log('Hello ' + (function(){ return 'World'; })()); // Hello World // 函数作为参数进行传递 function paraFn() { return 'Hello'; } function myFn(a, b) { return a + b; } console.log(myFn(paraFn(), 'World')); // Hello World // 函数作为返回值 function myFn2() { var a = 'Hello '; return function(){ return a + 'World'; } } console.log(myFn2()()); // Hello World 总结:例如for循环一个数组,命令式便是写出具体循环的方式,声明式便是只写声明函数,只要循环结果,具体方式交给程序执行; 例如: // 命令式 var a = [1, 2, 3]; var b = []; for (i = 0; i < 3; i++) { b.push(a[i] * a[i]); } console.log(b); // [1, 4, 9] // 声明式 var a = [1, 2, 3]; var b = a.map(function(i){ return i * i; }); console.log(b); // [1, 4, 9] 同样的结果,代码量和理解难易上,声明式都明显优于命令式对吧; 声明式编程: 特点: 说明想要实现的功能,让机器完成步骤以及如何实现; 免去一些不必要的命令步骤,让思维集中在功能开发上,而不是冗长的复杂过程实现; 递归实现阶乘便是一个典型的函数式: function factorial(n) { if (n == 0) return 1; return n * factorial(n-1); } console.log(factorial(3)); // 3 x 2 x 1 = 6 .map() .reduce()等 也是申明式编程函数; 函数合成 一个值变成另一个值,中间经过多个函数,将多个函数合并为一个函数来实现; 举个例子: // 高中数学常见的过程 g(x) = 2x; h(x) = x + 3; f(x) = 2x + 3; // 则可变换为以下形式,即我们所学的复合函数 f(x) = h(g(x)); 上面的f(x)便是一个合成函数,实现了变量x到2x + 3的转变; js的实现: function gFn(x) { return x *2; } function hFn(x) { return x + 3; } console.log(hFn(gFn(1))); // 5 // 使用函数合成 function fFn(x) { return hFn(gFn(x)); } console.log(fFn(1)); // 5 函数柯理化(Currying) 以逻辑学家Haskell Curry命名,即使接收多个参数的函数变成接受单个参数的函数的过程。单参数会使函数合成更简单; 例如: // 原函数 function plusFn(x, y, z) { return x + y + z; } console.log(plusFn(1, 2, 3)); // 6 // 柯理化后 function plusFn(x) { return function(y) { return function(z) { return x + y + z; } } } console.log(plusFn(1)(2)(3)); // 6

2019/1/27
articleCard.readMore

Kali Linux 自定义分辨率

Kali中无分辨率1920 x 1080,自定义的步骤: 方法一 控制台输入: cvt 1920 1080 会得到以下内容: # 1920x1080 59.96 Hz (CVT 2.07M9) hsync: 67.16 kHz; pclk: 173.00 MHz Modeline "1920x1080_60.00" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync 复制后面的内容,然后在新控制台输入: sudo xrandr --newmode "1920x1080_60.00" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync 然后再输入: sudo xrandr --addmode Virtual1 "1920x1080_60.00" 最后系统设置选择分辨率或者: sudo xrandr --output Virtual1 --mode "1920x1080_60.00" 但是此方法重启会失效,因此可以使用方法二; 方法二 在 /etc/profile 文件末尾假如以下代码: xrandr --newmode "1920x1080_60.00" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync xrandr --addmode Virtual1 "1920x1080_60.00" 重启完成。

2019/1/27
articleCard.readMore

Linux 强行取消挂载

Linux系统有时需要取消挂载一些设备或者目录, 例如: # /dev/sdb挂载到了 /mnt/usb umount /mnt/usb 但是多半会提示: umount: /mnt/usb: target is busy 如果已 备份 了数据需要强行卸载,并且尝试 umount -f /mnt/usb 还是失败的情况 可以使用命令: fuser -cu /mnt/usb #查看挂载文件进程 fuser -mv /dev/sdb #或者查看挂载点进程 fuser -ck /mnt/usb #结束进程 fuser -mk /dev/sdb #使用挂载点结束进程 或者使用 懒卸载 方式,命令执行后系统会自动关闭相关进程后再卸载: umount -l /mnt/usb

2019/1/27
articleCard.readMore

Linux 中 apt install 的 lock 问题

Linux系统中有时执行 apt install 时,可能会显示以下问题: E: 无法获得锁 /var/lib/dpkg/lock - open (11: 资源暂时不可用) E: 无法锁定管理目录(/var/lib/dpkg/),是否有其他进程正占用它? 根据提示分别找到对应的两个lock文件,移除即可; 解决方法: sudo rm /var/cache/apt/archives/lock sudo rm /var/lib/dpkg/lock 再次运行就成功了。

2019/1/27
articleCard.readMore

JavaScript 深度迭代遍历未知对象

面向对象编程的语言,都存在对对象的一些操作,其中就包括遍历未知对象的属性值。 通常情况 常见的遍历对象的方法: var o = { name: 'cloud', age: 20 } for (i in o) { console.log(i + ': ' + o[i]); } // name: cloud // age: 20 特殊情况 但是对象中又含有子对象,对象的属性又是另一个对象,或者更深层嵌套,上面方法就不适用了; 下面使用递归实现这个功能: var o = { name: { firstName: 'cloud', lastName: 'huang' }, age: 20 } function myFn(obj) { for (i in obj) { console.log(i + ': ' + obj[i]); // 这里使用递归,属性类型为对象则进一步遍历 if (typeof(obj[i]) == 'object') { myFn(obj[i]); } } } myFn(o); // 输出: // name: [object Object] // firstName: cloud // lastName: huang // age: 20 这样的话不论对象有多复杂的结构都能全部遍历到位; 困境 但同时,这也是个问题,一些对象层次非常深甚至是死循环的情况就尴尬了,类似于子对象属性与父对象属性一样,尝试用上诉函数遍历一下浏览器的window 对象就能体会了,你会后悔的; 所以为避免这种尴尬情况,设置一个迭代深度值吧,指定遍历到第几代: var depth = 0; // depth为迭代深度值 function myFn(obj) { for (i in obj) { console.log(i + ': ' + obj[i]); depth++; if (depth < 10 && typeof(obj[i]) == 'object') { myFn(obj[i]); } } } 或者使用一种类似懒加载的形式: function myFn(obj) { for (i in obj) { console.log(i + ': ' + obj[i]); if (typeof(obj[i]) == 'object') { // 判断用户是否要继续迭代 if (confirmFn('是否深入遍历?')) { myFn(obj[i]); } } } }

2019/1/27
articleCard.readMore

css 属性前浏览器厂商前缀

CSS3中一些新功能也是目前导致各大浏览器不兼容的一个原因,这些新功能的出现,浏览器厂商们变便开始尝试融合、试验,所以就在这些功能前加上自己的特定前缀来执行自己的特定解决方法,为了让这些功能能在完全确认下来前使用; 下面就是我们经常用到的前缀及其兼容浏览器: -webkit- Apple Webkit团队,兼容Android, Safari, Chrome, BlackBerry等; -moz- Mozilla,兼容Firefox等; -ms- Microsoft基金会,兼容IE; -o- 兼容Opera, Opera Mini, Opera Mobile; 因此对于一些较新的css3特性,需要添加以上前缀兼容每个浏览器,例如实现线性渐变,标准写法是 linear-gradient(),但是一下浏览器还未完全确定这一特性,就在前面添加一个前缀来进行试验执行,如 -webkit-linear-gradient; 下面是开发中常用的兼容写法: body { background: linear-gradient(0, green, blue); background: -webkit-linear-gradient(0, green, blue); background: -moz-linear-gradient(0, green, blue); background: -o-linear-gradient(0, green, blue); background: -ms-linear-gradient(0, green, blue); }

2019/1/27
articleCard.readMore

CSS3 过渡与动画

CSS3中出现很多新的特性,下面就讲一下其中比较好玩的3D操作,过渡和动画效果; 过渡(transition) 过渡就是使瞬间的样式变化,按照一定方式变得缓慢平缓; 例如鼠标划过超链接时颜色的变化,点击按钮后的颜色变化等,默认转化都是瞬间完成,可能你已经习惯了这种变换,但有时候平缓一些看着还是比较舒适的; 要实现样式的过渡变化,那么首先就学要有样式变换,例如鼠标划过,单击按钮,点击图片等操作,来实现颜色,尺寸,位置等样式的变化; 下面是鼠标划过段落使文本变红的操作,应用所有 transition属性: p:hover { color: red; } p { transition-property: color; transition-duration: 2s; transition-timing-function: linear; transition-delay: 0; } 一共四个值,功能基本都是字面翻译的意思: transition-property 执行过渡的属性,例子设置为颜色color的变化,也可以是width, font-size等,不设置的话默认是all,即所有属性; transition-duration 过渡的时间,单位是秒,如1s, 2.3s,不设置的话默认 0s,即无过渡效果; transition-timing-function 设置过渡时的变化方式,默认是 ease,即速度由慢到快再到慢,常用的还有 linear,线性变化速度均匀,还有其他几个方式,过渡时间短的话看不出什么区别; transition-delay 延迟时间,即多少秒后执行过渡效果,默认 0s,不延迟; 当然这么多单词可能记不住,一般使用快捷写法: p { transition: color 2s linear 0; } /*最少要指定过渡时间*/ p { transition: 2s; } 也可以设置每个样式分别过渡,例如: p { transition: color 2s linear, font-size .5s, background: 1s; } 每个样式过渡之间用逗号隔开就行了; 最后,由于是新特性,为了兼容性需要加上浏览器厂商前缀: p { transition: 2s; -webkit-transition: 2s; -moz-transition: 2s; -ms-transition: 2s; -o-transition: 2s; } 动画(animation) CSS3的动画是个很不错的技术,基本能取代一些动图,javascript,flash等; 而动画里最重要的概念就是关键帧,也许你用PS做gif动图的时候看见过这个概念,所谓动画就是一帧一帧图片连续切换实现的效果,关键帧就是里面主要的一些帧; 实现CSS动画也需要设置关键帧 @keyframes: @keyframes my-animation { 0% { color: red; } 50% { color: green; } 100% { color: blue; } } 格式如上,@keyframes后面跟的是自定义的动画名称,后面会用到。里面的0%,50%,100%便是设置的三个关键帧及其对应样式,如果只需要设置首尾两个关键帧,可以这样写: @keyframes my-animation { from { color: green; } to { color: blue; } } 效果: 当然样式除了color还能设置多项样式; 定义好关键帧后就直接在需要应用动画的元素标签内使用就行了,格式及所有属性如下: p { animation-name: my-animation; animation-duration: 3s; animation-timing-function: ease; animation-delay: 0; animation-iteration-count: 3; animation-direction: normal; animation-play-state: running; } 发现了吧,很多属性和transition里面一样,简单介绍下: animation-name 就是之前跟在@keyframea后面的自定义名称,之前设置的是 my-animation; animation-duration animation-timing-function animation-delay 和前面一样,默认分别为 0, ease, 0; animation-iteration-count 动画播放的次数,默认 1,但一般设置为 infinite,即无限循环; animation-direction 动画播放的方向,normal为默认,正向播放,reverse为反向播放,alternate为正向后反向,alternate-reverse为反向后正向; animation-play-state 播放状态,默认 running,运行,paused为暂停,可以在javascript中使用对动画进行控制; 当然,这个属性比之前的transition还多,也有简便写法: p { animation: my-animation 3s linear infinite alternate; } 其中 animation-name 和 animation-duration为必须设置的属性; 同样,记得考虑浏览器兼容: @-webkit-keyframes mycanimation { from { color: green; } to { color: blue; } } p { -webkit-animation: my-animation 3s linear infinite; } /* -moz-, -ms-, -o- 格式类似 */

2019/1/27
articleCard.readMore

CSS3 之 2D 与 3D 变换

目录 目录 关于坐标轴 2D变换 translate(x,y) rotate(deg) scale(x,y) skew(xdeg,ydeg) matrix(a,b,c,d,e) 3D变换 translate3d(x,y,z) rotate3d(x,y,z,deg) scale3d(x,y,z) matrix3d() perspective perspective-origin backface-visibility 其他属性 transform-origin transform-style CSS 3D css3实现了对元素执行2D平面变换,以及视觉上的3D空间变换,2D变换平时可能用的较多,3D效果也能锦上添花; 关于坐标轴 初中数学的几何学里我们便开始接触坐标轴,最基本的是平面直角坐标系XoY,然后延伸出空间直角坐标系XYZ,现在我们来说一下css中的坐标系; css甚至一下设备相关的开发中,基本都遵循这样一套坐标系:以手机屏幕为例,坐标系圆点位于屏幕最左上角;x轴水平,向右为正方向;y轴垂直,向下为正方向;z轴垂直于整个屏幕平面,向外为正方向,就是屏幕光线射向你眼睛的方向; 如图: 2D变换 包括平移translate(),旋转rotate(),缩放scale(),倾斜skew(),矩阵matrix(); translate(x,y) 平移操作,包括translateX(x), translateY(y),括号内填平移参数值,可以是负值,即反方向; 例如: div { /*元素向右平移50px,向下移60px*/ transform: translateX(50px); transform: translateY(60px); } /*简写形式,效果相同*/ div { transform: translate(50px, 60px); } 注意translate()只指定一个值则默认是x轴位移,即水平移动; rotate(deg) 元素旋转,括号中参数为旋转角度,顺时针为正值,逆时针为负值,单位为deg,即多少度; 例如: div { /* 顺时针旋转30° */ transform: rotate(30deg); } scale(x,y) 缩放元素,参数分别为x轴,y轴缩放倍数,包括scaleX(x)和scaleY(y),提供一个参数表示按比例缩放; 例如: div { /* 横向缩小一半,纵向放大一倍 */ transform: scale(.5, 2); } div { /* 按比例放大3倍 */ transform: scale(3); } skew(xdeg,ydeg) 包含skewX(deg), skewY(deg),表示在水平和垂直方向倾斜的角度; 例如: div { transform: skewX(30deg); transform: skewY(45deg); } /* 简写 */ div { transform: skew(30deg, 45deg); } 需要注意,如果元素为一个矩形,那么skewX(30deg)表示矩形顶边固定,底边向右倾斜30deg;skewY(30deg)表示矩形左边框固定,右边框向下倾斜30deg; 可以根据上面讲的屏幕坐标系来记忆,x轴位于屏幕顶部,方向向右;y轴位于屏幕左部,方向向下; 如果skew()只指定一个值,默认是水平倾斜; skewX(30deg) 的效果: skewY(30deg) 的效果: matrix(a,b,c,d,e) 这是一个综合属性,之前的平移,缩放,旋转,倾斜都能通过这个矩阵函数实现,对,大学里线性代数中的矩阵 T_T; 其实这个函数就是前面四种操作的原理,函数共有六个参数,四种操作都对应不同的参数改变方式,像我们这种非数学专业的就不赘述原理了,前面的操作基本够用了(想寻找刺激就去百度“css matrix”吧)~~; 3D变换 所谓3D就是在前面2D平面上多了一个z轴,方法名也差不多,然后能以分别以三根轴位基准进行变换,实现立体效果; 看一下所有3D操作方法: translate3d(x,y,z) 结合前面讲的空间坐标系和 x, y, z轴的位置,三个参数分别对应元素在三个坐标轴方向的平移值,也包含三个方法translateX(x), translateY(y), translateZ(z); 举例: div { transform: translateX(50px); transform: translateY(60px); transform: translateZ(70px); } /* 简写 */ div { transform: translate3d(50px, 60px, 70px); } 注意: 关于设置 translateZ(z) 看不出效果的问题,后面说到设置persoective时会解释; rotate3d(x,y,z,deg) 参数x, y, z为空间坐标系的一个坐标位置,然后由原点(0, 0, 0)指向这个点形成一个有方向的新轴,数学中称矢量,最后一个参数就是元素围绕刚才所形成的新轴旋转的度数; 也包括 rotateX(deg), rotateY(deg), rotateZ(deg),之前2D的 rotate() 便是这里的 rotateZ(); 至于旋转的方向,判断方法类似于物理中的左手定则:角度指定为正的话,左手拇指与四指垂直,拇指指向元素围绕旋转的坐标轴或自定义轴,四指弯曲围绕方向就是旋转方向; 举例: div { transform: rotateX(30deg); transform: rotateY(30deg); transform: rotataZ(30deg); } /* 自定义轴旋转 */ div { transform: rotate3d(10, 10, 10, 30deg); } rotateX(30deg) 的效果: rotateY(30deg) 的效果: 关于为什么这里的旋转不是想象中的效果,而是缩小,主要是没有设置视点,后面会讲; scale3d(x,y,z) 元素关于三个轴的缩放比例,包括scaleX(x), scaleY(y), scaleZ(z),举例: div { transform: scaleX(2); transform: scaleY(2); transform: scaleZ(2); } /* 简写 */ div { transform: scale3d(2, 2, 2); } 需要注意这里的scaleZ(),正常情况下,扩大z轴会是物体变厚,但是css里面呈现的平面元素并没有厚度,所以这里的缩放z轴其实是缩放元素在z轴的坐标,所以要有效果必须要指定translateZ()的值; 举例: body { perspective: 500; } div { /* 必须这个顺序,先缩放后平移,不然无效果 */ transform: scaleZ(2) translateZ(100px); } 按照上面样式才能看到scaleZ(2)的效果,因为后面在z轴上移动了100px,缩放比例为2,最终会移动200px,屏幕上则表现为元素放大了一下,这是透视效果,就是那个 perspective 值,下面会讲到; matrix3d() 和前面2D的matrix()相似,只不过这里括号里的参数有16个,矩阵更加复杂,跳过吧﹋o﹋,有兴趣可以自行百度~~; perspective 在上面的示例中,有关z轴的平移和缩放通常情况下是看不出效果的,正是缺少这项属性值,叫做 透视,美术或设计中会出现这个词汇,就是实现物体近大远小的效果,远小最终会小到一个点,那就是 透视点,perspective就是用来设置那个点距离元素有多远,一般300~600很体现很好的透视效果,值越小元素透视变形越严重; 需要注意的是,这项属性设置在应用透视效果元素的父元素的样式中,才能看出效果,例如: body { perspective: 500; /* 考虑浏览器兼容 */ -webkit-perspective: 500; } 也可以设置在元素本身,格式为: div { transform: perspective(500); -webkit-transform: perspective(500); } rotateX(45deg) 的更真实的效果: rotateY(45deg) 的效果: perspective-origin 此项设置透视点的位置,默认在元素几何中心,需要设置的话,格式如下: body { /* 默认中心 */ perspective-origin: center center; /* 左上角 */ perspective-origin: left top; /* 右边中心 */ perspective-origin: right center; /* 底部中心 */ perspective-origin: bottom center; /* 也可以是长度 */ perspective-origin: 30px 40px; /*最后记得加 -webkit- 兼容 */ } perspective-origin: left center 的效果: perspective-origin: right center 的效果: backface-visibility 翻译过来叫背面是否可见,可以设置visible和hidden,默认可见,比如元素正面有文字,设置背面可见,则关于y轴旋转180°后元素内文字变成镜像,否则不会出现; backface-visibility: visible 的效果: backface-visibility: hidden 的效果(有旋转,只是背面不可见,则看不见了): 其他属性 transform-origin 设置2D/3D变化的基准,可以是长度值,也可以是 left, right, top, bottom,例如: div { /* 关于元素左上角旋转 */ transform-origin: left top; transform: rotate(30deg); } transform-style 设置元素如何在3D空间呈现被嵌套的元素,选择是 flat 和 preserve-3d,默认flat; 这里这么来理解,之前我们对一个元素执行变换时,都是以屏幕所在平面的坐标系在变换,但是元素如果存在子元素的话,transform-style就是用来确定在哪套坐标系上进行变换,flat表示任然以屏幕坐标系为基准,preserve-3d表示以变换后的父元素所在平面的坐标系为基准; #parent { transform-style: preserve-3d; -webkit-transform-style: preserve-3d; transform: perspective(500) rotateY(45deg); -webkit-transform: perspective(500) rotateY(45deg); } #child { transform: perspective(500) rotateX(45deg); -webkit-transform: perspective(500) rotateX(45deg); } flat 的效果: preserve-3d 的效果: 例如,父元素绕x轴旋转了45度,并且设置transform-style: preserve-3d,而嵌套的子元素只绕y轴旋转45度,那么最终效果就是子元素绕父元素平面的y轴旋转了45度,看起来就像先x轴转45度后y轴转45度的效果; 最后,别忘了为以上所有特性添加浏览器兼容前缀; 顺便附上一个以上功能综合效果的演示页面,请戳下面: CSS 3D

2019/1/27
articleCard.readMore

CSS 单位与尺寸参数

css单位 px 常用的单位,即像素pixel缩写,但通常被当做绝对单位,但严格说并不是,因为官方考虑到观看不同设备显示屏时,使网页设计出的某一图形的显示大小在人眼中的观看效果差不多,而定义的一个相对值,即人以一臂之遥观看96DPI的显示屏的角度,大概就是利用透视的近大远小原理,照顾不同设备的最终观看效果。 比如某网页图形设置为一固定的px值,在手机浏览器上显示是用直尺测大概1cm,但是同样在不缩放情况下,电脑显示屏测量可能就是1.5cm左右,如果是打印机打印出来的话也许就是2cm左右了。 em 常用的相对单位,前面的数字是比例,即相对于父元素的字体尺寸的比例,比如父元素字体16px,子元素设置1em,也可以理解为100%,那么子元素也是16px,同样,2em就是200%,32px,也可以是小数0.2em,1.5em等等。 rem 类似于em,但rem是相对于根元素html,例如用css标签选择器给html标签设置字体尺寸font-size大小为20px,那么文档中的每个1rem就代表20px,1.5em代表30px,以此类推。 in,cm,mm 这些虽然是生活中的物体测量单位,但网页的1cm尺寸的元素显示到显示器上,用直尺测量通常不是标准1cm,因为css已经默认设置1in=96px,前面也讲过px会因显示屏而不同,因此最终尺寸也不是绝对的,其他也差不多,所以这类尺寸很少用。 尺寸比较: 显示原理 dpi, ppi dpi(dot per inch),即每英寸多少点,是针对打印机的一个概念,点可以理解为墨点; ppi(pixel per inch),即每英寸多少像素,是针对显示器的概念,开发中一般关心显示器问题,所以一般认为dpi和ppi是同一个概念; dpr dpr(device pixel ratio),即物理像素与独立像素的比例; 物理像素 物理像素也叫设备像素,屏幕显示图像都是由很多个像素点组成,屏幕出厂时本身带的点阵数就是它的物理像素; 独立像素 而独立像素,又叫逻辑像素,或者css像素,顾名思义,逻辑嘛,当然是独立于物理的概念,大小没有固定实际值,也是前面提到的css里面的1px; 逻辑像素可以通过js代码获取,screen.width获取逻辑像素宽度,screen.height是高度;至于dpr,可以通过devicePixelRatio这个全局属性获取,现在的新一些的安卓智能手机一般这个比例是3,电脑的一般是1,iphone、iPad一般是2; 注意,dpr为1,说明一个css像素块由1x1个物理像素块来显示,也就是一个物理像素,如果为2,则是一个css像素由2x2个物理像素来显示,也就是4个像素块,以此类推;这里也就能明白了,dpr的存在就是为了是小尺寸设备屏幕显示出高画质图形,细节更高就更清晰了; 现在来说设备像素的获取,并没有直接的获取方法,所以可以通过dpr乘以逻辑像素的方法获取; 综合举例: var logicWidth = screen.width; var logicHeight = screen.height; var dpr = devicePixelRatio; var deviceWidth = logicWidth * dpr; var deviceHeight = logicHeight * dpr; // 输出逻辑像素 console.log('逻辑像素:' + logicWidth + 'x' + logicHeight); // 输出设备像素 console.log('物理像素:' + deviceWidth + 'x' + deviceHeight);

2019/1/27
articleCard.readMore

用 CSS 画一些多边形状

CSS是个很强大的网页开发工具,使生硬的网页变得丰富绚丽,css能实现很多效果,比如css3中的过渡与动画效果都很好看,最基本的就是画一个具有长宽的矩形,通过设置 border-radius 又能实现画圆形和椭圆形,但是其他多边形似乎没有直接能用的属性,比如三角形,五角星,六边形等等; 下面根据几何顺序依次来实现一下: 圆形 分析: 在长宽相等的正方形中使用 border-radius 属性,其值等于长或宽的一半; 代码: <html> <head> <title>CSS</title> <style> div { width: 40px; height: 40px; background: red; border-radius: 20px; } </style> </head> <body> <div></div> </body> </html> 椭圆形 分析 同样是 border-radius 属性,只不过其值有变化,使用 border-radius: 30px/20px,意思是原矩形宽度方向半径设为 30px,高度方向半径设为 20px,或者简写为 boder-radius: 50%,一个意思,宽度和高度方向的半径各位宽度和高度的一半; 代码: <html> <head> <title>CSS</title> <style> div { width: 40px; height: 40px; background: red; border-radius: 30px / 20px; /* 或者这样 border-radius: 50%; */ } </style> </head> <body> <div></div> </body> </html> 三边形 分析 没有直接能用的三角形的属性,可以利用CSS的盒子模型,就是下面这种,像 <p>, <h1>, <div> 这些标签都是一个“盒子”,标签内的文本是内容区,周围的彩色边界设置的是 border 值,当然还有边界与内容区中间的 padding 值,以及边界外的 margin 值; 内容 所以由图就能想到办法了,就是让某一条边界的宽度值直接等于盒子的宽度,并设置一个边界颜色,其他边界线设置不同的宽度值来调整三角形的斜度,并把边界线颜色设置为透明 transparent 即可; 代码: <html> <head> <title>CSS</title> <style> div { width:30px; height:40px; border-left:20px solid transparent; border-right:20px solid transparent; border-bottom:40px solid red; } </style> </head> <body> <div></div> </body> </html> 矩形 最简单的形状,就不分析了; 梯形 分析 方法有些像三角形,只不过底部边界线宽度等于矩形高度,左右两边的边界线宽度小于矩形宽度值即可(感觉这两句话绕就比划着再读几遍 -_-); 代码: <html> <head> <title>CSS</title> <style> div { width: 60px; height: 40px; border-left: 10px solid transparent; border-right: 10px solid transparent; border-bottom: 40px solid red; } </style> </head> <body> <div></div> </body> </html> 平行四边形 分析 看成一个倾斜过的矩形,所以可以使用 transform: skew() 属性,括号内是倾斜角度,比如30度就是 30deg,还有 transform 是CSS3中的一个新属性,所以需要加浏览器前缀进行兼容,例如 : div { transform: skew(30deg); -webkit-transform: skew(30deg); -moz-transform: skew(30deg); -ms-transform: skew(30deg); -o-transform: skew(30deg); } 另外脑洞够大可以发挥一下想象,平行四边形可以看成一个直角梯形与一个直角三角形的组合,或者一个矩形与两个直角三角形的组合; 直角梯形与直角三角形组合的代码: <html> <head> <title>CSS</title> <style> #div0 { display: inline-block; width: 60px; height: 40px; border-left: 10px solid transparent; border-bottom: 40px solid red; } #div1 { display: inline-block; width: 10px; height: 40px; border-left: 10px solid red; border-bottom: 40px solid transparent; } </style> </head> <body> <div id="div0"></div><!-- --><div id="div1"></div> </body> </html> 注意: 两个 <div> 标签之间如果有换行或者空格的话,最终两个块图形间会出现一条细缝,所以写的时候就要避免换行,或者像上面一样把换行注释掉; 五边形 分析 五边形可以看成上面的三角形与下面梯形的组合,当然数学好的可以计算一下尺寸就能画出一个正五边形了; 代码: <html> <head> <title>CSS</title> <style> #div0 { width: 50px; height: 10px; border-left: 25px solid transparent; border-right: 25px solid transparent; border-bottom: 10px solid red; } #div1 { width: 50px; height: 40px; border-top: 40px solid red; border-left: 10px solid transparent; border-right: 10px solid transparent; } </style> </head> <body> <div id="div0"></div> <div id="div1"></div> </body> </html> 更多边的形状基本思路都一样,想着用三角形,矩形,梯形这些基本形象进行组合基本上都能实现,下面的形状就只放形状和源码了; 五角星 代码: <html> <head> <title>CSS</title> <style> #div0 { width: 40px; height: 60px; border-left: 20px solid transparent; border-right: 20px solid transparent; border-bottom: 60px solid red; } #div1 { height: 20px; } #div2 { width: 60px; height: 18px; border-top: 25px solid red; border-left: 30px solid transparent; border-right: 30px solid transparent; margin-left: -30px; } #div3 { width: 40px; height: 15px; border-left: 20px solid transparent; border-right: 20px solid transparent; border-bottom: 15px solid white; margin-left: -20px; } </style> </head> <body> <div id="div0"> <div id="div1"></div> <div id="div2"></div> <div id="div3"></div> </div> </body> </html> 六边形 代码: <html> <head> <title>CSS</title> <style> #div0 { width: 60px; height: 30px; border-left: 15px solid transparent; border-right: 15px solid transparent; border-bottom: 30px solid red; } #div1 { width: 60px; height: 30px; border-top: 30px solid red; border-left: 15px solid transparent; border-right: 15px solid transparent; } </style> </head> <body> <div id="div0"></div> <div id="div1"></div> </body> </html> 当然网页上画像上面这种基本图形,或者跟复杂的几何图形,曲线图形等,多半用到canvas或者SVG这两个工具,功能很强大,可以自行了解;

2019/1/27
articleCard.readMore

Base64 码简介

简介 base64是一个保存二进制数据的工具,将多种形式的二进制数据或其构成的文件以ASCII的形式保存,因为很多地方不支持直接的二进制文件保存或呈现,比如可以将图片直接转换成base64码嵌入HTML文档中,而避免使用网络http加载图片。另外,将数据编码为 base64 进行传输,然后解码获得数据,可以一定程度上保证数据的完整并且不用在传输过程中修改这些数据,避免在传输过程中可能出现的问题; 组成 A-Z a-z 0-9 + / 共64个字符(不信自己数一下); 格式 假设一个 .png 图片转换得到的base64码为 abcdefg,标准格式为: data:image/png;base64,abcdefg 在浏览器地址栏输入以上字符串回车就能看见图片了,一般浏览器都支持解析base64码(里面的base64码换成自己的); 或者用在html的 img 标签中: <img alt="" src="data:image/png;base64,abcdefg" /> 再或者用在markdown格式文本中: ![base64](data:image/png;base64,abcdefg) 可以把 data: 看成像 http: 一样的一种协议,下面是其他格式,根据格式应该就能猜到其用途: data:,文本数据 data:text/plain,文本数据 data:text/html,HTML代码 data:text/html;base64,base64编码的HTML代码 data:text/css,CSS代码 data:text/css;base64,base64编码的CSS代码 data:text/javascript,Javascript代码 data:text/javascript;base64,base64编码的Javascript代码 data:image/gif;base64,base64编码的gif图片数据 data:image/png;base64,base64编码的png图片数据 data:image/jpeg;base64,base64编码的jpeg图片数据 data:image/x-icon;base64,base64编码的icon图片数据 例如下面的星星图标: 其实它的base64码是这样的: iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAA AA2UlEQVR4AZWSAQZCQRCGv05QgXpQqCggukAgQeSpEwRE oO4Qqg5RoYA6T6GqIIACvKqMMdZK9C1r5t+dmbE7/E+SKj 26nz3BV9IseTlrSQqPHGc9jIjUOlPAIeBiGdZszD4SYCyc MjXqjjdHifM0cUuMGAfzn8QRQnGGdAgpAlAipMNQgtsIfY nZkcUly070PsJAU18pg1LmquoAoWUdjEAZm9byG28AFSpA 028cZioFTHkQMSGjysx/zBsnK3Li/tkvpHDI2wVb7MnjEb D6+cFGQkalZ6PyD2/u1Ikpara+FgAAAABJRU5ErkJggg== 为了显示方便所以进行了换行,理论上是很长的一行连续文本,中间不能有空格或者换行,这个图片仅有274字节(Byte)大,所以base64码还算短,几十上百K的图片就很长了,100K图片就有13万多个字符; 转换原理 base64 码的基本转换步骤如下: 如果是图片直接转换成二进制文件,字符先转换成ASCII字符码,再转换成二进制;(网上直接搜“base64转换工具”就会出来很多在线转换网站,上传图片或者输入文本,一键转换) 将 base64 的64个字符按上面组成部分里讲到的顺序排列得到64个索引,索引从0开始,63结束; 因为 64 = 2^6,所以要使二进制包含64种不同情况,需要取6比特位,即000000这种,把之前文件转换得到的二进制数据按每6比特位取一次,然后得到一个6位二进制数再转换成相应十进制数,这个数就是索引,然后按照索引取相应的64字符中的某一个,最后把所有取得的字符连接就是base64码; 编码需要原文本总字节数(字符数)能被3整除(字节数除以3无余数),因此取到最后的字符时如果凑不够,缺位全部用 0 补齐,最后的只要是全为0的6比特位,全部转换成字符 =,所以最多会出现2个 =; 以字符串 Hello 举例,后面类推,先将每个字符转换成ASCII码,再转换成二进制: 字符 H e l l o 空格 ASCII 72 101 108 108 111 32 二进制 01001000 01100101 01101100 01101100 01101111 01000000 再以字符串 Hel 为例,将二进制转化成base64码: 字符 H e l 二进制 0100 1000 0110 0101 0110 1100 Base64补0 0100 1000 0110 0101 0110 1100 Base64结果 S G V s 上面是不用补码的情况,即字符总数是3的倍数;下面是最后剩两个字符需要补码的情况: 字符 H e 二进制 0100 1000 0110 0101 Base64补0 0100 1000 0110 0101 0000 0000 Base64结果 S G T = 然后是最后只剩一个字符的例子: 字符 H 二进制 0100 1000 Base64补0 0100 0000 0000 0000 0000 0000 Base64结果 S A = = 值得注意的是 base64 码 A 的二进制也是 000000,上面例子中 A 的二进制码是部分补全后全为0,只有6个0全是补全的时候才都转换为字符 =; 对于图片文件,原理一样,因为图片文件也是一堆二进制数组成,只不过需要用特殊的编辑器查看,然后以同样的道理即可转换为 base64 码,这里就不赘述了; JavaScript实现转换 在 JavaScript 中,正好有这么两个函数可以实现对 base64 码的编码与解码,它们分别是 atob() 和 btoa(),可能光从名字上看会觉得它们的名字取有些随意,像 “a to b” 和 “b to a”,不要误会,其实这是缩写,全称应该是 “ascii to binary”(即进行 base64 解码) 和 “binary to ascii”(即进行 base64 编码),其中 ascii 指代数据经 base64 编码后的字符,而 binary 指代未经编码的原始数据,其实这里全称的含义其实就是 base64 码的功能所在,下面分别举例说明。 btoa() 该函数实现的是将数据编码为 base64 码,使用方法: var a = btoa('Hello'); console.log(a); // "SGVsbG8=" 会发现这里的输出结果与上面的转换原理部分所得到的结果一致; atob() 该函数实现的是将经过 base64 编码后的字符解码为原始数据,使用方法: var a = btoa('Hello'); // 先将数据编码 var b = atob(a); // 将编码后的字符解码 console.log(b); // "Hello" 另外,如果尝试对非正常编码的字符执行解码操作的话,则会报错: var b = atob('Hello'); console.log(b); // Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.

2019/1/27
articleCard.readMore

UTF-8, ASCII, Unicode 的介绍与区分

背景 人类能通过肉眼识别文字和字符,并能通过知识了解他们的含义,但是计算机内部不论存储还是控制,都是通过二进制码实现,因为二进制的 0, 1 刚好对应基础电路中的开和关,然后组合进行复杂的系统控制; 将人类识别的字符转换成计算机识别的二进制数据的过程,叫做编码,顾名思义,编程二进制数字码,如 0101101100011001这样的;相反,就叫做解码,把二进制码解释为字符; ASCII 首先ASCII全称(American Standard Code for Information Interchange,美国信息交换标准代码),是一个字符集,顾名思义,很多字符的集合; 像前面提到的,人类与计算机语言不通,一个识别字符,一个识别二进制,所以ASCII就充当了这样一个翻译官,其内容是编码与字符的映射,即一个字符只对应一个固定的编码,例如字符 A 的编码为 65,字符 a 的编码为 122;当然这个编码是十进制的,计算机内部把十进制转换成二进制就能供底层使用了; 另外需要知道的是,一字节(1 Byte)等于八比特位(8 bit),8 bit就是这样的:01010101,八位二进制的所有不同表示一共 2^8 = 256 个,而且一般都是从 0 开始数,所以表示的十进制数的范围就是 0 - 255,这也是ASCII编码映射字符数的范围,包含大小写字母和一些其他常用的符号; Unicode 看完上面肯定就会疑惑ASCII总共才表示256个字符,怎么处理当今世界巨大的信息量的,由于这个字符集最初是老外发明的,表示所有字母和一些字符对他们当时来说可能很足够了,但是先进计算机遍布全球大部分地方,汉语、韩语、日语、阿拉伯语等语言数不过来,所以ASCII明显不够用了;虽然中国之前也制定了是和中文的编码字符集叫 GB2313 等系列; 因此,便顺应时代需要产生了一种更庞大的字符集叫 Unicode,有时也叫万国码,顾名思义,几乎表示了世界上所有语言的字符,可以理解为 Unique code,独一无二的的编码; 目前Unicode的编码范围达到了21位,即 0x0000 - 0x10ffff 的范围,二进制为 1 0000 1111 1111 1111 1111,刚好21位;十进制表示为 1114111,就是一百万多个字符,已经相当多了; 如果要使用UNIcode,以在 HTML 中为例,假如知道一个字符的Unicode码是 0x0394,那么就在标签中添加代码: &#x0394; 放在标签中就是: <h5>这个Unicode码对应的字符是:&#x0394;</h5> 结果是这样: 这个Unicode码对应的字符是:Δ 其实那个Unicode编码就是对应的大小希腊字母德尔塔,数学或物理中经常用到的字符; 也可以用 JavaScript 来遍历一部分Unicode与字符的对应关系: for (i = 0x0000; i <= 0x00ff; i++) { document.write(i + ': &#x' + i + ';<br>'); } 页面就会出现前256个字符及其Unicode码; UTF-8, UTF-16, UTF-32 首先UTF全称(Unicode Transformation Format),所以它是一种针对前面提到的Unicode的编码格式,常见的格式就是 UTF-8,还有 UTF-16, UTF-32; UTF-8 其中的 8 表示的是 8 bit,即Unicode中每8位表示一个字符,UTF-16 和 UTF-32 类似,因为Unicode最多才21位,32位大于21位,所以 UTF-32 的格式就可以表示所有字符对应的Unicode码了,但是呢,32位也就是4字节,让每个字符都占用4字节太费空间了,所以出现了UTF-8和UTF-16; UTF-8 定义 0 - 7 bit 的 Unicode 用一字节表示,这里就与ASCII一样了,8 - 11 bit 用两字节表示,12 - 16 bit 用三字节表示,17 - 21 bit 用四字节表示; UTF-8 编码规则如下: Unicode bit UTF-8 byte 0x0000 - 0x007f 0 - 7 0XXX XXXX 1 0x0080 - 0x07ff 8 - 11 110X XXXX 10XX XXXX 2 0x0800 - 0xffff 12 - 16 1110 XXXX 10XX XXXX 10XX XXXX 3 0x1 0000 - 0x1f ffff 17 - 21 1111 0XXX 10XX XXXX 10XX XXXX 10XX XXXX 4 规律是: 每个字节中不足8位的,高位(左边)先用0补上,比如 0XXXX XXXX; 超过两字节表示的UTF-8,第一个字节高位添加两个 1 和一个 0,后面的字节高位添加 10; 三、四字节同理,几个字节高位就添几个 1 再加上一个 0,其余字节高位添 10; 可以看出 UTF-8 这种针对不同位数使用不同字节数编码的方式有效的利用了空间,避免了一些浪费,当然,事物都有利弊,空间降下去,时间也就升上去了;

2019/1/27
articleCard.readMore

电脑固态硬盘接口辨析

固态硬盘相比传统机械硬盘读写速度快许多,固态硬盘目前分为M.2接口与SATA接口。 SATA接口 SATA(Serial ATA,Serial Advanced Technology Attachment,串行高级技术附件),一种硬盘接口规范。 SATA3是目前大多数笔记本使用的接口规范版本,尺寸比msata大一些,有个硬壳盒包装; 如下图,就是笔记本上取下的固态: PCI-E(PCI Express)新一代总线接口,取代PCI总线接口,称为第三代I/O总线技术,提高了带宽,PCI Express的接口根据总线位宽不同而有所差异,包括x1、x4、x8以及x16(x2)模式将用于内部接口而非插槽模式。 PCI-E传输速度大于SATA。 M.2接口 M.2是intel推出替代mSATA(mini-SATA,小尺寸SATA规范接口,多应用于笔记本固态硬盘接口)的新接口规范,尺寸更小,传输性能更高,为满足超级本用户而推出; M.2接口有两种类型:Socket 2(ngff)和Socket 3(nvme),Socket2支持SATA、PCI-E X2接口,PCI-E ×2接口标准,最大的读取速度可以达到700MB/s,写入达到550MB/s。Socket 3可支持PCI-E ×4接口,理论带宽达4GB/s。 NVME(Non-Volatile Memory express,非易失性内存主机控制器接口规范),充分利用PCIE通道的低延时和并行性,降低AHCI的高延时和提高SATA的性能; NVME缺口靠右边,使用一缺固态; NGFF缺口靠左边,使用两缺m.2固态; 购买更换时需要注意主板插槽上缺口靠哪边,一般安装是固态条元器件面朝上。 并且,不要把它和内存条搞混,区别在于接口位置。

2018/12/8
articleCard.readMore

搭建个人论坛网站图文教程

关于建站 前面的文章介绍过如何使用Github Pages提供的仓库服务搭建Jekyll个人博客,链接在这 https://knightyun.github.io/2018/04/01/github-pages-blog,现在来了解一下如何一步步搭建自己的论坛网站。 搭建个人论坛网站,也相当于建站的一种,通常的套路便是域名+主机空间+网页源码程序,网页文件上传到虚拟空间,一般这些空间都安装有网站所需环境,如PHP和数据库等,然后把域名和主机空间绑定,这样就能在浏览器中输入域名(网址)访问所建网页,不然理论上只能通过虚拟主机的IP访问,但是通常有IP访问限制不允许你这么做。 域名和主机空间可以在各大网站市场购买,通常是按年或按月付费,如阿里云和华为云。这里说一个需要注意的东西,产品列表中的虚拟空间、虚拟主机这类产品,指的是云服务器分配出的一部分磁盘空间,也叫网站空间,即只能通过服务商提供的控制面板进行访问控制,上传或者下载网页文件;而云服务器、云主机这类名称指的是有独立操作系统的服务器,可理解为一台可远程桌面控制访问的-云电脑。当然他们也能通过价格区分,一般情况,域名几十块每年,空间几百每年,主机几千每年,更高级的企业级服务就更贵了-_-。 关于论坛 能力强大的大佬可以自己编写网页和程序文件上传到主机空间,普通选手一般选择使用一些开源的网站源码,网上能找到很多,懂一些网页基础的也可以后期修改一些内部样式效果。 这里我们使用Discuz提供的论坛建设服务,相当于让你下载一份网站源码压缩包,解压后上传到自己的网页空间,然后安装他们的程序。先看一下其官网的简介: 他们提供的服务还是不错的,这个源码也是免费下载的,当然也有其他付费服务可以自行了解,下面是他们提供服务的企业,看样子还是很厉害的: 开始安装 首先到其官网下载页面,然后选择下载的版本,我们这里选择简体UTF-8,点击下载等待完成: 也可以到其官方论坛了解,版本更新就在这里发布,我们去其官方gitee,点击下载页面下载: 如图所示下载zip压缩文件即可。由于作者使用最上面那个下载页面的压缩文件测试不成功,因此使用这个下载页面的压缩包。 解压后进入upload文件夹,其他都是一些说明性文档,可以自行了解一下: 然后把upload目录下的所有文件复制到自己购买的空间的网页根目录下,一般是wwwroot这类名字,可以在里面新建一个文件夹例如discuz,然后拷贝到这个文件目录里面,这样就不妨碍以后用虚拟空间制作其他网页。 其主要文件大概就这些,我这里用自己的虚拟机建设的网站做演示,大家就上传到自己虚拟空间目录下,然后在自己电脑浏览器输入绑定的域名进行访问,关于如何绑定域名,很简单并且网上教程很多,自行了解。我自己电脑作为服务器因此网址如图所示,如果域名为test.com,并且上面提到的所有文件保存在discuz·里面,那么访问网址就输入test.com/discuz/`,然后进入安装页面: 然后点同意,下一步,进入如下页面,选择全新安装: 设置数据库,数据库名,用户名,密码,一般买虚拟空间有送一个数据库,没有的话就单独买一个数据库服务器,把地址改一下就行了,邮箱填常用的,用于发送错误报告: 等待页面安装完成,就能点击访问了: 那些推荐应用可以暂时不管,以后还能安装,直接点击右下方访问,看看效果: 右上方是用户注册和登录的地方,管理员就用刚才安装页面设置那个账号和密码,就是admin那个。点击右上方模块管理,管理中心就可以进入后台管理一个内容和功能了,会再次验证密码,还是刚才那个。 整个系统比较庞大,功能模块也相当多,有时间多多摸索一下就能熟练管理了。

2018/11/25
articleCard.readMore

JavaScript 逻辑运算符 “&&” 和 “||” 短路原则的应用

逻辑运算符 在Javascript中,有逻辑运算符 与 &&, 或 ||, 非 !,常在条件句或循环中进行逻辑判断。 例如: var a = 1, b = 1, c = 2; if (a = b && (b = c || a != c)) { alert("true"); } else { alert("false"); } 括号中表达式值为真,最后提示“true”。 短路原则 在逻辑运算中,这是一个通用的原则,这是由于表达式从左到右执行的特性,为了减少运算量而给运算器规定的操作。主要针对 && 和 || 两种运算。 && 的判断是同真为真,一假为假,则运算如果左边的表达式值为 false,那么就不会再执行右边的表达式了,如果左表达式为 true,就会继续执行右表达式; || 的判断是一真为真,同假为假,则运算如果坐表达式值为 true,那么就不用执行右边的表达式了,如果左表达式为 false,就会继续执行右表达式; 举例说明: (1 == 1) && alert("msg1"); (1 != 1) && alert("msg2"); (1 == 1) || alert("msg3"); (1 != 1) || alert("msg4"); 结果是提示“msg1”和“msg4”。原理如上述。 因此,如果有以下表达式: var a = 9; if (a > 0) { alert("true"); } else { alert("false"); } 也许你会使用三目运算简化成这样: var a = 9; (a > 0) ? alert("true") : alert("false"); 其实也可以这样写: var a = 9; (a > 0) && alert("true"); (a > 0) || alert("false"); Javascript中的应用 在javascript中,只有对象(Object)和布尔值true为真,其它例如 undefined, NaN, false 等,值为 false。为被定义的对象或未赋值变量也是 false,因为其值都是 undefined,这里就可以应用于检查某变量是否已定义。 举例说明: var a = 1; var o = new Object(); var b; a && alert("defined"); //已定义a,提示“defined” o && alert("defined"); //已定义对象o,提示“defined” Object && alert("defined"); //Object是一个已知的全局对象,提示“defined” b || alert("not defined"); //b未赋值,提示“not defined” p || alert("not defined"); //未定义p,提示“not defined” 除了这个也能衍生出其它相同原理的应用,类似于判断赋值 var a = (b > 0) && '9' 或判断定义变量 var abc = abc || "" 等。 js中使用这种方法可以减少代码量,提示性能,但同时也降低了代码可读性,比如个人觉得还是看以下代码比较舒适: var a = 1; if (a > 1) { alert("true"); } 方法的选择就要视情况而权衡了。

2018/6/1
articleCard.readMore

jQuery 初识之安装与语法简介

概念 jQuery是一个JavaScript函数库,是一个比较流行的js框架,功能就是简化 js 代码的书写,因为一些功能用原生javascript书写代码量是很大的。可以理解为javascript query,毕竟Query也是它的一个功能。 安装 要使用jQuery库,可以从网上下载得到jQuery的 .js 文件,也可以使用CDN (Content Delivery Content 内容分发网络)加载jQuery。 下载 需要去jQuery官网:jquery.com 下载需要的jQuery库,一般有两个版本,production 表示已被压缩精简的版本,用于放到实际网站中,development 表示测试开发版,用于编写和开发,是可读的代码。 例如目前最新的版本是 jquery-3.3.1.js,压缩版后缀是 .min.js,开发版文件有一万多行,就是正常格式的JavaScript源代码,包含一些注释,文件大小为 266k;压缩版就是去掉里面不必要的空格,回车与注释,所以最后文件实际内容只有一行!,文件大小为 85k,压缩了近三倍,这也是网页都使用压缩版,提升网页性能的原因。 下载好后放到网页文件夹中,然后使用 <script> 标签引用,例如: <script scr="/js/jquery-3.3.1.min.js"></script> 路径中填写 .js 文件的实际存放位置。 CDN 使用CDN(内容分发网络)就可以不用下载jQuery文件,优点是可以使用这个机制尽量避开网络中一些影响数据传输的路线,提高访问速度和稳定性。原理就是使用在各处配置的节点服务器,让用户就近获取所需内容。 常见CDN有很多,例如百度、新浪、谷歌、微软等,如果是国内站点的话,建议使用国内CDN,国外站点可以使用谷歌或微软,提高速度。 以百度CDN为例,安装方法如下: <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script> 微软CDN: <script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.3.2.min.js"></script> 微软jQuery历史版本可以从这里查看https://docs.microsoft.com/en-us/aspnet/ajax/cdn/overview#jQuery_Releases_on_the_CDN_0。 更多内容可以访问百度静态资源公共库(http://cdn.code.baidu.com/),其他CDN可以自行百度搜索。 语法 jQuery的基础语法是通过选取(query)文档中的元素,对其进行操作(action),语法是: $(selector).action() 选择器 selector 是选择器,类似于CSS选择器,常见的有: 元素选择器,如: $("p") ID选择器,如:$("#myId") 类选择器,如:$(".myClass") 属性选择器,如:$("[href]") 操作 action() 是对所选元素执行的操作,例如: 隐藏元素:.hide() 单击事件:.click(myFunction()) 双击:.dblclick() 悬停:.hover() 其他语法与JavaScript类似,代码写在 <script> 中,例如隐藏 p 元素: <script> $("p").hide(); </script> 有时需要等文档加载完毕后执行代码,很像JavaScript中的 window.onload: window.onload = function(){ alert(); } jQuery中就要这么写: $(document).ready(function(){ $("p").hide(); }); 简化写法: $(function(){ $("p").hide(); }); 结果都是在整个文档加载完后执行语句。 方法链接 相对于JavaScript,jQuery又一种特殊的操作方法叫做方法链接(chaining),即在一条语句上,对一个元素执行多个操作,语法是: $(selector).action1().action2().action3() 例如: $("p").html("<b>Hello</b>").click(function(){ alert("Hello world"); }); 结果就是改变元素文本内容后绑定点击事件的调用函数,操作可以绑定多个,并且是依次执行,方法类似,其他操作以此类推。

2018/6/1
articleCard.readMore

addEventListener 方法与 on 事件的区别

on事件 Javascript中可以对一些页面的事件设定触发值,例如常用的点击 onclick,鼠标移动 onmousemove,或者移动端屏幕点击 ontouchstart,其它类似的还有 onmousedown, onmouseup, onchange, onfocus, onmouseenter, ontouchmove, ontouchend 等等,可以对其设定值来实现事件触发后执行的操作,例如: <h1 onclick="alert('hello');">Click me</h1> 点击后就会弹出提示框,也可以这样写: <h1 onclick=(function(){alert("hello");})()>Click me</h1> 这样也能实现同样效果,只是它的值变成了一个匿名函数。 addEventListener()方法 这个方法设定一个事件监听器,当某一事件发生通过设定的参数执行操作。语法是: addEventListener(event, function, useCapture) 参数 event 是必须的,表示监听的事件,例如 click, touchstart 等,就是之前不加前缀 on 的事件。 参数 function 也是必须的,表示事件触发后调用的函数,可以是外部定义函数,也可以是匿名函数。 参数 useCapture 是选填的,填true或者false,用于描述事件是冒泡还是**捕获,true表示捕获,默认的false表示冒泡。 移除事件监听 如果要移除 addEventListener() 添加的事件监听,就要使用removeEventListener(),语法是: removeEventListener(event, function) 参数与addEventListener()一致。 兼容性 IE 8及更早的版本,和Opera 7.0及更早的版本,不支持 addEventListener() 和 removeEventListener() 方法,他们使用的是一下方法代替: 添加事件: attachEvent(event, function) 移除事件: detachEvent(event, function) 可以用以下方法解决兼容性问题: <div id="div1">Click me</div> <script> var div1 = document.getElementById("div1"); if (div1.addEventListener) { div1.addEventListener('click', function(){ this.innerHTML = "your browser is compatible with addEventListener!"; }); } else if (div1.attachEvent) { div1.attachEvent('click', function(){ this.innerHTML = "your browser is not compatible with addEventListener!" }); } </script> 冒泡与捕获 这个参数设置的是元素事件的触发顺序,即页面中某元素设置了事件监听,其内部元素也设置有事件监听,冒泡是先触发最内部元素的事件,再依次触发外一层元素的事件,捕获刚好相反,由外到内依次触发。 综合举例: <div id="parent1"> <div id="child1" style="height:200px;background:#0cc"> 冒泡 </div> </div> <div id="parent2"> <div id="child2" style="height:200px;background:#0cc"> 捕获 </div> </div> <script> var parent1 = document.getElementById("parent1"); var child2 = document.getElementById("child1"); var parent2 = document.getElementById("parent2"); var child2 = document.getElementById("child2"); parent1.addEventListener('click', function(){ alert("this is parent element"); }); child1.addEventListener('click', function(){ alert("this is child element"); }); parent2.addEventListener('click', function(){ alert("this is parent element"); }, true); child2.addEventListener('click', function(){ alert("this is child element"); }, true); </script> 点击“冒泡”模块,先提示子元素后提示父元素;点击“捕获”模块,由于同时设置为 true,先提示父元素后提示子元素。 区别 为某元素设定事件触发函数时,可能会觉得addEventListener和on事件的功能差不多,但是,addEventListener除了可以设置元素触发顺序外,还能多次绑定事件,因为 on 事件多次绑定的话会出现覆盖。 举例说明: <div id="div1" style="height:200px;background:#0cc"> Click me </div> <script> var dib1 = document.getElementById("div1"); div1.addEventListener('click', function(){ alert("message1"); }); div1.addEventListener('click', function(){ alert("message2"); }); </script> 结果会依次提示“message1”,“message2“。 但是js这么写的话: div1.onclick = function(){ alert("message1"); }; div1.onclick = function(){ alert("message2"); } 这里就只会提示最后一个“message2”,因为onclick作为对象div1的一个属性,第二次对其进行赋值就会覆盖之前的函数值,这样on事件在某些场合就不适用了。

2018/5/31
articleCard.readMore

JavaScript 闭包详解

变量作用域 首先来了解一下Javascript中变量的作用域,除了常见的普通变量外,对象和函数也是一种变量。变量分为局部变量和全局变量。 局部变量 局部变量就是指在函数内部定义的变量,作用域是函数内部网,此变量通常只能在函数内部访问,和外界是区分开的,所以变量名即使和外部的重复,也是两个独立的变量,不会相互影响。局部变量在函数执行是创建,执行完后销毁。 全局变量 全局变量就是函数外部定义的变量,作用域是网页中的所有脚本和函数,它们都能够访问,全局变量是页面加载时创建,页面关闭后销毁。 综合举例: var a = 0; function fun1() { var a = 1; b = 2; } 这里的 var a = 0; 就是全局变量,var a = 1; 是局部变量,虽然名字重复,但这里是两个独立变量,但是还是不建议出现重复,提高代码可读性;b = 2; 也是全局变量,因为规定函数内部申明的变量,如果不加 var,即会被认为是全局变量,尤其这点需要小心。 闭包 先通俗的总结一下,闭包就是一个可以访问其他函数内部变量的函数,即一个定义在函数内部的函数,也叫内嵌函数。 其次,是闭包的作用,因为通常情况函数内部变量是无法在外部访问的,即全局变量也局部变量的区别,而闭包,就实现了能在外部访问某函数内部变量的功能,让这些变量值始终保存在内存中。 然后,来讲一下如何实现闭包。有以下代码: function fun1() { var a = 1; //定义一个局部变量 function fun2() { //这里的 fun2() 就是闭包 alert(a); //fun2() 是 fun1()的子函数,所以能访问之前定义的局部变量,这个是关键 } return fun2; //然后通过这里,把之前得到的局部变量成功返回到外部去 } fun1(); //正常执行函数 var result = fun1(); //将内部变量传递出去,传给变量 result result(); //执行这个函数实现对局部变量的访问 当然形式不止这一种,万变不离其宗,最后实现的功能是一样的,例如下面的方法也是可行的: function fun1() { var a = 1; return function(){ alert(a); }; } fun1(); var result = fun1(); result(); 注意 也许在很多文章中都能看到这句话“避免滥用闭包”,的确,由于闭包会使一些变量一直保存在内存中,所以如果大量使用的话就会消耗大量内存,影响网页性能。 同时,由于闭包的特性,还会在外部改变函数的内部变量值,有时候这是很危险的,举个例子: function fun1() { var a = 1; function fun2() { a++; console.log(a); } console.log(a); return fun2; } var change = fun1(); // 这里 fun1 函数被正常执行,输出 1 change(); // 在外部执行这个函数后,函数内部变量 a 的值就被改变了,输出 2 change(); // 输出 3,以此类推

2018/5/31
articleCard.readMore

JavaScript 计时器函数用法

Javascript中和大多数语言一样,存在计时函数,使某语句或函数不用立即执行,可以延时设定的时间值之后再执行。 setTimeout() 这个函数表示括号中的代码,延时指定时间后再执行,格式为 setTimeout("function()", time),其中 time 的单位是毫秒。 例如: function fx() { alert(); } setTimeout("fx()", 2000); 也可以写成: setTimoeout(function(){ alert(); }, 2000); 结果就是页面加载完 2 秒后弹出提示框。 clearTimeout() clearTimeout() 方法用于结束 setTimeout() 方法的执行,括号中参数为 setTimeout() 返回的 ID 值。 举例说明: var int1 = setTimeout(function(){alert();}, 5000); clearTimeout(int1); 这样就能终止代码执行,不会弹出提示框。 setInterval() 这个函数表示每隔指定时间间隔执行一次括号中的代码,格式为:setInterval("function()", time), time 单位依然为毫秒。 例如: function fx() { document.write("0"); } setInterval("fx()", 2000); 这里就不要用 alert() 做实验了,后果你懂的 -_- . 同样也能写成: setInterval(function(){ document.write("0"); }, 1000); 效果就是不断输出字符“0”。 clearInterval() 用法与 clearTimeout() 一样,终止 setInterval() 的执行,括号中填 setInterval() 的返回值。 例如: var int2 = setInterval(function()[ document.write("0"); }, 1000); clearInterval(int2); 这样就能终止输出。 注意 有个小问题,用 setTimeout() 举例,假如代码像下面这样写: function fx() { alert(); } setTimeout(fx(), 3000); 相比上面,就是函数第一个参数少了双引号,猜一下后果会怎样…… 后果当然是页面加载后立刻弹出提示框,并不会延时 3 秒。下面的写法也是类似的效果: setTimeout((function(){ alert(); })(), 3000); 原因都一样,无论是语句块 fx() 还是匿名函数 (function(){})(),都是会立刻执行的语句,而加双引号的 "fx()" 和 function(){} 就是当成一个参数传递给了函数 setTimeout(),然后这个参数语句直到 setTimeou() 真正执行时才生效,也就是延时3秒后执行。 函数 setInterval() 的这个性质与 setTimeout() 类似。 拓展 回调函数参数 setTimeout 常见的便是使用两个参数,回调函数和时间,但是它还可以接受更多的参数,作为回调函数调用时传入的参数(可以有多个,按顺序填入即可); 举个例子: setTimeout(function(a, b){ console.log(a + b); }, 3000, 1, 2); // 3 秒后输出: // 3 返回值 setTimeout, setInterval 的返回值都是一个数字,具体值取决于当前环境的分配,每次调用后这个数字会加一,clearTimeout(), clearInterval() 方法传入的参数便是这个数字,只不过平时都是以变量代替; let a = setTimeout(function(){}, 1000); let b = setTimeout(function(){}, 1000); let c = setTimeout(function(){}, 1000); console.log(a, b, c); // 1 (也可能是其他数字) // 2 // 3 clearTimeout(a); // 相当于 clearTiemout(1); clearTimeout(b); // clearTimeout(2) clearTimeout(c); // clearTimeout(3) 时间精准度 setTimeout 会等到当前任务执行完,即使延迟时间已经到了,所以这也是常说 JavaScript 计时器不一定准确的原因所在,存在所用时间大于指定时间的情况; console.log('start'); setTimeout('console.log("time")', 2000); // 该函数执行 5s 左右 function delay() { for (i = 0; i < 1000; i++) { for (j = 0; j < 1000; j++) { for (k = 0; k < 1000; k++) { ; } } } } delay(); console.log('end1'); console.log('end2'); // start // end1 // end2 // time setInterval 的执行间隔 该函数的作用是每隔一定时间执行一遍代码,但是代码的执行时间,被包括在间隔时间内,如果执行时间超过了间隔时间,那么下一次执行会立即执行; function delay() { let m = 3000; let t = new Date().getTime(); while (new Date().getTime() - t <= m) { ; // 使该函数 执行时间为 3s } console.log(new Date().getSeconds()); } console.log(new Date().getSeconds()); setInterval(delay, 2000); 最后的输出结果将是输出间隔变为 3s,而不是设定的 2s,因为 delay() 函数的执行时间超过了 setInterval() 的间隔时间,即间隔时间过了也要等到函数执行完毕,然后下一遍执行就紧接着来,不再有间隔时间,所以就使得最后的结果显示为每隔 3s 输出,其实就是整个 delay 函数的执行时间,间隔时间可以理解为被挤得没有了;

2018/5/24
articleCard.readMore

JavaScript 中语句与函数的执行辨析

Javascript代码中,语句和函数以及匿名函数的执行存在一些区别,所以在编写过程中也存在一些“坑“。 script 中的语句 html文档中的javascript语句是写在 <script></script> 中的,每条语句末尾需要添加分号 ;,表示此条语句执行结束。例如下面的代码: <script> var x = 9; alert(x); </script> 文档加载到这块代码区域时,就会立刻执行这两条语句,即弹出提示框为x的值,但是如果把语句换成函数定义,代码如下: <script> var x = 9; function fx() { alert(x); } fx(); </script> 这时第一行语句会被执行,第二至五行是函数定义,并不会执行这个函数,直到最后一行语句才会真正执行这个定义过的函数。 如果需要立刻执行函数的话,就需要使用匿名函数了。所谓匿名函数,顾名思义,即不会给这个执行的函数定义“函数名”,而且是一个立即执行的语句。格式如下: <script> var x = 9; (function(){ alert(x); })(); </script> 注意代码中的三个括号的位置,以及最后跟的那个分号,表示这是一个立即执行的语句。 当然匿名函数也能传递参数,例如: <script> (function(var x){ alert(x); })(9); </script> 效果和上面一样。 但有时又需要不立即执行的函数,例如: <script> setTimeout(function(){ var x = 9; alert(x); }, 2000); </script> 效果与下面代码一样: <script> var x = 9; setTimeout("alert(x)", 2000); </script> 这是Javascript中的延时函数,表示2秒后弹出提示。setTimeout() 自身就是一个函数,里面的 “alert(x)” 是这个函数的一个参数,即一个加引号的语句。便于理解,可以写成这样: setTimeout("alert();", 2000); 这样写并不会出错。 所以这个函数 setTimeout() 的参数是一个不用立即执行的匿名函数 function(){},也可以是一个语句块,从而进行参数传递。 通俗讲,这里加引号的语句块相当于不加引号的匿名函数。 标签属性中的语句 在 html 标签中也能使用语句,通常用于设置元素的属性。 先对比区分以下代码: <script> function fx() { alert(); } </script> <button onclick="alert()">Button1</button> <button onclick="fx()">Button2</button> <button onclick=fx()>Button3</button> <button onclick=(function(){alert();})()>Button4</button> <button onclick="(function(){alert();})()">Button5</button> <button onclick=function(){alert();}>Button6</button> <button onclick="function(){alert();}">Button7</button> 猜一下哪个按钮点击无效…… 答案是最后的“Button6”和“Button7”,然后就能发现规律了,属性所设置的值必须是能立即执行的语句块、函数或匿名函数,最后那两种情况是行不通的。

2018/5/23
articleCard.readMore

web 浏览器进化简史

发展史与时间线 1989年,科学家Tim-Berners-Lee发明了World Wide Web(万维网)。 1990年,他开发了世界上第一款网页浏览器,为避免与万维网混淆,改名为NEXUS,但不支持图片。 1993年,伊利诺大学的NCSA组织创造了第一款可显示图片的浏览器,Mosaic(马赛克) 1994年10月在麻省理工学院计算机科学实验室成立万维网联盟。建立者正是万维网的发明者蒂姆·伯纳斯·李。万维网联盟(World Wide Web Consortium,简称W3C)。 1994年,Mozilla出现了。不过,鉴于当时 Mosaic 的权势。为了避嫌,最终改名成了Netscape Navigator(网景公司开发的网络浏览器)。凭借着html框架显示等新特性,很快成为了新的霸主。 1995年微软发布了跟系统“捆绑”的浏览器Internet Explorer(IE),凭借着操作系统的占有率,IE就把Netscape挤下了霸主宝座。 和 IE 差不多时间诞生,还有一直不温不火的 Opera。 NetScape并未放弃,围绕着浏览器引擎衍生出了人们熟知 Firefox。 2003年,苹果推出的 Safari。 2008年 Google 携 Chrome 参战,让 IE 逐渐失利。 2015年,微软为了改变局面,推出了 Edge。 但 Chrome 以及类似内核的浏览器依旧是主流。

2018/5/22
articleCard.readMore

CSS 样式之内容居中方法

水平居中 HTML中要实现某一内容水平居中显示,要通过设置css样式来实现,主要分为行内元素和块状元素两种情况,块状元素又可分为块状定宽与块状不定宽两种情况,接下来依次介绍分析。 行内元素 像 a、span、i 这类元素叫做行内元素,文本和图片也是行内元素。行内元素水平居中方法简单,只需要给行内元素的父元素设置 text-align: center; css样式就内实现内容水平居中,例如: html: <div class = "txt">居中内容</div> css: .txt { text-align: center; } 文本内容的父元素就是 div ,这样就内实现水平居中,效果如下: 居中内容 块状定宽元素 常见块状元素有 div, p, h 等,定宽即为其设置固定宽度值 width,这时我们可以为元素设置 margin-left 和 margin-right 来实现水平居中,也可以简写为 margin: 0 auto;,例如: html: <div class = "txt">居中内容</div> css: .txt { width: 100px; margin: 10px auto; } 实现效果如下: 居中内容 块状不定宽 有时候我们不能限制块状元素的宽度,就是块状不定宽元素,主要有三种方法,接下来一次介绍。 1、加入 table 标签 利用 table 标签的长度自适应性,长度根据内容自动调整,然后通过设置 margin: auto; 实现水平居中,例如: html: <table> <tbody> <tr> <td> <div class = "txt">居中内容</div> </td> </tr> </tbody> </table> css: .txt { margin: auto; } 效果如下: 居中内容 2、设置为行内元素 就是通过设置 display: inline; 将块状元素设置为行内元素,然后就是像行内元素一样设置 text-align: center; 来是内容水平居中,例如: html: <div class = "txt">居中内容</div> css: .txt { display: inline; text-align: center; } 效果为: 居中内容 注: 使用这种方法虽然可以不用像table增加无语义标签,但是改变了display,所以会少了一些功能,例如不能设置宽度。 3、设置浮动和相对定位 这种方法设置就相对复杂,同时设置浮动和相对定位来实现元素的水平居中。 首先设置父元素: float: left; position: relative; left: 50%; 然后设置子元素: position: relative; left: -50%; 通过代码应该好理解,就是通过50%那个关键位置来实现水平居中效果,因为50%是界面的中央位置,将父元素右移,直到左边框移到中线位置,在将子元素向左移,这样子元素不就居中了吗。 这里要注意的是分别设置父元素的 50% 和子元素的 -50%。 实现效果如下: 居中内容 垂直居中 说完水平居中接着说垂直居中,这里主要又分为两种情况:父元素高度确定的单行文本 和父元素高度确定的多行文本。 单行文本 对于父元素高度确定的单行文本,可以通过设置父元素的 height 和 line-height 高度一致来实现。 这里可以这样理解,height 是元素的高度,例如文本字体的高度,line-height 是行高,例如文本的行间距,一行文本中,行间距被分为两部分,分别位于这行文本的顶部和底部(因为行间距是两行之间的距离),所以设置 height 和 line-height 一样大的话,line-height 就被均分为两部分,分别位于元素顶部和底部,这样中间设置为 height 的元素不就实现垂直居中了吗 ^_^ . 例如: html: <div class = "txt">居中内容</div> css: .txt { height: 200px; line-height: 200px; } 效果如下: 这里需要注意的是关键词“单行文本”,如果使用这种方法但是一行文本超过宽度限制的话,某些内容就会脱离元素块,子元素有多行的话,这几行就会并排居中,并保持设置的行高。 多行文本 对于父元素高度确定的文本和图片等内容设置垂直居中,主要有两种方法。 1、使用 table 标签 对元素使用table标签,包括 tbody,tr,td,然后对父元素设置 vertical-align: middle; 样式,就能使 inline-block 类型的子元素垂直居中显示。 因为 td 标签默认设置了 vertical-align: middle,所以也可以不用单独设置 vertical-align。 例如: html: <table> <tbody> <tr> <td class="txt"> <p>居中内容</p> <p>居中内容</p> <p>居中内容</p> </td> </tr> </tbody> </table> css: .txt { height: 300px; background-color: #ccc; } 这里的父元素就是 td,父元素的高度必须确定,就要为其设置 height。 注意这里的 p 元素是 inline 类型的,所以设置 vertical-align: middle 的话会出现错误,若果是图片元素 img 的话,就可以设置 vertical-align: middle,但是由于 td 标签默认,所以都可以不写。 效果如下: 2、设置 table-cell 第二种方法是把要垂直居中显示的元素的父元素设置为table-cell (表格单元)类型:display: table-cell;,然后设置 vertical-align: middle就能实现元素垂直居中。 但是这个方法存在兼容性问题,chrome, firefox, IE8以上才支持这个操作。 例如: html: <div class="txt"> <p>居中内容</p> <p>居中内容</p> <p>居中内容</p> </div> css: .txt { height: 300px; background-color: #ccc; display: table-cell; vertical-align: middle; } 同样,要为父元素 div 设置高度 height 效果如下: 这种方法除了兼容性问题外,同时也改变了 display类型,会在某些方面带来不便。

2018/5/22
articleCard.readMore

CSS 布局模型详细介绍

HTML中元素有三种布局模型:流动模型、浮动模型、层模型。 流动模型(flow) HTML网页默认布局就是流动模型,布局如下: 块级元素(block)自上而下垂直分布,因为块级元素默认宽度为浏览器窗口的100%,或者理解为每个块级元素默认占一行。常见块级元素有 div, p, h 等; 内联元素(inline)从左到右水平分布,即不像块级元素那样每个独占一行。常见内联元素有 a, span, em 等。 浮动模型(float) 上面提到的块级元素是每个独占一行显示,但是定义css浮动模型后就能使两个块级元素并排一行显示。 例如HTML代码: <div id = "div1"> <p>Hello</p> </div> <div id = "div2"> <p>World !</p> </div> 显示结果是这样: Hello World ! 但是设置浮动css后: div { float: left; } 效果就是这样: HelloWorld ! 也可以设置元素一左一右显示: #div1 { float: left; } #div2 { float: right; } 层模型(layer)(position) 类似于PS中的图层编辑,HTML中也存在层模型布局,对元素进行定位。 层模型有三种:绝对定位(absolute)、相对定位(relative)、固定定位(fixed)。 绝对定位 理解就是字面上的意思,简言之就是相对于上级设置了 position 属性的元素进行定位,如果没有这类上级就是相对于 body 标签,也是浏览器窗口。需要设置css:position: absolute;,然后就可以使用 top, right, bottom, left 这类属性进行定位。例如: div { position: absolute; top: 100px; left: 150px; } 这样就使板块向下移动100像素,向右移动150像素。 相对定位 这里的相对较难理解,与数理中的“相对”不太一样,这里是“相对于自己原来应在的位置”,需要设置css:position: relative;,重要的是不用关心上级是否设置了position属性,这样就很方便。例如: div { position: relative; top: 100px; right: 100px; } 板块就相对于自己没设置样式前的位置,同时向左向下移动100px。 固定定位 这个就好理解了,所谓固定就是指固定于整个浏览器网页窗口不动,即使滚动网页内容也不改变位置,需要设置css:position: fixed,也可以设置 top, right等调整固定的位置。还记得浏览器某些网页右下角的小广告吗,是不是固定在那怎么浏览网页都不动 -_- .

2018/5/4
articleCard.readMore

CSS 选择器详细介绍

文章出自个人博客https://knightyun.github.io/2018/05/02/css-selector,转载请申明 目录 基础 选择器 元素选择器 类选择器 ID选择器 ID选择器与类选择器的区别 子选择器 后代选择器 通用选择器 属性选择器 简单属性选择器 属性和值选择器 伪元素选择器 :active :hover :focus ::selection :first-child :nth-child() 组选择器 相邻同级选择器 基础 CSS(层叠样式表Cascading Style Sheets),用于修饰HTML网页内容,根据使用位置不同可分为三种样式:内联式,嵌入式,外部式。 内联式:标签 <>内部使用,例如: <p style = "color: red; font-size: 20px"></p>。 嵌入式:写在 <style></style> 之中,并放在 <head></head> 内,例如: <head> <!--这里放其它标签,例如 meta,link,script之类--> <style> p { color: red; font-size: 20px; } </style> </head> 外部式:写在外部 .css 文件中,使用如下方式引用: <link rel = "stylesheet" type="text/css" href="css/index.css"> href 写 .css 文件路径,可以是绝对路径或相对路径,相对路径类似于:../../css/index.css,绝对路径类似于:/css/index.css。 选择器(Selector) 三种样式中,嵌入式和外部式需要使用到选择器,也是组成 css 样式的主体例如上例中的 p { },主要分为: 元素选择器、类选择器、ID选择器、子选择器、后代选择器、通用选择器、属性选择器、伪元素选择器、组选择器、相邻同级选择器。 元素选择器(Type selector) 也叫类型选择器,可以理解为标签选择器,最基本的选择器,就是使用常见的HTML元素,例如:body { }, h { }, p { }, div { }, span { }, a { } 等。 类选择器(Class selector) 前提需要在标签内使用类标记某处文档,类似:class = "myClass",然后它的选择器的格式就为:.myClass { },就是在类名前面加个小数点。 ID选择器(ID selecctor) 和类选择器类似,前提需要在标签内使用ID标记某处文档,类似:id = "myId",格式为:#myId { },就是在 ID 前加个符号 #。 ID选择器与类选择器的区别: 类选择器可以使用多次,ID选择器只能使用一次,例如: <p class = "one">This is a test content, </p> <p class = "one">hello world !</p>,但是ID不能这样。 并且能同时使用多个类分别标记不同样式,例如:<p class = "one two"></p>,ID也不能这样。 子选择器(Child selector) 用于指定标签元素的子元素,使用符号 > 隔开父元素与子元素,例如: HTML: <p1> Hello World !!! <p2>This is a test content</p2> </p1> CSS: p1>p2 { color: green; font-size: 20px; } 这里就指定了父元素 p1 的子元素 p2 的样式,但是只作用于子元素,不作用于父元素。 后代选择器(Descendant selector) 用于指定标签元素的后代元素,使用空格符号隔开,例如: html: <p1> Hello world !!! <p2> This is a <a>test</a> content </p2> </p1> css: p1 a { color: green; font-size: 20px; } 这里指定了元素 p1 的后代元素 a 的样式,注意这里不是子代元素 p2,就是作用于所有指明的后代元素。 子代选择器与后代选择器的区别: 顾名思义的理解,后代就是包含子代在内的所有下代的元素,可以跨越子代直接作用于孙代;而子代只包含父级的第一代子代元素。 子代选择器使用符号 > 隔开,后代选择器使用空格 隔开。 通用选择器 顾名思义,使用通配符 * 设置 html 中所有标签的样式,例如: * { color: red; font-size: 20px; } 这样就设置了HTML中所有的标签的样式了。 属性选择器(Attribute selector) 对具有指定的属性(attribute)设置样式,使用方括号符号 [ ]。 简单属性选择器 不用管属性值,例如: [href] {color: red} a [href] {color: red} a [href] [title] {color: red} 以上格式都能实现相同效果,即具有该属性的 a 标签。 属性和值选择器 具有指定属性与其指定值的标签,格式为: a [title = "link"] {color: red} 伪元素选择器 HTML中存在一类与元素控制内容相同的抽象元素,但是并不实际存在于HTML文档,给标签的某种状态设置样式,例如单击某内容或鼠标滑过某内容,然后设置改变的样式。伪元素种类较多,只列举几个常用例子。 :active 为激活的元素设置样式,就是用户单击该标签时的样式,例如: a:active {color: red} 则用户点击这个链接文本时颜色变为红色。 :hover 悬停状态伪元素,为用户鼠标所停靠的标签设置样式,例如: p:hover {color: red} 则鼠标停留在该段落时,段落内容变为红色。 :focus 用于具有焦点的元素,常用就是输入框,用户点击输入框准备输入时则该输入框就具有了焦点,例如: input:focus {background-color: green} 则点击输入框时背景颜色变为绿色。 ::selection 为选中的元素设置样式,例如: ::selection {color: red} 为文档中鼠标选定的内容设置为红色字体。注意可以追加应用范围:p::selection 表示段落中选中的字体才应用该样式,不追加直接使用 ::selection 表示应用于所有内容。 火狐浏览器支持需要使用 -moz-selection。 :first-child 为元素的第一子代应用样式,例如: p:first-child {color: red} 这个比较好理解。 :nth-child() 为元素的父元素的第 n 个子代设置样式,只是括号内需要输入数字表示第几代,例如: p:nth-child(2) {color: red} 假如p标签父元素是body,就表示为body的第二个子元素设置样式。 组选择器 为多个元素设置相同样式时,可以使用逗号分隔元素,达到同时设置的效果。例如: p1,p1 {color: red} 表示为p1和p2 同时设置相同的样式。 相邻同级选择器 选择与指定元素同级的相邻的第一个某元素,设置样式,例如: div+p {color: red} 表示为 p 设置样式,并且这个 p 与 div 同级,并且是与 div 相邻的第一个 p,div 内部的 p 并不包含在内。 返回顶部

2018/5/2
articleCard.readMore

C 语言之变量存储类型与链接属性

C语言中一个重要的东西就是弄清申明变量的类型、作用域、存储类型、链接属性等,例如是整型还是浮点型,存储于普通内存还是堆栈或者寄存器,作用于全局还是局部,能否被其他文件 引用等。 链接属性 申明变量或函数时需要标识符,标识符的链接属性一共有三种:external(外部), internal(内部), none(无),external和internal常用,none表示无链接属性,该标识符的多个申明被当成独立不同的实体。 顾名思义,external表示能被其它源文件访问的变量或函数,internal则不能被其它源文件访问,并且缺省情况下代码块(block)外部的变量为external属性,也就是外部变量,代码块内部的变量为internal属性,即局部变量. 如果需要改变链接属性需要使用 extern 和 static 关键字,extern 是转化为external属性,static 是转化为internal属性。 举例说明: #include<stdio.h> int a = 1; /* 这里是代码块外部,external 属性,缺省为"extern" */ extern int aa = 11; /* 与上面效果一样 */ static int aaa = 111; /* 这里申明的是 internal 属性,不能被其它源文件访问 */ void example() /* 这里申明的 example() 函数也是external属性,能被其它源文件访问 */ { int b = 2; /* 这里是代码块内部,internal 属性 */ extern int bb = 22; /* 这里的意思是访问其它源文件的全局变量 */ } 注意上面指出的代码块内部和外部,定义的函数的花括号之外叫做代码块外部,之内叫做代码块内部。 代码块外部缺省为extern,并且代码块外部使用extern表示被其它源文件访问,代码块内部使用extern表示访问其它文件的外部变量。 存储类型 存储类型指申明的变量将被存储到什么地方去,并且与其存储周期有关,就是这个变量何时被创建,何时被销毁,保持多久。 存储类型有三种: 静态变量(static) —> 存储于普通内存。 自动变量(auto) —> 存储于堆栈。 寄存器变量(register) —> 存储于寄存器。 静态变量 任何代码块之外定义的变量,总是处于静态内存中,无需使用 static 关键字,在程序运行之前创建,程序整个执行期间始终存在。 自动变量 代码块内部申明的变量,并且缺省情况下都是(auto)类型,所以很少使用,存储于堆栈中,程序执行到申明变量处被创建,离开后被销毁,每次在堆栈中占据的内存位置都可能不同。 寄存器变量 用于申明自动变量,即在代码块内部使用,使这类变量存储于寄存器中,寄存器中的变量比内存中的变量访问效率更高。 但是,如果有太多变量申明为 register ,只会选取前几个存储于寄存器中,其它处理为普通自动变量。 通常把使用频率最高的变量申明为寄存器变量,或者指针申明为寄存器变量,以提高效率。例如可以把函数的形参申明为寄存器变量(有可能它节省的时间空间开销抵不上复制这几个值的开销)。 综合举例 #include<stdio.h> int a = 1; /* 静态变量(static),这里不能使用 register */ int main() { int b = 2; /* 自动变量,省略 auto */ register int c = 3; /* 寄存器变量(register) */ int aaa(register int d) /* 申明函数形参为 register */ static int e = 4; /* 申明静态变量,和变量 b 一个属性 */ } 初始化 静态变量在不指定初始值的时候,初始化为 0; 自动变量是否初始化赋值,并无效率的改变(每次执行都要重新初始化),这也是它的优点:可以用任何“表达式”作为初始值。例如: int add(int a) { int b = a + 1; /* 将表达式 a + 1 的值初始化给 b */ return b; } 注意 static 申明函数或代码块之外的变量时,只修改链接属性 ``external 为 internal`,存储类型和作用域不变。 static 申明代码块内部变量时,将自动变量修改为静态变量,但是链接属性和作用域不变。 综合举例 #include<stdio.h> int a = 1; /* 链接属性为 external,缺省 extern */ static int b = 2; /* 修改连接属性为静态变量,不能被其它源文件访问,依然为全局变量,存储于静态内存中 */ int main() { int c = 3; /* 自动变量,存储到堆栈 */ static int d = 4; /* 静态变量,存储到普通内存,依然不能被其它源文件访问 */ }

2018/4/28
articleCard.readMore

C 语言之随机数函数(rand())的使用方法

在程序设计中,难免会使用到随机值函数,其原理与语法大多类似,接下来以C语言为例介绍其随机值函数 rand() 用法。 原理 引用百度百科,首先,需要包含头文件: #include <stdlib.h> rand()函数是按指定的顺序来产生整数,因此每次执行上面的语句都打印相同的两个值,所以说C语言的随机并不是真正意义上的随机,有时候也叫[伪随机数][wei],使用 `rand()` 生成随机数之前需要用随机发生器的初始化函数 `srand(unsigned seed)`(也位于 `stdlib.h` 中) 进行伪随机数序列初始化,`seed` 又叫[随机种子][seed],通俗讲就是,如果每次提供的 `seed` 是一样的话,最后每一轮生成的几个随机值也都是一样的,因此叫伪随机数,所以需要每次提供不同的 `seed` 达到完全的随机,我们通常用时间函数 `time(NULL)` 作为 `seed` ,因为时间值每秒都不同,这个函数需要包含以下头文件: #include <time.h> 理论太泛,下面用例子分析理解。 举例分析 先来理解以下伪随机数,编译以下代码: #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand(1); int i; for (i = 0; i < 10; i++) printf("%d, ", rand()%11); } 运行结果 8, 9, 9, 1, 7, 5, 5, 10, 1, 0, 然后无论运行多少次,结果都依然是以上随机数,不会改变,因为每次设置的种子 seed 都是 1 。 但是假如把 seed 换成 time(NULL),每次就不一样了,如下: #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand(time(NULL)); int i; for (i = 0; i < 10; i++) printf("%d, ", rand()%11); } 结果是就变了,并且每次都不一样: 6, 3, 4, 5, 5, 9, 8, 10, 10, 4, 6, 4, 2, 4, 3, 2, 5, 1, 2, 9, 这里的 time(NULL) 的结果是一个类似于 1524655706 的数字,并且每秒都在递增 1,也就达成了 srand() 的 seed 不断变化的目的,不断生成新的随机数。 拓展 这里注意一下例子中函数 rand() 的用法,函数括号内不需要加参数,如果直接调用 rand() 的话会生成下面这样的数: 17163, 2663, 24810, 4875, 26975, 14119, 22193, 11233, 26009, 20105, 所以我们想要生成指定范围的随机数的话就需要使用到**求余**运算符 `%`,这里有个规律:例如我们需要 0--10的随机数时,就写成 `rand()%11`,0--100就写成 `rand()%101`,就是运算符后的数字需要比需求范围极值大 1,当然这也是取余运算的原理。

2018/4/25
articleCard.readMore

贪吃蛇 C 语言源码与算法分析

经典的贪吃蛇游戏算法,无疑是一个较大的挑战,综合性较高,像我这种刚入门C语言的也差不多花了整整一周时间才差不多理解透彻,内部包含了较多的函数,数组,二维数组,循环等思想。 Github项目地址:https://github.com/knightyun/gluttonousSnake/ 接下来以C语言为例,针对此算法截取代码片段进行详细分析,源码位于文章底部。 算法分析 概述 首先分析一下,贪吃蛇最基本和重要的动作,一段在屏幕上移动和转向的躯干,但是C语言没有移动字符的函数,只能不断向屏幕打印输出和清屏实现移动,躯干位置在屏幕上的变化可以用二维坐标系实现,用一个二维数组保存屏幕所有可见内容的x,y坐标,并赋予几种初始值,然后用函数打印出各种值对应的字符,再用循环和坐标值自增自减实现移动。使用输入函数判断方向,随机函数生成食物,根据头部坐标判断撞墙或吃到自己而结束游戏。 头文件 Windows环境中需要包含的头文件: #include<stdio.h> #include<stdlib.h> #include<Windows.h> /*需要使用system("cls")清屏函数*/ #include<conio.h> /*非标准库函数,VS中自带,需要使用_getch()函数获取输入*/ #include<time.h> /*使用随机数函数需要使用time()函数*/ 预定义 游戏内可自定义设置界面宽度与高度,需要给二维数组定义一个最大值: #define MAXX 10000 /* 定义游戏最大界面宽度坐标值 */ #define MAXY 10000 /* 定义最大高度 */ 申明变量 需要用到的变量和功能如下: int speed = 10; /* 设置蛇移动速度 */ int mapArr[MAXX][MAXY]; /* 保存界面坐标值的二维数组 */ int inputX = 50, inputY = 20; /* 默认游戏界面宽度和高度 */ int randX = -1, randY = -1; /* 生成的随机食物的坐标值 */ int foodFlag = 0; /* 判断是否更新食物 */ int sx = 1, sy = 1; /* 设置蛇身坐标值 */ int l = 0; /* 蛇身长度 */ int *body[MAXX * MAXY]; /* 储存蛇身的指针数组 */ char input = '6'; /* 默认移动方向 */ int overFlag = 1; /* 判断游戏结束,撞墙或吃到自己 */ int moveX = 1, moveY = 1; /* 游戏开始的动画效果坐标值 */ int moveFlag = 0; /* 开始动画的循环判断 */ 这里使用了指针数组控制蛇身,方便赋值 数组中的参数虽然不能是变量,但是可以是宏定义 函数 接下来就是重要环节了,分析实现游戏效果的各个函数。 第一步,定义初始化函数 InitMap() 将屏幕上每个点通过二维数组赋予坐标值 x、y,确定游戏界面的大小,我们将四周的墙赋值为 1,中间空白赋值为 0,蛇身赋值为 2,随机出现的食物赋值为 3。 第二步,定义 PrintMap() 函数给每个 坐标值打印对应的字符,我们将墙用字符 + 表示,空白用空字符 " " 表示,蛇身用星号 * 表示,食物用字符 @ 表示。 第三步,定义函数 StartMsg() 显示屏幕信息,提示控制的按键。 第四步,定义函数 GetSet() 使玩家可以自定义游戏界面宽和高和移动速度,使用 scanf() 函数获取并改变默认界面尺寸。 第五步,定义函数 SetRandNum() 在界面中随机出现食物,并且不与蛇身和墙重叠,使用 srand(time(0)) 初始随机函数,然后用一个循环不断用随机函数 rand() 生成随机坐标值,直到所生成位置是空白为止。 第六步,最核心的算法,较为复杂,定义函数 SetSnakeNum() 设置蛇身的坐标值,并通过通过输入判断前进方向,以设置的速度时间间隔不断自增或自减坐标值实现移动,里面用到了 _kbhit() 函数,作用是:有用户输入时,返回值为真,无输入时值为假。还有 _getch() 函数与 getchar() 的区别:_getch() 输入值后不用输入回车就能获取输入值。感觉最不好理解的就是蛇身的转弯算法,我的方法是:蛇身每一节在每一次循环不断继承前一节的值,然后蛇头位置不断获得新坐标值,这样就能实现身体的转向,这里就可以用到之前定义的指针数组 *body[] 来实现。算法中还需要注意的一点是,蛇身朝某个方向移动时,只能控制另外两个方向,例如向右移动时不能控制向左移动。然后随后的函数就好说了。 第七步,定义函数 EatFood() 实现遇到食物坐标值时,增加一节蛇身长度值 l 。 第八步,定义函数 StartGame() 来综合之前的函数并开始游戏,需要通过 overFlag 判断游戏结束。 第九步,定义函数 SetMoveNum() 实现游戏开始时的动画效果,对于游戏存在意义不大,仅供研究训练思维和算法。 第十步,定义函数 JudgeEnd() 判断之前的函数 SetMuveNum() 的结束时刻,然后不断循环动画效果。 第十一步,定义函数 StartView() 开始游戏,按任意键游戏正式开始。 下面是函数申明: void InitMap(); /* initialize the background coordinate system */ void PrintMap(); /* print every point in the arr mapArr to the screen */ void StartMsg(); /* start message */ void GetSet(); /* judge whether to edit the game setting */ void SetRandNum(); /* set a random 'food' point in the screen */ void SetSnakeNum(); /* the most complex and important algorithm of this game */ void EatFood(); /* judge when to eat food and elongate the body */ void StartGame(); /* start the game */ void SetMoveNum(); /* algorithm of the start animation, some complex */ void JudgeEnd(); /* judge the end of the animation and loop again*/ void StartView(); /* start the start animation view */ 源码 #include<stdio.h> #include<stdlib.h> #include<Windows.h> /* use the function 'system("cls")' to clear screen */ #include<conio.h> /* use the function '_getch()' to get input */ #include<time.h> /* the random function need the 'time()' function */ #include "public-fun.h" #define MAXX 10000 /* define the max width of game space */ #define MAXY 10000 /* define the max height */ void InitMap(); /* initialize the background coordinate system */ void PrintMap(); /* print every point in the arr mapArr to the screen */ void StartMsg(); /* start message */ void GetSet(); /* judge whether to edit the game setting */ void SetRandNum(); /* set a random 'food' point in the screen */ void SetSnakeNum(); /* the most complex and important algorithm of this game */ void EatFood(); /* judge when to eat food and elongate the body */ void StartGame(); /* start the game */ void SetMoveNum(); /* algorithm of the start animation, some complex */ void JudgeEnd(); /* judge the end of the animation and loop again*/ void StartView(); /* start the start animation view */ int speed = 10; /* default snake move speed */ int mapArr[MAXX][MAXY]; /* arr to store the point in the screen */ int inputX = 50, inputY = 20; /* default width and height */ int randX = -1, randY = -1; /* set a random food point in the background */ int foodFlag = 0; /* judge when to change random food point*/ int sx = 1, sy = 1; /* body x, y point */ int l = 0; /* body lenth */ int *body[MAXX * MAXY]; /* body array pointer */ char input = '6'; /* default direction */ int overFlag = 1; /* judge when the game over, hit wall or eat self*/ int moveX = 1, moveY = 1; /* start move effect */ int moveFlag = 0; /* restart the loop effection */ void GetSet() { printf("\n"); printf("请输入游戏空间的宽度:\n(Please enter the width of game space:)\n"); scanf_s("%d", &inputX); printf("\n"); printf("请输入游戏空间的高度:\n(Please enter the height of game space:)\n"); scanf_s("%d", &inputY); printf("\n"); printf("请输入游戏速度(1 到 n,1 最慢):\nPlease enter the speed of snake moving:(1 is slowest)\n"); scanf_s("%d", &speed); } void InitMap() { int x, y; for (y = 0; y < inputY; y++) { for (x = 0; x < inputX; x++) { if ((x == 0) || (x == inputX - 1) || (y == 0) || (y == inputY - 1)) { mapArr[x][y] = 1; } else { mapArr[x][y] = 0; } } } } void PrintMap() { int x, y; for (y = 0; y < inputY; y++) { for (x = 0; x < inputX; x++) { switch (mapArr[x][y]) { case 0: printf(" "); break; case 1: printf("+"); break; case 2: printf("*"); break; case 3: printf("@"); } } printf("\n"); } } void StartMsg() { printf ("'2(top)', '8(down)', '4(left)', 6(right)' 或 \n'w(top)', 'a(left)', 's(down)', 'd(right)'\n控制方向(control the direction)\n"); } void SetRandNum() { srand(time(0)); while ((mapArr[randX + 1][randY + 1] != 0) && (foodFlag == 0)) { randX = rand() % (inputX - 2), randY = rand() % (inputY - 2); } mapArr[randX + 1][randY + 1] = 3; /* set foot number 3 */ foodFlag = 1; } void SetSnakeNum() { if (_kbhit()) /* if there is an input, get it; if not, go on */ { int a = _getch(); switch (input) { case '2': case 'w': if (a == '4' || a == '6' || a == 'a' || a == 'd' || a == '2' || a == 'w') input = a; break; case '8': case 's': if (a == '4' || a == '6' || a == 'a' || a == 'd' || a == '8' || a == 's') input = a; break; case '4': case 'a': if (a == '2' || a == '8' || a == 'w' || a == 's' || a == '4' || a == 'a') input = a; break; case '6': case 'd': if (a == '2' || a == '8' || a == 'w' || a == 's' || a == '6' || a == 'd') input = a; break; } } switch (input) /* judge the direction by value of input */ { case '2': /* up */ case 'w': sy--; break; case '8': /* down */ case 's': sy++; break; case '4': /* left */ case 'a': sx--; break; case '6': /* right */ case 'd': sx++; break; } int i; for (i = l; i != 0; i--) /* every point's address of body move back one point */ { body[i] = body[i - 1]; *body[i] = 2; /* change value by pointer */ } body[0] = &mapArr[sx][sy]; if ((*body[0] == 1) || (*body[0] == 2)) /* judge when the snake hit the wall or eat itself */ { overFlag = 0; } *body[0] = 2; /* assign the head of snake by pointer */ } void EatFood() { if (*body[0] == 3) { l++; foodFlag = 0; } } void StartGame() { sx = 1; sy = 1; l = 0; input = '6'; int j; for (j = 0; j < l; j++) /* assign the snake body initial address value*/ { body[j] = &mapArr[sx - j][sy]; } while (overFlag) /* loop until the game over */ { InitMap(); SetSnakeNum(); SetRandNum(); EatFood(); PrintMap(); StartMsg(); Sleep(1000/speed); system("cls"); } } void SetMoveNum() { /* x move 1 -- (X - 2 ); y move 1 -- (Y - 2) */ /* move x from left to right */ if ((moveY == 1 + moveFlag) && (moveX < inputX - 2 - moveFlag)) { mapArr[moveX][moveY] = 2; moveX++; } /* move y from top to buttom */ else if ((moveX == inputX - 2 - moveFlag) && (moveY < inputY - 2 - moveFlag)) { mapArr[moveX][moveY] = 2; moveY++; } /* move x from right to left */ else if ((moveY == inputY - 2 - moveFlag) && (moveX > 1 + moveFlag)) { mapArr[moveX][moveY] = 2; moveX--; } /* move y from buttom to top */ else if ((moveX == 1 + moveFlag) && (moveY > 1 + moveFlag)) { mapArr[moveX][moveY] = 2; moveY--; if (moveY == 2 + moveFlag) /* judge when to jump to a deeper layer */ { moveFlag++; } } } void JudgeEnd() { int i, j; int tmp = 1; for (j = 0; j < inputY; j++) { for (i = 0; i < inputX; i++) { if (mapArr[i][j] == 0) goto out; } } moveX = 1, moveY = 1; InitMap(); moveFlag = 0; out:; } void StartView() { moveX = 1, moveY = 1, moveFlag = 0; int startFlag = 1; InitMap(); while (startFlag) { SetMoveNum(); PrintMap(); printf("按任意键开始游戏:\n(Press any key to start game: )\n"); Sleep(10); system("cls"); JudgeEnd(); if (_kbhit()) { int c = _getch(); if ((c != '2') && (c != 'w') && (c != '8') && (c != 's') && (c != '4') && (c != 'a') && (c != '6') && (c != 'd')) startFlag = 0; } } } int main() /* main function */ { while (1) { printf("是否修改设置(修改输入“y”,否则按任意键):\nEdit the game setting or not ? (Press 'y' to edit, or press another key to go on:)\n"); if (_getch() == 'y') { GetSet(); } StartView(); /* an animation before game start */ StartGame(); printf("Game Over !!!\n游戏结束,按任意键继续:\n(Press any key to restart: )\n"); _getch(); overFlag = 1; /* restart the game by the flag */ system("cls"); } } 有更简单的算法欢迎评论指正! 返回顶部

2018/4/24
articleCard.readMore

Kali Linux 之软件安装、卸载、更新和修改更新源

使用Linux系统,与Windows系统一样,也需要及时进行软件与系统的更新。 软件 这里以 Kali Linux 系统为例,介绍常用的软件安装、卸载与更新命令: 软件安装 安装前先搜索一下更新源中是否有该软件,这里使用 apt 命令,貌似比另外一个类似的命令 apt-get 友好一些。 例如安装 leafpad 这个软件: apt search leafpad 然后安装这个软件: apt install leafpad 然后确定安装就行了。 有时会出现一些 failed 可以按照提示使用命令 apt install --fix-missing 修复。 软件卸载 简单卸载软件: apt remove leafpad 卸载软件并移除配置文件: apt-get purge leafpad 卸载自动安装并且未使用的软件 apt autoremove 软件更新 先更新一下源: apt update 这个操作并没有开始更新软件,类似于将远程源中的最新版本信息更新到本地 接下来才开始更新软件: apt upgrade 更新系统: apt full-upgrade 或者: apt-get dist-upgrade 清理安装包: apt-get clean apt-get autoclean 更新源 Linux 更新源文件位于 /etc/apt/sources.list ,系统就是从这个文件中读取一些网址参数下载安装软件,默认的是 kali 官方源,我们可以修改为国内一些较快的源,例如阿里、中科大、网易等,加快下载速度。 找到并编辑上述 sources.list 源文件,替换为以下内容: #aliyun 阿里云 deb http://mirrors.aliyun.com/kali kali-rolling main non-free contrib deb-src http://mirrors.aliyun.com/kali kali-rolling main non-free contrib # ustc 中科大 deb http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib deb-src http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib #deb http://mirrors.ustc.edu.cn/kali-security kali-current/updates main contrib non-free #deb-src http://mirrors.ustc.edu.cn/kali-security kali-current/updates main contrib non-free # kali 官方源 deb http://http.kali.org/kali kali-rolling main non-free contrib deb-src http://http.kali.org/kali kali-rolling main non-free contrib # 默认的,可以注释掉不用管 #deb http://security.kali.org/kali-security kali-rolling/updates main contrib non-free #deb-src http://security.kali.org/kali-security kali-rolling/updates main contrib non-free 以上是目前加快的,也可以百度一下增加其它源

2018/4/21
articleCard.readMore

Linux 初识之 Kali Linux 系统安装详细教程(虚拟机)

目录 一、Kali Linux 介绍 1、Linux 2、Kali 二、虚拟机安装与配置 1、下载 2、安装配置 三、Kali系统安装与配置 一、Kali Linux 介绍 1、Linux 引用一下百度百科: Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。 Linux有多个发行版本,以下是官网提供下载的一些版本: 2、Kali kali是linux其中一个发行版,基于Debian,前身是BackTrack(简称BT系统)。kali系统内置大量渗透测试软件,黑客工具箱已不足以形容它,可以说是巨大的渗透系统,涵盖了多个领域,如无线网络、数字取证、服务器、密码、系统漏洞等等,知名软件有:wireshark、aircrack-ng、nmap、hashcat、metasploit-framework(msf)。 二、虚拟机软安装与配置 1、下载 这里将详细介绍在Windows虚拟机中安装kali linux,使用的虚拟机软件是 VMware,可以去官网下载最新版本,注意: VMware 10.0版本之后只支持64位系统,如果是32位系统用户需要下载10.0及之前的版本。 然后下载发型版的kali linux,官网下载,根据自己情况选择下载32位或64位,完整版或者轻便版,这里我下载的64位完整版。 1、安装配置 接下来一步步安装: 选择安装位置,注意保证空间充足: 等待安装完成: 这是试用版本,需要购买密钥,鼓励购买正版,蛮穷的可以使用以下任一密钥: FF31K-AHZD1-H8ETZ-8WWEZ-WUUVA CV7T2-6WY5Q-48EWP-ZXY7X-QGUWD 安装完成,然后新建虚拟机: 选择推荐的即可: 这里选择安装程序映像文件,浏览选择刚才下载的kali linux文件,后缀是 .iso: 选择操作系统“linux”,版本是“Debian”: 名称随便取,然后选择虚拟机文件存放位置: 注意选择合适位置,这里需要存放几十G的虚拟磁盘文件 设置虚拟磁盘的大小,一般不能低于默认值,下面一般选择单个文件,方便: 新建完成,硬件可以之后自定义: 觉得默认配置不合适可以自定义调整,例如修改内存大小,增加磁盘: 设置网络适配器模式,可以桥接物理网络(例如电脑连接wifi,虚拟机就和电脑连接同一个wifi),或者NAT模式(类似于电脑成为一个路由器,虚拟机连接电脑的热点),如果不想联网,只用于物理机和虚拟机之间的交流,可以选择主机模式: 虚拟机配置完成,接下来开始安装 kali 系统。 三、Kali系统安装与配置 点击启动虚拟机: 鼠标点击安装界面,之后用键盘方向键和Enter键操作: Live 开头的不是系统安装,类似于Windows PE,用于恢复系统,其他选项可以不用管,也暂时用不着。 这里我们选择简单的图形化安装 Graphical install 此时鼠标无法操作,退出虚拟机操作界面使用 ctrl + alt 键。 选择简体中文: 等待配置一会: 设置主机名称,和Windows的主机名一样: 可以暂时不用输入: 设置用户密码登录: 等待配置: 可以选择使用整个磁盘: 注: 这里简单介绍一下里面的 LVM 选项: 详见百度百科,LVM是 Logical Volume Manager(逻辑卷管理)的简写,它是Linux环境下对磁盘分区进行管理的一种机制。安装Linux时常出现的一个问题就是合理分区,根据使用情况设置 /boot, /var, /home 等区块的大小,设置好之后再想要改变就很麻烦,要用分区工具压缩一部分的空闲区出去,然后合并到空间不足的区域。 个人对LVM的理解是,它类似于使用一种文件夹的机制,直接使用整块磁盘,把每个分区设置成一种类似文件夹的存在,因为文件夹不会限制大小,因此就能动态调整各区的大小,方便管理。 继续: 可以使用推荐的: 确定分配,或者更改区块大小和新增区块: 确认写入: 接下来安装时间有点长,喝杯茶休息一下 -_- 终于安装完成,半小时安静的过去了…… 这里选择不使用网络镜像: 配置GRUB: 安装完成: 选择启动,第二个选项是恢复模式: 初始用户名是 root: 之前设置的密码: 到此kali系统就安装完毕了: 内置大量软件,更多功能可以自行发掘:

2018/4/15
articleCard.readMore

C 语言自增自减运算辨析

c语言中,自增(++)和自减(–)运算是很有c语言“感觉”的一种运算符,但是在实际编写中,尤其对初学者或者很久没接触它的,会对它的原理和运算结果产生混淆,接下来做详细辨析。 自增运算(++) 语法为:a++,其结果与:a = a + 1 一样,也和:a += 1 一样,作用很明显,方便阅读,减小代码量。例如下面的例子,就能看出明显的效果了: ((3+a)*sqrt(3)/(a%2))=((3+a)*sqrt(3)/(a%2))+1; /*简化后*/ ((3+a)*sqrt(3)/(a%2))++; 辨析 自增也可以表示为 ++a, 它们都能把a 的值加 1,但是两个表达式的值却不同,用一个例子说明: #include<stdio.h> int main() { int a = 1, b = 1, a2, b2; a2 = a++; b2 = ++b; printf("a = %d, b = %d, a2 = %d, b2 = %d", a, b, a2, b2); } 输出结果为: a = 2, b = 2, a2 = 1, b2 = 2 其实,a++ 的值为 a 本身,++a 的值为 a+1 后的值。 也可以按教科书那种记: b = a++ 是先赋值后运算,即先 b=a,然后 a++。 b = ++a 是先运算后赋值,即先 ++a,然后 ++a。 自减运算 自减运算原理和辨析与上面自增运算相似,a-- 等于 a = a -1 ,表达式 a-- 的值为 a ,表达式 --a 的值为 a-1。

2018/4/10
articleCard.readMore

递归函数之阶乘的实现

定义 在编程中函数有一个神奇又难理解的功能,就是递归。递归就是在一个过程中要调用上一步或上几步的结果,使用递归过程的函数就叫递归函数。简单说就是函数自身调用自身(听着有点反自然,像自己举起自己)。 递归实例 除了数学的复杂运算中,生活中也有不少递归的实例: 德罗斯特效应 德罗斯特效应(Droste effect)是递归的一种视觉形式,即一张图片中的某部分与整张图片相同,如下: 别晕、别晕 -_- 或者在自己身前和身后都放一面镜子,深刻体会一会儿 ▄︻┻┳══━一 递归实现 接下来就用C语言递归函数来实现阶乘功能,源码如下: #include <stdio.h> int factorial(int n) /*外部定义阶乘函数*/ { int m; if (n < 0) /*注意要给函数设置一个递归结束条件*/ { printf("输入错误!"); return 0; } else if (n == 1) { m = 1; } else { m = n * factorial(n-1); /*调用函数factorial()自身*/ } return m; } int main() { printf("%d", factorial(5)); } 这是程序大概的运算过程: 这里要注意给函数设置一个递归结束条件,可以是 if 判断句,不然函数就无限调用自身下去了,你之前看着镜子中的镜子中的……自己都晕,程序自然是崩溃了

2018/4/6
articleCard.readMore

位运算符与位运算

目录 概述 按位与 按位或 按位异或 简单应用 * 交换变量值 * 简单加密 按位取反 移位运算 左移位 右移位 复合赋值符 一、概述 程序中的所有数在计算机内存中都是以二进制的形式储存的。除了常见的算术运算符:+ - * / %,还有位运算:& | ^ ~ >> <<,就是直接对整数在内存中的二进制位进行操作。接下来以C语言为例介绍,其它语言大同小异。 二、按位与(&) 又叫 and 运算,用符号 & 表示,计算方式如下: 1 & 1 = 1 , 0 & 1 = 0 , ` 0 & 0 = 0` 布尔类型中,1 表示真,0 表示假,所以可以用高中数学那句话来记:同真为真,一假为假。 举个例子:3 & 5 = 1 ,运算过程如下: 三、按位或 (|) 又叫 or 运算,用符号 | 表示,运算方式: 1 | 1 = 1 , 1 | 0 = 1 , 0 | 0 = 0 记为:一真为真,同假为假 举例:3 | 5 = 7 , 运算过程: 四、按位异或(^) 又叫 xor 运算,用符号 ^ 表示,注意这里不是数学表达里面的次方的意思,运算方式: 1 ^ 1 = 0 , ` 1 ^ 0 = 1 , 0 ^ 0 = 0` 这种运算也较为特殊,记为:同为假,异为真 举例:3 ^ 5 = 6 , 运算过程: 简单应用 1、交换变量值 异或运算有如下特性: a ^ b ^a = b , a ^ b ^ b =a 因此可以用于程序中的变量值交换,C语言中,我们可能经常这样交换变量值: #include <stdio.h> int main() { int a = 3, b = 5; int temp; /*定义一个临时变量用于交换方便*/ temp = a; a = b; b = temp; printf("a = %d, b = %d", a, b); /*输出结果为:a = 5, b = 3 实现了变量值的交换*/ } 但是以后我们可以这样实现: #include <stdio.h> int main() { int a = 3, b = 5; a ^= b; /*等同于:a = a ^ b 之后会讲*/ b = a ^ b; /*这时 a ^ b 等于 原来 a 的值*/ a ^= b; /*a = a ^ b*/ printf("a = %d, b = %d", a, b); } a, b 为负数时同样适用,但位运算只适用于整型,浮点数不可用 2、简单加密 例如,你想传输一条信息给Ta,信息为:5201314 ,密码为:1998 ,使用如下代码加密: 5201314 ^ 1998 结果为 5200492,想要查看原信息,使用密码查看: 5200492 ^ 1998 这样就生成了原数字:`5201314`. 五、按位取反(~) 又叫 not 运算,即将二进制位中 0 和 1 全部取反,运算方式为: ~ 1 = 0 , ~ 0 = 1 记为:0 变 1,1 变 0 举例:~5 = -6, 运算过程: 注意变量如果定义是无符号短整型 unsigned short , ~5 将不再是 -6 ,而是 65530。 还有一个规律是正整数取反后结果是原数加一后取相反数,负数一样。 还要注意不要和逻辑运算符 ! 搞混,! 1 = 0 , ! 1234 = 0 , ! 0 = 1,即只有 真(1) 与假(0) 两种。 六、移位运算 顾名思义,移位即将数据的二进制数进行向左或向右平移一定的位数,然后得到新的数据,移位分为左移位和右移位。 1、左移位(«) 将数据的转换为二进制,所有位向左平移,高位(左端)舍弃,低位(右端)空位补 0,格式为: 需要移位的数据 << 需要移动的位数 举例:5 << 2 =20 ,运算过程: 右移位的数学意义是,原数左移 n 位,相当于原数乘以 2 的 n 次方 2、右移位 将数据的二进制所有位向右移位,低位舍弃,高位补 0(负数补 1),格式: 需要移位的数据 >> 需要移动的位数 举例:5 >> 2 = 1,运算过程: 负数情况:-6 >> 2 = -2,运算过程: 右移位的数学意义是,相当于原数除以 2 的 n 次方 七、复合赋值符 算术运算中有复合赋值符:+= -+ *= /= %=,位运算也有对应的复合赋值:&= |= ^= >>= <<= (注意没有 ~= ),运算一样: a &= b 等价于 a = a & b 以此类推 返回顶部

2018/4/6
articleCard.readMore

十进制负数的二进制表示法

十进制转正整数转二进制应该都会,用C语言代码简单表示算法: #include<stdio.h> int main() { int dec=65535; /*positive decimalism number to binary number*/ int bin[32]; int i=0, j; while (dec>1) { bin[i]=dec%2; dec/=2; i++; } bin[i]=dec; for (j=i; j>=0; j--) /*print binary number*/ { printf("%d",bin[j]); } printf("\n"); } 当然也有另外一种简单转换方法: #include<stdio.h> int main() { int dec=65535; char bin[32]; itoa(dec,bin,2); /*整型转换为字符串的函数,第一个参数是整型变量, 第二个是字符数组,用于存放字符串,第三个是进制,“2” 表示输出的字符串的进制格式, 可以这样记函数:“int to arr” */ printf("%s\n", bin); } 但是,十进制负整数转换为二进制稍微复杂一些,分为三步: 一、原码 例如一个十进制数 5,二进制原码表示为: 00000000 00000000 00000000 00000101 大小为 4 字节,每一个 0 或 1 表示一个比特位(bit),所以八位为一字节,好像32位和64位系统都这样。一字节用十进制整数表示大小则是:2的8次方(256)。 表示为十六进制是这样:ff ff ff ff(f=2^4-1) 二、反码 将二进制原码每一位取反,就是 0 变 1 ,1 变 0。 上面 5 的反码表示为这样: 11111111 11111111 11111111 11111010 三、补码 将反码最低位加 1 叫做补码,那么 5 的补码表示为: 11111111 11111111 11111111 11111011 注意反码末位是 1 时记得进位。 所以十进制数 -5 的二进制表示为: 11111111 11111111 11111111 11111011 所以 -1 在计算机中表示为全 1,就是: 11111111 11111111 11111111 11111111 我64位计算机中是这样的:

2018/4/4
articleCard.readMore

搭建 Github Pages 个人博客网站

目录 引言 关于博客 关于Github 创建Github账号 创建仓库 填充仓库 配置Github Pages功能 博客的书写与上传 Git基础 git配置 git Desktop版 创建本地仓库 安装Jekyll 关于Jekyll 安装步骤 开启jekyll 写博客与上传 Markdown基础 工具介绍 图床介绍 关于图片尺寸 域名配置 引言 关于博客 写博客对于程序猿来说,应该是个优秀的习惯,个人也觉得蛮高大上的 ^_^。网上的博客论坛网站也多种多样,个人觉得在长久以来的不断竞争淘汰中,各大网站的功能等可能都相差无几了,选择自己稍微偏好的就可以了。 我的个人情况就是结合CSDN博客和Github Pages的独立个人博客网页,因为听说拥有自己的Github主页也是一件蛮高大上的事 -_- 。 关于Github 然后简单介绍一下Github以及其Github Pages功能。 GitHub是一个面向开源及私有软件项目的托管平台,也是一个分布式版本控制系统,详情见百度百科。说到分布式,自然也有另外一种集中式版本控制系统:SVN,有兴趣小伙伴可以了解百度百科。GIt是SVN的发展版,而且现在主流也是GIt,但某些大公司依然在使用SVN,二者各有优劣,自行体会,此处不做详解,用一张图简单说明: GIthub Pages则是github上的一项功能,可以放置网页文件到指定文件夹,然后给你一个专属域名用于展示一些项目,但现在大多用来开发制作个人博客网站。接下来就一步步按照我曾经的步骤来搭建个人博客,顺便讲讲沿途遇到过的坑,如没有的提及请自行百度。 创建Github账号 github pages 功能依赖于github账号,没有的话先去官网注册一个: 然后好像要邮箱验证,就是填写的那个,点击那个验证链接就注册成功了。 创建仓库 有了自己的账号后,可以跟着官网的引导,创建自己的第一个仓库,就是 repository: 填好信息 创建完成 到这里就创建好了自己的仓库,可以上传文件到这个目录下,接下我们用这个仓库来使用github pages功能。 填充仓库 仓库建好了,接下来就是往里面装东西了,就是支撑博客首页的一些网页文件和配置文件,对于新手来说要自己编写这些文件就有点开玩笑了,所以可以选择使用已有的主题,你可以选择复制我的https://github.com/knightyun/knightyun.github.io,然后选择自己仓库,网页基础好的同学以后修改网页内容就行了。 嫌修改麻烦可以跳过这一步,到后面的步骤选择喜欢的主题 配置Github Pages功能 然后我们来配置github pages 重命名,注意格式 把上面的页面向下滑,现在就可以访问了 当然github也提供了一些主题供选择,点击上面的“choose a theme”按钮进行选择 这个网站有更多主题工选择:http://jekyllthemes.org/,如有选择困难症请绕路 -_- 博客的书写与上传 Git基础 前面说到向自己的github仓库上传文件,我们使用“git”这个工具,进行拉取、克隆、提交等一系列操作,Linux系统应该是自带,官网下载地址:https://git-scm.com/。 并且需要掌握一些git基本操作,如 git commit , git push, git clone 等,这里有很完整的教程:Git语法说明. git配置 安装好后cmd输入 git 有反应则安装成功: 进行如下配置: git config --global user.name "YOUR NAME" git config --global user.email "YOUR EMAIL ADDRESS" NAME 指你的昵称,EMAIL ADDRESS 是你的注册邮箱 然后生成相应的令牌,本地一份,Github 一份,这样 Github 可以在你使用仓库的时候,进行校验确定你的身份。 cd ~/.ssh mkdir key_backup ssh-keygen -t rsa -C "*your_email@youremail.com*" 注意这里不是在cmd里输入,是使用刚安装的 git bash 软件,可以在电脑菜单里面搜索 然后会生成如下两个文件: id_rsa.pub 就是我们待会需要的公钥文件,使用命令 cat id_rsa.pub 再将内容复制到剪切板,然后进入github账号设置里面粘贴 选择添加SSH key: 把刚才复制的内容粘贴进去 然后输入 ssh -T git@github.com 测试连通状态 我的Windows版没有成功,不知道Linux是否成功,报错如下 ,应该是windows ssh配置问题 git Desktop版 如果你也出现以上状况,不必担心,git还能使用https协议连接,只不过要每次输入账号和密码,但是可以选择github官方提供的git desktop软件: 这里我是下载过的 界面如下,需要登录,以后提交文件就方便了,cmd也能使用git提交,不用每次输入密码 可以查看变化文件,甚至文件内变化的内容,commit 后点击 fetch 按钮提交 软件功能不算复杂,自己摸索一会就会了,图形界面的软件使得一些命令行的操作变得容易、友好。 创建本地仓库 选择一个本地文件夹,用作保存本地仓库文件,尽量是空文件夹,然后使用命令 git init 初始化文件夹,其实是在当前文件夹下生成一个叫 .git 的隐藏文件夹,里面是一些配置文件,不要随意更改。 使用 git clone https://github.com/name/repository.git 将远程仓库克隆到本地此文件夹下, name 是自己的昵称,repository 是自己的仓库名,不要忘记末尾的 .git 后缀。 然后此文件夹下会多一个和你仓储名一样的文件夹,内部文件与远程仓库一样。 绑定远程仓库,方便提交: git remote add origin git@github.com:username/username.github.io.git 介绍几个常用命令: git add . # 添加文件 git commit -m "commit-messages" # 提交本地仓库 git push origin master # 提交远程仓库 git pull # 拉取远程文件,与以下命令类似 git branch temp # 创建本地分支 git fetch origin master:temp git merge master 安装Jekyll 关于Jekyll Jekyll 是一个简单免费的生成博客网页的框架,Github Pages 功能就是使用的 Jekyll 框架把仓库内的文件生成静态网页给人们浏览,其本来目的是提供给 GitHub 项目“自我介绍”用的,只不过后来陆续有人发现了其博客网站的用途,也就是上面介绍的博客网站,详情参考官网:https://jekyllrb.com/, 也有一个中文版的:https://www.jekyll.com.cn/ 方便阅读。上面那个主题网站也是jekyll的,还有一个类似的工具叫“hexo”,自行了解。 由于上传修改后的文件到 github 仓库后需要一段时间才能看到网页的变化或修改效果,使得对于页面效果和功能的调试不太方便,所以如果选择在本地安装 jekyll 框架开发环境的话,可以快速预览生成效果,方便调试,最后再把成品上传到 GitHub 仓库中就可以得到预期效果了。 安装步骤 安装Ruby:jekyll依赖于 Ruby 环境,需要提前安装,官网下载链接:http://www.ruby-lang.org/en/downloads/,windows/Linux/Mac 的版本都有。 安装gem:官网链接https://rubygems.org/pages/download,貌似安装 ruby 后自带 gem。 可以 cmd 命令行输入 gem 检查是否安装成功: 安装jekyll:cmd命令行输入 gem install jekyll 开启jekyll 直接输入 jekyll s 开启jekyll服务,windows可能会遇到以下问题: 使用 bundle exec jekyll s 命令就可以运行了,如果提示没有安装 bundler ,就 gem install bundler 再 bundle install,可能还会提示没有安装其他组件,记下名称, gem install xxx 就可以了; 然后就可以成功运行了,退出按 ctrl + c 键: 运行时保持这个窗口不要关闭,浏览器输入 127.0.0.1:4000 或 localhost:4000 进行预览,不过我的windows预览效果不太好,加载不出图片,其他系统没试过; 写博客与上传 Markdown基础 Jekyll使用Markdown语言书写博客,markdown是一种简单易读的标记性语言,不同于 html,大量的标签不易于阅读,写着也麻烦,用markdown写博客很合适。 首先你需要了解一些markdown语法,这里有完整版语法说明:Markdown语法说明,了解一些基础后就可以开始写博客了。 工具介绍 这篇文章:Markdown简明语法最后有介绍一些好用的markdown编辑器,自行选择。 不过每次都用编辑器写好 .md 文件然后用 git 上传到 github 根目录下的 _post 文件夹好像很繁琐,Jekyll官方提供了一款方便的博客编辑器,方便书写、预览、上传,官网链接:http://jekyllwriter.com/,三种系统版本都有。接下来简单介绍一些使用: 安装后主界面: 添加账号 配置 token 保存后会生成一个 token ,返回软件粘贴进输入框就行了 写完后保存并上传 可以在这里查看和修改账户下的博客 软件其他功能还在完善,自行摸索 图床介绍 写博客就无法避免上传图片,图床就是这么一个地方,就是一个网站,你发自己的图片上传到它的网站,然后它给你一个这个图片的链接,插入博客中就能显示图片了。 推荐一个知名的,七牛云https://portal.qiniu.com/,注册完实名认证后有一些优惠。 还有一个神奇的网站:https://sm.ms/,也能用 然后在 jekyll writer中配置一下: 当然我用的是CSDN在线编辑器写博客,图片能直接上传到CSDN上,直接生成链接,其工具也能用 关于图片尺寸 markdown 的图片插入方式 ![title](http://xxx.com/xxx.png/) 是没办法修改图片尺寸的,可以使用html中的 <img> 标签: <img src="http://xxx.com/xxx.png/" alt="title" width=XXpx height=XXpx> width 和 height 添加想要的尺寸。 域名配置 自己的博客网站就建好了,想要分享出去的小伙伴就要想办法让自己的网页能被百度等搜索引擎搜到,或者这样,百度搜索: site:name.github.io ,出现错误页面就表示搜不到。 很遗憾,百度是禁止抓取 github pages 的内容的,可以购买一个自己的专属域名,有很多选择,阿里云、腾讯、花生壳域名等,百度站长平台有个链接提交功能,但是它只是加速爬取,并未解决收录: 貌似它们的熊掌号服务可以解决这问题: 然后,就没有然后了 -_- 以花生壳域名为例,其它大同小异,配置一下: 再添加两条 github 的ip的 A记录值 :192.30.252.153 192.30.252.154 最后搜索: "site:你的域名" 有结果就成功了 开始自己的博客生涯吧。 返回顶部

2018/4/1
articleCard.readMore

C 语言统计素数

源码如下: #include<stdio.h> int main() { // 1-1000的素数 printf("1-1000的素数: \n\n"); int i, j, co = 0; //设置co用于统计个数 for (j = 1; j <= 1000; j++) //遍历1-1000 { for (i = 2; i < j; i++) //素数条件:只能被1和自身整除 { if (j % i == 0) //发现不是素数,则跳出本层循环 { break; } } if (i == j) //遍历后,没有除1和自身以外的因数,i==j 表示遍历了 2--自身的所有数并且未发现因数 { printf("%d ", j); co++; } } printf("\nTotal: %d\n\n", co); } 运行结果: 1-1000的素数: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 997 total: 168 注意 素数,又称质数,即整数在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。将1-1000中每个数都对“2–自身”进行整除,i==j 说明是对“2–自身”都遍历了一遍并且未发现符合条件的数,而不是前面发现有多余因数的break; 另一个优化的版本,供参考: #include<stdio.h> #include<math.h> int main() { // 1-1000的素数 printf("1-1000的素数: \n\n"); int i, j, co = 0; printf("2 "); for (j = 3; j <= 1000; j+=2) //不遍历偶数,除了2偶数都不是质数 { for (i = 2; i <= sqrt(j); i++) //实际遍历到此数的平方根就够了,需要引入 <math.h> 头文件 { if (j % i == 0) { break; } } if (i > sqrt(j)) //这里也要变一下 { printf("%d ", j); co++; } } printf("\n\ntotal: %d\n\n", co+1); //注意这里要加上“2”这个数,2是质数也是偶数,之前没有遍历 } 这样就大大缩短了时间。

2018/3/30
articleCard.readMore

C 语言打印九九乘法表

源码如下: #include<stdio.h> int main() { //打印九九乘法表 printf("九九乘法表:\n"); int x, y; //初始化打印的两个方向 for (y = 1; y < 10; y++) //两层循环嵌套打印输出 { for (x = 1; x <= y; x++) { printf("%d*%d=%2d ", y, x, x * y); //%2d表示固定输出两位 } printf("\n"); //打印到行尾,换行 } printf("This is the end.\n"); } 输出结果: 九九乘法表: 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 This is the end. 注意: 代码注释处说明的%2d是为了美观,强行使相乘的结果占两个字符位,否则打印结果如下: 九九乘法表: 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 This is the end. 没有上面美观吧-_-

2018/3/30
articleCard.readMore

C 语言统计闰年

源码如下: #include<stdio.h> int main() { // 统计1-2020的闰年 printf("1-2020的闰年: \n\n"); int ye, c = 0; for (ye = 1; ye <= 2020; ye++) //此处也可更改为指定年份区间 { if (((ye % 4 == 0) && (ye % 100 != 0)) || (ye % 400 == 0)) //闰年的定义:能被4整除但不能被100整除,或能被400整除的年份 { printf("%d ", ye); c++; } } printf("\n\nTotal: %d\n", c); //用定义的变量c统计 } 运行结果: 1-2020的闰年: 4 8 12 16 20 24 28 32 36 40 44 48 52 56 60 64 68 72 76 80 84 88 92 96 104 108 112 116 120 124 128 132 136 140 144 148 152 156 160 164 168 172 176 180 184 188 192 196 204 208 212 216 220 224 228 232 236 240 244 248 252 256 260 264 268 272 276 280 284 288 292 296 304 308 312 316 320 324 328 332 336 340 344 348 352 356 360 364 368 372 376 380 384 388 392 396 400 404 408 412 416 420 424 428 432 436 440 444 448 452 456 460 464 468 472 476 480 484 488 492 496 504 508 512 516 520 524 528 532 536 540 544 548 552 556 560 564 568 572 576 580 584 588 592 596 604 608 612 616 620 624 628 632 636 640 644 648 652 656 660 664 668 672 676 680 684 688 692 696 704 708 712 716 720 724 728 732 736 740 744 748 752 756 760 764 768 772 776 780 784 788 792 796 800 804 808 812 816 820 824 828 832 836 840 844 848 852 856 860 864 868 872 876 880 884 888 892 896 904 908 912 916 920 924 928 932 936 940 944 948 952 956 960 964 968 972 976 980 984 988 992 996 1004 1008 1012 1016 1020 1024 1028 1032 1036 1040 1044 1048 1052 1056 1060 1064 1068 1072 1076 1080 1084 1088 1092 1096 1104 1108 1112 1116 1120 1124 1128 1132 1136 1140 1144 1148 1152 1156 1160 1164 1168 1172 1176 1180 1184 1188 1192 1196 1200 1204 1208 1212 1216 1220 1224 1228 1232 1236 1240 1244 1248 1252 1256 1260 1264 1268 1272 1276 1280 1284 1288 1292 1296 1304 1308 1312 1316 1320 1324 1328 1332 1336 1340 1344 1348 1352 1356 1360 1364 1368 1372 1376 1380 1384 1388 1392 1396 1404 1408 1412 1416 1420 1424 1428 1432 1436 1440 1444 1448 1452 1456 1460 1464 1468 1472 1476 1480 1484 1488 1492 1496 1504 1508 1512 1516 1520 1524 1528 1532 1536 1540 1544 1548 1552 1556 1560 1564 1568 1572 1576 1580 1584 1588 1592 1596 1600 1604 1608 1612 1616 1620 1624 1628 1632 1636 1640 1644 1648 1652 1656 1660 1664 1668 1672 1676 1680 1684 1688 1692 1696 1704 1708 1712 1716 1720 1724 1728 1732 1736 1740 1744 1748 1752 1756 1760 1764 1768 1772 1776 1780 1784 1788 1792 1796 1804 1808 1812 1816 1820 1824 1828 1832 1836 1840 1844 1848 1852 1856 1860 1864 1868 1872 1876 1880 1884 1888 1892 1896 1904 1908 1912 1916 1920 1924 1928 1932 1936 1940 1944 1948 1952 1956 1960 1964 1968 1972 1976 1980 1984 1988 1992 1996 2000 2004 2008 2012 2016 2020 Total: 490

2018/3/30
articleCard.readMore

Markdown 完整语法说明

Markdown 完整语法说明 辛勤的搬运工,原文链接http://wowubuntu.com/markdown/index.html 目录 概述 宗旨 兼容 HTML 特殊字符自动转换 区块元素 段落和换行 标题 区块引用 Blockquotes 列表 分隔线 区段元素 链接 强调 代码 其它 自动链接 反斜杠 概述 宗旨 Markdown 的目标是实现「易读易写」。 可读性,无论如何,都是最重要的。一份使用 Markdown 格式撰写的文件应该可以直接以纯文本发布,并且看起来不会像是由许多标签或是格式指令所构成。Markdown 语法受到一些既有 text-to-HTML 格式的影响,包括 Setext、atx、Textile、reStructuredText、Grutatext 和 EtText,而最大灵感来源其实是纯文本电子邮件的格式。 总之, Markdown 的语法全由一些符号所组成,这些符号经过精挑细选,其作用一目了然。比如:在文字两旁加上星号,看起来就像强调。Markdown 的列表看起来,嗯,就是列表。Markdown 的区块引用看起来就真的像是引用一段文字,就像你曾在电子邮件中见过的那样。 兼容 HTML Markdown 语法的目标是:成为一种适用于网络的书写语言。 Markdown 不是想要取代 HTML,甚至也没有要和它相近,它的语法种类很少,只对应 HTML 标记的一小部分。Markdown 的构想不是要使得 HTML 文档更容易书写。在我看来, HTML 已经很容易写了。Markdown 的理念是,能让文档更容易读、写和随意改。HTML 是一种发布的格式,Markdown 是一种书写的格式。就这样,Markdown 的格式语法只涵盖纯文本可以涵盖的范围。 不在 Markdown 涵盖范围之内的标签,都可以直接在文档里面用 HTML 撰写。不需要额外标注这是 HTML 或是 Markdown;只要直接加标签就可以了。 要制约的只有一些 HTML 区块元素――比如 <div>、<table>、<pre>、<p> 等标签,必须在前后加上空行与其它内容区隔开,还要求它们的开始标签与结尾标签不能用制表符或空格来缩进。Markdown 的生成器有足够智能,不会在 HTML 区块标签外加上不必要的 <p> 标签。 例子如下,在 Markdown 文件里加上一段 HTML 表格: 这是一个普通段落。 <table> <tr> <td>Foo</td> </tr> </table> 这是另一个普通段落。 请注意,在 HTML 区块标签间的 Markdown 格式语法将不会被处理。比如,你在 HTML 区块内使用 Markdown 样式的`*强调*`会没有效果。 HTML 的区段(行内)标签如 <span>、<cite>、<del> 可以在 Markdown 的段落、列表或是标题里随意使用。依照个人习惯,甚至可以不用 Markdown 格式,而直接采用 HTML 标签来格式化。举例说明:如果比较喜欢 HTML 的 <a> 或 <img> 标签,可以直接使用这些标签,而不用 Markdown 提供的链接或是图像标签语法。 和处在 HTML 区块标签间不同,Markdown 语法在 HTML 区段标签间是有效的。 特殊字符自动转换 在 HTML 文件中,有两个字符需要特殊处理: < 和 & 。 < 符号用于起始标签,& 符号则用于标记 HTML 实体,如果你只是想要显示这些字符的原型,你必须要使用实体的形式,像是 &lt; 和 &amp;。 & 字符尤其让网络文档编写者受折磨,如果你要打「AT&T」 ,你必须要写成「AT&amp;T」。而网址中的 & 字符也要转换。比如你要链接到: http://images.google.com/images?num=30&q=larry+bird 你必须要把网址转换写为: http://images.google.com/images?num=30&amp;q=larry+bird 才能放到链接标签的 `href` 属性里。不用说也知道这很容易忽略,这也可能是 HTML 标准检验所检查到的错误中,数量最多的。 Markdown 让你可以自然地书写字符,需要转换的由它来处理好了。如果你使用的 & 字符是 HTML 字符实体的一部分,它会保留原状,否则它会被转换成 &amp;。 所以你如果要在文档中插入一个版权符号 ©,你可以这样写: &copy; Markdown 会保留它不动。而若你写: AT&T Markdown 就会将它转为: AT&amp;T 类似的状况也会发生在 < 符号上,因为 Markdown 允许 兼容 HTML ,如果你是把 < 符号作为 HTML 标签的定界符使用,那 Markdown 也不会对它做任何转换,但是如果你写: 4 < 5 Markdown 将会把它转换为: 4 &lt; 5 不过需要注意的是,code 范围内,不论是行内还是区块, < 和 & 两个符号都一定会被转换成 HTML 实体,这项特性让你可以很容易地用 Markdown 写 HTML code (和 HTML 相对而言, HTML 语法中,你要把所有的 < 和 & 都转换为 HTML 实体,才能在 HTML 文件里面写出 HTML code。) 区块元素 段落和换行 一个 Markdown 段落是由一个或多个连续的文本行组成,它的前后要有一个以上的空行(空行的定义是显示上看起来像是空的,便会被视为空行。比方说,若某一行只包含空格和制表符,则该行也会被视为空行)。普通段落不该用空格或制表符来缩进。 「由一个或多个连续的文本行组成」这句话其实暗示了 Markdown 允许段落内的强迫换行(插入换行符),这个特性和其他大部分的 text-to-HTML 格式不一样(包括 Movable Type 的「Convert Line Breaks」选项),其它的格式会把每个换行符都转成 <br /> 标签。 如果你确实想要依赖 Markdown 来插入 <br /> 标签的话,在插入处先按入两个以上的空格然后回车。 的确,需要多费点事(多加空格)来产生 <br /> ,但是简单地「每个换行都转换为 <br />」的方法在 Markdown 中并不适合, Markdown 中 email 式的 区块引用 和多段落的 列表 在使用换行来排版的时候,不但更好用,还更方便阅读。 标题 Markdown 支持两种标题的语法,类 Setext 和类 atx 形式。 类 Setext 形式是用底线的形式,利用 = (最高阶标题)和 - (第二阶标题),例如: This is an H1 ============= This is an H2 ------------- 任何数量的 `=` 和 `-` 都可以有效果。 类 Atx 形式则是在行首插入 1 到 6 个 # ,对应到标题 1 到 6 阶,例如: # 这是 H1 ## 这是 H2 ###### 这是 H6 你可以选择性地「闭合」类 atx 样式的标题,这纯粹只是美观用的,若是觉得这样看起来比较舒适,你就可以在行尾加上 #,而行尾的 # 数量也不用和开头一样(行首的井字符数量决定标题的阶数): # 这是 H1 # ## 这是 H2 ## ### 这是 H3 ###### 区块引用 Blockquotes Markdown 标记区块引用是使用类似 email 中用 > 的引用方式。如果你还熟悉在 email 信件中的引言部分,你就知道怎么在 Markdown 文件中建立一个区块引用,那会看起来像是你自己先断好行,然后在每行的最前面加上 > : > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. > > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse > id sem consectetuer libero luctus adipiscing. Markdown 也允许你偷懒只在整个段落的第一行最前面加上 > : > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse id sem consectetuer libero luctus adipiscing. 区块引用可以嵌套(例如:引用内的引用),只要根据层次加上不同数量的 > : > This is the first level of quoting. > > > This is nested blockquote. > > Back to the first level. 引用的区块内也可以使用其他的 Markdown 语法,包括标题、列表、代码区块等: > ## 这是一个标题。 > > 1. 这是第一行列表项。 > 2. 这是第二行列表项。 > > 给出一些例子代码: > > return shell_exec("echo $input | $markdown_script"); 任何像样的文本编辑器都能轻松地建立 email 型的引用。例如在 BBEdit 中,你可以选取文字后然后从选单中选择增加引用阶层。 列表 Markdown 支持有序列表和无序列表。 无序列表使用星号、加号或是减号作为列表标记: * Red * Green * Blue 等同于: + Red + Green + Blue 也等同于: - Red - Green - Blue 有序列表则使用数字接着一个英文句点: 1. Bird 2. McHale 3. Parish 很重要的一点是,你在列表标记上使用的数字并不会影响输出的 HTML 结果,上面的列表所产生的 HTML 标记为: <ol> <li>Bird</li> <li>McHale</li> <li>Parish</li> </ol> 如果你的列表标记写成: 1. Bird 1. McHale 1. Parish 或甚至是: 3. Bird 1. McHale 8. Parish 你都会得到完全相同的 HTML 输出。重点在于,你可以让 Markdown 文件的列表数字和输出的结果相同,或是你懒一点,你可以完全不用在意数字的正确性。 如果你使用懒惰的写法,建议第一个项目最好还是从 1. 开始,因为 Markdown 未来可能会支持有序列表的 start 属性。 列表项目标记通常是放在最左边,但是其实也可以缩进,最多 3 个空格,项目标记后面则一定要接着至少一个空格或制表符。 要让列表看起来更漂亮,你可以把内容用固定的缩进整理好: * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse id sem consectetuer libero luctus adipiscing. 但是如果你懒,那也行: * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse id sem consectetuer libero luctus adipiscing. 如果列表项目间用空行分开,在输出 HTML 时 Markdown 就会将项目内容用 <p> 标签包起来,举例来说: * Bird * Magic 会被转换为: <ul> <li>Bird</li> <li>Magic</li> </ul> 但是这个: * Bird * Magic 会被转换为: <ul> <li><p>Bird</p></li> <li><p>Magic</p></li> </ul> 列表项目可以包含多个段落,每个项目下的段落都必须缩进 4 个空格或是 1 个制表符: 1. This is a list item with two paragraphs. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. Donec sit amet nisl. Aliquam semper ipsum sit amet velit. 2. Suspendisse id sem consectetuer libero luctus adipiscing. 如果你每行都有缩进,看起来会看好很多,当然,再次地,如果你很懒惰,Markdown 也允许: * This is a list item with two paragraphs. This is the second paragraph in the list item. You're only required to indent the first line. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. * Another item in the same list. 如果要在列表项目内放进引用,那 > 就需要缩进: * A list item with a blockquote: > This is a blockquote > inside a list item. 如果要放代码区块的话,该区块就需要缩进两次,也就是 8 个空格或是 2 个制表符: * 一列表项包含一个列表区块: <代码写在这> 当然,项目列表很可能会不小心产生,像是下面这样的写法: 1986. What a great season. 换句话说,也就是在行首出现数字-句点-空白,要避免这样的状况,你可以在句点前面加上反斜杠。 1986\. What a great season. 代码区块 和程序相关的写作或是标签语言原始码通常会有已经排版好的代码区块,通常这些区块我们并不希望它以一般段落文件的方式去排版,而是照原来的样子显示,Markdown 会用 <pre> 和 <code> 标签来把代码区块包起来。 要在 Markdown 中建立代码区块很简单,只要简单地缩进 4 个空格或是 1 个制表符就可以,例如,下面的输入: 这是一个普通段落: 这是一个代码区块。 Markdown 会转换成: <p>这是一个普通段落:</p> <pre><code>这是一个代码区块。 </code></pre> 这个每行一阶的缩进(4 个空格或是 1 个制表符),都会被移除,例如: Here is an example of AppleScript: tell application "Foo" beep end tell 会被转换为: <p>Here is an example of AppleScript:</p> <pre><code>tell application "Foo" beep end tell </code></pre> 一个代码区块会一直持续到没有缩进的那一行(或是文件结尾)。 在代码区块里面, & 、 < 和 > 会自动转成 HTML 实体,这样的方式让你非常容易使用 Markdown 插入范例用的 HTML 原始码,只需要复制贴上,再加上缩进就可以了,剩下的 Markdown 都会帮你处理,例如: <div class="footer"> &copy; 2004 Foo Corporation </div> 会被转换为: <pre><code>&lt;div class="footer"&gt; &amp;copy; 2004 Foo Corporation &lt;/div&gt; </code></pre> 代码区块中,一般的 Markdown 语法不会被转换,像是星号便只是星号,这表示你可以很容易地以 Markdown 语法撰写 Markdown 语法相关的文件。 分隔线 你可以在一行中用三个以上的星号、减号、底线来建立一个分隔线,行内不能有其他东西。你也可以在星号或是减号中间插入空格。下面每种写法都可以建立分隔线: * * * *** ***** - - - --------------------------------------- ##区段元素 链接 Markdown 支持两种形式的链接语法: _行内式_和_参考式_两种形式。 不管是哪一种,链接文字都是用 [方括号] 来标记。 要建立一个行内式的链接,只要在方块括号后面紧接着圆括号并插入网址链接即可,如果你还想要加上链接的 title 文字,只要在网址后面,用双引号把 title 文字包起来即可,例如: This is [an example](http://example.com/ "Title") inline link. [This link](http://example.net/) has no title attribute. 会产生: <p>This is <a href="http://example.com/" title="Title"> an example</a> inline link.</p> <p><a href="http://example.net/">This link</a> has no title attribute.</p> 如果你是要链接到同样主机的资源,你可以使用相对路径: See my [About](/about/) page for details. 参考式的链接是在链接文字的括号后面再接上另一个方括号,而在第二个方括号里面要填入用以辨识链接的标记: This is [an example][id] reference-style link. 你也可以选择性地在两个方括号中间加上一个空格: This is [an example] [id] reference-style link. 接着,在文件的任意处,你可以把这个标记的链接内容定义出来: [id]: http://example.com/ "Optional Title Here" 链接内容定义的形式为: 方括号(前面可以选择性地加上至多三个空格来缩进),里面输入链接文字 接着一个冒号 接着一个以上的空格或制表符 接着链接的网址 选择性地接着 title 内容,可以用单引号、双引号或是括弧包着 下面这三种链接的定义都是相同: [foo]: http://example.com/ "Optional Title Here" [foo]: http://example.com/ 'Optional Title Here' [foo]: http://example.com/ (Optional Title Here) 请注意:有一个已知的问题是 Markdown.pl 1.0.1 会忽略单引号包起来的链接 title。 链接网址也可以用尖括号包起来: [id]: <http://example.com/> "Optional Title Here" 你也可以把 title 属性放到下一行,也可以加一些缩进,若网址太长的话,这样会比较好看: [id]: http://example.com/longish/path/to/resource/here "Optional Title Here" 网址定义只有在产生链接的时候用到,并不会直接出现在文件之中。 链接辨别标签可以有字母、数字、空白和标点符号,但是并不区分大小写,因此下面两个链接是一样的: [link text][a] [link text][A] 隐式链接标记功能让你可以省略指定链接标记,这种情形下,链接标记会视为等同于链接文字,要用隐式链接标记只要在链接文字后面加上一个空的方括号,如果你要让 "Google" 链接到 google.com,你可以简化成: [Google][] 然后定义链接内容: [Google]: http://google.com/ 由于链接文字可能包含空白,所以这种简化型的标记内也许包含多个单词: Visit [Daring Fireball][] for more information. 然后接着定义链接: [Daring Fireball]: http://daringfireball.net/ 链接的定义可以放在文件中的任何一个地方,我比较偏好直接放在链接出现段落的后面,你也可以把它放在文件最后面,就像是注解一样。 下面是一个参考式链接的范例: I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]. [1]: http://google.com/ "Google" [2]: http://search.yahoo.com/ "Yahoo Search" [3]: http://search.msn.com/ "MSN Search" 如果改成用链接名称的方式写: I get 10 times more traffic from [Google][] than from [Yahoo][] or [MSN][]. [google]: http://google.com/ "Google" [yahoo]: http://search.yahoo.com/ "Yahoo Search" [msn]: http://search.msn.com/ "MSN Search" 上面两种写法都会产生下面的 HTML。 <p>I get 10 times more traffic from <a href="http://google.com/" title="Google">Google</a> than from <a href="http://search.yahoo.com/" title="Yahoo Search">Yahoo</a> or <a href="http://search.msn.com/" title="MSN Search">MSN</a>.</p> 下面是用行内式写的同样一段内容的 Markdown 文件,提供作为比较之用: I get 10 times more traffic from [Google](http://google.com/ "Google") than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or [MSN](http://search.msn.com/ "MSN Search"). 参考式的链接其实重点不在于它比较好写,而是它比较好读,比较一下上面的范例,使用参考式的文章本身只有 81 个字符,但是用行内形式的却会增加到 176 个字元,如果是用纯 HTML 格式来写,会有 234 个字元,在 HTML 格式中,标签比文本还要多。 使用 Markdown 的参考式链接,可以让文件更像是浏览器最后产生的结果,让你可以把一些标记相关的元数据移到段落文字之外,你就可以增加链接而不让文章的阅读感觉被打断。 强调 Markdown 使用星号(*)和底线(_)作为标记强调字词的符号,被 * 或 _ 包围的字词会被转成用 <em> 标签包围,用两个 * 或 _ 包起来的话,则会被转成 <strong>,例如: *single asterisks* _single underscores_ **double asterisks** __double underscores__ 会转成: <em>single asterisks</em> <em>single underscores</em> <strong>double asterisks</strong> <strong>double underscores</strong> 你可以随便用你喜欢的样式,唯一的限制是,你用什么符号开启标签,就要用什么符号结束。 强调也可以直接插在文字中间: un*frigging*believable 但是如果你的 * 和 _ 两边都有空白的话,它们就只会被当成普通的符号。 如果要在文字前后直接插入普通的星号或底线,你可以用反斜线: \*this text is surrounded by literal asterisks\* 代码 如果要标记一小段行内代码,你可以用反引号把它包起来(`),例如: Use the `printf()` function. 会产生: <p>Use the <code>printf()</code> function.</p> 如果要在代码区段内插入反引号,你可以用多个反引号来开启和结束代码区段: ``There is a literal backtick (`) here.`` 这段语法会产生: <p><code>There is a literal backtick (`) here.</code></p> 代码区段的起始和结束端都可以放入一个空白,起始端后面一个,结束端前面一个,这样你就可以在区段的一开始就插入反引号: A single backtick in a code span: `` ` `` A backtick-delimited string in a code span: `` `foo` `` 会产生: <p>A single backtick in a code span: <code>`</code></p> <p>A backtick-delimited string in a code span: <code>`foo`</code></p> 在代码区段内,& 和尖括号都会被自动地转成 HTML 实体,这使得插入 HTML 原始码变得很容易,Markdown 会把下面这段: Please don't use any `<blink>` tags. 转为: <p>Please don't use any <code>&lt;blink&gt;</code> tags.</p> 你也可以这样写: `&#8212;` is the decimal-encoded equivalent of `&mdash;`. 以产生: <p><code>&amp;#8212;</code> is the decimal-encoded equivalent of <code>&amp;mdash;</code>.</p> 图片 很明显地,要在纯文字应用中设计一个「自然」的语法来插入图片是有一定难度的。 Markdown 使用一种和链接很相似的语法来标记图片,同样也允许两种样式: 行内式和参考式。 行内式的图片语法看起来像是: ![Alt text](/path/to/img.jpg) ![Alt text](/path/to/img.jpg "Optional title") 详细叙述如下: 一个惊叹号 ! 接着一个方括号,里面放上图片的替代文字 接着一个普通括号,里面放上图片的网址,最后还可以用引号包住并加上 选择性的 ‘title’ 文字。 参考式的图片语法则长得像这样: 「id」是图片参考的名称,图片参考的定义方式则和连结参考一样: 到目前为止, Markdown 还没有办法指定图片的宽高,如果你需要的话,你可以使用普通的 <img> 标签。 其它 自动链接 Markdown 支持以比较简短的自动链接形式来处理网址和电子邮件信箱,只要是用尖括号包起来, Markdown 就会自动把它转成链接。一般网址的链接文字就和链接地址一样,例如: <http://example.com/> Markdown 会转为: <a href="http://example.com/">http://example.com/</a> 邮址的自动链接也很类似,只是 Markdown 会先做一个编码转换的过程,把文字字符转成 16 进位码的 HTML 实体,这样的格式可以糊弄一些不好的邮址收集机器人,例如: <address@example.com> Markdown 会转成: <a href="&#x6D;&#x61;i&#x6C;&#x74;&#x6F;:&#x61;&#x64;&#x64;&#x72;&#x65; &#115;&#115;&#64;&#101;&#120;&#x61;&#109;&#x70;&#x6C;e&#x2E;&#99;&#111; &#109;">&#x61;&#x64;&#x64;&#x72;&#x65;&#115;&#115;&#64;&#101;&#120;&#x61; &#109;&#x70;&#x6C;e&#x2E;&#99;&#111;&#109;</a> 在浏览器里面,这段字串(其实是 `<a href="mailto:address@example.com">address@example.com</a>`)会变成一个可以点击的「address@example.com」链接。 (这种作法虽然可以糊弄不少的机器人,但并不能全部挡下来,不过总比什么都不做好些。不管怎样,公开你的信箱终究会引来广告信件的。) 反斜杠 Markdown 可以利用反斜杠来插入一些在语法中有其它意义的符号,例如:如果你想要用星号加在文字旁边的方式来做出强调效果(但不用 <em> 标签),你可以在星号的前面加上反斜杠: \*literal asterisks\* Markdown 支持以下这些符号前面加上反斜杠来帮助插入普通的符号: \ 反斜线 ` 反引号 * 星号 _ 底线 {} 花括号 [] 方括号 () 括弧 # 井字号 + 加号 - 减号 . 英文句点 ! 惊叹号 返回顶部

2018/3/28
articleCard.readMore

Markdown 简明教程

Markdown 及扩展 Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 —— [ 维基百科 ] 使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体,创建一个链接等,详细语法参考帮助?。 本编辑器支持 Markdown Extra ,  扩展了很多好用的功能。具体请参考Github. 表格 Markdown Extra 表格语法: 项目 价格 Computer $1600 Phone $12 Pipe $1 写法: 项目 | 价格 ---------|------ Computer | $1600 Phone | $12 Pipe | $1 可以使用冒号来定义对齐方式: 项目 价格 数量 Computer 1600 元 5 Phone 12 元 12 Pipe 1 元 234 写法: | 项目 | 价格 | 数量 | |:--------- |--------:|:----:| | Computer | 1600 元 | 5 | | Phone | 12 元 | 12 | | Pipe | 1 元 | 234 | 插入图片 格式一: ![picture-name](http://xxx.com/xxx.png) 格式二:(方便设置图片尺寸) <img src="http://xxx.com/xxx.png" alt="download-failed" width="XXXpx" height="XXXpx"> 定义列表 Markdown Extra 定义列表语法: 项目1 项目2 定义 A 定义 a 定义 B 项目3 定义 C 定义 D 定义 E 写法: - 项目1 - 项目2 - 定义 A - 定义 a - 定义 B 项目3 : 定义 C : 定义 D > 定义 E 代码块 代码块语法遵循标准markdown代码,例如: python: @requires_authorization def somefunc(param1='', param2=0): '''A docstring''' if param1 > param2: # interesting print 'Greater' return (param2 - param1 + 1) or None class SomeClass: pass >>> message = '''interpreter ... prompt''' c语言: #include <stdio.h> int main() { printf("Hello world!"); } 脚注 生成一个脚注[^footnote]. [^footnote]: 这里是 脚注 的 内容. 写法: 生成一个脚注[^footnote]. [^footnote]: 这里是 **脚注** 的 *内容*. 数学公式 使用 MathJax 渲染 LaTex 数学公式,详见 math.stackexchange.com. 行内公式 数学公式为:$ \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N $。 写法: 数学公式为:$ \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N $。 块级公式: 写法: $$ x = \dfrac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$ 更多LaTex语法请参考 这里. 浏览器兼容 目前,本编辑器对 Chrome 浏览器支持最为完整。建议大家使用较新版本的 Chrome。 IE9 以下不支持 IE9,10,11 存在以下问题 不支持离线功能 IE9 不支持文件导入导出 IE10 不支持拖拽文件导入 常用 Markdown 编辑器推荐 Markdownpad: 详情请点击 官网。 (貌似专业版需要收取一定dollars$,需要序列号自行baidu。) Markpad:详情前往 官网。 (推荐使用,Microsoft Store也有,完全免费,支持及时效果浏览。) CSDN博客编辑器:CSDN网站内置编辑器。(这篇文件就是这样写出来的-_-) Harropad: 官网 Retext: 简单强大的文本编辑器,可控制输出格式:pdf, html等,仅支持Linux(推荐).下载

2018/3/27
articleCard.readMore