[翻译]我们把最厉害的人炒掉了,这是我们做过最正确的决定

We fired our top talent. Best decision we ever made.

“你们不会理解我创造的一切。我TMD就是爱因斯坦,你们都是玩泥巴的小屁孩。”

我们的天才 Jekyll 医生愤怒的变身为 Hyde。(经典小说,善良的医生Jekyll,他将自己当作实验对象,结果却导致人格分裂,变成夜晚会转为邪恶Hyde的双重人格)

他是在产品设计小组、开发人员、管理人员和试用用户面前说出的这句话。我们项目的赞助商之一鲁莽的询问我们的产品何时修复严重缺陷。

天才都是反复无常的野兽。运气好时和你共事的是个疯狂的天才,其他时候他只是纯粹的疯狂。大多时候很难区分这两者。

这个故事关于一名天才团队成员的陨落,他对我们的产品架构有深入理解,有预测未来需求的不可思议的能力,有大量专业领域知识积累。

他是贡献最多的成员。他曾驾驭我们的旗舰项目。我们叫他 Rick 好了。(出自高分动画「瑞克和莫蒂」,豆瓣评分9.8)

你不会想让这个家伙在你的团队里的。

Rick 是团队里公认的天才。他是首席开发工程师及项目架构师。

任何时候任何人有关于代码的问题或任务需要帮忙,他们都会找 Rick。Rick 在办公室装了一块白板专门处理这种事情。白板上总留有无法擦去的凌乱痕迹记录过去的讨论。

无论什么时候出现特别有挑战性的问题,Rick 会处理它。Rick 电脑上安装了与生产环境服务器参数相同的服务器。他可以用这个服务器独立运行整个应用程序栈,同时在应用各个层面排查问题。

Rick 不依赖任何人。他喜欢孤军奋战在他的私人工作间。

Rick 从不需要别人造好的轮子。他什么都自己造,从无到有,因为自己造的轮子比其他凡人做的平庸之物要好太多。

很快,Rick 不再参加会议。因为太多代码需要编写,他没有时间参加会议。

Rick 关上工作间的门,白板也闲置了。他没有时间指导别人,因为他自己有很多事情需要解决。

Rick 身后开始积压工作。他造的轮子也开始出现 bug。这些压榨了他投入在新产品开发上的精力。

当然,这些 bug 出现是由于使用者操作不当,Rick 的工作没有任何问题。

在我们的项目仪表盘上,标志由绿色变成了黄色,黄色又变成了红色,红色的灯疯狂闪烁。任务一个又一个陷入不可用状态。所有人都在等待 Rick 处理。

不必担心,Rick 会把这些全都解决掉。

项目经理从赞助商那里获得了六个月的延期。六个月到了,生产准备预计还要七个月。等到一年过去了,生产准备两年了。

Rick 比之前更加高效地产出代码。他每周工作七天,每天工作十二个小时。

每个人都知道只有 Rick 能拯救团队。大家屏住呼吸等待 Rick 创造特效良药,将项目起死回生。

每天,Rick 的孤立感和好战心都在增加。面具逐渐脱离,Jekyll 变成了 Hyde。

我和项目团队第一次会议有关项目延期的两年。我突然意识到这个项目,因为它在公司里声名狼藉,我也是刚刚指派到这个项目。

我被指派进来,看看我们能不能拯救项目。

我和这个项目有关的第一场会面便是上面提到的“爱因斯坦”。

嗯······

我看了源码。Rick 说得对,除了他自己,没人理解他创造的东西。这些全都是他自己思维的工作产物。其中一些很巧妙,大部分还是复制粘贴的。它们都很独特,并不是所有东西都有文档。

我给了我们的 CIO(首席信息官)一份报告。只有 Rick 能够维护这个产品。另外,Rick 每工作一天都会让项目交付延期一周。他对项目的破坏速度比创造速度要快。

我们和 Rick 坐下来讨论他在项目中的角色。我们评估了关注点。我们回避了他和爱因斯坦的比较。我们解释了新的战略。团队需要合作,从零开始构建一个新产品。

我们的努力必须限制在一定范围内,并且仅仅完成产品基础功能。整个团队贡献代码并可以提供技术支持。不再会有瓶颈。

Rick 对此作何反应?

Rick 选择了一如既往风格,瞬间爆炸。

Rick 并不想理会这场闹剧。认为如果我们不能欣赏他的天赋,那就是我们的问题,而不是他的问题。Rick 预计不出一个月我们会狼狈而归乞求他拯救我们。

Rick 向我们咆哮说我们庙太小,容不下他这尊佛。

很遗憾,这之后,Rick 拒绝了领导的未来几个月的安排。他拒绝休息,也不同意工作移交他人。他屡次拒绝引入免费开源的框架来替代他自己的难以维护的定制工具。

他回退了其他开发者测试修复 bug 的代码变更。他宣称自己没有责任支持其他同事的工作。他不断公开藐视同事。

我们解雇了 Rick。

整整一周时间才尘埃落定。失去大将的军队需要一定时间来稳定军心。

然后我看到他们在白板前挤作一团。

合作。Rick 从不懂这个词

他们开始合作,一起设计更简单的替代产品。

新产品没有华而不实东西,也没有根据五年后的产品路线来预计需求。

Rick 的产品动态工作流支持超过一万五千种排列。事实上,我们 99% 的用例只遵循其中三分之一的路径。团队对工作流硬编码。这移除了超过 30% Rick 的工作。

每个任务并不能使用定制的硬编码组件。他们把能购买到的组件替换掉自己定制构建的组件。

这移除了 Rick 上百小时的贡献。当然这也移除了上千小时的技术债务(technical debt)。

我们和项目赞助商协商达成一致,砍掉一些边缘功能。

这些功能只服务于 5% 的试用用户组,但给在产品中占有四分之一的复杂度。

我们向试用用户组重新发布产品。产品包含 Rick 写的稳定运行的 10% 的源代码,另外用几千行新代码替换掉了十五万行难以理解的代码。

整个团队六个月完成了五年的工作量。接下来几个月时间我们把试用版扩展为完整的客户版本。

我们不仅替换掉 Rick 造的轮子,而且超越他并全面推出了产品,所有这一切只用了不到一年时间。产品的大小和复杂度只有 Rick 所做的五分之一。

尽管产品组装时间很短,使用的客户翻了十倍,但产品的效率仍提升上百倍,而且几乎没有 bug。

团队回到 Rick 其他产品中,他们剔除了 Rick 的旧代码。

团队协作三个月就重新发布了 Rick 三年时间开发的产品。

团队中不再有 Rick 这种人。我们也不再看到这种从无到有造轮子疯狂的天才。但我们的生产力居高不下。

Rick 是名非常有天赋的开发者。他能够解决复杂的商业逻辑问题,可以创造复杂精妙的结构来支持他的高级设计。Rick 不能解决的问题是如何使团队高效工作。

Master builders are cool, but skyscrapers are built by teams. (image © Warner Bros. Animation and The Lego Group)
建筑工程队长是很酷,但摩天大楼是团队盖起来的。

Rick 的存在在某些方面是有破坏性的。

首先,他创造了个人崇拜的依赖。所有问题都变成 Rick 的问题,他变成一个神话。开发者习惯放弃尝试,只等着 Rick 解决。

其次,他写的代码不易维护。他从不写文档或测试代码,他的聪明才智也无法阻止失败。他对自己可靠性的信仰让他忽略了常识。

然后,他个人有破坏性。团队成员不想和他交流想法,因为他总是批判他们。Rick 只尊重它自己,以他自己的方式生活,让其他人感觉渺小。

最后,他缺少个人责任感。所有失败都不是他的错。他坚信这一点,这也阻止了他从错误中学习和进步。

我并不认为 Rick 从开始就这个样子。我看到的是他最糟糕的样子。他经历了数年愈演愈烈的加班,面对了同事和客户逐渐增加的苛刻要求。

很遗憾,Rick 走远了。他的经理也有责任。事实上,原来的管理团队承担了责任,他们首先离开了。

不幸的是 Rick 走的太远了,以至于他不能回到正轨。即使再多的辅导、反馈、休假或指派其他项目都无法改变他的不良行为习惯。

在这一点上,整个团队知道他不好的地方。但个人崇拜的依赖如此强烈,每个人相信 Rick 是唯一的救命稻草。

其实一直都有其他选择的。

