Giter Site home page Giter Site logo

alvinhui.github.io's Introduction

Hi there 👋

I’m an engineer who loves to create tools.

alvinhui.github.io's People

Contributors

alishutc avatar alvinhui avatar alvinxu avatar coding46 avatar danielfone avatar daz avatar djoos avatar fleeting avatar jcn avatar jkuchta avatar jogjayr avatar koomar avatar koriroys avatar kvannotten avatar lzcabrera avatar marshallshen avatar nolith avatar opie4624 avatar parnmatt avatar philips avatar pierredup avatar plusjade avatar richardlitt avatar robot-c0der avatar roman-yagodin avatar rsertelon avatar studiomohawk avatar subosito avatar vattay avatar xuhdev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

alvinhui.github.io's Issues

一份关于问答系统的小结

智能音箱

一部问答系统发展史就是一部人工智能史。伴随着人工智能的兴衰,问答系统也经历了半个多世纪的浮沉,直到今天仍然方兴未艾。笔者近期一直在从事对话式智能助手的研发(ABot ),因此对问答系统的历史、现状、学术界的研究方向及业界的解决方案有持续 follow,本文即是对该方向输入的一番整理。希望对从事「类聊天机器人」领域的同仁有所帮助。

本文主要以概述方法论为主,不涉及到算法和具体的编程实现。

问答系统简介

问答系统(Question Answering System,QA System)是用来回答人提出的自然语言问题的系统。从狭义上来说,问答系统是聊天机器人的其中一个组成部分(图 1)。

聊天机器人的分类

图 1:聊天机器人的分类

按照知识领域,可以将问答系统分类为“具体领域”以及“开放领域”。具体领域系统专注于回答特定领域的问题,如医药、体育、政府事务等。开放领域系统则希望不设限问题的内容范围,天文地理无所不问。

按照问题类型,又可作如下划分:

  • 事实型问题:WH 问题,例如 when / who / where 等;
  • 是非型问题:Is Beijing the capital of China?
  • 对比型问题:Which city is larger, Shanghai or Beijing?
  • 观点型问题:What is Chinese opinion about Donald Trump?
  • 原因/结果型问题:how / why / what 等。

是非型和对比型这类客观问题以 事实型问题 为核心(对于是非型问题 Is Beijing the capital of China? ,事实型问题 What is the capital of China? 是其回答的基础。对于对比型问题 Which city is larger, Shanghai or Beijing? ,事实型问题 How large is Shanghai?How large is Beijing? 是其回答的基础。),因此我们又可以简单地将问题类型分为 事实型非事实型。针对两类问题类型的解决方案大相径庭。

经典方法

基于信息检索是传统问答系统的经典方法。其按照以下的流程工作(图 2):

  1. 问题解析:
    • 处理问题:处理用户输入的自然语言问题,系统对于问题进行处理和分析,并对问题进行分类,确定问题类型;
    • 生成搜索关键词:问题中的一些词不适合作为搜索关键词,另一些词的搜索权重则较高。系统需要对于用户的问题进行分析,来获得不同关键词的权重。
  2. 信息检索:系统使用从用户的问题中得到的关键词,对于数据库中的文档与关键词的计算匹配程度,从而获取若干个可能包含答案的候选文章,并且根据它们的相似度进行排序;
  3. 答案抽取:
    • 段落提取:段落(paragraph)是包含答案的一个小节。问答系统与搜索引擎的区别在于用户期望其返回精确的答案,而不是一个文章或段落。为此首先要从文章中提取出可能包含答案的段落;
    • 答案提取:在答案可能出现的段落被提取到以后,问答系统需要精确抽取段落中所包含的答案。这一步会用到问题分类。同时根据问题的关键词,对于段落中的词进行语义分析,最终找到最有可能是答案的字段。

基于信息检索的问答系统工作流程

图 2:基于信息检索的问答系统工作流程

基于知识图谱的问答

什么是知识图谱

  • “奥巴马出生在火奴鲁鲁”
  • “姚明是**人”
  • “谢霆锋的爸爸是谢贤”

这些就是一条条知识,而把大量的知识汇聚起来就成为了知识库。我们可以在 Wiki 百科、百度百科等百科全书查阅到大量的知识。然而,这些百科全书的知识组建形式是非结构化的自然语言,这样的组织方式很适合人们阅读但并不适合计算机去处理。为了方便计算机的处理和理解,我们需要更加形式化、简洁化的方式去表示知识,那就是知识图谱。

知识图谱是结构化的语义知识库,用于以符号形式描述物理世界中的概念及其相互关系,其基本组成单位是三元组(SPO: Subject, Predicate, Object 分别表示主语、属性、宾语)。

三元组是一种通用表示方式,即 G=(E,R,S),其中 E={e1,e2,⋯,e|E|} 是知识库中的实体集合,共包含 |E| 种不同实体;R={r1,r2,⋯,r|E|} 是知识库中的关系集合,共包含 |R| 种不同关系;S⊆E×R×E 代表知识库中的三元组集合。

三元组的基本形式主要包括(图 3):

  • 实体1 - 关系 - 实体2
  • 概念 - 属性 - 属性值

知识图谱的三元组

图 3:三元组的基本形式(图片来自《揭开知识库问答KB-QA的面纱1·简介篇》

实体是知识图谱中的最基本元素,不同的实体间存在不同的关系。概念主要指集合、类别、对象类型、事物的种类,例如人物、地理等;属性主要指对象可能具有的属性、特征、特性、特点以及参数,例如国籍、生日等;属性值主要指对象指定属性的值,例如**、1988-09-08 等。每个实体用一个全局唯一确定的 ID 来标识,每个属性 - 属性值对(attribute-value pair,AVP)可用来刻画实体的内在特性,而关系可用来连接两个实体,刻画它们之间的关联。

回到最初的例子: “奥巴马出生在火奴鲁鲁” 可以用三元组表示为 (BarackObama(实体1), PlaceOfBirth(关系), Honolulu(实体2))。

什么是知识图谱问答

以知识图谱构建事实型的问答系统,也称之为 KB-QA(Knowledge Base Question Answering)。即给定自然语言问题,通过对问题进行语义理解和解析,进而利用知识图谱进行查询、推理得出答案(图 4)。对事实型问答而言,这种做法依赖知识图谱准确率比较高,同时也要求我们的知识图谱是比较大规模的,因为 KB-QA 无法给出在知识图谱之外的答案。

知识图谱问答

图 4:基于知识图谱的问答系统工作流程(图片来自中科院刘康在知识图谱与问答系统前沿技术研讨会中的报告)

主流方法

通过知识图谱为知识源回答问题时,一个问题的答案对应于知识图谱的一个子结构。所以其问答过程的核心在于将自然语言问题映射为知识图谱上的结构化查询。例如对于 图 5 中的知识图谱,图 6 展示了一些它可以回答的问题,以及对应的子结构。

一个 RDF 知识图谱示例

图 5:一个 RDF 知识图谱示例(图片来自崔万云的博士学位论文《基于知识图谱的问答系统关键技术研究》)

自然语言问题及其在知识图谱中的属性对应

图 6:自然语言问题及其在知识图谱中的属性对应(图片来自崔万云的博士学位论文《基于知识图谱的问答系统关键技术研究》)

基于知识图谱的问答系统,需要解决两个核心问题:

  1. 如何理解问题语义,并用计算机可以接受的形式进行表示(问题的理解和表示);
  2. 以及如何将该问题表示关联到知识图谱的结构化查询中(语义关联)。

传统的主流方法可以分为三类:

  • 语义解析(Semantic Parsing):该方法是一种偏语言学的方法,主体**是将自然语言转化为一系列形式化的逻辑形式(logic form),通过对逻辑形式进行自底向上的解析,得到一种可以表达整个问题语义的逻辑形式,通过相应的查询语句在知识库中进行查询,从而得出答案。下图红色部分即逻辑形式,绿色部分 where was Obama born 为自然语言问题,蓝色部分为语义解析进行的相关操作,而形成的语义解析树的根节点则是最终的语义解析结果,可以通过查询语句直接在知识库中查询最终答案。

    语义解析

    该图片来自论文:Semantic Parsing on Freebase from Question-Answer Pairs

  • 信息抽取(Information Extraction):该类方法通过提取问题中的实体,通过在知识库中查询该实体可以得到以该实体节点为中心的知识库子图,子图中的每一个节点或边都可以作为候选答案,通过观察问题依据某些规则或模板进行信息抽取,得到问题特征向量,建立分类器通过输入问题特征向量对候选答案进行筛选,从而得出最终答案。

  • 向量建模(Vector Modeling):该方法**和信息抽取的**比较接近,根据问题得出候选答案,把问题和候选答案都映射为分布式表达(Distributed Embedding),通过训练数据对该分布式表达进行训练,使得问题和正确答案的向量表达的得分(通常以点乘为形式)尽量高,如下图所示。模型训练完成后则可根据候选答案的向量表达和问题表达的得分进行筛选,得出最终答案。

    向量建模

    该图片来自论文:Question answering with subgraph embeddings

基于阅读理解的问答

机器阅读理解在 NLP 领域近年来备受关注,自 2016 年 EMNLP 最佳数据集论文 SQuAD 发表后,各大企业院校都加入评测行列。利用机器阅读理解技术进行问答即是对非结构化文章进行阅读理解得到答案,又可以分成抽取式 QA 和生成式 QA。

抽取式

抽取式 QA 让用户输入若干篇非结构化文本及若干个问题,机器自动在阅读理解的基础上,在文本中自动寻找答案来回答用户的问题。抽取式 QA 的某个问题的答案肯定出现在某篇文章中。

抽取式 QA 的经典数据集是 SQuAD(斯坦福问答数据集),这是一个阅读理解数据集,由众包人员基于一系列维基百科文章的提问和对应的答案构成,其中每个问题的答案是相关文章中的文本片段或区间。SQuAD 一共有 107,785 个问题,以及配套的 536 篇文章。

SQuAD 示例:

  • 内容:阿波罗计划于 1962 至 1972 年间进行,期间得到了同期的双子座计划(1962 年 - 1966 年)的支持。双子座计划为阿波罗计划成功必需的一些太空旅行技术做了铺垫。阿波罗计划使用土星系列火箭作为运载工具来发射飞船。这些火箭还被用于阿波罗应用计划,包括 1973 年到 1974 年间支持了三个载人飞行任务的空间站 Skylab,以及 1975 年和前苏联合作的联合地球轨道任务阿波罗联盟测试计划。
  • 问题:哪一个空间站于 1973 到 1974 年间承载了三项载人飞行任务?
  • 答案:Skylab 空间站

基于机器阅读理解模型的问答流程如下图所示:

基于机器阅读理解模型的问答流程

该图片来自阿里小蜜团队吉仁的文章《阿里小蜜机器阅读理解技术探索与实践》

生成式

与抽取式 QA 的问题答案来自一篇文章的某个词语、句子不同,生成式 QA 答案形式是这样的:

  • 答案完全在某篇原文;
  • 答案分别出现在多篇文章中;
  • 答案一部分出现在原文,一部分出现在问题中;
  • 答案的一部分出现在原文,另一部分是生成的新词;
  • 答案完全不在原文出现(Yes / No 类型)。

目前比较好的数据集有 MSRA 的 MS MARCO 。针对这个数据集,国内的百度和猿题库都给出了自己的评测。

目前笔者在这一块的了解不多,持续关注中。

总结

  • 问答系统历史悠久,相关的解决方案有很多,就本文列举的来说,每一个小节深入下去都可以再展开一个篇幅;
  • 工业届的问答系统往往不是单点的方法,而是针对不同业务场景的多个方法的组合。

封面图由 Kevin Bhagat 发表在 Unsplash

机器学习,Hello World from Javascript!

导语

Javascript 适合做机器学习吗?这是一个问号。但每一位开发者都应该了解机器学习解决问题的思维和方法,并思考:它将会给我们的工作带来什么?同样,算法能力可能会是下一阶段工程师的标配。

本文旨在通过讲解识别手写字的处理过程,带读者了解机器学习解决问题的一般过程。本文适合以下背景的读者阅读:

  • 你不需要具备 Python、C++ 的编程能力:全文使用 Javascript 作为编程语言,且不依赖任何第三方库实现机器学习算法;
  • 你不需要具备算法能力和高数的背景,本文机器学习算法的实现不过 20 行代码。

作者学识有限,文章中难免会有疏漏,欢迎指正。

机器学习中的 Hello World

就像我们学习编程语言一样,我们的第一个尝试就是在终端命令行中输出的 “Hello World”。机器学习中的 “Hello World” 便是识别手写字数据集。手写字是形如下面的图像:

手写字图

我们可以编写一个网页程序,提供手写板的功能来捕获用户的输入,并返回我们识别的数字:用户在手写板内写下 0 到 9 中的任意一个数字,另一侧则显示我们识别的结果。正如 Keras.js 提供的示例那样[1]:

Keras.js - MNIST 示例截图

如何编写出这样的手写识别程序来获取用户的手写输入不是我们这篇文章的重点。我们的重点是,当我们的程序得到这样一张图像的数据后,如何识别出这组数据表示的数字?

数据的表示和收集

人类能够从图像中获得信息,但程序如何知道 A 图是表示 1,B 图是表示 2 ?因此我们需要确定数据的表示方式:用怎样的一种方式来在程序中表达一张白底黑字的图像它的像素点分布及点的黑白度?

观察 Keras.js 的示例,你会发现手写板的面积是 240px * 240px。即手写板内有 57,600 个像素点。我们可以把它们平铺开来,并且用 0 到 1 的数值表示每个点的黑白度,其中越接近 1 则表示该像素点越黑,那么就可以用一个数值矩阵来表示手写字:

手写字的数据表示

手写板程序获得用户的输入并生成图像后,识别程序将�图像转换成我们需要的数据格式。图像识别是另一个广泛的课题,在这里不再展开。我们会直接使用 MNIST 数据集[2],它的数据表示方式正如上面所描述的那样,只是 MNIST 数据集中每一张图片是包含 28 * 28 个像素点的。

确定了数据的表示方式,接下来我们还需要对每个数据的实际含义进行标识。

回想一下我们自己是如何认识这些数字的?即我们是怎样认定图像中的 1 形状表示的就是数字 1?————事物的认识。认知是由他人教育的。

同样,在机器学习中,我们也需要“教育”机器:A 这样的像素点排序就是 1, B 这样的像素点排序是 2。这就是训练数据

为了收集训练数据,我们可以随机找人在手写识别程序中画数字,然后标识它的结果,最终以任何的形式(文本、表格...)储存。以手写字为例,我们可以用文本的方式存储,格式可以是这样:

0 0 0.3 0 1 0 ... n(=28) | 4
1 0 0.1 0 0 0 ... n(=28) | 6
....
n(=1000)

其中每一行代表一个训练数据,使用 “|” 分割数据的表示和它对应的数字。

在本文中我们将直接使用 MNIST 数据集,因此如何收集数据在这里不再展开。在机器学习中经常会使用公开的数据集来进行训练和测试

通过确定数据的表示和收集,我们可以了解到的是:

  • 数据是一切机器学习的基础;
  • 训练数据的好坏将会影响到我们机器学习算法预测的准确率:
    • 想象一下如果某些数据我们标识错误,把 1 标识成 2;
    • 想象一下如果训练数据中有大量的重复值,或某个数字的数据量特别大而另外一些数字的数据量很小。

准备数据

我们收集到的数据可能会以任何的一种形式存储,例如文本、表格、二进制文件等等。MNIST 数据集是使用二进制存储的,因此在程序中我们需要将它转换为 Javascript 比较容易操作的的数据格式,例如数组。

本文中我们将使用一个 NPM 包 mnist[3] 提供的,已经转换好的数据,它的格式如下:

[
  {
    input: [0, 0.4, 0.5, 0, 0.1, 0, 0, 0, 0, ..., n], // n = 728
    output: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], // 1
  },
  {
    input: [1, 0.4, 0.5, 0, 0.8, 0, 0.1, 0, 1, ..., n], // n = 728
    output: [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], // 2
  },
  ....
]

