<?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[ 面向对象 - 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[ 面向对象 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 07 May 2026 08:47:29 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/object-oriented/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 面向对象编程的四大支柱 ]]>
                </title>
                <description>
                    <![CDATA[ JavaScript 是一种多范式语言，可以按照不同的编程范式进行编写。编程范式本质上就是你在写代码时遵循一些规则，以帮助你解决一个特定的问题。 这就是四大支柱的含义。它们是软件设计原则，帮助你编写整洁的面向对象的代码。 面向对象编程的四大支柱是：  * 抽象  * 封装  * 继承  * 多态 让我们仔细看看这些支柱。 面向对象编程中的抽象 抽象的意思是把实现的细节隐藏在某个东西里面——有时是一个原型，有时是一个函数。因此，当你调用这个函数时，你不需要确切地了解它在做什么。 如果你必须了解一个大代码库中的每一个函数，你将永远无法编码。要花几个月的时间才能读完它。 你可以通过抽象化某些细节来创建一个可重用的、简单易懂的、容易修改的代码库。让我给你举个例子： function hitAPI(type){ 	if (type instanceof InitialLoad) { 		// 实施示例 	} else if (type instanceof NavBar) { 		// 实施示例 	} else { 		// 实施示例 	} } 这完全没有被抽象你能从这个例子中 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/four-pillars-of-object-oriented-programming/</link>
                <guid isPermaLink="false">63f43440cf34b8063af88f03</guid>
                
                    <category>
                        <![CDATA[ 面向对象 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Wed, 01 May 2024 03:22:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/The-four-pillars-of-object-orientation.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/four-pillars-of-object-oriented-programming/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">The Four Pillars of Object-Oriented Programming</a>
      </p><p>JavaScript 是一种多范式语言，可以按照不同的编程范式进行编写。编程范式本质上就是你在写代码时遵循一些规则，以帮助你解决一个特定的问题。</p><p>这就是四大支柱的含义。它们是软件设计原则，帮助你编写整洁的面向对象的代码。</p><p>面向对象编程的四大支柱是：</p><ul><li>抽象</li><li>封装</li><li>继承</li><li>多态</li></ul><p>让我们仔细看看这些支柱。</p><h1 id="-">面向对象编程中的抽象</h1><p>抽象的意思是把实现的细节隐藏在某个东西里面——有时是一个原型，有时是一个函数。因此，当你调用这个函数时，你不需要确切地了解它在做什么。</p><p>如果你必须了解一个大代码库中的每一个函数，你将永远无法编码。要花几个月的时间才能读完它。</p><p>你可以通过抽象化某些细节来创建一个可重用的、简单易懂的、容易修改的代码库。让我给你举个例子：</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">function hitAPI(type){
	if (type instanceof InitialLoad) {
		// 实施示例
	} else if (type instanceof NavBar) {
		// 实施示例
	} else {
		// 实施示例
	}
}</code></pre><figcaption>这完全没有被抽象</figcaption></figure><p>你能从这个例子中看到，你如何准确地实现你的自定义用例所需要的东西吗？</p><p>你需要的每一个新的 API 都需要一个新的 <code>if</code> 块，以及它自己的自定义代码。这不是抽象的，因为你需要为你添加的每一个新类型担心实现。这不是可重用的，而且是一个维护的恶梦。</p><p>像下面这样的方法怎么样？</p><pre><code class="language-javascript">hitApi('www.kealanparr.com', HTTPMethod.Get)</code></pre><p>你现在只需向你的函数传递一个 URL 和你想使用的 HTTP 方法就可以了。</p><p>你不必担心这个函数如何工作。它已经被解决了。这极大地有利于代码重用！同时也使你的代码更容易维护。</p><p>这就是抽象的意义所在。在你的代码中找到相似的东西，并提供一个通用的函数或对象来服务于多个地方/多个关注点。</p><p>这里还有一个很好的<strong>抽象</strong>的例子：想象一下，如果你正在创建一台机器，为你的用户制作咖啡，可能有两种方法：</p><h2 id="--1">如何在有抽象的情况下创建按钮</h2><ul><li>有一个标题为“制作咖啡”的按钮</li></ul><h2 id="--2">如何在没有抽象的情况下创建按钮</h2><ul><li>有一个标题为“烧水”的按钮</li><li>有一个标题为“向水壶中加入冷水”的按钮</li><li>有一个标题为“向干净的杯子中加入一勺咖啡粉”的按钮</li><li>有一个标题为“清洁任何脏杯子”的按钮</li><li>以及所有其他的按钮</li></ul><p>这是一个非常简单的例子，但第一种方法将逻辑抽象到机器中去了，第二种方法迫使用户了解如何制作咖啡，并且基本上是自己制作。</p><p>下一个支柱向我们展示了一种我们可以实现<strong>抽象</strong>的方法，即使用<strong>封装</strong>。</p><h1 id="--3">面向对象编程中的封装</h1><p>封装的定义是“将某物封闭在或像封闭在一个胶囊中的行为”。移除对部分代码的访问并使之成为私有的东西正是<strong>封装</strong>的意义所在（很多时候，人们把它称为数据隐藏）。</p><p>封装意味着你代码中的每个对象都应该控制自己的状态。状态是你的对象的当前“快照”，例如键、你的对象上的方法、布尔属性等等。如果你要重置一个布尔值或从对象中删除一个键，它们都是对你状态的改变。</p><p>限制你的代码中哪些部分可以访问。如果不需要的话，让更多的东西无法访问。</p><p>在 JavaScript 中，私有属性是通过使用闭包来实现的。下面是一个例子：</p><pre><code class="language-javascript">var Dog = (function () {

	// 私有
	var play = function () {
		// 执行代码
	};
    
	// 私有
	var breed = "Dalmatian"
    
	// 公有
	var name = "Rex";

	// 公有
	var makeNoise = function () {
 		return 'Bark bark!';
	};

 	return {
		makeNoise: makeNoise,
		name: name
 	};
})();

</code></pre><p>我们做的第一件事是创建一个<strong>立即被调用的函数</strong>（Immediately Invoked Function<strong><strong> </strong></strong>Expression，简称 IIFE）。这创建了一个任何人都可以访问的对象，但隐藏了一些细节。你不能调用 <code>play</code>，也不能访问 <code>breed</code>，因为我们没有在最后的对象中用 return 暴露它。</p><p>上面这个特殊的模式被称为<strong>揭示模块模式</strong>（Revealing Module Pattern），但它只是一个你如何实现封装的例子。</p><p>我想更多地关注封装的思想（因为它比仅仅学习一个模式更重要）。</p><p>反思一下，多想想如何能把你的数据和代码藏起来，把它分开来。模块化和明确责任是<strong>面向对象</strong>的关键。</p><p>我们为什么要喜欢私有？为什么不把所有的东西都变成全局的呢？</p><ul><li>很多不相关的代码将通过全局变量成为相互依赖/耦合的对象。</li><li>如果名字被重复使用，你很可能会覆盖这些变量，这可能会导致错误或不可预测的行为。</li><li>你很可能最终会产生<strong>意大利面条式的代码</strong>——很难推理出是什么在读写你的变量和改变状态。</li></ul><p>封装可以通过将长行代码分离成较小的独立函数来应用。将这些函数分离成模块。我们把数据隐藏在一个没有其他需要访问的地方，并显示出需要的东西。</p><p>这就是<strong>封装</strong>。将你的数据绑定到某个东西上，无论是类、对象、模块还是函数，并尽你所能保持它的私有性。</p><h1 id="--4">面向对象编程中的继承</h1><p>继承让一个对象获得另一个对象的属性和方法。在 JavaScript 中，这是通过<strong>原型继承</strong>完成的。</p><p>主要的好处是可重用性。我们知道，有时多个地方需要做同样的事情，而且除了一个小部分之外，它们需要做的事情都是一样的。这就是继承可以解决的问题。</p><p>每当我们使用继承时，我们都会努力使父代和子代的代码具有高关联度。例如， <code>Bird</code> 类型是否从 <code>DieselEngine</code> 类型中延伸出来？</p><p>保持你的继承简单易懂，可以预测。不要因为有一个你需要的方法或属性而从完全不相关的地方继承。继承并不能很好地解决这个特殊问题。</p><p>使用继承时，你应该需要大部分的功能（你不一定需要所有的功能）。</p><p>开发人员有一个原则，叫做<strong>里氏替换原则</strong>（Liskov Substitution principle）。它指出，如果你能在使用子类（我们称之为 <code>ChildType</code>）的任何地方使用父类（我们称之为 <code>ParentType</code>）——并且 <code>ChildType</code> 继承自 <code>ParentType</code>——那么你就通过了测试。</p><p>你不能通过这个测试的主要原因是，如果 <code>ChildType</code> 从父类移除了一些东西。如果 <code>ChildType</code> 删除了它从父类继承的方法，就会导致 <code>TypeError</code>，即出现未定义的东西，不符合你的期望。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2020/12/image-146.png" class="kg-image" alt="image-146" width="600" height="400" loading="lazy"><figcaption>箭头看起来好像走错了方向，但 Animal 是基础--父代</figcaption></figure><p>继承链是一个术语，用来描述从基础对象的原型（其他东西都是从它继承的）到继承链的“终点”（最后继承的类型——上面例子中的 <strong><strong>Dog</strong></strong>）的继承流。</p><p>尽力保持你的继承链清晰且合理。在使用<strong>继承</strong>时，你很容易出现<strong>编程反模式</strong>（Fragile base anti-pattern）——当你的基础原型被认为是“脆弱的”，因为你对基础对象做了一个“安全”的改变，然后开始破坏你所有的子对象。</p><h1 id="--5">面向对象编程中的多态</h1><p>多态意味着“以多种不同形式出现的条件”。这正是第四个也是最后一个支柱所关注的——同一继承链中的类型能够做不同的事情。</p><p>从上一个图中，我们可能有一个叫做 <code>Animal</code> 的基本原型，它定义了 <code>makeNoise</code>。然后，从这个原型延伸出来的每个类型都可以覆盖，做他们自己的自定义工作。就像这样。</p><pre><code class="language-javascript">// 让我们创建一个 Animal 和 Dog 的例子
function Animal(){}
function Dog(){}

Animal.prototype.makeNoise = function(){
	console.log("Base noise");
};

// 我们编码的大多数动物有 4 个，如果需要的话，这可以被重写
Animal.prototype.legs = 4;

Dog.prototype = new Animal();

Dog.prototype.makeNoise = function(){
	console.log("Woof woof");  
};

var animal = new Animal();
var dog = new Dog();

animal.makeNoise(); // Base noise
dog.makeNoise();    // Woof woof- 这个被覆盖了
dog.legs;           // 4！被继承了</code></pre><p><code>Dog</code> 扩展自 <code>Animal</code>，可以利用默认的 <code>legs</code> 属性。但它也能自己实现制造自己的噪音。</p><p>多态的真正力量在于共享行为，并允许自定义重写。</p><h1 id="--6"><strong>总结</strong></h1><p>我希望这篇文章解释清楚了什么是面向对象编程的四大支柱，以及它们是如何帮助我们写出更整洁更高效的代码的。</p><p>如果你喜欢这篇文章并想看到更多文章，可以在 <a href="https://twitter.com/kealanparr">Twitter</a> 上关注我。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 通过创建计时器学习 JavaScript 面向对象编程 ]]>
                </title>
                <description>
                    <![CDATA[ 在这篇文章里，我们将通过创建一个简单的计时器应用来学习 JavaScript 面向对象编程。 面向对象编程是一种重要的编程范式，它将代码组织为对象，以便更好地管理和维护应用程序。 有很多文章详细地阐释面向对象编程的好处，以及如何应用它来构建应用程序。 但是，初学者可能会有疑问：为什么要应用面向对象编程？我应该何时应用它？ 本文将会通过面向过程和面向对象两种设计思路来实现同样的计时器功能，帮助初学者理解这些问题。 我们将学习哪些知识 我们会讨论到  * HTML 基础知识，例如 DOM 方法、DOM 事件  * CSS 基础知识，例如 flex 布局  * JavaScript 知识，例如正则表达式、class（类）、constructor() 构造函数、对象、this 关键字、setInterval()     方法、事件机制 目录  * 用 HTML 和 CSS 编写计时器界面  * 通过面向过程编程实现计时器 * 实现基本功能     ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-javascript-object-oriented-programming-by-creating-timers/</link>
                <guid isPermaLink="false">64298edbeb0a820685b6aa47</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 面向对象 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Mon, 03 Apr 2023 01:59:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/04/yogendra-singh-R5qGElpTl08-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/learn-javascript-object-oriented-programming/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Learn JavaScript Object-Oriented Programming by Building a Timer Application</a>
      </p><!--kg-card-begin: markdown--><p>在这篇文章里，我们将通过创建一个简单的计时器应用来学习 JavaScript 面向对象编程。</p>
<p>面向对象编程是一种重要的编程范式，它将代码组织为对象，以便更好地管理和维护应用程序。</p>
<p>有很多文章详细地阐释面向对象编程的好处，以及如何应用它来构建应用程序。</p>
<p>但是，初学者可能会有疑问：为什么要应用面向对象编程？我应该何时应用它？</p>
<p>本文将会通过面向过程和面向对象两种设计思路来实现同样的计时器功能，帮助初学者理解这些问题。</p>
<h2 id="">我们将学习哪些知识</h2>
<p>我们会讨论到</p>
<ul>
<li>HTML 基础知识，例如 DOM 方法、DOM 事件</li>
<li>CSS 基础知识，例如 flex 布局</li>
<li>JavaScript 知识，例如正则表达式、<code>class</code>（类）、<code>constructor()</code> 构造函数、对象、<code>this</code> 关键字、<code>setInterval()</code> 方法、事件机制</li>
</ul>
<h2 id="">目录</h2>
<ul>
<li><a href="#create-timer-with-html-and-css">用 HTML 和 CSS 编写计时器界面</a></li>
<li><a href="#timer-pp">通过面向过程编程实现计时器</a>
<ul>
<li><a href="#basic-functions">实现基本功能</a></li>
<li><a href="#inputs-range">限制时、分、秒数字的输入范围</a></li>
<li><a href="#inputs-format">优化时、分、秒数字的格式</a></li>
</ul>
</li>
<li><a href="#timer-oop">通过面向对象编程实现计时器</a>
<ul>
<li><a href="#new-class-1">新建计时器类</a></li>
<li><a href="#timer-update">计时器更新</a></li>
<li><a href="#timer-start">开始计时</a></li>
<li><a href="#timer-stop">停止计时</a></li>
<li><a href="#timer-pause">暂停计时</a></li>
<li><a href="#show-current-time">显示当前时间</a></li>
<li><a href="#create-objects-1">创建对象实例</a></li>
<li><a href="#UI-with-functions-1">通过函数实现用户界面的交互</a></li>
</ul>
</li>
<li><a href="#timer-oop-with-events">在面向对象编程中添加事件机制</a>
<ul>
<li><a href="#eventemitter">创建事件发生器</a></li>
<li><a href="#new-class-2">新建计时器类</a></li>
<li><a href="#create-objects-2">创建对象实例</a></li>
<li><a href="#UI-with-functions-2">通过函数实现用户界面的交互</a></li>
</ul>
</li>
<li><a href="#conclusions">总结</a></li>
</ul>
<h2 id="create-timer-with-html-and-css">用 HTML 和 CSS 编写计时器界面</h2>
<p>首先，我们用 HTML 和 CSS 编写一个基本的计时器界面，包含一个显示时间的区域和几个控制按钮。</p>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/04/timer_1.jpg" class="center db" width="768" height="500" alt="timer_1" loading="lazy">
<p>HTML 如下所示：</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;

&lt;head&gt;
    &lt;title&gt;计时器&lt;/title&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;h1&gt;计时器&lt;/h1&gt;
        &lt;div class="ipt"&gt;
            &lt;input id="inputh" type="number" placeholder="时"&gt;
            &lt;input id="inputm" type="number" placeholder="分"&gt;
            &lt;input id="inputs" type="number" placeholder="秒"&gt;
        &lt;/div&gt;
        &lt;div class="btn"&gt;
            &lt;button id="btn-start" onclick="start_counting()"&gt;开始&lt;/button&gt;
            &lt;button id="btn-pause" onclick="pause_counting()"&gt;暂停&lt;/button&gt;
            &lt;button id="btn-stop" onclick="end_counting()"&gt;结束&lt;/button&gt;
        &lt;/div&gt;
        &lt;p id="currentTime"&gt;当前时间：&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
</code></pre>
<p>计时器包含三个输入框，<code>id</code> 分别为 <code>inputh</code>、<code>inputm</code> 和 <code>inputs</code>，<code>type</code> 为 <code>number</code>，允许用户输入时、分和秒的值。</p>
<p>输入框下方有三个按钮 <code>button</code>，分别控制计时器时间的开始、暂停和结束。每个按钮都有一个 <code>onclick</code> 事件。<code>onclick</code> 事件的属性值是一个函数，我们将在 JavaScript 部分写这个函数的代码。当用户点击一个按钮时，会执行相应的函数。在 JavaScript 中，我们会通过函数名调用函数，所以需要在函数名后加上括号。</p>
<p>你可以在<a href="https://www.freecodecamp.org/chinese/news/html-button-onclick-javascript-click-event-tutorial/">这篇文章</a>中了解 <code>onclick</code> 事件的更多信息。</p>
<p>按钮下方的 <code>p</code> 文本同步显示输入框中的时间。</p>
<p>我们给计时器添加一些简单的 CSS 代码，设置它的样式：</p>
<pre><code class="language-css">&lt;style&gt;
    .container {
        margin: 0 auto;
        width: 300px;
        height: 300px;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
    }

    .ipt {
        margin: 0 auto;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    input {
        width: 100px;
        height: 50px;
        font-size: 20px;
        text-align: center;
    }

    .btn {
        margin: 10px;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    button {
        margin: 10px 10px;
        width: 50px;
        height: 30px;
        font-size: 10px;
    }

    #currentTime {
        margin: 10px;
        color: green;
    }
&lt;/style&gt;
</code></pre>
<p>我们使用 Flexbox 将元素居中。为父元素添加</p>
<pre><code class="language-css">display: flex;
justify-content: center;
align-items: center;
</code></pre>
<p>即可将子元素相对于父元素水平且垂直居中。</p>
<p>你可以在这篇文章里查看 <a href="https://www.freecodecamp.org/chinese/news/how-to-center-a-div-with-css-10-different-ways/">CSS 布局</a>的更多信息，也可以根据自己的喜好进一步设置计时器的样式。</p>
<p>接下来，我们为计时器添加 JavaScript 代码，实现开始、暂停和结束计时的功能。</p>
<h2 id="timer-pp">通过面向过程编程实现计时器</h2>
<h3 id="basic-functions">实现基本功能</h3>
<p>这段代码展示了一个以事件驱动、面向过程思路设计的计时器程序，主要包括：</p>
<ul>
<li>2 组全局变量：时/分/秒的存储变量、setInterval() 函数的返回值</li>
<li>4 个关键函数：开始计时按钮按下后的事件处理函数、暂停计时按钮按下后的事件处理函数、结束计时按钮按下后的事件处理函数、计时过程中被循环调用的计时动作执行函数</li>
</ul>
<p>首先，用 DOM 方法 <code>document.getElementById().disabled</code> 初始化按钮状态。</p>
<pre><code class="language-javascript">// 初始化按钮状态
document.getElementById("btn-pause").disabled = true;
document.getElementById("btn-stop").disabled = true;
</code></pre>
<p>接着定义全局变量以存储时、分、秒。</p>
<pre><code class="language-javascript">// 定义全局变量
var timer = null; // 用于存储计时器的返回值
var h = 0; // 用于存储小时
var m = 0; // 用于存储分钟
var s = 0; // 用于存储秒数
</code></pre>
<p>在开始计时的 <code>start_counting()</code> 函数中，我们通过 <code>document.getElementById().value</code> 获取 <code>id</code> 分别为 <code>inputh</code>、<code>inputm</code>、<code>inputs</code> 的元素的值，即用户在计时器输入框中输入的时、分、秒的值，并赋值给 <code>h</code>、<code>m</code> 和 <code>s</code>。</p>
<p>然后通过 <code>if</code> 语句判断用户输入的时、分、秒的值，如果值都等于 0，或者其中一个值小于 0，则弹出提示<code>输入的时间不合法！</code>并返回，程序停止执行。</p>
<p>为计时器变量 <code>timer</code> 赋值 <code>setInterval()</code>。这个方法有两个参数，第一个参数是一个函数，第二个参数是一个单位为”毫秒“的时间。在这个例子里，我们规定计时器每隔 1000 毫秒（即 1 秒）执行一个函数 <code>counting</code>，将在下方介绍。</p>
<p>关于 <code>setInterval()</code> 方法的详细信息，你可以查看 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/setInterval">MDN 文档</a>。</p>
<p>之后，改变按钮和输入框的状态，禁止用户再次输入。</p>
<pre><code class="language-javascript">// 定义函数
// 开始计时
function start_counting() {
    // 获取输入的时间，补充默认值
    h = +document.getElementById("inputh").value || h;
    m = +document.getElementById("inputm").value || m;
    s = +document.getElementById("inputs").value || s;

    // 判断输入的时间是否合法
    if (
        (h == 0 &amp;&amp; m == 0 &amp;&amp; s == 0) ||
        (h &lt; 0 || m &lt; 0 || s &lt; 0)
    ) {
        alert("输入的时间不合法！");
        return;
    }

    // 开始计时
    timer = setInterval(counting, 1000);

    // 改变按钮和输入框的状态，禁止用户再次输入
    document.getElementById("btn-start").disabled = true;
    document.getElementById("btn-pause").disabled = false;
    document.getElementById("btn-stop").disabled = false;
    document.getElementById("inputh").disabled = true;
    document.getElementById("inputm").disabled = true;
    document.getElementById("inputs").disabled = true;
}
</code></pre>
<p>接着，在负责暂停计时的 <code>pause_counting()</code> 函数中，设置按钮和输入框在计时暂停时的状态，并调用 <code>clearInterval()</code> 以移除计时器，停止计时。</p>
<pre><code class="language-javascript">// 暂停计时
function pause_counting() {
    // 改变按钮和输入框的状态，允许用户再次输入
    document.getElementById("btn-start").disabled = false;
    document.getElementById("btn-pause").disabled = true;
    document.getElementById("btn-stop").disabled = false;
    document.getElementById("inputh").disabled = false;
    document.getElementById("inputm").disabled = false;
    document.getElementById("inputs").disabled = false;

    // 暂停计时
    clearInterval(timer);
}
</code></pre>
<p><code>end_counting()</code> 函数使计时停止，同样调用 <code>clearInterval()</code>，并将时、分、秒重置为 0，下方的文本“当前时间：”被更新为“计时已结束”。</p>
<pre><code class="language-javascript">// 结束计时
function end_counting() {
    // 改变按钮和输入框的状态，允许用户再次输入
    document.getElementById("btn-start").disabled = false;
    document.getElementById("btn-pause").disabled = true;
    document.getElementById("btn-stop").disabled = true;
    document.getElementById("inputh").disabled = false;
    document.getElementById("inputm").disabled = false;
    document.getElementById("inputs").disabled = false;

    // 结束计时
    clearInterval(timer);

    // 重置时间变量
    h = 0;
    m = 0;
    s = 0;
    document.getElementById("currentTime").innerHTML = "计时已结束";
}
</code></pre>
<p>接下来是 <code>counting()</code> 函数，它是 <code>setInterval()</code> 中调用的函数，通过 <code>if</code> 语句判断时、分或秒是否为 0，并执行相应的动作。这是对于计时器的常规的实现方式，结合我们在日常生活中的经验，也容易理解，例如：当秒数为 0 时，从分钟数借 1，秒数变成 59。</p>
<p><code>setInterval()</code> 方法每秒更新一次 <code>h</code>、<code>m</code> 和 <code>s</code> 的值。我们通过 <code>document.getElementById().innerHTML</code> 把更新后的时间同步显示在”当前时间：“文本中。</p>
<p>最后，又通过 <code>if</code> 语句判断时、分、秒的值，当三个值都为 0 时，执行 <code>end_counting()</code> 函数和 <code>setTimeout()</code> 函数。在 <code>setTimeout()</code> 函数里，执行弹窗提示“时间到！”。</p>
<blockquote>
<p>这里有个有趣的知识，你可以试试删除 <code>setTimeout()</code>，直接在 <code>end_counting()</code> 后面执行 <code>alert("时间到！")</code>，会发现弹窗阻塞了 DOM 渲染——即先弹出“时间到！”，再改变按钮和输入框的状态。而当我们使用 <code>setTimeout()</code>，这两个动作是同时发生的。你可以思考一下 <code>setTimeout()</code> 在这里起了什么作用。</p>
</blockquote>
<p>如果你想了解更多 <code>setTimeout()</code> 的用法，可以查看 freeCodeCamp 的<a href="https://www.freecodecamp.org/chinese/news/javascript-wait-how-to-sleep-n-seconds-in-js-with-settimeout/">这篇文章</a>。</p>
<pre><code class="language-javascript">// 计时
function counting() {
    // 判断秒数是否为 0
    if (s == 0) {
        // 秒数为 0 时，判断分钟是否为 0
        if (m == 0) {
            // 启动计时前已经检查过输入的时间是否合法，所以这里不需要再检查变量 h 的值
            h--;
            m = 59;
            s = 59;
        } else {
            // 分钟不为 0 时，分钟减 1，秒数变为 59
            m--;
            s = 59;
        }
    } else {
        // 秒数不为 0 时，秒数减 1
        s--;
    }

    // 显示当前时间
    document.getElementById("currentTime").innerHTML = "当前时间：" + h + " 时 " + m + " 分 " + s + " 秒";
    document.getElementById("inputh").value = h;
    document.getElementById("inputm").value = m;
    document.getElementById("inputs").value = s;

    // 判断秒数是否为 0
    if (s == 0) {
        // 秒数为 0 时，判断分钟是否为 0
        if (m == 0) {
            // 分钟为 0 时，判断小时是否为 0
            if (h == 0) {
                // 小时为 0 时，结束计时
                // 停止计时
                end_counting();
                // 在下一个事件循环里执行弹窗，防止弹窗阻塞 DOM 渲染
                setTimeout(function () {
                    alert("时间到！");
                }, 0);
                return;
            }
        }
    }
}
</code></pre>
<p>有时用户可能会在时、分、秒输入框中输入负数，我们的代码通过弹出“输入的时间不合法！”来提醒用户；有时用户可能输入小时数大于 24、分数和秒数大于 59，这不符合实际情况。</p>
<p>此外，为了让时间显示更美观，我们可能想将时、分、秒显示为两位数字。</p>
<p>我们可以对以上代码做两处优化。</p>
<h3 id="inputs-range">限制时、分、秒数字的输入范围</h3>
<p>当输入的小时数大于 24 时，将它自动修改为 24；当它小于 0 时，修改为 0。</p>
<p>当输入的分数和秒数大于 59 时，将它们自动修改为 59；当它们小于 0 时，修改为 0。</p>
<p>这里用到事件监听方法 <code>addEventListener()</code>，在 <code>input</code> 事件发生时执行函数。还用到 <code>parseInt()</code> 将输入的值转换为数字类型。</p>
<pre><code class="language-javascript">var inputh = document.getElementById("inputh");
inputh.addEventListener("input", function() { 
    inputh.value = parseInt(inputh.value||0);
    if (inputh.value &gt; 24) inputh.value = 24;
    if (inputh.value &lt; 0) inputh.value = 0;
});

var inputm = document.getElementById("inputm");
inputm.addEventListener("input", function() {
    inputm.value = parseInt(inputm.value||0);
    if (inputm.value &gt; 59) inputm.value = 59;
    if (inputm.value &lt; 0) inputm.value = 0;
});

var inputs = document.getElementById("inputs");
inputs.addEventListener("input", function() {
    inputs.value = parseInt(inputs.value||0);
    if (inputs.value &gt; 59) inputs.value = 59;
    if (inputs.value &lt; 0) inputs.value = 0;
});
</code></pre>
<h3 id="inputs-format">优化时、分、秒数字的格式</h3>
<p>当小时数、分数或秒数为一位数字时，通过正则表达式给它们前面加上 0。</p>
<pre><code class="language-javascript">h = h.toString();
m = m.toString();
s = s.toString();
if (h.match(/^\d$/)) { //如果时为个位数，则在前面加 0
    h = "0" + h;
}
if (m.match(/^\d$/)) { //如果分为个位数，则在前面加 0
    m = "0" + m;
}
if (s.match(/^\d$/)) { //如果秒为个位数，则在前面加 0
    s = "0" + s;
}
</code></pre>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/04/pp.gif" class="center db" width="932" height="592" alt="pp" loading="lazy">
<p>你可以在 CodePen 查看在线 <a href="https://codepen.io/miyaliu666-the-styleful/pen/ExeBqOZ">demo</a>：</p>
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="ExeBqOZ" data-preview="true" data-editable="true" data-user="miyaliu666-the-styleful" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/miyaliu666-the-styleful/pen/ExeBqOZ">
  timer_procedural programming</a> by miyaliu666 (<a href="https://codepen.io/miyaliu666-the-styleful">@miyaliu666-the-styleful</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p>我们成功实现了一个计时器！</p>
<p>你可能会想，如果要在一个项目中添加多个计时器呢？</p>
<p>似乎需要给各个计时器的 <code>input</code> 输入框设置不同的 <code>id</code>，以便 <code>document.getElementById().value</code> 获取相应的值。</p>
<p>比如，对于 <code>id</code> 为 <code>inputh1</code> 的一号计时器，我们在 <code>start_counting()</code> 赋值 <code>h1 = +document.getElementById("inputh1").value</code>，而对于 <code>id</code> 为 <code>inputh2</code> 的二号计时器，赋值为 <code>h2 = +document.getElementById("inputh2").value</code>，以此类推。</p>
<p>而实际的项目业务通常不会只是创建一堆计时器这么简单。可以想象，代码很容易变得冗长而混乱。</p>
<p>这个时候，就需要引入面向对象编程了。</p>
<h2 id="timer-oop">通过面向对象编程实现计时器</h2>
<p>你可能在某些场合听过开发者们说 “new 一个对象”，这背后就是面向对象编程的思想。</p>
<p>在这一节里，我们以面向对象的思想重构上一节的计时器程序，将“计时器业务功能”和“用户界面的交互”分离开来。</p>
<p>HTML 和 CSS 部分和上一节类似，这里不再赘述。你可以在<a href="https://codepen.io/miyaliu666-the-styleful/pen/RwYzXzY">这个 CodePen demo</a> 中看到全部代码。</p>
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="RwYzXzY" data-preview="true" data-editable="true" data-user="miyaliu666-the-styleful" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/miyaliu666-the-styleful/pen/RwYzXzY">
  timer_object-oriented-programming</a> by miyaliu666 (<a href="https://codepen.io/miyaliu666-the-styleful">@miyaliu666-the-styleful</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p>如果你学习过 freeCodeCamp 的<a href="https://www.freecodecamp.org/chinese/learn/javascript-algorithms-and-data-structures/#object-oriented-programming">面向对象编程课程</a>，一定记得：所谓对象，可以映射到我们在现实生活中见到的事物，比如汽车、商场和小鸟，具有属性和方法。</p>
<p>那就让我们从定义对象的属性和方法开始吧:)</p>
<h3 id="new-class-1">新建计时器类</h3>
<p><code>class</code> 关键字创建一个类，命名为 <code>Timer</code>，它具有构造函数  <code>constructor()</code> 和 <code>_on_update()</code>、<code>start()</code>、<code>stop()</code>、<code>pause()</code>、<code>show()</code> 方法。</p>
<pre><code class="language-javascript">&lt;script&gt;
    class Timmer {
        constructor() {
            this.name = '未命名计时器';
            this.timmer = undefined;
            this.h = 0;
            this.m = 0;
            this.s = 10;

            this._on_update_callback = undefined;
            this._on_stop_callback = undefined;
        }

        _on_update() {

        }

        start() {

        }

        stop() {

        }

        pause() {

        }

        show() {

        }
    }

&lt;/script&gt;
</code></pre>
<p>根据 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/constructor">MDN 文档</a>，</p>
<blockquote>
<p><strong><code>constructor()</code></strong> 是一种用于创建和初始化 <code>class</code> 创建的对象的特殊方法。</p>
</blockquote>
<p>我们在其中通过 <code>this</code> 关键字初始化几个变量，即对象的属性。在 JavaScript 中，<code>this</code> 的指向是随环境而变的。在这里，它指的是调用函数的对象，即 <code>Timer</code> 的实例。同时用 <code>this</code> 初始化计时器更新时的回调函数 <code>_on_update_callback</code> 和计时器停止时的回调函数 <code>_on_stop_callback</code>。</p>
<p><code>_on_update()</code>、<code>start()</code>、<code>stop()</code>、<code>pause()</code>、<code>show()</code> 方法分别用于计时器更新、开始计时、停止计时、暂停计时和显示当前时间。我们将逐步为它们添加代码。</p>
<h3 id="timer-update">计时器更新</h3>
<p>在 <code>_on_update()</code> 方法里处理计时器更新时的程序。和上文一样，仍然通过 <code>if</code> 语句判断时分秒为 0 时执行相应的动作。</p>
<p>如果外部回调函数 <code>_on_update_callback</code> 存在，则调用它。</p>
<pre><code class="language-javascript">_on_update() { 
    if (0 === this.h &amp;&amp; 0 === this.m &amp;&amp; 0 === this.s) {
        this.stop();
        return;
    } else if (0 === this.s) {
        this.s = 59;
        if (0 === this.m) {
            this.m = 59;
            this.h = this.h - 1;
        } else {
            this.m = this.m - 1;
        }
    } else {
        this.s = this.s - 1;
    }

    this.show();
    if (0 === this.h &amp;&amp; 0 === this.m &amp;&amp; 0 === this.s) {
        this.stop();
    }

    // 检查外部回调函数是否存在，如果存在就调用
    if (this._on_update_callback &amp;&amp; typeof this._on_update_callback === 'function') {
        this._on_update_callback();
    }
}
</code></pre>
<h3 id="timer-start">开始计时</h3>
<p><code>start()</code> 方法用于开始计时，即用户点击“开始”按钮后执行这个方法。通过 <code>setInterval()</code> 方法，每秒钟执行一次 <code>_on_update()</code>。</p>
<pre><code class="language-javascript">start() {
    if (this.timmer) {
        console.log(`[${this.name}]已经在计时了`);
        return;
    }
    console.log(`[${this.name}]开始计时`);
    this.timmer = setInterval(() =&gt; {
        this._on_update();
    }, 1000);
    this.show();
}
</code></pre>
<h3 id="timer-stop">停止计时</h3>
<p><code>stop()</code> 方法用于停止计时。使用 <code>clearInterval()</code> 停止计时器。如果外部回调函数 <code>_on_stop_callback()</code> 存在，则调用它。</p>
<pre><code class="language-javascript">stop() {
    console.log(`[${this.name}]停止计时`);
    clearInterval(this.timmer);
    this.timmer = undefined;

    // 和 update 类似，检查 stop 函数
    if (this._on_stop_callback &amp;&amp; typeof this._on_stop_callback === 'function') {
        this._on_stop_callback();
    }
}
</code></pre>
<h3 id="timer-pause">暂停计时</h3>
<p>使用 <code>pause()</code> 方法暂停计时，在其中使用 <code>clearInterval()</code> 停止计时器。</p>
<pre><code class="language-javascript">pause() {
    console.log(`[${this.name}]暂停计时`);
    clearInterval(this.timmer);
    this.timmer = undefined;
}
</code></pre>
<h3 id="show-current-time">显示当前时间</h3>
<p><code>show()</code> 方法用于在控制台打印当前时间。</p>
<pre><code class="language-javascript">show() { // 显示当前时间
    console.log(`[${this.name}]当前时间：${this.h}:${this.m}:${this.s}`);
}
</code></pre>
<h3 id="create-objects-1">创建对象实例</h3>
<p>接下来，用 <code>new</code> 关键字创建两个对象实例，即两个计时器，它们具有 <code>Timer</code> 对象的属性和方法。</p>
<p>根据 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new">MDN 文档</a>，</p>
<blockquote>
<p><strong><code>new</code> 运算符</strong>创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。</p>
</blockquote>
<p>把两个对象分别赋值给 <code>t1</code> 和 <code>t2</code>，并把它们放入数组 <code>list_timmer</code>。另外，创建一个声音数组 <code>list_sound</code>，稍后我们会用到里面的声音。</p>
<pre><code class="language-javascript">const t1 = new Timmer();
t1.name = '计时器一';
const t2 = new Timmer();
t2.name = '计时器二';
const list_timmer = [t1, t2];
const list_sound = ['miao', 'wang'];
</code></pre>
<h3 id="UI-with-functions-1">通过函数实现用户界面的交互</h3>
<p>然后创建 6 个函数。</p>
<p>第一个函数 <code>play_audio()</code>，具有一个参数。函数在 DOM 中创建一个 <code>audio</code> 元素，把它赋值给 <code>audio</code>。然后设置 <code>src</code> 属性值为一个<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals">模板字面量</a>，并调用它的 <code>play()</code> 方法以播放提示音。</p>
<pre><code class="language-javascript">function play_audio(sound) {
    // miao.mp3 和 wang.mp3 是两个本地文件，需要放在和 HTML 文件同一目录下
    const audio = document.createElement('audio');
    audio.src = `${sound}.mp3`;
    audio.play();
}
</code></pre>
<p>第二个函数 <code>btn_start_onclick()</code>，具有一个参数 <code>i</code>。在两个计时器的“开始”按钮被点击时调用这个函数，传入参数为 1 或 2：</p>
<pre><code class="language-html">&lt;input id="tmr-1-btn-start" class=" btn" type="button" value="开始" onclick="btn_start_onclick(1)" /&gt;
</code></pre>
<pre><code class="language-html">&lt;input id="tmr-2-btn-start" class="btn" type="button" value="开始" onclick="btn_start_onclick(2)" /&gt;
</code></pre>
<p>函数首先获取输入框的值，并赋值给计时器，然后通过 <code>dom_update_inputs()</code> 函数设置输入框和按钮的状态。稍后我们将设置 <code>dom_update_inputs()</code>。</p>
<p><strong>还记得我们在一开始初始化了两个回调函数吗？这里我们将给它们赋值一个箭头函数。</strong></p>
<p><code>_on_stop_callback</code> 执行回调函数 <code>dom_update_inputs</code>，下方会讲到。</p>
<p>这里也执行回调函数 <code>play_audio()</code>，参数为 <code>list_sound[i - 1]</code>，即从前面定义的数组 <code>list_sound</code> 中获取元素 <code>miao</code> 或 <code>wang</code>，传给 <code>audio</code> 的 <code>src</code> 属性，当计时器停止计时时播放相应的提示音。例如，当 <code>i</code> 为 1 时，<code>audio.src = miao.mp3;</code>。</p>
<p><code>_on_update_callback</code> 执行回调函数 <code> dom_update_timmer()</code>，我们很快就会讲到这个函数。</p>
<p>通过 <code>const tmr = list_timmer[i - 1];</code> 从计时器数组中取出对应的计时器，执行 <code>start()</code> 方法以开始计时。</p>
<pre><code class="language-javascript">function btn_start_onclick(i) {
    // 获取输入框的值
    const ipt_h = document.getElementById(`ipt-${i}-h`);
    const ipt_m = document.getElementById(`ipt-${i}-m`);
    const ipt_s = document.getElementById(`ipt-${i}-s`);

    // 设置输入框和按钮状态
    dom_update_inputs(i, "COUNTING");
    // 从计时器数组中取出对应的计时器
    const tmr = list_timmer[i - 1];
    // 将输入框的值赋值给计时器
    tmr.h = Number(ipt_h.value);
    tmr.m = Number(ipt_m.value);
    tmr.s = Number(ipt_s.value);

    // 设置回调函数
    tmr._on_stop_callback = () =&gt; {
        // 播放提示音
        play_audio(list_sound[i - 1]);
        // 设置输入框和按钮状态
        dom_update_inputs(i, "STOPPED");
    }
    tmr._on_update_callback = () =&gt; {
        dom_update_timmer(i);
    }
    // 开始计时
    tmr.start();
}
</code></pre>
<p>第三个函数 <code>btn_pause_onclick()</code> 具有一个参数，在两个计时器的“暂停”按钮被点击时调用这个函数，传入参数为 1 或 2。同样执行回调函数 <code> dom_update_inputs</code> 以设置计时暂停时输入框和按钮的状态，并执行 <code>pause()</code> 方法暂停计时。</p>
<pre><code class="language-javascript">function btn_pause_onclick(i) {
    dom_update_inputs(i, "PAUSED");

    // 从计时器数组中取出对应的计时器
    const tmr = list_timmer[i - 1];

    // 暂停计时
    tmr.pause();
}
</code></pre>
<p>第四个函数 <code>btn_stop_onclick()</code> 和第三个函数类似，在两个计时器的“停止”按钮被点击时调用，设置计时停止时输入框和按钮的状态，然后执行 <code>stop()</code> 方法以停止计时。</p>
<pre><code class="language-javascript">function btn_stop_onclick(i) {
    dom_update_inputs(i, "STOPED");

    // 从计时器数组中取出对应的计时器
    const tmr = list_timmer[i - 1];


    // 停止计时
    tmr.stop();
}
</code></pre>
<p>第五个函数 <code>dom_update_inputs()</code> 具有两个参数，<code>i</code> 和 <code>status</code>，通过 <code>if...else if...</code> 语句设置当 <code>status</code> 为不同关键字时输入框和按钮的状态。当这个函数在上述第二、三、四个函数中被调用时，即定义计时器在开始、暂停和停止时输入框和按钮的状态。</p>
<pre><code class="language-javascript">function dom_update_inputs(i, status) {
    if ('COUNTING' === status) {
        // 设置输入框状态
        document.getElementById(`ipt-${i}-h`).disabled = true;
        document.getElementById(`ipt-${i}-m`).disabled = true;
        document.getElementById(`ipt-${i}-s`).disabled = true;

        // 设置按钮的状态
        document.getElementById(`tmr-${i}-btn-start`).disabled = true;
        document.getElementById(`tmr-${i}-btn-pause`).disabled = false;
        document.getElementById(`tmr-${i}-btn-stop`).disabled = false;
    } else if ('PAUSED' === status) {
        // 设置输入框状态
        document.getElementById(`ipt-${i}-h`).disabled = false;
        document.getElementById(`ipt-${i}-m`).disabled = false;
        document.getElementById(`ipt-${i}-s`).disabled = false;

        // 设置按钮的状态
        document.getElementById(`tmr-${i}-btn-start`).disabled = false;
        document.getElementById(`tmr-${i}-btn-pause`).disabled = true;
        document.getElementById(`tmr-${i}-btn-stop`).disabled = false;
    } else if ('STOPPED' === status) {
        // 设置输入框状态
        document.getElementById(`ipt-${i}-h`).disabled = false;
        document.getElementById(`ipt-${i}-m`).disabled = false;
        document.getElementById(`ipt-${i}-s`).disabled = false;

        // 设置按钮的状态
        document.getElementById(`tmr-${i}-btn-start`).disabled = false;
        document.getElementById(`tmr-${i}-btn-pause`).disabled = true;
        document.getElementById(`tmr-${i}-btn-stop`).disabled = true;
    }
}
</code></pre>
<p>第六个函数 <code>dom_update_timmer()</code> 的作用是将计时器的时间同步到页面。</p>
<pre><code class="language-javascript">function dom_update_timmer(i) {
    // 从计时器数组中取出对应的计时器
    const tmr = list_timmer[i - 1];

    // 将计时器的时间同步到页面
    document.getElementById(`ipt-${i}-h`).value = tmr.h;
    document.getElementById(`ipt-${i}-m`).value = tmr.m;
    document.getElementById(`ipt-${i}-s`).value = tmr.s;
} 
</code></pre>
<p>以上，我们将“计时器业务功能”封装到了 Timmer 类中，将“用户界面的交互”保留在全局作用域中，借助于 Timmer 类的实例，实现多个计时器同时运行。</p>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/04/oop.gif" class="center db" width="1090" height="894" alt="oop" loading="lazy">
<p>在这一节里，我没有设置限制时、分、秒数字的输入范围和优化时、分、秒数字的格式，如果你感兴趣，可以参考上一节的代码，在 CodePen demo 中自己动手实现:)</p>
<p>进一步思考，如果我们的项目除了这一组两个计时器，还有别的功能模块，例如，两组计时器，是同一个类型 <code>Timer</code> 的对象实例，在为回调函数 <code>_on_stop_callback</code> 赋值的时候，一组实例需要通过 <code>play_audio()</code> 函数播放提示音，而另一组则需要通过另一个函数设置计时器的颜色，这时候，第二次赋的值就会覆盖第一次赋的值。</p>
<p>我们引入事件机制来解决这个问题。</p>
<h2 id="timer-oop-with-events">在面向对象编程中添加事件机制</h2>
<p>这一节代码在上一节的基础上，添加事件机制代替原来的回调函数。事件机制好处是，当计时器的状态发生变化时，可以通知多个其他对象。对于我们的例子，当计时器的状态发生变化时，会通知页面上的按钮，让按钮的状态同步发生变化。</p>
<p>同样，HTML 和 CSS 部分和第一节类似，这里不再赘述。</p>
<p>你可以在这个 <a href="https://codepen.io/miyaliu666-the-styleful/pen/LYJwPYQ">CodePen demo</a> 中看到全部代码。</p>
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="LYJwPYQ" data-preview="true" data-editable="true" data-user="miyaliu666-the-styleful" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/miyaliu666-the-styleful/pen/LYJwPYQ">
  timer_oop-with-events</a> by miyaliu666 (<a href="https://codepen.io/miyaliu666-the-styleful">@miyaliu666-the-styleful</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h3 id="eventemitter">创建事件发生器</h3>
<p>首先，我们新建一个类 <code>EventEmitter</code>，这是一个事件发射器，用于实现事件机制，在本例中用于实现计时器的状态变化通知。</p>
<p>其中，<code>on</code> 用于监听（订阅）事件，当事件发生时，执行回调函数，回调函数的参数是事件的参数，回调函数的 this 指向事件的触发者。</p>
<p><code>emit</code> 用于发射（抛出）事件。</p>
<p><code>removeListener</code> 用于移除某个事件的某个监听器。</p>
<pre><code class="language-javascript">class EventEmitter {
    constructor() {
        this._events = {};
    }

    on(type, listener) {
        if (this._events[type]) {
            this._events[type].push(listener);
        } else {
            this._events[type] = [listener];
        }
    }

    emit(type, ...args) {
        if (this._events[type]) {
            this._events[type].forEach(listener =&gt; {
                listener(...args);
            });
        }
    }

    removeListener(type, listener) {
        if (this._events[type] &amp;&amp; listener) {
            this._events[type] = this._events[type].filter(l =&gt; l !== listener);
        } else if (this._events[type] &amp;&amp; !listener) {
            this._events[type] = [];
        }
    }
}
</code></pre>
<h3 id="new-class-2">新建计时器类</h3>
<p>这里，我们新建一个类 <code>Timmer</code>。<code>extends</code> 关键字表示 <code>Timmer</code> 是 <code>EventEmitter</code> 类的子类。子类继承父类的所有属性和方法。</p>
<p>关于 <code>extends</code> 关键字的更多信息，你可以在<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/extends">这里</a>查看。</p>
<pre><code class="language-javascript">class Timmer extends EventEmitter {
    constructor() {
        super();
        this.name = '未命名计时器';
        this.timmer = undefined;
        this.h = 0;
        this.m = 0;
        this.s = 10;
    }

    _on_update() {
        if (0 === this.h &amp;&amp; 0 === this.m &amp;&amp; 0 === this.s) {
            this.stop();
            return;
        } else if (0 === this.s) {
            this.s = 59;
            if (0 === this.m) {
                this.m = 59;
                this.h = this.h - 1;
            } else {
                this.m = this.m - 1;
            }
        } else {
            this.s = this.s - 1;
        }

        this.show()
        // 抛出事件
        this.emit('update', {
            h: this.h,
            m: this.m,
            s: this.s
        });
        if (0 === this.h &amp;&amp; 0 === this.m &amp;&amp; 0 === this.s) {
            this.stop();
        }
    }

    start() {
        if (this.timmer) {
            console.log(`[${this.name}]已经在计时了`);
            return;
        }
        console.log(`[${this.name}]开始计时`);
        this.timmer = setInterval(() =&gt; {
            this._on_update();
        }, 1000);
        this.show();

        // 抛出事件
        this.emit('start', {
            h: this.h,
            m: this.m,
            s: this.s
        });
    }

    stop() {
        console.log(`[${this.name}]停止计时`);
        clearInterval(this.timmer);
        this.timmer = undefined;

        // 抛出事件
        this.emit('stop', {
            h: this.h,
            m: this.m,
            s: this.s
        });
    }

    pause() {
        console.log(`[${this.name}]暂停计时`);
        clearInterval(this.timmer);
        this.timmer = undefined;

        // 抛出事件
        this.emit('pause', {
            h: this.h,
            m: this.m,
            s: this.s
        });
    }

    show() {
        console.log(`[${this.name}]当前时间：${this.h}:${this.m}:${this.s}`);
    }
}
</code></pre>
<p>你可以看到这段代码的注释中有四处“抛出事件”。<code>emit</code> 方法分别抛出 <code>update</code>、<code>start</code>、<code>stop</code>、<code>pause</code> 四个事件，将计时器内部的变化发射出去。所有订阅这些事件的对象都会执行相应的回调函数。</p>
<h3 id="create-objects-2">创建对象实例</h3>
<p>同样，我们新建两个计时器对象实例 <code>t1</code> 和 <code>t2</code>，以及储存计时器和声音的数组，赋值给 <code>list_timmer</code> 和 <code>list_sound</code>。</p>
<pre><code class="language-javascript">const t1 = new Timmer();
t1.name = '计时器一';
const t2 = new Timmer();
t2.name = '计时器二';
const list_timmer = [t1, t2];
const list_sound = ['miao', 'wang'];
const list_sound_str = ['🐱喵~~~', '🐶汪~汪~汪~'];
</code></pre>
<h3 id="UI-with-functions-2">通过函数实现用户界面的交互</h3>
<p>和上一节一样，这里需要创建六个函数。</p>
<p>其中，<code>play_audio()</code>、<code>btn_pause_onclick</code>、<code>btn_stop_onclick</code>、<code>dom_update_inputs()</code>、<code>dom_update_timmer()</code> 五个函数和上一节相同。</p>
<p>我们以 <code>btn_start_onclick()</code> 函数为例说明事件订阅的机制。</p>
<pre><code class="language-javascript">function btn_start_onclick(i) {
    // 获取输入框的值
    const ipt_h = document.getElementById(`ipt-${i}-h`);
    const ipt_m = document.getElementById(`ipt-${i}-m`);
    const ipt_s = document.getElementById(`ipt-${i}-s`);

    // 设置输入框和按钮状态
    dom_update_inputs(i, "COUNTING");

    // 从计时器数组中取出对应的计时器
    const tmr = list_timmer[i - 1];
    // 将输入框的值赋值给计时器
    tmr.h = Number(ipt_h.value);
    tmr.m = Number(ipt_m.value);
    tmr.s = Number(ipt_s.value);

    // 订阅计时器的 update 事件，同步显示时间到页面
    tmr.removeListener('update');
    tmr.removeListener('stop');
    tmr.on('update', () =&gt; dom_update_timmer(i));
    tmr.on('stop', () =&gt; {
        console.log(list_sound_str[i - 1]);
    });
    tmr.on('stop', () =&gt; {
        // 播放提示音
        play_audio(list_sound[i - 1]);
        // 设置输入框和按钮状态
        dom_update_inputs(i, "STOPPED");
    });

    // 开始计时
    tmr.start();
}
</code></pre>
<p>这个函数是在用户点击“开始”按钮时被调用的，它通过 <code>on()</code> 方法订阅了 <code>update</code> 事件，执行 <code>dom_update_timmer()</code> 回调函数。</p>
<p>并且，它订阅了两次 <code>stop</code> 事件，执行不同的功能模块，一次是在控制台打印声音文字，一次是播放提示音与设置输入框和按钮状态，互不干扰。<strong>这就是事件机制相对于上一节中 <code>_on_stop_callback</code> 回调函数的优势。</strong></p>
<p>注意开头的 <code>tmr.removeListener('update');</code> 和 <code>tmr.removeListener('stop');</code>，这是为了在每次执行 <code>this.start()</code> 时移除事件监听器（如有）。</p>
<h2 id="conclusions">总结</h2>
<p>在这篇文章中，我们对比了基于面向过程和面向对象两种编程思想创建计时器，并在面向对象编程中添加事件机制，逐步探索编程范式的最佳实践。如果你觉得某些代码块不容易理解，建议你多花点时间理解它，并且尝试自己写几遍。</p>
<p>如果你想就本文的内容和我讨论或者给我提出建议，欢迎你在 <a href="https://forum.freecodecamp.org/">freeCodeCamp 论坛</a>给我发送消息，我的 id 是 miyaliu。</p>
<p>谢谢你阅读本文，happy coding！</p>
<p>封面图 by <a href="https://unsplash.com/@yogendras31?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Yogendra Singh</a> on <a href="https://unsplash.com/s/photos/timer?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
<!--kg-card-end: markdown--> ]]>
                </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[ 面向对象编程的 SOLID 原则 ]]>
                </title>
                <description>
                    <![CDATA[ SOLID 原则是面向对象 class 设计的五条原则。他们是设计 class 结构时应该遵守的准则和最佳实践。 通常，这五个原则可以帮助我们了解设计模式和软件架构。这是每个开发人员都应该了解的主题。 这篇文章介绍了在项目中使用 SOLID 原则的细节。 首先我们先看一下 SOLID 原则的历史。然后我们会设计一个 class 并且逐步改善它，来亲密接触 SOLID 原则，了解为什么使用以及怎么使用各个原则。 准备一杯咖啡或者茶，让我们马上开始！ 背景 SOLID 原则首先由著名的计算机科学家 Robert C·Martin （著名的Bob大叔）由 2000 年在他的论文 [https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf]中提出。但是 SOLID 缩略词是稍晚由 Michael Feathers 先使用的。 Bob大叔也是畅销书《代码整洁之道》和《架构整洁之道》的作者，也是 "Agile Alliance" [https://agilemanifesto.org/history.htm ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/solid-principles/</link>
                <guid isPermaLink="false">602f3a2dc354c605689ea513</guid>
                
                    <category>
                        <![CDATA[ 面向对象 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Thu, 18 Feb 2021 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/solid.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>SOLID 原则是面向对象 class 设计的五条原则。他们是设计 class 结构时应该遵守的准则和最佳实践。</p>
<p>通常，这五个原则可以帮助我们了解设计模式和软件架构。这是每个开发人员都应该了解的主题。</p>
<p>这篇文章介绍了在项目中使用 SOLID 原则的细节。</p>
<p>首先我们先看一下 SOLID 原则的历史。然后我们会设计一个 class 并且逐步改善它，来亲密接触 SOLID 原则，了解为什么使用以及怎么使用各个原则。</p>
<p>准备一杯咖啡或者茶，让我们马上开始！</p>
<h2 id="">背景</h2>
<p>SOLID 原则首先由著名的计算机科学家 Robert C·Martin （著名的Bob大叔）由 2000 年在他的<a href="https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf">论文</a>中提出。但是 SOLID 缩略词是稍晚由 Michael Feathers 先使用的。</p>
<p>Bob大叔也是畅销书《代码整洁之道》和《架构整洁之道》的作者，也是 <a href="https://agilemanifesto.org/history.html">"Agile Alliance"</a> 的成员。</p>
<p>因此，代码整洁、面向对象架构、设计模式彼此互补并以这种方式连接就不足为奇了。</p>
<p>他们达成的目标是一致的：</p>
<blockquote>
<p>“创建可多人协作的、易于理解的、易读的以及可测试的代码。”</p>
</blockquote>
<p>现在依次看一下各个原则，SOLID 是以下是原则的缩写：</p>
<ul>
<li>S 单一职责原则</li>
<li>O 开闭原则</li>
<li>L 里氏替换原则</li>
<li>I 接口隔离原则</li>
<li>D 依赖倒置原则</li>
</ul>
<h2 id="">单一职责原则</h2>
<p>单一职责原则的描述是 ** 一个 class 应该只做一件事，一个 class 应该只有一个变化的原因**。</p>
<p>更技术的描述该原则：应该只有一个软件定义的潜在改变（数据库逻辑、日志逻辑等）能够影响 class 的定义。</p>
<p>这意味着如果 class 是一个数据容器，比如 Book class 或者 Student class，考虑到这个实体有一些字段，应该只有我们更改了数据定义时才能够修改这些字段。</p>
<p>遵守单一职责原则很重要。首先，可能很多不同的团队可能修改同一个项目，可能因为不同的原因修改同一个 class，会导致冲突。</p>
<p>其次，单一职责更容易版本管理，比如，有一个持久化 class 处理数据库操作，我们在 GitHub 看到某个文件上有一处修改。如果遵循 SRP 原则，根据文件就能判断这是关于存储或者数据库相关的提交。</p>
<p>另一个例子是合并冲突，当不同的团队修改同一个文件时，如果遵循 SRP原则，冲突很少会发生，因为文件只有一个变化的原因，即使出现冲突也会很容易解决。</p>
<h3 id="">常见错误和反面教材</h3>
<p>在本节我们会看一些违背单一职责原则的常见错误。然后会探讨修复他们的方法。</p>
<p>我们会以一个简单的书店发票程序代码作为例子。让我们从定义一个使用发票的图书 class 开始。</p>
<pre><code class="language-java">
class Book {
	String name;
	String authorName;
	int year;
	int price;
	String isbn;

	public Book(String name, String authorName, int year, int price, String isbn) {
		this.name = name;
		this.authorName = authorName;
		this.year = year;
        this.price = price;
		this.isbn = isbn;
	}
}

</code></pre>
<p>这是一个有一些字段的 book class。没什么新奇的。之所以没有把字段设置为私有的是因为想专注于逻辑而不是 getter 和 setter。</p>
<p>现在让我们来创建一个 invoice class，包含创建发票和计算总额的业务逻辑。目前为止，假设书店只卖书，不卖别的。</p>
<pre><code class="language-java">
public class Invoice {

	private Book book;
	private int quantity;
	private double discountRate;
	private double taxRate;
	private double total;

	public Invoice(Book book, int quantity, double discountRate, double taxRate) {
		this.book = book;
		this.quantity = quantity;
		this.discountRate = discountRate;
		this.taxRate = taxRate;
		this.total = this.calculateTotal();
	}

	public double calculateTotal() {
	        double price = ((book.price - book.price * discountRate) * this.quantity);

		double priceWithTaxes = price * (1 + taxRate);

		return priceWithTaxes;
	}

	public void printInvoice() {
            System.out.println(quantity + "x " + book.name + " " +          book.price + "$");
            System.out.println("Discount Rate: " + discountRate);
            System.out.println("Tax Rate: " + taxRate);
            System.out.println("Total: " + total);
	}

        public void saveToFile(String filename) {
	// Creates a file with given name and writes the invoice
	}

}

</code></pre>
<p>这是 invoice class。它包含一些发票相关的字段以及三个方法。</p>
<ul>
<li><strong>calculateTotal</strong> 方法，计算总价格</li>
<li><strong>printInvoice</strong>  方法，打印发票信息到控制台</li>
<li><strong>saveToFile</strong>  方法，负责将发票写到一个文件里</li>
</ul>
<p>在读下一段之前停下来想一想，这样的 class 设计有什么问题。</p>
<p>那么问题出在哪呢？ 我们的 class 在多个地方都违背了单一职责原则。</p>
<p>第一处是 <strong>printInvoice</strong> 方法，因为里面包含了打印逻辑。SRP 描述 class 应该只有一个变化的原因，这个变化原因应该是 class 里的发票计算。</p>
<p>在这个架构里，如果我们想要改变打印格式，我们需要修改这个 class。我们不能把打印逻辑和业务逻辑混合在一个class 里。</p>
<p>在 class 里面还有一个方法违背了 SRP： <strong>saveToFile</strong>  方法。这也是一个很常见的错误，把持久化逻辑和业务逻辑混合在了一起。</p>
<p>这不单单是写入文件 - 也可能是存库，发起 API 调用或者其他与持久化相关的操作。</p>
<p>你可能会问，怎样修复这个打印函数呢？</p>
<p>可以为打印和持久化逻辑创造一个新 class，因此就无需因为这些原因修改 invoice class 了。</p>
<p>创建两个 class， <strong>InvoicePrinter</strong> 和  <strong>InvoicePersistence</strong> ，并移入相应方法。</p>
<pre><code class="language-java">
public class InvoicePrinter {
    private Invoice invoice;

    public InvoicePrinter(Invoice invoice) {
        this.invoice = invoice;
    }

    public void print() {
        System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
        System.out.println("Discount Rate: " + invoice.discountRate);
        System.out.println("Tax Rate: " + invoice.taxRate);
        System.out.println("Total: " + invoice.total + " $");
    }
}

</code></pre>
<pre><code class="language-java">
public class InvoicePersistence {
    Invoice invoice;

    public InvoicePersistence(Invoice invoice) {
        this.invoice = invoice;
    }

    public void saveToFile(String filename) {
        // Creates a file with given name and writes the invoice
    }
}

</code></pre>
<p>现在 class 结构遵从了单一职责原则，每个 class 为我们应用的一个部分负责。棒！</p>
<h2 id="">开闭原则</h2>
<p>开闭原则要求“class 应该对扩展开放对修改关闭”。</p>
<p>修改意味着修改存在 class 的代码，扩展意味着添加新的功能。</p>
<p>这个原则想要表达的是：我们应该能在不动 class 已经存在代码的前提下添加新的功能。这是因为当我们修改存在的代码时，我们就面临着创建潜在 bug 的风险。因此，如果可能，应该避免碰通过测试的（大部分时候）可靠的生产环境的代码。</p>
<p>你可能会好奇，怎样不动 class 还能添加新功能，接口和抽象类可以做到。</p>
<p>现在基本概念已经介绍完了，让我们给发票应用应用一下这个原则。</p>
<p>假如老板来了，提了一个需求，他们想把发票存入数据库以方便查找。我一想，行啊，小菜一碟，五分钟搞定。</p>
<p>创建数据库，连接，给  <strong>InvoicePersistence</strong> 添加保存方法：</p>
<pre><code class="language-java">
public class InvoicePersistence {
    Invoice invoice;

    public InvoicePersistence(Invoice invoice) {
        this.invoice = invoice;
    }

    public void saveToFile(String filename) {
        // Creates a file with given name and writes the invoice
    }

    public void saveToDatabase() {
        // Saves the invoice to database
    }
}

</code></pre>
<p>很不幸，作为书店的懒家伙，并没有把 class 设计的易于未来扩展。为了添加这一特性，需要修改 <strong>InvoicePersistence</strong> class。</p>
<p>如果 class 设计遵循开闭原则，我们就不需要修改这个 class 了。</p>
<p>因此，作为书店里聪明的懒家伙，我们发现了设计问题并决定重构代码以符合开闭原则。</p>
<pre><code class="language-java">
interface InvoicePersistence {

    public void save(Invoice invoice);
}

</code></pre>
<p>我们把 <strong>InvoicePersistence</strong>  改成了接口类型并添加了 save 方法。每个持久化 class 都实现这个 save 方法。</p>
<pre><code class="language-java">
public class DatabasePersistence implements InvoicePersistence {

    @Override
    public void save(Invoice invoice) {
        // Save to DB
    }
}

</code></pre>
<pre><code class="language-java">
public class FilePersistence implements InvoicePersistence {

    @Override
    public void save(Invoice invoice) {
        // Save to file
    }
}

</code></pre>
<p>class 结构如下：<br>
<img src="https://erinc.io/wp-content/uploads/2020/08/SOLID-Tutorial-1-1024x554.jpeg" alt="SOLID-Tutorial-1-1024x554" width="600" height="400" loading="lazy"></p>
<p>现在持久化逻辑更易于扩展了，如果老板要求我们添加另一个数据库，有了两种不同类型的数据库如 MySQL和 MongoDB ，可以更快搞定了。</p>
<p>你可能会想，我们只需创建多个 class 给每个都添加一个 save 方法而无需接口。</p>
<p>来看一下如果我们不用接口来扩展 app，创建多个持久化 class 如 <strong>InvoicePersistence</strong>， <strong>BookPersistence</strong> ，还需要创建了一个 <strong>PersistenceManager</strong> class 来管理所有的持久化 class：</p>
<pre><code class="language-java">
public class PersistenceManager {
    InvoicePersistence invoicePersistence;
    BookPersistence bookPersistence;
    
    public PersistenceManager(InvoicePersistence invoicePersistence,
                              BookPersistence bookPersistence) {
        this.invoicePersistence = invoicePersistence;
        this.bookPersistence = bookPersistence;
    }
}

</code></pre>
<p>有了多态，我们可以把任何实现了 <strong>InvoicePersistence</strong> 接口的 class 作为入参。这就是接口的灵活性。</p>
<h2 id="">里氏替换原则</h2>
<p>里氏替换原则描述的是子类应该能替换为它的基类。</p>
<p>意思是，给定 class B 是 class A 的子类，在预期传入 class A 的对象的任何方法传入 class B 的对象，方法都不应该有异常。</p>
<p>这是一个预期的行为，因为继承假定子类继承了父类的一切。子类可以扩展行为但不会收窄。</p>
<p>因此，当 class 违背这一原则时，会导致一些难于发现的讨厌的 bug。</p>
<p>里氏替换原则容易理解但是很难在代码里发现。看一个例子：</p>
<pre><code class="language-java">
class Rectangle {
	protected int width, height;

	public Rectangle() {
	}

	public Rectangle(int width, int height) {
		this.width = width;
		this.height = height;
	}

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getHeight() {
		return height;
	}

	public void setHeight(int height) {
		this.height = height;
	}

	public int getArea() {
		return width * height;
	}
}

</code></pre>
<p>有一个简单的 Rectangle class，以及一个 <strong>getArea</strong> 方法返回矩形的面积。</p>
<p>现在准备创建另一个 Squares class。众所周知，正方形只不过是宽和高相等的特殊的矩形。</p>
<pre><code class="language-java">
class Square extends Rectangle {
	public Square() {}

	public Square(int size) {
		width = height = size;
	}

	@Override
	public void setWidth(int width) {
		super.setWidth(width);
		super.setHeight(width);
	}

	@Override
	public void setHeight(int height) {
		super.setHeight(height);
		super.setWidth(height);
	}
}

</code></pre>
<p>我们的 Square class 继承自 Rectangle class。在构造器里设置宽和高相等，我们不希望任何客户端（在他们的代码里使用我们的 class）违背了正方形的特性将宽高改成不相等。</p>
<p>因此我们重载了 setter 使宽和高任何一个改变时都会同时改变宽高。这样一来，我们就违背了里氏替换原则。</p>
<p>让我们先编写一个 test 来测试  <strong>getArea</strong>  函数。</p>
<pre><code class="language-java">
class Test {

   static void getAreaTest(Rectangle r) {
      int width = r.getWidth();
      r.setHeight(10);
      System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea());
   }

   public static void main(String[] args) {
      Rectangle rc = new Rectangle(2, 3);
      getAreaTest(rc);

      Rectangle sq = new Square();
      sq.setWidth(5);
      getAreaTest(sq);
   }
}

</code></pre>
<p>团队的测试人员提出测试函数  <strong>getAreaTest</strong> ，然后告诉你正方形对象的 <strong>getArea</strong> 函数不能通过测试。</p>
<p>在第一个测试中，我们创建了一个宽为 2 高为 3 的矩形，然后调用 <strong>getAreaTest</strong>，预期输出为 20，但是当传入一个正方形时出错了。这是因为调用测试里的  <strong>setHeight</strong>  函数会同时设置 width，导致输出结果不符预期。</p>
<h2 id="">接口隔离原则</h2>
<p>隔离意味着保持独立，接口隔离原则是关于接口的独立。</p>
<p>该原则描述了很多客户端特定的接口优于一个多用途接口。客户端不应该强制实现他们不需要的函数。</p>
<p>这是一个简单的原则，很好理解和实践，直接看例子。</p>
<pre><code class="language-java">
public interface ParkingLot {

	void parkCar();	// Decrease empty spot count by 1
	void unparkCar(); // Increase empty spots by 1
	void getCapacity();	// Returns car capacity
	double calculateFee(Car car); // Returns the price based on number of hours
	void doPayment(Car car);
}

class Car {

}

</code></pre>
<p>我们定义了一个非常简单的停车场。这是按小时付费的停车场，不考虑免费的情况。</p>
<pre><code class="language-java">
public class FreeParking implements ParkingLot {

	@Override
	public void parkCar() {
		
	}

	@Override
	public void unparkCar() {

	}

	@Override
	public void getCapacity() {

	}

	@Override
	public double calculateFee(Car car) {
		return 0;
	}

	@Override
	public void doPayment(Car car) {
		throw new Exception("Parking lot is free");
	}
}

</code></pre>
<p>停车场接口组合了两个事情：停车相关逻辑（停车、取车、获取车位信息）以及支付相关逻辑。</p>
<p>但是这太具体了。即使是 FreeParking class 也要必须实现不相关的支付相关的方法。让我们隔离接口。</p>
<p><img src="https://erinc.io/wp-content/uploads/2020/08/SOLID-Tutorial-1024x432.png" alt="SOLID-Tutorial-1024x432" width="600" height="400" loading="lazy"></p>
<p>现在停车场更干净了。有了新的 model，可以更进一步把 <strong>PaidParkingLot</strong> 分割一下以支持更多的支付类型。</p>
<p>现在我们的 model 更灵活、可扩展，客户端无需实现任何不相关的逻辑，因为只在停车场接口实现了停车相关的函数。</p>
<h2 id="">依赖倒置原则</h2>
<p>依赖倒置原则描述的是我们的 class 应该依赖接口和抽象类而不是具体的类和函数。</p>
<p>在这篇<a href="https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf">文章</a>（2000）里，Bob 大叔如下总结该原则：</p>
<blockquote>
<p>“如果 OCP 声明了 OO 体系结构的目标，那么 DIP 则声明了主要机制”。</p>
</blockquote>
<p>这两个原则的确息息相关，我们在讨论开闭原则之前也要用到这一模式。</p>
<p>我们想要我们的类开放扩展，因此我们需要明确我们的依赖的是接口而不是具体的类。我们的 PersistenceManager class 依赖 InvoicePersistence 而不是实现了这个接口的 class。</p>
<h2 id="">结论</h2>
<p>在本文中，先介绍了SOLID 原则的历史，接着尝试解释清楚为什么使用以及怎么使用各个原则。我们甚至重构了一个简单的发票应用来遵循 SOLID 原则。</p>
<p>感谢你花时间阅读本文，希望上面的概念已经解释清楚了。</p>
<p>建议在设计、编写、重构代码时能够记住这些原则，这样代码可以更干净、可扩展、可测试。</p>
<p>如果你想阅读更多类似的文章，可以邮件订阅我的<a href="https://erinc.io/">博客</a>，这样我发新文章的时候就会通知你啦。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/">The SOLID Principles of Object-Oriented Programming Explained in Plain English</a>，作者：<a href="https://www.freecodecamp.org/news/author/erinc/">Yiğit Kemal Erinç</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何模拟实现JS的new操作符 ]]>
                </title>
                <description>
                    <![CDATA[ 用过Vuejs的同学都知道，需要用new操作符来实例化。 new Vue({     el: '#app',     mounted(){}, }); 我们在面试的时候，面试官可能会问：是否想过new到底做了什么，怎么模拟实现呢？ 附上我在之前的文章中写过的一段话：已经有很多模拟实现new 操作符的文章，为什么我还要写一遍呢？学习就好比是座大山，人们沿着不同的路登山，分享着自己看到的风景。你不一定能看到别人看到的风景，体会到别人的心情。只有自己去登山，才能看到不一样的风景，体会才更加深刻。 new 操作符做了什么 先看简单例子1： // 例子1 function Student(){ } var student = new Student(); console.log(student); // {} // student 是一个对象。 console.log(Object.prototype.toString.call(student)); // [object Object] // 我们知道平时声明对象也可以用new Object(); 只是看起来更复杂 // 顺便提一下  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-new-operator/</link>
                <guid isPermaLink="false">5fa90b745f583f0565090fb5</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 面向对象 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Wed, 11 Nov 2020 05:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1544980919-e17526d4ed0a.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>用过<code>Vuejs</code>的同学都知道，需要用<code>new</code>操作符来实例化。</p><pre><code>new Vue({
    el: '#app',
    mounted(){},
});</code></pre><p><strong>我们在面试的时候，面试官可能会问：是否想过<code>new</code>到底做了什么，怎么模拟实现呢？</strong></p><p>附上我在之前的文章中写过的一段话：已经有很多模拟实现<code>new</code>操作符的文章，为什么我还要写一遍呢？学习就好比是座大山，人们沿着不同的路登山，分享着自己看到的风景。你不一定能看到别人看到的风景，体会到别人的心情。只有自己去登山，才能看到不一样的风景，体会才更加深刻。</p><h3 id="new-"><code>new</code> 操作符做了什么</h3><p>先看简单<strong>例子1</strong>：</p><pre><code>// 例子1
function Student(){
}
var student = new Student();
console.log(student); // {}
// student 是一个对象。
console.log(Object.prototype.toString.call(student)); // [object Object]
// 我们知道平时声明对象也可以用new Object(); 只是看起来更复杂
// 顺便提一下 `new Object`(不推荐)和Object()也是一样的效果
// 可以猜测内部做了一次判断，用new调用
/** if (!(this instanceof Object)) {
*    return new Object();
*  }
*/
var obj = new Object();
console.log(obj) // {}
console.log(Object.prototype.toString.call(student)); // [object Object]

typeof Student === 'function' // true
typeof Object === 'function' // true</code></pre><p>从这里例子中，我们可以看出：一个函数用<code>new</code>操作符来调用后，生成了一个全新的对象。而且<code>Student</code>和<code>Object</code>都是函数，只不过<code>Student</code>是我们自定义的，<code>Object</code>是<code>JS</code>本身就内置的。 再来看下控制台输出图，感兴趣的读者可以在控制台试试。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-12.png" class="kg-image" alt="image-12" width="573" height="870" loading="lazy"></figure><p>与<code>new Object()</code> 生成的对象不同的是<code>new Student()</code>生成的对象中间还嵌套了一层<code>__proto__</code>，它的<code>constructor</code>是<code>Student</code>这个函数。</p><pre><code>// 也就是说：
student.constructor === Student;
Student.prototype.constructor === Student;</code></pre><h4 id="-1-new-">小结1：从这个简单例子来看，<code>new</code>操作符做了两件事：</h4><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li></ol><p>接下来我们再来看升级版的<strong>例子2</strong>：</p><pre><code>// 例子2
function Student(name){
    console.log('赋值前-this', this); // {}
    this.name = name;
    console.log('赋值后-this', this); // {name: '若川'}
}
var student = new Student('若川');
console.log(student); // {name: '若川'}</code></pre><p>由此可以看出：这里<code>Student</code>函数中的<code>this</code>指向<code>new Student()</code>生成的对象<code>student</code>。</p><h4 id="-2-new-">小结2：从这个例子来看，<code>new</code>操作符又做了一件事：</h4><ol><li>生成的新对象会绑定到函数调用的<code>this</code>。</li></ol><p>接下来继续看升级版<strong>例子3</strong>：</p><pre><code>// 例子3
function Student(name){
    this.name = name;
    // this.doSth();
}
Student.prototype.doSth = function() {
    console.log(this.name);
};
var student1 = new Student('若');
var student2 = new Student('川');
console.log(student1, student1.doSth()); // {name: '若'} '若'
console.log(student2, student2.doSth()); // {name: '川'} '川'
student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是浏览器实现的查看原型方案。
// 用ES5 则是：
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-13.png" class="kg-image" alt="image-13" width="628" height="898" loading="lazy"></figure><p>关于JS的原型关系笔者之前看到这张图，觉得很不错，分享给大家。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-14.png" class="kg-image" alt="image-14" width="1019" height="741" loading="lazy"></figure><h4 id="-3-3-1-2-prototype-__proto__-new-student-prototype-student-protytype-">小结3：这个例子3再一次验证了<strong>小结1</strong>中的<strong>第2点</strong>。也就是这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。并且通过<code>new Student()</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个<code>Student.protytype</code>对象上。</h4><p>细心的同学可能会发现这三个例子中的函数都没有返回值。那么有返回值会是怎样的情形呢。 那么接下来请看<strong>例子4</strong></p><pre><code>// 例子4
function Student(name){
    this.name = name;
    // Null（空） null
    // Undefined（未定义） undefined
    // Number（数字） 1
    // String（字符串）'1'
    // Boolean（布尔） true
    // Symbol（符号）（第六版新增） symbol
    
    // Object（对象） {}
        // Function（函数） function(){}
        // Array（数组） []
        // Date（日期） new Date()
        // RegExp（正则表达式）/a/
        // Error （错误） new Error() 
    // return /a/;
}
var student = new Student('若川');
console.log(student); {name: '若川'}</code></pre><p>笔者测试这七种类型后<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/A_re-introduction_to_JavaScript" rel="nofollow noopener noreferrer">MDN JavaScript类型</a>，得出的结果是：前面六种基本类型都会正常返回<code>{name: '若川'}</code>，后面的<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)都会直接返回这些值。</p><h4 id="-4-">由此得出 小结4：</h4><ol><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li></ol><p>结合这些小结，整理在一起就是：</p><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li><li>生成的新对象会绑定到函数调用的<code>this</code>。</li><li>通过<code>new</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个函数的<code>prototype</code>对象上。</li><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li></ol><h3 id="new--1">new 模拟实现</h3><p>知道了这些现象，我们就可以模拟实现<code>new</code>操作符。直接贴出代码和注释</p><pre><code>/**
 * 模拟实现 new 操作符
 * @param  {Function} ctor [构造函数]
 * @return {Object|Function|Regex|Date|Error}      [返回结果]
 */
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向构造函数
    newOperator.target = ctor;
    // 1.创建一个全新的对象，
    // 2.并且执行[[Prototype]]链接
    // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
    // 除去ctor构造函数的其余参数
    var argsArr = [].slice.call(arguments, 1);
    // 3.生成的新对象会绑定到函数调用的`this`。
    // 获取到ctor函数返回结果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 小结4 中这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null，排除null
    var isObject = typeof ctorReturnResult === 'object' &amp;&amp; ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`)，那么`new`表达式中的函数调用会自动返回这个新的对象。
    return newObj;
}</code></pre><p>最后用模拟实现的<code>newOperator</code>函数验证下之前的<strong>例子3</strong>：</p><pre><code>// 例子3 多加一个参数
function Student(name, age){
    this.name = name;
    this.age = age;
    // this.doSth();
    // return Error();
}
Student.prototype.doSth = function() {
    console.log(this.name);
};
var student1 = newOperator(Student, '若', 18);
var student2 = newOperator(Student, '川', 18);
// var student1 = new Student('若');
// var student2 = new Student('川');
console.log(student1, student1.doSth()); // {name: '若'} '若'
console.log(student2, student2.doSth()); // {name: '川'} '川'

student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是浏览器实现的查看原型方案。
// 用ES5 则是：
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true</code></pre><p>可以看出，很符合<code>new</code>操作符。读者发现有不妥或可改善之处，欢迎指出。 回顾这个模拟<code>new</code>函数<code>newOperator</code>实现，最大的功臣当属于<code>Object.create()</code>这个<code>ES5</code>提供的<code>API</code>。</p><h3 id="object-create-">Object.create() 用法举例</h3><p>笔者之前整理的一篇文章中也有讲过，可以翻看<a href="https://segmentfault.com/a/1190000010753942" rel="nofollow noopener noreferrer">JavaScript 对象所有API解析</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create" rel="nofollow noopener noreferrer">MDN Object.create()</a></p><p><code>Object.create(proto, [propertiesObject])</code>方法创建一个新对象，使用现有的对象来提供新创建的对象的__proto__。 它接收两个参数，不过第二个可选参数是属性描述符（不常用，默认是<code>undefined</code>）。</p><pre><code>var anotherObject = {
    name: '若川'
};
var myObject = Object.create(anotherObject, {
    age: {
        value：18,
    },
});
// 获得它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 说明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "若川"} // 说明myObject的原型是{name: "若川"}
myObject.hasOwnProperty('name'); // false; 说明name是原型上的。
myObject.hasOwnProperty('age'); // true 说明age是自身的
myObject.name; // '若川'
myObject.age; // 18;</code></pre><p>对于不支持<code>ES5</code>的浏览器，<code>MDN</code>上提供了<code>ployfill</code>方案。</p><pre><code>if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' &amp;&amp; typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;
        return new F();
    };
}</code></pre><p>到此，文章就基本写完了。感谢读者看到这里。</p><h3 id="-">最后总结一下：</h3><ol><li><code>new</code>做了什么：</li></ol><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li><li>生成的新对象会绑定到函数调用的<code>this</code>。</li><li>通过<code>new</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个函数的<code>prototype</code>对象上。</li><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li><li>怎么模拟实现</li></ol><pre><code>// 去除了注释
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    newOperator.target = ctor;
    var newObj = Object.create(ctor.prototype);
    var argsArr = [].slice.call(arguments, 1);
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    var isObject = typeof ctorReturnResult === 'object' &amp;&amp; ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    return newObj;
}</code></pre><p>读者发现有不妥或可改善之处，欢迎指出。</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