团队力量和每个单独成员的天赋无关,而是有关于他们的合作、斗志和相互尊重。

构建团队要注重发挥每个人的价值,让每个人都发挥自己的最好水平。

团结一心,他们就可以应对 Rick 都无法触及的更大的挑战。

我发表了一篇后续故事,欢迎阅读

如果觉得文章不错,欢迎点赞。

备注:一些细节(比如人名)已经处理过。我的同事中没有叫 Rick 的。

[翻译]Quora 是如何做持续部署的?

Continuous Deployment at Quora

在 2013 年 4 月 25 日中午 12 点到晚上 11 点 59 分之间,Quora 站点发布了 46 次新版本。这对于我们来说只是普通的一天。我们执行非常快的持续部署周期,代码变动提交后就直接推送到线上。这使得我们可以在各个层面上实现平行化开发。我们希望推送系统足够快,让开发者尽快看到他们对生产环境的改动(目前生产环境修订版上线平均要 6、7 分钟),同时也要注意可靠性和灵活性,让我们可以迅速响应问题。

对开发者而言,只需要一个简单的命令把代码推送到到生产环境:git push

这背后发生的事情要复杂很多。每当一个开发者把提交推送到我们的主 git 仓库,一个 post-receive 钩子会将最新的修订版加入到发版申请列表,并记录到 MySQL 数据库。(post-receive 钩子也会把提交加到 Phabricator,我们用它做代码评审。更多关于我们代码评审的相关信息参阅这个回答 Does Quora engineering use a code review process?)一个内部监控网站展示每个等待发布的修订版的状态。

一个后端服务监控发版申请列表,每当提交新的修订版,服务收集此版本代码库中所有单元测试的名字。我们有上百个测试模块和上千个独立的测试,服务会将测试分配到一些 worker 机器中并行处理。当 worker 运行完测试,它们将结果返回给测试服务,服务在发版申请的列表中标记修订版的综合结果(成功或失败,以及多少个测试失败了和失败的详情)。

同时,另一个服务监控发版申请列表,等待打包新修订版。每当提交新的修订版,它将所有需要运行在我们服务器上的代码进行归档,并打包上传到 Amazon S3。

当修订版打包好后,一个集成测试服务将修订版推送到一台单独的机器(并不是生产环境),用新的包开启 web 服务,并向服务发送请求。只有每个请求返回 200 状态码,集成测试才算通过,如果任何请求返回 4xx 或 5xx 错误码,测试失败。

最后,第四个监控发版申请列表的服务由其他三个服务调用,当修订版的测试和打包没有问题,服务向 S3 上传一个包含版本号的小型元数据文件,来标记修订版的部署(发布)。Web 服务器和其他使用相同代码的机器会周期性检查元数据文件中的版本号,如果有变化,它们会立刻从 S3 上下载最新的包。下载并解压包大概需要一分钟,然后运行新的代码只需要几秒。每台需要代码的机器会独立完成上述操作。我们将这个系统命名为 Zerg,因为在所有需要包的机器上进行部署过程很像虫族(zerg)的 rush 战术。

下图描述了后端架构:

这套系统弹性很大,很少出现失败,但正如其他复杂的系统一样,也会出现失败情况。要么测试 worker 宕机,要么测试服务失败,要么打包程序出现问题。通过这种架构,内部的失败并不会引起一致性问题(比如把未通过单元测试的代码推送到生产环境),并且大多数时候只需要重启失败的机器或服务便可以让其正常工作。

对于大多数修订版,git push 到发布大约间隔 6 分钟,这取决于其中执行时间最长的任务。目前,单元测试是时间最长的任务;打包需要 2-3 分钟,集成测试需要 3 分钟多一点。之后 10 分钟(发布之后),机器下载运行新代码,同时后面的修订版开始测试和打包。(我们不会同一时间更新所有机器,这会导致每次我们发布时,Quora 会有几分钟处于不可用状态!)选择 10 分钟的间隔是为了可靠性 —— 如果我们需要响应突发事件,可以立刻覆盖部署代码。

我认为 6 分钟的测试 + 10 分钟的部署还不够好。我们可以改进测试系统,使其并行测试来提高效率。另外,我们去除掉其他服务中无用的东西,这让我们可以将部署时间由 10 分钟缩短为 5 分钟。

系统的设计主要基于其他公司部署项目时遇到的问题。我们在公司早期就决定采用持续部署方案。在公司规模、代码库、基础架构很小时便于使用这种方案,但我们仍努力多年来维护这一流程,因为持续部署在整个开发流程和开发文化中举足轻重:

  • 持续部署让我们尽可能迅速地将产品的变更展现在用户面前,包括从 bug 的修复到主要特性等一系列东西。
  • 持续部署让我们尽可能迅速隔离并解决出现的问题。当出现 bug,你倾向于在单个提交中 debug,还是从包含一百个提交的整体发版中 debug?
  • 持续部署让我们在改进网站时不需要投入过多精力。我们直到经历了这些之后才意识到这点 —— 持续部署让我们在几分钟之内完成发现问题、快速修复、推送代码并部署到生产环境。如果这些动作时间过长,开发者可能会想“我难道要花一个小时来坐下跟踪代码的推送吗?”。更糟糕的是,如果隔天部署,开发者会想“我难道要明天再审查一遍然后测试代码吗”
  • 持续部署减少了跟踪不同发布状态的多个版本这一工作上的投入。代码是在生产环境还是在发版列表的未推送状态,都一目了然。
  • 持续部署让我们有测试的习惯。毫无疑问,测试非常重要。伴随着变更需要立刻上线的压力,我们没有之后再写测试的余地。我们总是先写好测试。
  • 持续部署很有趣!写代码很有趣,我们的部署过程也应该同样有趣。

通过减少每次版本上线需要的时间,并加强测试,我们每天可以上线更多的修订版,并有效减小变更伴随的阻碍。这也是 Quora 这类快速起步的公司需要的东西。

[翻译]区块链是怎么回事?做一个应用带你了解

How does blockchain really work? I built an app to show you.

Wikipedia 上对区块链的描述:

维护不断增长的记录(称作区块)的分布式数据库。

听上去很简单,但到底是怎么回事呢?

我们用一款开源命令行界面 Blockchain CLI 来详细说明区块链。

我也构建了一个浏览器可以访问的在线版

安装命令行界面

首先请确保安装 Node.js

然后在终端里运行下面命令:

npm install blockchain-cli -g
blockchain

你将看到 👋 Welcome to Blockchain CLI!blockchain → 提示已准备好接受命令。

区块是什么样子的?

你可以在命令行中输入 blockchainbc 来查看你当前的区块链。你将看到下图类似的区块。

  • 索引(区块):这是哪个区块?(初始区块索引为 0)
  • 哈希:区块有效吗?
  • 前个哈希:之前一个区块有效吗?
  • 时间戳:区块什么时候添加的?
  • 数据:区块中存的什么信息?
  • 随机数(Nonce):我们重复了多少次才找到有效的区块?

初始区块

每个区块链都会以一个 🏆 Genesis Block 作为开始。你接下来将会看到每个区块都关联前一个区块。所以我们开采第一个区块前,要有初始区块。

当一个新的区块被开采出来会发生什么?

让我们来开采我们的第一个区块,在提示框输入 mine freeCodeCamp♥︎ 命令。
区块链根据最后一个区块生成当前索引和前个哈希。我们现在的区块链最后一个区块就是初始区块。

  • 索引:o+1 = 1
  • 前个哈希:0000018035a828da0…
  • 时间戳:区块什么时候添加的?
  • 数据:freeCodeCamp❤
  • 哈希:??
  • 随机数(Nonce):??

哈希值如何计算?

哈希值是固定长度的数值,用来标识唯一数据。

哈希通过将索引、前个哈希、时间戳、数据、随机数作为输入后计算得出。

CryptoJS.SHA256(index + previousHash + timestamp + data + nonce)

SHA256 算法通过给定的输入,计算出一个唯一的哈希。相同的输入总会生成相同的哈希。

你注意到哈希开头的四个 0 了吗?

开头的四个 0 是有效哈希的基本要求。开头 0 的个数被称为难度值(difficulty)。

function isValidHashDifficulty(hash, difficulty) {
  for (var i = 0, b = hash.length; i < b; i ++) {
      if (hash[i] !== '0') {
          break;
      }
  }
  return i >= difficulty;
}

这就是众所周知的工作量证明系统

什么是随机数?