其中 input 是图像的数据表示,output 是图像实际代表的数字。output 使用了一种叫做 One-Hot 的编码方式,它一共有 10 个项,为 1 的项就是它表示的数字(第一项为 1 则代表是 0,第二项为 1 则代表是 2 ,以此类推)。

选择一种算法

通过上面的数据准备,我们已经把一个现实中的问题转化成了一个数学问题:给定 728 个 0 到 1 之间数值的特征,应该将它分类到 0 ~ 9 哪个数字中?

这就是机器学习中的主要任务——分类。有很多的机器学习算法可以用来解决分类问题,文本将使用 k-近邻算法(k-NN)[4]来解决这个问题,因为它非常有效且容易理解。

k-近邻算法概述

在一个 10 * 10 的二维平面内画一条线把它分成 2 个区域(A/B)。假设我们不知道线是如何画的,但现已知有 4 个点,a 点坐标是 (1, 1) 属于区域 A,b 点坐标是 (2, 2) 属于区域 A,c 点坐标是 (9, 9) 属于区域 B,d 点坐标是 (8, 8) 属于区域 B。这时候再给定一个 e 点坐标是 (8.5, 8.5) ,请问它最有可能在哪个区域内?

Index Point 1 Point 2 Area
a 1 1 A
b 2 2 A
c 9 9 B
d 8 8 B
e 8.5 8.5 ?

二维平面图示例

绝大多数人都会说“可能是 B”。我们是如何得出这个答案的?——因为它和 c, d “看起来更接近一些,更有可能在同一个区域”。同样的推论可以延伸至三维、四维甚至更多纬度的数据中。MNIST 的数据表示就是 728 个特征的多纬数据,k-近邻算法同样适用。

存在一个训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本特征最近邻的分类标签。一般来说,我们只选择样本数据集中前 k 个最相似的数据,这就是 k-近邻算法的 k 的出处。
——《机器学习实战》k 近邻算法

两个向量之间的距离可以通过欧几里得距离公式求得:

欧几里得距离公式

于是实现一个 k-NN 算法就很简单了:

function classify(x, trainingData, labels, k) {

  // 确定目标点 x 与训练数据中每个点的距离(欧几里得距离公式)
  const distances =[];
  trainingData.forEach(element => {
    let distance = 0;
    element.forEach((value, index) => {
      const diff = x[index] - value;
      distance += (diff * diff);
    });
    distances.push(Math.sqrt(distance));
  });

  // 将训练数据按照与 x 点的距离从近到远排序
  const sortedDistIndicies = distances
    .map((value, index) => {
      return {value, index};
    })
    .sort((a, b) => a.value - b.value );

  // 确定前 k 个点类别的出现频率
  const classCount = {};
  for (let i = 0; k > i; i++) {
    const voteLabel = labels[sortedDistIndicies[i].index];
    classCount[voteLabel] = (classCount[voteLabel] || 0) + 1;
  }

  // 返回出现频率最高的类别作为当前点的预测分类
  let predictedClass = '';
  let topCount = 0;
  for (const voteLabel in classCount) {
    if (classCount[voteLabel] > topCount) {
      predictedClass = voteLabel;
      topCount = classCount[voteLabel];
    }
  }

  return predictedClass;
}

测试算法

为了验证我们的算法的效果,我们需要对其进行测试。这就需要引入测试数据。在机器学习中通常会将收集到的数据通过一定的方法划分为训练数据和测试数据然后用于训练和测试。如何划分数据在这里不展开,在本示例中,我们按照 80:20 的比例来划分训练和测试数据,互斥性和随机性由 MNIST 库进行保证。

拿到训练和测试数据后我们就可以对上一步编写的算法进行测试了,我们用错误率来评估算法的可靠性,错误率越低则越可靠:

const classify = require('./kNN');

// 1. 收集数据:忽略,直接使用 MNIST 
const mnist = require('mnist');

// 2. 准备数据
let trainingImages = [];
let labels = [];

// 划分数据
const trainingCount = 8000;
const testCount = 2000;
const set = mnist.set(trainingCount, testCount);
const trainingSet = set.training;
const testSet = set.test;

// 为我们的 k-NN 算法准备特定的数据格式
trainingSet.forEach(({input, output}) => {

  // One-Hot to number
  const number = output.indexOf(output.reduce((max, activation) => Math.max(max, activation), 0));
  trainingImages.push(input);
  labels.push(number);
});

// 3. 分析数据:在命令行中检查数据,确保它的格式符合要求
console.log('trainingImages', JSON.stringify(trainingImages));
console.log('labels', JSON.stringify(labels));

// 4. 测试算法
let errorCount = 0;
const startTime = Date.now();
testSet.forEach(({input, output}, key) => {
  const number = output.indexOf(output.reduce((max, activation) => Math.max(max, activation), 0));
  const predicted = classify(input, trainingImages, labels, 3);
  const result = predicted == number;
  console.log(`${key}. number is ${number}, predicted is ${predicted}, result is ${result}`);

  if (!result) {
    errorCount++;
  }
});

console.log(`The total number of errors is: ${errorCount}`);
console.log(`The total error rate is: ${errorCount/testCount}`);
console.log(`Spend: ${(Date.now() - startTime) / 1000}s`);

如无意外,你的终端将会输出这样的结果:

kNN�运行结果

最终错误率的值大约是 5%。这个结果好吗?并不好。我们可以通过改变 k 的值、改变训练样本的数目影响 k-近邻算法的错误率,读者可以尝试改变这些变量值观察错误率的变化。实际上,只要将 k-近邻算法稍加改良,我们就能够把错误率降到 1% 以下!

MNIST数据集中kNN算法的效率

表格中列出了一些 k-近邻算法对 MNIST 数据集进行测试的错误率,图片来自 http://yann.lecun.com/exdb/mnist/

我们也应该注意到的是,我们的算法在 8000 条训练数据集和 2000 条测试数据集上进行测试,运行了 325 秒!这是一个很差的结果。在实际生产环境中,我们不仅应该关注准确率也应该关注算法的执行效率。

使用算法

只要测试的算法效果符合预期,我们就可以将算法部署到生产环境进行使用了。我们可以将算法和手写识别程序结合起来,完成一整套获取输入 -> 算法预测 -> 输出结果的流程:首先手写识别程序将用户输入的图像转换为我们期望的数据格式,然后执行我们的算法获取预测的分类。代码可能是这样:

