2014年11月29日星期六

阮一峰的网络日志

阮一峰的网络日志


数据可视化:基本图表

Posted: 28 Nov 2014 10:14 PM PST

"数据可视化"可以帮助用户理解数据,一直是热门方向。

图表是"数据可视化"的常用手段,其中又以基本图表----柱状图、折线图、饼图等等----最为常用。

用户非常熟悉这些图表,但如果被问道,它们的特点是什么,最适用怎样的场合(数据集)?恐怕答得上来的人就不多了。

本文是电子书《Data Visualization with JavaScript》第一章的笔记,总结了六种基本图表的特点和适用场合,非常好地回答了上面的问题。

零、序言

进入正题之前,先纠正一种误解。

有人觉得,基本图表太简单、太原始,不高端,不大气,因此追求更复杂的图表。但是,越简单的图表,越容易理解,而快速易懂地理解数据,不正是"数据可视化"的最重要目的和最高追求吗?

所以,请不要小看这些基本图表。因为用户最熟悉它们,所以只要是适用的场合,就应该考虑优先使用。

一、柱状图(Bar Chart)

柱状图是最常见的图表,也最容易解读。

它的适用场合是二维数据集(每个数据点包括两个值x和y),但只有一个维度需要比较。年销售额就是二维数据,"年份"和"销售额"就是它的两个维度,但只需要比较"销售额"这一个维度。

柱状图利用柱子的高度,反映数据的差异。肉眼对高度差异很敏感,辨识效果非常好。柱状图的局限在于只适用中小规模的数据集。

通常来说,柱状图的X轴是时间维,用户习惯性认为存在时间趋势。如果遇到X轴不是时间维的情况,建议用颜色区分每根柱子,改变用户对时间趋势的关注。

上图是英国足球联赛某个年度各队的赢球场数,X轴代表不同球队,Y轴代表赢球数。

二、折线图(Line Chart)数据

折线图适合二维的大数据集,尤其是那些趋势比单个数据点更重要的场合。

它还适合多个二维数据集的比较。

上图是两个二维数据集(大气中二氧化碳浓度,地表平均气温)的折线图。

三、饼图(Pie Chart)

饼图是一种应该避免使用的图表,因为肉眼对面积大小不敏感。

上图中,左侧饼图的五个色块的面积排序,不容易看出来。换成柱状图,就容易多了。

一般情况下,总是应该用柱状图替代饼图。但是有一个例外,就是反映某个部分占整体的比重,比如贫穷人口占总人口的百分比。

四、散点图(Scatter Chart)

散点图适用于三维数据集,但其中只有两维需要比较。

上图是各国的医疗支出与预期寿命,三个维度分别为国家、医疗支出、预期寿命,只有后两个维度需要比较。

为了识别第三维,可以为每个点加上文字标示,或者不同颜色。

五、气泡图(Bubble Chart)

气泡图是散点图的一种变体,通过每个点的面积大小,反映第三维。

上图是卡特里娜飓风的路径,三个维度分别为经度、纬度、强度。点的面积越大,就代表强度越大。因为用户不善于判断面积大小,所以气泡图只适用不要求精确辨识第三维的场合。

如果为气泡加上不同颜色(或文字标签),气泡图就可用来表达四维数据。比如下图就是通过颜色,表示每个点的风力等级。

六、雷达图(Radar Chart)

雷达图适用于多维数据(四维以上),且每个维度必须可以排序(国籍就不可以排序)。但是,它有一个局限,就是数据点最多6个,否则无法辨别,因此适用场合有限。

下面是迈阿密热火队首发的五名篮球选手的数据。除了姓名,每个数据点有五个维度,分别是得分、篮板、助攻、抢断、封盖。

画成雷达图,就是下面这样。

面积越大的数据点,就表示越重要。很显然,勒布朗·詹姆斯(红色区域)是热火队最重要的选手。

需要注意的时候,用户不熟悉雷达图,解读有困难。使用时尽量加上说明,减轻解读负担。

七、总结