随机数是用来寻找有效哈希的一个数字。

let nonce = 0;
let hash;
let input;
while(!isValidHashDifficulty(hash)) {     
  nonce = nonce + 1;
  input = index + previousHash + timestamp + data + nonce;
  hash = CryptoJS.SHA256(input)
}

随机数不断迭代,直到哈希有效。在我们的例子中,有效的哈希值至少要四个 0 开头。寻找有效哈希对应随机数的过程就称为开采(挖矿)。

随着难度值的提升,有效哈希的数量逐步减少,我们需要投入更多资源来找到一个有效哈希。

为什么这很重要?

因为它确保了区块链不可变。

如果我们有一个这样的区块链 A → B → C,有人想修改区块 A 上的数据。会发生下面情况:

  1. 修改区块 A 上的数据。
  2. 区块 A 的哈希变动,因为计算哈希所用的数据变化了。
  3. 区块 A 无效,因为它的哈希不是四个 0 开头。
  4. 区块 B 的哈希变动,因为计算区块 B 的哈希所用到的区块 A 的哈希值变化了。
  5. 区块 B 无效,因为它的哈希不是四个 0 开头。
  6. 区块 C 的哈希变动,因为计算区块 C 的哈希所用到的区块 B 的哈希值变化了。
  7. 区块 C 无效,因为它的哈希不是四个 0 开头。

修改一个区块的唯一方式就是重新开采这个区块以及它之后的所有区块。因为新的区块不断增加,基本不可能修改区块链。

我希望这份说明对你有帮助。
如果你想检出例子的在线版本,移步 http://blockchaindemo.io

[翻译]惊叹!这个盲人程序员是这样写代码的

Software development 450 words per minute

我认为你第一次看到我的工作间肯定这样想 —— “总感觉少些什么”。没有显示器和鼠标,却有个人敲打着键盘,不知注视着哪里。

这就是我,我同事可以证明我没问题。我是位于坦佩雷(芬兰西南部一座城市)的 Vincit 写字楼中的一名软件开发者。我双目失明。这篇文章中我将讲述有关我工作中的事情。

你真的什么都看不到吗?

准确来说,我觉察到阳光和其他明亮的光线,不过也仅限这些。其实,这对我的工作也并没有什么帮助。

你工作内容是什么?

和大部分人一样:忙时写代码,闲时和同事吹逼。我做全栈项目,主攻后端。兼职访问顾问 – 或称监管,随你如何称呼。

你如何使用电脑?

我用的电脑是一台运行 Windows 10 的普通笔记本。是其中的软件让一切变得神奇。我使用一款叫做屏幕阅读器的程序来访问电脑。屏幕阅读器监听屏幕上的变化并通过盲文(需要单独的盲文设备)或合成的声音来展示给用户。这并不是你如今听到的各种智能助理的合成声音。我使用一种机械声音,每分钟能说 450 个单词。相比较而言,英语正常语速每分钟 120-150 个单词。我有一个怪癖:我既说英语也说芬兰语,我用芬兰语合成器读英语,因为老旧的屏幕阅读器在语言之间切换不够智能,所以我习惯这样做。下面是个例子是阅读这个段落,我能听懂。

https://www.vincit.fi/wp-content/uploads/2017/08/mpsample.mp3?_=1

下面是英语合成器发出的声音:

https://www.vincit.fi/wp-content/uploads/2017/08/essample.mp3?_=2

鼠标对于我来说并不是非常有用,所以我仅仅通过键盘工作。在座的各位应该十分熟悉我用到的命令:方向键和 tab 键控制窗口内的移动,alt+tab 切换窗口等等。屏幕阅读器也有很多自己的快捷键,比如阅读活动窗口的不同区域或开关一些功能特性。

有趣的是阅读网页和其他格式化文档。你看,屏幕阅读器分块呈现信息。每一块可能是一行,也可能是一个单词、一个字母,亦或是文本的片段。举个例子,我在网页中按向下的方向键,我听到页面的下一行。我并不能像正常人一样用眼睛从屏幕上阅读内容。相反,我听到一块一块的内容,或跳过我不感兴趣的部分。

语音或盲文并不能描绘出窗口的显示布局。信息以线性方式呈现给我。如果你把网页复制粘贴进记事本,你就能明白我看到的网页是什么样子的。就是剥离大部分格式的多行文本。然而屏幕阅读器可以获取网页上的 HTML 语法,所以我也能知道超链接、标题、表单等等。事实上,如果非复选框元素展示成复选框样式,我并不能知道这是复选框。我之后将写一篇文章详细讲述这些内容,记住我刚刚举的是个“反人类”例子。
(译者注:突然感到自责和羞愧,深深明白了一个道理:不要用各种有含意义的传统标签 hack 布局和样式,也不要因为 css 的强大而懒得使用各种有含义的传统标签。共勉)

我花费大量时间工作在命令行上。事实上我通常用浏览器和编辑器,很少用其他图形应用程序。相比那些为鼠标用户打造的图形界面,我发现用命令行处理手边的工作更加高效。

既然我如此热爱命令行,为什么我却要选择 Windows 这个并不以命令行出名的操作系统呢?答案很简单:Windows 是最方便的操作系统。NVDA是我所选择的屏幕阅读器,它是开源的并且维护比其他阅读器更频繁。如果上天再我一次机会,我可能会选 Mac 系统,因为我认为它是易用性和功能性平衡的典范。不幸的是 Mac 系统上的屏幕阅读器 VoiceOver 经历了漫长的发布周期从而被遗忘,并且它的导航模型和我独特的工作方式并不协调。当然这里也有一个 Gnome 桌面上的屏幕阅读器,虽然用户很少,依然被很好地维护着,不过还有一些不完善的地方和我日常工作不协调。所以,我选择 Windows。由 GNU 诞生的 Git Bash 和其他命令行工具弥补了 Windows 内置命令行的缺陷。

你如何写代码?

我花费好长时间才明白为什么大家觉得这个问题是个很高深的问题。记得我上面说过一行一行地阅读文本吗?我也是通过这种方式读代码。通常我会跳过无用的行,或仅听半行来获取内容,但当我需要知道完整信息的时候,我不得不像读小说一样读完所有东西。我当然无法阅读整个代码库。这种情况下我会在脑中抽象一部分代码:这个组件输入 x 返回 y,并不用关心细节逻辑。

这种阅读方式让我和正常同事的工作方式有些区别。举个例子,当代码审查时,我喜欢看原始 diff 输出,并列窗口显示 diff 对我并不适用,而且还容易让人分心。有修改的代码行上用符号 + 和 – 比用不同背景色标注也要好太多,并不是因为我不能获知颜色名字,而是因为在新增的一行中,读“加”这个字比读“带复杂阴影的高亮红色”用更短的时间。(嘿,我说你呢 Gerrit (一款代码审查工具))

你或许会认为缩进和其他代码格式和我无关,因为都是基本的视觉问题。并不是这样,正确的缩进对我的帮助和正常开发者一样。当我用盲文(比语音更加高效)读代码时,我像其他正常程序员一样清楚代码结构。当我进入一段有缩进或无缩进的代码时,我也会得到语音提醒。这些信息帮助我在脑中描绘代码结构。事实上我学的第一门语言就是 Python (PHP 不算),它强制使用代码缩进,这对我来说并不是问题。我有众多理由来强烈建议使用整洁统一的代码风格,其中之一就是不要让我的生活变得更加艰难了,好吗。

你喜欢哪款编辑器?

剧透一下:这个答案并不是以 V 或者 E 开头(我虽然通过命令行用 Vim 来写 git commit 信息和其他备注。我认为我在这场圣战中是中立的)(译者注:Vim 和 Emacs 梗)一年前我认为 Notepad++ 最棒,它是轻量级的做工精细的文本编辑器。然而一年前我还没有接触大规模 Java 项目,当我接触这种项目时,意味着我应该在 Notepad++ 和理智之间做个选择。最后我选择理智,抛弃 Notepad++ 转投 IntelliJ IDEA 的怀抱。从那之后 IntelliJ IDEA 便是我首选编辑器。我曾对各种 IDE 有深深怨念,它们大多数在纯键盘流操作下麻烦又低效。如果我视力没问题,我肯定早就跳到 IDE 阵营了。