// 手写识别程序将用户输入的图像转换为我们期望的数据格式
const input = [0, 0.3, 1, 1, 0, 0, 0.2, ...];

// 执行算法
classify(input, trainingImages, labels, 3);

很遗憾,在执行算法时我们还是看到了 trainingImages 的存在。这意味着每次预测我们的机器都需要给训练数据准备格外的存储空间。假设训练数据很大(这很常见),则会给我们的生产环境机器造成巨大的内存压力。

每次调用算法还需要传入训练数据的方式即浪费存储空间也不优雅,它只能作为我们的示例进行使用。

进一步思考

本文我们使用 Javascript 实现了一个非常简单的机器学习算法,并用其来测试 MNIST 数据集,完整代码实现在这个仓库中。这只是一个简单的示例,但从中我们了解到了机器学习的基本概念和解决问题的一般过程。进一步思考,上面的流程中每一步都可能被优化:

  • 对于手写字,还有没有其他的数据表示方式?例如我们非要用 0 到 1 的数值来表示点的黑白度吗?
  • 训练数据集是越大越好吗?例如我们将手写字所有的特征排列组合 (28^28) 个数据量作为训练数据集;
  • 如何调整算法参数以获得最佳的收益(准确率和效率)?

参考资料

  1. Keras.js - MNIST 示例
  2. MNIST 数据集
  3. mnist - NPM
  4. k-NN

从管理开机启动项看windows注册表

最近一直在折腾winy7系统,从管理开机启动项到管理鼠标右键菜单(后续的文章会讲到这方面的内容)等等。

“开机启动项”包括了开机启动程序和开机启动服务。

设置开机启动

查看和禁用开机启动程序

win7查看和管理开机启动项的方式非常简单,打开系统开始菜单,搜索msconfig,打开它,就会看到下图:

步骤一

image

系统设置图

image

在系统设置的启动选项卡里面就可以禁用启动项目了。

如何添加开机启动程序

那怎么添加一个程序到开机启动呢?我们继续细看启动选项卡里面的位置栏:

image

位置是什么?这一坨坨的是什么?

这里的位置是指该启动设置在注册表的位置。我们可以联想得到,有三种方式可以设置开机启动程序,一个是在注册表HKLM中,一个是在注册表HKCU中,另一个就是在C盘内的startup文件夹新建程序的快捷方式。

注册表

注册表是什么?

注册表(Registry)是微软公司从Windows95系统开始,引入用于代替原先Win32系统里.ini文件,管理配置系统运行参数的一个全新的核心数据库。它与老的win32系统里的ini文件相比,具有方便管理,安全性较高、适于网络操作等特点。

注册表整合集成了全部系统和应用程序的初始化信息。它存储下面这些内容:
(1)软、硬件的有关配置和状态信息,应用程序和资源管理器外壳的初始条件、首选项和卸载数据;
(2)计算机的整个系统的设置和各种许可,文件扩展名与应用程序的关联,硬件的描述、状态和属性;
(3)计算机性能纪录和底层的系统状态信息,以及各类其他数据。

编辑注册表的工具---Regedit.exe

Regedit可对注册表进行添加、修改主键、键值,备份注册表,局部导入导出注册表等操作。
启动方法:开始菜单→运行,所在对话框中输入regedit并点确定。

image

image

image

注册表的结构

在Windows中,注册表由两个文件组成:System.dat和User.dat,保存在windows所在的文件夹中。它们是由二进制数据组成。

System.dat包含系统硬件和软件的设置。
User.dat保存着与用户有关的信息,例如资源管理器的设置,颜色方案以及网络口令等等。

注册表编辑器与资源管理器的界面相似。左边窗格中,由“计算机”开始,以下是六个分支(WINNT只有前面5个),每个分之名都以HKEY开头,称为主键(KEY),展开后可以看到主键还包含次级主键(SubKEY)。当单击某一主键或次主键时,右边窗格中显示的是所选主键包含的一个或多个键值(Value)。键值由键值名称(Value Name)和数据(Value Data)组成。主键中可以包含多级的次级主键,注册表中的信息就是按照多级的层次结构组织的。

注册表中各分支的功能

HKEY-CLASSES-ROOT
文件扩展名与应用的关联及OLE信息(Object Linking and mbedding--对象连接与嵌入)
HKEY-CURRENT-USER
当前登录用户控制面板选项和桌面等的设置,以及映射的网络驱动器
HKEY-LOCAL-MACHINE
计算机硬件与应用程序信息
HKEY-USERS
所有登录用户的信息
HKEY-CURRENT-CONFIG
计算机硬件配置信息
HKEY-DYN-DATA
即插即用和系统性能的动态信息

注册表中的键值项数据

注册表通过键和子键来管理各种信息。但是注册表中的所有信息都是以各种形式的键值项数据保存的。在注册表编辑器右窗格中显示的都是键值项数据。这些键值项数据可以分为三种类型:

  1. 字符串值

    字符串值一般用来表示文件的描述和硬件的标识。通常由字母和数字组成,也可以是汉字,最大长度不能超过255个字符。

  2. 二进制值

    二进制值是没有长度限制的,可以是任意字节长。在注册表编辑器中,二进制以十六进制的方式表示。

  3. DWORD值

    DWORD值是一个32位(4个字节)的数值。在注册表编辑器中也是以十六进制的方式表示。

最初的问题

回到我们最开始的那个问题,添加启动程序就非常简单了,只需要在注册表HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run出新建一个字符串值即可

image

参考

注册表详解
注册表基础大全(基础篇)
玩转注册表:从入门到精通

写在2014

写在2014

年前就想写总结,不过总是静不下心来所以拖到后面才有了上一篇《我的2013》。写完总结,按例应该要写个计划。

想写一个计划有些时间了。不过最近很迷惘。迷惘主要是在职业生涯的选择上的。产品?技术?技术的话:前端?后端?如果说到乔布斯的“follow your heart”,我会说程序员不是我的heart。我的heart是什么?我也搞不清楚。有时候我憧憬freedom,有时候又憧憬success。走向程序员这条路,很大程度上是因为它给了我成就感。当然还有混了口安乐饭。

我一直都没有给理想下过肯定的目标和计划。

刚出来工作的时候,理想是做一名很牛逼的程序员。很牛逼很牛逼的那种。然后就会有无数屌丝崇拜我什么的,喊我大牛、大神、大师。

过了一段日子,发现牛逼是很遥远的事情。比你牛逼的人多了多得去了。而且有一部分人你是永远无法超越的,因为人家妈蛋就是天才而且比你还勤奋。

狠泡了一段时间《优酷-老友记》(一档牛逼人的对话栏目不是美剧)后,开始崇拜产品。梦想可以通过产品来致富。互联网产品改变人们的生活方式,迎着时代的大浪潮翻滚起来xxxxx。

看了一阵子《晓说》,又发现旅行的生活自己真向往,能不能把自己的爱好和特长结合起来……

……

人生的定位,是一个需要长期摸索的事情。

有些人,小学的时候就知道自己要做什么了,学编程,做软件,考清华,毕业后谷歌 (例如 byvoid; 这真不算什么,想想陶哲轩)。有些人觉悟得晚一点,中年才开始发力,例如马云。还有一些人晚年才得志,例如刚刚逝世的曼德拉。不过大多数人,都是平平淡淡地走了这辈子,一如你我的长辈。

24岁的我,应当如何去定位自己呢?

我们大多数人都不是天才(或受限于社会家庭等客观因素),没能在20岁前就出类拔萃。可是人生还是很漫长的一个路程,且不论成败,单单是为了不白来一回,都得好好地掂量着日子该怎么过。

可以摸着石头过河,慢慢探索。可以看看跟自己的过去有着相同人生经历或背景的前人的选择和他走的路,探头看看自己能走到的最远方。

玉伯的一篇文章《毕业十年与我的三个梦
给了我启发。作为一个技术人,无非就是:技术梦,产品梦,自由梦。

技术梦

2014,希望可以更“接地气”点儿。从事前端开发一年半后,发现自己离编程越来越远了。作为一个计算机学院的差等生,本身编程基础就几乎为0,写了一年半javascript后,积累的问题越来越多,无法突破的瓶颈越来越多。“接地气”,就是要更深入地了解计算机系统,恶补大学时期落下的基础知识。

2014,由前端走向全端。无论是前端还是后端,本质上都应当是程序员。前端和UI打交道,后端和数据库打交道。今年要两边都发展,加强对后端的学习。理由是两个:1是后端更接地气,2是后端对于项目的价值更大,3是全面发展的需要。

今年内的目标是至少刨12本书。有2本书贯穿全年:《深入理解计算机系统》和《HTTP权威指南》。前半年还会看的是《GO WEB编程》《用AngularJS开发下一代Web应用》《深入理解Javascript系列》(选读),其他待定。

产品梦

做雀彩就是产品梦最好的实践。虽然彩票业务不是我的菜,不过作为屌丝在没有选择权时,把能利用到的资源利用好,让自己上一个台阶,是当下唯一的选择。在业余时间也会折腾一些自己的idea。未来会选择做自己喜欢的产品,需要等待自我条件的成熟。

自由梦

财务自由还有相当长的一段距离,精神自由则是一个未知的层次。我心中的自由生活,每每能想到的就是和自己的爱人一起旅行,过着“没有计划”的日子。看些风景,拍些照片,吃到不能再胖。。。今年,想去西藏或者出国,去看一看他人的世界。

还有一些很重要的

身体。一年一度的体检又要来了,上一年又长胖了不少,不知道现在的自己可好?按理应该要定一个减肥计划了,怎么定?

家人。能陪伴的时间很少,面对逐渐远去的爷爷奶奶,珍惜与他们一起的一分一秒。

工资。到年底至少涨百分之二十。这取决于很多因素,不过涨20%是我对自己的指标。

愿一切顺利。

训练第一个机器学习模型

机器模型

导语

在笔者的上一篇文章中[1],使用了 k-NN 算法来识别手写字数据集,它的缺点是浪费存储空间且执行效率低。本文将使用决策树算法来解决同样的问题。相对 k-NN 算法,它更节约存储空间且执行效率更高。更重要的是,实施决策树算法的过程将训练算法并得到知识 —— 这是开发机器学习程序的一般步骤。一旦理解了这个工作流程,才有可能利用好机器学习这把利剑。

在本文中,笔者将训练一个决策树模型并使用该模型来识别手写字数据集。从中读者将可以了解到:如何构建学习模型?模型经过训练后学习到了怎样的知识?学习到的知识怎么表示和存储?又该如何利用这些学到的知识来解决同类的问题?

本文适合以下背景的读者阅读:

  • 了解 MNIST 数据集[2];
  • 使用 Javascript 作为编程语言的开发者;
  • 不需要具备算法能力和高数的背景:全文只有一道数学公式;
  • 加上示例代码,全文总共 460 行,大约需要 20 分钟的阅读时间。

作者学识有限,如有疏漏,敬请指正。

生活中的决策

在开始构建决策树之前,必须了解决策树的工作原理。更详细的内容可以从参考资料的链接[2]中获得。

一个例子是,如何教育一个学龄前的儿童辨认猫和老虎?