图表 维度 注意点
柱状图 二维 只需比较其中一维
折线图 二维 适用于较大的数据集
饼图 二维 只适用反映部分与整体的关系
散点图 二维或三维 有两个维度需要比较
气泡图 三维或四维 其中只有两维能精确辨识
雷达图 四维以上 数据点不超过6个

(完)

文档信息

2014年11月11日星期二

阮一峰的网络日志

阮一峰的网络日志


编译器的工作过程

Posted: 10 Nov 2014 09:43 PM PST

源码要运行,必须先转成二进制的机器码。这是编译器的任务。

比如,下面这段源码(假定文件名叫做test.c)。

 #include <stdio.h>  int main(void) {   fputs("Hello, world!\n", stdout);   return 0; } 

要先用编译器处理一下,才能运行。

 $ gcc test.c $ ./a.out Hello, world! 

对于复杂的项目,编译过程还必须分成三步。

 $ ./configure $ make   $ make install 

这些命令到底在干什么?大多数的书籍和资料,都语焉不详,只说这样就可以编译了,没有进一步的解释。

本文将介绍编译器的工作过程,也就是上面这三个命令各自的任务。我主要参考了Alex Smith的文章《Building C Projects》。需要声明的是,本文主要针对gcc编译器,也就是针对C和C++,不一定适用于其他语言的编译。

第一步 配置(configure)

编译器在开始工作之前,需要知道当前的系统环境,比如标准库在哪里、软件的安装位置在哪里、需要安装哪些组件等等。这是因为不同计算机的系统环境不一样,通过指定编译参数,编译器就可以灵活适应环境,编译出各种环境都能运行的机器码。这个确定编译参数的步骤,就叫做"配置"(configure)。

这些配置信息保存在一个配置文件之中,约定俗成是一个叫做configure的脚本文件。通常它是由autoconf工具生成的。编译器通过运行这个脚本,获知编译参数。

configure脚本已经尽量考虑到不同系统的差异,并且对各种编译参数给出了默认值。如果用户的系统环境比较特别,或者有一些特定的需求,就需要手动向configure脚本提供编译参数。

 $ ./configure --prefix=/www --with-mysql 

上面代码是php源码的一种编译配置,用户指定安装后的文件保存在www目录,并且编译时加入mysql模块的支持。

第二步 确定标准库和头文件的位置

源码肯定会用到标准库函数(standard library)和头文件(header)。它们可以存放在系统的任意目录中,编译器实际上没办法自动检测它们的位置,只有通过配置文件才能知道。

编译的第二步,就是从配置文件中知道标准库和头文件的位置。一般来说,配置文件会给出一个清单,列出几个具体的目录。等到编译时,编译器就按顺序到这几个目录中,寻找目标。

第三步 确定依赖关系

对于大型项目来说,源码文件之间往往存在依赖关系,编译器需要确定编译的先后顺序。假定A文件依赖于B文件,编译器应该保证做到下面两点。

(1)只有在B文件编译完成后,才开始编译A文件。

(2)当B文件发生变化时,A文件会被重新编译。

编译顺序保存在一个叫做makefile的文件中,里面列出哪个文件先编译,哪个文件后编译。而makefile文件由configure脚本运行生成,这就是为什么编译时configure必须首先运行的原因。

在确定依赖关系的同时,编译器也确定了,编译时会用到哪些头文件。

第四步 头文件的预编译(precompilation)

不同的源码文件,可能引用同一个头文件(比如stdio.h)。编译的时候,头文件也必须一起编译。为了节省时间,编译器会在编译源码之前,先编译头文件。这保证了头文件只需编译一次,不必每次用到的时候,都重新编译了。

不过,并不是头文件的所有内容,都会被预编译。用来声明宏的#define命令,就不会被预编译。

第五步 预处理(Preprocessing)

预编译完成后,编译器就开始替换掉源码中bash的头文件和宏。以本文开头的那段源码为例,它包含头文件stdio.h,替换后的样子如下。

 extern int fputs(const char *, FILE *); extern FILE *stdout;  int main(void) {     fputs("Hello, world!\n", stdout);     return 0; } 

为了便于阅读,上面代码只截取了头文件中与源码相关的那部分,即fputs和FILE的声明,省略了stdio.h的其他部分(因为它们非常长)。另外,上面代码的头文件没有经过预编译,而实际上,插入源码的是预编译后的结果。编译器在这一步还会移除注释。