但你可能会问,为什么当初选 Notepad++。还有其他很多更先进的轻量级编辑器,比如 Sublime 或 Atom。原因很简单:屏幕阅读器无法访问它们。Vim 一类的文本编辑器也是如此,我使用的屏幕阅读器对命令行程序的支持有问题,在这些编辑器上无法处理多于 commit 信息的文本。很遗憾,可用性决定了我能够使用的工具。即使我不能高效工作,也不是什么大问题。

你编写过前端代码吗?

你应该认为前端开发和视觉有关,注定与盲人程序员无缘。基本上是这样。我从来不自己做概念原型,我做都是有界面,需要随后加入功能的项目。

然而,我也做过 Angular 和 React 工作任务。怎么会这样?如今很多 APP 基于浏览器。举个例子,我曾花费两周时间为一个 Angular APP 增加国际化支持。我并不需要做任何视觉上的改动。

我发现对于我这类开发者开说,像 Bootstrap 这类的库简直是上天的礼物。正因为栅格系统(Bootstrap的响应式布局解决方案),我可以自己构建一个粗糙的界面。尽管如此,我做的有关界面的改动在呈现给用户之前仍然要有一双眼睛检查。所以,总而言之,我可以在一定程度上做些前端开发,至少不是和表现层太相关。

有什么其他没有提到的东西?

其实这篇文章有很多东西没有表达出来。正如上文所承诺,我将全力以赴写一篇文章,有关制作易访问网页的艺术,因为一言未尽是我讨厌的事情之一。我不会半途而废的,敬请期待。

[翻译]每个程序员都该知道的五大定律

5 laws every developer should know

定律-或称法则,可以指导我们并让我们在同伴的错误中学习。这篇文章中,我将介绍我每次设计或实现软件时出现在我脑海的五大定律。其中有些和开发有关,有些和系统组织有关。它们可以帮助你成为合格的软件工程师。

墨菲定律

“凡事可能出错,就一定出错。”

这条定律来源于 Edward Murphy —— 一名航天工程师在 50 年代初对火箭测试失败的回应。这条定律给我们的启示是永远在系统关键地方使用防御性设计,因为系统某些地方总会出错!

这条定律很容易引入软件工程领域。当你将软件暴露给终端用户,他们会创造性地输入一些出人意料的内容,使系统宕机。所以你需要让你的软件足够健壮,能够检测并警告非预期行为。

当你在机器上运行软件时,任何地方都有可能发生问题 —— 从硬盘上的系统到数据中心的电力供应。所以你必须确保你设计的架构在每个层级都可以应对故障。

我曾经有机会领略过几次墨菲定律。
举个例子,我曾经在一个批处理框架中使用字符串“null”来表示空值,我并不认为这有问题,直到有个名字叫“Null”的用户提交了一个交易订单,我们的报表流程中断了几个小时……
还有一次,在另一个项目中。当所有东西都准备好部署到生产环境了,突然 Azure 基础设施故障导致我们运行自动化脚本的服务器宕机了。

现实世界中的经验教训提醒着我生活的艰难 —— “凡事可能出错,就一定出错”。
所以,心中牢记墨菲定律,设计健壮的软件。

Knuth定律

“在(至少大部分)编程中,过早优化是万恶之源。”

这条定律也是 Donald Knuth 的经典语录之一,它告诫我们不要过早优化应用程序中的代码,直到必须优化时再优化。

的确,简单易读的源码可以满足 99% 的性能需要,并能提高应用的可维护性。最开始使用简单的解决方案也让后期性能出现问题时更容易迭代和改进。

垃圾自动回收的编程语言中,字符串的连接常常是过早优化的例子。在 Java 或 C# 中,String 对象是不可变的,我们学会使用其他结构动态创建字符串,比如 StringBuilder。但事实上直到你分析完个应用程序前,你并不知道 String 对象创建了多少次并对性能的产生多大影响。所以首先编写尽可能整洁的代码,之后在必须的时候再优化,往往这样做更有意义。

然而,这条规则并不应该阻止你去学习编程语言的性能权衡和正确的数据结构。并且,正如所有其他性能问题,你在优化前要测量开销。

North定律

“每一个决定都是一次权衡”

好吧,我承认这是取自 Dan North 的演讲 Decisions,Decisions,它目前还不是公认的定律。
但这条语录影响了我做的每个决定,所以我把它放在这。

开发者日复一日的生活中,我们每天都做无数个大大小小的决定。从命名变量到自动化(手动)任务,再到定义平台架构。

这条语录强调无论你做的选择是什么,你总会放弃一个或多个选项

但这不是最重要的。
最重要的是理智地做出决定,了解其他选项,清楚你为什么不选择它们。你要始终根据当前你掌握的信息来权衡并做出决定。

但是如果后来你了解到新的信息,并发现之前的决定是错误的,这也没关系。关键是记清楚你为什么做出那个决定,重新评估新的选项之后再做出新的理智的决定。

重复一遍

“每一个决定都是一次权衡”

所以,做出选择并对所有选项心知肚明。

Conway定律

“系统设计的架构受限于生产设计,反映出公司组织的沟通架构”

在 60 年代,一位名叫 Melvin Conway 的工程师注意到公司组织结构影响到他们开发的系统的设计。他用一篇论文描述了这个观点,并命名为“Conway定律”。

这条定律很适用于软件开发领域,甚至体现到代码层面上。交付软件组件的各个团队组织结构直接影响到组件的设计。

举个例子,一个集中式的开发者团队会开发出各组件耦合的整体应用。另一方面,分布式的团队会开发出单独的(微)服务,每一部分关注点分离清晰。

这些设计没有好坏之分,但它们都是受到团队沟通方式的影响。在全球有大量独立开发者的开源项目,通常是模块化和可重用库,这就是很有说服力的例子。

如今,将大的集成应用解耦成微服务已成趋势。这很棒,因为这可以加速交付使用项目。但你也应该牢记 Conway定律,在公司组织构建中投入与技术开发同样多的工作。

琐碎定律(帕金森琐碎定律)

“组织成员投入大量精力到琐碎的事情上。”

这条定律论点是在会议中花费的时间与事情的价值成反比。的确是这样,人们更愿意把注意力和观点放在他们熟悉的事物上,而不是复杂的问题上。

帕金森给出一个例子,一场会议中,成员们讨论两件事:为公司建核反应堆和为员工建车棚。建反应堆是一件巨大而复杂的任务,没有人能完全掌控全局。他们完全信赖流程和系统专家,并很快接受了项目。

另一边,建车棚是一般人都可以做的,每个人都可以对颜色有意见。事实上,每个会议成员都会表达自己的意见,使得建车棚的决议所花费的时间远远超过建反应堆的。

这条定律在软件行业十分出名,这个故事随后也被称为车棚效应

举个例子,开发者会花费更多时间到讨论正确缩进或函数命名,而不是讨论类的职责或应用架构。这是因为每个人都能认知几个字符的变动,但项目架构的变动则需要巨大的认知负载

你能注意到的车棚效应的另一个例子是 Scrum 演示。不要误会我,我喜欢演示,我认为这是一个很好的机会来面对用户并获得对应用程序的反馈。但通常 Scrum 演示过程中的讨论会转向琐碎问题,而不是审视全局。这些讨论也很重要,但你应该注意权衡更重要更复杂的问题。

一旦你了解这种规律,你将在会议和交流中发觉这种行为。
我并不是让你在每次讨论中避免“小”问题,提高你的意识可以帮助你关注真正的问题,并为这些会议做好准备。

结论

这五条定律只是我们行业中总结出的教训中一些例子。随着软件开发经验的增长,我们将会学会更多。
尽管其中某些定律现在看起来是常识,我始终坚信了解这些原则可以帮助你识别这些模式并做出反应。

[翻译]有哪些奇怪的排序算法

原文https://www.quora.com/What-is-the-strangest-sorting-algorithm

下面是Nipun Ramakrishnan的回答

睡眠排序

另一个搞笑算法流传于 4chan 的 /prog/ 板块。无从查证具体出自哪位程序员,伪代码如下

procedure printNumber(n)
    sleep n seconds
    print n
end

for arg in args
    run printNumber(arg) in background
end
wait for all processes to finish

算法运行如下:
对于数组中每个元素 x,开启一个新程序:
* 休眠 x 秒
* 打印 x
所有元素同时开始计时。
只适用于非负数字。

下面是Ryan Turner的回答

Bogo 排序(猴子排序)