猫和老虎

  • 我们会拿来一些示例照片,对照这些照片根据某些特征来训练小孩,告他 A 是猫,B 是老虎;
  • 这些特征可能是,表面的颜色、耳朵的形状、体积的大小等等;
  • 我们总是希望儿童能快速辨认出猫和老虎,毕竟假如他们真的遇到了老虎,则需要和老虎保持一定的距离;
  • 其中一种筛选方法就是决策模型:把认为最重要的特征先进行甄别,然后到次要的,再到次次要的,以此来加速决策过程并得出判定。

作为一个示例,这里假设将识别老虎分为 2 个特征,分别是耳朵的形状和体积大小,那么已知的数据可能是这样的:

Index Shape of the ear Size Animal
1 Triangle Small Cat
2 Triangle Small Cat
3 Triangle Big Tiger
4 Circular Small Tiger
5 Circular Big Tiger

在程序中将使用数组的形式来表示上列数据,我把它称为「抓虎的数据集」:

const dataSet = [
  ['Triangle', 'Small', 'Cat'],
  ['Triangle', 'Small', 'Cat'],
  ['Triangle', 'Big', 'Tiger'],
  ['Circular', 'Small', 'Tiger'],
  ['Circular', 'Big', 'Tiger'],
];

根据已有的数据集(经验),猫和老虎的决策树则是这样:

「抓虎」的决策树

这就是决策树的工作原理了。因为属于分类算法,所以决策树也可以推演到 MNIST 数据集的识别中。把 728 个点作为特征,对应的数字作为分类目标即可应用决策树算法。当然决策树算法不适合解决 MNIST 数据集这类特征为数值型的问题,但是因为它易于理解和实现,人们在通过解释后都有能力去理解决策树所表达的意义,因此作为机器学习中训练模型的算法来进行入门则非常合适。

那么决策树模型在程序中应该如何构建和表示呢?

构建决策树

决策树的构建过程就是在训练数据集中不断划分数据集,直到找到目标分类的过程。在此过程中需要找到最好的数据集划分方式,递归地不断划分数据集,直到所有的分类都属于同一类目或没有多余特征时停止生长。可以结合上一章节的「抓虎」的决策树进行理解。

找出最佳特征来划分数据

不难看出,构建决策树的关键问题是如何找出最佳的特征来划分数据集。先要回答问题是,假设我按照某个特征将数据集一分为二,那么有 N 种划分方式,哪一种才算做「最好的划分方式」?这就得引入香农熵的概念。

香农熵

划分数据集的大原则是:将无序的数据变得更加有序。

在「抓虎」的决策树中,耳朵的形状是最佳的划分特征,因为根据它来划分后的数据集更加有序了(混杂项更少)。度量集合有序程度的其中一种方法就是香农熵。香农熵是信息论中的内容,有兴趣的读者可以从参考资料的链接[4]中获得更详细的内容。在此只需要知道的是,香农熵越低则集合越有序

香农熵的计算公式是:

香农熵公式图

根据公式,在程序中实现计算香农熵的代码:

function calcShannonEnt(dataSet) {
  const labelCounts = {};
  for (let featVec of dataSet) {
    const currentLabel = featVec[featVec.length - 1];
    if (Object.keys(labelCounts).indexOf(currentLabel) === -1) {
      labelCounts[currentLabel] = 1;
    } else {
      labelCounts[currentLabel]++;
    }
  }

  let shannonEnt = 0.0;
  const numEntries = dataSet.length;
  for (let i in labelCounts) {
    const x = labelCounts[i];
    const probability = x / numEntries; // p(x)
    shannonEnt = shannonEnt - probability * log2(probability); // -Σp*log(p) 
  }
  return shannonEnt;
}

进行一些测试将会有助于理解香农熵的含义:

// 注意:初始化时数据集里面只有 2 个目标分类(yes or no)
const dataSet = [
  [1, 1, 'yes'],
  [1, 1, 'yes'],
  [1, 0, 'no'],
  [0, 1, 'no'],
  [0, 0 'no']
];

console.log(calcShannonEnt(dataSet)); // 0.9709505944546686

dataSet[0][dataSet[0].length - 1] = 'maybe'; // 混合更多的分类
console.log(calcShannonEnt(dataSet)); // 1.3709505944546687 (香农熵变大,说明数据集更无序了)

根据特征划分数据集

实现一个函数,根据特征来划分数据集:

function splitDataSet(dataSet, index, value) {
  const retDataSet = [];
  for (let featVec of dataSet) {
    if (featVec[index] === value) {
      let reducedFeatVec = featVec.slice(0, index);
      reducedFeatVec = reducedFeatVec.concat(featVec.slice(index + 1));
      retDataSet.push(reducedFeatVec);
    }
  }

  return retDataSet;
}

拿「抓虎」的数据集进行测试,看看划分后的数据长什么样?

console.log(splitDataSet(dataSet, 0, 'Triangle'));
// Triangle [ [ 'Small', 'Cat' ], [ 'Small', 'Cat' ], [ 'Big', 'Tiger' ] ]

console.log(splitDataSet(dataSet, 0, 'Circular'));
// Circular [ [ 'Small', 'Tiger' ], [ 'Big', 'Tiger' ] ]

从结果上看,成功地按照某个特征值把数据划分了出来。

组合计算熵的算法和划分数据集的函数,就可以找出最佳的数据划分特征项。以下是代码实现:

function uniqueDataSetColumn(dataSet, i) {
  const uniqueValues = [];
  dataSet.forEach((element) => {
    const value = element[i];
    if (uniqueValues.indexOf(value) === -1) {
      uniqueValues.push(value)
    }
  });

  return uniqueValues;
}
function chooseBestFeatureToSplit(dataSet) {
  const numberFeatures = dataSet[0].length;
  let baseEntropy = calcShannonEnt(dataSet);
  let bestInfoGain = 0.0;
  let bestFeature = -1;

  // 对比每个特征划分数据的熵,找出最佳划分特征
  for (let i = 0, length = numberFeatures - 1; length > i; i++) {
    const uniqueValues = uniqueDataSetColumn(dataSet, i);

    // 计算熵
    let newEntropy = 0.0;
    uniqueValues.forEach((value) => {
      const subDataSet = splitDataSet(dataSet, i, value);
      const probability = subDataSet.length / dataSet.length;
      newEntropy += probability * calcShannonEnt(subDataSet);
    });

    const infoGain = baseEntropy - newEntropy;
    if (infoGain > bestInfoGain) {
      bestInfoGain = infoGain;
      bestFeature = i;
    }
  }

  return bestFeature;
}

将该函数在「抓虎」的数据集进行测试,这个数据集的第一划分依据是什么特征?

console.log(chooseBestFeatureToSplit(dataSet));

如无意外,程序将输出 0。耳朵的形状是最佳的划分特征,证明程序达到了我们预想的效果。

递归构建决策树

将上面的函数结合起来,再不断地进行递归就可以构建出决策树模型。什么时候应该停止递归?有 2 种情况:

  1. 当所有的分类都属于同一类目时,停止划分数据 —— 该分类即是目标分类;
  2. 划分的数据集中没有其他特征时,停止划分数据 —— 根据出现次数最多的类别作为目标分类。

构建树的入参是什么?

  1. 训练数据集 —— 从训练数据中提取决策知识;
  2. 特征的标签 —— 用于绘制决策树每个节点。

以下是代码实现:

// 辅助函数,根据出现次数最多的类别作为目标分类
function majority(classList) {
  const classCount = {};
  for (let vote of classList) {
    if (Object.keys(classCount).indexOf(vote) === -1) {
      classCount[vote] = 1;
    } else {
      classCount[vote]++;
    }
  }

  let predictedClass = '';
  let topCount = 0;
  for (const voteLabel in classCount) {
    if (classCount[voteLabel] > topCount) {
      predictedClass = voteLabel;
      topCount = classCount[voteLabel];
    }
  }
  return predictedClass;
}
function createTree(dataSet, featureLabels) {
  const classList = dataSet.map((elements) => elements[elements.length - 1]);
  
  // 当所有的分类都属于同一类目时,停止划分数据
  let count = 0;
  classList.forEach((classItem) => {
    if (classItem === classList[0]) {
      count++;
    }
  });
  if (count == classList.length) {
    return classList[0]
  }

  // 数据集中没有其他特征时,停止划分数据,根据出现次数最多的类别作为返回值
  if (dataSet[0].length === 1) {
    return majority(classList);
  }

  // 1. 找到最佳划分数据集的特征
  const bestFeat = chooseBestFeatureToSplit(dataSet);
  const bestFeatLabel = featureLabels[bestFeat];
  const myTree = {[bestFeatLabel]: {}};

  // 2. 获得特征的枚举值
  const uniqueValues = uniqueDataSetColumn(dataSet, bestFeat);

  // 3. 根据特征值划分数据(创建子节点)
  uniqueValues.forEach((value) => {
    const newDataSet = splitDataSet(dataSet, bestFeat, value);
    const subLabels = featureLabels.filter((label, key) => key !== bestFeat);

    // 4. 递归划分
    myTree[bestFeatLabel][value] = createTree(newDataSet, subLabels)
  });

  return myTree;
}

自此就完成了学习模型的构建。

训练算法得到知识

将已有的数据集使用决策树模型进行训练,将会得到怎样的知识?

以「抓虎」为例,运行以下代码:

const tree = createTree(dataSet, ['Shape', 'Size']);
// {"Shape":{"Triangle":{"Size":{"Small":"Cat","Big":"Tiger"}},"Circular":"Tiger"}}

可见,能得到的知识是针对数据集学习到的特征权重顺序排列,是层层筛选决策的依据。

为了更加直观和易于理解,可以将数据可视化(关于如何进行数据可视化不是本文的内容),它大概长这样:

决策树图

在程序中加入知识的存储和提取函数,方便利用已有的知识进行推理。所以再声明 2 个辅助函数:

function storeTree(inputTree, filename) {
  fs.writeFileSync(filename, JSON.stringify(inputTree));
}

function grabTree(filename) {
  return JSON.parse(fs.readFileSync(filename, 'utf8'))
}

使用已有的知识进行推理

只需要写一个解析树的函数就可以将学习到决策知识推理到同类的数据集中。以下是代码实现:

function classify(inputTree, featureLabels, testVec) {
  const firstStr = Object.keys(inputTree)[0];
  const secondElement = inputTree[firstStr];
  const featIndex = featureLabels.indexOf(firstStr);
  const key = testVec[featIndex];
  const valueOfFeat = secondElement[key];
  if (typeof valueOfFeat === 'object') {
    return classify(valueOfFeat, featureLabels, testVec);
  } else {
    return valueOfFeat;
  }
}

以「抓虎」为例,下次见到一个耳朵形状是三角形,体积较小的动物,根据我们之前学习到的知识,它应该是猫还是老虎?

console.log(classify(tree, ['Shape', 'Size'], ['Triangle', 'Small']));
// Cat

如无意外,将会输出 "Cat"。

应用到 MNIST 数据集

最后,组合上面的函数,将其应用到 MNIST 数据集的识别中。

值得注意的是,在数据准备环节需要一些工作以适应上文构建的算法:

  • 将特征由数值型转化为标称型,这里我用了 0 / 1;
  • 将分类值由 one-hot 向量转化为具体的数字。

准备数据

const mnist = require('mnist');
const fs = require('fs');
const path = require('path');
const trainingCount = 8000;
const testCount = 2000;
const {training, test} = mnist.set(trainingCount, testCount);