这一步称为"预处理"(Preprocessing),因为完成之后,就要开始真正的处理了。

第六步 编译(Compilation)

预处理之后,编译器就开始生成机器码。对于某些编译器来说,还存在一个中间步骤,会先把源码转为汇编码(assembly),然后再把汇编码转为机器码。

下面是本文开头的那段源码转成的汇编码。

     .file   "test.c"     .section    .rodata .LC0:     .string "Hello, world!\n"     .text     .globl  main     .type   main, @function main: .LFB0:     .cfi_startproc     pushq   %rbp     .cfi_def_cfa_offset 16     .cfi_offset 6, -16     movq    %rsp, %rbp     .cfi_def_cfa_register 6     movq    stdout(%rip), %rax     movq    %rax, %rcx     movl    $14, %edx     movl    $1, %esi     movl    $.LC0, %edi     call    fwrite     movl    $0, %eax     popq    %rbp     .cfi_def_cfa 7, 8     ret     .cfi_endproc .LFE0:     .size   main, .-main     .ident  "GCC: (Debian 4.9.1-19) 4.9.1"     .section    .note.GNU-stack,"",@progbits 

这种转码后的文件称为对象文件(object file)。

第七步 连接(Linking)

对象文件还不能运行,必须进一步转成可执行文件。如果你仔细看上一步的转码结果,会发现其中引用了stdout函数和fwrite函数。也就是说,程序要正常运行,除了上面的代码以外,还必须有stdout和fwrite这两个函数的代码,它们是由C语言的标准库提供的。

编译器的下一步工作,就是把外部函数的代码(通常是后缀名为.lib和.a的文件),添加到可执行文件中。这就叫做连接(linking)。这种通过拷贝,将外部函数库添加到可执行文件的方式,叫做静态连接(static linking),后文会提到还有动态连接(dynamic linking)。

make命令的作用,就是从第四步头文件预编译开始,一直到做完这一步。

第八步 安装(Installation)

上一步的连接是在内存中进行的,即编译器在内存中生成了可执行文件。下一步,必须将可执行文件保存到用户事先指定的安装目录。

表面上,这一步很简单,就是将可执行文件(连带相关的数据文件)拷贝过去就行了。但是实际上,这一步还必须完成创建目录、保存文件、设置权限等步骤。这整个的保存过程就称为"安装"(Installation)。

第九步 操作系统连接

可执行文件安装后,必须以某种方式通知操作系统,让其知道可以使用这个程序了。比如,我们安装了一个文本阅读程序,往往希望双击txt文件,该程序就会自动运行。

这就要求在操作系统中,登记这个程序的元数据:文件名、文件描述、关联后缀名等等。Linux系统中,这些信息通常保存在/usr/share/applications目录下的.desktop文件中。另外,在Windows操作系统中,还需要在Start启动菜单中,建立一个快捷方式。

这些事情就叫做"操作系统连接"。make install命令,就用来完成"安装"和"操作系统连接"这两步。

第十步 生成安装包

写到这里,源码编译的整个过程就基本完成了。但是只有很少一部分用户,愿意耐着性子,从头到尾做一遍这个过程。事实上,如果你只有源码可以交给用户,他们会认定你是一个不友好的家伙。大部分用户要的是一个二进制的可执行程序,立刻就能运行。这就要求开发者,将上一步生成的可执行文件,做成可以分发的安装包。

所以,编译器还必须有生成安装包的功能。通常是将可执行文件(连带相关的数据文件),以某种目录结构,保存成压缩文件包,交给用户。

第十一步 动态连接(Dynamic linking)

正常情况下,到这一步,程序已经可以运行了。至于运行期间(runtime)发生的事情,与编译器一概无关。但是,开发者可以在编译阶段选择可执行文件连接外部函数库的方式,到底是静态连接(编译时连接),还是动态连接(运行时连接)。所以,最后还要提一下,什么叫做动态连接。