Bogo 排序,名字很奇怪。它是愚蠢排序中的一员。
主要来说,算法就是你把元素随机排列。
如果没有排好序,再次把元素随机排列。
如果还没有排好序,你懂的。下面是个例子:
4, 7, 9, 6, 5, 5, 2, 1 (未排序)
2, 5, 4, 7, 5, 9, 6, 1 (随机排列)
1, 4, 5, 6, 9, 7, 5, 2 (再次随机排列)
1, 2, 4, 5, 5, 6, 7, 9 (天呐,真幸运)
你不停地随机排序,直到得到一个有序数组。
毫无疑问这是最低效的排序算法之一,除非你非常非常幸运。它时间复杂度是令人窒息的 O(n!),而且随着元素数量增加,很有 O(∞) 的趋势。

下面是Tyler Schroeder的回答

量子 Bogo 排序

我是量子 Bogo 排序的粉丝:

  • 随机排列数组中元素。
  • 如果数组没有排好序,摧毁当前宇宙(这一步就拜托你了)
  • 存活的宇宙将会有排好序的数组。
    时间复杂度仅仅 O(n)
    注意:这种算法依赖于量子力学的平行宇宙理论的可靠性。如果量子力学的平行宇宙理论不准确,这个算法时间复杂度达不到 O(n)

下面是Yi Wang的回答

打印店页码排序

这并不是我发明的,我从别处看到的。
一个学生去打印店打印材料。他需要两份,但并没有直接打印两份,而是将每一页打印了两次,像下面这样:
需要的页码顺序: 1 2 3 4 … N; 1 2 3 4 … N
手上的页码顺序: 1 1 2 2 3 3 4 4 …. N N
他开始对打印材料排序,取一页放在左边,然后取一页放在右边。打印店老板看不下去了,直接把材料拿过来。
老板首先取一页放在左边,然后两页放在右边,再然后两页左边,两页右边……
排序速度瞬间翻倍 ……
(网友评论:这是归纳,不是排序)

下面是其他网友的回答

慢排序

这是一个非常幽默却没什么用的排序算法。它基于“合而不治”的原则(分治算法基本思想“分而治之”的反义词,文字游戏),它由 Andrei Broder 和 Jorge Stolfi 于 1986 年发表在论文《Pessimal Algorithms and Simplexity Analysis(最坏排序和简单性分析)》中,伪代码如下:

function slowSort(array,start,end){
    if( start >= end ) return; //已经不能再慢了
    middle = floor( (start+end)/2 );

    //递归
    slowSort(array,start,middle);
    slowSort(array,middle+1,end);

    //比较得出最大值放在队尾
    if( array[end] < array[middle] )
        swap array[end] and array[middle]

    //去掉最大值之后再排序
    slowsort(array,start,end-1);
}
  • 递归排序好前一半
  • 递归排序好后一半
  • 比较中间和队尾的值,得到整个数组的最大值,将最大值放到队尾。
  • 去掉最大值,递归整个数组

Stack 排序

从 StackOverflow 上搜索标题含有“数组排序”的帖子,复制粘贴并运行其中的代码片段,直到数组排好序。我认为这种排序算法事实上验证了整个数组。它被发表在xkcd网站上,这里有一个在线版的具体实现stacksort

随机排序

运行如下:
创建一个随机程序。
传入数组并运行随机程序。
如果程序的输出恰好是排好序的,完成。
否则重复上面过程。

太阳能比特翻转排序

太阳发出的阿尔法粒子偶尔能够翻转内存中的比特位,所以这种算法主要基于希望这种翻转能够使元素正确排序。运行方式如下:

检查数组是否排好序。
如果排好序,返回这个数组。
如果没有,等 10 秒钟并祈祷太阳辐射使得比特位翻转,而且使得数组排好序,重复第一步。

意大利面排序

这是一种线性时间算法,是需要 O(n) 空间的稳定排序。它需要并行处理器。简单来说,假设我们排序一列自然数。排序方法需要使用很多根生的意大利面条。

将数据按比例转换成表示意大利面条长度的数字。
在每根面条上写下数字,并将面条折断成数字表示的长度。
把所有面条攥成一捆并把底部在平面上敲击。
取出最突出的一根面条,也就是最长的一根,获取上面的数字,转换成原始的数据并记录下来。
重复这个过程直到处理完所有意大利面。

指鹿为马排序

这个算法时间复杂度 O(n)。
聚集一帮人并向他们展示数组。
询问他们这个数组是否是排序好的。
干掉其中认为没有排序好的人。
重复几次,直到所有人同意这个数组是排序好的。

智能设计排序