fs.writeFileSync(path.join(__dirname, 'mnist_trainingData.json'), JSON.stringify(training));
fs.writeFileSync(path.join(__dirname, 'mnist_testData.json'), JSON.stringify(test));

学习阶段

const mnist = require('mnist');
const path = require('path');
const fs = require('fs');

// 1. 加载数据
const trainingData = JSON.parse(fs.readFileSync(path.join(__dirname, 'mnist_trainingData.json'), 'utf8'));

// 2. 准备数据
let data = [];
trainingData.forEach(({input, output}) => {
  // 将分类值由 one-hot 向量转化为具体的数字
  const number = String(output.indexOf(output.reduce((max, activation) => Math.max(max, activation), 0)));
  
  // 数值型特征转换为标称型
  data.push(toZeroOne(input).concat([number]));
});

// 特征的标签
const labels = mnist[0].get().map((number, key) => `number_${key}`);

// 3. 分析数据:在命令行中检查数据,确保它的格式符合要求
console.log('data', JSON.stringify(data[0]));
console.log('labels', JSON.stringify(labels));

// 4. 训练算法
const startTime = Date.now();
const tree = createTree(data, labels);
console.log('tree', JSON.stringify(tree));
console.log(`Spend: ${(Date.now() - startTime) / 1000}s`);

// 存储学到的知识
storeTree(tree, path.join(__dirname, 'mnist_tree.txt'));

在笔者的电脑上大概运行了 10 分钟:

学习解决的耗时

看起来运行时间很长,那怎么能说比 k-NN 算法更有效率?!

其实这是训练阶段的耗时,而训练阶段往往是离线处理,有大量的手段可以优化这部分的性能。

应用阶段

const mnist = require('mnist');
const path = require('path');
const fs = require('fs');

// 1. 加载测试数据
const testData = JSON.parse(fs.readFileSync(path.join(__dirname, 'mnist_testData.json'), 'utf8'));
const testCount = testData.length;

// 获取先前学习的知识
const tree = grabTree(path.join(__dirname, './mnist_tree.txt'));
const labels = mnist[0].get().map((number, key) => `number_${key}`);

// 2. 测试算法
let errorCount = 0;
const startTime = Date.now();
testData.forEach(({input, output}, key) => {
  const number = output.indexOf(output.reduce((max, activation) => Math.max(max, activation), 0));
  const predicted = classify(tree, labels, toZeroOne(input));
  const result = predicted == number;
  console.log(`${key}. number is ${number}, predicted is ${predicted}, result is ${result}`);

  if (!result) {
    errorCount++;
  }
});
console.log(`The total number of errors is: ${errorCount}`);
console.log(`The total error rate is: ${errorCount / testCount}`);
console.log(`Spend: ${(Date.now() - startTime) / 1000}s`);

// 3. 使用算法
const number = 8;
console.log('Result is', classify(tree, labels, toZeroOne(mnist[number].get())));

如无意外,终端命令行中将输出以下结果:

应用的输出结果

在同样的数据集中,笔者上一篇文章构建的 k-NN 算法,运行时长是 325 秒,错误率是 0.05。这组数据该如何解读?笔者认为:

  1. 决策树的在预测阶段计算量非常小,所以执行效率非常高;
  2. 本文做特征处理时丢失了很多信息,数值型特征转换到 0/1 的方式太过于粗暴。

使用决策树算法来识别 MNIST 数据集效果很不理想,不过从中可以看到构建一个机器学习应用的完整过程。

参考资料

  1. 机器学习,Hello World from Javascript!
  2. MNIST 数据集
  3. 决策树
  4. 香农熵
  5. 本文示例代码

文章封面图由 Igor Ovsyannykov 发表在 Unsplash

前端开发的价值

对于互联网应用而言,前端开发的价值在哪里?以下是我的一些思考。

  1. 产品原型的实现

    对于整个产品实现过程来说,前端实现是其中一部分,不可或缺。

    基于这个价值点,要求前端开发:

    a. 快速产出
    b. 质量保证

    如何才能快?

    方案最优;
    控制复杂度;
    协作效率高;
    代码可维护性强;
    工具链健全;

    质量如何保障?

    引入测试

  2. 用户体验优化

    前端开发是产品实现过程的最后一环,产出直接与用户相关。

    用户体验优化的要求有:

    a. 多终端、多语言、无障碍
    b. 快速响应用户需求的能力

    基于这个价值点,要求前端开发:

    a. 跨平台和客户端开发的能力(JS)、处理排版的能力(CSS)、书写表述语言的能力(HTML)
    b. 提高应用的性能

我的2013

我的2013

工作

在开始写这篇日志前,我一直在想我的2012。

2012年是我过得最为混混沌沌的一年,当时的我年头刚进入了一家外企,拿着觉得还算不错的薪水,住进了舒适点的小区公寓,朝九晚五。

2012年我给自己定的计划现在听起来有点不可思议,是深入了解YII,Mongodb,linux系统…… 当然到现在为止一件都没做成,因为后来(12年7月份)我转做了全职前端。现在反过头来想想,也许坚持了那条路,会是更好的选择。

2012年的混沌,主要是因为没有计划。其实每一天都在编程,却不知道自己的方向在哪里。最后其实就是在重复劳动:完成A功能完成B功能完成C功能。

2013年始,项目的前端开始大重构,借这机会我自学了backbonerequirejs,用佷裸的代码写出了残废版的单页面网站(刚才回去看了看这个网站,还是用我之前搭的那个基础架构,不禁有点伤感)。也就是从那个阶段开始,我开始接触开源技术,开源社区,从国外到国内。

2013年,是我的前端年。

从1到3月份的前端MVC探索,3-6月份的代码堆砌完成网站功能,6-9月份的单页面探索,10-12月前端工程化探索……

走向前端这条路,完全是一个意外。属于“这个烂摊子实在没人想搞,于是让你来搞。”

在3-6月份时,我曾经经历过一段迷惘期,当时的生活枯燥无味,工资不见上涨空间,工作总是在重复,我不断地在想,我的价值在哪?

为此我只身行走,广阔的世界给了我新的眼光和勇气。

因此也才有了后来的做出改变,2013年10月份加入UC。

生活

2012年,想最多的事情就是吃什么、穿什么、玩什么。也正是那段时间入手了我现在所有的个人物品。

而2013?我都不记得我买了些什么给自己。屈指可数。

这一年住进了一个老式小区,生活得一塌糊涂。具体的都不愿意再提。我突然也很佩服我自己,真是能屈能伸啊,当初的日子也不知道怎么过来的。

也就是从这一年,我开始爆发式发胖……

2013年,我摆脱了信用卡依赖,终于有了自己的积蓄……

总结

今年最喜欢的一部电影是《**合伙人》。看这部电影时是跟自己的2个大学舍友一起去看的。关于大学,关于爱情,关于理想,关于兄弟。不得不说,这类电影在我现在这个年纪,确实激发了荷尔蒙,让人热血沸腾。也让人重新思考,青春,应该怎么度过?

今年最喜欢的一首歌是郭龙和张玮玮的《眼望着北方》。听到这首歌的时候是在南京的夫子庙青旅。下午5点,7点的飞机。青旅里面就这样循环播放着张玮玮的歌,人们慵懒极了在吧台里和猫聊着天。大多数男人,都有一个“远方梦”,张玮玮的声音总让人憧憬那片未知的远方。现在依然想念那段自由没有目标和方向的日子。

今年最喜欢的一本书,想来想去,应该是《UNIX编程艺术》。虽然由于自身能力原因,没能看完,后半部分的内容超出了我的认识范围。前面关于UNIX编程**的内容让自己对程序设计有了新的认识。看这本书的时候是3、4月份,接下来的大半年工作,算是对这本书中“模块化”的实践。

今年最开心的一件事就是家里住进老家的新房了。一切回到了原点。家里摆了喜酒,好像从来都没有那么热闹过。。。

今年最大的遗憾是,职业生涯没有能上升到自己理想的高度。工资也没涨多少…… 不过我对这些都看得很淡。依然怀有美好的愿望,相信厚积薄发。

如上。

一套 Javascript 测试题

Javascript 测试

前些天阮一峰老师在微博转发的一套 Javascript 测试题传得挺火。我初次回答正确率仅为65%,恼羞成怒,痛定思痛,总结了一下。

