SegmentFault David Chen 的编程大杂烩最新的文章
2017-02-25T22:53:59+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
用 JavaScript 实现链表操作 - 18 Recursive Reverse
https://segmentfault.com/a/1190000008485170
2017-02-25T22:53:59+08:00
2017-02-25T22:53:59+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>用递归的方式反转链表,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现函数 <code>reverse()</code> 用递归的方式反转链表。例子如下:</p>
<pre><code class="js">var list = 2 -> 1 -> 3 -> 6 -> 5 -> null
reverse(list) === 5 -> 6 -> 3 -> 1 -> 2 -> null</code></pre>
<h2>解法</h2>
<p>让我们先思考一下递归的大概解法:</p>
<pre><code class="js">function reverse(head) {
const node = new Node(head.data)
const rest = reverse(head.next)
// 把 node 放到 rest 的末尾,并返回 rest
}</code></pre>
<p>麻烦的地方就在最后,把节点加入链表的末尾需要首先遍历整个链表,这无疑非常低效。我们在上一个 kata 的循环里是怎么解决的呢?维护一个 <code>result</code> 变量代表反转链表,然后每次把新节点放到 <code>result</code> 的头部,同时把新节点当做新的 <code>result</code> ,大概这个样子:</p>
<pre><code class="js">let result
for (let node = list; node; node = node.next) {
result = new Node(node.data, result)
}</code></pre>
<p>为了在递归里达到同样的效果,我们也必须维护这么一个变量。为了在每次递归过程中都能用到这个变量,我们得把它当函数的参数传递下去,<code>reverse</code> 的函数签名就变成这样:</p>
<pre><code class="js">function reverse(head, acc) { ... }</code></pre>
<p>这里 <code>acc</code> 就是反转的链表。整理一番后的代码如下:</p>
<pre><code class="js">function reverse(head, acc = null) {
return head ? reverse(head.next, new Node(head.data, acc)) : acc
}</code></pre>
<p>上面这段代码同时也是尾递归。在递归函数中开额外的参数很是常见的做法,也是尾递归优化的必要手段。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=UenTkCUTTo0PVYVd4%2BmrAw%3D%3D.ENZMV8DTt4N8yp7edjYkxlig310gX%2F0nFOYMxtS5cNPwXhXM8DRvqBgXryEEb4rotRLcUCX0Sju%2BYVM59ID5xA%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=mAB9RozluoQBC6Ghx7aPBg%3D%3D.MSoPqlsFLBApz7choJ%2FrcBc47qAAKUOommd4gpz0YJr1JS4GEO1xfB67HS589SjVQS7j4PIzDHk1pkMH1et%2B9TrJaVIjAPjIJqrdRUub7fB1UfTfrmJUS8fMwqzPyCMW" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=oUNeaDhs0GLVS43IoJ1N%2FA%3D%3D.TM1Ih3hSj4TFYIyqA%2FJJWblklsgoYC2J4%2B%2F%2FIUdQGymi73iujHJQ9VS4pmN5JZ6aF2GAR673LVFW65Kk2%2BqeftpJUzoDszInpWJvUAibRwVMzuiwcVsid4LGkG3%2B0c%2BI44BFvnlE43AWCG9bqPVsAg%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 17 Iterative Reverse
https://segmentfault.com/a/1190000008476661
2017-02-24T21:03:05+08:00
2017-02-24T21:03:05+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>用循环的方式反转链表,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现方法 <code>reverse()</code> 用循环的方式反转链表,链表应该只遍历一次。注意这个函数直接修改了链表本身,所以不需要返回值。</p>
<pre><code class="js">var list = 2 -> 1 -> 3 -> 6 -> 5 -> null
reverse(list)
list === 5 -> 6 -> 3 -> 1 -> 2 -> null</code></pre>
<h2>解法</h2>
<p>代码如下:</p>
<pre><code class="js">function reverse(list) {
if (!list) return null
let result
for (let node = list; node; node = node.next) {
result = new Node(node.data, result)
}
list.data = result.data
list.next = result.next
}</code></pre>
<p>思路是,从前到后遍历链表,对每个节点复制一份,并让它的 <code>next</code> 指向前一个节点。最后 <code>result</code> 就是一个反转的新链表了。那么如何修改 <code>list</code> 呢?很简单,把 <code>result</code> 的首节点值赋给 <code>list</code> ,然后让 <code>list</code> 指向 <code>result</code> 的第二个节点就行。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=w1Aojbdq7KW4wC7K%2Bcr3qg%3D%3D.3Tq1Hq7tzsFfYiohsw%2FvbIFo5ScB7m6CZ%2Fm4VzchJPKOeiVTh8t6rmA6KH3sXqpaz%2BFM3L7qw70100Jz9fZG%2Fg%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=ccYTm0Q5S2%2Bj53fzPa42hA%3D%3D.RRcr3lz06jttihSUACNHz5eGW3d1NsKtu25IVdp6ujGbfPxJHiXuJ%2By96FTrNt1jzYliokLWAN%2FqSNPcLiJFvyetUJrv9DGR1gzlocRnfh4WDGu72G09GyywL%2BJPc0xg" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=in4e8CBZDRM31%2BeVQPCDLg%3D%3D.s%2F%2FGtN%2BqdNnY8JBLMV2bH%2Fo1L69NOIPqv9fGDwu%2FT%2FGusne%2FOI%2Fo8uHEpgAqvyYgA%2Fi4%2F1rcH0Dow4YOatGtm3BQcCnueaUQEhUkZRfZmfXl5bPKuIhSnE6ySzU58SWLZ%2FwVK%2Fr8zEeIjrypfAwFBA%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 16 Sorted Intersect
https://segmentfault.com/a/1190000008416965
2017-02-20T21:52:38+08:00
2017-02-20T21:52:38+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>一次遍历取两个排序链表的交集,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现函数 <code>sortedIntersect()</code> 取两个已排序的链表的交集,交集指两个链表都有的节点,节点不一定连续。每个链表应该只遍历一次。结果链表中不能包含重复的节点。</p>
<pre><code class="js">var first = 1 -> 2 -> 2 -> 3 -> 3 -> 6 -> null
var second = 1 -> 3 -> 4 -> 5 -> 6 -> null
sortedIntersect(first, second) === 1 -> 3 -> 6 -> null</code></pre>
<h2>分析</h2>
<p>最容易想到的解法可能是从链表 A 中取一个节点,然后遍历链表 B 找到相同的节点加入结果链表,最后取链表 A 的下一个节点重复该步骤。但这题有 <strong>每个链表只能遍历一次</strong> 的限制,那么如何做呢?</p>
<p>我们先假象有两个指针 <code>p1</code> 和 <code>p2</code>,分别指向两个链表的首节点。当我们对比 <code>p1</code> 和 <code>p2</code> 的值时,有这几种情况:</p>
<ol>
<li><p><code>p1.data === p2.data</code> ,这时节点肯定交集,加入结果链表中。因为两个节点都用过了,我们可以同时后移 <code>p1</code> 和 <code>p2</code> 比较下一对节点。</p></li>
<li><p><code>p1.data < p2.data</code> ,我们应该往后移动 <code>p1</code> ,不动 <code>p2</code> ,因为链表是升序排列的,<code>p1</code> 的后续节点有可能会跟 <code>p2</code> 一样大。</p></li>
<li><p><code>p1.data > p2.data</code> ,跟上面相反,移动 <code>p2</code> 。</p></li>
<li><p><code>p1</code> 或 <code>p2</code> 为空,后面肯定没有交集了,遍历结束。</p></li>
</ol>
<p>基本思路就是这样,递归和循环都是如此。</p>
<h2>递归解法</h2>
<p>代码如下:</p>
<pre><code class="js">function sortedIntersect(first, second) {
if (!first || !second) return null
if (first.data === second.data) {
return new Node(first.data, sortedIntersect(nextDifferent(first), nextDifferent(second)))
} else if (first.data < second.data) {
return sortedIntersect(first.next, second)
} else {
return sortedIntersect(first, second.next)
}
}
function nextDifferent(node) {
let nextNode = node.next
while (nextNode && nextNode.data === node.data) nextNode = nextNode.next
return nextNode
}</code></pre>
<p>需要注意的是不能加入重复节点的判断。我是在第 5 行两个链表的节点相等后,往后遍历到下一个值不同的节点,为此单独写了个 <code>nextDifferent</code> 函数。这个做法比较符合我的思路,但其实也可以写进循环体中,各位可以自行思考。</p>
<h2>循环解法</h2>
<p>代码如下,不赘述了:</p>
<pre><code class="js">function sortedIntersectV2(first, second) {
const result = new Node()
let [pr, p1, p2] = [result, first, second]
while (p1 || p2) {
if (!p1 || !p2) break
if (p1.data === p2.data) {
pr = pr.next = new Node(p1.data)
p1 = nextDifferent(p1)
p2 = nextDifferent(p2)
} else if (p1.data < p2.data) {
p1 = p1.next
} else {
p2 = p2.next
}
}
return result.next
}</code></pre>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=UqPcx%2FpCtXGREQdD8%2Fwekw%3D%3D.5EPZawdjDd1MHoai1zUjKdG2K9TDIjs1Y26Zujh%2Fve1ouM0rfMbYEKCnBmUL6wiAGnbY5bPilvVhnBlda4dH9A%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=WZ9r%2BbvpnSipQyd7XEDC4w%3D%3D.zEJeS5D81CBtygSiOLSjK7%2FaPKdFKl6EzutLjTOxKnclz1Vr4lhPXsGxGgW%2BanX%2F1ml0T7wz20nFUx2y4%2FtysKPpw7ZxocNno69P9lp21LVebi3x%2F5IS%2BITgoyOKr%2Fr4" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=ZXA4z2Z9htqPoTIGt%2FC3ww%3D%3D.LTADI0aiTwbAZBPQDHMOihW2Ymx7s%2FGnBDlo8urw8Y2ywn%2F495UjYQL8qBSkrZNVX97MNrCPvnJFoXKk8NCqO%2BzTeOErV1DUNVoVSFECV8p1xUyDwQk%2FUjqK9050hvqVpwiY5RER4do9go7MRoiIXQ%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 15 Merge Sort
https://segmentfault.com/a/1190000008398162
2017-02-18T22:56:52+08:00
2017-02-18T22:56:52+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>对链表进行归并排序,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现函数 <code>mergeSort()</code> 进行归并排序。注意这种排序法需要使用递归。在 <a href="https://segmentfault.com/a/1190000008243727">frontBackSplit()</a> 和 <a href="https://segmentfault.com/a/1190000008397427">sortedMerge()</a> 两个函数的帮助下,你可以很轻松的写一个递归的排序。基本算法是,把一个链表切分成两个更小的链表,递归地对它们进行排序,最终把两个排好序的小链表合成完整的链表。</p>
<pre><code class="js">var list = 4 -> 2 -> 1 -> 3 -> 8 -> 9 -> null
mergeSort(list) === 1 -> 2 -> 3 -> 4 -> 8 -> 9 -> null</code></pre>
<h2>解法</h2>
<p>归并排序的运行方式是,递归的把一个大链表切分成两个小链表。切分到最后就全是单节点链表了,而单节点链表可以被认为是已经排好序的。这时候再两两合并,最终会得到一个完整的已排序链表。</p>
<p>因为切分和合并两个最重要的功能都已经实现,需要思考的就只是如何递归整个过程了。我们分析一下可以把整个过程分成:</p>
<ol>
<li><p>用 <code>frontBackSplit()</code> 把链表切分成两个,分别叫 <code>first</code> 和 <code>second</code> 。</p></li>
<li><p>对 <code>first</code> 和 <code>second</code> 排序。</p></li>
<li><p>用 <code>sortedMerge()</code> 把排好序的两个链表合并起来。</p></li>
</ol>
<p>其中第 2 步就是递归的点,因为排序这个事情恰好是 <code>mergeSort</code> 本身可以做的。</p>
<p>代码如下:</p>
<pre><code class="js">const { Node } = require('./00-utils')
const { frontBackSplit } = require('./12-front-back-split')
const { sortedMerge } = require('./14-sorted-merge')
function mergeSort(list) {
if (!list || !list.next) return list
const first = new Node()
const second = new Node()
frontBackSplit(list, first, second)
return sortedMerge(mergeSort(first), mergeSort(second))
}</code></pre>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=XETUD2Xi%2BJkpkyT4qirAtA%3D%3D.76RT4kSEQRJY28N3KiX0oPVDA6WFks4obC80WD3k3LY%2Bmq%2FwIPewv3V%2Fr7Q8k7WMWGSVOeLSkGRmkOXkrLJMCQ%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=OGDnpVOP0Wlmw8h4vtU%2BAw%3D%3D.lRTEATT0Jk4ZrhDk9UIFQGvW2ziha5xaN%2FK%2BKINwgpKvlCcxX0J9EPOmD4v3AKeRClrxlNbZ%2FlCpGxdpEcekMYPyHMi4O2tDlixjlqRvFHPKqVpNtVx%2FqESns62x6%2BAO" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=218GZjm2HldKin7jCn98Zw%3D%3D.Pz7HpwYa1Q7Ee1qejfo4kOrhyLpmkKYJrOWm7q5oP6ZvYy%2BerEG7PP9vZUKf3pvYyn%2Bi8mHkDGAFa6lNwRAjsOXIb70YlymtposQIcZKd%2B%2FMWIVD9KcGq0hIpvaLJa0Z" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 14 Sorted Merge
https://segmentfault.com/a/1190000008397427
2017-02-18T21:02:26+08:00
2017-02-18T21:02:26+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>把两个升序排列的链表合并成一个,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现函数 <code>sortedMerge()</code> 把两个升序排列的链表合并成一个新链表,新链表也必须是升序排列的。这个函数应该对每个输入的链表都只遍历一次。</p>
<pre><code class="js">var first = 2 -> 4 -> 6 -> 7 -> null
var second = 1 -> 3 -> 5 -> 6 -> 8 -> null
sortedMerge(first, second) === 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 6 -> 7 -> 8 -> null</code></pre>
<p>有一些边界情况要考虑:<code>first</code> 或 <code>second</code> 可能为 <code>null</code> ,在合并过程中 <code>first</code> 或 <code>second</code> 的数据有可能先取完。如果一个链表为空,就返回另一个链表(即使它也为空),不需要抛出异常。</p>
<p>在做这个 kata 之前,建议先完成 <a href="https://segmentfault.com/a/1190000008396683">Shuffle Merge</a> 。</p>
<h2>递归解法</h2>
<p>代码如下:</p>
<pre><code class="js">function sortedMerge(first, second) {
if (!first || !second) return first || second
if (first.data <= second.data) {
return new Node(first.data, sortedMerge(first.next, second))
} else {
return new Node(second.data, sortedMerge(first, second.next))
}
}</code></pre>
<p>跟上个 kata 类似的思路。不过为了保证最后的结果是升序排列的,我们要取两个链表中值更小的首节点,添加到结果链表的末尾。思路就不赘述了 。</p>
<h2>循环解法</h2>
<p>循环是这个 kata 有意思的一点,很多边界情况的判断也发生在这里。很容易写出这样的 <code>if/else</code> :</p>
<pre><code class="js">let [p1, p2] = [first, second]
while (p1 || p2) {
if (p1 && p2) {
if (p1.data <= p2.data) {
// append p1 data to result
} else {
// append p2 data to result
}
} else if (p1) {
// append p1 to result
} else {
// append p2 to result
}
}</code></pre>
<p>上面例子里 <code>p1</code> 和 <code>p2</code> 是指向两个链表节点的指针,在循环中它们随时可能变成空,因此要比较数据大小首先就要判断两个都不为空。而且注释中的 append 代码也会有一定重复。</p>
<p>为了解决这个问题,我们可以上个 kata 里调换指针的方法。完整代码如下:</p>
<pre><code class="js">function sortedMergeV2(first, second) {
const result = new Node()
let [pr, p1, p2] = [result, first, second]
while (p1 || p2) {
// if either list is null, append the other one to the result list
if (!p1 || !p2) {
pr.next = (p1 || p2)
break
}
if (p1.data <= p2.data) {
pr = pr.next = new Node(p1.data)
p1 = p1.next
} else {
// switch 2 lists to make sure it's always p1 <= p2
[p1, p2] = [p2, p1]
}
}
return result.next
}</code></pre>
<p>第 7 行判断 <code>p1</code> 或 <code>p2</code> 为空,并且把非空的链表直接添加到 <code>result</code> 末尾,省去了继续循环每个节点。第 17 行的指针调换让 <code>p1</code> 始终小于等于 <code>p2</code> ,从而避免了重复的 append 代码 。其他技巧如 dummy node 在之前的 kata 都有讲,就不多说了。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=d043CknpcvFqzLP52cZjnw%3D%3D.1KnaSYOc7smfiJAkNSuFB%2FBLrqUdQAbSA641nHQN81zRecRC38bf1TvBjeR4ciB2NIZzT2Pnupz7PuFFCX5n%2Fw%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=dBoEMSb%2Bh0TwlskMUz1tUA%3D%3D.Njy1gPr%2FyBkCld6ksjlCrEIct6Cw6ov4JzmIYlc24cXIH%2BaZiMBE1r9CcadnZcMz4Wv6BBUUwmfogt8NA5wxDYtB6H3Y1r1YRtmV6QLG5un%2FqK6Blt%2FvZ62C0GTVI9Qp" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=jSfnD%2BBWzmqjQ%2F3X7OfmKA%3D%3D.Dk%2BxqqKA9w6mcXZxGO2a68De1hOJK0WhmRJpwguDRf7XLMaKYIN%2FpSYfQpqtoYh6ivZgGYysgeoypQe4erNdRcvwnQUEm16VM7QkOwtdXGix9vA4ZFUKdyA6ya1kPI%2Bn" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 13 Shuffle Merge
https://segmentfault.com/a/1190000008396683
2017-02-18T18:44:41+08:00
2017-02-18T18:44:41+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>把两个链表洗牌合并成一个,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现函数 <code>shuffleMerge()</code> 把两个链表合并成一个。新链表的节点是交叉从两个链表中取的。这叫洗牌合并。举个例子,当传入的链表为 <code>1 -> 2 -> 3 -> null</code> 和 <code>7 -> 13 -> 1 -> null</code> 时,合并后的链表为 <code>1 -> 7 -> 2 -> 13 -> 3 -> 1 -> null</code> 。如果合并过程中一个链表的数据先取完了,就从另一个链表中取剩下的数据。这个函数应该返回一个新链表。</p>
<pre><code class="js">var first = 3 -> 2 -> 8 -> null
var second = 5 -> 6 -> 1 -> 9 -> 11 -> null
shuffleMerge(first, second) === 3 -> 5 -> 2 -> 6 -> 8 -> 1 -> 9 -> 11 -> null</code></pre>
<p>如果参数之一为空,应该直接返回另一个链表(即使另一个链表也为空),不需要抛异常。</p>
<h2>递归解法 1</h2>
<p>代码如下:</p>
<pre><code class="js">function shuffleMerge(first, second) {
if (!first || !second) return first || second
const list = new Node(first.data, new Node(second.data))
list.next.next = shuffleMerge(first.next, second.next)
return list
}</code></pre>
<p>解题思路是,首先判断是否有一个链表为空,有就返回另一个,结束递归。这个判断过了,下面肯定是两个链表都不为空的情况。我们依次从两个链表中取第一个节点组合成新链表,然后递归 <code>shuffleMerge</code> 两个链表的后续节点,并把结果衔接到 <code>list</code> 后面。这段代码基本跟题目描述的意思一致。</p>
<h2>递归解法 2</h2>
<p>在上面的基础上我们还能做个更聪明的版本,代码如下:</p>
<pre><code class="js">function shuffleMergeV2(first, second) {
if (!first || !second) return first || second
return new Node(first.data, shuffleMerge(second, first.next))
}</code></pre>
<p>通过简单的调换 <code>first</code> 和 <code>second</code> 的顺序,我们能把递归过程从 “先取 first 的首节点再取 second 的首节点” 变成 “总总是取 first 的首节点” 。解法 1 中的三行代码简化成了一行。</p>
<h2>循环解法</h2>
<p>循环其实才是本题的考点,因为这题主要是考指针(引用)操作。尤其是把 “依次移动两个链表的指针” 写进一个循环里。不过上个解法中调换两个链表顺序的方式也可以用到这里。代码如下:</p>
<pre><code class="js">function shuffleMergeV3(first, second) {
const result = new Node()
let pr = result
let [p1, p2] = [first, second]
while (p1 || p2) {
if (p1) {
pr.next = new Node(p1.data)
pr = pr.next
p1 = p1.next
}
[p1, p2] = [p2, p1]
}
return result.next
}</code></pre>
<p>首先我们生成一个 dummy node <code>result</code> ,同时建立一个 <code>pr</code> 代表 <code>result</code> 的尾节点(方便插入)。两个链表的指针分别叫 <code>p1</code> 和 <code>p2</code> 。在每次循环中我们都把 <code>p1</code> 的节点数据写到 <code>result</code> 链表的末尾,然后修改指针指向下一个节点。通过 12 行的调换指针,我们可以保证下一次循环就是对另一个链表进行操作了。这样一直遍历到两个链表末尾,返回 <code>result.next</code> 结束。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=BuMvB2XyrdBXRSr3qalFhQ%3D%3D.BVzEZq1EuKNo%2FGXDLDD1ZW4bsyQu2DhVBm8pntO3cF1opzO2FGD0jg%2FCvHdDRx9ZrdCQQoJ4M5DsJHu7HClDaA%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=4DUttL12qqGvvxUqxZ55Nw%3D%3D.myD7BmMZanQEHgAC9oXSQIXpq2aB%2BzuuqJRtC3hkuy0byBitcga2vnt9rzVH7vGXCh3Mk3jqDr6rmyf5pipcIHd%2FgAGkFAkz74pcaoABK%2FBkoSJ1OBhcbvTpsislciI4" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=FYcaUzbytJwzPUaZdgjiFQ%3D%3D.suVKhqql%2BuxBh1Q6lxJzShope9BexHloJZcZY%2B99GNT3K5ftv31SnzOjwsUqiTR%2FBwDKbBAbAwav76bwxEfcadvhV9ZE2TydPxA7nLNUd%2B1iPtbAAzObzbHLHWtwKY5n" rel="nofollow">GitHub 的测试</a></p>
用 PostgreSQL 的 COPY 导入导出 CSV
https://segmentfault.com/a/1190000008328676
2017-02-12T23:09:42+08:00
2017-02-12T23:09:42+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
3
<h2>TL;DR</h2>
<p>无意中看到了一篇讲 <a href="https://link.segmentfault.com/?enc=zdz8ALE7RoMmWrUIauDCEA%3D%3D.B0F2wvswUL5Bp7cPbdL%2Fle3um%2BgLwvE1%2FU9xg8Z7FJr%2F6mlCW3RNx1nQB%2BX6HN29Cx12sfuJb7eBWq9QjM8v9bCHlgFghVVeDOoxKXoi1%2FI%3D" rel="nofollow">数据批量导入</a> 的文章,才注意到 PostgreSQL 的 <code>COPY</code> 命令。简而言之,它用来在文件和数据库之间复制数据,效率非常高,并且支持 CSV 。</p>
<h2>导出 CSV</h2>
<p>以前做类似的事情都是用程序语言写,比如用程序读取数据库的数据,然后用 CSV 模块写入文件,当数据量大的时候还要控制不要一次读太多,比如一次读 5000 条,处理完再读 5000 条之类。</p>
<p>PostgreSQL 的 <code>COPY TO</code> 直接可以干这个事情,而且导出速度是非常快的。下面例子是把 <code>products</code> 表导出成 CSV :</p>
<pre><code class="sql">COPY products
TO '/path/to/output.csv'
WITH csv;</code></pre>
<p>可以导出指定的属性:</p>
<pre><code class="sql">COPY products (name, price)
TO '/path/to/output.csv'
WITH csv;</code></pre>
<p>也可以配合查询语句,比如最常见的 <code>SELECT</code> :</p>
<pre><code class="sql">COPY (
SELECT name, category_name
FROM products
LEFT JOIN categories ON categories.id = products.category_id
)
TO '/path/to/output.csv'
WITH csv;</code></pre>
<h2>导入 CSV</h2>
<p>跟上面的导出差不多,只是把 <code>TO</code> 换成 <code>FROM</code> ,举例:</p>
<pre><code class="sql">COPY products
FROM '/path/to/input.csv'
WITH csv;</code></pre>
<p>这个命令做导入是非常高效的,在开头那篇博客作者的测试中,<code>COPY</code> 只花了 <code>INSERT</code> 方案 1/3 的时间,而后者还用 prepare statement 优化过。</p>
<h2>总结</h2>
<p><code>COPY</code> 还有一些其他配置,比如把输入输出源指定成 STDIN/STDOUT 和 shell 命令,或者指定 CSV 的 header 等等。这里不再赘述。数据库也有很多细节可挖,有些简单却非常实用。合理使用能大大提高效率。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=I4hgm0izyF0sJ3VeiGHpYw%3D%3D.gso1TWiaRBXlFm960ok7EkRgX0zKp7P0oqq3%2Fq3iP8b%2BeofBr1Xovdr7qYygu0Ns0kCy3MyVwfV%2F%2B2RP8HkTryavOiM%2B9uTNHZi6uhWIiI0%3D" rel="nofollow">Friends Don’t Let Friends Use Loops</a><br><a href="https://link.segmentfault.com/?enc=awm%2B6jUN3J1VTdwAm3ujtA%3D%3D.%2BSclLn3ZkLDRbcJhonC3YrVlD0F30vcSG1x7NqKK3a8gEg2ljtbakemVv2fMVtNqcHi6PE12oKM5gqDNaBniDw%3D%3D" rel="nofollow">PostgreSQL: COPY</a></p>
用 JavaScript 实现链表操作 - 12 Front Back Split
https://segmentfault.com/a/1190000008243727
2017-02-03T17:42:46+08:00
2017-02-03T17:42:46+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>把一个链表居中切分成两个,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现函数 <code>frontBackSplit()</code> 把链表居中切分成两个子链表 -- 一个前半部分,另一个后半部分。如果节点数为奇数,则多余的节点应该归类到前半部分中。例子如下,注意 <code>front</code> 和 <code>back</code> 是作为空链表被函数修改的,所以这个函数不需要返回值。</p>
<pre><code class="js">var source = 1 -> 3 -> 7 -> 8 -> 11 -> 12 -> 14 -> null
var front = new Node()
var back = new Node()
frontBackSplit(source, front, back)
front === 1 -> 3 -> 7 -> 8 -> null
back === 11 -> 12 -> 14 -> null</code></pre>
<p>如果函数的任何一个参数为 <code>null</code> 或者原链表长度小于 2 ,应该抛出异常。</p>
<p>提示:一个简单的做法是计算链表的长度,然后除以 2 得出前半部分的长度,最后分割链表。另一个方法是利用双指针。一个 “慢” 指针每次遍历一个节点,同时一个 ”快“ 指针每次遍历两个节点。当快指针遍历到末尾时,慢指针正好遍历到链表的中段。</p>
<p>这个 kata 主要考验的是指针操作,所以解法用不上递归。</p>
<h2>解法 1 -- 根据长度分割</h2>
<p>代码如下:</p>
<pre><code class="js">function frontBackSplit(source, front, back) {
if (!front || !back || !source || !source.next) throw new Error('invalid arguments')
const array = []
for (let node = source; node; node = node.next) array.push(node.data)
const splitIdx = Math.round(array.length / 2)
const frontData = array.slice(0, splitIdx)
const backData = array.slice(splitIdx)
appendData(front, frontData)
appendData(back, backData)
}
function appendData(list, array) {
let node = list
for (const data of array) {
if (node.data !== null) {
node.next = new Node(data)
node = node.next
} else {
node.data = data
}
}
}</code></pre>
<p>解法思路是把链表变成数组,这样方便计算长度,也方便用 <code>slice</code> 方法分割数组。最后用 <code>appendData</code> 把数组转回链表。因为涉及到多次遍历,这并不是一个高效的方案,而且还需要一个数组处理临时数据。</p>
<h2>解法 2 -- 根据长度分割改进版</h2>
<p>代码如下:</p>
<pre><code class="js">function frontBackSplitV2(source, front, back) {
if (!front || !back || !source || !source.next) throw new Error('invalid arguments')
let len = 0
for (let node = source; node; node = node.next) len++
const backIdx = Math.round(len / 2)
for (let node = source, idx = 0; node; node = node.next, idx++) {
append(idx < backIdx ? front : back, node.data)
}
}
// Note that it uses the "tail" property to track the tail of the list.
function append(list, data) {
if (list.data === null) {
list.data = data
list.tail = list
} else {
list.tail.next = new Node(data)
list.tail = list.tail.next
}
}</code></pre>
<p>这个解法通过遍历链表来获取总长度并算出中间节点的索引,算出长度后再遍历一次链表,然后用 <code>append</code> 方法选择性地把节点数据加入 <code>front</code> 或 <code>back</code> 两个链表中去。这个解法不依赖中间数据(数组)。</p>
<p><code>append</code> 方法有个值得注意的地方。一般情况下把数据插入链表的末尾的空间复杂度是 O(n) ,为了避免这种情况 <code>append</code> 方法为链表加了一个 <code>tail</code> 属性并让它指向尾节点,让空间复杂度变成 O(1) 。</p>
<h2>解法 3 -- 双指针</h2>
<p>代码如下:</p>
<pre><code class="js">function frontBackSplitV3(source, front, back) {
if (!front || !back || !source || !source.next) throw new Error('invalid arguments')
let slow = source
let fast = source
while (fast) {
// use append to copy nodes to "front" list because we don't want to mutate the source list.
append(front, slow.data)
slow = slow.next
fast = fast.next && fast.next.next
}
// "back" list just need to copy one node and point to the rest.
back.data = slow.data
back.next = slow.next
}</code></pre>
<p>思路在开篇已经有解释,当快指针遍历到链表末尾,慢指针正好走到链表中部。但如何修改 <code>front</code> 和 <code>back</code> 两个链表还是有点技巧的。</p>
<p>对于 <code>front</code> 链表,慢指针每次遍历的数据就是它需要的,所以每次遍历时把慢指针的数据 <code>append</code> 到 <code>front</code> 链表中就行(第 9 行)。</p>
<p>对于 <code>back</code> 链表,它所需的数据就是慢指针停下的位置到末尾。我们不用复制整个链表数据到 <code>back</code> ,只用复制第一个节点的 <code>data</code> 和 <code>next</code> 即可。这种 <strong>复制头结点,共用剩余节点</strong> 的技巧经常出现在一些 Immutable Data 的操作中,以省去不必要的复制。这个技巧其实也可以用到上一个解法里。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=akz38BosSjEKy4nNVgulMA%3D%3D.5qakcWUOZS8XEvK1CI1a3y2Tn%2BjJqXyzcus69akwtxnjGB%2FDtjIik22tRrbeq44irTbXIIWlSoDhtdQo%2FdMRb%2FrLcsWDLItjwbk8SswBHqc%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=WPhboDmIjJlmDawU%2BuXOpw%3D%3D.vIIz5uMQ7RHq1cYDzrVRgzHyAdcS0ZOrYgp070VGQI5LFdlrC8RvSvt%2F1p9Yi04aDQv4Ufw9I9v1Nj006pqIZSCNVUl21Yzp13OxOb%2FdNJES4d1BlwJtRExOes6TUBBz" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=5wbRV%2Fz3ljoVD30MUx9Vmw%3D%3D.69Ux3%2B6tn0K5uxqeoJPBi2%2BBSEt92klP5skNaBXH5tYuSsuNKjWBtEhRkD8Q29w3kHJQyrAGhv3BB%2Fh3L7G6xTA%2BxwT5CrmROd4aaXlQXCIOA6P%2F4JqsZ%2B8qAS2P7r5leql8zPemg3xhpC3eWpPLmw%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 11 Alternating Split
https://segmentfault.com/a/1190000008239747
2017-02-02T23:01:21+08:00
2017-02-02T23:01:21+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>TL;DR</h2>
<p>把一个链表交替切分成两个,系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>alternatingSplit()</code> 函数,把一个链表切分成两个。子链表的节点应该是在父链表中交替出现的。如果原链表是 <code>a -> b -> a -> b -> a -> null</code> ,则两个子链表分别为 <code>a -> a -> a -> null</code> 和 <code>b -> b -> null</code> 。</p>
<pre><code class="js">var list = 1 -> 2 -> 3 -> 4 -> 5 -> null
alternatingSplit(list).first === 1 -> 3 -> 5 -> null
alternatingSplit(list).second === 2 -> 4 -> null</code></pre>
<p>为了简化结果,函数会返回一个 Context 对象来保存两个子链表,Context 结构如下所示:</p>
<pre><code class="js">function Context(first, second) {
this.first = first
this.second = second
}</code></pre>
<p>如果原链表为 <code>null</code> 或者只有一个节点,应该抛出异常。</p>
<h2>递归版本</h2>
<p>代码如下:</p>
<pre><code class="js">function alternatingSplit(head) {
if (!head || !head.next) throw new Error('invalid arguments')
return new Context(split(head), split(head.next))
}
function split(head) {
const list = new Node(head.data)
if (head.next && head.next.next) list.next = split(head.next.next)
return list
}</code></pre>
<p>这个解法的核心思路在于 <code>split</code> ,这个方法接收一个链表并返回一个以奇数位的节点组成的子链表。所以整个算法的解法就能很容易地用 <code>new Context(split(head), split(head.next))</code> 表示。</p>
<h2>另一个递归版本</h2>
<p>代码如下:</p>
<pre><code class="js">function alternatingSplitV2(head) {
if (!head || !head.next) throw new Error('invalid arguments')
return new Context(...splitV2(head))
}
function splitV2(head) {
if (!head) return [null, null]
const first = new Node(head.data)
const [second, firstNext] = splitV2(head.next)
first.next = firstNext
return [first, second]
}</code></pre>
<p>这里的 <code>splitV2</code> 的作用跟整个算法的含义一样 -- 接收一个链表并返回交叉分割的两个子链表(以数组表示)。第一个子链表的头自然是 <code>new Node(head.data)</code> ,第二个子链表呢?它其实是 <code>splitV2(head.next)</code> 的第一个子链表(见第 4 行)。理解这个逻辑后就能明白递归过程。</p>
<h2>循环版本</h2>
<p>代码如下:</p>
<pre><code class="js">function alternatingSplitV3(head) {
if (!head || !head.next) throw new Error('invalid arguments')
const first = new Node()
const second = new Node()
const tails = [first, second]
for (let node = head, idx = 0; node; node = node.next, idx = idx ? 0 : 1) {
tails[idx].next = new Node(node.data)
tails[idx] = tails[idx].next
}
return new Context(first.next, second.next)
}</code></pre>
<p>这个思路是,先用两个变量代表子链表,然后对整个链表进行一次遍历,分别把节点交替插入每个子链表中。唯一需要考虑的就是在每个循环体中判断节点该插入哪个链表。我用的是 <code>idx</code> 变量,在每轮循环中把它交替设置成 <code>0</code> 和 <code>1</code> 。也有人使用持续增长的 <code>idx</code> 配合取余来做,比如 <code>idx % 2</code> 。做法有很多种,就不赘述了。</p>
<p>这里也用了 dummy node 的技巧来简化 “判断首节点是否为空” 的情况。关于这个技巧可以看看 <a href="https://segmentfault.com/a/1190000007800288">Insert Nth Node</a></p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=5fbcmPm007NqiSr3O18BpA%3D%3D.qFTSrAZ5iParARh10abX%2BHtvfb1BAYno1aH6t%2BMDObcaFRra8r9UxPV7t10%2BVwKsl109Sbo3lBQLyKh%2FNs5R6Nm8Vk6dvToI3Bu9KFD16eQ%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=TpolhwjR0gbhnVwOCVHO9g%3D%3D.auq%2BOesS2HwooWIlQouvovyHLA7POOqxoGBdag6sim26frFvH%2B%2B4FLbOuqzNv%2Ffz3%2BWE%2BraIS7t7lv7XgEo5z3NpFju0e4eC02CIu65fQTv9oKhx9mFZXnoaGJGmtaDm" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=5HA%2BMfIsG95ilp0AfwTF3A%3D%3D.ZKhL%2FvunT%2FOCEM0Ui4j7x8%2FIa4dWY89YW4hul4Auqbt1JwO%2BT6eFecK4vIP5mo9bl2Io6xBYFdwYAK%2F1%2BGL7w7BHau67mhf4yLKkhryqcwO6ZLYwkJd0WOrFdEqDiVucMO4buRTiTzBdZU8dW1RXNw%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 10 Move Node In-place
https://segmentfault.com/a/1190000008085135
2017-01-11T17:22:18+08:00
2017-01-11T17:22:18+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>用 in-place 的方式把一个链表的首节点移到另一个链表(不改变链表的引用),系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>moveNode()</code> 函数,把源链表的头结点移到目标链表的开头。要求是不能修改两个链表的引用。</p>
<pre><code class="js">var source = 1 -> 2 -> 3 -> null
var dest = 4 -> 5 -> 6 -> null
moveNode(source, dest)
source === 2 -> 3 -> null
dest === 1 -> 4 -> 5 -> 6 -> null</code></pre>
<p>当碰到以下的情况应该抛出异常:</p>
<ul>
<li><p>源链表为 <code>null</code></p></li>
<li><p>目标链表为 <code>null</code></p></li>
<li><p>源链表是空节点,<code>data</code> 属性为 <code>null</code> 的节点定义为空节点。</p></li>
</ul>
<p>跟 <a href="https://segmentfault.com/a/1190000008051315">前一个 kata</a> 不同的是,这个 kata 是在不改变引用的情况下修改两个链表自身。因此 <code>moveNode()</code> 函数不需要返回值。同时这个 kata 也提出了 <strong>空节点</strong> 的概念。空节点会用于目标链表为空的情况(为了保持引用),在函数执行之后,目标链表会由空节点变成一个包含一个节点的链表。</p>
<p>你可以使用 <a href="https://segmentfault.com/a/1190000007625419">第一个 kata</a> 的 <code>push</code> 方法。</p>
<h2>最优的方案</h2>
<p>这个算法考的是对链表节点的插入和删除。基本只对 source 和 dest 分别做一次操作,所以不用区分递归和循环。大致思路为:</p>
<ol>
<li><p>对 <code>source</code> 做删除一个节点的操作。如果只有一个节点就直接置空。如果有多个节点,就把第二个节点的值赋给头节点,然后让头结点指向第三个节点。</p></li>
<li><p>对 <code>dest</code> 做插入一个节点的操作。如果头结点为空就直接赋值,否则把头结点复制一份,作为第二个节点插入到链表中,再把新值赋给头结点。</p></li>
</ol>
<p>代码如下:</p>
<pre><code class="js">function moveNode(source, dest) {
if (!source || !dest || source.data === null) throw new Error("invalid arguments")
const data = source.data
if (source.next) {
source.data = source.next.data
source.next = source.next.next
} else {
source.data = null
}
if (dest.data === null) {
dest.data = data
} else {
dest.next = new Node(dest.data, dest.next)
dest.data = data
}
}</code></pre>
<h2>递归方案</h2>
<p>这是我最开始思考的方案,差别在于对 <code>dest</code> 如何插入新节点的处理上用了递归。思路是把所有节点的 <code>data</code> 往后移一位,即把新值赋给第一个节点,第一个节点的值赋给第二个节点,第二个节点的值赋给第三个节点,以此类推。但实际操作中的顺序必须是反的,就是把倒数第二个节点的值赋给最后一个节点,倒数第三个节点的值赋给倒数第二个节点…… 这个思路对 <code>dest</code> 操作了 N 次,不如上一个解法的 1 次操作高效。不过也算是个有意思的递归用例,所以我仍然把它放了上来。</p>
<p>代码如下,主要看 <code>pushInPlaceV2</code> :</p>
<pre><code class="js">function moveNodeV2(source, dest) {
if (source === null || dest === null || source.isEmpty()) {
throw new Error('invalid arguments')
}
pushInPlaceV2(dest, source.data)
if (source.next) {
source.data = source.next.data
source.next = source.next.next
} else {
source.data = null
}
}
function pushInPlaceV2(head, data) {
if (!head) return new Node(data)
if (!head.isEmpty()) head.next = pushInPlaceV2(head.next, head.data)
head.data = data
return head
}</code></pre>
<h2>总结</h2>
<p>总是使用递归会产生惯性,导致忽略了数据结构的基本特性。链表的特性就是插入和删除的便利,改改引用就成了。</p>
<p>算法相关的代码和测试我都放在 <a href="https://link.segmentfault.com/?enc=Tx1SkFsouf0r%2FaKAwDaBiw%3D%3D.G%2BIgVI1JFRbvJH2xtFNT6LtrK6yypj0taTeSqfZdHEE1NGtxlQ5QXUXOezvcEzPCr8Jam%2B%2Bankf7sUBYynUK5g%3D%3D" rel="nofollow">GitHub</a> 上,如果对你有帮助请帮我点个赞!</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=nHe%2BmfN8ZZ6%2BMJ4xYsBwEA%3D%3D.UyPedylSfDmLVVUK8160rnPhjEh1WO5UlJz6Xv%2B4XfMcpyUKAwJLzxFkBNLZkFXS4NqnOnkND1uroBbSWhAGTdCy41wRgLHamJmnc38%2Fw7c%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=HywwZcr43GGdAPJb9NoJgw%3D%3D.7TGZ513p8NdsUq7YpDS95vsg86BNL2FaEMxi30eF4Kuyt87CPeAcSTLY%2FoGmWqm0dwbsKLT3duu02UY6RT1MKT4ohKMO8%2FmlJrFp1C01%2FYCH5YDQijVkrnmQnOkKYHxx" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=WpC1KVTzwEehBRy9kftaqQ%3D%3D.XLjW25U7QEqaXczciZfZDfW9i6ogDoGMyIeLdIo0Pu1DHE0c6z8YjqWqbdjI98RaPq0%2B3CXklDAuvJk1bvCBjUM54QHhl%2BvfmWcFEomnSRr5d3q0uekTeP%2F7vlN9qx6RgI0SMkECCsBplEwiN5W2AQ%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 09 Move Node
https://segmentfault.com/a/1190000008051315
2017-01-08T22:03:22+08:00
2017-01-08T22:03:22+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>把一个链表的首节点移到另一个链表。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>moveNode()</code> 函数,把源链表的头节点移到目标链表。当源链表为空时函数应抛出异常。为了简化起见,我们会用一个 <code>Context</code> 对象来存储改变后的源链表和目标链表的引用。它也是函数的返回值。</p>
<pre><code class="js">var source = 1 -> 2 -> 3 -> null
var dest = 4 -> 5 -> 6 -> null
moveNode(source, dest).source === 2 -> 3 -> null
moveNode(source, dest).dest === 1 -> 4 -> 5 -> 6 -> null</code></pre>
<p>这个 kata 是下一个 kata 的简化版,你可以重用 <a href="https://segmentfault.com/a/1190000007625419">第一个 kata</a> 的 <code>push</code> 方法。</p>
<h2>关于 Context</h2>
<p><code>Context</code> 的定义长这个样子,<code>source</code> 代表源链表,<code>dest</code> 代表目标链表。</p>
<pre><code class="js">function Context(source, dest) {
this.source = source
this.dest = dest
}</code></pre>
<h2>解法</h2>
<p>配合 <code>push</code> ,这个 kata 非常简单,注意这个函数没有改变两个链表本身。代码如下:</p>
<pre><code class="js">function moveNode(source, dest) {
if (!source) throw new Error('source is empty')
return new Context(source.next, push(dest, source.data))
}</code></pre>
<h2>总结</h2>
<p>这个 kata 本身很简单,就没有分递归和循环的版本了,其存在意义主要是为了下一个 kata 做铺垫。</p>
<p>算法相关的代码和测试我都放在 <a href="https://link.segmentfault.com/?enc=JbuNUQziCEt4pboZrP89rQ%3D%3D.qtkFjBvvY3%2B2d1U8fIufGhRhV4730hh07W7BtWbmEVc1Nw3UEmOMZz5L0VgjUHDWhgGVZ8HoLBvSPqXMDeIdwA%3D%3D" rel="nofollow">GitHub</a> 上,如果对你有帮助请帮我点个赞!</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=qnRi6LGV89IJi52t%2FQNDGw%3D%3D.5cUw4BTDsyUMc6MvAMUS%2Fmbk%2Bzjjzo8%2BcC4iAEUHAVtSzmOfFxbf2oqfcltftiHTkXriA6IUWxm9R5ocV3rC7g%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=5RA%2Bcs%2BaW1d4bQUL%2BHxzpg%3D%3D.uYnIdcIdBLYDYJD8VxdcudS29fnBpHjYOSAyJ8JoQNelwNFi%2BaWO5HND3ex70UQQVqGphxbMgeC4xdhsXA5iz3CSEHX%2BZnDrtwM0Q2JiiVW5UKAEvwW%2FPg%2BMCr4KltYw" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=rz%2Bkv2rjK7nxMk%2Fd%2FpE8IQ%3D%3D.dwZG%2BfnXZyK0sJAZppSczoZfV1%2Fa5rX1uqfMptq1UvYKkkTrO8YVR8CQGFZewqvybLLflaTY1N7NVrlRijZ2%2FetmZOaSwODSMP235T%2FX8OLEFD810m38BA1ea%2FKicLJl" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 08 Remove Duplicates
https://segmentfault.com/a/1190000008049580
2017-01-08T18:03:04+08:00
2017-01-08T18:03:04+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
2
<h2>TL;DR</h2>
<p>为一个已排序的链表去重,考虑到很长的链表,需要尾调用优化。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>removeDuplicates()</code> 函数,给定一个升序排列过的链表,去除链表中重复的元素,并返回修改后的链表。理想情况下链表只应该被遍历一次。</p>
<pre><code class="js">var list = 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 -> null
removeDuplicates(list) === 1 -> 2 -> 3 -> 4 -> 5 -> null</code></pre>
<p>如果传入的链表为 <code>null</code> 就返回 <code>null</code> 。</p>
<p>这个解决方案需要考虑链表很长的情况,递归会造成栈溢出,所以递归方案必须用到尾递归。</p>
<p>因为篇幅限制,这里并不解释什么是尾递归,想详细了解的可以先看看 <a href="https://link.segmentfault.com/?enc=zrbzw20In%2BGXS1msbsCQzw%3D%3D.ClxeSteWLsbmwkcjRyXTXnLGPuIUQK2LNgxXpYIAEhsoIIRGdEzK%2BVM0nJwn5AJOM%2B66SyyyAeq9sqWkclIk3A%3D%3D" rel="nofollow">尾调用</a> 的定义。</p>
<h2>递归版本 - 非尾递归</h2>
<p>对数组或者链表去重本身是个花样很多的算法,但如果链表是已排序的,解法就单一很多了,因为重复的元素都是相邻的。假定链表为 <code>a -> a1 -> a2 ... aN -> b</code> ,其中 <code>a1</code> 到 <code>aN</code> 都是对 <code>a</code> 的重复,那么去重就是把链表变成 <code>a -> b</code> 。</p>
<p>因为递归版本没有循环,所以一次递归操作只能减去一个重复元素,比如第一次去除 <code>a1</code> ,第二次去除 <code>a2</code> 。</p>
<p>先看一个简单的递归版本,这个版本递归的是 <code>removeDuplicates</code> 自身。先取链表的头结点 <code>head</code>,如果发现它跟之后的节点有重复,就让 <code>head</code> 指向之后的节点(减去一个重复),然后再把 <code>head</code> 放入下一个递归里。如果没有重复,则递归 <code>head</code> 的下一个节点,并把结果指向 <code>head.next</code> 。</p>
<pre><code class="js">function removeDuplicates(head) {
if (!head) return null
const nextNode = head.next
if (nextNode && head.data === nextNode.data) {
head.next = nextNode.next
return removeDuplicates(head)
}
head.next = removeDuplicates(nextNode)
return head
}</code></pre>
<p>这个版本只有第一个 <code>return removeDuplicates(head)</code> 处是尾递归,最后的 <code>return head</code> 并不是。所以这个解法并不算完全的尾递归,但性能并不算差。经我测试可以处理 30000 个节点的链表,但 40000 个就一定会栈溢出。</p>
<h2>递归版本 - 尾递归</h2>
<p>很多递归没办法自然的写成尾递归,本质原因是无法在多次递归过程中维护共有的变量,这也是循环的优势所在。上面例子中的 <code>head.next = removeDuplicates(nextNode)</code> 就是一个典型,我们需要保留 <code>head</code> 这个变量,好在递归结束把结果赋值给 <code>head.next</code> 。尾递归优化的基本思路,就是把共有的变量继续传给下一个递归过程,这种做法往往需要用到额外的函数参数。下面是一个改变后的尾递归版本:</p>
<pre><code class="js">function removeDuplicatesV2(head, prev = null, re = null) {
if (!head) return re
re = re || head
if (prev && prev.data === head.data) {
prev.next = head.next
} else {
prev = head
}
return removeDuplicatesV2(head.next, prev, re)
}</code></pre>
<p>我们加了两个变量 <code>prev</code> 和 <code>re</code> 。<code>prev</code> 代表 <code>head</code> 的前一个节点,在递归过程中我们判断的是 <code>prev</code> 和 <code>head</code> 是否有重复。为了最后能返回链表的头我们加了 <code>re</code> 这个参数,它是最后的返回值。<code>re</code> 仅仅指向最开始的 <code>head</code> ,也就是第一次递归的链表的头结点。因为这个算法是修改链表自身,只要链表非空,头结点作为返回值就是确定的,即使链表开头就有重复,被移除的也是头结点之后的节点。</p>
<h2>如何测试尾递归</h2>
<p>首先我们需要一个支持尾递归优化的环境。我测试的环境是 Node v7 。Node 应该是 6.2 之后就支持尾递归优化,但需要指定 <code>harmony_tailcalls</code> 参数开启,默认并不启动。我用的 Mocha 写测试,所以把参数写在 <code>mocha.opts</code> 里,配置如下:</p>
<pre><code>--use_strict
--harmony_tailcalls
--require test/support/expect.js</code></pre>
<p>其次我们需要一个方法来生成很长的,随机重复的,生序排列的链表,我的写法如下:</p>
<pre><code class="js">// Usage: buildRandomSortedList(40000)
function buildRandomSortedList(len) {
let list
let prevNode
let num = 1
for (let i = 0; i < len; i++) {
const node = new Node(randomBool() ? num++ : num)
if (!list) {
list = node
} else {
prevNode.next = node
}
prevNode = node
}
return list
}
function randomBool() {
return Math.random() >= 0.5
}</code></pre>
<p>然后就可以测试了,为了方便同时测试溢出和不溢出的情况,写个 helper ,这个 helper 简单的判断函数是否抛出 <code>RangeError</code> 。因为函数的逻辑已经在之前的测试中保证了,这里就不测试结果是否正确了。</p>
<pre><code class="js">function createLargeListTests(fn, { isOverflow }) {
describe(`${fn.name} - max stack size exceed test`, () => {
it(`${isOverflow ? 'should NOT' : 'should'} be able to handle a big random list.`, () => {
Error.stackTraceLimit = 10
expect(() => {
fn(buildRandomSortedList(40000))
})[isOverflow ? 'toThrow' : 'toNotThrow'](RangeError, 'Maximum call stack size exceeded')
})
})
}
createLargeListTests(removeDuplicates, { isOverflow: true })
createLargeListTests(removeDuplicatesV2, { isOverflow: false })</code></pre>
<p>完整的测试见 <a href="https://link.segmentfault.com/?enc=rK%2F9Ur%2FUKOpelTwD7PkdYQ%3D%3D.fRnP57yuXiUnGme6fd7OQjq2JBMAPWytBcTgpeEaoTD1%2B7PS9EO6wkE5eLQypZwAE4usXTekLPOKBOK97Sntw7WNhMJpUnoQnUUAeLiqtRKgUnXTQqqRZRqLgO37BQUNMslH6mXOeeyC5TxU5lvlMw%3D%3D" rel="nofollow">GitHub</a> 。</p>
<p>顺带一提,以上两个递归方案在 Codewars 上都会栈溢出。这是因为 Codewars 虽然用的 Node v6 ,但并没有开启尾递归优化。</p>
<h2>循环版本</h2>
<p>思路一致,就不赘述了,直接看代码:</p>
<pre><code class="js">function removeDuplicatesV3(head) {
for (let node = head; node; node = node.next) {
while (node.next && node.data === node.next.data) node.next = node.next.next
}
return head
}</code></pre>
<p>可以看到,因为循环体外的共有变量 <code>node</code> 和 <code>head</code> ,这个例子代码比递归版本要简单直观很多。</p>
<h2>总结</h2>
<p>循环和递归没有孰优孰劣,各有合适的场合。这个 kata 就是一个循环比递归简单的例子。另外,尾递归因为要传递中间变量,所以写起来的感觉会更类似循环而不是正常的递归思路,这也是为什么我对大部分 kata 没有做尾递归的原因 -- 这个教程的目的是展示递归的思路,而尾递归有时候达不到这一点。</p>
<p>算法相关的代码和测试我都放在 <a href="https://link.segmentfault.com/?enc=JBPBdYq%2F5RpBT0nUnHq%2BTQ%3D%3D.4rPhAEOVnda8E8tP0E%2BGVFFRQtDqdhpUo9ncWb7Dkol7Pr7TDFuPZVmEmWiEECCAsqav0S8vSUtRWXFm7OaOaA%3D%3D" rel="nofollow">GitHub</a> 上,如果对你有帮助请帮我点个赞!</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=14VX41y2iWN%2BD0lqakN5Og%3D%3D.mEGnIeDxNTFmCUgDJoBGHK%2F3NARr6VfgUFL9isvakrVPSy7bVyJgErAdHmI7alAslZ%2FxBoFh5I0T6YHzqtLJ%2FSBmb7eWtSv1z9g4NPpWEYw%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=urIf9Uh26imRdO4tpOzPjw%3D%3D.qpWS9UAj9hdh%2BQmFgEboLVFn1JlrfYs5Aoct1Wu1YWHaZtbDQtOKMP7KiiP8%2FD5H5xML%2FroQ4tWVB79NYyaKNwiSlQTALAbc7DbZ55nvI%2BZCx6Rb7t7KKFP%2B9JWlWhIt" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=wWGVZaivCkP%2BURF62O395A%3D%3D.4QJ3yXQHoUo39rTH9WiRrYYaTUwtls%2FBCby67Bf7ryVPv4IsbOS0oREe0a9Mg1ynK1pgPYIGLWlZEwK%2FJrY9f4ZOMr0pISUaUlRWfpSSiOFXHO5PWcWxJH4uCzxi1No99mVjCdZjHhcRK17f1B8pAw%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 07 Append
https://segmentfault.com/a/1190000008047926
2017-01-08T12:56:01+08:00
2017-01-08T12:56:01+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>TL;DR</h2>
<p>把一个链表连接到另一个链表的末尾。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>append()</code> 函数,把两个链表连接起来,并返回连接后的链表头结点。</p>
<pre><code class="js">var listA = 1 -> 2 -> 3 -> null
var listB = 4 -> 5 -> 6 -> null
append(listA, listB) === 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> null</code></pre>
<p>如果两个链表都是 <code>null</code> 就返回 <code>null</code> ,如果其中一个是 <code>null</code> 就返回另一个链表。</p>
<h2>递归版本</h2>
<p><code>append</code> 本身就可以作为递归的逻辑。<code>append(listA, listB)</code> 实际上等于 <code>listA.next = append(listA.next, listB)</code> ,直到 <code>listA</code> 递归到末尾 <code>null</code> ,这时 <code>append(null, listB)</code> 直接返回 <code>listB</code> 即可。加上边界条件判断,代码如下:</p>
<pre><code class="js">function append(listA, listB) {
if (!listA) return listB
if (!listB) return listA
listA.next = append(listA.next, listB)
return listA
}</code></pre>
<h2>循环版本</h2>
<p>循环的思路是,在 <code>listA</code> 和 <code>listB</code> 都不为空的情况下,先找到 <code>listA</code> 的尾节点,假设为 <code>node</code> ,然后 <code>node.next = listB</code> 即可。代码如下:</p>
<pre><code class="js">function appendV2(listA, listB) {
if (!listA) return listB
if (!listB) return listA
let node = listA
while (node.next) node = node.next
node.next = listB
return listA
}</code></pre>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=c%2FXE6yj6pihFcybnAORH%2BA%3D%3D.049BOf9sAe9ApriuvbK5whaydSmczel5jWZLRVVSf6HOdfPaUkG0fLuG5EwUA5iJVfXlXvIjyF51OZ%2FtogLEJA%3D%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=hglen%2BNFRvvIMDoe16JQHQ%3D%3D.R%2BbyTuudTnUSE8kODSlmKa1%2B623X%2B%2BNG1HfqTrDVVlGg45b0a3tVnMMFFmBJ7iUQXsCME3sN9exDV2QHArwWmHiRNtFx6PPpzSPz0K9bDLQ0k79Tcq%2BbaKl1jlUHSXQw" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=j3h5EkS6oj313TnunwQNwg%3D%3D.1eGXGNwm6Skczsjwp0WnED3ome6OGQ4NK6v1rZAigYftPzSmRSZ9pkzJDuX6YOsmGP%2B0oL1jXwtmy9D6amYEaYYyr9hiFWlJ%2FAb6q%2FuE8keUxgmI9Me6DGO4eUHmVa2g" rel="nofollow">GitHub 的测试</a></p>
2016 年度总结 - 明确方向,养成习惯
https://segmentfault.com/a/1190000007983714
2017-01-02T00:03:03+08:00
2017-01-02T00:03:03+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>TL;DR</h2>
<p>又到了年度总结的时候了。有人说年度总结是一个 “感觉还是干了很多事嘛” 的自我催眠,虽然我感觉不是如此,但文章也难逃这个套路。不过无论客观与否,总结这事情也只有自己是最大的受益者,因为一年的经历浓缩成数行文字,其背后隐藏的意义只有当事人有最深刻的理解,对旁人的借鉴意义十分有限。这篇放到博客上,更多的是为了给自己提个醒。</p>
<p>今年对我是头一次把年初计划都完成的一年,没有挪到 2017 年去。养成了几个有用的,也许能影响一生的习惯,整体还算稳步向前。</p>
<h2>年度目标</h2>
<p>年初制定的,全部完成,可见定小目标的好处 :)</p>
<ul>
<li><p>学习软件设计的原则,对框架不用生搬硬套,更应注重了解其设计初衷和思路。</p></li>
<li><p>学习算法,培养逻辑思维能力。</p></li>
<li><p>培养技能深度,学习框架基础内容。</p></li>
<li><p>继续学习 Elixir 。</p></li>
<li><p>积累团队和培养新人的经验。</p></li>
<li><p>继续写博客。</p></li>
<li><p>学习做手冲咖啡。</p></li>
<li><p>看一本非技术书籍和一本小说。</p></li>
<li><p>照顾好家庭(老婆,孩子,父母)。</p></li>
<li><p>了解财务状况,从记账开始。</p></li>
</ul>
<h2>日记</h2>
<p>去年底我开始尝试晨间日记。从隔几天随便写写,到天天坚持写,分门别类的记录,并保持定期回顾,大半年后才形成自己的风格。我不敢说这个过程中有多大的进步,但现在我已经养成了制定月度计划和每日计划的习惯,每天打钩,月底回顾,跟年度目标做对比,确保努力没有偏离方向。目前看来还是挺有效的。</p>
<p>很多事情时间久了是记不住的,记录是回顾的基础,定期回顾可以切实的认识到自己的成长和不足,从而制定计划改变,让生活变得有序。这是我今年养成的最有用的习惯。推荐感兴趣的朋友阅读《晨间日记的奇迹》。</p>
<h2>工作</h2>
<p>我一直在思考如何提升自己的同时也为公司创造价值。对部分人而言 “自己喜欢的事” 和 ”对公司有用的事“ 不完全是一个东西,对我现在的工作而言两者重合度还比较高,但公司的业务模式很难让我有进一步的积累。经过一番思考我在年底请辞,并去了一家产品导向的创业公司,产品我还比较感兴趣,前景不错,技术方面有可信任的小伙伴和足够的折腾空间。希望新的一年有好的发展。</p>
<h2>技术</h2>
<p>搞技术也有 8 年,自从 3 年前开始接触前端框架至今,我算是不折不扣的全栈工程师了,大部分项目我都是前后端兼顾。不过广度的延伸也不可避免导致深度的问题,甚至因为前端的飞速发展导致没有办法分配更多精力维持前后端的深度。</p>
<p>为了让学习效率最大化,今年我做了一个尝试:</p>
<ul>
<li><p>更集中的发展一个方向的深度,我选择的前端。</p></li>
<li><p>花更多时间投资通用的基础能力 — 数据结构,算法,软件设计能力。</p></li>
<li><p>广度积累有一定的筛选,避免看跟我的知识体系相关性不大的信息。</p></li>
</ul>
<p>目前为止我看了不少设计相关的书和博客,在 Codewars 上练习了 <a href="https://link.segmentfault.com/?enc=7DBeDkzE2TfdqB7rS51FvA%3D%3D.Yp%2FOzUnb4ckkubKhms0NnmCfd4mT0ucOJ2UsxdeHb5ucZagFEacW584qzG1AnxPA" rel="nofollow">几十个 kata</a> 并积累了一部分到 <a href="https://link.segmentfault.com/?enc=XRs8Qzwn04q6q5ZVNcHGnA%3D%3D.00euymnzDkKSc3%2F1tAyW0yB8OquZd4FIfYEOBBpId0oE81llOfANDwMecOeu2oqwIdGxh2FoKGiIWd9w6hJ5McH3UVRf0Xeg%2F280fM6FNSU%3D" rel="nofollow">GitHub</a> ,开始写 <a href="https://segmentfault.com/a/1190000007543189">链表系列博客</a> 。名年也会继续坚持,看效果如何。</p>
<p>学习效率最大化的另一个改变就是更关注通用的解决方案而不是满足于自行 hack 了,这也间接促成了 7 月为 Rails 贡献的一点代码。</p>
<p>年底换工作带来了一个好处,我终于可以在实际工作中使用 Elixir 了,工作需要加上兴趣的动力是巨大的,我成功地摆脱了断断续续学习的现状,迅速啃完了《Programming Elixir 1.3》和《What’s New in Ecto 2.0》,《Programming Phoenix》作为工作之余的补充看看,目前也完成了大半。这门语言给我的感觉一如第一次接触 Ruby 的时候,感觉非常有意思,对编程思路的扩宽也非常有用。</p>
<h2>博客</h2>
<p>我写博客已经断断续续很多年了,从 JavaEye ,博客园,到 GitHub pages,Logdown,还尝试过 Medium ,Ghost ,简书,甚至 Evernote ,一直都没坚持下来。原因有很多,包括没有足够的正向反馈,写到一半发现知识没完全吃透,追求完美导致一再延期,讲述知识的能力有限导致动笔困难,一贯的拖延懒散性子,等等。去年我就有写博客的目标,不过这个目标成功的被我拖延到了今年……</p>
<p>为了减少干扰,今年我给自己定的博客目标很简单,每月一篇,只求对自己有用,先培养习惯,不操心任何额外的事情(载体,阅读量,选题等)。年底看来,虽然有三个月没有更新,但一共写了 18 篇,也算及格。其中日记也有部分功劳,让我更习惯写字记录和反思。另外今年还真有几个问题是通过翻以前的博客快速解决了,让我感觉这个投入是对的。</p>
<h2>家庭</h2>
<p>今年最大的改变就是添了一个娃。后半年就是各种痛并快乐着。尤其是刚出生那会儿,生活不可避免的被打乱了,一些培养了一半的习惯直接断了。事到临头才发现之前做的所有心里建设都是扯淡,初期也有心情烦躁的时候,不过渐渐地学会了如何跟娃相处,后来就莫名其妙的喜欢起来了。也许是出于责任,也许是出于小生命最自己的依赖(其实主要是对他妈)。孩子一出生,家里就回不到二人世界的样子了,有点可惜。不过人生总要不断向前,体会不同的经历,把当下过好就足够了。</p>
<h2>读书 & 电影</h2>
<p>这部分是为了扩展视野。书和电影各自看了 20 多部,包括《神经漫游者》,《神们自己》这类科幻,也包括《精力管理》,《GTD》这类工具书,这点比年初计划乐观多了,事实证明看一本精彩的小说是不需要投入精力去坚持的事情,更操心的反而是让自己放下书干点别的…… 一年下来算是养成了用 Kindle 的习惯。发现用 Kindle 看书更适合用整块时间专注的看,而不是零碎时间看。所以为了更好的利用碎片时间看手机而买了 iPhone 7 ,嗯,真不是剁手 :)</p>
<h2>健康</h2>
<p>年中开始重视身体,决定锻炼了,目前还没能形成习惯,隔天一次的锻炼都经常给自己找借口,喝酒了,喝咖啡了,回家晚了,搞工作了,单纯的不想动了,不一而足。明年争取达到天天锻炼的程度。</p>
<h2>人际关系</h2>
<p>自从写晨间日记后我就比以前更在意人际关系的维护。不过对此最有感触的是找工作的时候。一些朋友都推荐了不错的机会。让我突然有种超出准备的意外感觉。年初通过网络认识了余凡,直接促成了年底工作的工作机会。想想也挺奇妙。这次换工作的经历也让我深刻理解了社交圈的重要性,有时候还能发现自己和想去的公司的间接联系(所谓的六度空间)。</p>
<h2>总结</h2>
<p>该写的都写了,也算是对 2016 盖棺定论了。2017 年希望继续保持专注,继续做个靠谱的人,在事业和家庭上都得到收获。</p>
用 JavaScript 实现链表操作 - 06 Insert Sort
https://segmentfault.com/a/1190000007977789
2016-12-31T15:56:46+08:00
2016-12-31T15:56:46+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>2016 年末最后一篇,对链表进行插入排序。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>insertSort()</code> 函数对链表进行升序排列(插入排序)。实现过程中可以使用 <a href="https://segmentfault.com/a/1190000007912308">上一个 kata</a> 中的 <code>sortedInsert()</code> 函数。<code>insertSort()</code> 函数接受链表头为参数并返回排序后的链表头。</p>
<pre><code class="js">var list = 4 -> 3 -> 1 -> 2 -> null
insertSort(list) === 1 -> 2 -> 3 -> 4 -> null</code></pre>
<p>如果传入的链表为 <code>null</code> 或者只有一个节点,就原样返回。</p>
<h2>关于插入排序</h2>
<p>插入排序的介绍可以看 <a href="https://link.segmentfault.com/?enc=UjuMoVl%2FKRF%2Bymbd80VIsQ%3D%3D.4kgTmEUut%2BR%2Fhzjyagkkutur%2FCdguh%2Bw7DBNlaiZrZsFhbTTv1pgxZyT2UPC7E4n" rel="nofollow">Wikipedia</a> ,大体逻辑为:</p>
<ol>
<li><p>建立一个新的空链表。</p></li>
<li><p>依次遍历待排序的链表节点,挨个插入新链表的合适位置,始终保持新链表是已排序的。</p></li>
<li><p>遍历完成,返回新链表。</p></li>
</ol>
<p>观察这段逻辑不难发现,第二个步骤其实就是上个 kata 中 <code>sortedInsert</code> 做的事情 -- 把节点插入一段已排序的链表的合适位置。在此之上稍微包装一下就可以实现 <code>insertSort</code> 。</p>
<h2>递归版本</h2>
<p>首先我们记住两个函数的表达的意思:</p>
<ol>
<li><p><code>insertSort</code> 返回链表的排序版本。</p></li>
<li><p><code>sortedInsert</code> 把节点插入一个已排序链表的合适位置,并返回修改后的链表(也是已排序的)。</p></li>
</ol>
<p>然后我们用递归的思路描述 <code>insertSort</code> 逻辑,应该是先把原链表的第一个节点插入某个已排序的链表的合适位置,这段逻辑可以用 <code>sortedInsert(someList, head.data)</code> 表达。而这个 “某个已排序的链表” ,我们需要它包含除了 <code>head</code> 之外其他的所以节点,这个链表可以用 <code>insertSort(head.next)</code> 来表达。</p>
<p>整理后的代码如下:</p>
<pre><code class="js">function insertSort(head) {
if (!head) return null
return sortedInsert(insertSort(head.next), head.data)
}</code></pre>
<h2>循环版本</h2>
<p>循环版本是最接近算法描述的版本,所以不多赘述。代码如下:</p>
<pre><code class="js">function insertSort(head) {
for (var sortedList = null, node = head; node; node = node.next) {
sortedList = sortedInsert(sortedList, node.data)
}
return sortedList
}</code></pre>
<h2>总结</h2>
<p>因为有上个 kata 的函数的帮助,这个插入排序实现起来非常简单。递归版本再次体现了声明式编程的优势。有时候能表达某种数据的不只是变量,也可以是函数。只要我们发现表达合适逻辑的函数,实现过程就会非常简单。</p>
<p>算法相关的代码和测试我都放在 <a href="https://link.segmentfault.com/?enc=tPwv%2FdhiQFIiwT6ocEyp%2FA%3D%3D.rjl7pFcetxWPESSkyDO%2BvVxn5XAC4QQE%2FfCrJBvJry2dZqTvAwqQ36zsf9w2%2BbAjyU2t5XQF7bEBPuIS3hcMHA%3D%3D" rel="nofollow">GitHub</a> 上,如果对你有帮助请帮我点个赞!</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=hnKVxE4C2v0lUJscCiWs3g%3D%3D.OhR8IYg3LTIda1oTINjc55hhlYiLCu7pQM0EOjMwj30nPYkOQ4KjieLhvJzh01smg353Bv0adsbza0B9HymTTFHmI9T6cGFzcdOGLmNQUSA%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=2WOuQMJW0lXUxgXuqhjQEA%3D%3D.k%2F8qXK35xOe%2FprqUn4BILejknHrEcpFP3NsWBsEq%2B05bOL5KQnx0iEjgnZj2qfEMY%2F%2BRIL1nf%2FO1jclrTcHTdlYJZcpVBXtKBrydnerCZXoG7vhwo6a0ZzanU9xU%2FFp6" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=L6YQo5qi8uBQEFbYvxJjYA%3D%3D.NA%2B9t3jpXxaBTp%2F%2FMIUspVZlPuPI07SJTi0Gh1n2rzVdhua1YZTC7XcOsArCfbXlksse3WlRi%2FxfkJay0zY0q%2F%2BWt2nAUSzCmkNViUxMUngIKvpYfJAjke6jSpRKIJpQ" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 05 Sorted Insert
https://segmentfault.com/a/1190000007912308
2016-12-25T15:31:41+08:00
2016-12-25T15:31:41+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>把节点插入一个已排序的链表。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>写一个 <code>sortedInsert()</code> 函数,把一个节点插入一个已排序的链表中,链表为升序排列。这个函数接受两个参数:一个链表的头节点和一个数据,并且始终返回新链表的头节点。</p>
<pre><code class="js">sortedInsert(1 -> 2 -> 3 -> null, 4) === 1 -> 2 -> 3 -> 4 -> null)
sortedInsert(1 -> 7 -> 8 -> null, 5) === 1 -> 5 -> 7 -> 8 -> null)
sortedInsert(3 -> 5 -> 9 -> null, 7) === 3 -> 5 -> 7 -> 9 -> null)</code></pre>
<h2>递归版本</h2>
<p>我们可以从简单的情况推演递归的算法。下面假定函数签名为 <code>sortedInsert(head, data)</code> 。</p>
<p>当 <code>head</code> 为空,即空链表,直接返回新节点:</p>
<pre><code class="js">if (!head) return new Node(data, null)</code></pre>
<p>当 <code>head</code> 的值大于或等于 <code>data</code> 时,新节点也应该插入头部:</p>
<pre><code class="js">if (head.data >= data) return new Node(data, head)</code></pre>
<p>如果以上两点都不满足,<code>data</code> 就应该插入后续的节点了,这种 “把数据插入某链表” 的逻辑恰好符合 <code>sortedInsert</code> 的定义,因为这个函数始终返回修改后的链表,我们可以新链表赋值给 <code>head.next</code> 完成链接:</p>
<pre><code class="js">head.next = sortedInsert(head.next, data)
return head</code></pre>
<p>整合起来代码如下,非常简单并且有表达力:</p>
<pre><code class="js">function sortedInsert(head, data) {
if (!head || data <= head.data) return new Node(data, head)
head.next = sortedInsert(head.next, data)
return head
}</code></pre>
<h2>循环版本</h2>
<p>循环逻辑是这样:从头到尾检查每个节点,对第 n 个节点,如果数据小于或等于节点的值,则新建一个节点插入节点 n 和节点 n-1 之间。如果数据大于节点的值,则对下个节点做同样的判断,直到结束。</p>
<p>先上代码:</p>
<pre><code class="js">function sortedInsertV2(head, data) {
let node = head
let prevNode
while (true) {
if (!node || data <= node.data) {
let newNode = new Node(data, node)
if (prevNode) {
prevNode.next = newNode
return head
} else {
return newNode
}
}
prevNode = node
node = node.next
}
}</code></pre>
<p>这段代码比较复杂,主要有几个边界情况处理:</p>
<ol>
<li><p>函数需要始终返回新链表的头,但插入的节点可能在链表头部或者其他地方,所以返回值需要判断是返回新节点还是 <code>head</code> 。</p></li>
<li><p>因为插入节点的操作需要连接前后两个节点,循环体要维护 <code>prevNode</code> 和 <code>node</code> 两个变量,这也间接导致 <code>for</code> 的写法会比较麻烦,所以才用 <code>while</code> 。</p></li>
</ol>
<h2>循环版本 - dummy node</h2>
<p>我们可以用 <a href="https://segmentfault.com/a/1190000007800288">上一个 kata</a> 中提到的 dummy node 来解决链表循环中头结点的 <code>if/else</code> 判断,从而简化一下代码:</p>
<pre><code class="js">function sortedInsertV3(head, data) {
const dummy = new Node(null, head)
let prevNode = dummy
let node = dummy.next
while (true) {
if (!node || node.data > data) {
prevNode.next = new Node(data, node)
return dummy.next
}
prevNode = node
node = node.next
}
}</code></pre>
<p>这段代码简化了第一版循环中返回 <code>head</code> 还是 <code>new Node(...)</code> 的问题。但能不能继续简化一下每次循环中维护两个节点变量的问题呢?</p>
<h2>循环版本 - dummy node & check next node</h2>
<p>为什么要在循环中维护两个变量 <code>prevNode</code> 和 <code>node</code> ?这是因为新节点要插入两个节点之间,而我们每次循环的当前节点是 <code>node</code> ,单链表中的节点没办法引用到上一个节点,所以才需要维护一个 <code>prevNode</code> 。</p>
<p>如果在每次循环中检查的主体是 <code>node.next</code> 呢?这个问题就解决了。换言之,我们检查的是数据是否适合插入到 <code>node</code> 和 <code>node.next</code> 之间。这种做法的唯一问题是第一次循环,我们需要 <code>node.next</code> 指向头结点,那 <code>node</code> 本身又是什么? dummy node 正好解决了这个问题。这块有点绕,不懂的话可以仔细想想。这是链表的一个常用技巧。</p>
<p>简化后的代码如下,顺带一提,因为可以少维护一个变量,<code>while</code> 可以简化成 <code>for</code> 了:</p>
<pre><code class="js">function sortedInsertV4(head, data) {
const dummy = new Node(null, head)
for (let node = dummy; node; node = node.next) {
const nextNode = node.next
if (!nextNode || nextNode.data >= data) {
node.next = new Node(data, nextNode)
return dummy.next
}
}
}</code></pre>
<h2>总结</h2>
<p>这个 kata 是递归简单循环麻烦的一个例子,有比较才会理解递归的优雅之处。另外合理使用 dummy node 可以简化不少循环的代码。算法相关的代码和测试我都放在 <a href="https://link.segmentfault.com/?enc=6qvT8bw%2Fwd1eFJ8cd8NfNQ%3D%3D.THzHuIojusuzLiq85WCPhM6RlyMrPk8GgvBjXbgwk99JTljPzdfY7Trh8tEiN4NemoxQZSxdqNZJwoYyCB6%2BhA%3D%3D" rel="nofollow">GitHub</a> 上,如果对你有帮助请帮我点个赞!</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=IdRrXxhnRKliuF6yOR5ReQ%3D%3D.7zvPbxp1cVYSSrswTW%2B5P1IJ9DK1Nle2XQjqD0sNGO%2FEaParSojOqkUQLtWzJFCqsF%2F7Ch5LdBq8hmUdXwOsJDZ%2BZ2sNrj0zJn5E0YR4Ye4%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=Bb3TlS%2FxttQabFoxE0QE0w%3D%3D.nqtzU2h8cgceBVIJ4lIswN1l8x%2BO5HXO6WsfGU61dqlxYk7YFqTZ4L0PqA4Dc%2BuIhofY%2FctXUUutbYkPuNen6wf2sl3q5Omj3IcPFwrBiJO4fg1ybM9SbT68%2BPlJ2IdU" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=nT%2BP7Z7KX9nGeuGYexgDVQ%3D%3D.wWCkm1X3yRGc6FyBrB384Tm3%2F1o%2B4mR7BhsWCjQSwNxeMHyPkNknyrV%2FYRNbwgufomqfOoOdvmheVTp%2BsQDNTGagfIvVbFGGGn4b%2FdWKROStFwbDuI6N4KN3IRL0wHf3" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 04 Insert Nth Node
https://segmentfault.com/a/1190000007800288
2016-12-14T15:29:47+08:00
2016-12-14T15:29:47+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>TL;DR</h2>
<p>插入第 N 个节点。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>insertNth()</code> 方法,在链表的第 N 个索引处插入一个新节点。</p>
<p><code>insertNth()</code> 可以看成是 <a href="https://segmentfault.com/a/1190000007625419">01 Push & Build List</a> 中的 <code>push()</code> 函数的更通用版本。给定一个链表,一个范围在 0..length 内的索引号,和一个数据,这个函数会生成一个新的节点并插入到指定的索引位置,并始终返回链表的头。</p>
<pre><code class="js">insertNth(1 -> 2 -> 3 -> null, 0, 7) === 7 -> 1 -> 2 -> 3 -> null)
insertNth(1 -> 2 -> 3 -> null, 1, 7) === 1 -> 7 -> 2 -> 3 -> null)
insertNth(1 -> 2 -> 3 -> null, 3, 7) === 1 -> 2 -> 3 -> 7 -> null)</code></pre>
<p>如果索引号超出了链表的长度,函数应该抛出异常。</p>
<p>实现这个函数允许使用第一个 kata 中的 <code>push</code> 方法。</p>
<h2>递归版本</h2>
<p>让我们先回忆一下 <code>push</code> 函数的用处,指定一个链表的头和一个数据,<code>push</code> 会生成一个新节点并添加到链表的头部,并返回新链表的头。比如:</p>
<pre><code class="js">push(null, 23) === 23 -> null
push(1 -> 2 -> null, 23) === 23 -> 1 -> 2 -> null</code></pre>
<p>现在看看 <code>insertNth</code> ,假设函数方法签名是 <code>insertNth(head, index, data)</code> ,那么有两种情况:</p>
<p>如果 <code>index === 0</code> ,则等同于调用 <code>push</code> 。实现为 <code>push(head, data)</code> 。</p>
<p>如果 <code>index !== 0</code> ,我们可以把下一个节点当成子链表传入 <code>insertNth</code> ,并让 <code>index</code> 减一。<code>insertNth</code> 的返回值一定是个链表,我们把它赋值给 <code>head.next</code> 就行。这就是一个递归过程。如果这次递归的 <code>insertNth</code> 完不成任务,它会继续递归到下一个节点,直到 <code>index === 0</code> 的最简单情况,或 <code>head</code> 为空抛出异常(索引过大)。</p>
<p>完整代码实现为:</p>
<pre><code class="js">function insertNth(head, index, data) {
if (index === 0) return push(head, data)
if (!head) throw 'invalid argument'
head.next = insertNth(head.next, index - 1, data)
return head
}</code></pre>
<h2>循环版本</h2>
<p>如果能理解递归版本的 <code>head.next = insertNth(...)</code> ,那么循环版本也不难实现。不同的是,在循环中我们遍历到 <code>index</code> 的前一个节点,然后用 <code>push</code> 方法生成新节点,并赋值给前一个节点的 <code>next</code> 属性形成一个完整的链表。</p>
<p>完整代码实现如下:</p>
<pre><code class="js">function insertNthV2(head, index, data) {
if (index === 0) return push(head, data)
for (let node = head, idx = 0; node; node = node.next, idx++) {
if (idx + 1 === index) {
node.next = push(node.next, data)
return head
}
}
throw 'invalid argument'
}</code></pre>
<p>这里有一个边界情况要注意。因为 <code>insertNth</code> 要求返回新链表的头。根据 <code>index</code> 是否为 <code>0</code> ,这个新链表的头可能是生成的新节点,也可能就是老链表的头 。这点如果写进 <code>for</code> 循环就不可避免有 <code>if/else</code> 的返回值判断。所以我们把 <code>index === 0</code> 的情况单独拿出来放在函数顶部。这个边界情况并非无法纳入循环中,我们下面介绍的一个技巧就与此有关。</p>
<h2>循环版本 - dummy node</h2>
<p>在之前的几个 kata 里,我们提到循环可以更好的容纳边界情况,因为一些条件判断都能写到 <code>for</code> 的头部中去。但这个例子的边界情况是返回值不同:</p>
<ol>
<li><p>如果 <code>index === 0</code> ,返回新节点 。</p></li>
<li><p>如果 <code>index !== 0</code> ,返回 head 。新节点会被插入 head 之后的某个节点链条中。</p></li>
</ol>
<p>如何解决这个问题呢,我们可以在 <code>head</code> 前面再加入一个节点(数据任意,一般赋值 null)。这个节点称为 dummy 节点。这样一来,不管新节点插入到哪里,<code>dummy.next</code> 都可以引用到修改后的链表。</p>
<p>代码实现如下,注意 <code>return</code> 的不同。</p>
<pre><code class="js">function insertNthV3(head, index, data) {
const dummy = push(head, null)
for (let node = dummy, idx = 0; node; node = node.next, idx++) {
if (idx === index) {
node.next = push(node.next, data)
return dummy.next
}
}
throw 'invalid argument'
}</code></pre>
<p>dummy 节点是很多链表操作的常用技巧,虽然在这个 kata 中使用 dummy 节点的代码量并没有变少,但这个技巧在后续的一些复杂 kata 中会非常有用。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=aUz6UJf1DcoM6Clff8HU1w%3D%3D.6FVknQw8Pd17ZxVzP25u7AS3oo%2BobKjQSM1tWQyVISNgY7tigYjJj5Ofx4kl9j1a4kTkJg68pP%2Fw64%2F41zGimIWt7O8Fvxk%2FvlfYHOyrB2U%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=tvWeveQ%2B6i4KsgFQoaFbmA%3D%3D.1qb7lLmuDURBX41aSEgiklYzYkckWEs0AXdJaq%2FKBbPo2m4tse7LoexH78Imj4z6xiBeuDalciGlHTV0ZryuZQlkv2D1LLotgPkWTZWIHPMvUh3wRizs%2B5xDlx9PWlaN" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=p1clAM00AA%2FjvmNtI%2FIY6Q%3D%3D.N8hIYvjSYnzFpCK5gKKuJvJmhAG7xFhivkuqWL5B0Mv10unuZIdPHsZS3vYKC1jULsdZAQe03Q%2Fw0EOw9akCJgqbLs6%2FuDsw9dS7akwhl2gRVvvKIt6bD%2B98poLOrWtT3NaHiSaKU%2BtW8%2F1ltAtmeQ%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 03 Get Nth Node
https://segmentfault.com/a/1190000007737715
2016-12-08T15:03:31+08:00
2016-12-08T15:03:31+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>获得链表的第 N 个节点。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>getNth()</code> 方法,传入一个链表和一个索引,返回索引代表的节点。索引以 0 为起始,第一个元素索引为 0 ,第二个为 1 ,以此类推。比如:</p>
<pre><code class="js">getNth(1 -> 2 -> 3 -> null, 0).data === 1
getNth(1 -> 2 -> 3 -> null, 1).data === 2</code></pre>
<p>传入的索引必须是在效范围内,即 <code>0..length-1</code> ,如果索引不合法或者链表为空都需要抛出异常。</p>
<h2>递归版本</h2>
<p>假设函数定义为 <code>getNth(head, idx)</code> ,递归过程为:当 <code>idx</code> 为零,直接返回该节点,否则递归调用 <code>getNth(head.next, idx - 1)</code> 。再处理下边界情况就完成了,代码如下:</p>
<pre><code class="js">function getNth(head, idx) {
if (!head || idx < 0) throw 'invalid argument'
if (idx === 0) return head
return getNth(head.next, idx - 1)
}</code></pre>
<h2>循环版本</h2>
<p>我选择的 <code>for</code> 循环,这样方便把边界情况检查都放到循环里去。如果循环结束还没有查到节点,那肯定是链表或者索引不合法,直接抛异常即可。对比这两个版本和 <a href="https://segmentfault.com/a/1190000007689904?_ea=1435259">02 Length & Count</a> 的例子,不难看出循环可以比递归更容易地处理边界情况,因为一些条件检查可以写进循环的头部,递归就得自己写 <code>if/else</code> 逻辑。</p>
<pre><code class="js">function getNthV2(head, idx) {
for (let node = head; node && idx >= 0; node = node.next, idx--) {
if (idx === 0) return node
}
throw 'invalid argument'
}</code></pre>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=uI0BlhzPGsEKEpQHgajM4g%3D%3D.vUYClf4qEyoaapV6Du7lJsYkbSF1AtaTNd8ybc3xufwgacNcHWT7u2RiLlktiT4oNVF55eSkdyYfEv9w1qSaFRvUFFU%2B1YCWhTPU1sBfY1Q%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=TAuMAe0TVIauKausKOmoyw%3D%3D.9C6xQyckRSZ%2FnQN0NEBx9T0A4YHhFKyMj2Gz%2Ft1EBEb8DOKzvgJhFLdGzgKeHc2NDXGiHedNDjC0hdcxQw%2FVqaPZlphVy84Phn5q1OhshsA97SJhrB3XHb5DqYiOmKG2" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=9%2BQCE6uWA%2BVFZzfCbzT5VA%3D%3D.Ni9Fz6f4v6SFghNoTMeT6tb%2B67C1hbnskIg2tqesTwvmYyDr0ynQC%2FP24Fq4oOUMiqPAeEwLC7FTjJyf2nLgfV9%2FbGTZKWcm6EFl3BSeZdhi1K0Z0BXwH50Z%2BxkqrgB1" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 02 Length & Count
https://segmentfault.com/a/1190000007689904
2016-12-04T17:31:32+08:00
2016-12-04T17:31:32+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>计算链表的长度和指定元素的重复次数。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>实现一个 <code>length()</code> 函数来计算链表的长度。</p>
<pre><code class="js">length(null) === 0
length(1 -> 2 -> 3 -> null) === 3</code></pre>
<p>实现一个 <code>count()</code> 函数来计算指定数字在链表中的重复次数。</p>
<pre><code class="js">count(null, 1) === 0
count(1 -> 2 -> 3 -> null, 1) === 1
count(1 -> 1 -> 1 -> 2 -> 2 -> 2 -> 2 -> 3 -> 3 -> null, 2) === 4</code></pre>
<h2>length</h2>
<h3>递归版本</h3>
<p>递归是最有表达力的版本。思路非常简单。每个链表的长度 <code>length(head)</code> 都等于 <code>1 + length(head.next)</code> 。空链表长度为 0 。</p>
<pre><code class="js">function length(head) {
return head ? 1 + length(head.next) : 0
}</code></pre>
<h3>循环版本 - while</h3>
<p>链表循环第一反应是用 <code>while (node) { node = node.next }</code> 来做,循环外维护一个变量,每次自增 1 即可。</p>
<pre><code class="js">function lengthV2(head) {
let len = 0
let node = head
while (node) {
len++
node = node.next
}
return len
}</code></pre>
<h3>循环版本 - for</h3>
<p><code>for</code> 和 <code>while</code> 在任何情况下都是可以互换的。我们可以用 <code>for</code> 循环把变量初始化,节点后移的操作都放到一起,简化一下代码量。注意因为 <code>len</code> 要在 <code>for</code> 外部作为返回值使用,我们只能用 <code>var</code> 而不是 <code>let/const</code> 声明变量。</p>
<pre><code class="js">function lengthV3(head) {
for (var len = 0, node = head; node; node = node.next) len++
return len
}</code></pre>
<h2>count</h2>
<h3>递归版本</h3>
<p>跟 <code>length</code> 思路类似,区别只是递归时判断一下节点数据。</p>
<pre><code class="js">function count(head, data) {
if (!head) return 0
return (head.data === data ? 1 : 0) + count(head.next, data)
}</code></pre>
<h3>循环版本</h3>
<p>这里我直接演示的 <code>for</code> 版本,思路类似就不多说了。</p>
<pre><code class="js">function countV2(head, data) {
for (var n = 0, node = head; node; node = node.next) {
if (node.data === data) n++
}
return n
}</code></pre>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=o3MAbzk%2BmDzlV4AdQGlfeQ%3D%3D.bA0rAoMmYw5exDdDyVueZDpXxDX4VZysVQIwKRRwcFyINqHi85EygsRiTSXohVN0pCojF%2BaJJGhxrJaXghCkfmcuQSn3WJwj1IBcXcPzNmQ%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=gguEZ4u5QJAIh9CFGc7moQ%3D%3D.Hw84gI4e%2BBiPwOV7OWRDSoq%2BnniLqRSwPwLXHX4j6PaTJdDHbtfN698itALBoklkgKir2BrNWAxrwFfgtoYijdSm9X6nF4VANYBwI8hOvTHIAsAyxRMz2NjMUxaMqbMm" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=hTEG8ykWFiz9fcfsGMzevA%3D%3D.fk1fSEiuzuLy292XZ7rj%2B3w5WBl7C8HByq24h6iFnXYM1tFrFEFvTd%2BTbg4cHcfAIU4BLoJNo6jyz6v5FBBU9KbMLTD1F8DVQEPgDXzbjbxmRTvzcOLDc9FZYkGozLMWKtDBLnWqloE%2BiOHOj1gWaw%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 01 Push & Build List
https://segmentfault.com/a/1190000007625419
2016-11-28T16:57:41+08:00
2016-11-28T16:57:41+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>写两个帮助函数来创建链表。系列目录见 <a href="https://segmentfault.com/a/1190000007543189">前言和目录</a> 。</p>
<h2>需求</h2>
<p>写两个方法 <code>push</code> 和 <code>buildList</code> 来初始化链表。尝试在 <code>buildList</code> 中使用 <code>push</code> 。下面的例子中我用 <code>a -> b -> c</code> 来表示链表,这是为了书写方便,并不是 JavaScript 的有效语法。</p>
<pre><code class="js">let chained = null
chained = push(chained, 3)
chained = push(chained, 2)
chained = push(chained, 1)
push(chained, 8) === 8 -> 1 -> 2 -> 3 -> null</code></pre>
<p><code>push</code> 用于把一个节点插入到链表的头部。它接受两个参数 head 和 data ,head 可以是一个节点对象或者 <code>null</code> 。这个方法应该始终返回一个新的链表。</p>
<p><code>buildList</code> 接收一个数组为参数,创建对应的链表。</p>
<pre><code class="js">buildList([1, 2, 3]) === 1 -> 2 -> 3 -> null</code></pre>
<h2>定义节点对象</h2>
<p>作为链表系列的第一课,我们需要先定义节点对象是什么样子。按照 Codewars 上的设定,一个节点对象有两个属性 data 和 next 。data 是这个节点的值,next 是下一个节点的引用。这是默认的类模板。</p>
<pre><code class="js">function Node(data) {
this.data = data
this.next = null
}</code></pre>
<h2>push</h2>
<p>这是 <code>push</code> 的基本实现:</p>
<pre><code class="js">function push(head, data) {
const node = new Node(data)
if (head) {
node.next = head
return node
} else {
return node
}
}</code></pre>
<p>我更倾向于修改一下 Node 的构造函数,把 next 也当成参数,并且加上默认值,这会让后面的事情简化很多:</p>
<pre><code class="js">function Node(data = null, next = null) {
this.data = data
this.next = next
}</code></pre>
<p>新的 <code>push</code> 实现:</p>
<pre><code class="js">function push(head, data) {
return new Node(head, data)
}</code></pre>
<h2>buildList</h2>
<h3>递归版本</h3>
<p>这个函数非常适合用递归实现。这是递归的版本:</p>
<pre><code class="js">function buildList(array) {
if (!array || !array.length) return null
const data = array.shift()
return push(buildList(array), data)
}</code></pre>
<p>递归的思路是,把大的复杂的操作逐步分解成小的操作,直到分解成最基本的情况。拿这个例子解释,给定数组 <code>[1, 2, 3]</code>,递归的实现思路是逐步往链表头部插入数据 3,2,1 ,一共三轮。第一轮相当于 <code>push(someList, 3)</code> 。这个 <code>someList</code> 是什么呢,其实就是 <code>buildList([1, 2])</code> 的返回值。以此类推:</p>
<ul>
<li><p>第一轮 <code>push(buildList([1, 2]), 3)</code></p></li>
<li><p>第二轮 <code>push(buildList([1]), 2)</code></p></li>
<li><p>第三轮 <code>push(buildList([]), 3)</code></p></li>
</ul>
<p>到第三轮就已经是最基本的情况了,数组为空,这时返回 <code>null</code> 代表空节点。</p>
<h3>循环版本</h3>
<p>依照上面的思路,循环也很容易实现,只要反向遍历数组就行。因为循环已经考虑了数组为空的情况,这里就不用进行边界判断了。</p>
<pre><code class="js">function buildListV2(array) {
let list = null
for (let i = array.length - 1; i >= 0; i--) {
list = push(list, array[i])
}
return list
}</code></pre>
<h3>One-liner</h3>
<p>结合循环版本的思路和 JavaScript 的数组迭代器,我们可以得出一个 one-liner 版本。</p>
<pre><code class="js">function buildListV3(array) {
return (array || []).reduceRight(push, null)
}</code></pre>
<p>这个就不解释了,留给各位自己思考下吧。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=rLPh5tTc0b9YC%2F1xCUrNmg%3D%3D.gqnE5rlai2DLyBZ9YK0ksXlCmcBUjrQB8FEE2Lw2R%2FdPcZHWCQNs0mgh7gUOIdMHziDPAa2zv%2BuODn%2Bsw4cYdp67PVRqstDZK4q4nGTI9Lw%3D" rel="nofollow">Codewars Kata</a><br><a href="https://link.segmentfault.com/?enc=akAD0Cr6TsOXJl8r4WbQUg%3D%3D.VqJxqvnd7VMA6i37gqaD7j0CisMmMNpZ%2BgYkc2zc6TxzAyEEsEN0WBY6MDSbv4Yh5s1eqWhR9gMsH5g1kW%2FWzmt9Eie3yO6ugMCSIpPuE0mqyr%2BojVUgeWASgh2C%2BH4bAXSDQOn3pl6Acb3Jq9zNWQ%3D%3D" rel="nofollow">GitHub 的代码实现</a><br><a href="https://link.segmentfault.com/?enc=%2FeqkFrl3F6sxIUBh9zxHvA%3D%3D.UJN%2FvONmN6TJbXfkL9YtKLJgxqdWu2RIGaUQI7TAelKvb9VO8TaVE6bVgKR%2ByxawE5HPc4CHd7B%2BgxcO%2FS5nRqJAFTNjCxTdVIR25Bkn0U6sv05HEIXsYddj1%2BC8Tw91yygYyyrhA0Wrr0buEWUTqA%3D%3D" rel="nofollow">GitHub 的测试</a></p>
用 JavaScript 实现链表操作 - 前言和目录
https://segmentfault.com/a/1190000007543189
2016-11-20T23:32:58+08:00
2016-11-20T23:32:58+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>TL;DR</h2>
<p>我打算写一个链表操作的系列,来自 Codewars 的 <a href="https://link.segmentfault.com/?enc=kmZln7duNXzJdcAiDr9urw%3D%3D.6ZK%2FK1pQj9dTi%2BhC0oyahho0LLh7FG5v0t9s7O3o1qPdGehJd5yvvcXXeyPvYRqSonZw0pGRBslNafN4LKsbYw5%2B%2FWIbopn6eNXrb1E%2B7Vo%3D" rel="nofollow">Linked List 系列 kata</a> ,实现语言是 JavaScript 。这篇是开篇,简单描述了一下我写这个的目的,也作为系列的目录。</p>
<h2>为什么要学习链表</h2>
<p>我的年度目标之一就是学习一些数据结构和算法,用于打基础和培养逻辑思维能力。Codewars 上的这个系列同时符合这两点。</p>
<p>链表是常用数据结构之一,它甚至是某些语言(比如 Elixir)的内置数据结构。通过自己实现一个链表和常用操作,可以加深理解这类数据结构的优缺点。</p>
<p>链表经常用来训练指针操作,虽然这只对 C 适用,但 JavaScript 等高级语言中控制引用的思路其实也差不多。而且链表也是练习递归的理想数据结构之一。</p>
<p>基于此,每个 kata 我都会尽量提供递归和循环两个版本,某些操作会实现尾递归以符合题目要求。这是一个很有意思的过程,有时候递归更好,有时候循环更好,也有少数时候两者都不是最优化的方案。</p>
<h2>目录</h2>
<p>Codewars 上没有总纲,但每一个 kata 都有整个系列的目录。我列举如下,一共 18 个 kata 。每篇的博客链接会在更新后附上。另外,<a href="https://link.segmentfault.com/?enc=sVt7R1Q8PbUso3rmHqJcCw%3D%3D.%2BEOAreRiZX%2B7%2FMWuHA48NbWyclBPhXiPDW%2BMIF6mdgeDHTGn4NhESPnVOJEQIO4HsQjGyNxbwFX%2BljT49nVtRA%3D%3D" rel="nofollow">所有代码</a> 都放在 GitHub 上,代码的更新比博客要快,如果觉得对你有帮助,请帮我点个赞!</p>
<ol>
<li><p><a href="https://segmentfault.com/a/1190000007625419">Push & BuildOneTwoThree</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000007689904">Length & Count</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000007737715">Get Nth Node</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000007800288">Insert Nth Node</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000007912308">Sorted Insert</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000007977789">Insert Sort</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008047926">Append</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008049580">Remove Duplicates</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008051315">Move Node</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008085135">Move Node In-place</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008239747">Alternating Split</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008243727">Front back split</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008396683">Shuffle Merge</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008397427">Sorted Merge</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008398162">Merge sort</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008416965">Sorted Intersect</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008476661">Iterative Reverse</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000008485170">Recursive Reverse</a></p></li>
</ol>
Does Rails Hurt?
https://segmentfault.com/a/1190000007525319
2016-11-18T16:05:40+08:00
2016-11-18T16:05:40+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>这篇文章来自于 Ruby China 上一个很有意思的 <a href="https://link.segmentfault.com/?enc=jObRzyPGkuH5vs7V6VaFvg%3D%3D.fcRGkm3Q%2F2pW2TI7GjPWMJ4wsM1OBBKBV6knXgHRzI7ugWpAt7O8eHnUgbFZk7kC" rel="nofollow">讨论</a> 。原文是一篇国外开发者的吐槽 <a href="https://link.segmentfault.com/?enc=b0oLQhb8BSX%2FAHIqkUEw%2FQ%3D%3D.eCc3UXjPHN7CPcpjY4fN57wV%2Fg2RCQgv2XCqxf3o4GczVFs8snxMeEMmCRHVRTYJ" rel="nofollow">Rails Hurts</a> 。你看的没错,他还特意注册了一个域名 :)</p>
<p>看完后我觉得作者有些断章取义和偷换概念,因此有了这篇文章。主要是想探讨一下,Rails 真的 Hurts 吗?下面我们根据原文的结构一条一条来分析(吐槽)。</p>
<p>看这篇文章之前,我建议看看作者的原文。不管是否赞同他的想法,都会有些收获。</p>
<h2>DRY - Don't repeat yourself</h2>
<p>DRY 只是一种被推崇的原则,但不代表必须无时无刻的严格执行。作者把它理解成 “<strong>无论什么时候</strong> 你看到一个重复的概念,你都 <strong>应该马上</strong> 去把它抽象成一个方法或类” 太狭隘和绝对了。我们老祖宗也说过 “事不过三”,生活中估计也没人能严格做到这一点。</p>
<p>关于代码重复和耦合的问题,作者的论据显然来自 Sandi Metz 说的 “重复比错误的抽象要廉价的多”,但这其实不是 DRY 的问题,而是错误的抽象的问题。而错误的抽象往往来自于对一些概念缺乏全局理解。拿构建领域模型举例,对一个概念的完全理解往往需要反复不断的尝试和探索,这是需要花时间的,而不是了解了一点概念就 <strong>应该马上</strong> 去设计的。所以我想应该是作者对 DRY 的片面理解和执行导致了错误的抽象,从而认为 DRY 会导致代码耦合。其实这两者一点关系都没有。</p>
<h2>KISS - Keep it simple, and stupid</h2>
<p>这个其实是吐槽 AR 而不是吐槽 KISS 的。关于 KISS 各人有各人的理解。作者把 KISS 跟 AR 联系在一起是因为 AR 是 <strong>用简单的接口隐藏复杂的实现</strong> 。其实更应该吐槽的是 AR 混合了太多的关注点,与这点有关联的原则应该是 Single Responsibility Principle 。我也觉得 AR 操心的事情太多了,它的设计在现在来看已经过于复杂了。正因为功能强大,才导致了之后 Fat model 这种理念的兴起。</p>
<p>最后我还是比较赞同这句的:Rails is not simple, it is convenient 。</p>
<h2>Conventions over Configurations</h2>
<p>我一直觉得一个 Convention 好不好得取决于两点,一是大众对这个 Convention 的接受程度,二是这个 Convention 符不符合直觉思考。Rails 在这两点上做的都没问题。作者主要吐槽的是 Rails 提出的 CoC 理念把很多第三方开发者都影响了,让他们制造了很多诡异的 Conventions 和滥用元编程写 DSL 。但这仍然不是 Rails 的问题,也不能证明 CoC 是不好的。</p>
<p>是否 CoC 从广义上看是 implicit 和 explicit 孰优孰劣的问题。我觉得把符合直觉的配置变成 Conventions 能简化很多事情,其他部分不妨用 Configurations 。</p>
<h2>Fat model, skinny controller</h2>
<p>这确实不是个好事。但 Rails 社区也早就不这么干了,现在几乎找不到 Fat model 的文章了,反 Fat model 的文章倒是不少。</p>
<p>说到 Fat model 这个话题,我抛一个不成熟的观点,我觉得 Rails 对开发的简化很大程度上来自于 <strong>对抽象层级的压缩</strong> 。这点在 AR 上体现得尤其明显。这种设计在做简单的功能时会非常省事,在 model 里写几行 validation 和 callback 就把数据库事务,表单验证,业务逻辑前后的副作用(比如发送邮件)全部搞定了。但如果复杂的场景下还沿用这种开发方式就会导致 model 臃肿,业务逻辑互相缠绕等问题。究其原因,还是抽象层次的压缩导致了代码缠成一团。</p>
<p>但从另一个角度看,Rails 也并没有阻止开发者去加自己的抽象层。validation 和 callback 这些功能也被设计成很容易加到其他 class 里面。所以 Rails 还是蛮灵活的。只是开发者必须意识到对简单问题的处理思路(很多人理解的 Rails way)基本不可能沿用到复杂问题领域,而这点认识往往是在被坑过之后才知道的。</p>
<h2>Rails is not your application</h2>
<p>这部分提到的业务逻辑和 Rails 框架混杂,本质上还是没有对业务逻辑做足够的抽象,或者说对 OO 缺乏足够的理解才导致把 Rails 当做英语考试的完形填空去做。但现实中的业务逻辑是千变万化的,没有一个框架能完美地抽象出公共的模式。</p>
<p>其实一个引申的问题更值得思考,那就是框架到底应不应该提供架构决策?我目前的想法是框架也是系统架构的一部分,但不能完全代表整个系统的设计。这个话题也许会让人联想到 Domain-Driven Design 和 Uncle Bob 说的 Hexagonal Architecture ,或者 DCI 等模式。有任何想法欢迎探讨。</p>
<h2>YAGNI - You aren't gonna need it</h2>
<p>这一段的论点是 Rails 默认给你了太多的东西,但这就是所有全栈框架的思路,拿这点吐槽 Rails 也是不客观的。关于全栈框架和微框架的讨论已经很多了,就不多说了。</p>
<h2>Prefer composition over inheritance</h2>
<p>Ruby 的 mixin 到底算不算 composition 这点真不好说。我赞同作者文章下面的评论说的,这是 composition 本身定义太模糊了。Sandi Metz 在 Practical Object-Oriented Design in Ruby 里定义的概念更清晰:</p>
<blockquote><p>I think maybe what we're getting at here is "composition at the object graph level" rather than "composition at the object level"</p></blockquote>
<h2>总结</h2>
<p>作为一个干了 8 年的开发者,这些年也用过不少框架。我发现但凡红极一时的框架或类库,都有这么两个特性:</p>
<ul>
<li><p>优点足够明显,能在当时的世界引发一些新的思考,指引一些新的方向。</p></li>
<li><p>并非完美无缺,只是缺点都能通过一些方法来解决或绕过,换句话说,没有无法解决的致命缺陷。</p></li>
</ul>
<p>Rails ,AngularJS ,React 莫不如此。Rails 是有一些设计缺陷甚至架构上的误导,但也确实带来了一批新的理念,也许现在看起来都是老生常谈,但在 07 年那个时候却是一种颠覆。</p>
<p>另外,再先进的框架也无法取代架构设计,开发者仍然应该学习一些设计能力和原则,虽然有时候它们跟框架理念会有冲突,但更多是互补的。</p>
<p>So, does Rails hurt? 看到这里,我想你自己心里应该有了答案。</p>
Kata: 下一个更大的数字
https://segmentfault.com/a/1190000006873554
2016-09-10T23:51:58+08:00
2016-09-10T23:51:58+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
2
<h2>TL;DR</h2>
<p>这是一个 <a href="https://link.segmentfault.com/?enc=OYAuiKmomI4nFnqW%2Fo23LA%3D%3D.K1xV9ikqqdrO4AUUA36kxHezCEm4hhbCuvZDfZkZ3kT9oKwBv74Kdcu9%2Buhn%2BteZuutlumVZzafqbCN5%2BJotK50V7L3L%2BLqYwj85M2w%2FaQ03X6oI73Ah95q0af8KfObO" rel="nofollow">Coderwars</a> 上的练习,等级 4kyu 。Coderwars 上的题目分为 8 级,数字越小越难。这题算是中等难度。下面是我的分析和解法,语言使用的 JavaScript ,但你也可以用任何其他语言来实现。</p>
<h2>Kata 描述</h2>
<p>你需要提供一个函数,它接受一个正整数为参数并返回另一个正整数。返回值必须由输入的整数的每位数字构造而成,并且是最接近原整数的更大的数字。英文原文是 <strong>the next bigger number formed by the same digits</strong> 。如果这种数字不存在,函数返回 -1 。</p>
<p>听起来挺绕的,看看例子吧。下面的 <code>nextBigger</code> 就是要写的函数:</p>
<pre><code class="js">nextBigger(12) == 21
nextBigger(513) == 531
nextBigger(2017) == 2071
nextBigger(9) == -1
nextBigger(111) == -1
nextBigger(531) == -1</code></pre>
<p>拿 2017 举例子,比它更大的数有 2071, 2107, 2170, 2701 等等,但最接近 2017 的大数是 2071 ,这就是函数的返回值。再拿 531 举例子,不管怎么组合都无法形成比它更大的数,就返回 -1 。</p>
<h2>思路</h2>
<p>我觉得对任何 kata 题目而言,最重要的不是用什么技巧写代码,而是如何发现问题中的规律。这个问题也是如此。想知道如何解题,我们先想想 <strong>数字是怎么比大小</strong> 的。</p>
<p>数字比大小的规则很简单,大概描述起来如下:</p>
<ul>
<li><p>先比较位数,位数高的更大。</p></li>
<li><p>如果位数相同,则从第一位数字开始比较,数字更大的取胜。如果第一位数字相等,则比较第二位数字,以此类推直到末位数。</p></li>
</ul>
<p>对这个题目而言,构造出来的新数字位数跟原数字是一样的,所以只用考虑上面的第二条规则。加上题目的描述,我们就可以分析出 <strong>下一个更大数字</strong> 到底是什么意思:尽量只调整末位 x 位数获得满意的结果,并且 x 尽可能小。换句话说,能动最后两位数字的就别动最后三位。</p>
<p>那么怎么知道最少动最后几位数字能满足要求呢?这就得进一步分析下规律了。让我们回顾两个例子:</p>
<pre><code class="js">nextBigger(513) == 531
nextBigger(531) == -1
nextBigger(2531) == 3125</code></pre>
<p>第一个例子里,我们把 13 换成了 31 ,5 根本没必要动。第二个例子里完全没有可换的。第三个例子最有趣,我们把首位换成了 3 ,然后把其次三位数全部重排了,重排规律是从小到大,这样才能保证新数字是 "下一个更大" 的 。</p>
<p>规律得自己琢磨。我就说说结论。对于 <code>xyz</code> 这种数字,先分析一下最后两位 <code>yz</code> ,如果 <code>y < z</code> ,就只用换最后两位。如果 <code>y >= z</code> ,说明换两位不可行,所以只能考虑最后三位 <code>xyz</code> 。这时候如果 <code>x >= max(y, z)</code> ,则三位也不能换,以此类推。如果 <code>x < any(y, z)</code> ,则可以把 <code>y</code> 和 <code>z</code> 中比 <code>x</code> 大的最小的数拿出来,跟 <code>x</code> 互换位置,剩下的数按顺序排列,就组成下一个更大的数字了。</p>
<h2>解法</h2>
<p>按照上面的思路,我们可以梳理一下解法:</p>
<ul>
<li><p>取出最后两位数字,判断它能否达到要求(通过不同组合生成更大的数字)。如果无法生成更大的数字,换三位试试,以此类推,如果扫描到首位还没有结果,返回 -1 。</p></li>
<li><p>如果找到了符合要求的后 x 位数字,则把整个数字单独分割开来,前面的称为 <code>left</code> ,后面 x 位称为 <code>right</code> 。</p></li>
<li>
<p>对 <code>right</code> 重排,形成下一个更大的数字。重排规则如下:</p>
<ul>
<li><p>对 <code>right</code> 而言,找到比 <code>right[0]</code> 的下一个更大数字,把它作为新的 <code>right[0]</code> 。</p></li>
<li><p>剩下的数字升序排列,然后跟新的 <code>right[0]</code> 组合。</p></li>
</ul>
</li>
<li><p>组合 <code>left</code> 和 <code>right</code> 形成新的数字,这就是完整的 "下一个更大的数字" 。</p></li>
</ul>
<p>下面来实际编码,我用 JavaScript 实现的。这是主体的 <code>nextBigger</code> 函数:</p>
<pre><code class="js">function nextBigger(n) {
// 通过 splitDigits 分隔出 left 和 right 两部分
const [left, right] = splitDigits(`${n}`.split(''), 2)
if (!left) return -1
// 对 right 部分重新排列,再跟 left 组合成返回值
return Number(left.concat(resort(right)).join(''))
}
// 按照 rightSize 分割 digits 数组,如果不和规格,则按 rightSize+1 来递归分割
function splitDigits(digits, rightSize) {
if (rightSize > digits.length) return []
const right = digits.slice(-rightSize)
// 判断 right 是否符合要求
if (right[0] < right[1]) return [digits.slice(0, -rightSize), right]
return splitDigits(digits, rightSize + 1)
}
function resort(right) {
const first = right[0]
// 这里用 sort 和 reverse 都行
const rest = right.slice(1).sort()
// 找到下一个更大数字的索引
const idx = rest.findIndex(n => n > first)
const p = rest[idx]
rest[idx] = first
return [p].concat(rest)
}</code></pre>
<p>有点注意一下, <code>splitDigits</code> 函数里面判断 right 是否符合要求是用的 <code>right[0] < right[1]</code> ,其中道理可以自己想想。提醒一点,如果代码能走到这里,那么 <code>right[1]</code> 往后的所有数字只可能是 <strong>降序排列</strong> 的。</p>
<p>源代码和测试可以见我的 <a href="https://link.segmentfault.com/?enc=OZEySzNyBa55tSnZrHjpVA%3D%3D.A8JUrX%2BvdN7pFcRUQJK2xiJJTTyhx3%2F1LyOqj7IxHw7CxFQUGCm8dsFkMNuR2%2Fxobpi1OymcYAZZa%2Fq08XFx8A%3D%3D" rel="nofollow">GitHub</a> 。如果觉得文章对你有帮助,请帮我点个赞 :)</p>
<p>最后,你可以去 Coderwars 上自己看看 best practise 和 clever 的答案。我觉得这个 kata 的答案思路基本相同,而且 clever 的那个思路其实挺笨的,就没分析它们了。</p>
<h2>小结</h2>
<p>Kata 的乐趣在于思考和分析问题的规律,然后用合适的编程方式表达出来。这个过程可以有效锻炼逻辑思维和对语言的掌控力。Coderwars 上从低到高的 kata 挺多,主流语言也基本都支持,基本上想放松或想烧脑都能找到合适的选择。</p>
<h2>参考链接</h2>
<p><a href="https://link.segmentfault.com/?enc=SIYHvnSj7xeDzd4bdxyRAg%3D%3D.Wy03ZZrtby5DNSX7%2BzhisnP6KSEceylwkgytf29ATKEnlM8fc%2FgH2dpGzpha7thirr238Ss9gCrF0F4n1T8BfsJ3qSnvTNgjvgCTC4oZ1oE%3D" rel="nofollow">Kata: Next bigger number with the same digits</a><br><a href="https://link.segmentfault.com/?enc=F71pa2Gqz2IdHjJyyXsq6g%3D%3D.85OjNLaGEcZNakQE2SPKkDGR9R25yuuffA7ypEI8CV1o17urkpviSS6RAtbNgCdi8C9vgIxyr5MUg6BGd6Mo3CdYpHINYIfCmO3M8q0nksa0oDaNYGM32FqZQs3zMiVV" rel="nofollow">My solution on Coderwars</a><br><a href="https://link.segmentfault.com/?enc=P%2BKf9oLzry6KDjwKcYuOEQ%3D%3D.vIPfMu1KwC22AOEf0u34MFwctTpv0T1omLOqkY%2FuO7J8w5NN73Mp8ovY%2FenxMHg00gqlzXv45kpQZBUDVZ2%2F%2FA%3D%3D" rel="nofollow">My solution on GitHub</a></p>
React PureComponent 源码解析
https://segmentfault.com/a/1190000006741060
2016-08-28T16:28:33+08:00
2016-08-28T16:28:33+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
26
<h2>TL;DR</h2>
<p>React 15.3.0 新增了一个 <code>PureComponent</code> 类,以 ES2015 class 的方式方便地定义纯组件 (pure component)。这篇文章分析了一下源码实现,并衍生探讨了下 <code>shallowCompare</code> 和 <code>PureRenderMixin</code>。相关的 GitHub PR 在 <a href="https://link.segmentfault.com/?enc=N5NIyfPPiv0uiVpmj2BQGg%3D%3D.P6HCn%2Bdfe9fhG5pEU9Bg3lU0DJqtipNrtEJSsKVozrO3hRpsH09f9jQ%2Bx1mv%2BUeq" rel="nofollow">这里</a> 。</p>
<h2>PureComponent 源码分析</h2>
<p>这个类的用法很简单,如果你有些组件是纯组件,那么把继承类从 <code>Component</code> 换成 <code>PureComponent</code> 即可。当组件更新时,如果组件的 <code>props</code> 和 <code>state</code> 都没发生改变,render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。</p>
<pre><code class="js">import React, { PureComponent } from 'react'
class Example extends PureComponent {
render() {
// ...
}
}</code></pre>
<p><code>PureComponent</code> 自身的源码也很简单,节选如下:</p>
<pre><code class="js">function ReactPureComponent(props, context, updater) {
// Duplicated from ReactComponent.
this.props = props;
this.context = context;
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
ComponentDummy.prototype = ReactComponent.prototype;
ReactPureComponent.prototype = new ComponentDummy();
ReactPureComponent.prototype.constructor = ReactPureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(ReactPureComponent.prototype, ReactComponent.prototype);
ReactPureComponent.prototype.isPureReactComponent = true;</code></pre>
<p>上面的 <code>ReactPureComponent</code> 就是暴露给外部使用的 <code>PureComponent</code> 。可以看到它只是继承了 <code>ReactComponent</code> 再设定了一下 <code>isPureReactComponent</code> 属性。<code>ComponentDummy</code> 是典型的 JavaScript 原型模拟继承的做法,对此有疑惑的可以看 <a href="https://segmentfault.com/a/1190000003798438#articleHeader2">我的另一篇文章</a> 。另外,为了避免原型链拉长导致方法查找的性能开销,还用 <code>Object.assign</code> 把方法从 <code>ReactComponent</code> 拷贝过来了。</p>
<p>跟 <code>PureRenderMixin</code> 不一样的是,这里完全没有实现 <code>shouldComponentUpdate</code>。那 <code>PureComponent</code> 的 props/state 比对是在哪里做的呢?答案是 <code>ReactCompositeComponent</code>。</p>
<p><code>ReactCompositeComponent</code> 这个类的信息太少,我只能推测它是负责实际渲染并维护组件实例的对象。建议大家从高层次了解 React 对组件的更新机制即可。以下几篇官方文档看完就足够了。</p>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=G6o8QYiKnhfx%2BW9pfNw1IQ%3D%3D.OgNX%2FY3i3VmVfSrxCyZ23gr9FxPDOuPmJGP%2BtHqn3r83lDt8tG43pM%2B7TJ%2FDC8B4u7pooJGuYlCgKZnxgTnVRw%3D%3D" rel="nofollow">Advanced Performance</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=GizLHjE2W6Plkv1l8Pjx7A%3D%3D.h3yaST5joAUYsz2fDjuRTxmsN2KKSp2GiVSQjQyXPtJnEySc3clYZaNy7ugziWKKUpS6RwOj2h5kx59ft34XJw%3D%3D" rel="nofollow">Reconciliation</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=ahEb99VzSMgdqeZ3LJ6bAg%3D%3D.LwfVlJtzVf7Lz4mstlFhbLrvmOCnX8q8rhTKqsaSMPmKrZqVcmLPax3kT4bWFc%2FxQwKO5I9gW9RBMPdvAPvFxQ%3D%3D" rel="nofollow">React (Virtual) DOM Terminology</a></p></li>
</ul>
<p>这个类的代码改动很多,但关键就在 <a href="https://link.segmentfault.com/?enc=ssv7M0vUFGpyl1Mq1R%2FWcA%3D%3D.yYm6JeW9X2wREUMRZSxSDSiOrV3xUZeSJXT0MXBE8TI57JOZZ3KB8VqOEwfzovRxTCTmnZH%2BTsshsFkn%2Bu8VKKQDERQMraAEkWQbUGx5Dk%2F0O9uMTw%2FFcCRJbaVwxNft" rel="nofollow">这里</a> 。下面是我简化后的代码片段:</p>
<pre><code class="js">// 定义 CompositeTypes
var CompositeTypes = {
ImpureClass: 0, // 继承自 Component 的组件
PureClass: 1, // 继承自 PureComponent 的组件
StatelessFunctional: 2, // 函数组件
};
// 省略一堆代码,因为加入了 CompositeTypes 造成的调整
// 这个变量用来控制组件是否需要更新
var shouldUpdate = true;
// inst 是组件实例
if (inst.shouldComponentUpdate) {
shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
} else {
if (this._compositeType === CompositeType.PureClass) {
// 用 shallowEqual 对比 props 和 state 的改动
// 如果都没改变就不用更新
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState);
}
}</code></pre>
<p>简而言之,<code>ReactCompositeComponent</code> 会在 mount 的时候判断各个组件的类型,设定 <code>_compositeType</code> ,然后根据这个类型来判断是非需要更新组件。这个 PR 中大部分改动都是 因为加了 <code>CompositeTypes</code> 而做的调整性工作,实际跟 <code>PureComponent</code> 有关的就是 <code>shallowEqual</code> 的那两行。</p>
<p>关于 <code>PureComponent</code> 的源码分析就到这里。其他的就都是细节和测试,有兴趣的可以自己看看 PR 。</p>
<h2>shallowEqual, shallowCompare, PureRenderMixin 的联系</h2>
<p>我们知道在 <code>PureComponent</code> 出现之前,<code>shallowCompare</code> 和 <code>PureRenderMixin</code> 都可以做一样的事情。于是好奇看了一下后两者的代码。</p>
<p><code>shallowCompare</code> 的源码:</p>
<pre><code class="js">var shallowEqual = require('shallowEqual');
function shallowCompare(instance, nextProps, nextState) {
return (
!shallowEqual(instance.props, nextProps) ||
!shallowEqual(instance.state, nextState)
);
}
module.exports = shallowCompare;</code></pre>
<p><code>PureRenderMixin</code> 的源码:</p>
<pre><code class="js">var shallowCompare = require('shallowCompare');
var ReactComponentWithPureRenderMixin = {
shouldComponentUpdate: function(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
},
};
module.exports = ReactComponentWithPureRenderMixin;</code></pre>
<p>可以看到,<code>shallowCompare</code> 依赖 <code>shallowEqual</code> ,做的是跟刚才在 <code>ReactCompositeComponent</code> 里一样的事情 -- 对比 props 和 state 。这个工具函数一般配合组件的 <code>shouldComponentUpdate</code> 一起使用,而这就是 <code>PureRenderMixin</code> 做的事情。不过 <code>PureRenderMixin</code> 是配合 <code>React.createClass</code> 这种老的组件定义方式使用的,在 ES2015 class 里用起来不是很方便,这也是 <code>PureComponent</code> 诞生的原因之一。</p>
<p>最后 <code>shallowEqual</code> 这玩意定义在哪里呢?它其实不是 React 的一部分,而是 <a href="https://link.segmentfault.com/?enc=Z4ndxgHLbO98NQiHLXBZrg%3D%3D.yCuk%2B9R7DlQv99Vtq%2F6%2F6smzp%2F57YGojEqPPlsRfUkaw4iYfW7VpCmebRcprT1vwjsx3rYWRNUrz0vLsbOnaPzn3bXP61szmxGoDuV1lL2yS%2Bq2h8%2Bkqgj16aSW%2BIBz3" rel="nofollow">fbjs 的一部分</a>。这是 Facebook 内部使用的一个工具集。</p>
<h2>小结</h2>
<p>React 之前一直没有针对 ES2015 class 的纯组件写法,虽然自己实现起来并不麻烦,但这也算给出了一个官方的解决方案,可以不再依赖 addon 了。不过 <code>PureComponent</code> 也不是万能的,特定情况下自己实现 <code>shouldComponentUpdate</code> 可能更高效。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=74YggM6D94cFtXmYHl9RwA%3D%3D.e1qkSBa35TorfMNgQ8DkQ1gERtMa%2BubH1yKOWRzCJT5Qcarf0vwTFul9P4IhCdma" rel="nofollow">PureComponent PR</a><br><a href="https://link.segmentfault.com/?enc=tKJriaj%2BYpjpzSmbevSg5g%3D%3D.08vAxEISKYGRMAIhFVXELEyiiistx28pIaY7cUPz6n%2FBxe388oU01OzjPdQ3kTMlgDgpdDAg87xm5o4elr6jOct1gBgrQnDec3tN%2FVBZoF9zmODp4jy6EMphAs0JsDVC" rel="nofollow">shallowEqual</a><br><a href="https://link.segmentfault.com/?enc=2co5QVs18JIpHcaTa0xZgw%3D%3D.aFFhyuS3cC7WgcXSXejuCziQ1jVEKLTkxgxgMc7gM3NOjoQxevzf02WSrBSapcyDmhKrD08AiKyg%2FsaeHy47MA%3D%3D" rel="nofollow">Shallow Compare</a><br><a href="https://link.segmentfault.com/?enc=2MuKPJHNlIVnHVjMWdwNmw%3D%3D.urRijbzVusra68mvdyceJQX2Sg0hlx3RZruSSHQXJiJqOe64xDlP20AA3nuNRdCkLoYk7BO6OD%2Fu%2F2Kc6CsEzw%3D%3D" rel="nofollow">PureRenderMixin</a></p>
使用 Reek 检查 feature branch 相关文件的 code smell
https://segmentfault.com/a/1190000006709225
2016-08-24T22:50:59+08:00
2016-08-24T22:50:59+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
3
<h2>TL;DR</h2>
<p>最近为 Rails 项目加了一个代码分析工具叫 Reek ,用来检查代码中的坏味道。因为项目已经有一段时间了,一跑就几百个提示。平时也没工夫专门优化代码。于是我想到一个折中的办法:只检查 feature branch 中修改了的文件,针对性地优化。</p>
<h2>思路</h2>
<p>像大多数 CLI 一样,Reek 也可以接受额外的参数来检查指定的文件。大概如此:</p>
<pre><code class="bash">$ bundle exec reek file1 file2</code></pre>
<p>而同时,Git 的 <code>diff</code> 可以比对两个分支的改动,加一个参数 <code>--name-only</code> 就可以只输出改动的文件名。以下假定当前分支是 feature branch ,对比 staging 分支的情况。</p>
<pre><code class="bash"># assume current branch is feature branch
$ git diff --name-only staging
file1
file2
...</code></pre>
<p>我们可以写个 rake task ,通过 Ruby 调用 <code>git diff</code> 命令,把返回的文件名转换成一行,再传给 <code>reek</code> 命令。就可以达到目的。</p>
<h2>实现</h2>
<p>实现起来挺简单的,就只放代码了:</p>
<pre><code class="ruby"># lib/tasks/reek.rake
namespace :reek do
desc "Check code smells for changed files (based on staging)"
task :changed, [:branch] do |t, args| # 加一个 branch 参数
branch = args[:branch] || 'staging'
# 获取改变的文件名,剔除掉被删的文件
re = `git diff --name-only #{branch}`
files = re.split("\n").delete_if { |name| !File.exist?(name) }
if files.blank?
puts "\nNo files changed\n"
return
end
# 打印一下文件列表
puts "\nReek changed files:"
files.each do |file|
puts " #{file}"
end
puts
# 调用 reek
system "bundle exec reek #{files.join(' ')}"
end
end</code></pre>
<p>需要注意的有几点:</p>
<ul>
<li><p>这个 task 可以通过 branch 参数修改要比较的分支,方便查看。</p></li>
<li><p>调用 <code>git diff</code> 命令使用的是 "`" ,这样方便获取返回值。</p></li>
<li><p>调用 <code>bundle exec reek</code> 得用 <code>system</code> ,因为需要把输出显示在屏幕上。</p></li>
</ul>
<p>使用起来很简单:</p>
<pre><code class="bash"># 默认情况,对比 staging 和当前分支
./bin/rake reek:changed
# 对比 master 分支,换成 commit 也行
./bin/rake reek:changed[master]</code></pre>
<h2>小结</h2>
<p>代码分析工具是好东西,可以潜默移化地改变人的编程习惯,即使对很有经验的程序员也能起到查漏补缺的作用。但世事无绝对,分析规则是否跟项目匹配,工作流程如何改进,这些都是因人而异的,需要不停地思考和总结。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=1nsq19n5jvclPRZpwMh87w%3D%3D.P5a2r64edJOMESzUK60kXDcRqBQUm%2FKyx1VXfLMPQRwOTBkOVSHAMu6sbDdeI75C" rel="nofollow">Reek</a><br><a href="https://link.segmentfault.com/?enc=9GcbgzACFRgAT2XaRtNzMA%3D%3D.EapxnxFVQuUzbW7vAv94hWvUoauY4mnatynSvMnP6o3yNGxkRyqGpiEH12UR0vyZyC%2BGbidwoLtY%2BrNfXni6Ea3A57y8VcukysadoIOOk5XHeX5jS8fzVSQ8FB8%2FcFIyAO4vUSe0kHEiCpgNtGwazqj7t0mCRw1ktITlMHSiqCo%3D" rel="nofollow">Can I make git diff only show the changed file names and line numbers?</a></p>
Cordova 打包 Android release app 过程详解
https://segmentfault.com/a/1190000005177715
2016-05-22T21:30:59+08:00
2016-05-22T21:30:59+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
12
<h2>TL;DR</h2>
<p>Android app 的打包分为 debug 和 release 两种,后者是用来发布到应用商店的版本。这篇文章会告诉你 Cordova 如何打包 Android release 版本,也会让你了解 Android app 的打包流程。</p>
<h2>创建一个 demo app</h2>
<p>为了演示,首先我们需要创建一个 Cordova 项目的基本步骤。如果你已经对此很熟悉,可以跳过这一步。</p>
<p>先全局安装 Cordova CLI :</p>
<pre><code class="bash">npm install -g cordova</code></pre>
<p>在 <code>cordova-demo</code> 目录创建一个项目,ID 为 <code>com.example.cordovaDemo</code> ,项目名为 <code>cordovaDemo</code> 。</p>
<pre><code class="bash">cordova create cordova-demo com.example.cordovaDemo cordovaDemo</code></pre>
<p>加上 Android 平台,这会下载一个 Android 项目的框架,并把版本信息保存到 <code>config.xml</code> 。你可以去 <code>platforms/android</code> 目录下查看它。</p>
<pre><code class="bash">cordova platform add android --save</code></pre>
<p>你可以检查下平台需求是否满足。基本上 Cordova 需要你把 Java SDK, Android SDK 和 Gradle 都配置好。</p>
<pre><code class="bash">cordova requirements android</code></pre>
<p>现在一个 Cordova 项目就已经准备好了。你可以尝试构建一个版本。一切顺利的话,你会在 <code>platforms/android/build/outputs/apk</code> 目录下看到 APK 文件。这个目录后面会经常用到,为了方便我们建立一个符号链接 <code>android-apk</code> 。</p>
<pre><code class="bash"># 构建 apk
cordova build android
# 建立符号链接 android-apk
ln -s platforms/android/build/outputs/apk android-apk
# 查看一下这个目录,你应该会看到 android-debug-unsigned.apk
ls android-apk</code></pre>
<p>搞定!但这个构建的 APK 是 debug 版本的。要构建 release 版本,我们需要先了解一下 Android 手动打包的流程。</p>
<h2>Android APK 手动打包流程</h2>
<p>Android app 的打包流程大致分为 <strong>build</strong> , <strong>sign</strong> , <strong>align</strong> 三部分。</p>
<p><strong>build</strong> 是构建 APK 的过程,分为 debug 和 release 两种。release 是发布到应用商店的版本。</p>
<p><strong>sign</strong> 是为 APK 签名。不管是哪一种 APK 都必须经过数字签名后才能安装到设备上,签名需要对应的证书(keystore),大部分情况下 APK 都采用的自签名证书,就是自己生成证书然后给应用签名。</p>
<p><strong>align</strong> 是压缩和优化的步骤,优化后会减少 app 运行时的内存开销。</p>
<p>debug 版本的的打包过程一般由开发工具(比如 Android Studio)自动完成的。开发工具在构建时会自动生成证书然后签名,不需要我们操心。而 release 版本则需要开发者自己生成证书文件。Cordova 作为 hybrid app 的框架不像纯 Android 开发那么自动化,所以第一次打 release 包我们需要了解一下手动打包的过程。</p>
<h3>Build</h3>
<p>首先,我们生成一个 release APK 。这点在 <code>cordova build</code> 命令后加一个 <code>--release</code> 参数局可以。如果成功,你可以在 <code>android-apk</code> 目录下看到一个 <code>android-release-unsigned.apk</code> 文件。</p>
<pre><code class="bash">cordova build android --release</code></pre>
<h3>Sign</h3>
<p>我们需要先生成一个数字签名文件(keystore)。这个文件只需要生成一次。以后每次 sign 都用它。</p>
<pre><code class="bash">keytool -genkey -v -keystore release-key.keystore -alias cordova-demo -keyalg RSA -keysize 2048 -validity 10000</code></pre>
<p>上面的命令意思是,生成一个 release-key.keystore 的文件,别名(alias)为 cordova-demo 。<br>过程中会要求设置 keystore 的密码和 key 的密码。我们分别设置为 <code>testing</code> 和 <code>testing2</code>。这四个属性要记牢,下一步有用。</p>
<p>然后我们就可以用下面的命令对 APK 签名了:</p>
<pre><code class="bash">jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore release-key.keystore android-apk/android-release-unsigned.apk cordova-demo</code></pre>
<p>这个命令中需要传入证书名 <code>release-key.keystore</code>,要签名的 APK <code>android-release-unsigned.apk</code>,和别名 <code>cordova-demo</code>。签名过程中需要先后输入 keystore 和 key 的密码。命令运行完后,这个 APK 就已经改变了。注意这个过程没有生成新文件。</p>
<h3>Align</h3>
<p>最后我们要用 <code>zipalign</code> 压缩和优化 APK :</p>
<pre><code class="bash">zipalign -v 4 android-apk/android-release-unsigned.apk android-apk/cordova-demo.apk</code></pre>
<p>这一步会生成最终的 APK,我们把它命名为 <code>cordova-demo.apk</code> 。它就是可以直接上传到应用商店的版本。</p>
<h2>自动打包</h2>
<p>一旦有了 keystore 文件,下次打包就可以很快了。你可以在 <code>cordova build</code> 中指定所有参数来快速打包。这会直接生成一个 <code>android-release.apk</code> 给你。</p>
<pre><code class="bash">cordova build android --release -- --keystore="release-key.keystore" --alias=cordova-demo --storePassword=testing --password=testing2</code></pre>
<p>但每次输入命令行参数是很重复的,Cordova 允许我们建立一个 <code>build.json</code> 配置文件来简化操作。文件内容如下:</p>
<pre><code class="json">{
"android": {
"release": {
"keystore": "release-key.keystore",
"alias": "cordova-demo",
"storePassword": "testing",
"password": "testing2"
}
}
}</code></pre>
<p>下次就可以直接用 <code>cordova build --release</code> 了。</p>
<p>为了安全性考虑,建议不要把密码放在在配置文件或者命令行中,而是手动输入。你可以把密码相关的配置去掉,下次 build 过程中会弹出一个 Java 小窗口,提示你输入密码。</p>
<h2>用 Gradle 配置自动打包</h2>
<p>另一种配置方法是使用 Gradle ,一个 Android 的自动化构建工具。<code>cordova build android</code> 的过程其实就是使用它。你要在 <code>platforms/android</code> 目录下建立 <code>release-signing.properties</code> 文件,内容类似下面这样:</p>
<pre><code>storeFile=relative/path/to/keystore
storePassword=SECRET1
keyAlias=ALIAS_NAME
keyPassword=SECRET2</code></pre>
<p>这个文件的名称和位置也是可以通过 Gradle 的配置 <code>cdvReleaseSigningPropertiesFile</code> 修改的。我觉得一般情况使用 <code>build.json</code> 就足够了。有兴趣的可以看这个 <a href="https://link.segmentfault.com/?enc=yLblitMrEWChSl2flQX7yQ%3D%3D.nN%2BezWPtxbPsU8isy9UMJi89QGLP%2FDUZ0WfVnbYZqHJGJrcNH7ekdOAvohJKr%2FvRSYJoO4vu4d9tP8oKR98%2Fc%2BEgVoUFaXss%2BuTzpn3T96Kbt71%2FbeYbAIUUVv8wezcDn6iDeeENO24J89uXNr8EBQ%3D%3D" rel="nofollow">Cordova 官方教程</a></p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=k6gZnNINGvALvLBLyIKQzg%3D%3D.Kprtl%2BNPZ81Ysbplc%2FgD70sr4AdXill8l5vlf56aO0NOXiTBWIN3fLPukbN%2FmT1g%2FVS0UAwuh5GAcBkvJD1kzQ%3D%3D" rel="nofollow">Ionic: Publishing your app</a><br><a href="https://link.segmentfault.com/?enc=TC4%2FLxgsKe2fC8fncOBl7A%3D%3D.flhYxgN5mrDF44mo0vpB3sIzWQ9rl5HkiOTsZToAd3OoxmOM4znsnzFsq4a%2FNjYOAtdXfolwZhcnIZH0KML1YQ%3D%3D" rel="nofollow">Android Studio: Sign Your App</a><br><a href="https://link.segmentfault.com/?enc=khBIDDERQ%2BZiYKpOwxde6w%3D%3D.v40WsGo4t2u6ZeGLKIVf6qYyKXpqwlOGzd3Fn4OW2Rht%2Bcwa9recJ%2BwfLbI2vD90w3jy8Z%2FcwH1JneGqatxp7zMYrNJxy%2BlrGKL3X9zKgUE%3D" rel="nofollow">Cordova: Android Platform Guide</a><br><a href="https://link.segmentfault.com/?enc=EVAJdsCZwAUsb%2BkD%2Fk%2Frhg%3D%3D.qikKlwjJezLBkEHWyuExTb9PGtGpM5KYOIBwjP4SnftRn4yfW9hNG8CTZbczOxDlFi1AkFJ7yJ0N0qPkZDqhI6M73MBLaF512%2FGmzznhE0BPpmPjzEQoeV7ObyXlUbFPAG3SI8azv0t2NwVF0FwLBYWmpSEHVAkkOKe0YHswjU0%3D" rel="nofollow">How to automatically sign your Android apk using Ionic framework and Crosswalk</a></p>
Ruby String/Integer/Array 的一些不常用方法
https://segmentfault.com/a/1190000004910322
2016-04-10T17:55:36+08:00
2016-04-10T17:55:36+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>TL;DR</h2>
<p>仅用于个人整理,对他人无甚帮助。有段时间练习算法,我记录了一些数据操作的方法,它们都不太常用,所以单独写篇博客保存。</p>
<h2>String</h2>
<h3>bytes</h3>
<p>返回 byte 数组,适合获取 codepoint 。</p>
<h3>center</h3>
<p>把 str 居中,两边填充 padstr ,默认为空格。</p>
<h3>chars</h3>
<p>返回 character 数组。</p>
<h3>codepoints</h3>
<p>返回 codepoint 数组。</p>
<pre><code class="ruby">"a".codepoints[0] # => 97</code></pre>
<h2>Integer</h2>
<h3>chr</h3>
<p>codepoint 转换 character 。</p>
<pre><code class="ruby">97.chr # => "a"</code></pre>
<h3>even?, odd?</h3>
<p>判断奇数和偶数。</p>
<h3>integer?</h3>
<p>判断是否整数。</p>
<h3>divmod</h3>
<p>做除法,同时返回除数和余数。有时候比分开使用 <code>/</code> 和 <code>%</code> 要方便。</p>
<pre><code class="ruby">1234 / 1000 # => 1
1234 % 1000 # => 234
1234.divmod 1000 # => [1, 234]</code></pre>
<h3>两个乘号(因为 markdown 原因打不出来)</h3>
<p>平方和开方。</p>
<pre><code class="ruby">4 ** 2 # => 8
4 ** (1.0/2) # => 2</code></pre>
<h3>abs</h3>
<p>求绝对值(始终正数)。</p>
<h3>fdiv</h3>
<p>除法,返回 float 。类似 <code>a.to_f / b</code> 。</p>
<pre><code class="ruby">10.fdiv 4 # => 2.5</code></pre>
<h2>Array</h2>
<h3>with_index</h3>
<p>迭代数组并返回每个元素和 index 。这其实是迭代器 <code>Enumerator</code> 的方法。适用于所有迭代中需要 index 的情况,比如 <code>map</code> 和 <code>reverse_each</code> 。</p>
<pre><code class="ruby">%w[a b c].reverse_each.with_index { |char, i| puts i }</code></pre>
<h3>sample</h3>
<p>随机选择一个数组元素并返回。适合做随机数。</p>
<h3>pop(x)</h3>
<p>pop x 个元素。如果数组元素数量少于 x,就返回最多能返回的元素。这点比用 <code>[range]</code> 要好。</p>
<pre><code class="ruby">%w[a b c].pop(4) # => ["a", "b", "c"]
%w[a b c][-4..-1] # => nil</code></pre>
<h3>& 和 |</h3>
<p>取两个数组的交集和并集。</p>
Angular 根据 service 的状态更新 directive
https://segmentfault.com/a/1190000004845354
2016-04-01T17:47:44+08:00
2016-04-01T17:47:44+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
0
<h2>TL;DR</h2>
<p>这篇文章讲解了三种根据 service 的状态更新 directive 的做法。分别是 <code>$watch</code> 表达式,事件传递,和 controller 的计算属性。</p>
<h2>问题</h2>
<p>我有一个 <code>readerService</code> ,其中包含一些状态信息(比如连接状态和电量)。现在我需要做一个 directive 去展示这些状态。因为它只需要从 <code>readerService</code> 中获取数据,不需要任何外部传值,所以我直接把 service 注入进去。但如何更新就成了一个问题。</p>
<p>service 的代码如下。</p>
<pre><code class="js">const STATUS = {
DETACH: 'DETACH',
ATTACH: 'ATTACH',
READY: 'READY'
}
class ReaderService {
constructor() {
this.STATUS = STATUS
// The status will be changed by some callbacks
this.status = STATUS.DETACH
}
}
angular.module('app').service('readerService', readerService)</code></pre>
<p>directive 代码如下:</p>
<pre><code class="js">angular.module('app').directive('readerIndicator', (readerService) => {
const STATUS = readerService.STATUS
const STATUS_DISPLAY = {
[STATUS.DETACH]: 'Disconnected',
[STATUS.ATTACH]: 'Connecting...',
[STATUS.READY]: 'Connected',
}
return {
restrict: 'E',
scope: {},
template: `
<div class="status">
{{statusDisplay}}
</div>
`,
link(scope) {
// Set and change scope.statusDisplay here
}
}
})</code></pre>
<p>我尝试过以下几种办法,下面一一介绍。</p>
<h2>方法一:$watch</h2>
<p>第一个想到的方法就是在 directive 中用 <code>$watch</code> 去监视 <code>readerService.status</code>。因为它不是 directive scope 的属性,所以我们需要用一个函数来包裹它。Angular 会在 dirty-checking 时计算和比较新旧值,只有状态真的发生了改变才会触发回调。</p>
<pre><code class="js">// In directive
link(scope) {
scope.$watch(() => readerService.status, (status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
})
}</code></pre>
<p>这个做法足够简单高效,只要涉及 <code>readerService.status</code> 改变的代码会触发 dirty-checking ,directive 就会自动更新。service 不需要修改任何代码。</p>
<p>但如果有多个 directive 的属性都受 service status 的影响,那 <code>$watch</code> 代码就看得比较晦涩了。尤其是 <code>$watch</code> 修改的值会影响其他的值的时候。比如:</p>
<pre><code class="js">// In directive
link(scope) {
scope.$watch(() => readerService.status, (status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
scope.showBattery = status !== STATUS.DETACH
})
scope.$watch('showBattery', () => {
// some other things depend on showBattery
})
}</code></pre>
<p>这种时候声明式的编程风格会更容易看懂,比如 Ember 或 Vue 里面的 computed property 。这个待会讨论。</p>
<h2>方法二:$broadcast/$emit + $on</h2>
<p>这种思路是 service 每次状态改变都发送一个事件,然后 directive 监听事件来改变状态。因为 directive 渲染的时候也许 status 已经更新了。所以我们需要在 <code>link</code> 中计算一个初始值。</p>
<p>我最开始是用 <code>$broadcast</code> 去做的。代码如下:</p>
<pre><code class="js">// In service
setStatus(value) {
this.status = value
// Need to inject $rootScope
this.$rootScope.$broadcast('reader.statusChanged', this.status)
}
// In directive
link(scope) {
scope.statusDisplay = STATUS_DISPLAY[nfcReaderService.status]
scope.$on('reader.statusChanged', (event, status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
})
}</code></pre>
<p>但马上发现 <code>$broadcast</code> 之后 UI 更新总要等 1 秒多(不过 <code>$on</code> 回调倒是很快)。Google 一番后知道原因是 <code>$broadcast</code> 是向下层所有 scope 广播,广播完成后再 dirty-checking 。一个更好的做法是使用 <code>$emit</code> ,它只会向上传递事件,不过不管发送事件还是监听事件都得用 <code>$rootScope</code> 。</p>
<p>修改后的代码如下:</p>
<pre><code class="js">// In service
setStatus(value) {
this.status = value
// Use $emit instead of $broadcast
this.$rootScope.$emit('reader.statusChanged', this.status)
}
// In directive
link(scope) {
scope.statusDisplay = STATUS_DISPLAY[nfcReaderService.status]
// Use $rootScope instead of scope
$rootScope.$on('reader.statusChanged', (event, status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
})
}</code></pre>
<p>如果因为某些原因不得不用 <code>$broadcast</code> 的话,你可以在 <code>$on</code> 回调最后用 <code>$digest</code> 或 <code>$apply</code> 强制触发 dirty-checking ,这也可以达到快速更新 UI 的目的。</p>
<h2>方法三:controller + property</h2>
<p>我个人觉得前两个方法能解决问题,但代码维护性都不太好。<code>$watch</code> 在属性相互关联的情况下非常难看懂,<code>$emit/$on</code> 需要把一些逻辑写两次(初始化 directive 时和回调执行时)。方法一中我提到了有些时候声明式的属性比 <code>$watch</code> 更容易看懂。这个方法就是使用 controller 。directive 可以设置自己的 controller 作为数据来源(或者说 view model),我们可以把那些需要计算的属性作为 controller 的属性。这样 dirty-checking 时它们就会自动计算。</p>
<pre><code class="js">// In directive
class ReaderController {
constructor($scope, readerService) {
this.readerService = readerService
}
get statusDisplay() {
return STATUS_DISPLAY[this.readerService.status]
}
}
return {
// ...
controller: ReaderController,
controllerAs: 'vm',
template: `
<div class="status">
{{vm.statusDisplay}}
</div>
`
}</code></pre>
<p>这样一来,大部分逻辑都可以挪到 controller 中。如果没有 DOM 操作我们甚至可以不写 <code>link</code> 方法。也没必要加入额外的 <code>$watch</code> 和 <code>$on</code> 。只是因为 dirty-checking 的特性,绑定到 template 的属性往往会多计算几次。所以属性必须非常简单。大部分情况下这不会有什么问题。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=8L8MVkZZ2wlLb%2B3a3MGiMg%3D%3D.%2FwHTUJXYS6e8i3Ae51cwEbD2tFrupyaztZeWxdUgCxBHb56RHUNUw6r9P%2Fki5vcDJ%2Fyz8Cl7C9R7OQpFxneoOw%3D%3D" rel="nofollow">$rootScope.Scope</a><br>Angular API ,可以看看里面对 <code>$watch</code> ,<code>$broadcast</code> ,<code>$emit</code> , <code>$on</code> 的描述。</p>
<p><a href="https://link.segmentfault.com/?enc=wYsYsrW27znSVAPLPqtWZw%3D%3D.eufF6aEvYQSgF9UfsYFsULwjhXUtxvXv993aYHCiVbaIeq%2B0BzWfiL2xRheMLWDAw100hHdw33FqlBFk1laKyw%3D%3D" rel="nofollow">$rootScope.$emit() vs $rootScope.$broadcast()</a><br><code>$emit</code> 和 <code>$broadcast</code> 的性能比较。注意后来的 Angular 已经解决了性能差异,两者相差无几。</p>
Angular $rootScope:inprog 问题探究
https://segmentfault.com/a/1190000004823495
2016-03-31T16:35:07+08:00
2016-03-31T16:35:07+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
3
<h2>TL;DR</h2>
<p>这是一个关于 <code>$rootScope:inprog</code> 错误在什么样的情况下被触发,和如何解决的故事。</p>
<h2>场景和问题</h2>
<p>这几天在写一个 service 。这个 service 中有个状态需要注入到 directive 中做页面展现。因为状态的改变由另一个插件控制,不在 Angular 的 event loop 中。为了触发 dirty-checking 我在 service 中调用了 <code>$rootScope.$digest()</code> 。</p>
<p>service 代码大概如下所示:</p>
<pre><code class="js">const STATUS = {
A: 'A',
B: 'B',
}
class SomeService {
constructor($rootScope) {
this.$rootScope = $rootScope
}
start() {
this.plugin = initPlugin
// Register plugin callbacks
this.plugin.onStateA = () => { this._setStatus(STATUS.A) }
this.plugin.onStateB = () => { this._setStatus(STATUS.B) }
}
_setStatus(status) {
this.status = status
this.$rootScope.$digest()
}
}
angular.module('app.someMod').service('someService', SomeService)</code></pre>
<p>目前为止一切正常,直到因为需求改动,需要加一个状态,这个状态的改变是通过 directive 中的按钮触发的,于是我在 service 中加了一个方法,在 directive 中调用,代码如下:</p>
<pre><code class="js">// In service
class someService {
connect() {
this._setStatus(STATUS.C)
}
}
// In directive, the "btnClick" is bound to an element's ng-click
scope.btnClick = () => {
someService.connect()
}</code></pre>
<p>然后一点击按钮,程序就跪了…… 控制台中报的错误是 <a href="https://link.segmentfault.com/?enc=B2%2FB5HHXyEoN3%2FSSCzhh4w%3D%3D.L%2FWTYoDhDWDA1ATOyegVeCVFGT1C15uQZhDHaist26%2FtOt4shWkxVPRAuELQiA52v5nc92CdNC5t9L%2FqV3C1oht8SFZlDmKbrneTlyvBJfE%3D" rel="nofollow">$rootScope:inprog</a> 。</p>
<h2>解决方法</h2>
<p>这段错误的官方描述如下:</p>
<blockquote><p>At any point in time there can be only one $digest or $apply operation in progress. This is to prevent very hard to detect bugs from entering your application. The stack trace of this error allows you to trace the origin of the currently executing $apply or $digest call, which caused the error.</p></blockquote>
<p>简单来说,<code>$digest</code> 和 <code>$apply</code> 是用来触发 dirty-checking 的方法。前者强制触发一次 dirty-checking ,后者让一段代码执行完成后触发 dirty-checking 。但是 Angular 一次只允许一个 <code>$digest</code> 或者 <code>$apply</code> 运行。上面例子里的代码会挂,是因为 <code>scope.btnClick</code> 本身已经在 $apply 中执行了,但 <code>someService.connect</code> 内部通过 <code>_setStatus</code> 又调用了一次 <code>$digest</code> ,这就触发了两次。</p>
<p>这让我反思为什么要手动调用 <code>$digest</code> ?其实我的目的只是确保所有状态改变都触发 dirty-checking 。因为这个 service 中哪些代码不会触发 dirty-checking 是很明确的,那就是插件回调。所以直接在回调中使用 <code>$apply</code> 就可以解决问题。</p>
<p>修改后的代码如下:</p>
<pre><code class="js">// In service
start() {
// Wrap code in $apply
this.plugin.onStateA = () => { this._wrapStatusChange(STATUS.A) }
this.plugin.onStateB = () => { this._wrapStatusChange(STATUS.B) }
}
connect() {
// Change status directly
this.status = STATUS.C
}
_wrapStatusChange(status) {
this.$rootScope.$apply(() => {
this.status = status
})
}</code></pre>
<h2>总结</h2>
<p>只在必要的时候使用 <code>$apply</code> 处理那些不会触发 dirty-checking 的代码。大部分的时候 <code>$digest</code> 都可以被 <code>$apply</code> 取代。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=I1n4ErnvCbDkQktlspAVjw%3D%3D.h3JJU1tRJYEcfADT%2Bh5vkvG5gnehhxqU5JCMuDarpkchf%2BV1DdpisbJTUczkaj8zhoky7c%2FD9xXtUuOUDic1jfKj%2F0LtXz0YdlRq2FH%2BmA8%3D" rel="nofollow">$rootScope:inprog</a><br>Angular 对异常的描述。这种异常附带在线文档的方式还是很方便的。顺带一提 React 的异常信息也是这样。</p>
<p><a href="https://link.segmentfault.com/?enc=pkmHKuJu9n0YtC2TSWpklA%3D%3D.ETJivQLRfmWf%2FSbqx6g6aXYyBa9kO%2BC7lnJdQxdqRIyCtfkVfaPqoU59PBPHd5uWD9bxRIpqyHTQDXGEa0REfA%3D%3D" rel="nofollow">$rootScope.Scope</a><br>Scope 的 API ,里面可以查到 $digest 和 $apply 的详细解释。</p>
JavaScript ASI 机制详解
https://segmentfault.com/a/1190000004548664
2016-03-06T12:21:52+08:00
2016-03-06T12:21:52+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
26
<h2>TL;DR</h2>
<p>最近在清理 Pocket 的未读列表,看到了 <a href="https://link.segmentfault.com/?enc=l0YI1DPIFh15KdHPhPUoXA%3D%3D.TQZBHtgRdfkhlgSqFs%2Fpai8AhZ1M%2FJ2anzEGo88ygN%2BGInGTXRRdBcwtboRgC9LMdcxwkB2C1gysEU0K%2Be83IidTxUIrU1opVt4CkUN%2F67%2FvqGmxC87iq3a%2FzTGguo69" rel="nofollow">An Open Letter to JavaScript Leaders Regarding Semicolons</a> 才知道了 JavaScript 的 ASI,一种自动插入分号的机制。因为我是 “省略分号风格” 的支持者,之前也碰到过一次因为忽略分号产生的问题,所以对此比较重视,也特意多看了几份文档,但越看心里越模糊。并不是我记不住 <code>( 和 [ 前面记得加 ;</code> 这种结论,而是觉得看过的几篇文章跟 ECMAScript 标准描述的有点区别。直到最近反复琢磨才突然有了 “原来如此” 的想法,于是就有了此文。</p>
<p>这篇文章会用 ECMAScript 标准的 ASI 定义来解释它到底是如何运作的,我会尽量用平易近人的方法描述它,避免官方文档的晦涩。希望你跟我一样有收获。掌握 ASI 并不能够让你马上解决手头的问题,但能让你成为一个更好的 JavaScript 程序员。</p>
<h2>什么是 ASI</h2>
<p>按照 ECMAScript 标准,一些 <strong>特定语句</strong>(statement) 必须以分号结尾。分号代表这段语句的终止。但是有时候为了方便,这些分号是有可以省略的。这种情况下解释器会自己判断语句该在哪里终止。这种行为被叫做 “自动插入分号”,简称 ASI (Automatic Semicolon Insertion) 。实际上分号并没有真的被插入,这只是个便于解释的形象说法。</p>
<p>这些特定的语句有:</p>
<ul>
<li><p>空语句</p></li>
<li><p><code>let</code></p></li>
<li><p><code>const</code></p></li>
<li><p><code>import</code></p></li>
<li><p><code>export</code></p></li>
<li><p>变量赋值</p></li>
<li><p>表达式</p></li>
<li><p><code>debugger</code></p></li>
<li><p><code>continue</code></p></li>
<li><p><code>break</code></p></li>
<li><p><code>return</code></p></li>
<li><p><code>throw</code></p></li>
</ul>
<p>下面这段是我 <strong>个人的理解</strong>,上的定义同时也表示:</p>
<ol>
<li><p>所有这些语句中的分号都是可以省略的。</p></li>
<li><p>除此之外其他的语句有两种情况,一是不需要分号的(比如 <code>if</code> 和函数定义),二是分号不能省略的(比如 <code>for</code>),稍后会详细介绍。</p></li>
</ol>
<p>那么 ASI 如何知道在哪里插入分号呢?它会按照一些规则去判断。但在说规则之前,我们先了解一下 JS 是如何解析代码的。</p>
<h2>Token</h2>
<p>解析器在解析代码时,会把代码分成很多 token 。一个 token 相当于一小段有特定意义的语法片段。看一个例子你就会明白:</p>
<pre><code class="js">var a = 12;</code></pre>
<p>上面这段代码可以分成四个 token :</p>
<ol>
<li><p><code>var</code> 关键字</p></li>
<li><p><code>a</code> 标识符</p></li>
<li><p><code>=</code> 运算符</p></li>
<li><p><code>12</code> 数字</p></li>
</ol>
<p>除此之外,<code>(</code>,<code>.</code> 等都算 token ,这里只是让你有个大概的概念,比如 <code>12</code> 整个是一个 token ,而不是 <code>1</code> 和 <code>2</code>。字符串同理。</p>
<p>解释器在解析语句时会一个一个读入 token 尝试构成一个完整的语句 (statement),直到碰到特定情况(比如语法规定的终止)才会认为这个语句结束了。记得上文提到的 <strong>变量赋值</strong> 这个语句必须以分号结尾么?这个例子中的终止符就是分号。用 token 构成语句的过程类似于正则里的贪婪匹配,解释器总是试图用尽可能多的 token 构成语句。</p>
<p>接下来是重点:任意 token 之间都可以插入一个或多个换行符 (Line Terminator) ,这完全不影响 JS 的解析,所以上面的代码可以写成下面这样(功能等价):</p>
<pre><code class="js">var
a
=
// = 和 12 之间有两个换行符
12
;</code></pre>
<p>这个特性可以让开发者通过增加代码的可读性,更灵活地组织语言风格。我们平时写的跨多行的数组,字符串拼接,和链式调用都属于这一类。不过在省略分号的风格中,这种解析特性会导致一些意外情况。</p>
<p>比如这个例子中,以 <code>/</code> 开头的正则会被理解成除法:</p>
<pre><code class="js">var a
, b = 12
, hi = 2
, g = {exec: function() { return 3 }}
a = b
/hi/g.exec('hi')
console.log(a)
// 打印出 2, 因为代码会被解析成:
// a = b / hi / g.exec('hi');
// a = 12 / 2 / 3</code></pre>
<p>事实上这并不是省略分号的风格的错误,而是开发者没有理解 JS 解释器的工作原理。如果你倾向省略分号的风格,那了解 ASI 是必修课。</p>
<h2>ASI 规则</h2>
<p>ECMAScript 标准定义的 ASI 包括 <strong>三条规则</strong> 和 <strong>两条例外</strong>。</p>
<p>三条规则是描述何时该自动插入分号:</p>
<ol>
<li>
<p>解析器从左往右解析代码(读入 token),当碰到一个不能构成合法语句的 token 时,它会在以下几种情况中在该 token 之前插入分号,此时这个不合群的 token 被称为 offending token :</p>
<ul>
<li><p>如果这个 token 跟上一个 token 之间有至少一个换行。</p></li>
<li><p>如果这个 token 是 <code>}</code>。</p></li>
<li><p>如果 <strong>前一个</strong> token 是 <code>)</code>,它会试图把前面的 token 理解成 <code>do...while</code> 语句并插入分号。</p></li>
</ul>
</li>
<li><p>当解析到文件末尾发现语法还是有问题,就会在文件末尾插入分号。</p></li>
<li><p>当解析时碰到 restricted production 的语法(比如 <code>return</code>),并且在 restricted production 规定的 <code>[no LineTerminator here]</code> 的地方发现换行,那么换行的地方就会被插入分号。</p></li>
</ol>
<p>两条例外表示,就算符合上述规则,如果分号会被解析成下面的样子,它也不能被自动插入:</p>
<ol>
<li><p>分号不能被解析成空语句。</p></li>
<li><p>分号不能被解析成 <code>for</code> 语句头部的两个分号之一。</p></li>
</ol>
<p>你会发现这些规则相当晦涩,好像存心考你智商的,还有些坑爹的专有名词。不要紧,我们来看几个非常简单的例子,看完之后你就会明白所有这些东西的含义。</p>
<h2>例子解析</h2>
<h3>第一个例子:换行</h3>
<pre><code class="js">a
b</code></pre>
<p>我们模拟一下解析器的思考过程,大概是这样的:解析器一个个读取 token ,但读到第二个 token <code>b</code> 时它就发现没法构成合法的语句,然后它发现 <code>b</code> 和前面是有换行的,于是按照规则一(情况一),它在 <code>b</code> 之前插入分号变成 <code>a\n;b</code>,这样语句就合法了。然后继续处理,这时读到文件末了,<code>b</code> 还是不能构成合法的语句,这时候按照规则二,它在末尾插入分号,结束。最终结果是:</p>
<pre><code class="js">a
;b;</code></pre>
<h3>第二个例子:大括号</h3>
<pre><code class="js">{ a } b</code></pre>
<p>解析器仍然一个个读取 token ,读到 token <code>}</code> 时发现 <code>{ a }</code> 是不合法的,因为 <code>a</code> 是表达式,它必须以分号结尾。但当前 token 是 <code>}</code>,所以按照规则一(情况二),它在 <code>}</code> 前面插入分号变成 <code>{ a ;}</code>,这句就通过了,然后继续处理,按照规则二给 <code>b</code> 加上分号,结束。最终结果是:</p>
<pre><code class="js">{ a ;} b;</code></pre>
<p>顺带一提,也许有人会觉得 <code>{ a; };</code> 这样才更自然。但 <code>{...}</code> 属于块语句,而按照定义块语句是不需要分号结尾的,不管是不是在一行。因为块语句也被用在其他地方(比如函数定义),所以下面这种代码也是完全合法的,不需要任何分号:</p>
<pre><code class="js">function a() {} function b() {}</code></pre>
<h3>第三个例子:do while</h3>
<p>这个是为了解释规则一(情况三),这是最绕的部分,代码如下:</p>
<pre><code class="js">do a; while(b) c</code></pre>
<p>这个例子中解析到 token <code>c</code> 的时候就不对了。这里面既没有换行也没有 <code>}</code>,但 <code>c</code> 前面是 <code>)</code>,所以解析器把之前的 token 组成一个语句,并判断该语句是不是 <code>do...while</code>,结果正好是的!于是插入分号变成 <code>do a; while(b) ;</code>,最后给 <code>c</code> 加上分号,结束。最终结果为:</p>
<pre><code class="js">do a; while (b) ; c;</code></pre>
<p>简单点说,<code>do...while</code> 后面的分号是会自动插入的。但如果其他以 <code>)</code> 结尾的情况就不行了。规则一(情况三)就是为 <code>do...while</code> 量身定做的。</p>
<h3>第四个例子:return</h3>
<pre><code class="js">return
a</code></pre>
<p>你一定知道 <code>return</code> 和返回值之间不能换行,因为上面代码会解析成:</p>
<pre><code class="js">return;
a;</code></pre>
<p>但为什么不能换行?因为 <code>return</code> 语句就是一个 restricted production。这是什么意思?它是一组有严格限定的语法的统称,这些语法都是在某个地方不能换行的,不能换行的地方会被标注 <code>[no LineTerminator here]</code>。</p>
<p>比如 ECMAScript 的 <code>return</code> 语法定义如下:</p>
<pre><code>return [no LineTerminator here] Expression ;</code></pre>
<p>这表示 <code>return</code> 跟表达式之间是不允许换行的(但后面的表达式内部可以换行)。如果这个地方恰好有换行,ASI 就会自动插入分号,这就是规则三的含义。</p>
<p>刚才我们说了 restricted production 是一组语法的统称,它一共包含下面几个语法:</p>
<ul>
<li><p>后缀的 <code>++</code> 和 <code>--</code></p></li>
<li><p><code>return</code></p></li>
<li><p><code>continue</code></p></li>
<li><p><code>break</code></p></li>
<li><p><code>throw</code></p></li>
<li><p>ES6 箭头函数(参数和箭头之间不能换行)</p></li>
<li><p><code>yield</code></p></li>
</ul>
<p>这些不用死记,因为按照常规书写习惯,几乎没人会这样换行的。顺带一提,<code>continue</code> 和 <code>break</code> 后面是可以接 <a href="https://link.segmentfault.com/?enc=3Xb3hPrh7owKEfJUdKy0jQ%3D%3D.fz%2F1jw4GavkA8yVamSaXYB2qh9BggJzLyaURR4490Iw%2FLq7Vx2%2BtgEAz7QhUu1cX2V6Pcs%2FcfXure5pVC%2Bu752p41F%2F%2B1gOj7uGMt58QsqyDM%2Fr4S%2FaiNQI137D6EUck" rel="nofollow">label</a> 的。但这不在本文讨论范围内,有兴趣可以自己探索。</p>
<h3>第五个例子:后缀表达式</h3>
<pre><code class="js">a
++
b</code></pre>
<p>解析器读到 token <code>++</code> 时发现语句不合法,因为后缀表达式是不允许换行的,换句话说,换行的都不是后缀表达式。所以它只能按照规则一(情况一)在 <code>++</code> 前面加上分号来结束语句 <code>a</code>,然后继续执行,因为前缀表达式并不是 restricted production ,所以 <code>++</code> 和 <code>b</code> 可以组成一条语句,然后按照规则二在末尾加上分号。最终结果为:</p>
<pre><code class="js">a
;++
b;</code></pre>
<h3>第六个例子:空语句</h3>
<pre><code class="js">if (a)
else b</code></pre>
<p>解释器解析到 token <code>else</code> 时发现不合法,本来按照规则一(情况一),它在应该加上分号变成 <code>if (a)\n;</code>,但这样 <code>;</code> 就变成空语句了,所以按照例外一,这个分号不能加。程序在 <code>else</code> 处抛异常结束。Node.js 的运行结果:</p>
<pre><code>else b
^^^^
SyntaxError: Unexpected token else</code></pre>
<h3>第七个例子:for</h3>
<pre><code class="js">for (a; b
)</code></pre>
<p>解析器读到 token <code>)</code> 时发现不合法,本来换行可以自动插入分号,但按照例外二,不能为 <code>for</code> 头部自动插入分号,于是程序在 <code>)</code> 处抛异常结束。Node.js 运行结果如下:</p>
<pre><code>)
^
SyntaxError: Unexpected token )</code></pre>
<h2>如何手动测试 ASI</h2>
<p>我们很难有办法去测试 ASI 是不是如预期那样工作的,只能看到代码最终执行结果是对是错。ASI 也没有手动打开或关掉去对比结果。但我们可以通过对比解析器生成的 tree 是否一致来判断 ASI 加的分号是不是跟我们预期的一致。这点可以用 <a href="https://link.segmentfault.com/?enc=ZXawDzTyQBokCmixnkXT2g%3D%3D.QDwcla4yITBANLtOzqbOCzzcC5%2FapK1sr2pq2WVxCcZOgu4ag%2FqgvhGiJ9xonXgQ" rel="nofollow">Esprima 在线解析器</a> 完成。</p>
<p>拿这段代码举例子:</p>
<pre><code class="js">do a; while(b) c</code></pre>
<p>Esprima 解析的 Syntax 如下所示(不需要看懂,记住大概样子就行):</p>
<pre><code class="json">{
"type": "Program",
"body": [
{
"type": "DoWhileStatement",
"body": {
"type": "ExpressionStatement",
"expression": {
"type": "Identifier",
"name": "a"
}
},
"test": {
"type": "Identifier",
"name": "b"
}
},
{
"type": "ExpressionStatement",
"expression": {
"type": "Identifier",
"name": "c"
}
}
],
"sourceType": "script"
}</code></pre>
<p>然后我们把加上分号的版本输入进去:</p>
<pre><code class="js">do a; while(b); c;</code></pre>
<p>你会发现生成的 Syntax 是一致的。这说明解释器对这两段代码解析过程是一致的,我们并没有加入任何多余的分号。</p>
<p>然后试试这个有多余分号的版本:</p>
<pre><code class="js">do a; while(b); c;; // 结尾多一个分号</code></pre>
<p>Esprima 结果:</p>
<pre><code class="js">{
"type": "Program",
"body": [
{
"type": "DoWhileStatement",
"body": {
"type": "ExpressionStatement",
"expression": {
"type": "Identifier",
"name": "a"
}
},
"test": {
"type": "Identifier",
"name": "b"
}
},
{
"type": "ExpressionStatement",
"expression": {
"type": "Identifier",
"name": "c"
}
},
{
// 多出来一个空语句
"type": "EmptyStatement"
}
],
"sourceType": "script"
}</code></pre>
<p>你会发现多出来一条空语句,那么这个分号就是多余的。</p>
<h2>结尾</h2>
<p>如果看到这里,相信你对 ASI 和 JS 的解析机制已经有所了解。也许你会想 “那我再也不省略分号了”,那我建议你看看参考资料里的链接。而且就我的经验,即使是分号的坚持者,少数地方也会无意识地使用 ASI 。比如有时候忘了写分号,或者写迭代器中的单行函数时。下次我会说下对省略分号的风格的看法,和如何用 ESLint 保证代码风格的一致性。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=BdSn%2Fsn1bU%2By35Mm4M53gg%3D%3D.1hbg%2FeqvLY%2FGIhF6igq87Sjyp6bEmpy7BZmG7BwFsUaAwnt6kPUe5RrxxCdvkXvHSQu0tGpprJ7hqz9lTT%2Bke0Pvbv54iiRszg2woznHL5y8jFfmyFaxg10zOXRztld%2B" rel="nofollow">ECMAScript: ASI</a><br>ECMAScript 标准定义。本文的概念和很多例子完全遵照它来写的。但也强烈建议你自己看看。</p>
<p><a href="https://link.segmentfault.com/?enc=rgmoUfFsDyTO%2BQhAldXOsQ%3D%3D.%2FxcFc0cX7SCX4XqSuI79jZqrtYh3LSFCNC5VObb9UrTTyyA5tYU3xrFWCU5pZwQKw%2FC5EruS1guFnI8YemQ%2FKA%3D%3D" rel="nofollow">JavaScript Semicolon Insertion Everything you need to know</a><br>关于 ASI 的解释,略微学术化,讲得很详细,也很客观。</p>
<p><a href="https://link.segmentfault.com/?enc=sZ8kIpo5%2BNtGfMPCLE3kMg%3D%3D.XI%2B4Sh6tDMk0jMiOvPLDZSk%2FRuX9hqYLZrs%2BIJlBul7FiPbA237fYsHC2nFqssJm2IiCjwYjHR181J7Uy35LC2jL5K%2BmALjdD8R3Y0ZHX8WtXyEO%2FOzh4%2BbcDWJgxRue" rel="nofollow">An Open Letter to JavaScript Leaders Regarding Semicolons</a><br>NPM 作者对 ASI 和两种风格的看法,这篇更注重个人观点的表达。他是省略分号风格的倾向者。</p>
<p><a href="https://link.segmentfault.com/?enc=BuKPvxPO12g9Yj7OyAXwcQ%3D%3D.GukCO7KKO7UABhav2jy1tFLOW1cThqo9DlnbZw49FDhBIyUxDlC07VkeMguSqHvN" rel="nofollow">Esprima: Parser</a><br>一个在线 JS 解析器。你可以输入一些语句来看看 token 都是什么。也可以通过 Tree 的变化来测试加不加分号的影响。</p>
Nginx 重定向时获取域名
https://segmentfault.com/a/1190000004477106
2016-02-23T14:45:40+08:00
2016-02-23T14:45:40+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
2
<h2>TL;DR</h2>
<p>如果你在处理 Nginx 重定向时要获取原请求的域名(比如 HTTP 到 HTTPS),请用 <code>$host</code> 而不是 <code>$server_name</code> 。</p>
<h2>问题和解决方案</h2>
<p>今天碰到一个问题,服务器上一个子域名的请求重定向到另一个子域名上面去了。查了一段时间发现这个问题只有在 HTTP 到 HTTPS 跳转的时候才会发生。大概是这样:</p>
<pre><code>从 HTTP 的 sub2 子域名跳转到 HTTPS 的 sub1 子域名
http://sub2.example.com/more_things -> https://sub1.example.com/more_things</code></pre>
<p>我用的 Nginx ,当初为了让 HTTP 请求跳转到同名的 HTTPS 请求,配置如下:</p>
<pre><code>http {
server {
listen 80;
server_name sub1.example.com sub2.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl spdy;
server_name sub1.example.com sub2.example.com;
# ...
}
}</code></pre>
<p>因为 301 是永久重定向,某些浏览器的缓存会记住重定向,下次访问原地址就会直接向新地址发请求,所以这个问题在浏览器里面不一定重现得了(包括 Chrome 的 Incognito Window),能每次完整重现的方式只有 <code>curl</code> 。</p>
<pre><code class="bash">$ curl -I http://sub2.example.com/
HTTP/1.1 301 Moved Permanently
Server: nginx/1.9.3 (Ubuntu)
Date: Tue, 23 Feb 2016 06:06:30 GMT
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://sub1.example.com/</code></pre>
<p>查了一下,发现问题出在 <code>$server_name</code> 变量上。这个变量会始终返回 server_name 中第一个名字。这里其实应该用 <code>$host</code> 变量。修改后的配置如下:</p>
<pre><code>http {
server {
listen 80;
server_name sub1.example.com sub2.example.com;
return 301 https://$host$request_uri;
}
}</code></pre>
<p><code>$host</code> 变量会按照以下优先级获取域名:</p>
<ol>
<li><p>Request-Line 中的域名信息。Request-Line 包含 method, uri 和 HTTP 版本。</p></li>
<li><p>请求头信息中的 "Host" 。</p></li>
<li><p>Nginx 中匹配的 server_name 配置。</p></li>
</ol>
<p>这几乎可以保证在任何环境下正确地得到域名。如果是同域名下的重定向最好都用 <code>$host</code> 。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=pJ7orTfxgy1KARTYI1rmYg%3D%3D.iNjgwZqI9NJcN%2ByN4%2FPPQ%2FYKHT6TLID5N5ZNW7PswWNMzrrw3CXk0Fxhb9t%2Ba3xuZZCHqRsMIktbOsShUrWx4JCh3zkrYPwgXk4WNHHAwdk%3D" rel="nofollow">Nginx Wiki - $host</a><br>Nginx 官方文档。其中对 <code>$host</code> 讲的比较详细,但 <code>$server_name</code> 只是一笔带过。</p>
<p><a href="https://link.segmentfault.com/?enc=BP%2FrWQknj4NYRF2tBP223Q%3D%3D.xASsWSjxKv0njxQKTBfCf%2Bg56uyuVXjsJyQ3hMz%2FzGOxaV0LJcCq98RwVtjmuj7IY4g3ZOsyolmLa%2B6yELbZ9u8RIQj3xj%2FZTx7JqZ8pZllCShSqmi0y7Ot8Z%2BlDdNyNf6pJb%2FQa1TJQAUmmM6I1yPKO%2FUAlj1vFHvUtb4pUMRUItjPpvq48x8w8cStnXhsG" rel="nofollow">StackOverflow - What is the difference between Nginx variables $host, $http_host, and $server_name?</a><br>StackOverflow 上关于三个变量区别的讨论。里面提到了为什么 <code>$host</code> 是适用于所有场景的唯一选择。</p>
<p><a href="https://link.segmentfault.com/?enc=SK%2BTo9DGoPsbo96ulH%2F46g%3D%3D.LND2zHy%2FK57%2Fl%2BJArxEtPAVJ3LgzEZR3Pmhb91TaTRHTElv6HiFniXakLaHfMQykdRkzXR%2Fao8OKwWhkSq7mAA%3D%3D" rel="nofollow">HTTP/1.1 : Request-Line</a><br>HTTP/1.1 规范中对 Request-Line 的描述。</p>
你应该定期更新 Homebrew
https://segmentfault.com/a/1190000004353419
2016-01-23T00:04:27+08:00
2016-01-23T00:04:27+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
49
<h2>TL;DR</h2>
<p>这篇文章是关于定期更新 Homebrew 的话题。它会告诉你定期更新的好处,常用的命令,以及用 <code>brew pin</code> 尽可能无痛地更新。</p>
<h2>为什么要定期更新</h2>
<p>我发现不少人都不会经常更新,或者只在必须用某个工具的新版本的时候才更新。他们的看法是,更新有可能产生一些意外的问题,反正当前环境足够稳定可以用,干嘛自找麻烦呢?</p>
<p>这个看法对也不对。对是因为,更新产生的潜在问题不可避免。不对是因为总有一天你需要升级的,也许是为了某个工具的新特性,也许是为了修复软件的漏洞,也许你安装的包非要依赖另一个包的新版本,等等。如果隔了很长一段时间才升级,那潜在的小问题可能就会变成大问题。</p>
<p>另一个有意思的现象是,当碰到比较破坏性的事情,比如 Mac OS 大版本更新后,很多人会选择重装 Homebrew 然后顺带安装最新版的包。很少人会去装一个指定的旧版本(除了特殊项目需要)。这说明他们不是不想用新版本,而是不想痛苦地更新。</p>
<p>既然总有一天需要更新,而更新带来问题不可避免,那为什么不更新得频繁点呢?这个道理跟 Git 的冲突解决有相似性。长时间不 pull/push 的代码更容易产生冲突,一个解决方法就是频繁地 commit & merge 。</p>
<p>我现在试着一个月更新一次,两次下来发现这些好处:</p>
<ol>
<li><p>每次更新的包很少,更新风险也小。</p></li>
<li><p>更容易发现不需要的包,便于清理,不为不需要的东西买单。</p></li>
<li><p>定期清理旧版本,释放空间。</p></li>
</ol>
<p>更新流程其实都差不多,下面列一下我常用的命令。</p>
<h2>更新 Homebrew</h2>
<p>要获取最新的包的列表,首先得更新 Homebrew 自己。这可以用 <code>brew update</code> 办到。</p>
<pre><code class="bash">brew update</code></pre>
<p>完后会显示可以更新的包列表,其中打钩的是已经安装的包。输出类似下面这样:</p>
<pre><code>Updated Homebrew from fe93aa3 to 6ae64c3.
Updated 1 tap (homebrew/versions).
==> Updated Formulae
awscli cmake ✔ homebrew/versions/libmongoclient-legacy</code></pre>
<h2>更新包 (formula)</h2>
<p>更新之前,我会用 <code>brew outdated</code> 查看哪些包可以更新。</p>
<pre><code class="bash">brew outdated</code></pre>
<p>然后就可以用 <code>brew upgrade</code> 去更新了。Homebrew 会安装新版本的包,但旧版本仍然会保留。</p>
<pre><code class="bash">brew upgrade # 更新所有的包
brew upgrade $FORMULA # 更新指定的包</code></pre>
<h2>清理旧版本</h2>
<p>一般情况下,新版本安装了,旧版本就不需要了。我会用 <code>brew cleanup</code> 清理旧版本和缓存文件。Homebrew 只会清除比当前安装的包更老的版本,所以不用担心有些包没更新但被删了。</p>
<pre><code class="bash">brew cleanup # 清理所有包的旧版本
brew cleanup $FORMULA # 清理指定包的旧版本
brew cleanup -n # 查看可清理的旧版本包,不执行实际操作</code></pre>
<p>这样一套下来,该更新的都更新了,旧版本也被清理了。</p>
<h2>锁定不想更新的包</h2>
<p>如果经常更新的话,<code>brew update</code> 一次更新所有的包是非常方便的。但我们有时候会担心自动升级把一些不希望更新的包更新了。数据库就属于这一类,尤其是 PostgreSQL 跨 minor 版本升级都要迁移数据库的。我们更希望找个时间单独处理它。这时可用 <code>brew pin</code> 去锁定这个包,然后 <code>brew update</code> 就会略过它了。</p>
<pre><code class="bash">brew pin $FORMULA # 锁定某个包
brew unpin $FORMULA # 取消锁定</code></pre>
<h2>其他几个常用命令</h2>
<p><code>brew info</code> 可以查看包的相关信息,最有用的应该是包依赖和相应的命令。比如 Nginx 会提醒你怎么加 <code>launchctl</code> ,PostgreSQL 会告诉你如何迁移数据库。这些信息会在包安装完成后自动显示,如果忘了的话可以用这个命令很方便地查看。</p>
<pre><code class="bash">brew info $FORMULA # 显示某个包的信息
brew info # 显示安装了包数量,文件数量,和总占用空间</code></pre>
<p><code>brew deps</code> 可以显示包的依赖关系,我常用它来查看已安装的包的依赖,然后判断哪些包是可以安全删除的。</p>
<pre><code class="bash">brew deps --installed --tree # 查看已安装的包的依赖,树形显示</code></pre>
<p>输出如下:</p>
<pre><code>elixir (required dependencies)
└── :erlang
wxmac (required dependencies)
├── jpeg
├── libpng
│ └── xz
└── libtiff
└── jpeg</code></pre>
<p>还有很多有用的命令和参数,没事 <code>man brew</code> 一下可以涨不少知识。</p>
<h2>小结</h2>
<p>不想更新 Homebrew 往往有两个原因,害怕潜在的风险和对工具的不熟悉,我之前也是这样。写这篇文章最开始是为了帮我记录常用的命令方便以后查阅的。希望它也能帮到你。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=L4kURzjcdFSudXlv1WmIAg%3D%3D.l6Sxd47t5oJ8NosaZKdZg19uU6RTS0jZ%2F%2FtJUq9MobbbSTJvupGpbnPLuF7iF7CrNvF5pVA86ZSLnoyMwPgyYRGYJrF3dZ59UERrGEjDuz4%3D" rel="nofollow">Keeping Your Homebrew Up to Date</a><br><a href="https://link.segmentfault.com/?enc=eeYMmdhPNrUPWYaP6swiEA%3D%3D.DMKTesYmRHbDfG%2FyP5Tqm766IBSRmLEK%2BJP2sMUorp49TV%2Fup128yJGUydmU%2Facyys80aaLAZABZMUUGvL8IzX81auu051yrmYG%2F2ZgLdx0%3D" rel="nofollow">Homebrew FAQ</a></p>
Phoenix render 迷思
https://segmentfault.com/a/1190000004211697
2015-12-28T11:31:45+08:00
2015-12-28T11:31:45+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>前言</h2>
<p>最近在学习用 Elixir 的 MVC 框架 Phoenix 写一个 Chatroom 。有一个问题是在 channel 中渲染模板,虽然我用 <code>Phoenix.View.render</code> 方法顺利解决了。但这让我开始思考另外几个问题:</p>
<ol>
<li><p>Phoenix 中有哪些 <code>render</code> 方法?</p></li>
<li><p>它们分别是干什么用的?</p></li>
<li><p>它们有内部联系吗?</p></li>
</ol>
<h2>render 的使用场景</h2>
<p>在研究有哪些 <code>render</code> 方法之前,我们先看看 Phoenix 的几个使用 <code>render</code> 的场景。</p>
<p>在 controller 中使用 <code>render</code> :</p>
<pre><code class="elixir">def foo(conn, _params) do
render conn, "foo.html"
end</code></pre>
<p>在 template 中使用 <code>render</code> :</p>
<pre><code class="eex"><!-- 渲染同一视图中的另一个模板 -->
<%= render "foo.html" %>
<%= render "foo.html", some_model: some_model %>
<!-- 渲染另外一个视图中的模板 -->
<%= render YourApp.OtherView, "bar.html" %>
<%= render YourApp.OtherView, "bar.html", some_model: some_model %></code></pre>
<p>在其他地方使用 <code>render</code> ,多用于 channel 或者 iex 调试:</p>
<pre><code class="elixir">Phoenix.View.render(YourApp.CustomView, "foo.html")</code></pre>
<p>除了最后一个例子可以清楚地看到 <code>render</code> 方法来自 <code>Phoenix.View</code> 模块之外,其他几个地方的 <code>render</code> 都不知道出处。这些 <code>render</code> 来自哪里?</p>
<p>如果查一下 Phoenix 的文档,可以发现两个模块定义了 <code>render</code> 方法,它们是 <code>Phoenix.Controller</code> 和 <code>Phoenix.View</code> ,我们可以猜测前者为所有 controller 提供 <code>render</code> ,后者为所有 view 和 template 提供 <code>render</code> 。不过这还需要验证一下。</p>
<h2>controller 的 render</h2>
<p>先看看 controller ,Phoenix 的 controller 定义非常简单:</p>
<pre><code class="elixir">defmodule YourApp.SomeController do
use YourApp.Web, :controller
end</code></pre>
<p>显然一个空的模块是没有实现 <code>render</code> 方法的,那关键就在 <code>YourApp.Web</code> 里。其实这个模块就在项目的 <code>web/web.ex</code> 文件里。大概像下面这样:</p>
<pre><code class="elixir">defmodule YourApp.Web do
# def model ...
def controller do
quote do
use Phoenix.Controller
alias YourApp.Repo
import Ecto
import Ecto.Query, only: [from: 1, from: 2]
import YourApp.Router.Helpers
import YourApp.Gettext
end
end
# def view ...
end</code></pre>
<p><code>YourApp.Web</code> 模块的职责是为其他模块加入一些通用的功能,基本上就是执行一些 alias, import, use 。<br>controller 中的 <code>use</code> 那一行代码会调用 <code>YourApp.Web</code> 的 <code>controller</code> 方法,这里我们看到执行了 <code>use Phoenix.Controller</code> 。</p>
<p>先大致解释一下 <code>use</code> 。它是一个 Elixir 的 macro ,一般用来为模块附加额外的特性。当模块 A use 模块 B 时,B 的 <code>__using__</code> 回调会被调用,我们可以在里面写代码为模块 A 附加一些东西。</p>
<p><code>Phoenix.Controller</code> 的 <code>__using__</code> 大概如下所示,看不懂语法和 API 不要紧,明白意思就行:</p>
<pre><code class="elixir">defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
import Phoenix.Controller
# ...
end
end</code></pre>
<p>这里我们可以看到 <code>import Phoenix.Controller</code> ,结合开头 controller 中的 <code>use YourApp.Web, :controller</code> ,其实 <code>Phoenix.Controller</code> 用这种形式被 <code>import</code> 到了所有 controller 中,这意味着 <code>Phoenix.Controller</code> 的所有公有方法都可以在 controller 内部使用,其中也包括 <code>render</code> 方法。注意这是 <strong>内部使用</strong> ,像 <code>YourApp.SomeController.render</code> 这种调用是不可行的。</p>
<h2>template 的 render</h2>
<p>在 template 中调用 <code><%= render %></code> 应该属于哪个模块的呢?要回答这个问题,我们得先了解下 view 和 template 的关系。</p>
<p>Phoenix 的视图层分为两个部分:view 和 template ,view 是一个 Elixir 模块,template 是一个 EEx 模板文件。一个 view 管理多个 template 。举个例子,一个 <code>YourApp.RoomView</code> 下面可以定义 <code>index.html</code> , <code>show.html</code> 等几个不同 template 。要渲染 <code>show.html</code> ,我们可以用 <code>Phoenix.View.render(YourApp.RoomView, "show.html")</code> 。</p>
<p>template 本质上是一个函数,接收动态数据作为参数,组合静态内容并返回结果。对服务器端渲染而言,结果大多是一个字符串。我们经常使用的模板文件,实际上只是把静态内容存放在文件系统里而已。Phoenix 在编译期间会把 template 编译成函数放在 view 中。模板渲染最终会调用 view 中相应的函数。因此在 template 里调用的方法全都来自于 view 。template 里的 <code><%= render %></code> 等于调用相对应的 view 的 <code>render</code> 方法。</p>
<p>一个典型的 view 定义如下:</p>
<pre><code class="elixir">defmodule YourApp.RoomView do
use YourApp.Web, :view
end</code></pre>
<p>跟 controller 非常类似的代码。具体源码追溯过程我就不写了,通过同样追溯方法我们最终可以在 <code>Phoenix.View</code> 的 <code>__using__</code> 中看到同样的 <code>import</code> ,如下所示:</p>
<pre><code class="elixir">defmacro __using__(options) do
# ...
quote do
import Phoenix.View
use Phoenix.Template, root: ...
end
end</code></pre>
<p>看来 view 中的 <code>render</code> 方法应该来自于 <code>Phoenix.View</code> 。不过先别下结论,我们来对比一下方法签名。<code>Phoenix.View.render</code> 的方法签名是 <code>render(module, template, assigns)</code> ,注意其中有 <strong>三个参数,并且都是不能省略的</strong> 。</p>
<p>再回顾一下 template 中的 <code>render</code> :</p>
<pre><code class="eex"><!-- 渲染同一视图中的另一个模板 -->
<%= render "foo.html" %>
<%= render "foo.html", some_model: some_model %>
<!-- 渲染另外一个视图中的模板 -->
<%= render YourApp.OtherView, "bar.html" %>
<%= render YourApp.OtherView, "bar.html", some_model: some_model %></code></pre>
<p>可见这个 <code>render</code> 可以接受 <strong>一个到三个参数</strong> 。这跟 <code>Phoenix.View</code> 的 <code>render</code> 明显不一样。这是怎么回事?</p>
<p>答案在 <code>Phoenix.Template</code> 中。回顾一下上面的代码,<code>Phoenix.View</code> 的 <code>__using__</code> 中还有一行 <code>use Phoenix.Template</code> ,让我们看看 <code>Phoenix.Template</code> 的 <code>__using__</code> :</p>
<pre><code class="elixir">defmacro __using__(options) do
quote do
@doc """
Renders the given template locally.
"""
def render(template, assigns \\ ${})
def render(template, assigns) when is_list(assigns) do
render(template, Enum.into(assigns, %{}))
end
def render(module, template) when is_atom(module) do
Phoenix.View.render(module, template, %{})
end
# ...
end
end</code></pre>
<p>它居然为 view 定义了 <code>render</code> 方法!这下事情明白了,当我们在 template 中使用 <code>render</code> 时,如果传入一个或两个参数,其实我们调用的是 <code>Phoenix.Template</code> 为 view 生成的 <code>render</code> 方法;如果传入三个参数,则是调用 <code>Phoenix.View</code> 中的 <code>render</code> 方法。因为这个 <code>render</code> 方法是在 <code>__using__</code> 中定义的,所以 Phoenix 文档是查不到的 。</p>
<p>注:Elixir 允许为一个方法定义不同的变种,这些方法并不会互相覆盖。当方法被调用时 Elixir 会通过 pattern match 和 guard 自动去寻找最匹配的方法执行,合理利用可以省不少 <code>if/else</code> 。</p>
<p>有一点值得提醒,跟 <code>Phoenix.View</code> 的 <code>import</code> 不同,<code>Phoenix.Template</code> 是为 view 动态地定义方法(其实是编译期做的),而且这个方法是公有的。这意味着我们可以在其他模块里调用 <code>YourApp.SomeView.render</code> 去渲染 template 。</p>
<h2>view 的 render</h2>
<p>这里想说的有两种,一是在 view 中调用 <code>render</code> ,二是在其他模块中调用 <code>Phoenix.View.render</code> 。</p>
<p>在 view 里面,<code>Phoenix.View</code> 的所有公有方法都可以使用。<code>Phoenix.Template</code> 给了 view 三个 <code>render</code> 方法,但没有把它自己 <code>import</code> 进去 ,所以它的方法是不能在 view 里直接用的。不过大部分情况下 <code>Phoenix.View</code> 提供的方法已经足够了。</p>
<p>在其他模块中,如果要渲染某个 template ,我们其实有两种办法,它们是等价的:</p>
<pre><code class="elixir">Phoenix.View.render(YourApp.SomeView, "some_template.html")
YourApp.SomeView.render("some_template.html")</code></pre>
<p>虽然第二种方式更简洁,但第一种方式,也就是 <code>Phoenix.View.render</code> 比较推荐,因为它可以设置 layout ,而且也算是 view 渲染的标准入口函数。比如 <code>Phoenix.Controller</code> 的 <code>render</code> 内部调用的就是它,而且 <code>Phoenix.View</code> 的其他几个方法比如 <code>render_to_iodata</code> ,<code>render_to_string</code> 调用的也是它。源码追溯过程我就不放上来了,有兴趣的可以自己挖掘。</p>
<p>另外 <code>Phoenix.View.render</code> 渲染某个 view 的 template 的时候,它会在内部调用 view 的 <code>render</code> 方法(<code>Phoenix.Template</code> 提供的方法)。</p>
<h2>小结</h2>
<p>现在我们可以回答开篇的几个问题作为总结:</p>
<h4>Phoenix 中有哪些 <code>render</code> 方法?</h4>
<p>Phoenix 文档中可以查到两个 <code>render</code> 方法,但实际上有三个 <code>render</code> 方法,前两个在 <code>Phoenix.Controller</code> 和 <code>Phoenix.View</code> 中定义并被 <code>import</code> 到相应的模块中使用,第三个在 <code>Phoenix.Template</code> 的 <code>__using__</code> 中被定义,并在编译时附加给 view 。</p>
<h4>它们分别是干什么用的?</h4>
<p><code>Phoenix.Controller.render</code> 在 controller 中使用,它关注的是<a href="https://link.segmentfault.com/?enc=%2F2PuS66Dxfww48lC58nO9g%3D%3D.nS9tZlMPH9QhSvsyyplgcEGFj45Nwy05vT0EKlgZEA4UamSfeg9oRPXISB9mi%2BE1XvlzJSt1svwJ8EDsLX9F9Q%3D%3D" rel="nofollow">内容协商</a>,即根据客户端的要求来决定渲染类型(HTML/JSON/XML 等)。具体渲染细节会代理给 <code>Phoenix.View.render</code> 去处理。</p>
<p><code>Phoenix.View.render</code> 可以算是通用的 view 渲染入口,既用在 view 和 template 中,也被其他需要渲染 view 的模块调用。比如 controller 。它处理视图层的渲染细节。</p>
<p><code>Phoenix.Template</code> 提供的 <code>render</code> 是作为 view 的 <code>render</code> 方法的补充,让 view 的 <code>render</code> 方法变得更灵活多变。</p>
<h4>它们有内部联系吗?</h4>
<p><code>Phoenix.Controller.render</code> 内部会调用 <code>Phoenix.View.render</code>,<code>Phoenix.View.render</code> 内部会调用传入的 view 模块的 <code>render</code> 方法,而这个方法是 <code>Phoenix.Template</code> 为每个 view 生成的。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=c8uEPuBMbxGNN0r1oBZukg%3D%3D.MKYM3IxqC7pUNYsOYelfY7wE3mUazSBxKn9tJKclVunk8OpWMgONrxAhC3egUvC8OsuzJmZTRlIHYtlfFYL1qg%3D%3D" rel="nofollow">Phoenix.Controller.render</a><br><a href="https://link.segmentfault.com/?enc=ulQI7TEKIsQOCVDZibpMNw%3D%3D.eaESIf230opy2fjDfZmN9ac8mKGrMjJ7MKcqKyh5Y9ORxDMGOMrHCC4sC3oAtI7pAnb5NsgKY%2BQJXoJo6JSZbQ%3D%3D" rel="nofollow">Phoenix.View.render</a><br><a href="https://link.segmentfault.com/?enc=BK%2Fwxi4XNRvj2YcOOw%2FgVw%3D%3D.gzOySjYTklvSGntuaeJNngaSDPtHD4YOykMcCT9py%2Fgv9ghmxs18BqRAlcWzv4zQ" rel="nofollow">Phoenix.Template</a><br><a href="https://link.segmentfault.com/?enc=xs5KnMnnW75cAZ897kTq4Q%3D%3D.YXFvBj5aJ4tgalOlMQT3NiUp0MmwraUb%2BgtjEIRtxrMyxALMwOUzMSKw5qG%2BsE6n" rel="nofollow">Phoenix Guide: Views</a><br><a href="https://link.segmentfault.com/?enc=Mk3nlFucz49rWu5FyBFmQQ%3D%3D.7YYFnv%2F4HWVNNaitvgGbDz%2FhU1a5K%2F6yWeAEeMJpRN4E%2Bc%2FbgbKp5zJ1SewTvmqy309Fj%2BBtaBUVPF7jYqTh1Q%3D%3D" rel="nofollow">Content negotiation</a></p>
更简单灵活地管理 Ruby 版本
https://segmentfault.com/a/1190000003957439
2015-11-05T23:39:29+08:00
2015-11-05T23:39:29+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>概述</h2>
<p>这篇文章教你怎么用 ruby-install 和 chruby 这两个工具来管理和切换 Ruby 版本,相对 RVM 和 rbenv 来说这是一个更加轻量级且绿色环保的组合。</p>
<h2>为什么不用 RVM 或者 rbenv</h2>
<p>首先说明一点,切换 ruby-install 和 chruby 并不是因为它们有什么独一无二的特性。它们能做到的事情 RVM 和 rbenv 都可以做到。如果你用着现有的工具感觉良好,也没有尝鲜的打算,完全可以不换。</p>
<p>那我为什么要换?对我而言有几个原因:</p>
<ol>
<li><p>我想用一个 Homebrew 可以管理和升级的工具。</p></li>
<li><p>最好是工具不升级也可以安装最新的 Ruby 版本。</p></li>
<li><p>功能和实现都很简单。因为我只需要安装和切换不同的 Ruby 版本,不需要其他的功能。</p></li>
</ol>
<p>关于最后一点我想多说几句,最近一年我对软件的看法有些改变。对我而言好的软件最重要的是 <strong>简单够用</strong> 。我不想为用不到的功能买单,它们可能增加潜在的复杂度和维护成本。我也不喜欢软件为了达到功能做了太多 hack ,这会影响它跟其他软件组合使用的轻松程度,进而影响未来替换它的轻松程度。我以前一直在用 RVM ,这次本来准备换成 rbenv ,但当我看了 <a href="https://link.segmentfault.com/?enc=69u1dRXm8gP8gM2Krk%2BYHA%3D%3D.CDNnADbnwJquTJoTaIHyylmq0unfHCGrrOsNg8PHFJKDc2Gq2GqJ%2B3ErLCSrQA5BZf6IQ2fDE7gR2VtpCcLL9w%3D%3D" rel="nofollow">rbenv 对 shim 的大堆解释</a> 后觉得这不是我想要的理想替代品。正好前几天同事推荐 chruby ,<a href="/u/hooopo">@Hooopo</a> 的<a href="https://link.segmentfault.com/?enc=BrexBUaUpvWa0NZKLFtnUQ%3D%3D.7wHT2uPjrYgMruIs4YucSJdhGY2hCCnXzrS8O47eIlGyVPPtVxL30TY0sgvWDkNI" rel="nofollow">这篇文章</a> 也让我觉得这应该是个不错的玩意(希望没拼错那几个 o),于是就果断删了 RVM 切换过去了。过程比我想得还要轻松许多。</p>
<h2>使用 ruby-install & chruby</h2>
<p>这其实是两个工具,<a href="https://link.segmentfault.com/?enc=qale0RvsTWo%2BACq3VMmpsQ%3D%3D.mn3ITPvNhLqQFGXy8TfQtD1Qtgl8eR7DB%2FtUFylBhpgUyVkwnrTBM7PX3qaNmOLa" rel="nofollow">ruby-install</a> 只负责下载、编译和安装多个 Ruby 版本,<a href="https://link.segmentfault.com/?enc=%2B1ycU2FYHo0p4eLSBv7W7g%3D%3D.vc35jRo8RfYtAObsy1qNYP6A7FK%2B%2F8oswFLUK361Oqp5UDa7UMazYexFN6IZUEDo" rel="nofollow">chruby</a> 负责切换。它们的名字就是命令行的名字,所以你需要用到两个命令(但都非常简单)。你可以点它们的名字去 Github 看 README.md 。下面我只提供基本用法的例子(用的 Homebrew):</p>
<p>安装 ruby-install</p>
<pre><code class="bash">brew install ruby-install</code></pre>
<p>安装指定 Ruby 版本</p>
<pre><code class="bash">ruby-install ruby 2.2.3</code></pre>
<p>安装 chruby</p>
<pre><code class="bash">brew install chruby</code></pre>
<p>切换 Ruby 版本</p>
<pre><code class="bash">chruby ruby-2.2.3</code></pre>
<p>然后在 <code>.bashrc</code> 或者 <code>.bash_profile</code> 里加入脚本(具体路径最好照官方说明来)。第一个脚本加载 chruby ,第二个脚本控制自动切换(按 .ruby-version 文件)。</p>
<pre><code class="bash">source /usr/local/opt/chruby/share/chruby/chruby.sh
source /usr/local/opt/chruby/share/chruby/auto.sh</code></pre>
<p>关于默认 Ruby 版本,chruby 没有这种命令,但我们需要的只是 “在适当的时候让 chruby 自动切换到指定版本” 。chruby 会从当前目录向上一层层地找 .ruby-version 文件,所以你只要把默认 Ruby 版本写在 <code>~/.ruby-version</code> 里就可以了,以此类推,如果需要在任何目录下都能切换到默认版本,你可以考虑 <code>/.ruby-version</code>,我没这个需求,就没有尝试。</p>
<h2>在命令提示符里显示 Ruby 版本</h2>
<p>这几乎是 Ruby 开发的 “刚需” 了。我是自己写了个简单的脚本做到这点的。原理很简单,用 <code>ruby --version</code> 显示当前使用的版本,截取版本号再插入提示符就行了(修改 PS1 变量)。以我的 bash 举例子:</p>
<pre><code class="bash"># 如果当前目录有 Gemfile 就显示 Ruby 版本;如果有 package.json 就显示 Node.js 版本,否则什么都不显示。
# 结果大概会显示成 ruby@2.2.3 或者 node@5.0.0
function env_version {
if [ -e ./Gemfile ]; then
# ruby 2.2.2p95 (2015-04-13..) -> 2.2.2p95 -> 2.2.2
echo "ruby@$(ruby --version | awk '{print $2}' | awk -F'p' '{print $1}') ";
elif [ -e ./package.json ]; then
# v4.2.1 -> 4.2.1
echo "node@$(node -v | awk -F'v' '{print $2}') ";
else
echo "";
fi
}
# 用 $() 嵌入 env_version 的结果
PS1="$(env_version)"</code></pre>
<p>我自己用的 <code>PS1</code> 变量显示了路径,Ruby/Node.js 版本,和 Git 分支名。感兴趣的可以参考 <a href="https://link.segmentfault.com/?enc=0pSn%2BIU1mHBtc6mx2NOxQw%3D%3D.QpJn9v7fdBwrccdqz%2FcDrJqJ2Qt%2FdkKLQGV5zQcM8YGvhNmSvPbhrqdouhl3g0eh2XvYcKbwsobTdUsJ9lZY%2FXx84evyZx2RHEExgMy82to%3D" rel="nofollow">我的 dotfile</a>,最终效果大概如此:</p>
<pre><code class="bash">~/workspace/my-project ruby@2.2.2 [staging] $</code></pre>
<p>最后附带一点,如果不想频繁地敲 bundle exec xxx ,可以看看文末链接的 <a href="https://link.segmentfault.com/?enc=OR8J21I2SwB1lMMWm2crYg%3D%3D.q5tMV%2F09Tkv2t0hgq0G45USDWpYu16tqcCP7pbww0oKypNsml23wCtH8%2BPR9Krm%2F" rel="nofollow">Automating bundle exec</a> 。这是一个简单的 alias 脚本。不过我在同事的 rbenv 环境上没试成功,不确定是不是受 rbenv 的 shim 影响。如果是的话,这也侧面证明了 less hack 的好处。</p>
<h2>参考链接</h2>
<p><a href="https://link.segmentfault.com/?enc=gWIIkpqKXW%2FqcObl%2FhXOqw%3D%3D.7Hrd9fhqFEMYeLhUYfEH1tA65qnqDN7Yheng%2BVHWayMObclSb1xCw7sGeAWXNq17" rel="nofollow">Install Ruby The "Postmodern" Way</a><br><a href="https://link.segmentfault.com/?enc=iioBDVvdf%2BF%2Fl9GkYf3niw%3D%3D.kI2l9X3C5uGyDrirGYS%2B2AN9Elw8h%2BVWii0mjbQJsKuCxZ%2FrRIQv5AQ6uOtrWRGRmEc82WEyxm%2BqX74hRwgos7qcOb7tZoqOjKmQcKTikrCFbOUmvkyg%2FP9ZcfXud4hx" rel="nofollow">Programmer's guide to choosing a Ruby version manager</a><br><a href="https://link.segmentfault.com/?enc=SDKQrE4gc%2BFSzpvcC7sQdA%3D%3D.pp1T1AFDRZLgQ1WLVxSFh2apubVVPMbmKRJvEXuBObBvnbS4LTfC10AX9QPhfyXQ" rel="nofollow">Automating bundle exec</a></p>
用 ES6 编写 Webpack 的配置文件
https://segmentfault.com/a/1190000003932889
2015-10-31T08:00:00+08:00
2015-10-31T08:00:00+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
5
<h2>概述</h2>
<p>我最近在整理一个 Ionic + Webpack 的项目模板,因为项目代码都是 ES6 的,所以我也想在其他地方也用 ES6 。其中一个地方就是 <code>webpack.config.js</code> 。目前有三种方法可以做到这一点,不过各有利弊。</p>
<h2>方法 1:升级到 Node.js 4</h2>
<p>Node.js 4 合并了 io.js ,所以自然带有所有 io.js 的特性,其中就包括部分 ES6 特性的支持。不过目前的版本 (4.2.1) 只支持部分特性,尤其是以下几个很常用的都不支持:</p>
<ol>
<li><p>函数默认值</p></li>
<li><p>解构和其相关的所有功能</p></li>
<li><p>ES6 模块</p></li>
</ol>
<p>具体支持程度可看 <a href="https://link.segmentfault.com/?enc=fNtMXABqVOaMBW%2BdMCFulw%3D%3D.oTMGEmig%2FrUkC%2FShwsBUgKQdSvvO8etTxCIg7ro%2Fa7mPrVpHb8UararMFKduzkd4" rel="nofollow">这里</a> 。Babel 达到了 71% ,Node.js 4 达到了 53% ,Node.js 5 也只达到了 59% 。真是不容乐观。</p>
<p>总结:这个方法适合依赖少数 ES6 特性,又确定使用 Node.js 4 及以上版本的人,不能算是大众方案。</p>
<h2>方法 2:webpack.config.babel.js</h2>
<p>这个最简单,把 <code>webpack.config.js</code> 改名成 <code>webpack.config.babel.js</code> 就行。一切命令照旧。Webpack 在执行时会先用 Babel 把配置文件转成 ES5 代码再继续处理。一切 Babel 支持的语言特性都可以用。</p>
<p><strong>这是一个 Webpack 支持,但文档里完全没有提到的特性</strong> (应该马上就会加上)。只要你把配置文件命名成 <code>webpack.config.[loader].js</code> ,Webpack 就会用相应的 loader 去转换一遍配置文件。所以要使用这个方法,你需要安装 <strong>babel-loader</strong> 和 <strong>babel-core</strong> 两个包。记住你不需要完整的 <strong>babel</strong> 包。</p>
<p>理论上这种做法支持任何 loader ,所以你也可以用 CoffeeScript 或者其他语言去写,只要有相应的 loader 就行。</p>
<p>这个方法还有个好处,如果你在 <code>webpack.config.babel.js</code> 里 <code>import</code> 了其他文件,那个文件也会被 Babel 编译。比如:</p>
<pre><code class="js">// webpack.config.babel.js
// 这个文件也可以用 ES6 写
import config from './some-config'
export default {
// webpack config
}</code></pre>
<p>不过,如果你打算自己写脚本去加载 Webpack 的配置,这个方法就不管用了。</p>
<p>总结:这个方法适合那些不在乎 Node.js 版本,只使用 <code>webpack</code> 和 <code>webpack-dev-server</code> 命令,不打算自己写脚本或过多折腾,但想使用完整的 ES6 特性的人。</p>
<h2>方法 3:用 babel-node</h2>
<p>这是我在 <a href="https://link.segmentfault.com/?enc=TIHv%2FtTGp77TEgaub1ESXg%3D%3D.D0KxN9btj%2BvNWfWDjK4I3ej7o1HzOkSzRnxuz5djLAkIDWByMfvDw9DZvqBpbBpJYUHPdgo7gdMfURk%2FVE3paxlehXXJ3NbmtKDh65%2B1qyE%3D" rel="nofollow">这个问题</a> 中看到的。其中提问者提到的 React Starter Kit 挺有意思。它没改 <code>webpack.config.js</code> 的文件名,但配置文件和各种脚本都是完全的 ES6 语法。这是怎么做到的呢?</p>
<p>关键就在于 <code>babel-node</code> 。这是 Babel 提供的一个命令行工具,你可以用它代替 <code>node</code> 去执行文件。文件会被 Babel 编译后再交给 <code>node</code> 命令执行。</p>
<p>让我们看看 React Starter Kit 如何利用这一点的。首先它用 <code>package.json</code> 里定义的 <code>scripts</code> 来代替 <code>webpack</code> 命令。可以看到它完全使用了 <code>babel-node</code> 命令代替 <code>node</code> 。比如:</p>
<pre><code class="js">{
"scripts": {
"bundle": "babel-node tools/run bundle",
...
}
}</code></pre>
<p>这样就可以用 <code>npm run bundle</code> 来执行相应的任务了。这个命令会会先调用 <code>tools/run.js</code>,然后调用 <code>tools/bundle.js</code>,然后加载 <code>tools/webpack.config.js</code> 。整个流程中的所有文件都是用 ES6 和 ES7 语法写的,非常整洁漂亮。</p>
<p>总结:这个方法适合需要自己写脚本并且想用完整的 ES6 语法的人。不过 <code>babel-node</code> 因为要编译,而且换成结果会存在内存中,所以命令执行时间会比单纯使用 <code>node</code> 要长(主要是启动时间)。这点就见仁见智了。记住不要在生产环境下用 <code>babel-node</code> 。</p>
<h2>总结</h2>
<p>得益于 Babel ,ES6 几乎已经是现在的标配了。在不折腾的情况下用用 ES6 是大家都能接受的结果。所以我推荐大部分人用方法 2 。但如果需要写点 <code>npm run xxx</code> 的脚本,难免又会觉得不能用 ES6 有点不一致。这种情况我觉得要么就都用 ES6 ,要么就干<br>脆不用。因为我个人觉得一致性比用不用 ES6 更加重要。build 脚本勉强也算是后端的一部分,而我们不能强求所有后端代码都写成 ES6 的(比如自己写个 server)。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=PI3KKwkUmOOBgo6doKBE3g%3D%3D.gC6lIgeX9Tk2X8u80lvYhDpNx5aPh6LUlR3Aunrh7DzleleoTIT8YbBEphC2AUym" rel="nofollow">Allow webpack.config.js to be written in ES6</a><br><a href="https://link.segmentfault.com/?enc=e6QmNc2aojufanBtKqaPNg%3D%3D.e3nSxD3OPmrcdMVFyEO1WJx%2BTPzX1k55fHU4%2BmrScelfbfR5C03%2Bz3irWHr1md1z" rel="nofollow">ES6 Compatible Table</a><br><a href="https://link.segmentfault.com/?enc=xgr9nadYznXgMT2nEoV5TA%3D%3D.i96yrAEnbR14Z5sLN49ATqs%2BJS7uBwgF0aaOJ9BTGQuL2o0bOY2Z%2FeNLv8MJ3XL29Qfo%2BALKAT5iELB90UZjwRQGXPrRL18lr2jIj%2BWmJi0%3D" rel="nofollow">StackOverflow - How to use ES6 in Webpack config</a><br><a href="https://link.segmentfault.com/?enc=LzS7SmvHIq1iRHfXPPfIMA%3D%3D.izhBdM8lKQg6S%2BlGCMwNYfPKIRJz5e1Tsc92mufOCkHNCTLiSs4ke%2FGmbKMSdcss" rel="nofollow">React Starter Kit</a><br><a href="https://link.segmentfault.com/?enc=3pNNtz964OJ8HB1Hx7DfSQ%3D%3D.v99ThHZB%2FaqoEqFt3O5jVJ6y49VLkaRLf9dYFxAlA7pRrNNJKRU9leH8kLOpoLjG" rel="nofollow">Babel CLI</a></p>
JavaScript 原型系统的变迁,以及 ES6 class
https://segmentfault.com/a/1190000003798438
2015-09-27T17:32:17+08:00
2015-09-27T17:32:17+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
27
<h2>概述</h2>
<p>JavaScript 的原型系统是最初就有的语言设计。但随着 ES 标准的进化和新特性的添加。它也一直在不停进化。这篇文章的目的就是梳理一下早期到 ES5 和现在 ES6,新特性的加入对原型系统的影响。</p>
<p>如果你对原型的理解还停留在 <code>function + new</code> 这个层面而不知道更深入的操作原型链的技巧,或者你想了解 ES6 class 的知识,相信本文会有所帮助。</p>
<p>这篇文章是我学习 You Don't Know JS 的副产品,推荐任何想系统性地学习 JavaScript 的人去阅读此书。</p>
<h2>JavaScript 原型简述</h2>
<p>很多人应该都对原型(prototype)不陌生。简单地说,JavaScript 是基于原型的语言。当我们调用一个对象的属性时,如果对象没有该属性,JavaScript 解释器就会从对象的原型对象上去找该属性,如果原型上也没有该属性,那就去找原型的原型。这种属性查找的方式被称为原型链(prototype chain)。</p>
<p>对象的原型是没有公开的属性名去访问的(下文再谈 <code>__proto__</code> 属性)。以下为了方便称呼,我把一个对象内部对原型的引用称为 [[Prototype]]。</p>
<p>JavaScript 没有类的概念,原型链的设定就是少数能够让多个对象共享属性和方法,甚至模拟继承的方式。在 ES5 以前,如果我们想设置对象的 [[Prototype]],只能通过 <code>new</code> 关键字,比如:</p>
<pre><code class="js">function User() {
this._name = 'David'
}
User.prototype.getName = function() {
return this._name
}
var user = new User()
user.getName() // "David"
user.hasOwnProperty('getName') // false</code></pre>
<p>当 <code>User</code> 函数被 <code>new</code> 关键字调用时,它就类似于一个构造函数,其生成的对象的 [[Prototype]] 会引用 <code>User.prototype</code> 。因为 <code>User.prototype</code> 也是一个对象,它的 [[Prototype]] 是 <code>Object.prototype</code> 。</p>
<p>一般我们对这种构造函数命名都会采用 CamelCase ,并把它称呼为“类”,这不仅是为了跟 OOP 的理念保持一致,也是因为 JavaScript 的内建“类”也是这种命名。</p>
<p>由 <code>SomeClass</code> 生成的对象,其 [[Prototype]] 是 <code>SomeClass.prototype</code>。除了稍显繁琐,这套逻辑是可以自圆其说的,比如:</p>
<ol>
<li><p>我们用 <code>{..}</code> 创建的对象的 [[Prototype]] 都是 <code>Object.prototype</code>,也是原型链的顶点。</p></li>
<li><p>数组的 [[Prototype]] 是 <code>Array.prototype</code> 。</p></li>
<li><p>字符串的 [[Prototype]] 是 <code>String.prototype</code> 。</p></li>
<li><p><code>Array.prototype</code> 和 <code>String.prototype</code> 的 [[Prototype]] 是 <code>Object.prototype</code> 。</p></li>
</ol>
<h2>模拟继承</h2>
<p>模拟继承是自定义原型链的典型使用场景。但如果用 <code>new</code> 的方式则比较麻烦。一种常见的解法是:子类的 <code>prototype</code> 等于父类的实例。这就涉及到定义子类的时候调用父类的构造函数。为了避免父类的构造函数在类定义过程中的潜在影响,我们一般会建造一个临时类去做代替父类 <code>new</code> 的过程。</p>
<pre><code class="js">function Parent() {}
function Child() {}
function createSubProto(proto) {
// fn 在这里就是临时类
var fn = function() {}
fn.prototype = proto
return new fn()
}
Child.prototype = createSubProto(Parent.prototype)
Child.prototype.constructor = Child
var child = new Child()
child instanceof Child // true
child instanceof Parent // true</code></pre>
<h2>ES5: 自由地操控原型链</h2>
<p>既然原型链本质上只是建立对象之间的关联,那我们可不可以直接操作对象的 [[Prototype]] 呢?</p>
<p>在 ES5(准确的说是 5.1)之前,我们没有办法直接获取对象的原型,只能通过 [[Prototype]] 的 <code>constructor</code>。</p>
<pre><code class="js">var user = new User()
user.constructor.prototype // User
user.hasOwnProperty('constructor') // false</code></pre>
<p>类可以通过 <code>prototype</code> 属性获取生成的对象的 [[Prototype]]。[[Prototype]] 里的 <code>constructor</code> 属性又会反过来引用函数本身。因为 <code>user</code> 的原型是 <code>User.prototype</code> ,它自然也能够通过 <code>constructor</code> 获取到 <code>User</code> 函数,进而获取到自己的 [[Prototype]]。比较绕是吧?</p>
<p>ES5.1 之后加了几个新的 API 帮助我们操作对象的 [[Prototype]],自此以后 JavaScript 才真的有自由操控原型的能力。它们是:</p>
<ul>
<li><p><code>Object.prototype.isPrototypeOf</code></p></li>
<li><p><code>Object.create</code></p></li>
<li><p><code>Object.getPrototypeOf</code></p></li>
<li><p><code>Object.setPrototypeOf</code></p></li>
</ul>
<p>注:以上方法并不完全是 ES5.1 的,<code>isPrototypeOf</code> 是 ES3 就有的,<code>setPrototypeOf</code> 是 ES6 才有的。但它们的规范都在 ES6 中修改了一部分。</p>
<p>下面的例子里,<code>Object.create</code> 创建 <code>child</code> 对象,并把 [[Prototype]] 设置为 <code>parent</code> 对象。<code>Object.getPrototypeOf</code> 可以直接获取对象的 [[Prototype]]。<code>isPrototypeOf</code> 能够判断一个对象是否在另一个对象的原型链上。</p>
<pre><code class="js">var parent = {
_name: 'David',
getName: function() { return this._name },
}
var child = Object.create(parent)
Object.getPrototypeOf(child) // parent
parent.isPrototypeOf(child) // true
Object.prototype.isPrototypeOf(child) // true
child instanceof Object // true</code></pre>
<p>既然有 <code>Object.getPrototypeOf</code>,自然也有 <code>Object.setPrototypeOf</code> 。这个函数可以修改任何对象的 [[Prototype]] ,包括内建类型。</p>
<pre><code class="js">var anotherParent = {
name: 'Alex'
}
Object.setPrototypeOf(child, anotherParent)
Object.getPrototypeOf(child) // anotherParent
// 修改数组的 [[Prototype]]
var a = []
Object.setPrototypeOf(a, anotherParent)
a instanceof Array // false
Object.getPrototypeOf(a) // anotherParent</code></pre>
<p>灵活使用以上的几个方法,我们可以非常轻松地创建原型链,或者在已知原型链中插入自定义的对象,玩法只取决于想象力。我们以此修改一下上面的模拟继承的例子:</p>
<pre><code class="js">function Parent() {}
function Child() {}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child</code></pre>
<p>因为 <code>Object.create(..)</code> 传入的参数会作为 [[Prototype]] ,所以这里有一个有意思的小技巧。我们可以用 <code>Object.create(null)</code> 创建一个没有任何属性的对象。这个技巧适合做 proxy 对象,有点类似 Ruby 中的 <code>BasicObject</code>。</p>
<h2>尴尬的私生子 __proto__</h2>
<p>说到操作 [[Prototype]] 就不得不提 <code>__proto__</code> 。这个属性是一个 getter/setter ,可以用来获取和设置任意对象的 [[Prototype]] 。</p>
<pre><code class="js">child.__proto__ // equal to Object.getPrototypeOf(child)
child.__proto__ = parent // equal to Object.setPrototypeOf(child, parent)</code></pre>
<p>它本来不是 ES 的标准,无奈众多浏览器早早地都实现了这个属性,而且应用得还挺广泛的。到了 ES6 为了向下兼容性只好接纳它成为标准的一部分。这是典型的现实倒逼标准的例子。</p>
<p>看看 MDN 的描述都充满了怨念。</p>
<blockquote><p>The use of <strong>proto</strong> is controversial, and has been discouraged. It was never originally included in the EcmaScript language spec, but modern browsers decided to implement it anyway. Only recently, the <strong>proto</strong> property has been standardized in the ECMAScript 6 language specification for web browsers to ensure compatibility, so will be supported into the future. It is deprecated in favor of Object.getPrototypeOf/Reflect.getPrototypeOf and Object.setPrototypeOf/Reflect.setPrototypeOf (though still, setting the [[Prototype]] of an object is a slow operation that should be avoided if performance is a concern).</p></blockquote>
<p><code>__proto__</code> 是不被推荐的用法。大部分情况下我们仍然应该用 <code>Object.getPrototypeOf</code> 和 <code>Object.setPrototypeOf</code> 。什么是少数情况,待会再讲。</p>
<h2>ES6: class 语法糖</h2>
<p>不得不说开发者世界受 OO 的影响非常之深,虽然 ES5 给了我们足够灵活的 API ,但是:</p>
<ul>
<li><p>很多人还是倾向于用 class 来组织代码。</p></li>
<li><p>很多类库、框架创造了自己的 API 来实现 class 的功能。</p></li>
</ul>
<p>产生这一现象的原因有很多,但事实如此。而且如果用别人的轮子,有些事是我们无法选择的。也许是看到了这一现象,ES6 时代终于有了 class 语法,有望统一各个类库和框架不一致的类实现方式。来看一个例子:</p>
<pre><code class="js">class User {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
let user = new User('David', 'Chen')
user.fullName() // David Chen</code></pre>
<p>以上的类定义语法非常直观,它跟以下的 ES5 语法是一个意思:</p>
<pre><code class="js">function User(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
User.prototype.fullName = function() {
return '' + this.firstName + this.lastName
}</code></pre>
<p>ES6 并没有改变 JavaScript 基于原型的本质,只是在此之上提供了一些语法糖。<code>class</code> 就是其中之一。其他的还有 <code>extends</code>,<code>super</code> 和 <code>static</code> 。它们大多数都可以转换成等价的 ES5 语法。</p>
<p>我们来看看另一个继承的例子:</p>
<pre><code class="js">class Child extends Parent {
constructor(firstName, lastName, age) {
super(firstName, lastName)
this.age = age
}
}</code></pre>
<p>其基本等价于:</p>
<pre><code class="js">function Child(firstName, lastName, age) {
Parent.call(this, firstName, lastName)
this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Child.constructor = Child</code></pre>
<p>无疑上面的例子更加直观,代码组织更加清晰。这也是加入新语法的目的。不过虽然新语法的本质还是基于原型的,但新加入的概念或多或少会引起一些连带的影响。</p>
<h2>extends 继承内建类的能力</h2>
<p>因为语言内部设计原因,我们没有办法自定义一个类来继承 JavaScript 的内建类的。继承类往往会有各种问题。ES6 的 <code>extends</code> 的最大的卖点,就是不仅可以继承自定义类,还可以继承 JavaScript 的内建类,比如这样:</p>
<pre><code class="js">class MyArray extends Array {
}</code></pre>
<p>这种方式可以让开发者继承内建类的功能创造出符合自己想要的类。所有 Array 已有的属性和方法都会对继承类生效。这确实是个不错的诱惑,也是继承最大的吸引力。</p>
<p>但现实总是悲催的。<code>extends</code> 内建类会引发一些奇怪的问题,很多属性和方法没办法在继承类中正常工作。举个例子:</p>
<pre><code class="js">var a = new Array(1, 2, 3)
a.length // 3
var b = new MyArray(1, 2, 3)
b.length // 0</code></pre>
<p>如果说语法糖可以用 Babel.js 这种 transpiler 去编译成 ES5 解决 ,扩充的 API 可以用 polyfill 解决,但是这种内建类的继承机制显然是需要浏览器支持的。而目前唯一支持这个特性的浏览器是………… Microsoft Edge 。</p>
<p>好在这并不是什么致命的问题。大多数此类需求都可以用封装类去解决,无非是多写一点 wrapper API 而已。而且个人认为封装和组合反而是比继承更灵活的解决方案。</p>
<h2>super 带来的新概念(坑?)</h2>
<h3>super 在 constructor 和普通方法里的不同</h3>
<p>在 constructor 里面,<code>super</code> 的用法是 <code>super(..)</code>。它相当于一个函数,调用它等于调用父类的 constructor 。但在普通方法里面,<code>super</code> 的用法是 <code>super.prop</code> 或者 <code>super.method()</code>。它相当于一个指向对象的 [[Prototype]] 的属性。这是 ES6 标准的规定。</p>
<pre><code class="js">class Parent {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
class Child extends Parent {
constructor(firstName, lastName, age) {
super(firstName, lastName)
this.age = age
}
fullName() {
return `${super.fullName()} (${this.age})`
}
}</code></pre>
<p>注意:Babel.js 对方法里调用 <code>super(..)</code> 也能编译出正确的结果,但这应该是 Babel.js 的 bug ,我们不该以此得出 <code>super(..)</code> 也可以在非 constructor 里用的结论。</p>
<h3>super 在子类的 constructor 里必须先于 this 调用</h3>
<p>如果写子类的 <code>constructor</code> 需要操作 <code>this</code> ,那么 <code>super</code> 必须先调用!这是 ES6 的规则。所以写子类的 <code>constructor</code> 时尽量把 <code>super</code> 写在第一行。</p>
<pre><code class="js">class Child extends Parent {
constructor() {
this.xxx() // invalid
super()
}
}</code></pre>
<h3>super 是编译时确定,不是运行时确定</h3>
<p>什么意思呢?先看代码:</p>
<pre><code class="js">class Child extends Parent {
fullName() {
super.fullName()
}
}</code></pre>
<p>以上代码中 <code>fullName</code> 方法的 ES5 等价代码是:</p>
<pre><code class="js">fullName() {
Parent.prototype.fullName.call(this)
}</code></pre>
<p>而不是</p>
<pre><code class="js">fullName() {
Object.getPrototypeOf(this).fullName.call(this)
}</code></pre>
<p>这就是 <code>super</code> 编译时确定的特性。不过为什么要这样设计?个人理解是,函数的 <code>this</code> 只有在运行时才能确定。因此在运行时根据 <code>this</code> 的原型链去获得上层方法并不太符合 class 的常规思维,在某些情况下更容易产生错误。比如 <code>child.fullName.call(anotherObj)</code> 。</p>
<h3>super 对 static 的影响,和类的原型链</h3>
<p><code>static</code> 相当于类方法。因为编译时确定的特性,以下代码中:</p>
<pre><code class="js">class Child extends Parent {
static findAll() {
return super.findAll()
}
}</code></pre>
<p><code>findAll</code> 的 ES5 等价代码是:</p>
<pre><code class="js">findAll() {
return Parent.findAll()
}</code></pre>
<p><code>static</code> 貌似和原型链没关系,但这不妨碍我们讨论一个问题:类的原型链是怎样的?我没查到相关的资料,不过我们可以测试一下:</p>
<pre><code class="js">Object.getPrototypeOf(Child) === Parent // true
Object.getPrototypeOf(Parent) === Object // false
Object.getPrototypeOf(Parent) === Object.prototype // false
proto = Object.getPrototypeOf(Parent)
typeof proto // function
proto.toString() // function () {}
proto === Object.getPrototypeOf(Object) // true
proto === Object.getPrototypeOf(String) // true
new proto() //TypeError: function () {} is not a constructor</code></pre>
<p>可见自定义类的话,子类的 [[Prototype]] 是父类,而所有顶层类的 [[Prototype]] 都是同一个函数对象,不管是内建类如 <code>Object</code> 还是自定义类如 <code>Parent</code> 。但这个函数是不能用 <code>new</code> 关键字初始化的。虽然这种设计没有 Ruby 的对象模型那么巧妙,不过也是能够自圆其说的。</p>
<h3>直接定义 object 并设定 [[Prototype]]</h3>
<p>除了通过 <code>class</code> 和 <code>extends</code> 的语法设定 [[Prototype]] 之外,现在定义对象也可以直接设定 [[Prototype]] 了。这就要用到 <code>__proto__</code> 属性了。“定义对象并设置 [[Prototype]]” 是唯一建议用 <code>__proto__</code> 的地方。另外,另外注意 <code>super</code> 只有在 <code>method() {}</code> 这种语法下才能用。</p>
<pre><code class="js">let parent = {
method1() { .. },
method2() { .. },
}
let child = {
__proto__: parent,
// valid
method1() {
return super.method1()
},
// invalid
method2: function() {
return super.method2()
},
}</code></pre>
<h2>总结</h2>
<p>JavaScript 的原型是很有意思的设计,从某种程度上说它是更加纯粹的面向对象设计(而不是面向类的设计)。ES5 和 ES6 加入的 API 能更有效地操控原型链。语言层面支持的 <code>class</code> 也能让忠于类设计的开发者用更加统一的方式去设计类。虽然目前 <code>class</code> 仅仅提供了一些基本功能。但随着标准的进步,相信它还会扩充出更多的功能。</p>
<p>本文的主题是原型系统的变迁,所以并没有涉及 getter/setter 和 <code>defineProperty</code> 对原型链的影响。想系统地学习原型,你可以去看 <a href="https://link.segmentfault.com/?enc=l54Cx7TTQauDOAbVSe2EHg%3D%3D.Uq%2F6VTsBWAW2IlCpNs7FmLAYzlT2hJVS%2BO1P4yFd%2Fq7CEPpSZcrEn459DbDuapowe3Pi8QDhwJJdw88lFuuTEujQB654xjXvd35AnGgIEc5smngl0CktcqkIhOQzXKYg" rel="nofollow">You Don't Know JS: this & Object Prototypes</a> 。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=nc%2B%2FRogI76b%2BTKMhmGEiRA%3D%3D.0ZxPkrW9bUYkNUR%2FHwWYwZhqdXOxVN69iibp0dnA2gsCF4Zwh3xoldLQKIQbMWt254DfKGb9Oa4%2B9OFhEuhueb5Ua3yo7koGOA%2FGvJ%2F2lPK%2FGzImC7QiQj%2F8NFy7voNc" rel="nofollow">You Don't Know JS: this & Object Prototypes</a><br><a href="https://link.segmentfault.com/?enc=MwZfHnCCwzDNYn6oV7VFjQ%3D%3D.AFT6H2svzeZ1Rho95xyA61RRu78mO1Ng1EhwruhZ%2BSmfDMXZMN%2F7%2FahuGHvVkPrac2N07JjBwdLY4T3SxG8%2FAJWQiPA4WlKSi%2Fsufrfdf4s%3D" rel="nofollow">You Don't Know JS: ES6 & Beyond</a><br><a href="https://link.segmentfault.com/?enc=zEcOF3xNsA5TRz79JByQlA%3D%3D.eCqjEVQRRU8y%2FoIbRQZIMJUpwlEITioSbQgeAgIPUg%2BVn%2FhFWlEFsoyFEBr0%2BcjZfPJEvDQ9%2BRe5nQfjuuEN4g%3D%3D" rel="nofollow">Classes in ECMAScript 6 (final semantics)</a><br><a href="https://link.segmentfault.com/?enc=79XOEWpvtdSgonrtK6PnGg%3D%3D.sogHU2fasrkjsszYNGVe0rN5o9%2BW2tteme3Tf1Hs2pAO22NSk7%2Fg%2FzUE9V8n88622iurqOe5y%2BndesLWJSGvjnFy%2FNdGkWgUI8yibIqfKqW8n6RBdyDqc7tHX4WxH%2BwW" rel="nofollow">MDN: Object.prototype.__proto__</a></p>
JavaScript Date.parse 的小坑
https://segmentfault.com/a/1190000002640166
2015-04-01T11:53:58+08:00
2015-04-01T11:53:58+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
4
<h2>TL;DR</h2>
<p><code>new Date</code> 和 <code>Date.parse</code> 在格式化某些日期字符串的时候,时区具有不确定性,最好用 <strong>moment.js</strong> 这类工具去处理。</p>
<h2>不确定的日期字符串</h2>
<p>事情的起源是客户跟我说网页上的某个日期总是比实际日期少一天。经过一步步 debug 后发现问题在此:</p>
<pre><code>js</code><code>// 客户时区为美国东部时区夏令时
new Date("2015-03-31") // Mon Mar 30 2015 20:00:00 GMT-0400 (EDT)
</code></pre>
<p>明明写的 31 号,为什么生成的对象是 30 号的?因为 <code>new Date</code> 把它解析为 2015-03-31 00:00:00 ,时区为 UTC 。美国东部时区是减 4 小时的,于是就变成了前一天 20:00:00 。</p>
<p>那么 <code>new Date</code> 传入的时间字符串有没有规律可循呢?</p>
<h2>混乱的规律</h2>
<p><code>new Date</code> 和 <code>Date.parse</code> 使用的是同样的解析规律,只是一个返回 Date object 另一个返回毫秒数。为了方便查看结果,以下例子只用 <code>new Date</code> 。但请记住它们遵循一样的规律。</p>
<p><code>new Date</code> 可以传入一个日期字符串来生成对象的,官方规定日期字符串需要符合 <a rel="nofollow" href="http://tools.ietf.org/html/rfc2822#page-14">RFC2822</a> 或者 <a rel="nofollow" href="http://www.w3.org/TR/NOTE-datetime">ISO8601</a> 的格式。拿上面的日期举个例子,前者可以写成 "Mar 31 2015" 后者可以写成 "2015-03-31" 。</p>
<p>如果日期字符串不符合这两种标准,<code>new Date</code> 对结果概不负责……</p>
<p>不过就算符合标准了,结果还是有点不同的。看几个例子:</p>
<pre><code>js</code><code>new Date("Mar 31 2015") // Tue Mar 31 2015 00:00:00 GMT-0400 (EDT)
new Date("2015-03-31") // Mon Mar 30 2015 20:00:00 GMT-0400 (EDT)
</code></pre>
<p>RFC2822 的格式如果不带时区,<code>new Date</code> 会当做本地时区处理,而 ISO8601 格式则会当做 UTC 时区处理。</p>
<p>是有点绕人,但只要记住这个规律不就完了吗?骚年你太天真了…… 因为 ES6 草案为了简化这种情况,规定所有不带时区的字符串都默认为本地时区。注意这是草案,所以结果你懂的。</p>
<h2>解决方案</h2>
<p>一种解决方案是每次格式化日期都严格指定时区,以防止各种幺蛾子情况出现,比如:</p>
<pre><code>js</code><code>new Date("2015-03-31T00:00:00-04:00") // Tue Mar 31 2015 00:00:00 GMT-0400 (EDT)
</code></pre>
<p>不过鉴于人都是懒惰的,这种情况交给工具做更靠谱,比如 moment.js 。</p>
<pre><code>js</code><code>moment("2015-03-31").toDate()
</code></pre>
<h2>参考链接</h2>
<p><a rel="nofollow" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse">Date.parse() - JavaScript | MDN</a></p>
Service Object 整理和小结
https://segmentfault.com/a/1190000002505972
2015-01-22T17:49:06+08:00
2015-01-22T17:49:06+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
1
<h2>TL;DR</h2>
<p>这篇文章整理了 Service Object 的一套 Convention,用 PORO 结合 Rails 的功能完成了一个例子,并介绍了一些其他思路。</p>
<h2>Why Service Object (Again)?</h2>
<p>Service Object 已经不是一个新鲜话题了。从 <a rel="nofollow" href="http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/">7 Patterns to Refactor Fat ActiveRecord Models</a> 开始就有不少人尝试照着这些 pattern 从 Rails 项目抽象出各种 object 进行解耦。这些 pattern 也催生了不少 gem ,比如关注 policy 的 <a rel="nofollow" href="https://github.com/elabs/pundit">Pundit</a> ,关注 form 的 <a rel="nofollow" href="https://github.com/apotonick/reform">Reform</a>,关注 presenter 的……太多不举例了……</p>
<p>但 Service Object 却很少看到有相关的 gem ,DHH 还跟别人讨论了大半天 <a rel="nofollow" href="https://twitter.com/dhh/status/280743505641484288">service 的话题</a>,看起来每个人对于 Service Object 的理解都有些差别。这是为什么?</p>
<p>我个人的理解是,Service Object 没有一个固定的形态,因为它完全就是业务逻辑的封装。</p>
<p>那讨论还有意义吗?有。因为我们需要它,需要更有效率地使用和讨论它。</p>
<h2>Convention over Configuration</h2>
<p>说到效率,就不得不提关于 Rails 的核心哲学 Convention over Configuration 。如果你的理解仅仅是用 Convention 省去了配置,那并不是它的全部含义。</p>
<p>Convention 的另一层意义在于,它就是一个最佳实践的表现形式,Rails 本质上是一系列 web 开发中最佳实践的集合体。通过 Convention ,Rails 开发者不仅可以避免为一些琐碎的事情费神,从而去处理真正需要关心的事情。更重要的是,遵循 Convention 的 Rails 项目都长得差不多,这使得 Rails 开发者的经验能够跨项目地重用。而且开发者互相交流起来天生就在一个频道上。We are on the same page !</p>
<p>但真正的项目千差万别,Rails 为我们做的毕竟有限,在没有 Convention 覆盖到的地方,开发者的理解就各有千秋了。Service Object 就是其中最典型的例子。有自己想法的人自然可以不拘泥于形式,但也有不少人在疑惑 “怎么才算 Service Object” 和 “如何更好地实现 Service Object” ?</p>
<p>这篇文章推荐了一些 Service Object 的 Convention ,来自 <a rel="nofollow" href="http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html">这篇文章</a> 和 <a rel="nofollow" href="https://netguru.co/blog/service-objects-in-rails-will-help">这篇文章</a>。</p>
<h2>Service Object & Convention</h2>
<p>简单的说,Service Object 是用对象来封装一段操作。通常情况下我们用它封装业务逻辑 。关于什么情况下该使用 Service Object ,7 patterns 里的话我觉得已经总结得很好了。</p>
<ol>
<li>操作逻辑很复杂。</li>
<li>操作涉及到多个 model。</li>
<li>操作涉及到调用外部服务。</li>
<li>操作不是 model 该关注的逻辑(比如定时清理过期数据)。</li>
<li>操作涉及到一系列不同的具体实现(比如用 token 认证或者 password 认证),策略模式就是干这个的。</li>
</ol>
<p>因为和业务逻辑比较接近,Service Object 通常用在 Controller 中,但也可以单独使用(比如在 job , console 或者其他 Service Object 中嵌套使用)。</p>
<p>Service Object 的一些简单的约定:</p>
<ol>
<li>一个 Service Object 只做一件事。</li>
<li>每个 Service Object 一个文件,统一放在 app/services 目录下。</li>
<li>命名采用动作,比如 SignEstimate ,而不是 EstimateSigner 。</li>
<li>instance 级别实现两个接口,<code>initialize</code> 负责传入所有依赖,<code>call</code> 负责调用。</li>
<li>class 级别实现一个接口 <code>call</code> ,用于简单的实例化 Service Object 然后调用 call 。</li>
<li>
<code>call</code> 的返回值默认为 <code>true/false</code> ,也可以有更复杂的形式,比如 StatusObject 。</li>
</ol>
<p>以上这些只是约定,不是必须遵循的规范。比如你可以叫 <code>SignEstimateService</code>,把 <code>call</code> 改成 <code>invoke</code>,<code>execute</code>,<code>perform</code> 或者其他你喜欢的。但记住 <strong>如果没有特殊的理由,请让你的所有 Service Object 保持一致的约定</strong> 。</p>
<p>一个 Service Object 的例子:</p>
<pre><code>ruby</code><code># app/services/sign_estimate.rb
class SignEstimate
def self.call(*args)
new(*args).call
end
def initialize(estimate, params)
@estimate = estimate,
@params = params
end
def call
# Do whatever you want
# Return true/false
end
end
</code></pre>
<p>如何使用它:</p>
<pre><code>ruby</code><code>class EstimatesController
# POST /estimates/:id/sign
def sign
@estimate = Estimate.find(params[:id])
if SignEstimate.call(@estimate, estimate_params)
# Do something like redirect
else
# Display errors
end
end
end
</code></pre>
<h2>With Rails's help</h2>
<p>Service Object 就是一个纯粹的 Ruby Object (PORO),但这不代表我们不能复用 Rails 已有的功能。我一直觉得为了开发便利,可以视情况增加 MVC 之外的层,但如果抛弃 Rails 已有的东西就本末倒置了,比如没必要为了建一个 Form Object 而把 Model 层的 validation 全部扔到 Form Object 里面去。</p>
<p>上个例子里的 SignEstimate 是我自己项目中的例子,实际使用时我会需要对 Estimate 这个 Model 做额外的 validation ,但我不希望把这些逻辑放到 Model 层去,因为它们只有在 Sign 这个过程中有用 。所以我会用到 ActiveModel 。</p>
<p>另外,因为约定中每个 Service Object 中都有类方法 <code>call</code> 。我们可以把它单独抽出来变成一个 Concern 。我比较喜欢用组合的方式,你也可以用继承来实现。</p>
<pre><code>ruby</code><code>module Serviceable
extend ActiveSupport::Concern
class_methods do
def call(*args)
new(*args).call
end
end
end
class SignEstimate
include Serviceable
include ActiveModel::Model
include ActionLoggable
attr_reader :estimate
delegate :signer_name,
:sign_via,
:signer_driver_lic,
:signer_ssn,
:errors,
to: :estimate
validates :signer_name, presence: true
validates :sign_via, inclusion: { in: %w[driver_lic ssn] }
validates :signer_driver_lic, presence: true, if: :sign_via_driver_lic?
validates :signer_ssn, presence: true, if: :sign_via_ssn?
def initialize(estimate, params)
@estimate = estimate,
@params = params
end
def call
valid? && persist
end
private
def persist
@estimate.transaction do
sign_estimate!
close_sales_lead!
transform_prospect_to_customer!
copy_forms!
end
create_activity
write_log('sign_est', resource: @estimate, operator: @estimate.assigned_to)
true
rescue ActiveRecord::RecordInvalid
false
end
def sign_via_driver_lic?
sign_via == 'driver_lic'
end
def sign_via_ssn?
sign_via == 'ssn'
end
end
</code></pre>
<p>有些方法是纯粹的业务逻辑,具体实现就不写了。这里我用了以下 Rails 的功能:</p>
<ol>
<li>
<code>ActiveSupport::Concern</code> 来抽离 Service Object 的公共接口。</li>
<li>
<code>ActiveModel::Model</code> 来做校验,你也可以只要 <code>ActiveModel::Validations</code>。</li>
<li>
<code>delegate</code> 方法来代理需要验证的字段和 <code>errors</code> 接口。这样添加的错误就自动给 <code>@estimate</code> 了。</li>
<li>
<code>ActionLoggable</code> 是我自己写的 Concern ,用来添加一些操作日志,生成报表用。</li>
</ol>
<p>统一的约定可以方便抽离接口,PORO 可以方便我添加任何其他东西,不用考虑继承了什么类带来的 side effect 。而且易于理解和修改。</p>
<h2>Status Object as Return Value</h2>
<p><a rel="nofollow" href="https://netguru.co/blog/service-objects-in-rails-will-help">这篇文章</a> 的作者也提到了返回值的约定。一个有意思的概念是,当需要返回的内容比较复杂时(操作失败返回错误信息),可以抽象出一个 Object 去封装返回值,这就是 Status Object 。它定义了一个 <code>success?</code> 接口来判断操作是否成功,其他的信息就由各人自己 DIY 了。</p>
<pre><code>ruby</code><code>class Success
attr_reader :data
def initialize(data)
@data = data
end
end
class Error
attr_reader :error
def initialize(error)
@error = error
end
end
</code></pre>
<p>你也可以用自己的方法来 one liner</p>
<pre><code>ruby</code><code>Success = Struct(:data) { def success?; true; end }
Error = Struct(:error) { def success?; false; end }
</code></pre>
<p>怎么用呢:</p>
<pre><code>ruby</code><code>def call
if valid?
# Dirty business logic...
Success.new(@estimate)
else
Error.new("customized error message")
end
end
</code></pre>
<p>我目前没有用到 Status Object 的必要,所以没有深入的例子。感兴趣的可以参考作者原文的例子,他在 <code>AuthorizationError</code> 里带了 code 和 message ,方便 Controller 做针对性的操作。</p>
<p>Service Object 的构建很灵活,你可以想出最符合自己习惯的用法,形成约定。但记住 <strong>不要为了 pattern 而 pattern</strong> ,在满足要求的同时,尽量保持简单,重用 Rails 已有的功能,提高效率 。</p>
<h2>Testing</h2>
<p>Service Object 的所有依赖都是在初始化的时候注入的,所以也可以很方便地使用 <code>double</code> 或者 Fake Object 来伪造对象,隔离依赖。</p>
<p>但根据我的实际经验,大部分 Service Object 都要跟 Model 层打交道,<strong>建议这种情况下全部用真实的 Model 对象,不要 Mock/Stub</strong> 。</p>
<p>因为 Service Object 的存在必然会抽走一部分的 Model 逻辑。Model 中也许就只剩下比较简单的 validation, callback 和自定义方法了(比如关联保存 relationship,我不大喜欢 autosave)。这时候 Model 的 Unit Test 实际上是不足以保证数据库层面的功能正确的。如果 Service Object 都 Mock 了,那么保证功能的正确性就要靠 Integration Test 了。<strong>测试是为了保证系统稳定性的,为了一些速度降低稳定性不值得</strong>。</p>
<h2>Another Way</h2>
<p>刚才的 Service Object 是一种思路,但并不是没有其他的方法去抽离业务逻辑。这里是我在学习过程中看到的一些其他 gem 。都可以达到相同的目的。我最终没用只是因为觉得这些 gem 的理念不太符合。不代表它们不好。</p>
<h3>ActiveType</h3>
<p><a rel="nofollow" href="https://github.com/makandra/active_type">ActiveType</a> 的理念是尽量利用 ActiveRecord 的 lifecycle,你可以写一个自己的 Object ,但是像 Model 一样把逻辑封装进 validation 和 callback,从而让自定义的 Object 有和 ActiveRecord 一样的接口和使用方式。</p>
<p>这是我在 <a rel="nofollow" href="https://leanpub.com/growing-rails">Growing Rails Applications in Practice</a> 一书里看到的。里面提倡的一点就是把所有接口 CRUD 化,接口统一了之后就容易做更高层次的抽象。这个理念还是值得学习的。如果你没看过这本书,强烈建议看一看。</p>
<p>有人会疑惑为什么不用 ActiveModel 自己造?因为有太多的东西仍然在 ActiveRecord 里面。有些看似简单的需求很难实现,比如 <code>save</code> 之前调用你的 Object 的 validation 和内部的 Model 的 validation。 如果你想自己写一个 Object 并沿袭 ActiveRecord 的接口,你需要做不少事情,但最终会发现自己仿造 ActiveRecord 写了一个 Object 。可能还有各种问题……</p>
<p>上面的 Service Object 用 ActiveType 写,可能就是这个样子:</p>
<pre><code>ruby</code><code>class SignEstimate < ActiveType::Record[Estimate]
validates :signer_name, presence: true
validates :sign_via, inclusion: { in: %w[driver_lic ssn] }
validates :signer_driver_lic, :signer_state, presence: true, if: :sign_via_driver_lic?
validates :signer_ssn, presence: true, if: :sign_via_ssn?
before_save :set_sign_date
after_save :close_sales_lead
after_save :transform_prospect_to_customer
after_save :copy_forms
after_commit :create_activity, on: :update
after_commit :write_log, on: :update
after_rollback :clear_sign_info
end
</code></pre>
<p>这种 Service Object 在 Controller 中就跟 Model 一样用。喜不喜欢这种思路就见仁见智了。</p>
<h3>Wisper</h3>
<p><a rel="nofollow" href="https://github.com/krisleech/wisper">Wisper</a> 是一个以 pub/sub 为理念的 gem ,主张用 event + callback 的方式解耦。我是在搜索 “为什么 Rails observer 被废掉了” 的过程中偶然找到这个 gem 的。它同样可以用来解耦业务逻辑。</p>
<p>我个人不喜欢这种方式。因为有 callback 的代码很难被外层 Object 封装,比如官方的 Controller 例子很难抽象成统一的接口,进而使用 <code>respond_with</code> 。</p>
<p>不管怎么样,我想作为一个 900+ stars 的 gem 它还是很成功的。也许它是 observer 的一个很好的替代品。</p>
<h2>Conclusions</h2>
<p>Service Object 是 Rails 开发者回归 OO 方式思考的结果之一。它并不违反 Rails way,我们也没必要把任何操作都封装成 Service Object。解决方案通常是跟适用场景息息相关的,No silver bullet 。作为 Rails 开发者,充分利用它的优势加上适当地拥抱变化,可以让人走的更远。</p>
<h2>References</h2>
<p><a rel="nofollow" href="http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/">7 Patterns to Refactor Fat ActiveRecord Models</a></p>
<p><a rel="nofollow" href="http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html">Gourmet Service Objects</a></p>
<p><a rel="nofollow" href="https://netguru.co/blog/service-objects-in-rails-will-help">Service objects in Rails will help you design clean and maintainable code. Here's how.</a></p>
<p><a rel="nofollow" href="http://pivotallabs.com/object-oriented-rails-writing-better-controllers/">Object Oriented Rails – Writing better controllers</a></p>
<p><a rel="nofollow" href="https://twitter.com/dhh/status/280743505641484288">Twitter 上 DHH 关于 Service Object 的讨论</a></p>
我的 2014 年度总结
https://segmentfault.com/a/1190000002495507
2015-01-18T23:17:44+08:00
2015-01-18T23:17:44+08:00
darkbaby123
https://segmentfault.com/u/darkbaby123
2
<h2>开篇的话</h2>
<p>转眼间一年又过去了,在新年的第一天,我按照惯例总结一下去年干了些什么,看看一年下来自己有哪些成长。这篇总结最开始发布在 <a rel="nofollow" href="https://ruby-china.org/topics/23504">Ruby China</a> 上,我转过来作为我的新博客的第一篇,希望 2015 年有个好的开端。</p>
<p>2014 年总的来说比较平淡,没什么大的波折,年初计划的事情,不出意外有一些没完成,好在同时也干了一些意料之外的事情,并且在我看来更有帮助,也算没白白浪费时间了。</p>
<h2>按计划完成的事情</h2>
<ul>
<li>整理知识,丢弃不重要的和不需要的东西。</li>
<li>把 Ember 和前端技能从初级提升到中级。</li>
<li>获得了一些 hybrid app 的经验,实际参与了两个 hybrid app 项目,一个 PhoneGap/Cordova 一个 Ionic 。</li>
<li>看完了几本 Ruby/JavaScript 有关的书。</li>
<li>玩完了两款很喜欢但一直没坚持的 iOS 游戏。</li>
</ul>
<h2>意料之外的完成的事情</h2>
<ul>
<li>娶了一个可爱的,值得爱的妞。</li>
<li>咖啡从提神工具上升到了爱好。</li>
<li>开始研究如何提升效率,用 Pocket 保存好的资源, 用 OmniFocus 做 GTD, 用 Evernote 做笔记, 用 Pomotodo 践行番茄工作法。</li>
<li>换了几个编辑器之后,最终换回 Vim ,开始深入学习 Vim 和 Vimscript 。</li>
<li>看完了一本技术无关的书,三体。</li>
</ul>
<h2>没有完成的事情</h2>
<ul>
<li>学另一门编程语言。</li>
<li>学习 iOS/Android 开发。</li>
<li>学几首英文歌。</li>
<li>积攒 StackOverflow 的 reputation 。</li>
</ul>
<h2>心得总结</h2>
<p>这是我回顾去年,最重要的几点心得。开始写的更多,但最终被我删了一半。减去不必要的东西,留下来的才更有价值。</p>
<p><strong>关注最新的知识</strong></p>
<p>多多引入获取信息的渠道,花点时间了解技术行业最新的发展,对扩展视野有很大的帮助。有些前沿知识,你不需要懂,但你要知道有这么个东西。以后碰到对应的问题,脑子里一下就会有几种大概的解决方案,而不是盲目地去 Google ,甚至连个关键词都不知道。</p>
<p>我目前获取知识的主要来源是 Twitter, Feedly 和各种 email 订阅(比如 Ruby weekly,Ember weekly,Good UI 等)。<br>
Twitter 纯粹被我当新闻客户端来用。优点是信息比较新,容易发掘相关的感兴趣的资源(别人转推),也容易获取到一些个人化的意见,比如 DHH 的各种犀利吐槽。Tom Dale 和 Yehuda 对 AngularJS/Ember.js 的讨论。这些信息不会出现在博客里,但含金量同样很高。</p>
<p>Twitter 上发现的好资源可以放到 Feedly 和 email 订阅,定期扫一下,筛选出需要的知识阅读就行。</p>
<p>但随着信息获取的日益便利,信息过载马上成为了更大的问题。Pocket 里面收藏的文章日益增多,Feedly 中未读条目越来越多,这时候就需要减负了。</p>
<p><strong>减少关注方向,集中精力在需要的事情上</strong></p>
<p>我们总有很多想要学的东西,想要完成的事情,但我们都不是超人,没有那么多时间精力。精力分散导致我尝试了很多事情,但都没到质变的程度。一年到头,感觉忙了很多,却少有拿得出来说的东西。所以想把事情做好,就要有所取舍。</p>
<p>减少关注方向,不但能让人集中更多的时间精力在少数的事情上。而且负担更少,相应的没完成的事情产生的自我否定感觉也更少。</p>
<p><strong>时间会帮你筛选最重要的事情</strong></p>
<p>制定的计划难免赶不上变化,有时是因为有更重要的事情要处理,有时是尝试一段时间后发现计划的事情对自己的帮助有限,<br>
有时是因为一些意料之外的情况。但一年下来,我发现还是完成了一些事情,并且不管计划内还是计划外,那些事情都是我最在意的。</p>
<p>不用为了没完成预定计划而自责,因为时间会逼迫你做选择,投入你最喜欢,最重要的事情,放弃价值较少的事情。</p>
<p><strong>制定计划优先考虑需要的和喜欢的,否则容易变成空谈</strong></p>
<p>接上条所说的,那为什么还需要计划呢?计划对我来说是一个大致的方向和一些我在新的一年期望达到的事情。它是愿景但非标准。</p>
<p>但制定计划也不能随便瞎扯淡,比如 “变成高富帅迎娶白富美跨上人生巅峰” 什么的。那怎么选择呢?我的想法是考虑需要的和喜欢的。需要往往代表我在某方面遇到了困难,这时候也许今天学的知识明天就能用上,这种 “付出终有回报” 的感觉会让人容易继续下去。至于做喜欢的事情,喜欢就是最大的动力,而且可以让人乐在其中,不会有种 “迫不得已不得不做” 的强迫感。</p>
<h2>2015 年计划</h2>
<p>今年我准备减少关注点,继续去年没有完成的一些事情,适当做一些新的尝试。</p>
<ul>
<li>在 Hybrid app 的道路上更进一步。</li>
<li>在测试理论上更进一步。</li>
<li>学习一门和 Erlang 类似的语言(初步意向为 Elixir),Ruby 对人友好,并发性能稍差,Node.js 性能很好,但因为 JavaScript 本身的原因,callback 写起来比较蛋疼。希望新语言能提供一个新的思路。给 web 开发带来一些改变。</li>
<li>深入学习 Vim 和 Vimscript ,提高效率。</li>
<li>积攒 StackOverflow 的 reputation 。</li>
<li>看一本技术无关的书,暂定为狼图腾。</li>
<li>提升咖啡技能,学会享受和辨别不同的单品咖啡。</li>
<li>锻炼身体,保持健康。</li>
<li>英文博客。</li>
</ul>
<p>如果你已经看到了这里,我衷心地感谢你的耐心。也非常希望能看到你的总结。一起分享,共同成长!</p>