无论你的数组状态是什么样的,它都算是排好序的。
解释:原始输入按照某种顺序的概率是 1/(n!)。概率是如此小,(当前的顺序)归结于运气成分显然是荒谬的,所以它是按照“智能设计”排序过的。所以完全可以说数组已经排好序了,只是不是我们传统意义上的“升序”。如果按照我们传统观点对它进行操作,只会让它乱序。(“智能设计”涉及宗教和哲学,不过多解释

互联网排序

这是一种冒泡排序,但每次比较都依靠互联网的搜索。比如 “0.211 和 0.75 哪个大?”

委员会排序

排序一个包含 N 个自然数的数组,首先用纸打印出 N 份整个数组。
然后在办公室周围选择几个恰好路过的倒霉委员。每个委员对应数组中的一个数字。
给每个委员一份打印的数组,并让他们通过开会或其他手段,来决定自己代表的数字应该在有序数组中的位置。
当这些委员有结论并答复你时,数组自然排好序了。

[翻译]十行HTML代码实现增强现实

原文Augmented Reality in 10 Lines of HTML

你想通过网络实现增强现实吗?现在你只需要 10 行 HTML 代码!真的!让我带你看一看代码,非常简单。

我们最近发布了AR.js。你不需要安装任何应用,用你的手机通过网络就能体验到强大的增强现实。但让我们更进一步,看一下如何让你也创作出自己的增强现实体验。多亏了神奇的a-frame,最短的 AR.js 只有 10 行 HTML 代码。你可以在codepen看到在线版:

<!-- Augmented Reality on the Web in 10 lines of html! https://github.com/jeromeetienne/ar.js --> 
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://rawgit.com/jeromeetienne/ar.js/master/aframe/build/aframe-ar.js"></script>
<script>THREEx.ArToolkitContext.baseURL = 'https://rawgit.com/jeromeetienne/ar.js/master/three.js/'</script>
<body style='margin : 0px; overflow: hidden;'>
    <a-scene embedded artoolkit='sourceType: webcam;'>
        <a-box position='0 0.5 0' material='opacity: 0.5;'></a-box>
        <a-marker-camera preset='hiro'></a-marker-camera>
    </a-scene>
</body>

我们来一行一行看.

引入库

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://rawgit.com/jeromeetienne/ar.js/master/aframe/build/aframe-ar.js"></script>
<script>THREEx.ArToolkitContext.baseURL = 'https://rawgit.com/jeromeetienne/ar.js/master/three.js/'</script>

首先,你需要引入a-frame,一款MozillaVR引领的开发 VR 体验的利器。A-frame 包含了 three.js。然后你只需要为 a-frame 引入 AR.js。AR.js能让 AR 中的 3d 显示在你的手机上高速运行,哪怕是 2、3 年前的旧手机。

定义 Body

<body style='margin : 0px; overflow: hidden;'>
    <!-- ... -->
</body>

这一步,国际惯例。就像你在所有 HTML 页面中做的一样,定义 body。

创建 3d 场景

<a-scene embedded artoolkit='sourceType: webcam;'>
    <!-- put your 3d content here -->
</a-scene>

然后,我们将要创建我们的 a-farme 场景。我们当然也需要加入 ARToolkit 组件。ARToolkit是一个开源库,我们通过它来实现摄像头定位。

添加简单的内容

<a-box position='0 0.5 0' material='opacity: 0.5;'></a-box>

一旦我们创建了 3d 场景,我们可以开始向里面添加对象。在这行代码中,我们添加了一个简单的盒子。然后我们修改了它的材质,让它变得透明。我们也改变了它的位置,所以它出现在 AR 标识(AR marker)的上方。

增加 AR 摄像头

<a-marker-camera preset='hiro'></a-marker-camera>

在最后一步,我们增加一个摄像头。我们预设一个 ‘hiro’(来自Hiro marker)最后,我们让它像你手机一样移动。是不是很简单?

恭喜!你完成了。你仅用了 10 行 HTML 代码实现了增强现实,手机上也能运行飞快,而且免费。

[翻译]理解 JavaScript 作用域

Understanding Scope in JavaScript

简介

JavaScript 有个特性称为作用域。尽管对于很多开发新手来说,作用域的概念不容易理解,我会尽可能地从最简单的角度向你解释它们。理解作用域能让你编写更优雅、错误更少的代码,并能帮助你实现强大的设计模式。

什么是作用域?

作用域是你的代码在运行时,各个变量、函数和对象的可访问性。换句话说,作用域决定了你的代码里的变量和其他资源各个区域中的可见性。

为什么是作用域?最小访问原则

那么,限制变量的可见性,不允许你代码中所有的东西在任意地方都可用的好处是什么?其中一个优势,是作用域为你的代码提供了一个安全层级。计算机安全中,有个常规的原则是:用户只能访问他们当前需要的东西。

想想计算机管理员吧。他们在公司各个系统上拥有很多控制权,看起来甚至可以给予他们拥有全部权限的账号。假设你有一家公司,拥有三个管理员,他们都有系统的全部访问权限,并且一切运转正常。但是突然发生了一点意外,你的一个系统遭到恶意病毒攻击。现在你不知道这谁出的问题了吧?你这才意识到你应该只给他们基本用户的账号,并且只在需要时赋予他们完全的访问权。这能帮助你跟踪变化并记录每个人的操作。这叫做最小访问原则。眼熟吗?这个原则也应用于编程语言设计,在大多数编程语言(包括 JavaScript)中称为作用域,接下来我们就要学习它。

在你的编程旅途中,你会意识到作用域在你的代码中可以提升性能,跟踪 bug 并减少 bug。作用域还解决不同范围的同名变量命名问题。记住不要弄混作用域和上下文。他们是不同的特性。

JavaScript中的作用域

在 JavaScript 中有两种作用域

  • 全局作用域
  • 局部作用域

当变量定义在一个函数中时,变量就在局部作用域中,而定义在函数之外的变量则从属于全局作用域。每个函数在调用的时候会创建一个新的作用域。

全局作用域

当你在文档中(document)编写 JavaScript 时,你就已经在全局作用域中了。JavaScript 文档中(document)只有一个全局作用域。定义在函数之外的变量会被保存在全局作用域中。

// the scope is by default global
var name = 'Hammad';

全局作用域里的变量能够在其他作用域中被访问和修改。

var name = 'Hammad';

console.log(name); // logs 'Hammad'

function logName() {
    console.log(name); // 'name' is accessible here and everywhere else
}

logName(); // logs 'Hammad'

局部作用域

定义在函数中的变量就在局部作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。

// Global Scope
function someFunction() {
    // Local Scope ##1
    function someOtherFunction() {
        // Local Scope ##2
    }
}

// Global Scope
function anotherFunction() {
    // Local Scope ##3
}
// Global Scope

块级声明

块级声明包括ifswitch,以及forwhile循环,和函数不同,它们不会创建新的作用域。在块级声明中定义的变量从属于该块所在的作用域。

if (true) {
    // this 'if' conditional block doesn't create a new scope
    var name = 'Hammad'; // name is still in the global scope
}

console.log(name); // logs 'Hammad'

ECMAScript 6 引入了letconst关键字。这些关键字可以代替var

var name = 'Hammad';

let likes = 'Coding';
const skills = 'Javascript and PHP';

var关键字不同,letconst关键字支持在块级声明中创建使用局部作用域。

if (true) {
    // this 'if' conditional block doesn't create a scope

    // name is in the global scope because of the 'var' keyword
    var name = 'Hammad';
    // likes is in the local scope because of the 'let' keyword
    let likes = 'Coding';
    // skills is in the local scope because of the 'const' keyword
    const skills = 'JavaScript and PHP';
}

console.log(name); // logs 'Hammad'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defined

一个应用中全局作用域的生存周期与该应用相同。局部作用域只在该函数调用执行期间存在。

上下文(Context)

很多开发者经常弄混作用域和上下文,似乎两者是一个概念。但并非如此。作用域是我们上面讲到的那些,而上下文通常涉及到你代码某些特殊部分中的this值。作用域指的是变量的可见性,而上下文指的是在相同的作用域中的this的值。我们当然也可以使用函数方法改变上下文,这个之后我们再讨论。在全局作用域中,上下文总是 Window 对象。

// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
console.log(this);

function logFunction() {
    console.log(this);
}
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
// because logFunction() is not a property of an object
logFunction(); 

如果作用域定义在一个对象的方法中,上下文就是这个方法所在的那个对象

class User {
    logName() {
        console.log(this);
    }
}

(new User).logName(); // logs User {}

(new User).logName()是创建对象关联到变量并调用logName方法的一种简便形式。通过这种方式你并不需要创建一个新的变量。

你可能注意到一点,就是如果你使用new关键字调用函数时上下文的值会有差异。上下文会设置为被调用的函数的实例。考虑一下上面的这个例子,用new关键字调用的函数。

function logFunction() {
    console.log(this);
}

new logFunction(); // logs logFunction {}

当在严格模式(strict mode)中调用函数时,上下文默认是 undefined。

执行环境

为了解决掉我们从上面学习中会出现的各种困惑,“执行环境(context)”这个词中的“环境(context)”指的是作用域而并非上下文。这是一个怪异的命名约定,但由于 JavaScript 的文档如此,我们只好也这样约定。

JavaScript 是一种单线程语言,所以它同一时间只能执行单个任务。其他任务排列在执行环境中。当 JavaScript 解析器开始执行你的代码,环境(作用域)默认设为全局。全局环境添加到你的执行环境中,事实上这是执行环境里的第一个环境。

之后,每个函数调用都会添加它的环境到执行环境中。无论是函数内部还是其他地方调用函数,都会是相同的过程。

每个函数都会创建它自己的执行环境。

当浏览器执行完环境中的代码,这个环境会从执行环境中弹出,执行环境中当前环境的状态会转移到父级环境。浏览器总是先执行在执行栈顶的执行环境(事实上就是你代码最里层的作用域)。

全局环境只能有一个,函数环境可以有任意多个。
执行环境有两个阶段:创建和执行。

创建阶段

第一阶段是创建阶段,是函数刚被调用但代码并未执行的时候。创建阶段主要发生了 3 件事。

  • 创建变量对象
  • 创建作用域链
  • 设置上下文(this)的值

变量对象

变量对象(Variable Object)也称为活动对象(activation object),包含所有变量、函数和其他在执行环境中定义的声明。当函数调用时,解析器扫描所有资源,包括函数参数、变量和其他声明。当所有东西装填进一个对象,这个对象就是变量对象。

'variableObject': {
    // contains function arguments, inner variable and function declarations
}

作用域链

在执行环境创建阶段,作用域链在变量对象之后创建。作用域链包含变量对象。作用域链用于解析变量。当解析一个变量时,JavaScript 开始从最内层沿着父级寻找所需的变量或其他资源。作用域链包含自己执行环境以及所有父级环境中包含的变量对象。

'scopeChain': {
    // contains its own variable object and other variable objects of the parent execution contexts
}

执行环境对象

执行环境可以用下面抽象对象表示:

executionContextObject = {
    'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
    'variableObject': {}, // contains function arguments, inner variable and function declarations
    'this': valueOfThis
}

代码执行阶段

执行环境的第二个阶段就是代码执行阶段,进行其他赋值操作并且代码最终被执行。

词法作用域

词法作用域的意思是在函数嵌套中,内层函数可以访问父级作用域的变量等资源。这意味着子函数词法绑定到了父级执行环境。词法作用域有时和静态作用域有关。

function grandfather() {
    var name = 'Hammad';
    // likes is not accessible here
    function parent() {
        // name is accessible here
        // likes is not accessible here
        function child() {
            // Innermost level of the scope chain
            // name is also accessible here
            var likes = 'Coding';
        }
    }
}

你可能注意到了词法作用域是向前的,意思是子执行环境可以访问name。但不是由父级向后的,意味着父级不能访问likes。这也告诉了我们,在不同执行环境中同名变量优先级在执行栈由上到下增加。一个变量和另一个变量同名,内层函数(执行栈顶的环境)有更高的优先级。

闭包

闭包的概念和我们刚学习的词法作用域紧密相关。当内部函数试着访问外部函数的作用域链(词法作用域之外的变量)时产生闭包。闭包包括他们自己的作用域链、父级作用域链和全局作用域。

闭包不仅能访问外部函数的变量,也能访问外部函数的参数。

即使函数已经return,闭包仍然能访问外部函数的变量。这意味着return的函数允许持续访问外部函数的所有资源。

当你的外部函数return一个内部函数,调用外部函数时return的函数并不会被调用。你必须先用一个单独的变量保存外部函数的调用,然后将这个变量当做函数来调用。看下面这个例子:

function greet() {
    name = 'Hammad';
    return function () {
        console.log('Hi ' + name);
    }
}

greet(); // nothing happens, no errors

// the returned function from greet() gets saved in greetLetter
greetLetter = greet();

 // calling greetLetter calls the returned function from the greet() function
greetLetter(); // logs 'Hi Hammad'

值得注意的是,即使在greet函数return后,greetLetter函数仍可以访问greet函数的name变量。如果不使用变量赋值来调用greet函数return的函数,一种方法是使用()两次()(),如下所示:

function greet() {
    name = 'Hammad';
    return function () {
        console.log('Hi ' + name);
    }
}

greet()(); // logs 'Hi Hammad'

共有作用域和私有作用域

在许多其他编程语言中,你可以通过 public、private 和 protected 作用域来设置类中变量和方法的可见性。看下面这个 PHP 的例子

// Public Scope
public $property;
public function method() {
  // ...
}

// Private Sccpe
private $property;
private function method() {
  // ...
}

// Protected Scope
protected $property;
protected function method() {
  // ...
}

将函数从公有(全局)作用域中封装,使它们免受攻击。但在 JavaScript 中,没有 共有作用域和私有作用域。然而我们可以用闭包实现这一特性。为了使每个函数从全局中分离出去,我们要将它们封装进如下所示的函数中:

(function () {
  // private scope
})();

函数结尾的括号告诉解析器立即执行此函数。我们可以在其中加入变量和函数,外部无法访问。但如果我们想在外部访问它们,也就是说我们希望它们一部分是公开的,一部分是私有的。我们可以使用闭包的一种形式,称为模块模式(Module Pattern),它允许我们用一个对象中的公有作用域和私有作用域来划分函数。

模块模式

模块模式如下所示:

var Module = (function() {
    function privateMethod() {
        // do something
    }

    return {
        publicMethod: function() {
            // can call privateMethod();
        }
    };
})();

Module 的return语句包含了我们的公共函数。私有函数并没有被return。函数没有被return确保了它们在 Module 命名空间无法访问。但我们的共有函数可以访问我们的私有函数,方便它们使用有用的函数、AJAX 调用或其他东西。

Module.publicMethod(); // works
Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined

一种习惯是以下划线作为开始命名私有函数,并返回包含共有函数的匿名对象。这使它们在很长的对象中很容易被管理。向下面这样:

var Module = (function () {
    function _privateMethod() {
        // do something
    }
    function publicMethod() {
        // do something
    }
    return {
        publicMethod: publicMethod,
    }
})();

立即执行函数表达式(IIFE)

另一种形式的闭包是立即执行函数表达式(Immediately-Invoked Function Expression,IIFE)。这是一种在 window 上下文中自调用的匿名函数,也就是说this的值是window。它暴露了一个单一全局接口用来交互。如下所示:

(function(window) {
    // do anything
})(this);

使用 .call(), .apply() 和 .bind() 改变上下文

Call 和 Apply 函数来改变函数调用时的上下文。这带给你神奇的编程能力(和终极统治世界的能力)。你只需要使用 call 和 apply 函数并把上下文当做第一个参数传入,而不是使用括号来调用函数。函数自己的参数可以在上下文后面传入。

function hello() {
    // do something...
}

hello(); // the way you usually call it
hello.call(context); // here you can pass the context(value of this) as the first argument
hello.apply(context); // here you can pass the context(value of this) as the first argument

.call().apply()的区别是 Call 中其他参数用逗号分隔传入,而 Apply 允许你传入一个参数数组。

function introduce(name, interest) {
    console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
    console.log('The value of this is '+ this +'.')
}

introduce('Hammad', 'Coding'); // the way you usually call it
introduce.call(window, 'Batman', 'to save Gotham'); // pass the arguments one by one after the contextt
introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // pass the arguments in an array after the context

// Output:
// Hi! I'm Hammad and I like Coding.
// The value of this is [object Window].
// Hi! I'm Batman and I like to save Gotham.
// The value of this is [object Window].
// Hi! I'm Bruce Wayne and I like businesses.
// The value of this is Hi.

Call 比 Apply 的效率高一点。

下面这个例子列举文档中所有项目,然后依次在控制台打印出来。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Things to learn</title>
</head>
<body>
    <h1>Things to Learn to Rule the World</h1>
    <ul>
        <li>Learn PHP</li>
        <li>Learn Laravel</li>
        <li>Learn JavaScript</li>
        <li>Learn VueJS</li>
        <li>Learn CLI</li>
        <li>Learn Git</li>
        <li>Learn Astral Projection</li>
    </ul>
    <script>
        // Saves a NodeList of all list items on the page in listItems
        var listItems = document.querySelectorAll('ul li');
        // Loops through each of the Node in the listItems NodeList and logs its content
        for (var i = 0; i < listItems.length; i++) {
          (function () {
            console.log(this.innerHTML);
          }).call(listItems[i]);
        }

        // Output logs:
        // Learn PHP
        // Learn Laravel
        // Learn JavaScript
        // Learn VueJS
        // Learn CLI
        // Learn Git
        // Learn Astral Projection
    </script>
</body>
</html>

HTML文档中仅包含一个无序列表。JavaScript 从 DOM 中选取它们。列表项会被从头到尾循环一遍。在循环时,我们把列表项的内容输出到控制台。

输出语句包含在由括号包裹的函数中,然后调用call函数。相应的列表项传入 call 函数,确保控制台输出正确对象的 innerHTML。

对象可以有方法,同样函数对象也可以有方法。事实上,JavaScript 函数有 4 个内置方法:

  • Function.prototype.apply()
  • Function.prototype.bind() (Introduced in ECMAScript 5 (ES5))
  • Function.prototype.call()
  • Function.prototype.toString()

Function.prototype.toString()返回函数代码的字符串表示。

到现在为止,我们讨论了.call().apply()toString()。与 Call 和 Apply 不同,Bind 并不是自己调用函数,它只是在函数调用之前绑定上下文和其他参数。在上面提到的例子中使用 Bind:

(function introduce(name, interest) {
    console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
    console.log('The value of this is '+ this +'.')
}).bind(window, 'Hammad', 'Cosmology')();

// logs:
// Hi! I'm Hammad and I like Cosmology.
// The value of this is [object Window].

Bind 像call函数一样用逗号分隔其他传入参数,不像apply那样用数组传入参数。

结论

这些概念是 JavaScript 的基础,如果你想钻研更深的话,理解这些很重要。我希望你对 JavaScript 作用域及相关概念有了更好地理解。如果有东西不清楚,可以在评论区提问。

作用域常伴你的代码左右,享受编码!

[翻译]大规模 Web 服务:负载均衡

原文https://blog.vivekpanyam.com/scaling-a-web-service-load-balancing/

Facebook 这类网站如何处理数十亿请求并保持高可用性呢,答案是负载均衡,本文将对其一探究竟。

什么是负载均衡

负载均衡是许多协同工作资源(通常是计算机)的分配策略。它们通常用于提高容量和可靠性。

为了便于讨论负载均衡,对于服务扩展我假设以下两点:

  • 我可以运行任意数量实例
  • 任何请求可以到达任意实例

第一个假设表明服务是无状态的(或者像 Redis 集群一样可以共享状态)。第二个假设实际中并不是必须的(比如粘性负载均衡),但在这片文章中做这样的假设便于讨论。

下面是我将要讨论的负载均衡技术:

  1. 应用层(OSI 第七层)负载均衡(HTTP、HTTPS、WS)
  2. 传输层(OSI 第四层)负载均衡(TCP、UDP)
  3. 网络层(OSI 第三层)负载均衡
  4. DNS 负载均衡
  5. 多个子域手动负载均衡
  6. 任播(Anycast)

最后还有一些其他知识点:

  • 延迟和吞吐量
  • 服务器直接返回

这些技术大致按照「网站流量增大时需要的操作步骤」排序。比如,应用层的负载均衡是首先要做的事(远远早于任播)。前三条技术提高吞吐量和可用性,但存在单点故障。其余的三条技术在提高吞吐量的同时避免了单点故障。

为了帮助我们理解负载均衡,我们来看一个简单的服务扩展。

注意:每种扩展技术都采用了非技术的比喻(在商店购物)。这些比喻仅仅是描述技术背后的思想,并不完全准确。

服务

我们假设我们正在构建大规模的服务。就像下图所示:

这个系统并不能处理大量通信,并且如果它宕机,整个应用就停了。

比喻:

  • 你走向商店唯一一条结账的队伍。
  • 你购买你的商品,如果那里没有收银员,你无法完成购买。

应用层(OSI 第七层)负载均衡

为了承载更大的通信量,首先用到的就是应用层负载均衡。应用层是 OSI 第七层。它包括 HTTP、HTTPS 和 WebSockets。一款非常流行又久经考验的应用层负载均衡器就是 Nginx。让我们看看它如何帮助我们扩展服务:

请注意,通过这种技术,我们能负载均衡数十或上百服务器实例。上面图片只展示两个作为例子。

比喻

  • 商店员工引领你到一个特别的(有收银员的)结账队伍
  • 你购买你的商品

工具:

  • Nginx
  • HAProxy

注意:

  • 我们要在这里停止使用 SSL(分发时不用 SSL)

传输层(OSI 第四层)负载均衡(TCP、UDP)

上一条技术帮助我们承载大量通信,但如果我们需要承载更大的通信量,传输层负载均衡非常有用。传输层是 OSI 第四层,包括 TCP 和 UDP。流行的传输层负载均衡器有 HAProxy(这个也用于应用层负载均衡)和 IPVS。让我们看看它们如何帮助我们扩展服务:

应用层负载均衡+传输层负载均衡能处理大多数情况下的通信量。然而我们我们仍要担心可用性。单点故障有可能出现在传输层的负载均衡器。我们在下面一节的 DNS 负载均衡解决这个问题。

比喻:

  • 根据客户会员卡卡号有不同结账区域。举个例子,如果你的会员卡卡号是偶数,去电器区附近的结账台,否则就去食品区附近的结账台。
  • 一旦你到达正确的结账区域,商店员工引领你到一个特别的结账队伍
  • 你购买你的商品

工具:

  • HAProxy
  • IPVS

网络层(OSI 第三层)负载均衡

如果我们要继续扩展,我们需要增加网络层负载均衡。这比上面两条技术更复杂。网络层是 OSI 第三层,包括 IPv4 和 IPv6。下面是网络层负载均衡:

为了搞清楚它如何工作,我们需要一点等价路由的知识(ECMP)。当有多条等价链路到达相同地址时,我们使用等价路由。简单来说,它允许路由器或交换机通过不同链接发送数据包(支持高吞吐量),最终到达同一地址。

我们可以利用这一点来实现网络层负载均衡,因为在我们看来,每个传输层负载均衡器是相同的。这意味着我们可以把从网络层负载均衡器到传输层负载均衡器的链接看做相同目的地的链路。如果我们把所有负载均衡器绑定到相同 IP 地址,我们可以使用等价路由在传输层负载均衡器之间分配通信。

比喻:

  • 街对面有两家彼此分开却又一模一样的商店,你去哪一家完全取决于你的习惯。
  • 一旦你到达了商店,根据客户会员卡卡号有不同结账区域。举个例子,如果你的会员卡卡号是偶数,去电器区附近的结账台,否则就去食品区附近的结账台。
  • 一旦你到达正确的结账区域,商店员工引领你到一个特别的结账队伍
  • 你购买你的商品

工具:

  • 通常在机柜里交换机内部的硬件中处理。

太长不阅:

  • 除非你的服务规模相当大或有自己的硬件,否则你不需要它。

DNS 负载均衡

DNS 是将名称转换为 IP 地址的系统。举个例子,它可以把 example.com 转换为 93.184.216.34 。它当然也可以返回多个 IP 地址,像下面这样:

如果返回了多个 IP,客户端通常会使用第一个可用的地址(然而一些应用只看第一个返回的 IP)。

目前有很多 DNS 负载均衡技术,比如 GeoDNS 和轮询调度(round-robin)。GeoDNS 基于不同请求者而返回不同响应。这让我们可以将客户端路由到其最近的服务器或数据中心。轮询调度会循环所有可用的 IP 地址,对于每个响应会返回不同的 IP。如果多个 IP 可用,这两种技术仅仅改变响应里的 IP 顺序。

下图展示 DNS 负载均衡如何工作:

在这个例子中,不同的用户被路由到不同的服务集群(随机或基于地理位置)。

现在这里不再有单点故障的可能性(假设有多台 DNS 服务器)。为了进一步提高可靠性,我们可以在不同数据中心运行多个服务集群。

比喻:

  • 你在网上查询购物中心,返回的列表把最近的购物中心放在第一个。你查看通往每个购物中心的路,然后选择列表中第一个营业的购物中心。
  • 街对面有两家彼此分开却又一模一样的商店,你去哪一家完全取决于你的习惯。
  • 一旦你到达了商店,根据客户会员卡卡号有不同结账区域。举个例子,如果你的会员卡卡号是偶数,去电器区附近的结账台,否则就去食品区附近的结账台。
  • 一旦你到达正确的结账区域,商店员工引领你到一个特别的结账队伍
  • 你购买你的商品

手动负载均衡和路由

如果你的内容在许多数据中心或服务间共享,而我们需要路由到其中特定的一个,那么这条技术就很有用了。比如 cat.jpg 储存在伦敦的集群中,但其他集群中没有。相似的,dog.jpg 储存在纽约的集群中,其他数据中心或集群中没有。举个例子,这很可能发生在内容刚刚上传,还未在数据中心之间复制的时候。

然而,用户获取内容时不应该等待复制完成。这意味着我们的应用需要临时把所有 cat.jpg 请求发送到伦敦,所有 dog.jpg 请求发送到纽约。所以我们需要用 https://lon-1e.static.example.net/cat.jpg 代替 https://cdn.example.net/cat.jpg。对 dog.jpg 来说也一样。

为了实现这一点,我们需要为每个数据中心设置子域(最好细分到每个集群每台机器)。除了上面的 DNS 负载均衡,这一点也很有必要。

注意:我们的应用需要保持追踪内容的位置,以便重写请求。

比喻:

  • 你拨打公司电话询问哪个购物中心提供猫粮。
  • 你查看列表上的购物中心路线,然后选择第一个营业的。
  • 街对面有两家彼此分开却又一模一样的商店,你去哪一家完全取决于你的习惯。
  • 一旦你到达了商店,根据客户会员卡卡号有不同结账区域。举个例子,如果你的会员卡卡号是偶数,去电器区附近的结账台,否则就去食品区附近的结账台。
  • 一旦你到达正确的结账区域,商店员工引领你到一个特别的结账队伍
  • 你购买你的商品

任播(Anycast)

这篇文章讨论的最后一种技术就是任播。首先来看一点背景知识:

大多数网路使用单播。这本质上意味着每台计算机拥有独一无二的 IP 地址。有另一种称为任播的理论。通过任播,一些机器可以使用相同的 IP 地址和路由,并把请求发送到最近的一台机器。我们可以把这种技术和上面所讲的技术结合起来,构建出高可靠性和可用性,能承载巨大通信量的系统。

任播根本上来说是允许互联网为我们处理部分负载均衡。

比喻:

  • 你告诉别人你打算去商店,他们把你带到最近的位置。
  • 街对面有两家彼此分开却又一模一样的商店,你去哪一家完全取决于你的习惯。
  • 一旦你到达了商店,根据客户会员卡卡号有不同结账区域。举个例子,如果你的会员卡卡号是偶数,去电器区附近的结账台,否则就去食品区附近的结账台。
  • 一旦你到达正确的结账区域,商店员工引领你到一个特别的结账队伍
  • 你购买你的商品

杂项

延迟和吞吐量

顺便一提的是,这些技术也可以提升低延迟服务的吞吐量。增加服务数量而不是让每个服务处理更多的通信。这样我们就可以得到低延迟、高吞吐量的系统。

服务器直接返回

在传统负载均衡系统中,请求穿过负载均衡的所有层级,响应也同样穿过它们。降低负载均衡通信量的一个优化点就是服务器直接返回。这意味着服务端的响应不通过负载均衡。如果服务端的响应十分巨大,这点尤其有用。