原微博:http://weibo.com/1400854834/AvM7yeoiJ
题目出处:JavaScript Puzzlers! or: do you really know JavaScript?

  1. ["1", "2", "3"].map(parseInt)

    答案:[1, NaN, NaN]

    解答:题目考查的是对 map 方法和 parseInt 方法以及二进制的了解。

    Array.pototype.map 方法第一个参数是函数时,传递2三个参数给函数,分别是:element, index, array

    parseInt(string, radix) 有两个参数,分别意义是:

    string 必需。要被解析的字符串。
    radix 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

    在问题中,将1,0传给 parseInt,得出1;将2,1传给 parseInt,得出 NaN;将3,2传给 parseInt,得出 NaN(2进制中没有3)。

  2. [typeof null, null instanceof Object]

    答案:["object", false]

    解答:题目考查的是对 null 字面量的理解。

    字面量 null 的 typeof 结果为 "object" 被普遍认为是一个 ECMAscript 标准的 bug。理解 null 的最好方式时将它当做对象占位符。

    在使用 typeof 运算符时采用引用类型存储值会出现一个问题,无论引用的是什么类型的对象,它都返回 "object"。ECMAScript 引入了另一个 Java 运算符 instanceof 来解决这个问题。
    instanceof 被用作检测引用值。例如:

    var oStringObject = new String("hello world");
    alert(oStringObject instanceof String); //输出 "true"

    这段代码问的是“变量 oStringObject 是否为 String 对象的实例?” oStringObject 的确是 String 对象的实例,因此结果是 "true"。

    null instanceof Object 就像在问“null 是否为 Object 对象的实例?” 显然不是。

  3. [ [3,2,1].reduce(Math.pow), [].reduce(Math.pow)] ]

    答案:an error

    解答:题目考查对 reduce 方法和 Math.pow 方法的熟悉度

    关于 reduce 方法参考:ECMAScript 5.1
    关于 Math.pow 方法参考:ECMAScript 5.1

    很明显,对于第二个表达式由于是空数组且调用 reduce 时没有提供第二个参数,将引发 TypeError 异常。

    我们来看看第一个表达式的结果:
    reduce 方法向第一个参数函数传递四个值 previousValue, currentDigit, currentIndex, array ; Match.pow 只接受两个参数。
    reduce 第一次调用 Match.pow 时传入 3,2 返回 9
    reduce 第二次调用 Match.pow 时传入 9,1 返回 9
    所以最后的结果是 9

  4. 下面代码的运行结果是?

    var val = 'smtg';
    console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');

    答案:"Something"

    解答:题目考查对运算符优先级的认识。

    + 号运算符的优先级比 ? 号高。所以表达式的意思是:'Value is true' 是否为真;结果为真,所以输出 "Something"

  5. 详见第二题

  6. 下面代码的运行结果是?

    var name = 'World!';
    (function () {
        if (typeof name === 'undefined') {
            var name = 'Jack';
            console.log('Goodbye ' + name);
        } else {
            console.log('Hello ' + name);
        }
    })();

    答案:"Goobye Jack"

    解答:题目考查对作用域链和变量对象的理解。

    关于作用域链请参考:作用域链(Scope Chain)
    关于变量对象请参考:变量对象(Variable Object)

    简单地理解就是:变量声明会提前,变量取值遵循就近原则。
    所以var name; 在if之前已经声明,因为条件判断正确。而name的取值则以最近的为准所以为 "Jack" 。

  7. 下面代码的运行结果是?

    var END = Math.pow(2, 53);
    var START = END - 100;
    var count = 0;
    for (var i = START; i <= END; i++) {
        count++;
    }
    console.log(count);

    答:死循环

    解答:题目考查对 Javascript 数字范围的了解。

  8. 下面代码的运行结果是?

    var ary = [0,1,2];
    ary[10] = 10;
    ary.filter(function(x) { return x === undefined;});

    答案:[]

    解答:题目考查对数组和 filter 方法的了解。

    ary[10] 将会将ary数组长度扩展为11,3-10的项都是不存在的。
    filter 方法不会调用不存在的项。

  9. 下面代码的运行结果是?

    var two   = 0.2
    var one   = 0.1
    var eight = 0.8
    var six   = 0.6
    [two - one == one, eight - six == two]

    答案:[true, false]

    解答:题目考查对浮点数的认识。

    二进制的浮点数不能正确地处理十进制的小数,因此0.8-0.6不等于0.2。这是 Javascript 中最经常被报告的 bug,并且它是遵循二进制浮点数算术标准(IEEE 754)而有意导致的结果。

    通常的最佳编程实践是,通过指定精度来避免小数表现处理的错误。

  10. 下面代码的运行结果是?

    function showCase(value) {
        switch(value) {
        case 'A':
            console.log('Case A');
            break;
        case 'B':
            console.log('Case B');
            break;
        case undefined:
            console.log('undefined');
            break;
        default:
            console.log('Do not know!');
        }
    }
    showCase(new String('A'));

    答案:"Do not know!"

    解答:题目考查对 String 对象和 switch 语句的认识。

    switch的比较采用的是 '===' ,因为对象不等于字符串,所以语句掉入最后的条件中输出 "Do not know"

  11. 下面代码的运行结果是?

    function showCase2(value) {
        switch(value) {
        case 'A':
            console.log('Case A');
            break;
        case 'B':
            console.log('Case B');
            break;
        case undefined:
            console.log('undefined');
            break;
        default:
            console.log('Do not know!');
        }
    }
    showCase(String('A'));

    答案:"Case A"

    解答:题目考查对 String() 函数的认识。

    String 用途是强制转换类型。

  12. 下面代码的运行结果是?

    function isOdd(num) {
        return num % 2 == 1;
    }
    function isEven(num) {
        return num % 2 == 0;
    }
    function isSane(num) {
        return isEven(num) || isOdd(num);
    }
    var values = [7, 4, '13', -9, Infinity];
    values.map(isSane);

    答案:[true, true, true, false, false]

    解答:题目考查对 % 和 || 运算符的了解。

    7 % 2 == 1 => true;
    4 % 2 == 0 => true;
    '13' % 2 == 1 => true;
    -9 % 2 == -1 => false
    Infinity % 2 == NaN => false

  13. 下面代码的运行结果是?

    parseInt(3, 8)
    parseInt(3, 2)
    parseInt(3, 0)

    答案:3, NaN, 3

    解答:考查对 parseInt 函数和进制的了解。详见第一题。

  14. Array.isArray( Array.prototype )

    答案:true

    解答:考查对 Array.prototype 的认识。

  15. 下面代码的运行结果是?

    var a = [0];
    if ([0]) { 
      console.log(a == true);
    } else { 
      console.log("wut");
    }

    答案:false

    解答:题目考查对if语句, == 运算符的认识。非空数组在if语句里是为true,但用作 == 运算符时,它有完全不同的转换规则。

  16. 下面代码的运行结果是?

    []==[] 

    答案:false

    解答:题目考查对 == 运算符的认识。 == 有着复杂的转换规律,有时候会表现出令人意外的结果。

  17. 下面代码的运行结果是?

    '5' + 3  
    '5' - 3  

    答案:"53", 2

    解答:题目考查对 +/- 运算符的认识。

    + 运算符可以用于加法运算或字符串连接。它究竟会如何执行取决于其参数的类型。如果其中一个运算数是一个空字符串,它会把另一个运算数转换成字符串并返回。如果两个运算数都是数字,它会返回两者之和。否则,它把两个运算数都转换成字符串并连接起来。

    - 运算符进行减法运算。

  18. 下面代码的运行结果是?

    1 + - + + + - + 1 

    答案:2

    解答:不明觉厉

  19. 下面代码的运行结果是?

    var ary = Array(3);
    ary[0]=2;
    ary.map(function(elem) { return '1'; }); 

    答案:[1, undefined * 2]

    解答:题目考查对数组和数组方法的认识。

    第一行 var ary = Array(3); 得出 ary 为 [undefined * 3]
    第二行 ary[0]=2 得出 ary 为 [2, undefined * 2]
    第三行 map 方法只会处理存在的值,最后得出 [1, undefined * 2]

  20. 下面代码的运行结果是?

    function sidEffecting(ary) { 
      ary[0] = ary[2];
    }
    function bar(a,b,c) { 
      c = 10
      sidEffecting(arguments);
      return a + b + c;
    }
    bar(1,1,1)

    答案:21

    解答:题目考查对 arguments 对象的认识。

    Arguments对象是活动对象的一个属性,它包括如下属性:

    callee — 指向当前函数的引用
    length — 真正传递的参数个数
    properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。

    这个共享其实不是真正的共享一个内存地址,而是2个不同的内存地址,使用JavaScript引擎来保证2个值是随时一样的,当然这也有一个前提,那就是这个索引值要小于你传入的参数个数,也就是说如果你只传入2个参数,而还继续使用arguments[2]赋值的话,就会不一致。

    通过上面的介绍可以了解到,c=10,则 arguments[2] 也被赋值为 10。而将 arguments 传递给 sidEffection 函数,ary[0]=ary[2] 相当于 a=c ;所以最后 a+b+c 是 10+1+10。

  21. 下面代码的运行结果是?

    var a = 111111111111111110000,
        b = 1111;
    a + b;

    答案:111111111111111110000

    解答:Javascript的计算缺乏精度。这将影响较大的值和较小的值。

  22. 下面代码的运行结果是?

    var x = [].reverse;
    x();

    答案:window对象

    解答:题目考查对 reverse 方法的认识和对 this 的理解。

    关于 reverse 方法参考:标准

    reverse 方法返回调用者自身。x() 的调用者是 window 对象。

  23. Number.MIN_VALUE > 0

    答案:true

    解答:Number.MIN_VALUE 是一个 Javascript 能够表示的最小数值,指的是能够表示的最大小数点后位数。0.00....5

  24. [1 < 2 < 3, 3 < 2 < 1]

    答案:[true, true]

    解答:题目考查对 < 运算符的认识。< 会进行隐身类型转换;所以第二个表达式 3 < 2 结果为 false,false < 1 转换的结果为 true。

  25. 2 == [[[2]]]

    答案:true

    解答:== 运算符会进行类型转换。左右两边不断调用 toString 的结果就是 2。

  26. 下面代码的运行结果是?

    3.toString()
    3..toString()
    3...toString()

    答案:error, "3", error

    解答:不明觉厉

  27. 下面代码的运行结果是?

    (function(){
      var x = y = 1;
    })();
    console.log(y);
    console.log(x);

    答案:1, error

    解答:考查对变量声明和作用域的认识。注意 var x = y =1; 分解成实际是 var x = y; y = 1; 。函数内的变量没有用 var 声明,则产生了一个隐身的全局变量。

  28. 下面代码的运行结果是?

    var a = /123/,
        b = /123/;
    a == b
    a === b

    答案:false, false

    解答:题目考查对正则对象的认识。a和b都是正则实例的字面量表示,永远都不会相等。类似:var a = {a: 1},b = {a: 1}; a==b;a===b 结果是 false 一样。

  29. 下面代码的运行结果是?

    var a = [1, 2, 3],
        b = [1, 2, 3],
        c = [1, 2, 4]
    a ==  b
    a === b
    a >   c
    a <   c

    答案:false, false, false, true

    解答:题目考查对几个运算符的认识。数组是无法直接用来做是否相等的比较的。数组用作大小比较时,会比较每一项。

  30. 下面代码的运行结果是?

    var a = {}, b = Object.prototype;
    [a.prototype === b, Object.getPrototypeOf(a) === b]

    答案:[false, true]

    解答:题目解答考查对 prototype 属性的认识。prototype 是函数的一个属性,所以 a.prototype 是 undefined。 a是通过字面量声明的对象,它的原型就是 Object.prototype。

    关于 getPrototypeOf 参考:标准

  31. 下面代码的运行结果是?

    function f() {}
    var a = f.prototype, b = Object.getPrototypeOf(f);
    a === b

    答案:false

    解答:f.prototype 是 f 实例化后的原型。f 的原型是函数。

  32. 下面代码的运行结果是?

    function foo() { }
    var oldName = foo.name;
    foo.name = "bar";
    [oldName, foo.name]

    答案:['foo', 'foot']

    解答:考查对函数属性的认识。name 属性是只读的,所以无法修改(不报错是奇葩)。

  33. 下面代码的运行结果是?

    "1 2 3".replace(/\d/g, parseInt)

    答案:"1, NaN, 3"

    解答:题目考查对 replace 方法和 parseInt 函数的认识。

    replace 方法的介绍参考:标准

    replace 方法可以接受多种类型的参数,当第二个参数是函数时,将会传三个值给函数,分别是 一次匹配的字符串,字符串偏移量,整个匹配的字符串。

    在该问题中,分别传了 "1", 0, "1 2 3"; "2", 2, "1 2 3"; "3", 4, "1 2 3"给 parseInt。
    所以每个是分别运行了 parseInt("1", 0); parseInt("2", 2); parseInt("3", 4);然后替换里面的匹配值。

  34. 下面代码的运行结果是?

    function f() {}
    var parent = Object.getPrototypeOf(f);
    f.name // ?
    parent.name // ?
    typeof eval(f.name) // ?
    typeof eval(parent.name) //  ?

    答案:"f", "Empty", "function", error

    解答:题目考查对 prototype 属性和 name 属性以及作用域链的了解。

  35. 下面代码的运行结果是?

    var lowerCaseOnly =  /^[a-z]+$/;
    [lowerCaseOnly.test(null), lowerCaseOnly.test()]

    答案:[true, true]

    解答:题目考查对 test 方法的了解。

    关于 test 方法参考:标准

    test 方法接收字符串参数,如果不是则强制转型。

  36. 下面代码的运行结果是?

    [,,,].join(", ")

    答案:", , "

    解答:题目考查对数组的认识。

    Javascript 数组允许以 , 号结尾。所以题目中的数组实际上是一个 undefined * 3 的数组。
    相当于[undefined, undefined, undefined,] => [undefined, undefined, undefined]
    想一想这个表达式的结果是什么?[1,1,1].join(", "")
    结果是:"1, 1, 1"
    所以就不难理解为什么 [undefined, undefined, undefined].join(", ") 的结果是 ", , "

  37. 下面代码的运行结果是?

    var a = {class: "Animal", name: 'Fido'};
    a.class

    答案:最后运行的结果与浏览器相关。

    解答:题目考查对 Javascript 保留字的认识。

前端编程规范

编程规范的制定很大程度上是为了弥补语言的不足。Javascript作为一门弱类型动态原型的语言,如果在团队开发中没有编程规范,后果将不堪设想。各花入各眼,欢迎交流。

编程规范


目录

前言

目标:提高项目的可维护性和可扩展性

约定

代号

编程风格的制定参考了以下业界文档:

  1. (代号①)jQuery核心风格指南(jQuery Core Style Guide
  2. (代号②)Dauglas Crockford的JavaScript代码规范(Code Conventions for the JavaScript Programming Language
  3. (代号③)Google的JavaScript风格指南(Google JavaScript Style Guide
  4. (代号④)Dojo编程风格指南(Dojo Style Guide

在编程风格章节,部分条目是参照以上的文档制定,在说明的最后会有如下注释,即代表该条目是参照《Google的JavaScript风格指南》

参考:③

编程风格

“程序是写给人读的,只是偶尔让计算机执行一下。” —— Donald Knuth

在团队开发中,所有的代码看起来一致是极其重要的,原因有以下几点:

  • 任何开发者都不会在乎某个文件的作者是谁,也没有必要花费额外精力去理解代码逻辑并重新排版,因为所有代码排版格式看起来非常一致。
  • 我能很容易地识别出问题代码并发现错误。

语言规范

总是开启严格模式,即在各模块顶部添加 'use strict'; 声明。

关于严格模式参考 MDN - Strict mode

define(function (require, exports, module) {
    'use strict';

    // ...
});

function doSomething () {
    'use strict';

    //...
}
<script>
    'use strict';

    //...
</script>

变量

声明

总是使用 var 来声明变量

var name = 'alvin';

变量声明总是提前。

将所有的var语句合并为一个语句,每个变量的初始化独占一行。对于那些没有初始值的变量来说,它们应当出席在var语句的尾部。

// Good
function doSometingWithItems (items, count) {
    var value = 10,
        num = value + count,
        item,
        result,
        i,
        len;

    if (num > 0) {
        for (i = 0, len = items.length; i < len; i += 1) {
            item = items[i];
            result += item - num;
        }
    }

    return result;
}

// Bad
function doSometingWithItems (items, count) {
    var value = 10;
    var num = value + count;

    if (num > 0) {
        var result;
        for (var i = 0, len = items.length; i < len; i += 1) {
            var item = items[i];
            result += item - num;
        }

        return result;
    }
}
赋值

总是使用直接量

// Good
var name = 'alvin';
var count = 100;
var forever = true;
var numbers = [1, 2, 3, 4];
var book = {
    title: 'Javascript',
    author: 'Brendan Eich'
};

// Bad
var name = new String('alvin');
var count = new Number(100);
var forever = new Boolean(true);
var numbers = new Array(1, 2, 3, 4);
var book = new Object();
book.title = 'Javascript';
book.author = 'Brendan Eich';

分号

总是使用分号。

如果不加分号JS解释器也会按隐式分隔的标准去执行,但那样调试、压缩、合并的时候都很不方便。

而且在某些情况下,不写分号可是很危险的:

MyClass.prototype.myMethod = function() {
  return 42;
}  // 这个缺德的没写分号

(function() {
  // 匿名函数的执行
})();

上段代码会发生什么事情?

会报错(number is not a function)-第一个方法返回了42,因为没分号啊,后面就直接跟括号,所以第二个方法就很杯具的被当成一个参数传进来给42执行了(效果等同于 42(func)() ),可42并不是一个方法,报错。

括号

if...else...whilefordo...while...try...catch..finally...总是使用括号

// Good
if (condition) {
    doSomething();
} else if (otherCondition) {
    doOtherThing();
} else {
    doSomethigElse()
}

// Bad
if (condition)
    doSomething();
else if (otherCondition)
    doOtherThing();
else
    doSomethingElse();

// Good
var i;
for (i in object) {
    doSomething();
} 

// Bad
var i;
for (i in object) 
    doSomething();

Switch

  1. 禁止出现连续执行(fall through)。每一个case代码块内都应当使用 break
  2. default 什么都不做时,省略 dafault ,但必须写上注释。
switch () {
    case 'first':
        //代码
        break;
    case 'second':
        //代码
        break;
    case 'third':
        //代码
        break;
    default:
        //代码
}

switch () {
    case 'first':
        //代码
        break;
    case 'second':
        //代码
        break;
    case 'third':
        //代码
        break;

    //没有default
}

for-in循环

for-in循环是用来遍历对象属性的。不用定义任何控制条件,循环将会有条不紊地遍历每一个对象属性,并返回属性名。

for-in循环有一个问题,就是它不仅遍历对象的实例属性,同意还遍历原型继承来的属性。出于这个原因,最好使用 hasOwnProperty() 方法来为for-in循环过滤出实例属性。

var person = {name: 'alvin'};
var student = Object.create(person);
student.age = 12;

var i;
for (i in student) {
    console.log(i);
    if (student.hasOwnProperty(i)) {
        console.log(student[i]);
    }
}

相等

使用 ===!== 而不是 ==!=,除非你百分百确定等式两边的类型是相等的。

eval()

只用于反序列化。(反序列化的意思是从字节流中重构对象,这里指的应该是JSON字符串重构成对象,或是执行服务器返回的JS语句)

eval() 很不稳定,会造成语义混乱,如果代码里还包含用户输入的话就更危险了,因为你无法确切得知用户会输入什么。

然而 eval 很容易解析被序列化的对象,所以反序列化的任务还是可以交给它做的。

arguments

arguments.calleearguments.caller 将在未来的 JavaScript 版本中被禁用,因此在代码中禁止使用。

this

仅在构造函数,方法,闭包中去使用它。

this 语义很特别。它大多数情况下会指向全局对象,有的时候却是指向调用函数的作用域的(使用 eval 时),还可能会指向DOM树的某个节点(绑定事件时),新创建的对象(构造函数中),也可能是其他的一些什么乱七八糟的玩意(如果函数被 call() 或者被 apply() )。

很容易出错的,所以最好是以下这两种情况的时候再选择使用:

  1. 在构造函数中(原型对象)
  2. 在对象的方法中(包括创建的闭包)

多级原型结构

不是怎么推荐使用。

多级原型结构指的是 JavaScript 实现继承。

比如自定义类D,并把自定义类B作为D的原型,那就是一个多级原型结构了。

原型结构越来越复杂了就越难维护,所以无非必要,或许你非常确定你在做些什么,不要使用继承。

多行字符串字面量

不要这样写:

var myString = 'A rather long string of English text, an error message \
                actually that just keeps going and going -- an error \
                message to make the Energizer bunny blush (right through \
                those Schwarzenegger shades)! Where was I? Oh yes, \
                you\'ve got an error and all the extraneous whitespace is \
                just gravy.  Have a nice day.';

空白字符开头字符行不能被很安全的编译剥离,以至于斜杠后面的空格可能会产生奇怪的错误。虽然大多数脚本引擎都支持这个,但它并不是ECMAScript标准的一部分。

可以用 + 号运算符来连接每一行:

var myString = 'A rather long string of English text, an error message ' +
    'actually that just keeps going and going -- an error ' +
    'message to make the Energizer bunny blush (right through ' +
    'those Schwarzenegger shades)! Where was I? Oh yes, ' +
    'you\'ve got an error and all the extraneous whitespace is ' +
    'just gravy.  Have a nice day.';

修改内置对象的原型

永远不要修改原生对象及其原型中已存在的方法,如需增加方法要先做判断。

var aProto = Array.prototype;

aProto.isArray = aProto.isArray || function () {
    // ...
};

代码风格

缩进

使用四空格字符为一个缩进层级

// Good
function doSomething () {
    var name = 'alvin';

    if (name = 'alvin') {
        for () {

        }
    }
}

// Not good
function doSomething () {
  var name = 'alvin';

  if (name = 'alvin') {
    for () {

    }
  }
}

引号

使用单引号(')比双引号(")更好,特别是当创建一个HTML代码的字符串时候:

var msg = 'This is some HTML<a href="">link</a>';

介于此,我们字符串的字面量以单引号为准。

空白

  1. 运算符两边总是留有一个空格

    // Good 
    var count = max + min;
    var result = condition ? goodOne : badOne;
    
    for (i = 0, l = o.leng; l > i; i++) {
        //...
    }
    
    // Bad
    var count=max+min;
    var result=condition?goodOne:badOne;
    
    for (i=0,l=o.leng;l>i;i++) {
        //...
    }
  2. 块语句的间隔

    在左圆括号之前和右圆括号之后添加一个空格

    // Good
    if (condition) {
        //...
    }
    
    switch (condition) {
        //..
    }
    
    // Bad
    if(condition){
        //...
    }
    
    switch(condition){
        //..
    }

括号的对齐方式

将左花括号放置在块语句中第一句代码的末尾

// Good
if () {
    //...
} else if () {
    //...
} else {
    //...
}

// Bad
if () 
{
    //...
}
else if ()
{
    //...
}
else
{
    //...
}
// Good
switch () {

}

while () {

}

for () {

}

do {

} while () {

}

try {

} catch () {

} finally {

}

行的长度

行的长度应限定在80个字符

换行

当一行长度达到了单行最大字符数限制时,就需要手动将一行拆成两行。通常我们会在运算符后换行,下一行会增加两个层级的缩进。

// Good
callAFunction(document, element, window, 'some string value', true, 123,
        navigator);

// Bad
callAFunction(document, element, window, 'some string value', true, 123,
    navigator);

// Very bad
callAFunction(document, element, window, 'some string value', true, 123
        , navigator);
// 语句换行
if (isLeapYear && isFebruary && day === 29 && itsYourBirthday &&
        noPlans) {
    waitAnotherFourYears();
}
// 变量赋值时:第二行的位置应当和赋值运算符的位置保持对齐
var result = somethig + anotherThing + yetAnotherThing + someThingElse + 
             anotherSomeThingElse;

空行

在下列场景中添加空行:

  1. 在方法之间

    // Good
    function doSometing() {
        //...
    }
    
    function  doOtherThing () {
        //...
    }
    
    // Bad
    function doSometing() {
        //...
    }
    function  doOtherThing () {
        //...
    }
  2. 在方法中的局部变量和第一条语句之间

    // Good
    function doSomething () {
        var name = 'Alvin',
            age = 23;
    
        if (condition) {
    
        }
    }
    
    // Bad
    function doSomething () {
        var name = 'Alvin',
            age = 23;
        if (condition) {
    
        }
    }
  3. 在多行或单行注释之前

    // Good
    function doSomething() {
        var name = 'Alvin';
    
        // 如果代码执行到这里,则表明通过了所有安全性检查
        if (condition) {
    
        }
    }
    
    // Bad
    function doSomething() {
        var name = 'Alvin';
        // 如果代码执行到这里,则表明通过了所有安全性检查
        if (condition) {
    
        }
    }
  4. 在方法内的逻辑片段之间插入空行,提高可读性。

    // Good
    if (w1 && w1.length) {
    
        for (i = 0, l = w1.length; i < l; i += 1) {
            p = w1[i];
            type = Y.Lang.type(r[p]);
    
            if (s.hasOwnProperty(p)) {
    
                if (merge && type =='object') {
                    Y.mix(r[p], s[p]);
                } else if (ov || ! (p in r)) {
                    r[p] = s[p];
                }
            }
        }
    }
    
    // Bad 
    if (w1 && w1.length) {
        for (i = 0, l = w1.length; i < l; i += 1) {
            p = w1[i];
            type = Y.Lang.type(r[p]);
            if (s.hasOwnProperty(p)) {
                if (merge && type =='object') {
                    Y.mix(r[p], s[p]);
                } else if (ov || ! (p in r)) {
                    r[p] = s[p];
                }
            }
        }
    }

命名

采用驼峰式大小写命名法。

var thisIsMyName;
var anotherVariable;
var aVeryLongVariableName;
变量

变量命名应总是遵守小驼峰命名法。

  1. 变量命名前缀应当是名词

    以名词作为前缀可以让变量和函数区分开来,因为函数名前缀应当是动词。

  2. 命名长度应该尽可能短,并且抓住要点(有意义)。

    foo、bar和thisIsBannerAndBodyWidth之类的命名应当避免。

  3. 尽量在变量名中体现出值的数据类型。

    比如:命名count、length、size表示数据类型是数字,而命名name、title和message表明数据类型是字符串。

// Good
var count = 10;
var myName = 'Alvin';
var found = true;

// Bad
var getCount = 10;
var isFound = true;
属性和方法

私有的属性,变量和方法(在文件或类中)都应该改以下划线开头。

受保护的属性,变量和方法不需要用下划线(和公开的一样)。

函数

函数名的第一个单词应该是动词。这里有一些使用动词常见的约定。

  1. can => 函数返回一个布尔值
  2. has => 函数返回一个布尔值
  3. is => 函数返回一个布尔值
  4. get => 函数返回一个非布尔值
  5. set => 函数用来保存一个值
常量

使用大写字母和下划线来命名,下划线用以分隔单词,比如:

var MAX_COUNT = 10;
var URL = 'http://m.quecai.com';
构造函数

构造函数命名应总是遵守大驼峰命名法。

function Person (name) {
    this.name = name;
}

Person.prototype.sayName = function() {
    alert(this.name);
};

var me = new Person('Alvin');

构造函数命名也常常是名词,因为它们是用来创建某个类型的实例的。

注释

单行注释

以两个斜线开始,双斜线后敲入一个空格。

// 这是一个单行注释

使用方法:

  1. 独占一行的注释,用来解释下一行代码。这行注释之前总是有一个空行,且缩进层级和下一行代码保持一致。

    // Good
    if (condition) {
    
        //如果代码执行到这里,则表明通过了所有安全性检查
        allowed();
    }
    
    // Bad
    if (condition) {
    //如果代码执行到这里,则表明通过了所有安全性检查
        allowed();
    }
  2. 在代码行的尾部的注释。代码结束到注释之间至少有一个空格。

    // Good
    var result = something + somethingElse; // somethingElse不应当取值为null
    
    // Bad
    var result = something + somethingElse;// somethingElse不应当取值为null
  3. 注释掉一大段代码

    // if (condition) {
    //     allowed();
    // }
    // var result = something + somethingElse;
    // var result = something + somethingElse;

多行注释

范例:

/*
 * 另一段注释
 * 这段注释包含两行文本
 */

多行注释总是出现在将要描述的代码段之前,注释和代码之间没有空行间隔。多行注释之前应当有一个空行,且缩进层级和其描述的代码保持一致。

// Good

if (condition) {

    /*
     * 另一段注释
     * 这段注释包含两行文本
     */
    allowed();
}

文档注释

最流行的文档注释格式来自于JavaDoc文档格式:多行注释以单斜线加双星号(/**)开始,接下来是描述信息,其中使用@符号来表示一个或多个属性。

关于文档注释,请参照:JsDoc

范例:

/**
返回一个对象,这个对象包含被提供对象的所有属性。
后一个对象的属性会覆盖前一个对象的属性。
传入一个单独的对象,会创建一个它的签拷贝。
@method merge
@param {Object} 被合并的一个或多个对象
@return {Object} 一个新的合并后的对象
**/
merge () {

}

文件和目录规划

编程最佳实践

避免使用全局变量

全局变量就是在所有作用域中都可见的变量。

在浏览器中,window对象往往重载并等同于全局对象,因此在全局作用域中声明的变量和函数都是window对象的属性。

var color = 'red';

function sayColor () {
    alert(color);
}

console.log(window.color); // 'red'
console.log(typeof window.sayColor); // 'function'

全局变量带来的问题

  1. 命名冲突

    当脚本中的全局变量越来越多时,和浏览器未来的API或其他开发者的代码产生冲突的概率就越高。

  2. 代码的脆弱性

    一个依赖全局变量的函数即是深度耦合于上下文环境之中。如果环境发生改变,函数有可能就失效了。

    // 如果全局变量color不存在,sayColor方法将会报错
    function sayColor () {
        alert(color);
    }
  3. 难以调试

    任何依赖全局变量才能正常工作的函数,只有为其重新创建完整的全局环境才能正确地测试它。

正确地使用parseInt

parseInt 是把字符串转换为整数的函数。它在遇到非数字时会停止解析,所以 parseInt('16')parseInt('16 coins') 会产生一样的结果。

如果该字符串第一个字符是0,那么该字符串会基于八进制二不是十进制来求值。在八进制中,8和9不是数字,所以 parseInt('08')parseInt('09') 都产生0的结果。

parseInt 可以接受一个基数作为参数,如此一来, parseInt('08', 10) 结果为8。请总是带上基数参数。

+ 运算符

+运算符可以用于加法运算或字符串连接。它究竟会如何执行取决于其参数的类型。

  1. 如果其中一个运算数是一个空字符串,它会把另一个运算符转换成字符串并返回。
  2. 如果两个运算数都是数字,返回两者之和。
  3. 其他情况,它把两个运算符都转换成字符串并连接起来。

假值

Javascript的众多假值:

  1. 值:0;类型:Number
  2. 值:NaN(非数字);类型:Number
  3. 值:''(空字符串);类型:String
  4. 值:false;类型:Boolean
  5. 值:null;类型:Object
  6. 值:undefined;类型:Undefined

但是这些值是不可以互换的。

感谢

最后,感谢两本书和它们的作者。

  1. 《JavaScript语言精粹》 Douglas Crockford
  2. 《编写可维护的JavaScript》 Nicholas C. Zakas

本规范中很多条目都是直接引用或总结了两本书中的观点。
同时,两位作者的其他书籍或框架对学习Javascript可以提供很多帮助。

字符串编码详解

字符串编码详解

参考并整理自 - http://blog.csdn.net/stilling2006/article/details/4129700

基础

0,1 表示一个位
8个位表示一个字节
8位的字节可以组合出256(2的8次方)种不同的状态

字符串编码标准

ASCII

计算机起源在美国。(英语)

把编号从0开始的32种状态分别规定了特殊的用途,把这些0×20以下的字节状态称为”控制码”。

把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。这个方案叫做 ANSI 的”Ascii”编码(American Standard Code for Information Interchange,美国信息互换标准代码)。

但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他 们的文字,他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一 个状态255。从128到255这一页的字符集被称”扩展字符集”。

GB2312

汉字方案叫做 “GB2312″。GB2312 是对 ASCII 的中文扩展

规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。

GBK标准

GBK标准:(后来又扩展成了GB18030,通称他们叫做 “DBCS”(Double Byte Charecter Set 双字节字符集))

但是**的汉字太多了,后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字 符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。

UNICODE

ISO(国际标谁化组织)废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母 和符号的编码!叫它”Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “UNICODE”。

UNICODE 开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。于是 ISO 就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ascii里的那些“半角”字符,UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于”半角”英文符号只需要用到低8位,所以其高 8位永远是0,

但是,UNICODE 在制订时没有考虑与任何一种现有的编码方案保持兼容,这使得 GBK 与UNICODE 在汉字的内码编排上完全是不一样的,没有一种简单的算术方法可以把文本内容从UNICODE编码和另一种编码进行转换,这种转换必须通过查表来进行。

如前所述,UNICODE 是用两个字节来表示为一个字符,他总共可以组合出65535不同的字符,这大概已经可以覆盖世界上所有文化的符号。如果还不够也没有关系,ISO已经准备 了UCS-4方案,说简单了就是四个字节来表示一个字符,这样我们就可以组
合出21亿个不同的字符出来(最高位有其他用途)。

UTF

UNICODE 来到时,一起到来的还有计算机网络的兴起,UNICODE 如何在网络上传输也是一个必须考虑的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8就是每次8个位传输数据,而UTF16就是每次16个位,只不过为了传输时的可靠性,从UNICODE到 UTF时并不是直接的对应,而是要过一些算法和规则来转换。

高地位判断

受到过网络编程加持的计算机僧侣们都知道,在网络里传递信息时有一个很重要的问题,就是对于数据高低位的解读方式,一些计算机是采用低位先发送的方法,例 如我们PC机采用的 INTEL 架构,而另一些是采用高位先发送的方式,在网络中交换数据时,为了核对双方对于高低位的认识是否是一致的,采用了一种很简便的方法,就是在文本流的开始时 向对方发送一个标志符——如果之后的文本是高位在位,那就发送”FEFF”,反之,则发送”FFFE”。不信你可以用二进制方式打开一个UTF-X格式的 文件,看看开头两个字节是不是这两个字节?

示例

当你在 windows 的记事本里新建一个文件,输入”联通”两个字之后,保存,关闭,然后再次打开,你会发现这两个字已经消失了,代之的是几个乱码!

其实这是因为GB2312编码与UTF8编码产生了编码冲撞的原因。

从网上引来一段从UNICODE到UTF8的转换规则:

Unicode
UTF-8

0000 - 007F
0xxxxxxx

0080 - 07FF
110xxxxx 10xxxxxx

0800 - FFFF
1110xxxx 10xxxxxx 10xxxxx

例如”汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以要用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 1100 0100 1001,将这个比特流按三字节模板的分段方法分为0110 110001 001001,依次代替模板中的x,得到:1110-0110 10-110001 10-001001,即E6 B1 89,这就是其UTF8的编码。

而当你新建一个文本文件时,记事本的编码默认是ANSI,如果你在ANSI的编码输入汉字,那么他实际就是GB系列的编码方式,在这种编码下,”联通”的内码是:

c1 1100 0001
aa 1010 1010
cd 1100 1101
a8 1010 1000

注意到了吗?第一二个字节、第三四个字节的起始部分的都是”110″和”10″,正好与UTF8规则里的两字节模板是一致的,于是再次打开记事本时,记事 本就误认为这是一个UTF8编码的文件,让我们把第一个字节的110和第二个字节的10去掉,我们就得到了”00001 101010″,再把各位对齐,补上前导的0,就得到了”0000 0000 0110 1010″,不好意思,这是UNICODE的006A,也就是小写的字母”j”,而之后的两字节用UTF8解码之后是0368,这个字符什么也不是。这就 是只有”联通”两个字的文件没有办法在记事本里正常显示的原因。

而如果你在”联通”之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时,记事本就不会坚持这是一个utf8编码的文件,而会用ANSI的方式解读之,这时乱码又不出现了。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.