前面已经说过,静态连接就是把外部函数库,拷贝到可执行文件中。这样做的好处是,适用范围比较广,不用担心用户机器缺少某个库文件;缺点是安装包会比较大,而且多个应用程序之间,无法共享库文件。动态连接的做法正好相反,外部函数库不进入安装包,只在运行时动态引用。好处是安装包会比较小,多个应用程序可以共享库文件;缺点是用户必须事先安装好库文件,而且版本和安装位置都必须符合要求,否则就不能正常运行。

现实中,大部分软件采用动态连接,共享库文件。这种动态共享的库文件,Linux平台是后缀名为.so的文件,Windows平台是.dll文件,Mac平台是.dylib文件。

(文章完)

=====================================================

以下为广告部分。欢迎大家在我的网络日志投放广告,推广自己的产品。今天介绍的是100offer

[赞助商广告]

优秀的人才找到合适的归宿,是这个世界最幸福的事情之一。100offer程序员拍卖网站通过创新的拍卖方式,致力于帮助优秀程序员寻找归宿,给予求职者更多更好的职业选择。

过去三个月,100offer中成功的求职者,平均薪资涨幅高于30%,在2周内拿到3-5个offer。100offer与传统招聘网站存在极大差异,主要为下:

1、只接受部分候选人:100offer目前仅仅接受年薪高于15万,有一二线知名互联网公司工作经验的优秀程序员申请者。

2、反向模式:传统招聘网站是写简历投递给多家公司,而这里程序员只需要提交一次简历给offer,待审核通过后,100offer会邀约平台企业来竞拍候选人,产生一次投递数百家互联网公司的效果。拍卖时程序员会接受到来自各公司新鲜热辣的面试邀请,体验与传统网站截然不同。

3、绝对隐私:担心自己的隐私被雇主看到是完全不必要的:1、候选人同意面试邀请前,公司是完全看不到候选人的姓名、联系方式等隐私信息。2、拍卖开始前,候选人可以手动屏蔽掉3家公司,他们将永远看不到你的简历!

已经有众多大牛程序员通过100offer找到心仪的工作,目前11月候选人在征集中,点击图片注册100offer并提交完整简历的程序员朋友,即可获赠15元亚马逊礼品卡!(活动截止期为2014年12月30日)

100offer目前阶段对企业免费,欢迎极客型创业公司和有实力的互联网公司前来注册招聘

(完)

文档信息

2014年10月9日星期四

阮一峰的网络日志

阮一峰的网络日志


JavaScript 运行机制详解:再谈Event Loop

Posted: 08 Oct 2014 06:11 AM PDT

一年前,我写了一篇《什么是 Event Loop?》,谈了我对Event Loop的理解。

上个月,我偶然看到了Philip Roberts的演讲《Help, I'm stuck in an event-loop》。这才尴尬地发现,自己的理解是错的。我决定重写这个题目,详细、完整、正确地描述JavaScript引擎的内部运行机制。下面就是我的重写。

进入正文之前,插播一条消息。我的新书《ECMAScript 6入门》出版了(版权页内页1内页2),铜版纸全彩印刷,非常精美,还附有索引(当然价格也比同类书籍略贵一点点)。预览和购买点击这里

cover

一、为什么JavaScript是单线程?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

二、任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时CPU完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,JavaScript就有了两种执行方式:一种是CPU按顺序执行,前一个任务结束,再执行下一个任务,这叫做同步执行;另一种是CPU跳过等待时间长的任务,先处理后面的任务,这叫做异步执行。程序员自主选择,采用哪种执行方式。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后继续执行后续的任务。

(3)一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

任务队列

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

三、事件和回调函数

"任务队列"实质上是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当异步任务从"任务队列"回到执行栈,回调函数就会执行。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先返回主线程。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动返回主线程。但是,由于存在后文提到的"定时器"功能,主线程要检查一下执行时间,某些事件必须要在规定的时间返回主线程。

四、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。

Event Loop

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

执行栈中的代码,总是在读取"任务队列"之前执行。请看下面这个例子。

     var req = new XMLHttpRequest();     req.open('GET', url);         req.onload = function (){};         req.onerror = function (){};         req.send(); 

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。

     var req = new XMLHttpRequest();     req.open('GET', url);     req.send();     req.onload = function (){};         req.onerror = function (){};    

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。

