<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Yuping Wu - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Yuping Wu - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 23 May 2026 08:28:39 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/yuping/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 可改进的 Java BigDecimal 的几种用法 ]]>
                </title>
                <description>
                    <![CDATA[ 我在实际项目中发现现存的代码中对BigDecimal的使用有些可以改进的地方，在此记录下来，供大家参考。 1、new BigDecimal(0)、new BigDecimal("0")、new BigDecimal(1)、new BigDecimal("1")、new BigDecimal(10)、new BigDecimal("10") BigInteger 和 BigDecimal 这两个高精度数字类，对应整数 0、1、10 的值都有对应的常量（BigInteger.ZERO / BigDecimal.ZERO、BigInteger.ONE / BigDecimal.ONE、BigInteger.TEN / BigDecimal.TEN）可供使用。 为了节省创建对象和垃圾回收的开销，我们应该直接使用这些常量。  尤其是 new BigDecimal("0") 这种用法，它不仅会带来创建 BigDecimal 对象的开销，还有把字符串解析成数字的开销。 ZERO、ONE、TEN 这 3 个常量的值是不会变的，因为跟 Integer、Long 这些实现 Number 接口的类一样 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/java-bigdecimal/</link>
                <guid isPermaLink="false">628ef82c60237306d260722e</guid>
                
                    <category>
                        <![CDATA[ Java ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yuping Wu ]]>
                </dc:creator>
                <pubDate>Thu, 26 May 2022 03:50:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/05/web-developer.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我在实际项目中发现现存的代码中对BigDecimal的使用有些可以改进的地方，在此记录下来，供大家参考。</p>
<h2 id="1newbigdecimal0newbigdecimal0newbigdecimal1newbigdecimal1newbigdecimal10newbigdecimal10">1、new BigDecimal(0)、new BigDecimal("0")、new BigDecimal(1)、new BigDecimal("1")、new BigDecimal(10)、new BigDecimal("10")</h2>
<p>BigInteger 和 BigDecimal 这两个高精度数字类，对应整数 0、1、10 的值都有对应的常量（BigInteger.ZERO / BigDecimal.ZERO、BigInteger.ONE / BigDecimal.ONE、BigInteger.TEN / BigDecimal.TEN）可供使用。</p>
<p><strong>为了节省创建对象和垃圾回收的开销，我们应该直接使用这些常量。</strong> 尤其是 new BigDecimal("0") 这种用法，它不仅会带来创建 BigDecimal 对象的开销，还有把字符串解析成数字的开销。</p>
<p>ZERO、ONE、TEN 这 3 个常量的值是不会变的，因为跟 Integer、Long 这些实现 Number 接口的类一样，BigInteger 和 BigDecimal 也是不可变的（immutable），也就是说调用一个 BigDecimal 对象的任何公共的成员方法都不会改变该 BigDecimal 对象的值。执行如 BigDecimal.TEN.multiply(new BigDecimal(15)) 这样的代码不会改变 BigDecimal.TEN 的值。</p>
<h2 id="2bigdecimalzerosubtractbigdecimal">2、BigDecimal.ZERO.subtract(bigDecimal)</h2>
<p><strong>取一个 BigDecimal 变量的相反数，我们可以直接使用 BigDecimal 类的 negate 方法。</strong></p>
<p>BigDecimal 类的核心其实是3个私有的成员属性：一个 BigInteger 属性 value 表示数字的各个位、一个整数属性 scale 表示小数点的位置、一个整数属性 sign 表示数字的符号（-1 表示负数、0 表示 0、1 表示正数）。一个 BigDecimal 对象的值就是 sign * value / (10 ^ scale)。例如 value 属性的值为 15、scale 属性的值为 0、sign 属性的值为 -1，则该 BigDecimal 对象表示 -15；如果 value 属性的值为 15、scale 属性的值为 1、sign 属性的值为 1，则该BigDecimal对象表示 15 / 10 = 1.5；如果 BigInteger 属性的值为 15、小数点属性的值为 -1、符号属性的值为 1，则该 BigDecimal 对象表示 15 * 10 = 150。</p>
<p>negate 方法是调用 BigDecimal 的私有构造方法，直接创建一个 sign 属性为原 BigDecimal 对象的 sign 的相反数的新的 BigDecimal 对象，不会涉及到减法运算的逻辑。如果我们使用 BigDecimal.ZERO 的 subtract 方法，则会带来高精度小数减法的开销（BigDecimal 的 subtract 方法需要先判断两个 BigDecimal 对象的 scale 属性的大小，调整其中一个 BigDecimal 对象的 scale 属性和 value 属性使两个 BigDecimal 对象的 scale 属性相等）。</p>
<h2 id="3bigdecimalcomparetobigdecimalzero0bigdecimalzerosubtractbigdecimalbigdecimal">3、bigDecimal.compareTo(BigDecimal.ZERO) &lt; 0 ? BigDecimal.ZERO.subtract(bigDecimal) : bigDecimal</h2>
<p><strong>取一个 BigDecimal 变量的绝对值，我们可以直接使用 BigDecimal 类的 abs 方法。</strong></p>
<p>上文已提到，BigDecimal 类的值由 value、scale 和 sign 3个私有成员属性决定。abs 方法是调用 BigDecimal 的私有构造方法，直接创建一个 sign 属性为 1 的新的 BigDecimal 对象，可以为我们省去判断一个 BigDecimal 对象的值是否小于 0 的开销。</p>
<h2 id="4bigdecimalmultiplynewbigdecimal100bigdecimaldividenewbigdecimal100">4、bigDecimal.multiply(new BigDecimal(100))、bigDecimal.divide(new BigDecimal(100))</h2>
<p><strong>要得到一个 BigDecimal 变量乘以 10 的整数次幂，我们可以直接使用 BigDecimal 类的 scaleByPowerOfTen 方法。</strong></p>
<p>bigDecimal.scaleByPowerOfTen(n) 返回一个值为 bigDecimal * 10 ^ n 的新的 BigDecimal 对象。scaleByPowerOfTen 方法是调用 BigDecimal 的私有构造方法，直接创建一个 scale 属性等于原 BigDecimal 对象的 scale - n 的新的 BigDecimal 对象，可以为我们省去高精度小数乘除法的开销。</p>
<p>要全面深入了解 BigDecimal 的用法，可参考 <a href="https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html">JavaSE 8 的 API 文档</a>和 <a href="http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/math/BigDecimal.java">OpenJDK 8 中 BigDecimal.java 的源码</a>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Java Bean、EJB、Spring Bean 和一些对象的概念 ]]>
                </title>
                <description>
                    <![CDATA[ JavaBean 根据维基百科上的介绍[1]以及Shaun Abram的一篇博文[2]，一个JavaBean通常是一个有以下3种特点的公共Java类：  1. 有一个无参的构造方法（默认构造方法）；  2. 所有属性都是private的，类外部需要通过public的getter和setter来访问属性；  3. 实现了Serializable接口。 JavaBeans规范[3]中指出，JavaBeans是一种能在开发工具中可视化地编辑的可重用的软件组件，它需要有一系列的属性，可能有一些事件。在JavaBeans规范的第7章中规定了一个JavaBean的属性应该是private的，类外部应该通过pubilc的getter和setter方法来访问属性。在JavaBeans规范的第2章中有规定一个JavaBean需要是可序列化的，以便传输和持久化JavaBean的状态。但是JavaBeans规范中没有规定一个JavaBean必须有一个无参的构造方法。 JavaBeans规范发布于1997年，所以JavaBeans规范其实主要是围绕Java Applet来设计的。Java Applet中 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/java-bean-ejb-spring-bean-and-objects/</link>
                <guid isPermaLink="false">628ef6ed60237306d260721b</guid>
                
                    <category>
                        <![CDATA[ Java ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yuping Wu ]]>
                </dc:creator>
                <pubDate>Thu, 26 May 2022 03:45:42 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/05/artem-sapegin-DErxVSSQNdM-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="javabean">JavaBean</h2>
<p>根据维基百科上的介绍[1]以及Shaun Abram的一篇博文[2]，一个JavaBean通常是一个有以下3种特点的公共Java类：</p>
<ol>
<li>有一个无参的构造方法（默认构造方法）；</li>
<li>所有属性都是private的，类外部需要通过public的getter和setter来访问属性；</li>
<li>实现了Serializable接口。</li>
</ol>
<p>JavaBeans规范[3]中指出，JavaBeans是一种能在开发工具中可视化地编辑的可重用的软件组件，它需要有一系列的属性，可能有一些事件。在JavaBeans规范的第7章中规定了一个JavaBean的属性应该是private的，类外部应该通过pubilc的getter和setter方法来访问属性。在JavaBeans规范的第2章中有规定一个JavaBean需要是可序列化的，以便传输和持久化JavaBean的状态。但是JavaBeans规范中没有规定一个JavaBean必须有一个无参的构造方法。</p>
<p>JavaBeans规范发布于1997年，所以JavaBeans规范其实主要是围绕Java Applet来设计的。Java Applet中的GUI组件会有属性（例如按钮的名称）和事件（例如输入框的输入事件），GUI组件的状态可能需要在服务器端和客户端之间传输，因此JavaBeans的定义中除了属性还包括了事件，而且也规定了JavaBeans需要可序列化。但是现在许多的Java开发是在纯服务器端软件的领域，服务器应用上的如数据库连接这样的对象或者一些实现业务逻辑的对象是不应该在电脑之间传输的，所以我认为这些对象就不适用传统的JavaBeans的定义，至少不适用可序列化这个特性。</p>
<h2 id="ejb">EJB</h2>
<p>EJB是企业级JavaBeans（Enterprise JavaBeans）的英文缩写。根据EJB规范[4]，一个企业级Bean（Enterprise Bean）有以下特性：</p>
<ol>
<li>通常包含操作企业数据的业务逻辑；</li>
<li>由容器在运行时管理；</li>
<li>用户需要通过容器访问企业级Bean；</li>
<li>能在部署时根据运行环境定制；</li>
<li>能通过注解或XML在编译或部署时指定其中使用的一些配置信息（可配置）；</li>
<li>只使用了EJB规范中规定的服务的企业级Bean能在任意EJB容器中使用（可移植）；</li>
<li>企业级Bean可以不需要重新编译就被封装在一个企业级应用中。</li>
</ol>
<p>EJB规范中还指出，一个企业级Bean可以是有状态的，也可以是无状态的；可以实现业务逻辑，也可以代表一个持久化的实体。由此可见，EJB和JavaBeans其实是有挺大区别的，可以说一个EJB并不一定是一个JavaBean。一个EJB也不一定有无参构造方法和实现Serializable接口。</p>
<p>EJB规范中也没有规定一个企业级Bean的属性必须是private的，要通过public的getter和setter来访问。不过我们基于类的封装性和降低类之间的耦合的考虑，通常还是会遵循将属性设为private，为其写public的getter和setter这样的设计。</p>
<h2 id="springbean">Spring Bean</h2>
<p>根据Spring Framework的官方文档[5]，在Spring中由Spring IoC容器管理的构成应用主干的对象就是bean。（注：IoC是控制反转 Inverse of Control 的英文缩写。）Spring beans都是由Spring IoC容器根据XML配置文件或注解等方式来实例化、组装和管理的。</p>
<p>我认为，Spring beans和EJB比较类似，它们通常都是数据库连接、事务管理器、消息中间件连接、Session管理器、数据访问对象、业务逻辑服务之类的，只是Spring beans是由Spring IoC容器管理的，EJB是由EJB容器管理的。Spring beans同样不一定有无参构造方法和实现Serializable接口。</p>
<h2 id="pojo">POJO</h2>
<p>POJO是简单的传统的Java对象（Plain Old Java Object）的英文缩写，另有Plain Ordinary Java Object、Pure Old Java Object等说法，最早由Martin Fowler、Rebecca Parsons和Josh MacKenzie提出。[6][7] 结合Spring文档中对POJO的介绍[8]，我们可以知道，POJO是一种尽量不依赖任何第三方库、框架甚至JavaEE规范的实现的Java对象，它应该尽量不继承任何类、不实现任何接口、不包含任何与第三方库或框架相关的注解。</p>
<p>由上述介绍我们可以发现，POJO和JavaBeans、EJB和Spring beans都没有必然的联系。负责GUI组件的继承java.awt.Component的JavaBeans、实现JavaEE规范中的接口的EJB、实现Spring框架中的接口的Spring beans或者使用了Spring框架中的注解的Spring beans都不是POJO。POJO的定义中同样没有规定它有怎样的构造方法和怎样的属性。</p>
<p>因为一个POJO不依赖任何第三方库和框架，它的可维护性和可移植性会更强，开发人员开发与这个POJO相关的功能时，不必考虑它依赖的第三方库或框架的实现，可以更专注于这个POJO本身的业务功能。第三方库或框架升级的时候，不必对POJO进行修改。POJO给了开发人员充分的灵活性，开发人员可以选择将一个POJO应用在任意一个框架中（例如Spring或者Struts、Hibernate或者MyBatis），或者选择不应用在框架中。</p>
<h2 id="bo">BO</h2>
<p>BO是业务对象（Business Object）的英文缩写。通常认为，业务对象是用于描述业务逻辑中的对象，但业务对象不依赖具体实现。如果换一种实现方式，例如从关系式数据库迁移到非关系式数据库，或者从单点系统改成一个分布式系统，一个业务对象类需要发生改变的话，那这个业务对象类的设计就是不合理的。</p>
<p>业务对象中的属性应该与业务人员、需求人员、客户理解的一致。例如一个用户信息类中，开发人员可能会在里面设置一个”创建时间“字段，但对于业务人员来说，用户没有“创建时间”，只有“注册时间”，那么用户业务对象中的字段就应该叫做“注册时间”而非“创建时间”。又例如，多数互联网应用，删除一个实体的时候并不会从数据库中物理删除，而是在数据库记录中将该实体的记录的状态字段改为“已删除”，这种“已删除”状态的对象可以是下文说的PO或者DTO，但是一个BO。</p>
<p>又例如，用关系式数据库时，通常会用一个中间表/中间对象来帮助描述多对多关系，我认为这种多对多关系对象（通常是一个PO）就不能算是业务对象。</p>
<p>我觉得，由于业务对象是业务人员，通常业务对象只是在需求或者实现设计中出现，很少人会在代码里写一个纯粹的BO类，即使写BO类，也是作为下文所说的DTO、PO或者VO的基类或组成部分。</p>
<h2 id="dto">DTO</h2>
<p>DTO是数据传输对象（Data Transfer Object）的英文缩写。</p>
<p>DTO通常是一个JavaBean（按照有无参构造方法、属性都为private、属性通过public的getter/setter来访问、实现Serializable接口的定义）。DTO也通常是一个POJO，因为要考虑其在交互的系统之间的可移植性。</p>
<h2 id="po">PO</h2>
<p>PO是持久化对象（Persistent Object）的英文缩写。通常我们对PO的理解就跟百度百科[9]中说的一样：一个PO类与一个数据库表对应，一个PO与数据库表中的一行对应。PO通常是也一个JavaBean（按照有无参构造方法、属性都为private、属性通过public的getter/setter来访问、实现Serializable接口的定义）。PO可以是一个POJO，也可能含有JPA规范中定义的一些注解（例如@Entity、@Table、@Column、@Id等）。</p>
<p>虽然在很多架构设计中，DTO和PO等对象没有作区分，都放在model包或者entity包中，我认为一个结构清晰的架构应该对PO与DTO等其他对象作区分，因为存储在数据库中的对象与其他业务对象还是有一些区别的。例如数据库对象通常会有创建时间、创建者的用户ID、最后一次修改时间、最后一次修改者的用户ID、状态、数据库自增ID等字段，但这些信息很多是不需要甚至不应该暴露给用户或者其他系统的，即不应该出现在DTO等其他对象中的。</p>
<p>而在对象关系映射（Object/Relation Mapping，简称ORM）框架Hibernate中，上述含义的PO有三种状态：持久化对象状态（也简称PO）、值对象（Value Object，简称VO）状态、游离（Detached）状态。根据Hibernate的文档[10]，在Hibernate中，与一个Hibernate session绑定的广义PO是一个Hibernate PO，对一个Hibernate PO作修改后，对Hibernate session做flush或close操作时，Hibernate session会将修改后的Hibernate PO的状态持久化到数据库中。开发者手动将Hibernate PO从Hibernate session中解除绑定（detach）之后，该PO就进入游离状态。游离状态的PO可以重新与一个Hibernate session绑定而重新变为持久化对象状态。</p>
<h2 id="vo">VO</h2>
<p>VO有两种含义，一种是值对象（Value Object）的英文缩写，另一种是展现层对象（View Object）的英文缩写。</p>
<p>对于值对象，上文有提到在Hibernate中，值对象是广义PO的一种状态。在Hibernate中，除了持久化对象状态和游离状态的广义PO都是值对象。</p>
<p>值对象也有另一种定义，即所有用于存储数据的对象（如PO和DTO）都是值对象。</p>
<p>展现层对象，又可称“视图对象”，是对应一个客户端页面或者组件中数据的对象。展现层对象跟DTO的结构很相似，都有一些private的属性及其public的getter/setter，因为它们本质上都是用来承载传输的数据，DTO通常用于跨应用传输数据，而展现层对象用于业务逻辑层和客户端页面之间传输数据。对于要不要将DTO和展现层对象合并在一起，下文中推荐的博文有详细的讨论，这里不再赘述。</p>
<h2 id="do">DO</h2>
<p>DO有两种含义，一种是数据对象（Data Object）的英文缩写，另一种是领域对象（Domain Object）的英文缩写。</p>
<p>阿里巴巴的《Java开发手册》中的DO用的就是数据对象这个概念，它的含义跟PO的含义是一样的（一个DO类与一个数据库表对应，一个DO与数据库表中的一行对应）。</p>
<p>而领域对象是领域驱动设计（Domain Driven Design）中的一个概念。对领域驱动设计的解释，推荐大家参考一下<a href="https://mp.weixin.qq.com/s/c_5QUFu778NM67gNSrzvqA">阿里的盒马技术团队的文章</a>。对领域对象的具体解释，推荐大家参考一下《<a href="http://www.cnblogs.com/qixuejia/p/4390086.html">领域驱动设计系列文章——浅析VO、DTO、DO、PO的概念、区别和用处</a>》《<a href="https://blog.csdn.net/zjrbiancheng/article/details/6253232">浅析VO、DTO、DO、PO的概念、区别和用处</a>》这两篇博文。</p>
<h2 id="dao">DAO</h2>
<p>DAO是数据访问对象（Data Access Object）的英文缩写。DAO是对数据库具体实现细节的封装、对数据库访问方法的抽象。[11] DAO通常需要依赖注入容器为其注入数据库连接对象之类的对象，因此DAO通常是一个EJB或者是Spring bean。</p>
<h2 id="">再啰嗦两句</h2>
<p>我认为，上文所说的BO、DTO、PO、VO（展现层对象）、DO和DAO，其作用、功能、职责都是有区别的，为了一个软件工程的结构清晰、软件的部件的功能明确，为了最大程度的软件的可拓展性、可移植性和可维护性，应该将这些对象分别放在不同的包（package）中，不要将这些对象混淆或合并在一起使用，虽然这样会使首次开发时的工作量增加。</p>
<h2 id=""><strong>参考资料</strong></h2>
<p>[1] DropDeadGorgias, Fvdham, JimmyShelter, 等．JavaBeans[M/OL]．<a href="https://en.wikipedia.org/wiki/JavaBeans">https://en.wikipedia.org/wiki/JavaBeans</a>, 引用于2018-09-05 10:11</p>
<p>[2] Shaun Abram. JavaBeans vs Spring beans vs POJOs[J/OL]. <a href="http://www.shaunabram.com/beans-vs-pojos/">http://www.shaunabram.com/beans-vs-pojos/</a>, 引用于2018-09-05 10:16</p>
<p>[3] Sun Microsystems, JavaBeans(TM) API specification Version 1.01-4[S/OL], 1997-08-08:9. <a href="http://download.oracle.com/otndocs/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec/">http://download.oracle.com/otndocs/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec/</a>, 引用于2018-09-05 10:53</p>
<p>[4] Sun Microsystems, JSR-000220 Enterprise JavaBeans v.3.0 Final Release[S/OL], 2006-05-08:30-35. <a href="http://download.oracle.com/otndocs/jcp/ejb-3_0-fr-eval-oth-JSpec/">http://download.oracle.com/otndocs/jcp/ejb-3_0-fr-eval-oth-JSpec/</a>, 引用于2018-09-05 11:19</p>
<p>[5] Spring Source, Core Technologies[M/OL], 2018-07-26. <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-introduction">https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-introduction</a>, 引用于2018-09-05 12:24</p>
<p>[6] Chronist, WikiFan04, Graham87, 等. Plain old Java object[M/OL]. <a href="https://en.wikipedia.org/wiki/Plain_old_Java_object">https://en.wikipedia.org/wiki/Plain_old_Java_object</a>, 引用于2018-09-05 12:40</p>
<p>[7] chunchill. 理解POCO[J/OL], 2011-01-08. <a href="http://www.cnblogs.com/shineqiujuan/archive/2011/01/08/1930911.html">http://www.cnblogs.com/shineqiujuan/archive/2011/01/08/1930911.html</a>, 引用于2018-09-05 12:47</p>
<p>[8] Spring Source. Understanding POJOs[M/OL]. <a href="https://spring.io/understanding/POJO">https://spring.io/understanding/POJO</a>, 引用于2018-09-05 12:54</p>
<p>[9] 匿名. （持久对象 (persistent object)）[M/OL]. <a href="https://baike.baidu.com/item/Po/6446468">https://baike.baidu.com/item/Po/6446468</a>, 引用于2018-09-05 18:27</p>
<p>[10] hibernate.org. Working with objects[M/OL]. <a href="http://docs.jboss.org/hibernate/orm/3.3/reference/en/html/objectstate.html">http://docs.jboss.org/hibernate/orm/3.3/reference/en/html/objectstate.html</a>, 引用于2018-09-06 15:49</p>
<p>[11] Anonymous. Data access object[M/OL]. <a href="https://en.wikipedia.org/wiki/Data_access_object">https://en.wikipedia.org/wiki/Data_access_object</a>, 引用于2018-09-05 14:34</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 一文了解 Java 中的 String、StringBuffer 与 StringBuilder ]]>
                </title>
                <description>
                    <![CDATA[ String、StringBuffer 和 StringBuilder 是 java.lang 包里的 3 个与字符串密切相关的类，它们都实现了 CharSequence、Comparable 和 Serializable 接口。 在 OpenJRE 的实现中，它们都有一个 byte[] 类型的 value 变量用于存储字符串的编码后的值（encoded value），以及一个 byte 类型的 coder 变量用于指示 value 中存储字符用的是 Latin1 编码还是 UTF-16 编码。 String 是不可变的（immutable）；而 StringBuffer 和 StringBuilder 都继承了抽象类 AbstractStringBuilder，都是可变的（mutable）。 StringBuffer 中的绝大多数所有公有的成员方法（public method）都是 synchronized 的，没加 synchronized 的公有方法也是对其他 synchronized 的方法的包装；而StringBuilder 中的方法都不是 synchronized 的 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/java-string-stringbuffer-and-stringbuilder/</link>
                <guid isPermaLink="false">5f89617f5f583f0565090b0f</guid>
                
                    <category>
                        <![CDATA[ Java ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 后端开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 字符串 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yuping Wu ]]>
                </dc:creator>
                <pubDate>Thu, 25 Mar 2021 11:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/10/iswanto-arif-_ihGttRnT_I-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>String、StringBuffer 和 StringBuilder 是 java.lang 包里的 3 个与字符串密切相关的类，它们都实现了 CharSequence、Comparable 和 Serializable 接口。</p><p>在 OpenJRE 的实现中，它们都有一个 byte[] 类型的 value 变量用于存储字符串的编码后的值（encoded value），以及一个 byte 类型的 coder 变量用于指示 value 中存储字符用的是 Latin1 编码还是 UTF-16 编码。</p><p>String 是不可变的（immutable）；而 StringBuffer 和 StringBuilder 都继承了抽象类 AbstractStringBuilder，都是可变的（mutable）。</p><p>StringBuffer 中的绝大多数所有公有的成员方法（public method）都是 synchronized 的，没加 synchronized 的公有方法也是对其他 synchronized 的方法的包装；而StringBuilder 中的方法都不是 synchronized 的。</p><p>下面进行更详细的介绍。</p><h3 id="1-string">1、String</h3><p>String 是 immutable 的，即除非利用反射强制修改它的值，否则一个 String 对象一旦被创建，其值就不会被修改。</p><p>Java 使 String 类为 immutable 的实现方式是：String 类是 final 的，不可以被继承，而且其成员属性（property）都是 private 的，也没有 public 的方法会对字符串的值（String 中的 value 属性进行修改）。</p><p>GrepCode 网站上的 <a href="http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/lang/String.java?av=f" rel="nofollow noopener">OpenJDK 8 中 String 类的实现</a>的截图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/1acc8ba86528b8fb8e3cd7aac5e3a38b45149907.png" class="kg-image" alt="1acc8ba86528b8fb8e3cd7aac5e3a38b45149907" width="600" height="400" loading="lazy"></figure><p>String 的 replace、concat 和 substring 方法，都会返回一个新的 String 对象，而不是对原来的 String 对象进行修改。你可以用如下代码验证这一点：</p><pre><code>String a = "aaa";
String b = a.replace("a", "b"); // 不会对a的值产生影响
System.out.println(a == b); // false，b是一个新创建的字符串对象
System.out.println(a); // 此时a还是"aaa"而不是"bbb"

String c = a.concat("cc");
System.out.println(c); // aaacc
System.out.println(a == c); // false，c是一个新创建的字符串对象
System.out.println(a); // 此时a还是"aaa"而不是"aaacc"

String d = a.substring(2);
System.out.println(d); // a
System.out.println(a == d); // false，d是一个新创建的字符串对象
System.out.println(a); // 此时a还是"aaa"而不是"a"
</code></pre><p>在需要频繁对字符串做修改操作的场景下（最常见的是在字符串后追加内容的场景），如果只用String类提供的方法（如concat方法），则每次字符串操作都会创建一个新的String对象，这样对性能会有影响。例如：</p><pre><code>for (int i = 0; i &lt; 1000; i += 1) {
    str = str + arr[i] + ',';
}
</code></pre><p>如果用String的concat方法来实现，则实际的实现代码会变成：</p><pre><code>for (int i = 0; i &lt; 1000; i += 1) {
    str = str.concat(arr[i]).concat(",");
}
</code></pre><p>这样在 1000 次循环里，每次都会新创建两个 String 对象，一共需要创建 2000 个 String 对象，这对系统性能会造成影响。</p><h3 id="2-stringbuffer">2、StringBuffer</h3><p>为了应对频繁对字符串做修改操作的场景，Java 从 JDK1 开始就提供了 mutable 的 StringBuffer 类。StringBuffer 类对外暴露了可以修改其值的 append、insert、delete 等方法。一个 StringBuffer 对象在其缓冲区（一个字符数组 char[]）的容量足够的情况下，调用这些方法可以直接修改 StringBuffer 的值而不必创建新的对象。（在一个StringBuffer &nbsp;的缓冲区的容量不足的时候，调用其 append 或者 insert 就会使 StringBuffer 创建一个新的更大的缓冲区，这时则会创建一个新的字符数组 char[] 对象。）</p><p>因此，如果上一小节中循环追加内容到字符串的代码改用以下的实际实现，则创建的对象会少很多：</p><pre><code>StringBuffer buffer = new StringBuffer(str);
for (int i = 0; i &lt; 1000; i += 1) {
    buffer.append(arr[i]).append(',');
}
str = buffer.toString();
</code></pre><p>因此在 JDK 1.5 以前，Java 编译器都是把字符串 + 字符串那样的代码编译成使用 StringBuffer.append 方法，或者说字符串的加号运算符其实是 StringBuffer.append 的一个语法糖。</p><h3 id="3-stringbuilder">3、StringBuilder</h3><p>那从 JDK 1.5 开始又如何呢？从 JDK 1.5 开始，Java 编译器改用了 StringBuilder。</p><p>StringBuilder 类是非线程安全的 StringBuffer 类。即：</p><pre><code>StringBuffer buffer = new StringBuffer();
Thread a = new Thread(new Runnable() {
    public void run() {
        buffer.append("aaaaaaaa");
    }
});
Thread b = new Thread(new Runnable() {
    public void run() {
        buffer.append("bbbbbbbb");
    }
});
a.start(); b.start();
a.join(); b.join();
System.out.println(buffer.toString());
// 要不就是 aaaaaaaabbbbbbbb，要不就是 bbbbbbbbaaaaaaaa

StringBuilder builder = new StringBuilder();
a = new Thread(new Runnable() {
    public void run() {
        builder.append("aaaaaaaa");
    }
});
b = new Thread(new Runnable() {
    public void run() {
        builder.append("bbbbbbbb");
    }
});
a.start(); b.start();
a.join(); b.join();
System.out.println(builder.toString());
// 这时候情况就复杂多了，有可能 a 线程追加a的过程执行到一般被系统暂停，然后系统调度 b 线程执行，这样的话结果就可能是 aaaabbbbbbbbaaaa
</code></pre><p>为了实现线程安全，StringBuffer 在 insert、append、delete 这些 public 方法的定义处加了synchronized关键字：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/ae39f839111946d405c8e88ef04185c60c223222.png" class="kg-image" alt="ae39f839111946d405c8e88ef04185c60c223222" width="600" height="400" loading="lazy"></figure><p>但是，实际应用时，对字符串追加内容的操作几乎都是在一个线程中进行的（例如最开始的循环向字符串追加内容的代码），这样如果用 StringBuffer 的话，就会额外有给 StringBuffer 对象加锁的开销。因此从 JDK 1.5 开始，编译器改为了使用 StringBuilder 来实现字符串 + 字符串的操作，即字符串的加号运算符的变成了 StringBuilder.append 的语法糖。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JavaScript 的面向对象 ]]>
                </title>
                <description>
                    <![CDATA[ 1. 什么是类 在说 JavaScript 的面向对象的实现方法之前，我们先来看面向对象编程的一个核心概念——类（class）。 类是对拥有同样属性（property）和行为的一系列对象（object）的抽象。  这里说的“行为”，在基于类的面向对象的语言中通常叫做类的方法（method）。而在 JavaScript 里，函数也是“一等公民”，可以被直接赋值给一个变量或一个对象的属性，因此在本文后续的讨论中，把“行为”也归入“属性”的范畴。 2. JavaScript 对“类”的实现 JavaScript 一开始是被设计成在网页上对表单进行校验或者对网页上的元素进行操纵的一种脚本语言，没有像 C++ 和 Java 那样用 class、 private、protected 等关键字来定义类的语法。JavaScript 采用的是一种更简单的实现方式：既然类就是拥有同样属性的一系列对象，那么只要通过一种方式能使某一些对象拥有同样的属性就行了。 JavaScript 规定每一个对象都可以有一个原型（[[prototype]] 内部属性）。（在实现 ECMAScript 5.1 规范以前，除 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/object-oriented-programming-in-javascript/</link>
                <guid isPermaLink="false">5f97c1f55f583f0565090c2e</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 面向对象 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yuping Wu ]]>
                </dc:creator>
                <pubDate>Thu, 04 Mar 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/10/michiel-annaert-gI8uZx_wYJ8-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="1-">1. 什么是类</h2><p>在说 JavaScript 的面向对象的实现方法之前，我们先来看面向对象编程的一个核心概念——类（class）。<strong>类是对拥有同样属性（property）和行为的一系列对象（object）的抽象。</strong> 这里说的“行为”，在基于类的面向对象的语言中通常叫做类的方法（method）。而在 JavaScript 里，函数也是“一等公民”，可以被直接赋值给一个变量或一个对象的属性，因此在本文后续的讨论中，把“行为”也归入“属性”的范畴。</p><h2 id="2-javascript-">2. JavaScript 对“类”的实现</h2><p>JavaScript 一开始是被设计成在网页上对表单进行校验或者对网页上的元素进行操纵的一种脚本语言，没有像 C++ 和 Java 那样用 <code>class</code>、<code>private</code>、<code>protected</code> 等关键字来定义类的语法。JavaScript 采用的是一种更简单的实现方式：既然类就是拥有同样属性的一系列对象，那么只要通过一种方式能使某一些对象拥有同样的属性就行了。</p><p>JavaScript 规定每一个对象都可以有一个原型（<code>[[prototype]]</code> 内部属性）。（在实现 ECMAScript 5.1 规范以前，除了 <code>Object.prototype</code> 以外的对象都必须有一个原型。）每个对象都“共享”其原型的属性：在访问一个对象的属性时，如果该对象本身没有这个属性，则 JavaScript 会继续试图访问其原型的属性。这样，就可以<strong>通过指定一些对象的原型来使这些对象都拥有同样的属性</strong>。从而我们可以这样认为，<strong>在 JavaScript 中，以同一个对象为原型的对象就是属于同一个类的对象</strong>。</p><h3 id="2-1-javascript-">2.1. JavaScript 中对象的原型的指定方式</h3><p>那么 JavaScript 中的对象与其原型是怎样被关联起来的呢？或者说，JavaScript 中的对象的原型是怎样被指定的呢？</p><p><strong>2.1.1. new 操作符</strong></p><p>JavaScript 有一个 new 操作符（operator），它基于一个函数来创建对象。这个用 new 操作符创建出来的对象的原型就是 new 操作符后面的函数（称为“构造函数”）的 prototype 属性。例如：</p><pre><code>var a = {"aa": 1};
function B() {}
B.prototype = a;
var b = new B();
</code></pre><p>此时 b 对象的原型就是 a 对象。我在<a href="https://chinese.freecodecamp.org/forum/t/javascript-new/113">另一篇文章 1</a>中介绍了 new 操作符的具体实现逻辑，供大家参考。</p><p><strong>2.1.2. Object.create 方法</strong></p><p>Object.create 方法直接以给定的对象作为原型创建对象。一个代码例子：</p><pre><code>var a = {"aa": 1};
var b = Object.create(a);
</code></pre><p>此时 b 对象的原型就是 a 对象。关于 Object.create 方法的实现细节，大家可参考我的<a href="https://chinese.freecodecamp.org/forum/t/object-create/114/2">这篇文章</a>。</p><p><strong>2.1.3. Object.setPrototypeOf 方法</strong></p><p>new 操作符和 Object.create 方法都是在创建一个对象的同时就指定其原型。而 Object.setPrototypeOf 方法则是指定一个已被创建的对象的原型。代码例子：</p><pre><code>var a = {"aa": 1};
var b = Object.create(a);
// 此时 b 的原型是 a
var c = {"cc": 2};
Object.setPrototypeOf(b, c);
// 此时 b 的原型变为 c 了
</code></pre><p><strong>2.1.4. 隐式指定</strong></p><p>数字、布尔值、字符串、数组和函数在 JavaScript 中也是对象，而它们的原型是被 JavaScript 隐式指定的：</p><ol><li>函数（例如 <code>function () {}</code>、<code>function (a) { return a + '1'; }</code>） 的原型为 <code>Function.prototype</code>；</li><li>数组（如 <code>[]</code>、<code>[1, '2']</code>）的原型是 <code>Array.prototype</code>；</li><li>用花括号直接定义的对象（如 <code>{}</code>, <code>{"a": 1}</code>）的原型是 <code>Object.prototype</code>。</li></ol><h3 id="2-2-javascript-">2.2. JavaScript 中定义类的代码示例</h3><p>下面给出定义一个类的一段 JavaScript 代码的示例。它定义一个名为 Person 的类，它的构造函数接受一个字符串的名称，还一个方法 introduceSelf 会输出自己的名字。</p><pre><code>// ----==== 类定义开始 ====----
function Person(name) {
    this.name = name;
}
Person.prototype.introduceSelf = function () {
    console.log("My name is " + this.name);
};
// ----==== 类定义结束 ====----
// 下面实例化一个 Person 类的对象
var someone = new Person("Tom");
// 此时 someone 的原型为 Person.prototype
someone.introduceSelf(); // 输出 My name is Tom
</code></pre><p>如果转换为 ECMAScript 6 引入的类声明（class declaration）语法，则上述 Person 类的定义等同于：</p><pre><code>class Person {
    constructor(name) {
        this.name = name;
    }
    introduceSelf() {
        console.log("My name is " + this.name);
    }
}
</code></pre><h3 id="2-3-">2.3. 对“构造函数”的再思考</h3><p>在上面的例子中，假如我们不通过 <code>Person.prototype</code> 来定义 introduceSelf 方法，而是在构造函数中给对象指定一个 introduceSelf 属性：</p><pre><code>function Person(name) {
    this.name = name;
    this.introduceSelf = function () {
        console.log("My name is " + this.name);
    };
}
var someone = new Person("Tom");
someone.introduceSelf(); // 也会输出 My name is Tom
</code></pre><p>虽然这种方法中，通过 Person 构造函数 new 出来的对象也都有 introduceSelf 属性，但这里 introduceSelf 变成了 someone 自身的一个属性而不是 Person 类的共有的属性：</p><pre><code>function Person1(name) {
    this.name = name;
}
Person1.prototype.introduceSelf = function () {
    console.log("My name is " + this.name);
};
var a = new Person1("Tom");
var b = new Person1("Jerry");
console.log(a.introduceSelf === b.introduceSelf); // 输出 true
delete a.introduceSelf;
a.introduceSelf(); // 仍然会输出 My name is Tom，因为 introduceSelf 不是 a 自身的属性，不会被 delete 删除
b.introduceSelf = function () {
    console.log("I am a pig");
};
Person1.prototype.introduceSelf.call(b); // 输出 My name is Jerry
// 即使 b 的 introduceSelf 属性被覆盖，我们仍然可以通过 `Person1.prototype` 来让 b 执行 Person1 类规定的行为。
</code></pre><pre><code>function Person2(name) {
    this.name = name;
    this.introduceSelf = function () {
        console.log("My name is " + this.name);
    };
}
a = new Person2("Tom");
b = new Person2("Jerry");
console.log(a.introduceSelf === b.introduceSelf); // 输出 false
// a 的 introduceSelf 属性与 b 的 introduceSelf 属性是不同的对象，分别占用不同的内存空间。
// 因此这种方法会造成内存空间的浪费。
delete a.introduceSelf;
a.introduceSelf(); // 会抛 TypeError
b.introduceSelf = function () {
    console.log("I am a pig");
};
// 此时 b 的行为已经与 Person2 类规定的脱节，对象 a 和对象 b 看起来已经不像是同一个类的对象了
</code></pre><p>但是这种方法也不是一无是处。例如我们需要利用闭包来实现对 name 属性的封装时：</p><pre><code>function Person(name) {
    this.introduceSelf = function () {
        console.log("My name is " + name);
    };
}
var someone = new Person("Tom");
someone.name = "Jerry";
someone.introduceSelf(); // 输出 My name is Tom
// introduceSelf 实际用到的 name 属性已经被封装起来，在 Person 构造函数以外的地方无法访问
// name 相当于 Person 类的一个私有（private）成员属性
</code></pre><h2 id="3-javascript-">3. JavaScript 的类继承</h2><p>类的继承实际上只需要实现：</p><ol><li>子类的对象拥有父类定义的所有成员属性；</li><li>子类的任何一个构造函数都必须在开头调用父类的构造函数。</li></ol><p>实现第 2 点的方式比较直观。而怎样实现第 1 点呢？其实我们只需要让子类的构造函数的 prototype 属性 <em>（子类的实例对象的原型）</em> 的原型是父类的构造函数的 prototype 属性 <em>（父类的实例对象的原型）</em>，简而言之就是：<strong>把父类实例的原型作为子类实例的原型的原型</strong>。这样在访问子类的实例对象的属性时，JavaScript 会沿着原型链找到子类规定的成员属性，再找到父类规定的成员属性。而且<strong>子类可在子类构造函数的 prototype 属性中重载（override）父类的成员属性</strong>。</p><h3 id="3-1-">3.1. 代码示例</h3><p>下面给出一个代码示例，定义一个 ChinesePerson 类继承上文中定义的 Person 类：</p><pre><code>function ChinesePerson(name) {
    Person.apply(this, name); // 调用父类的构造函数
}
ChinesePerson.prototype.greet = function (other) {
    console.log(other + "你好");
};
Object.setPrototypeOf(ChinesePerson.prototype, Person.prototype); // 将 Person.prototype 设为 ChinesePerson.prototype 的原型

var someone = new ChinesePerson("张三");
someone.introduceSelf(); // 输出“My name is 张三”
someone.greet("李四"); // 输出“李四你好”
</code></pre><p>上述定义 ChinesePerson 类的代码改用 ECMAScript 6 的类声明语法的话，就变成：</p><pre><code>class ChinesePerson extends Person {
    constructor(name) {
        super(name);
    }

    greet(other) {
        console.log(other + "你好");
    }
}
</code></pre><p><strong>3.1.1. 重载父类成员属性的代码示例</strong></p><p>你会不会觉得上面代码示例中，introduceSelf 输出半英文半中文挺别扭的？那我们让 ChinesePerson 类重载 introduceSelf 方法就好了：</p><pre><code>ChinesePerson.prototype.introduceSelf = function () {
    console.log("我叫" + this.name);
};
var someone = new ChinesePerson("张三");
someone.introduceSelf(); // 输出“我叫张三”

var other = new Person("Ba Wang");
other.introduceSelf(); // 输出 My name is Ba Wang
// ChinesePerson 的重载并不会影响父类的实例对象</code></pre> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 函数与闭包的前世今生（二） ]]>
                </title>
                <description>
                    <![CDATA[ 2.2、计算机的发展 在上一篇《函数与闭包的前世今生（一） [https://chinese.freecodecamp.org/news/function-and-closure-part-one/] 》中，我说过最早的计算机输入输出用的都是纸带。后来随着电磁元件技术的进步，纸带变成了容量更大的磁带，磁带又变成了磁盘。 磁盘不仅比磁带更轻便更易保存，而且读写速度可以比磁带快很多。例如同样存储 1000 个数，读完第 1 个数想读第 1000 个数，读磁带需要磁带移动很久，但读磁盘则只需要磁盘旋转一点加上读写头移动一点就好了。 磁盘又可分为容量较小、较易损坏的软磁盘和容量较大、不易损坏的硬磁盘（硬盘，英文 Hard Drive）。而到了今天我们还有速度更快的固态硬盘（英文 Solid State Drive，缩写 SSD）。 使用磁带的计算机： 磁带计算机软磁盘与读取软磁盘的软驱： 2.2.1、CPU 与 IO 设备的分离 为了更好的组件分工和更快的处理速度，首先，图灵机模型中的数据处理和内部状态存储部分与读写头部分分离开，数据处理和内部状态存储部分放在中央处理器（英文 C ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/function-and-closure-part-two/</link>
                <guid isPermaLink="false">5f895d6a5f583f0565090af0</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 闭包 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yuping Wu ]]>
                </dc:creator>
                <pubDate>Fri, 16 Oct 2020 08:55:20 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/10/emile-perron-xrVDYZRGdw4-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="2-2-">2.2、计算机的发展</h2><p>在上一篇《<a href="https://chinese.freecodecamp.org/news/function-and-closure-part-one/">函数与闭包的前世今生（一）</a>》中，我说过最早的计算机输入输出用的都是纸带。后来随着电磁元件技术的进步，纸带变成了容量更大的磁带，磁带又变成了磁盘。</p><p>磁盘不仅比磁带更轻便更易保存，而且读写速度可以比磁带快很多。例如同样存储 1000 个数，读完第 1 个数想读第 1000 个数，读磁带需要磁带移动很久，但读磁盘则只需要磁盘旋转一点加上读写头移动一点就好了。</p><p>磁盘又可分为容量较小、较易损坏的软磁盘和容量较大、不易损坏的硬磁盘（<strong>硬盘</strong>，英文 Hard Drive）。而到了今天我们还有速度更快的固态硬盘（英文 Solid State Drive，缩写 SSD）。</p><p>使用磁带的计算机：<br></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/optimized/1X/ce9805b382dc0d54fdb1a3354e41be280fa07878_2_425x250.png" class="kg-image" alt="磁带计算机" width="600" height="400" loading="lazy"><figcaption><strong><strong>磁带计算机</strong></strong></figcaption></figure><p>软磁盘与读取软磁盘的软驱：<br></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/46ad52a7dffdd4e614463e48395c1f9f8636aec0.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><h3 id="2-2-1-cpu-io-">2.2.1、CPU 与 IO 设备的分离</h3><p>为了更好的组件分工和更快的处理速度，首先，图灵机模型中的数据处理和内部状态存储部分与读写头部分分离开，数据处理和内部状态存储部分放在<strong>中央处理器</strong>（英文 Central Processing Unit，缩写 <strong>CPU</strong>）芯片中，而原本读写头的功能由 IO 芯片和硬盘承担。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/d6cb1e953a90082f2470ee691fd4215978baed45.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>有了 IO 芯片之后，输入输出设备也不仅限于硬盘了，输入设备还可以是键盘、鼠标、触摸板等等，输出设备还可以是屏幕、打印机等等。</p><h3 id="2-2-2-">2.2.2、内存</h3><p>随着集成电路技术的发展，CPU 频率的逐渐地比硬盘快了成千上万倍，于是人们在 IO 芯片和 CPU 之间加了一层速度比硬盘快、但容量比硬盘小、而且只有通电的时候能存储的<strong>内存</strong>（学术名为<strong>随机访问存储器</strong>，英文 Random Access Memory，缩写 RAM）。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/9013e03d534b74f874df59b6c3f2da22edbdf095.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>内存只有通电的时候能存储，断电之后存储的数据会马上丢失，这个特点被称为<strong>易失性（volatile）</strong>。而硬盘、软盘、U 盘等断电后还能存储数据的存储器则可被称为<strong>非易失性</strong>存储器。</p><p>现在的电子计算机（包括服务器、个人电脑以及智能手机）中，CPU 大多数情况下都是从内存中读取代码执行，从内存中读写数据，少部分时间才会从 IO 芯片读写数据。或许因为这个原因，内存才被称为<strong>内</strong>存。而硬盘、软盘等其他存储设备则可以说是<strong>外</strong>存（虽然硬盘现在也安装在电脑机壳里、智能手机里的非易失性存储芯片安装在手机里）。</p><p>为了提升 IO 速度，一些 IO 芯片也可以直接读写内存。</p><p>有了内存之后，读写数据不再需要移动纸带、磁带或者磁盘的读写头，而只需要一个某个内存存储单元的编号（内存地址）即可。</p><p>CPU 执行指令也不再是移动程序代码纸带，而是在每个 CPU 核心内有一个专门记录当前执行的指令的内存地址的寄存器（称为<strong>当前指令地址寄存器</strong>或者<strong>指令指针寄存器</strong>），CPU 会拿这个寄存器里的地址去内存中找接下来要执行的指令。</p><h2 id="2-3-">2.3、子程序与函数</h2><p>计算机科学家和工程师很快就发现，有一些程序功能经常被用到，在很多地方都需要用到，例如计算某个数的正整数次方、排序一个数组（array）、将存储器的某一大块数据从一个地方复制到另一个地方等等。如果每次使用这些功能的时候都将实现这些功能的代码重新写一次，那么就很浪费代码的存储空间。所以计算机科学家和工程师决定将一些会反复用到的功能的代码抽离出来，抽离出来的这些小功能代码就叫做<strong>子程序 (subroutine)</strong> 或者<strong>函数</strong>。要用到这些功能的时候，先跳转到子程序的代码里执行，执行完再跳转回原来的地方继续执行。</p><ul><li>PS：注意，在这里我们定义子程序或者函数为一些<strong>代码</strong>，而不包括这些代码所处理的数据。因此我在《<a href="https://chinese.freecodecamp.org/news/function-and-closure-part-one/">函数与闭包的前世今生（一）</a>》中倾向于把<strong>闭包</strong>定义成函数与其能访问的自由变量所组成的<strong>词法环境</strong>而不是能访问自由变量的<strong>函数</strong>。</li></ul><p>但是把一些代码抽离出来作子程序会带来 4 个问题：</p><ol><li>这些子程序可能需要一些<strong>输入参数</strong>，这些输入参数如何从子程序的调用方传递给子程序？</li><li>子程序怎么知道执行完之后应该返回到什么地方（函数的<strong>返回地址</strong>）继续执行？</li><li>子程序的执行结果（函数的<strong>返回值</strong>）怎么传递给调用方？</li><li>子程序里面可能用到一些临时的存储空间（<strong>局部变量</strong>），这些存储空间应该如何分配？</li></ol><p>在此解释一下第 4 条。在没有子程序的情况下，这些功能代码需要用到临时的存储空间时，可以使用全局的存储空间，即程序开始运行时就分配好的空间。但是有子程序而且子程序可能递归调用自己的话，如果用全局存储空间，那么在递归调用自己的时候，第二次调用就可能覆盖掉第一次调用的值。</p><h3 id="2-2-1-">2.2.1、函数的底层实现</h3><p>由于函数调用具有先调用的函数后返回、后调用的函数先返回的 <strong>F</strong>irst <strong>I</strong>n <strong>L</strong>ast <strong>O</strong>ut 的特点，我们自然而然地可以想到用<strong>栈 (stack)</strong> 这个数据结构来解决上述 4 个问题中的 1、2、4：</p><ol><li>函数的输入参数由调用方压入栈中，函数中需要用到输入参数时从栈上获取；</li><li>函数调用方在调用函数前将返回地址也压入栈（push）中，函数返回时将返回地址从栈上弹出并写到<strong>当前指令地址寄存器</strong>中；</li><li>函数中要用到的局部变量在栈上分配空间，函数返回前将局部变量占用的栈空间释放掉。</li></ol><p>用一段 JavaScript 代码举个例子。</p><p>当前准备执行第 16 行的函数调用：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/a3b239d923c615ae1faf0bde47787a99443af840.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>将输入参数和返回地址压入栈中，在栈中开辟局部变量的空间，开始执行函数中的代码：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/b3e7915cc97d2b6fcc3532155871a3777c271888.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>start = 0 小于 end = 1，跳过第 3、4、5 行。第 6 行计算出 mid 为 0。target = 2 大于 arr[mid] = 1，跳过第 7 到第 10 行，进入第 11 行：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/1e82af9d45835fbdd1c9737ac7570839bc8cc36a.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>再次将输入参数和返回地址压入栈中，在栈中开辟局部变量的空间，开始执行函数中的代码：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/e1ad70da12d0db93e3865727fcd07a84eb70a532.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>start = 1 不大于 end = 1，跳过第 3、4、5 行。第 6 行计算出 mid 为 1。target = 2 等于 arr[mid] = 2，进入第 8 行：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/c8fe5400e96e6cdb2240dfa329469c3eb79e537f.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>第二次函数调用返回（清理局部变量占用的栈空间，从栈上获取并跳转到返回地址，清理输入参数占用的栈空间）：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/aed697a6c69f968c6707a56d3b5a7331f357a65e.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>第一次函数调用返回（清理局部变量占用的栈空间，从栈上获取并跳转到返回地址，清理输入参数占用的栈空间）：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/0e48614869c849a37c5aa43dce07862e99eaa175.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 开发者如何选择编程语言（一） ]]>
                </title>
                <description>
                    <![CDATA[ “PHP是最好的语言！”“人生苦短，我用 Python！”“JavaScript 一统天下”……关于编程语言的讨论和争论时不时就在我们眼前出现。 其实没有一种编程语言在所有领域（操作系统、硬件驱动、办公软件、游戏、Web 服务器、数据库、搜索引擎、大数据处理、高频交易、机器学习、数值计算等等）都是银弹。我们要学会根据应用场景选择最适合的编程语言。 那么我们如何根据应用场景和编程语言的特性来选择适合的编程语言呢？我认为可以考察编程语言的以下特点和特性： 1. 执行方式 1.1. 汇编执行型 汇编语言（assembly）的源文件由汇编器（assembler）转换为 CPU 可直接执行的二进制程序文件。 1.2. 编译、（汇编）、执行型 C / C++ / Fortran / Pascal / Go / Rust 等编程语言的源文件一般由编译器（compiler）先编译为汇编指令，再由汇编器生成 CPU 可直接执行的二进制程序文件。 现在编译器一般不会把编译出来的汇编指令输出到文件，而通常在内存中直接交给内置的汇编器进行处理，所以我们会看到这些编程语言的编译器直接就输出一个可执行的 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-choose-a-programming-language/</link>
                <guid isPermaLink="false">5f8038aa5f583f0565090a33</guid>
                
                    <category>
                        <![CDATA[ 编程语言 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 编程学习 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 软件开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yuping Wu ]]>
                </dc:creator>
                <pubDate>Fri, 09 Oct 2020 10:25:06 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/10/daria-shevtsova-zbWFT4eVopE-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>“PHP是最好的语言！”“人生苦短，我用 Python！”“JavaScript 一统天下”……关于编程语言的讨论和争论时不时就在我们眼前出现。</p><p>其实没有一种编程语言在所有领域（操作系统、硬件驱动、办公软件、游戏、Web 服务器、数据库、搜索引擎、大数据处理、高频交易、机器学习、数值计算等等）都是银弹。我们要学会根据应用场景选择最适合的编程语言。</p><p>那么我们如何根据应用场景和编程语言的特性来选择适合的编程语言呢？我认为可以考察编程语言的以下特点和特性：</p><h2 id="1-">1. 执行方式</h2><h3 id="1-1-">1.1. 汇编执行型</h3><p>汇编语言（assembly）的源文件由汇编器（assembler）转换为 CPU 可直接执行的二进制程序文件。</p><h3 id="1-2-">1.2. 编译、（汇编）、执行型</h3><p>C / C++ / Fortran / Pascal / Go / Rust 等编程语言的源文件一般由编译器（compiler）先编译为汇编指令，再由汇编器生成 CPU 可直接执行的二进制程序文件。</p><p>现在编译器一般不会把编译出来的汇编指令输出到文件，而通常在内存中直接交给内置的汇编器进行处理，所以我们会看到这些编程语言的编译器直接就输出一个可执行的程序文件。</p><p>C / C++ 的编译器 GCC / G++ 和编译器后端 LLVM 都可以把编译出来的汇编指令输出到文件中，供我们查看。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/ilya-pavlov-OqtafYT5kTw-unsplash.jpg" class="kg-image" alt="ilya-pavlov-OqtafYT5kTw-unsplash" width="600" height="400" loading="lazy"></figure><h3 id="1-3-">1.3. 编译、解释执行型</h3><p>Java / Scala 源文件由 Java 编译器编译成二进制的 class 文件，由 Java 虚拟机（Java virtual machine ，简称 JVM）解释执行。C# / <code>VB.net</code> 源文件由编译器编译为二进制的 exe 或 dll 文件，由 .net 运行时（runtime）程序解释执行。</p><h3 id="1-4-">1.4. 解释、执行型</h3><p>解释执行型语言通常又被称为脚本（script）语言。例如 Bash 脚本、Powershell脚本、JavaScript、Python、PHP、Ruby、Matlab 脚本、Mathematica 脚本等。它们的源文件由相应的运行时程序直接读取并解释执行。</p><h3 id="1-5-">1.5. 编译转换、解释执行型语言</h3><p>TypeScript、JSX、CoffeeScript 等语言通常是先由编译转换程序转换为 JavaScript，再由 JavaScript 运行时解释执行。</p><h3 id="1-6-">1.6. 其他类型</h3><p>例如 Kotlin 即可以被编译为 Java 的 class文件由 JVM 解释执行，也可以被编译转换为 JavaScript ，还可以被编译为可执行程序直接被 CPU 执行。</p><h3 id="1-7-">1.7. 不同执行方式的执行效率</h3><p>Java 的 class 文件中和 .net 的二进制文件中的指令都是类似 CPU 指令的代码，各硬件平台上的虚拟机 / 运行时程序只需花很少的代价就可以将其转换成该平台上的 CPU 指令执行，而不需要再对源代码进行词法语法分析。因此这些编译、解释执行型语言的运行效率通常比下面要介绍的解释执行型语言要高。</p><p>但是编译、解释执行型语言又比汇编 / 编译执行型语言多一个将虚拟机指令转换为 CPU 指令的过程，所以它们运行效率通常又比汇编 / 编译执行型语言的低。</p><p>因此，在对执行效率要求高的系统（例如高频交易的系统、核心网络的路由器中的路由程序）中，通常不采用解释执行型语言，而是采用编译执行型语言来开发。</p><h3 id="1-8-">1.8. 在系统级编程方面的适用性</h3><p>由于编译解释型语言或者脚本语言的运行效率不如编译执行型语言，而且需要虚拟机或者解释器才能运行，因此在操作系统或者驱动程序的编程中通常使用的是编译执行型的语言，如 C / C++ / Rust。</p><p>而在与 CPU 架构密切相关的地方，例如操作系统的内存管理模块的设置页表地址的程序，或者需要使用 CPU 的 IO / 中断等指令的时候，基本上只能使用汇编语言。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/aleksander-vlad-jiVeo0i1EB4-unsplash--1-.jpg" class="kg-image" alt="aleksander-vlad-jiVeo0i1EB4-unsplash--1-" width="600" height="400" loading="lazy"></figure><h2 id="2-">2. 内存管理</h2><h3 id="2-1-">2.1. 手动管理内存</h3><p>汇编语言以及 C / C++、Fortran、Pascal 等许多编译执行型语言都需要手动管理内存，由程序员手动回收无用的数据占用的内存。</p><h3 id="2-2-">2.2. 自动管理内存</h3><p>编译执行型语言中的 Go，Java、C#、<code>VB.net</code> 等绝大多数编译、解释执行型语言，还有 JavaScript、Python、PHP 等绝大多数脚本语言都是由运行环境自动管理内存（增加作为对象池的堆的内存、无用对象的垃圾回收等）。</p><p>其中 Go 主要采用标记清除算法做垃圾回收（英文 Garbage Collection，缩写 GC）。而 Java 可以指定标记清除、标记整理、标记复制与标记清除结合的分代 GC 算法等等算法中的一种做垃圾回收。对于其他语言的 GC 算法，我没了解过，读者有兴趣可以自己查阅资料。</p><h3 id="2-3-">2.3. 半自动管理内存</h3><p>Rust 语言中多数情况下，通过对象的所有权机制可以使得对象的内存申请和释放在编译期就可以确定，这些对象的内存管理可以看成是由编译器自动处理的。对于少数不受所有权机制限制的不安全对象，仍需要手动管理内存。</p><h3 id="2-4-">2.4. 自动管理内存的优劣</h3><p><strong>2.4.1. 优点</strong></p><p>把程序员从释放无用内存或者管理内存池等逻辑中解放出来，降低内存泄漏的可能性。</p><p><strong>2.4.2. 缺点</strong></p><p>程序员通常无法控制垃圾回收的时机，而垃圾回收通常需要暂停程序的运行，这会导致无法预料的程序卡顿。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/goran-ivos-TorAcb4AQRc-unsplash--1--1.jpg" class="kg-image" alt="goran-ivos-TorAcb4AQRc-unsplash--1--1" width="600" height="400" loading="lazy"></figure><h2 id="3-vs-">3. 强类型 VS 弱类型</h2><h3 id="3-1-">3.1. 强类型</h3><p>通常在需要编译的语言（如 C / C++ / Java / C# / Kotlin 等语言）中，一个变量 / 对象的属性必须在声明之后才能使用（一个变量必须被声明后才能被读，必须被声明后或者在声明时才能写；一个对象的属性没被定义的话不能被写入），而且一个变量 / 对象的属性 / 函数的入参与返回值的数据类型在声明时就确定下来、不能再改变了。如果违反了这两个约束，那么在编译源代码的时候就会报错。</p><h3 id="3-2-">3.2. 弱类型</h3><p>通常在脚本语言（如 Python、JavaScript、PHP）中，一个对象的属性是可以动态变化的，甚至变量不需要声明就可以使用（读或者写）；一个变量 / 对象的属性 / 函数的入参与返回值的数据类型是不固定的。</p><h3 id="3-3-">3.3. 强弱结合</h3><p>在 TypeScript 和在版本 7 以后的 PHP 等语言之中，既可以使用动态类型，也可以规定某些变量 / 对象的属性 / 函数的返回值的数据类型。如果违反了规定的数据类型，会引起编译转换器 / 解释器报错。</p><h3 id="3-4-">3.4. 不适用</h3><p>在汇编语言中通常没有变量（不考虑预处理器），因此不存在静态类型或者动态类型的问题。</p><h3 id="3-5-">3.5. 弱类型的优劣</h3><p><strong>3.5.1. 优点</strong></p><p>初学者不需理解数据类型就可入门，进行简单的编程。</p><p>程序员不受数据类型的限制，可以采取更灵活的实现方式，例如在某个类的对象中增加一个属性来存储一些额外的数据，而不需要改变类的定义。</p><p>程序变更前后的兼容性比较好。例如一个类增减了成员属性，旧类的实例对象序列化后，反序列化为新类的实例对象时，强类型语言可能会因字段不一致而出错，而弱类型语言则通常不会出错。又例如 JavaScript 中某个函数增加一个默认为空的入参，调用方不需修改即可兼容。</p><p><strong>3.5.2. 缺点</strong></p><p>弱类型语言不能在编译期确定变量的数据类型，从而不能在编译期对一些变量的内存使用进行优化（例如内存页对齐、缓存对齐等）。</p><p>弱类型语言在运行时可能出现由于类型混乱导致的错误。例如错把数字变量比较的规则应用于字符串变量的比较，从而得到 “10” &lt; “2”。</p><p>弱类型语言中，如果一些变量、对象属性、函数的入参或返回值使用的数据类型过多，会降低代码的可读性和可维护性，甚至导致程序代码混乱而出错。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/maxwell-nelson-taiuG8CPKAQ-unsplash--1-.jpg" class="kg-image" alt="maxwell-nelson-taiuG8CPKAQ-unsplash--1-" width="600" height="400" loading="lazy"></figure><h2 id="4-">4. 并行 / 并发模型</h2><p>首先要说明的是，并行和并发并不是一样的概念，它们的区别可以参考这个<a href="https://www.zhihu.com/question/33515481">知乎问题 1</a>。并行的一定是并发的，并发的不一定是并行的。</p><h3 id="4-1-">4.1. 多线程并行</h3><p>汇编语言可以直接调用操作系统的 API 来实现多线程。</p><p>C / C++ 可以通过 pthread 库或者直接调用操作系统的 API 来实现多线程。</p><p>Java 可以调用 Java API 来实现多线程，C# / <code>VB.net</code> 则可以调用 .net 的 API 来实现多线程。</p><p>Python 也可以通过其官方 API 实现多线程。虽然最普遍使用的 Python 运行时 CPython 和 PyPy 目前由于全局解释器锁（GIL）的缘故使不同线程不能在多个 CPU 核心（core）中并行运行，但还是有某些 Python 运行时如 Jython 已去掉了 GIL。</p><p>JavaScript 在支持 worker API 的运行时程序上可以通过创建 worker 来实现多线程，但这些线程之间只能通过消息通讯，不能共享内存。</p><h3 id="4-2-">4.2. 用户态线程 / 协程并发</h3><p>Python 可以通过生成器、asyncio 库或者 async / await 语法来使用协程（coroutine）。</p><p>Go 可以通过 goroutine 来使用协程。Gorotine 是可以并行的，可以充分利用多个 CPU 核心。</p><p>Kotlin 可以通过 kotlinx.coroutine 包提供的 API 来使用协程。</p><h3 id="4-3-io-api-">4.3. 非阻塞 IO API + 内置事件循环并发</h3><p>JavaScript 运行时通过提供非阻塞的 IO API 以及内部实现事件循环的方式，使得 JavaScript 可以在等待 IO 返回数据的同时可以并发地运行其他处理逻辑。</p><h3 id="4-4-">4.4. 多进程并行</h3><p>PHP 目前没有语言上的特性或者自带的 API 可以实现多线程或协程，要实现多线程需要额外安装 pthreads 拓展或者 Swoole 拓展。通常 PHP 的并发是通过多进程来实现的。</p><p>如果使用有 GIL 的 Python 运行时程序、Ruby 运行时程序或者不支持 worker API 的 JavaScript 运行时程序（例如版本 10 以前的 Node.js），如果想要充分利用多个 CPU 核心，也需要通过多进程来实现并行。</p><p>未完待续。</p><p></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 函数与闭包的前世今生（一） ]]>
                </title>
                <description>
                    <![CDATA[ 1、浅谈闭包 要掌握 JavaScript，闭包是一个必须理解的概念。 1.1、闭包的定义 我查阅了维基百科 [https://en.wikipedia.org/wiki/Closure_(computer_programming)]和一些技术博客， 闭包（closure）的定义有两种说法：  1. （可以访问函数体以外定义的自由变量）的函数；  2. （可以访问函数体以外定义的自由变量）的函数及其可以访问的自由变量组成的集合。 1.2、自由变量 上面闭包的定义中说到的“自由变量”是与“全局变量”“函数参数”和“局部变量”相对比而言的。 全局变量可以看作是所有函数都能访问的变量。函数参数则是指函数的输入参数。局部变量 按字面意思理解就是作用域局限在被定义的函数中的变量。函数参数和局部变量都是在函数定义中被定义的。一般我们提到函数参数和局部变量的时候，都只在其被定义的函数中说的，或者说一般只有在定义它们的函数使用它们的时候，我们才会称它们为参数和局部变量。 例如，有一个全局变量 a，有一个函数 x，x 有一个输入参数 b 和一个局部变量 c，还有一个函数 y。在函数 x 和函数 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/function-and-closure-part-one/</link>
                <guid isPermaLink="false">5f8032635f583f0565090a23</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 函数式编程 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 闭包 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yuping Wu ]]>
                </dc:creator>
                <pubDate>Thu, 08 Oct 2020 10:07:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/10/joshua-aragon-FkjaN-7gWC0-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="1-">1、浅谈闭包</h2><p>要掌握 JavaScript，<strong>闭包</strong>是一个必须理解的概念。</p><h3 id="1-1-">1.1、闭包的定义</h3><p>我查阅了<a href="https://en.wikipedia.org/wiki/Closure_(computer_programming)">维基百科</a>和一些技术博客， 闭包（closure）的定义有两种说法：</p><ol><li>（可以访问函数体以外定义的自由变量）的<strong>函数</strong>；</li><li>（可以访问函数体以外定义的自由变量）的函数及其可以访问的自由变量组成的集合。</li></ol><h3 id="1-2-">1.2、自由变量</h3><p>上面闭包的定义中说到的“自由变量”是与“全局变量”“函数参数”和“局部变量”相对比而言的。</p><p><strong>全局变量</strong>可以看作是所有函数都能访问的变量。<strong>函数参数</strong>则是指函数的输入参数。<strong>局部变量</strong>按字面意思理解就是作用域局限在被定义的函数中的变量。函数参数和局部变量都是在函数定义中被定义的。一般我们提到函数参数和局部变量的时候，都只在其被定义的函数中说的，或者说一般只有在定义它们的函数使用它们的时候，我们才会称它们为参数和局部变量。</p><p>例如，有一个全局变量 a，有一个函数 x，x 有一个输入参数 b 和一个局部变量 c，还有一个函数 y。在函数 x 和函数 y 中都能访问全局变量 a，函数 x 中能使用参数 b 和局部变量 c，但函数 y 通常与 b 和 c 就没有关系了。（没有闭包而且不能通过引用间接访问的话，函数 y 一定不能访问 b 和 c 的。）</p><p><strong>自由变量</strong>则是在一个函数内被定义，但可以被其他函数访问的一种变量。也就是说，一般我们说到自由变量的时候，就隐含了“一个函数访问它以外的函数中定义的变量”这个情形。</p><p>在上述的例子中，如果函数 y 能访问 b 或者 c，那么 b / c 对于函数 y 而言就是自由变量。</p><h3 id="1-3-javascript-">1.3、JavaScript 中的闭包</h3><p><strong>1.3.1、JavaScript 中变量的作用域</strong></p><p>一个变量的作用域是指能够使用该变量的范围。</p><p>在 JavaScript 中，不在任何函数中定义的变量是全局变量。全局变量的作用域在此不再赘述。除了全局变量以外的变量的作用域，包括函数参数，首先都被局限在定义它的函数内。</p><p>在实现 ECMAScript 版本 6 （不含）以前的 JavaScript，定义局部变量必须使用 var 关键字或者 function 关键字（函数作为局部变量），例如：</p><pre><code>function x(a, b) {
    var c = 1; // c是函数x的一个局部变量
    function y(d, e) { // y是函数x的一个局部变量
        console.log(d + e);
    }
    y(a + b, c);
}
</code></pre><p>根据变量提升规则，JavaScript 解释器在遇到使用 <code>var</code> 关键字或者 <code>function</code> 关键字定义的局部变量，会将其视为在函数的最开头定义的。因此下面的两段代码是等价的：</p><pre><code>function x(a, b) {
    y(a + b, c);
    if (a &gt; b) {
        var c = 1;
        function y(d, e) {
            console.log(d + e);
        }
    }
}
</code></pre><pre><code>function x(a, b) {
    var c; // 此时c为undefined
    function y(d, e) { // c和y的定义会被提升到函数体的最开头
        console.log(d + e);
    }
    y(a + b, c);
    if (a &gt; b) {
        c = 1;
    }
}
</code></pre><p>由于变量提升规则允许在声明某个变量之前就使用它，可能会导致一些不符合直观的执行结果，所以在 ECMAScript 6 规范中，加入了 <code>let</code> 和 <code>const</code> 关键字用来定义块级作用域的变量。块级作用域的变量的作用域是定义变量的方括号内，而且没有变量提升的规则，因此块级作用域的变量都必须先定义后使用。</p><p>块级作用域的变量的例子：</p><pre><code>function x(a, b) {
    return a + b + c; // 会报ReferenceException：c未被定义
    if (a &gt; b) {
        let c = 1; // c的作用域被局限在这个if语句的大括号内
    }
}
</code></pre><pre><code>function x(a, b) {
    if (a &gt; b) {
        return a + b + c; // 会报ReferenceException：不能在c初始化前访问c
        let c = 1; // 在c的定义之前不能使用c
    }
    return a + b - c;
}
</code></pre><p><strong>1.3.2、JavaScript 中的闭包</strong></p><p>在一个变量的作用域内定义的函数的函数体内也可以访问该变量，或者说在一个变量的作用域内定义的函数的函数体也属于该变量的作用域，又或者说一个变量是在其作用域内定义的函数的自由变量。</p><p>还是举一个例子：</p><pre><code>function x(a) {
    let b = 1 - a;
    if (a &gt; b) {
        return function y(c) {
            return a - b + c; // 函数y内也可以访问x函数定义里的参数a和局部变量b
        };
    }
    return a + b + c;
}
function z() {
    // 不能访问x函数的参数a和局部变量b，因为这里已经超出了它们的作用域
}
</code></pre><p>下面再以一个常见面试题来举例说明闭包：</p><pre><code>function x() {
    const result = [];
    let i;
    for (i = 0; i &lt; 5; i++) {
        result[i] = function y() { // 变量i与函数y组成一个闭包
            console.log(i); // 变量i是函数y的自由变量
        };
    }
    return result;
}
const functions = x();
for (let j = 0; j &lt; functions.length; j++) (functions[j])();
</code></pre><p>这里会输出 5 个 5，而不是 0、1、2、3、4，因为 functions 中的 5 个函数中使用的 i 其实是同一个变量，而且与函数 x 执行时 for 循环中的变量 i 是同一个变量。在函数 x 中跳出 for 循环后到执行 <code>(functions[i])()</code> 时，变量 i 就已经变成了 5。</p><p>最后留一个思考题，以下代码会输出什么呢？为什么会这样输出呢？</p><pre><code>function x() {
    const result = [];
    for (let i = 0; i &lt; 5; i++) {
        result[i] = function y() { // 变量i与函数y组成一个闭包
            console.log(i); // 变量i是函数y的自由变量
        };
    }
    return result;
}
const functions = x();
for (let j = 0; j &lt; functions.length; j++) (functions[j])();</code></pre><h2 id="2-">2、重新审视函数</h2><p>在上文中，我提到<strong>闭包</strong>有两种定义，一种认为闭包是函数，另一种认为闭包是函数与自由变量的集合。我个人比较认同后一种定义。</p><p>我们经常谈论<strong>函数</strong>和<strong>闭包</strong>，但是却几乎没思考过函数是什么（函数的定义）。对于只学习过 JavaScript 而没有学习过 C 语言的人来说，他们可能不理解函数与闭包的区别，因为在一个变量的作用域内定义的函数都能访问该变量，函数能访问其定义以外的变量看起来是自然而然的事情。很多人也不知道底层的 JavaScript 解释器是如何实现闭包的，不知道实现闭包其实比没有闭包、只有全局变量、函数参数和局部变量要复杂一些。</p><p>首先我们来看看编程语言中的函数是如何应运而生的。</p><h3 id="2-1-">2.1、图灵机与“古代”的编程</h3><p>伟大的数学家、密码学家<strong>阿兰·图灵</strong>提出了<strong>图灵机</strong>这种计算机模型。图灵机的原始版本是：一台机器有一个读写头和可以存储有限状态的内部存储器，读写头可以移动和读写一条纸带，纸带上只能存储一个一个的二进制位，机器根据读到的纸带上的信息以及内部状态来决定如何移动或者写纸带。机器内用于存储状态的内部存储器通常叫做<strong>寄存器（register）</strong>。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/246008ccf1011a7886b128234cec4ec04b1e1c7d.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>不久人们就按照这个模型制造了真正的计算机。<strong>是的，最早的计算机读写的不是磁盘或者SSD，而是纸带！</strong> 但是这个时候计算机能做的计算的种类和过程是固定的，例如只能从纸带上读入两个数然后在纸带上输出两个数的乘积，而不能动态地改变计算机执行的指令（编程）。也就是说<strong>这时候纸带的输入数据仅仅是被看作要处理的数据而不是可执行的代码</strong>。</p><p><strong>2.1.1、通用图灵机与可编程计算机</strong></p><p><strong>通用图灵机</strong>是指可以实现任意一个图灵机的功能的图灵机。有了通用图灵机，我们就可以用一台计算机来实现多种多样的计算功能。怎样实现一台通用图灵机呢？让计算机可编程就行了。<strong>可编程计算机不仅可将纸带上的信息看作要处理的数据，还能将其看作代码来执行。</strong></p><p>纸带上的可执行代码就是我们常说的<strong>机器语言</strong>。机器语言的执行单位是<strong>指令</strong>，也就是说可编程计算机每次从纸带上读取一条指令来执行。一个程序的可执行代码就是由若干条指令构成。</p><p><strong>2.1.2、指令集</strong></p><p>接下来就产生了一个问题：纸带上的指令应该怎么设计呢？如果设计得太复杂，一来一条指令占用的纸带就会太长，太耗纸带；二来执行指令的计算机也要设计得很复杂，制造成本太高。</p><p>一台可编程计算机的指令的设计方式就是这台计算机的<strong>指令集</strong>。</p><p>经过计算机科学家和工程师的研究和设计，直到今天各种处理器的指令集相对于高级编程语言（如 C++、Java、JavaScript、Python 等）来说还是很简单的，基本上只有如下几类：</p><ol><li>从输入 / 存储设备读取一个数（通常是 1 / 2 / 4 / 8 个字节的）到某个寄存器中；</li><li>对某个寄存器中的数作一元运算，如按位取反、变换正负号；</li><li>将某个寄存器 a 中的数与另一个寄存器 b 中的数做运算（加减乘除、按位与、按位或等），结果存回寄存器 a；</li><li>将某个寄存器中的数写到输出 / 存储设备中；</li><li>跳转执行另外一个地方的代码；</li><li>根据某个寄存器中的值是否为 0，来决定是否跳转执行另一个地方的代码</li></ol><p>大家可以看到，上面没有可以直接实现函数的指令，甚至没有可以直接实现循环结构的指令，只能实现顺序执行以及简单的条件结构。</p><p>未完待续。</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