五、定时器

除了放置异步任务,"任务队列"还有一个作用,就是可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。

 console.log(1); setTimeout(function(){console.log(2);},1000); console.log(3); 

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

 setTimeout(function(){console.log(1);}, 0); console.log(2); 

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。

另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

六、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

请看下面的示意图(作者@BusyRich)。

Node.js

根据上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

Node.js有一个process.nextTick()方法,可以将指定事件推迟到Event Loop的下一次执行,或者说放到"任务队列"的头部,也就是当前的执行栈清空之后立即执行。

 function foo() {     console.error(1); }  process.nextTick(foo); console.log(2); // 2 // 1 

process.nextTick(foo)的作用,与setTimeout(foo, 0)很相似,但是执行效率高得多

(完)

文档信息

2014年10月2日星期四

阮一峰的网络日志

阮一峰的网络日志


乔布斯的管理课

Posted: 01 Oct 2014 09:13 PM PDT

2011年11月出版的《乔布斯传》很畅销,也写得很好,我还写过一篇读后感

但是,很少有人知道,它的作者沃尔特·艾萨克森(Walter Isaacson)在6个月后的2012年4月,为《哈佛商业评论》写过一篇2万多字的长文,总结乔布斯的管理艺术,名字叫做《乔布斯的管理课:卓越领导力的14位密码》(The Real Leadership Lessons of Steve Jobs,英文版在线阅读PDF下载中文版在线阅读亚马逊豆瓣)。

乔布斯的管理课

这篇长文基本上是原著的浓缩,但是有一些新的细节。如果你没有时间读原著,可以只读它。下面,我摘录几个打动我的段落。

一、保持专注

1997年,乔布斯重新执掌濒临破产的苹果公司。那时,苹果公司有庞大的产品线,单单Macintosh电脑就有十几个版本。乔布斯决定砍掉大部分产品。

products

他在白板上画了一个2x2的网格。"这才是我们要的,"在两栏的底部,他写下"普通消费者"(Consumer)和"专业用户"(Pro),在两行的前端他写下"桌面设备"(Desktop)和"便携设备"(Portable)。他告诉团队,我们的工作就是要生产四个伟大的产品,表格的每一格代表一个产品,其余的产品应该全部取消。

还有一次,乔布斯问大家:"我们下一步最应该做的10件事是什么?",大家七嘴八舌,列出了10件事。乔布斯划掉了后面7件,说我们只做最前面的3件。

二、保持简单

乔布斯参观施乐公司的Palo Alto实验室,见到了刚刚发明的鼠标。他一下子被吸引住了。

Palo Alto

回来以后,他告诉设计人员,立刻仿造一个。但是,施乐公司的鼠标有三个键,乔布斯要求苹果的鼠标只能有一个键。

Apple II mouse

设计iPod操作界面时,乔布斯坚持最多只需三次点击,用户就能完成所有操作。

乔布斯经常问别人,你觉得有没有什么东西过于复杂了?他以此作为参考,决定下一步要开发的产品。

三、追求完美

乔布斯从小养成追求完美的性格。有一次,他与父亲在后院搭篱笆。父亲要求篱笆的背面也要精心施工。乔布斯说:"谁会去看篱笆的背面?"父亲回答:"你会看到。"

Macintosh Case

开发Macintosh电脑时,他不顾整个团队的反对,要求机箱内部的电路板和电线也要整齐美观。设计定案后,他把设计人员的签名镌刻在机箱上,"真正的工匠都对自己的作品签名"。

四、重视设计

乔布斯重返苹果后的第一件产品,是彩色的桌面电脑iMac。它的顶部有一个把手,并不是那么有用,有谁会提着桌面电脑走来走去呢?

iMac

生产部门希望去掉这个把手,这样可以降低成本。但是,乔布斯坚持要加上。因为,他觉得普通消费者仍然对电脑有敬畏感,认为那是高深莫测的产品。他希望,苹果的产品能够给人一种友好的、平易近人的、易于操作的感觉。这个把手可以传递这样的信号,仿佛在那里说,快来使用我吧。

(完)

文档信息