Javascript | 酷 壳 - CoolShell https://coolshell.cn 享受编程和技术所带来的快乐 - Coding Your Ambition Mon, 06 Jul 2020 09:19:45 +0000 zh-CN hourly 1 https://wordpress.org/?v=6.2 程序员练级攻略(2018) 与我的专栏 https://coolshell.cn/articles/18360.html https://coolshell.cn/articles/18360.html#comments Tue, 29 May 2018 04:38:23 +0000 https://coolshell.cn/?p=18360 写极客时间8个月了,我的专栏现在有一定的积累了,今天想自己推荐一下。因为最新的系列《程序员练级攻略(2018)版》正在连载中,而且文章积累量到了我也有比较足的自...

Read More Read More

The post 程序员练级攻略(2018) 与我的专栏 first appeared on 酷 壳 - CoolShell.]]>
写极客时间8个月了,我的专栏现在有一定的积累了,今天想自己推荐一下。因为最新的系列《程序员练级攻略(2018)版》正在连载中,而且文章积累量到了我也有比较足的自信向大家推荐我的这个专栏了。推荐就从最新的这一系统的文章开始。

2011年,我在 CoolShell 上发表了 《程序员技术练级攻略》一文,得到了很多人的好评(转载的不算,在我的网站上都有近1000W的访问量了)。并且陆续收到了一些人的反馈,说跟着这篇文章找到了不错的工作。几年过去,也收到了好些邮件和私信,希望我把这篇文章更新一下,因为他们觉得有点落伍了。是的,老实说,抛开这几年技术的更新迭代不说,那篇文章写得也不算特别系统,同时标准也有点低,当时是给一个想要入门的朋友写的,所以,非常有必要从头更新一下《程序员练级攻略》这一主题

目前,我在我极客时间的专栏上更新《程序员练级攻略(2018版)》。升级版的《程序员练级攻略》会比Coolshell上的内容更多,也更专业。这篇文章有【入门篇】、【修养篇】、【专业基础篇】、【软件设计篇】、【高手成长篇】五大篇章,它们会帮助你从零开始,一步步地,系统地,从陌生到熟悉,到理解掌握,从编码到设计再到架构,从码农到程序员再到工程师再到架构师的一步一步进阶,完成从普通到精通到卓越的完美转身……

这篇文章是我写得最累也是最痛苦的文章,原因如下:

  •  学习路径的梳理。这是一份计算编程相关知识地图,也是一份成长和学习路径。所以有太多的推敲了,知识的路径,体,地图……这让我费了很多工夫,感觉像在编写一本教材一样,即不能太高大上,也不能误人子弟。
  • 新旧知识的取舍。另外,因为我的成长经历中很多技术都成了过去时,所以对于新时代的程序员应该学习新的技术,然后,很多基础技术在今天依然管用,所以,在这点上,哪些要那些不要,也花了我很多的工夫。
  • 文章书籍的推荐。为了推荐最好的学习资料和资源,老实说,我几乎翻遍了整个互联网,进行了大量的阅读和比较。这个过程让我也受益非浅。一开始,这篇文章的大小居然在500K左右,太多的信息就是没有信息,所以在信息的筛选上我花费了很多的工夫,删掉了60%的内容。但是,依然很宠大。

总之,你一定会被这篇文章的内容所吓到的,是的,我就是故意这样做的,因为,这本来就没有什么捷径,也不可能速成,很多知识都是硬骨头,你只能一口一口的啃,我故意这样做就是为了让你不要有“速成”的幻想,也可以轻而一举的吓退那些不想用功不想努力的人

但是,我们也要知道《易经》有云:“取法其上,得乎其中,取法其中,得乎其下,取法其下,法不得也”。所以,我这里会给你立个比较高标准,你要努力达到,相信我,就算是达不到,也会比你一开始期望的要高很多……

下面是这份练级攻略的目录,目前只在极客时间上发布,你需要付费阅读(在本文最后有相关的二维码)。

 

那么,除程序员练级攻略外,我还写了哪些内容?下面是迄今为止我所有的文章的目录。你可以在下面看一下相关的目录。这也算是我开收费专栏来8个月给大家的一份答卷吧。我也没有想到,我居然写了这么多的文章,而且对很多人都很有用。

首先是个人成长和经验之谈的东西,在这里的文章还没有完全更新完,未来要更新什么我也不清楚,但是可以呈现出来的内容和方向如下所示,供你参考。对于个人成长中的内容,都是我多年来的心得和体会,从读者的反馈来看是非常不错的,你一定要要阅读的。

分布式系统架构,我一共出了两个系列,一个是分布式系统架构的本质,另一个是设计模式。前者偏概念,后者偏技术。这里旨在让你看到整个分布式系统设计的一个非常系统的蓝图,但是因为在手机端上,不可能写得非常细,所以,会缺失一些细节,这些细节我是故意缺失的,主要是有几方面的原因,

  • 一方面,这是为了阅读的效果,手机上的文章不过长,所以,不能有太多的细节。
  • 另一方面,也是是想留给大家自行学习,而不是一定要我把饭喂到你的嘴里,你才能吃得着。学习不只是为要答案,而是学方法
  • 最后是我的私心,因为我也在创业,所以,技术细节上东西正是我在做的产品,所以,如果你想了解得更细,你需要和我有更商业合作。

 

区块链的技术专栏本来不在我的写作计划中的,但是因为来问我这方面的技术人太多了,所以,就被问了一系列的文章,这里的文章除了一些技术上的科普,同样有有很多我的观点,你不但可以学到技术,还可以了解一些金融知识和相关的逻辑,我个人觉得这篇文章是让你有独立思考的文章。

我的专栏还在继续,接下来还有一个系列的文章——《从技术到管理》,欢迎关注,也欢迎扫码订阅。

最后友情提示一下:在手机上学习并不是最好的学习方式,也不要在我的专栏上进行学习,把我的专栏当成一个你的助手,当成一个向导,当成一个跳板,真正的学习还是要在线下,专心的,系统地、有讨论地、不断实践地学习,这点希望大家切记!

 

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 程序员练级攻略(2018) 与我的专栏 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/18360.html/feed 63
Chrome开发者工具的小技巧 https://coolshell.cn/articles/17634.html https://coolshell.cn/articles/17634.html#comments Thu, 19 Jan 2017 12:25:55 +0000 http://coolshell.cn/?p=17634 Chrome的开发者工具是个很强大的东西,相信程序员们都不会陌生,不过有些小功能可能并不为大众所知,所以,写下这篇文章罗列一下可能你所不知道的功能,有的功能可能...

Read More Read More

The post Chrome开发者工具的小技巧 first appeared on 酷 壳 - CoolShell.]]>
Chrome的开发者工具是个很强大的东西,相信程序员们都不会陌生,不过有些小功能可能并不为大众所知,所以,写下这篇文章罗列一下可能你所不知道的功能,有的功能可能会比较实用,有的则不一定,也欢迎大家补充交流。

话不多话,我们开始。

代码格式化

有很多css/js的代码都会被 minify 掉,你可以点击代码窗口左下角的那个 { }  标签,chrome会帮你给格式化掉。

强制DOM状态

有些HTML的DOM是有状态的,比如<a> 标签,其会有 active,hover, focus,visited这些状态,有时候,我们的CSS会来定关不同状态的样式,在分析网页查看网页上DOM的CSS样式时,我们可以点击CSS样式上的 :hov 这个小按钮来强制这个DOM的状态。

 

 

动画

现在的网页上都会有一些动画效果。在Chrome的开发者工具中,通过右上角的菜单中的 More Tools => Animations 呼出相关的选项卡。于是你就可以慢动作播放动画了(可以点选 25%10%),然后,Chrome还可以帮你把动画录下来,你可以拉动动再画的过程,甚至可以做一些简单的修改。

 

直接编辑网页

在你的 console 里 输入下面的命令:

document.designMode = "on"

于是你就可以直接修改网页上的内容了。

P.S. 下面这个抓屏中还演示了一个如何清空console的示例。你可以输入 clear() 或是 按 Ctrl+L(Windows下),CMD + K (Mac下)

 

网络限速

你可以设置你的网络的访问速度来模拟一个网络很慢的情况。

 

复制HTTP请求

这个是我很喜欢 的一个功能,你可以在 network选项卡里,点击 XHR 过滤相关的Ajax请求,然后在相关的请求上点鼠标右键,在菜单中选择: Copy => Copy as cURL,然后就可以到你的命令行下去 执行 curl 的命令了。这个可以很容易做一些自动化的测试。

 

友情提示:这个操作有可能会把你的个人隐私信息复制出去,比如你个人登录后的cookie。

抓个带手机的图

这个可能有点无聊了,不过我觉得挺有意思的。

在device显示中,先选择一个手机,然后在右上角选 Show Device Frame,然后你就看到手机的样子了,然后再到那个菜中中选 Capture snapshot,就可以抓下一个有手机样子的截图了。

我抓的图如下(当然,不是所有的手机都有frame的)

 

设置断点

除了给Javascript的源代码上设置断点调试,你还可以:

给DOM设置断点

选中一个DOM,然后在右键菜单中选 Break on … 你可以看到如下三个选项:

给XHR和Event Lisener设置断点

在 Sources 面页中,你可以看到右边的那堆break points中,除了上面我们说的给DOM设置断点,你还可以给XHR和Event Listener设置断点,载图如下:

关于Console中的技巧

DOM操作
  • chrome会帮你buffer 5个你查看过的DOM对象,你可以直接在Console中用 $0, $1, $2, $3, $4来访问。
  • 你还可以使用像jQuery那样的语法来获得DOM对象,如:$("#mydiv")
  • 你还可使用 $$(".class") 来选择所有满足条件的DOM对象。
  • 你可以使用 getEventListeners($("selector")) 来查看某个DOM对象上的事件(如下图所示)。

  • 你还可以使用 monitorEvents($("selector")) 来监控相关的事件。比如:

monitorEvents(document.body, "click");

Console中的一些函数

1)monitor函数

使用 monitor函数来监控一函数,如下面的示例

2)copy函数

copy函数可以把一个变量的值copy到剪贴板上。

3)inspect函数

inspect函数可以让你控制台跳到你需要查看的对象上。如:

更多的函数请参数官方文档 – Using the Console / Command Line Reference

Console的输出

我们知道,除了console.log之外,还有console.debugconsole.infoconsole.warnconsole.error这些不同级别的输出。另外一个鲜为人知的功能是,console.log中,你还可以对输出的文本加上css的样式,如下所示:

console.log("%c左耳朵", "font-size:90px;color:#888")

于是,你可以定义一些相关的log函数,如:

console.todo = function( msg){
  console.log( '%c%s %s %s', 'font-size:20px; color:yellow; background-color: blue;', '--', msg, '--');
}
console.important = function( msg){
  console.log( '%c%s %s %s', 'font-size:20px; color:brown; font-weight: bold; text-decoration: underline;', '--', msg, '--');
}

关于console.log中的格式化,你可以参看如下表格:

指示符 输出
%s 格式化输出一个字符串变量。
%i or %d 格式化输出一个整型变量的值。
%f 格式化输出一个浮点数变量的值。
%o 格式化输出一个DOM对象。
%O 格式化输出一个Javascript对象。
%c 为后面的字符串加上CSS样式

 

除了console.log打印js的数组,你还可以使用console.table来打印,如下所示:

var pets = [
  { animal: 'Horse', name: 'Pony', age: 23 },
  { animal: 'Dog', name: 'Snoopy', age: 13 },
  { animal: 'Cat', name: 'Tom', age: 18 },
  { animal: 'Mouse', name: 'Jerry', age: 12}
];
console.table(pets)

 

关于console对象

  • console对象除了上面的打日志的功能,其还有很多功能,比如:
  • console.trace() 可以打出js的函数调用栈
  • console.time() 和 console.timeEnd() 可以帮你计算一段代码间消耗的时间。
  • console.profile() 和 console.profileEnd() 可以让你查看CPU的消耗。
  • console.count() 可以让你看到相同的日志当前被打印的次数。
  • console.assert(expression, object) 可以让你assert一个表达式

这些东西都可以看看Google的Console API的文档

其实,还有很多东西,你可以参看Google的官方文档 – Chrome DevTools

关于快捷键

点击在 DevTools的右上角的那三个坚排的小点,你会看到一个菜单,点选 Shortcuts,你就可以看到所有的快捷键了

如果你知道更多,也欢迎补充!

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post Chrome开发者工具的小技巧 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/17634.html/feed 63
如何读懂并写出装逼的函数式代码 https://coolshell.cn/articles/17524.html https://coolshell.cn/articles/17524.html#comments Sun, 23 Oct 2016 09:56:29 +0000 http://coolshell.cn/?p=17524 今天在微博上看到了 有人分享了下面的这段函数式代码,我把代码贴到下面,不过我对原来的代码略有改动,对于函数式的版本,咋一看,的确令人非常费解,仔细看一下,你可能...

Read More Read More

The post 如何读懂并写出装逼的函数式代码 first appeared on 酷 壳 - CoolShell.]]>
drawing-recursive今天在微博上看到了 有人分享了下面的这段函数式代码,我把代码贴到下面,不过我对原来的代码略有改动,对于函数式的版本,咋一看,的确令人非常费解,仔细看一下,你可能就晕掉了,似乎完全就是天书,看上去非常装逼,哈哈。不过,我感觉解析那段函数式的代码可能会一个比较有趣过程,而且,我以前写过一篇《函数式编程》的入门式的文章,正好可以用这个例子,再升华一下原来的那篇文章,顺便可以向大家更好的介绍很多基础知识,所以写下这篇文章。

先看代码

这个代码平淡无奇,就是从一个数组中找到一个数,O(n)的算法,找不到就返回 null。

下面是正常的 old-school 的方式。不用多说。

//正常的版本
function find (x, y) {
  for ( let i = 0; i < x.length; i++ ) {
    if ( x[i] == y ) return i;
  }
  return null;
}

let arr = [0,1,2,3,4,5]
console.log(find(arr, 2))
console.log(find(arr, 8))

结果到了函数式成了下面这个样子(好像上面的那些代码在下面若影若现,不过又有点不太一样,为了消掉if语言,让其看上去更像一个表达式,动用了 ? 号表达式):

//函数式的版本
const find = ( f => f(f) ) ( f =>
  (next => (x, y, i = 0) =>
    ( i >= x.length) ?  null :
      ( x[i] == y ) ? i :
        next(x, y, i+1))((...args) =>
          (f(f))(...args)))

let arr = [0,1,2,3,4,5]
console.log(find(arr, 2))
console.log(find(arr, 8))

为了讲清这个代码,需要先补充一些知识。

Javascript的箭头函数

首先先简单说明一下,ECMAScript2015 引入的箭头表达式。箭头函数其实都是匿名函数,其基本语法如下:

(param1, param2, …, paramN) => { statements } 
(param1, param2, …, paramN) => expression
     // 等于 :  => { return expression; } 

// 只有一个参数时,括号才可以不加:
(singleParam) => { statements }
singleParam => { statements }

//如果没有参数,就一定要加括号:
() => { statements }

下面是一些示例:

var simple = a => a > 15 ? 15 : a; 
simple(16); // 15
simple(10); // 10

let max = (a, b) => a > b ? a : b;

// Easy array filtering, mapping, ...

var arr = [5, 6, 13, 0, 1, 18, 23];
var sum = arr.reduce((a, b) => a + b);  // 66
var even = arr.filter(v => v % 2 == 0); // [6, 0, 18]
var double = arr.map(v => v * 2);       // [10, 12, 26, 0, 2, 36, 46]

看上去不复杂吧。不过,上面前两个 simple 和 max 的例子都把这箭头函数赋值给了一个变量,于是它就有了一个名字。有时候,某些函数在声明的时候就是调用的时候,尤其是函数式编程中,一个函数还对外返回函数的时候。比如下在这个例子:

function MakePowerFn(power) {
  return function PowerFn(base) {
    return Math.pow(base, power);
  } 
}

power3 = MakePowerFn(3); //制造一个X的3次方的函数
power2 = MakePowerFn(2); //制造一个X的2次方的函数

console.log(power3(10)); //10的3次方 = 1000
console.log(power2(10)); //10的2次方 = 100

其实,在 MakePowerFn 函数里的那个 PowerFn 根本不需要命名,完全可以写成:

function MakePowerFn(power) {
  return function(base) {
    return Math.pow(base, power);
  } 
}

如果用箭头函数,可以写成:

MakePowerFn = power  => {
  return base => {
    return Math.pow(base, power);
  } 
}

我们还可以写得更简洁(如果用表达式的话,就不需要 { 和 }, 以及 return 语句 ):

MakePowerFn = power => base => Math.pow(base, power)

我还是加上括号,和换行可能会更清楚一些:

MakePowerFn = (power) => (
  (base) => (Math.pow(base, power))
)

好了,有了上面的知识,我们就可以进入一个更高级的话题——匿名函数的递归。

匿名函数的递归

函数式编程立志于用函数表达式消除有状态的函数,以及for/while循环,所以,在函数式编程的世界里是不应该用for/while循环的,而要改用递归(递归的性能很差,所以,一般是用尾递归来做优化,也就是把函数的计算的状态当成参数一层一层的往下传递,这样语言的编译器或解释器就不需要用函数栈来帮你保存函数的内部变量的状态了)。

好了,那么,匿名函数的递归该怎么做?

一般来说,递归的代码就是函数自己调用自己,比如我们求阶乘的代码:

function fact(n){
  return n==0 ? 1 :  n * fact(n-1);
};
result = fact(5);

在匿名函数下,这个递归该怎么写呢?对于匿名函数来说,我们可以把匿名函数当成一个参数传给另外一个函数,因为函数的参数有名字,所以就可以调用自己了。 如下所示:

function combinator(func) {
  func(func);
}

这个是不是有点作弊的嫌疑?Anyway,我们再往下,把上面这个函数整成箭头函数式的匿名函数的样子。

(func) => (func(func))

现在你似乎就不像作弊了吧。把上面那个求阶乘的函数套进来是这个样子:

首先,先重构一下fact,把fact中自己调用自己的名字去掉:

function fact(func, n) {
  return n==0 ? 1 :  n * func(func, n-1);
}

fact(fact, 5); //输出120

然后,我们再把上面这个版本变成箭头函数的匿名函数版:

var fact = (func, n) => ( n==0 ? 1 :  n * func(func, n-1) )
fact(fact, 5)

这里,我们依然还要用一个fact来保存这个匿名函数,我们继续,我们要让匿名函数声明的时候,就自己调用自己。

也就是说,我们要把

(func, n) => ( n==0 ? 1 : n * func(func, n-1) )

这个函数当成调用参数,传给下面这个函数:

(func, x) => func(func, x)

最终我们得到下面的代码:

 
( (func, x) => func(func, x) ) (  //函数体
  (func, n) => ( n==0 ? 1 :  n * func(func, n-1) ), //第一个调用参数
  5 //第二调用参数
); 

好像有点绕,anyway, 你看懂了吗?没事,我们继续。

动用高阶函数的递归

但是上面这个递归的匿名函数在自己调用自己,所以,代码中有hard code的实参。我们想实参去掉,如何去掉呢?我们可以参考前面说过的那个 MakePowerFn 的例子,不过这回是递归版的高阶函数了。

HighOrderFact = function(func){
  return function(n){
    return n==0 ? 1 : n * func(func)(n-1);
  };
};

我们可以看,上面的代码简单说来就是,需要一个函数做参数,然后返回这个函数的递归版本。那么,我们怎么调用呢?

fact = HighOrderFact(HighOrderFact);
fact(5); 

连起来写就是:
HighOrderFact ( HighOrderFact ) ( 5 )

但是,这样让用户来调用很不爽,所以,以我们一个函数把 HighOrderFact ( HighOrderFact ) 给代理一下:

fact = function ( hifunc ) {
  return hifunc ( hifunc );
} (
  //调用参数是一个函数
  function (func) { 
    return function(n){
      return n==0 ? 1 : n * func(func)(n-1);
    };
  }
);

fact(5); //于是我们就可以直接使用了

用箭头函数重构一下,是不是简洁了一些?

fact = (highfunc => highfunc ( highfunc ) ) (
  func => n =>  n==0 ? 1 : n * func(func)(n-1)
);

上面就是我们最终版的阶乘的函数式代码。

回顾之前的程序

我们再来看那个查找数组的正常程序:

//正常的版本
function find (x, y) {
  for ( let i = 0; i < x.length; i++ ) {
    if ( x[i] == y ) return i;
  }
  return null;
}

先把for干掉,搞成递归版本:

function find (x, y, i=0) {
  if ( i >= x.length ) return null;
  if ( x[i] == y ) return i;
  return find(x, y, i+1);
}

然后,写出带实参的匿名函数的版本(注:其中的if代码被重构成了 ?号表达式):

( (func, x, y, i) => func(func, x, y, i) ) (  //函数体
  (func, x, y, i=0) => (
      i >= x.length ?  null :
         x[i] == y  ?  i : func (func, x, y, i+1)
  ), //第一个调用参数
  arr, //第二调用参数
  2 //第三调用参数
)

最后,引入高阶函数,去除实参:

const find = ( highfunc => highfunc( highfunc ) ) (
   func => (x, y, i = 0) => (
     i >= x.length ?  null :
           x[i] == y  ?  i : func (func) (x, y, i+1)
   )
);

注:函数式编程装逼时一定要用const字符,这表示我写的函数里的状态是 immutable 的,天生骄傲!

再注:我写的这个比原来版的那个简单了很多,原来版本的那个又在函数中套了一套 next, 而且还动用了不定参数,当然,如果你想装逼装到天上的,理论上来说,你可以套N层,呵呵。

现在,你可以体会到,如此逼装的是怎么来的了吧?

其它

你还别说这就是装逼,简单来说,我们可以使用数学的方式来完成对复杂问题的描述,那怕是递归。其实,这并不是新鲜的东西,这是Alonzo Church 和 Haskell Curry 上世纪30年代提出来的东西,这个就是 Y Combinator 的玩法,关于这个东西,你可以看看下面两篇文章:

The Y Combinator (Slight Return)》,

Wikipedia: Fixed-point combinator

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 如何读懂并写出装逼的函数式代码 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/17524.html/feed 66
函数式编程 https://coolshell.cn/articles/10822.html https://coolshell.cn/articles/10822.html#comments Fri, 27 Dec 2013 00:11:03 +0000 http://coolshell.cn/?p=10822 当我们说起函数式编程来说,我们会看到如下函数式编程的长相: 函数式编程的三大特性: immutable data 不可变数据:像Clojure一样,默认上变量是...

Read More Read More

The post 函数式编程 first appeared on 酷 壳 - CoolShell.]]>
当我们说起函数式编程来说,我们会看到如下函数式编程的长相:

  • 函数式编程的三大特性:
    • immutable data 不可变数据:像Clojure一样,默认上变量是不可变的,如果你要改变变量,你需要把变量copy出去修改。这样一来,可以让你的程序少很多Bug。因为,程序中的状态不好维护,在并发的时候更不好维护。(你可以试想一下如果你的程序有个复杂的状态,当以后别人改你代码的时候,是很容易出bug的,在并行中这样的问题就更多了)
    • first class functions:这个技术可以让你的函数就像变量一样来使用。也就是说,你的函数可以像变量一样被创建,修改,并当成变量一样传递,返回或是在函数中嵌套函数。这个有点像Javascript的Prototype(参看Javascript的面向对象编程
    • 尾递归优化:我们知道递归的害处,那就是如果递归很深的话,stack受不了,并会导致性能大幅度下降。所以,我们使用尾递归优化技术——每次递归时都会重用stack,这样一来能够提升性能,当然,这需要语言或编译器的支持。Python就不支持。
  • 函数式编程的几个技术
    • map & reduce :这个技术不用多说了,函数式编程最常见的技术就是对一个集合做Map和Reduce操作。这比起过程式的语言来说,在代码上要更容易阅读。(传统过程式的语言需要使用for/while循环,然后在各种变量中把数据倒过来倒过去的)这个很像C++中的STL中的foreach,find_if,count_if之流的函数的玩法。
    • pipeline:这个技术的意思是,把函数实例成一个一个的action,然后,把一组action放到一个数组或是列表中,然后把数据传给这个action list,数据就像一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。
    • recursing 递归 :递归最大的好处就简化代码,他可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。
    • currying:把一个函数的多个参数分解成多个函数, 然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,可以简化函数的多个参数。在C++中,这个很像STL中的bind_1st或是bind2nd。
    • higher order function 高阶函数:所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。

  • 还有函数式的一些好处
    • parallelization 并行:所谓并行的意思就是在并行环境下,各个线程之间不需要同步或互斥。
    • lazy evaluation 惰性求值:这个需要编译器的支持。表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值,也就是说,语句如x:=expression; (把一个表达式的结果赋值给一个变量)明显的调用这个表达式被计算并把结果放置到 x 中,但是先不管实际在 x 中的是什么,直到通过后面的表达式中到 x 的引用而有了对它的值的需求的时候,而后面表达式自身的求值也可以被延迟,最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树。
    • determinism 确定性:所谓确定性的意思就是像数学那样 f(x) = y ,这个函数无论在什么场景下,都会得到同样的结果,这个我们称之为函数的确定性。而不是像程序中的很多函数那样,同一个参数,却会在不同的场景下计算出不同的结果。所谓不同的场景的意思就是我们的函数会根据一些运行中的状态信息的不同而发生变化。

上面的那些东西太抽象了,还是让我们来循序渐近地看一些例子吧。

我们先用一个最简单的例子来说明一下什么是函数式编程。

先看一个非函数式的例子:

int cnt;
void increment(){
    cnt++;
}

 

那么,函数式的应该怎么写呢?

int increment(int cnt){
    return cnt+1;
}

你可能会觉得这个例子太普通了。是的,这个例子就是函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你

我们再来看一个简单例子:

def inc(x):
    def incx(y):
        return x+y
    return incx

inc2 = inc(2)
inc5 = inc(5)

print inc2(5) # 输出 7
print inc5(5) # 输出 10

我们可以看到上面那个例子inc()函数返回了另一个函数incx(),于是我们可以用inc()函数来构造各种版本的inc函数,比如:inc2()和inc5()。这个技术其实就是上面所说的Currying技术。从这个技术上,你可能体会到函数式编程的理念:把函数当成变量来用,关注于描述问题而不是怎么实现,这样可以让代码更易读。

Map & Reduce

在函数式编程中,我们不应该用循环迭代的方式,我们应该用更为高级的方法,如下所示的Python代码

name_len = map(len, ["hao", "chen", "coolshell"])
print name_len
# 输出 [3, 4, 9]

你可以看到这样的代码很易读,因为,这样的代码是在描述要干什么,而不是怎么干

我们再来看一个Python代码的例子:

def toUpper(item):
return item.upper()

upper_name = map(toUpper, ["hao", "chen", "coolshell"])
print upper_name
# 输出 ['HAO', 'CHEN', 'COOLSHELL']

顺便说一下,上面的例子个是不是和我们的STL的transform有些像?

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;

int main() {
    string s="hello";
    string out;
    transform(s.begin(), s.end(), back_inserter(out), ::toupper);
    cout << out << endl;
    // 输出:HELLO
}

在上面Python的那个例子中我们可以看到,我们写义了一个函数toUpper,这个函数没有改变传进来的值,只是把传进来的值做个简单的操作,然后返回。然后,我们把其用在map函数中,就可以很清楚地描述出我们想要干什么。而不会去理解一个在循环中的怎么实现的代码,最终在读了很多循环的逻辑后才发现原来是这个或那个意思。 下面,我们看看描述实现方法的过程式编程是怎么玩的(看上去是不是不如函数式的清晰?):

upname =['HAO', 'CHEN', 'COOLSHELL']
lowname =[]
for i in range(len(upname)):
lowname.append( upname[i].lower() )

对于map我们别忘了lambda表达式:你可以简单地理解为这是一个inline的匿名函数。下面的lambda表达式相当于:def func(x): return x*x

squares = map(lambda x: x * x, range(9))
print squares
# 输出 [0, 1, 4, 9, 16, 25, 36, 49, 64]

我们再来看看reduce怎么玩?(下面的lambda表达式中有两个参数,也就是说每次从列表中取两个值,计算结果后把这个值再放回去,下面的表达式相当于:((((1+2)+3)+4)+5) )

print reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
# 输出 15

Python中的除了map和reduce外,还有一些别的如filter, find, all, any的函数做辅助(其它函数式的语言也有),可以让你的代码更简洁,更易读。 我们再来看一个比较复杂的例子:

# 计算数组中正数的平均值",
num =[2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8]
positive_num_cnt = 0
positive_num_sum = 0
for i in range(len(num)):
    if num[i] > 0:
        positive_num_cnt += 1
        positive_num_sum += num[i]

if positive_num_cnt > 0:
    average = positive_num_sum / positive_num_cnt

print average
# 输出 5

如果用函数式编程,这个例子可以写成这样:

positive_num = filter(lambda x: x>0, num)
average = reduce(lambda x,y: x+y, positive_num) / len( positive_num )

C++11玩的法:

#include <iostream>
#include <algorithm>
#include <numeric>
#include <string>
#include <vector>
using namespace std;

vector num {2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8};
vector p_num;
copy_if(num.begin(), num.end(), back_inserter(p_num), [](int i){ return (i>0);} );
int average = accumulate(p_num.begin(), p_num.end(), 0) / p_num.size();
cout << "averge: " << average << endl;

我们可以看到,函数式编程有如下好处:

1)代码更简单了。
2)数据集,操作,返回值都放到了一起。
3)你在读代码的时候,没有了循环体,于是就可以少了些临时变量,以及变量倒来倒去逻辑。
4)你的代码变成了在描述你要干什么,而不是怎么去干。

最后,我们来看一下Map/Reduce这样的函数是怎么来实现的(下面是Javascript代码)

//map函数
var map = function (mappingFunction, list) {
    var result = [];
    forEach(list, function (item) {
        result.push(mappingFunction(item));
    });
    return result;
};

下面是reduce函数的javascript实现(谢谢 @下雨在家 修正的我原来的简单版本)

//reduce函数
function reduce(actionFunction, list, initial){
    var accumulate;
    var temp;
    if(initial){
        accumulate = initial;
    }else{
        accumulate = list.shfit();
    }
    temp = list.shift();
    while(temp){
        accumulate = actionFunction(accumulate,temp);
        temp = list.shift();
    }
    return accumulate;
};

Declarative Programming vs Imperative Programming

前面提到过多次的函数式编程关注的是:describe what to do, rather than how to do it. 于是,我们把以前的过程式的编程范式叫做 Imperative Programming – 指令式编程,而把函数式的这种范式叫做 Declarative Programming – 声明式编程。

下面我们看一下相关的示例(本示例来自这篇文章 )。

比如,我们有3辆车比赛,简单起见,我们分别给这3辆车有70%的概率可以往前走一步,一共有5次机会,我们打出每一次这3辆车的前行状态。

对于Imperative Programming来说,代码如下(Python):

from random import random

time = 5
car_positions = [1, 1, 1]

while time:
    # decrease time
    time -= 1

    print ''
    for i in range(len(car_positions)):
        # move car
        if random() > 0.3:
            car_positions[i] += 1

        # draw car
        print '-' * car_positions[i]

我们可以把这个两重循环变成一些函数模块,这样有利于我们更容易地阅读代码:

from random import random
def move_cars():
    for i, _ in enumerate(car_positions):
        if random() > 0.3:
            car_positions[i] += 1
def draw_car(car_position):
    print '-' * car_position
def run_step_of_race():
    global time
    time -= 1
    move_cars()
def draw():
    print ''
    for car_position in car_positions:
        draw_car(car_position)
time = 5
car_positions = [1, 1, 1]
while time:
    run_step_of_race()
    draw()

上面的代码,我们可以从主循环开始,我们可以很清楚地看到程序的主干,因为我们把程序的逻辑分成了几个函数,这样一来,我们的代码逻辑也会变得几个小碎片,于是我们读代码时要考虑的上下文就少了很多,阅读代码也会更容易。不像第一个示例,如果没有注释和说明,你还是需要花些时间理解一下。而把代码逻辑封装成了函数后,我们就相当于给每个相对独立的程序逻辑取了个名字,于是代码成了自解释的

但是,你会发现,封装成函数后,这些函数都会依赖于共享的变量来同步其状态。于是,我们在读代码的过程时,每当我们进入到函数里,一量读到访问了一个外部的变量,我们马上要去查看这个变量的上下文,然后还要在大脑里推演这个变量的状态, 我们才知道程序的真正逻辑。也就是说,这些函数间必需知道其它函数是怎么修改它们之间的共享变量的,所以,这些函数是有状态的

我们知道,有状态并不是一件很好的事情,无论是对代码重用,还是对代码的并行来说,都是有副作用的。因此,我们要想个方法把这些状态搞掉,于是出现了我们的 Functional Programming 的编程范式。下面,我们来看看函数式的方式应该怎么写?

from random import random

def move_cars(car_positions):
    return map(lambda x: x + 1 if random() > 0.3 else x,
               car_positions)

def output_car(car_position):
    return '-' * car_position

def run_step_of_race(state):
    return {'time': state['time'] - 1,
            'car_positions': move_cars(state['car_positions'])}

def draw(state):
    print ''
    print '\n'.join(map(output_car, state['car_positions']))

def race(state):
    draw(state)
    if state['time']:
        race(run_step_of_race(state))

race({'time': 5,
      'car_positions': [1, 1, 1]})

上面的代码依然把程序的逻辑分成了函数,不过这些函数都是functional的。因为它们有三个症状:

1)它们之间没有共享的变量。
2)函数间通过参数和返回值来传递数据。
3)在函数里没有临时变量。

我们还可以看到,for循环被递归取代了(见race函数)—— 递归是函数式编程中带用到的技术,正如前面所说的,递归的本质就是描述问题是什么。

Pipeline

pipeline 管道借鉴于Unix Shell的管道操作——把若干个命令串起来,前面命令的输出成为后面命令的输入,如此完成一个流式计算。(注:管道绝对是一个伟大的发明,他的设哲学就是KISS – 让每个功能就做一件事,并把这件事做到极致,软件或程序的拼装会变得更为简单和直观。这个设计理念影响非常深远,包括今天的Web Service,云计算,以及大数据的流式计算等等)

比如,我们如下的shell命令:

ps auwwx | awk '{print $2}' | sort -n | xargs echo

如果我们抽象成函数式的语言,就像下面这样:

xargs( echo, sort(n, awk('print $2', ps(auwwx))) )

也可以类似下面这个样子:

pids = for_each(result, [ps_auwwx, awk_p2, sort_n, xargs_echo])

好了,让我们来看看函数式编程的Pipeline怎么玩?

我们先来看一个如下的程序,这个程序的process()有三个步骤:

1)找出偶数。
2)乘以3
3)转成字符串返回

def process(num):
    # filter out non-evens
    if num % 2 != 0:
        return
    num = num * 3
    num = 'The Number: %s' % num
    return num

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

for num in nums:
    print process(num)

# 输出:
# None
# The Number: 6
# None
# The Number: 12
# None
# The Number: 18
# None
# The Number: 24
# None
# The Number: 30

我们可以看到,输出的并不够完美,另外,代码阅读上如果没有注释,你也会比较晕。下面,我们来看看函数式的pipeline(第一种方式)应该怎么写?

def even_filter(nums):
    for num in nums:
        if num % 2 == 0:
            yield num
def multiply_by_three(nums):
    for num in nums:
        yield num * 3
def convert_to_string(nums):
    for num in nums:
        yield 'The Number: %s' % num

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pipeline = convert_to_string(multiply_by_three(even_filter(nums)))
for num in pipeline:
    print num
# 输出:
# The Number: 6
# The Number: 12
# The Number: 18
# The Number: 24
# The Number: 30

我们动用了Python的关键字 yield,这个关键字主要是返回一个Generator,yield 是一个类似 return 的关键字,只是这个函数返回的是个Generator-生成器。所谓生成器的意思是,yield返回的是一个可迭代的对象,并没有真正的执行函数。也就是说,只有其返回的迭代对象被真正迭代时,yield函数才会正真的运行,运行到yield语句时就会停住,然后等下一次的迭代。(这个是个比较诡异的关键字)这就是lazy evluation。

好了,根据前面的原则——“使用Map & Reduce,不要使用循环”,那我们用比较纯朴的Map & Reduce吧。

def even_filter(nums):
    return filter(lambda x: x%2==0, nums)

def multiply_by_three(nums):
    return map(lambda x: x*3, nums)

def convert_to_string(nums):
    return map(lambda x: 'The Number: %s' % x,  nums)

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pipeline = convert_to_string(
               multiply_by_three(
                   even_filter(nums)
               )
            )
for num in pipeline:
    print num

但是他们的代码需要嵌套使用函数,这个有点不爽,如果我们能像下面这个样子就好了(第二种方式)。

pipeline_func(nums, [even_filter,
                     multiply_by_three,
                     convert_to_string])

那么,pipeline_func 实现如下:

def pipeline_func(data, fns):
    return reduce(lambda a, x: x(a),
                  fns,
                  data)

好了,在读过这么多的程序后,你可以回头看一下这篇文章的开头对函数式编程的描述,可能你就更有感觉了。

最后,我希望这篇浅显易懂的文章能让你感受到函数式编程的思想,就像OO编程,泛型编程,过程式编程一样,我们不用太纠结是不是我们的程序就是OO,就是functional的,我们重要的品味其中的味道

参考

补充:评论中redraiment这个评论大家也可以读一读。

感谢谢网友S142857 提供的shell风格的python pipeline:

class Pipe(object):
    def __init__(self, func):
        self.func = func

    def __ror__(self, other):
        def generator():
            for obj in other:
                if obj is not None:
                    yield self.func(obj)
        return generator()

@Pipe
def even_filter(num):
    return num if num % 2 == 0 else None

@Pipe
def multiply_by_three(num):
    return num*3

@Pipe
def convert_to_string(num):
    return 'The Number: %s' % num

@Pipe
def echo(item):
    print item
    return item

def force(sqs):
    for item in sqs: pass

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

force(nums | even_filter | multiply_by_three | convert_to_string | echo)

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 函数式编程 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/10822.html/feed 197
Lua简明教程 https://coolshell.cn/articles/10739.html https://coolshell.cn/articles/10739.html#comments Tue, 03 Dec 2013 00:29:06 +0000 http://coolshell.cn/?p=10739 这几天系统地学习了一下Lua这个脚本语言,Lua脚本是一个很轻量级的脚本,也是号称性能最高的脚本,用在很多需要性能的地方,比如:游戏脚本,nginx,wires...

Read More Read More

The post Lua简明教程 first appeared on 酷 壳 - CoolShell.]]>
The Programming Language Lua这几天系统地学习了一下Lua这个脚本语言,Lua脚本是一个很轻量级的脚本,也是号称性能最高的脚本,用在很多需要性能的地方,比如:游戏脚本,nginx,wireshark的脚本,当你把他的源码下下来编译后,你会发现解释器居然不到200k,这是多么地变态啊(/bin/sh都要1M,MacOS平台),而且能和C语言非常好的互动。我很好奇得浏览了一下Lua解释器的源码,这可能是我看过最干净的C的源码了。

我不想写一篇大而全的语言手册,一方面是因为已经有了(见本文后面的链接),重要的原因是,因为大篇幅的文章会挫败人的学习热情,我始终觉得好的文章读起来就像拉大便一样,能一口气很流畅地搞完,才会让人爽(这也是我为什么不想写书的原因)。所以,这必然又是一篇“入厕文章”,还是那句话,我希望本文能够让大家利用上下班,上厕所大便的时间学习一个技术。呵呵。

相信你现在已经在厕所里脱掉裤子露出屁股已经准备好大便了,那就让我们畅快地排泄吧……

运行

首先,我们需要知道,Lua是类C的,所以,他是大小写字符敏感的。

下面是Lua的Hello World。注意:Lua脚本的语句的分号是可选的,这个和GO语言很类似

print("Hello World")

你可以像python一样,在命令行上运行lua命令后进入lua的shell中执行语句。

chenhao-air:lua chenhao$ lua
Lua 5.2.2  Copyright (C) 1994-2013 Lua.org, PUC-Rio
> print("Hello, World")
Hello, World
> 

也可以把脚本存成一个文件,用如下命令行来运行。

>lua file.lua

或是像shell一样运行:

chenhao-air:lua chenhao$ cat hello.lua
#!/usr/local/bin/lua
print("Hello, World")
chenhao-air:lua chenhao$ chmod +x hello.lua
chenhao-air:test chenhao$ ./hello.lua
Hello, World

语法

注释

-- 两个减号是行注释

 

--[[
 这是块注释
 这是块注释
 --]]
变量

Lua的数字只有double型,64bits,你不必担心Lua处理浮点数会慢(除非大于100,000,000,000,000),或是会有精度问题。

你可以以如下的方式表示数字,0x开头的16进制和C是很像的。

num = 1024
num = 3.0
num = 3.1416
num = 314.16e-2
num = 0.31416E1
num = 0xff
num = 0x56

字符串你可以用单引号,也可以用双引号,还支持C类型的转义,比如: ‘\a’ (响铃), ‘\b’ (退格), ‘\f’ (表单), ‘\n’ (换行), ‘\r’ (回车), ‘\t’ (横向制表), ‘\v’ (纵向制表), ‘\\’ (反斜杠), ‘\”‘ (双引号), 以及 ‘\” (单引号)

下面的四种方式定义了完全相同的字符串(其中的两个中括号可以用于定义有换行的字符串)

a = 'alo\n123"'
a = "alo\n123\""
a = '\97lo\10\04923"'
a = [[alo
123"]]

C语言中的NULL在Lua中是nil,比如你访问一个没有声明过的变量,就是nil,比如下面的v的值就是nil

v = UndefinedVariable

布尔类型只有nil和false是 false,数字0啊,‘’空字符串(’\0’)都是true!

另外,需要注意的是:lua中的变量如果没有特殊说明,全是全局变量,那怕是语句块或是函数里。变量前加local关键字的是局部变量。

theGlobalVar = 50
local theLocalVar = "local variable"

控制语句

不多说了,直接看代码吧(注意:Lua没有++或是+=这样的操作)

while循环
sum = 0
num = 1
while num <= 100 do
    sum = sum + num
    num = num + 1
end
print("sum =",sum)
if-else分支
if age == 40 and sex =="Male" then
    print("男人四十一枝花")
elseif age > 60 and sex ~="Female" then
    print("old man without country!")
elseif age < 20 then
    io.write("too young, too naive!\n")
else
    local age = io.read()
    print("Your age is "..age)
end

上面的语句不但展示了if-else语句,也展示了
1)“~=”是不等于,而不是!=
2)io库的分别从stdin和stdout读写的read和write函数
3)字符串的拼接操作符“..”

另外,条件表达式中的与或非为分是:and, or, not关键字。

for 循环
sum = 0
for i = 1, 100 do
    sum = sum + i
end

 

sum = 0
for i = 1, 100, 2 do
    sum = sum + i
end

 

sum = 0
for i = 100, 1, -2 do
    sum = sum + i
end
until循环
sum = 2
repeat
   sum = sum ^ 2 --幂操作
   print(sum)
until sum >1000

函数

Lua的函数和Javascript的很像

递归
function fib(n)
  if n < 2 then return 1 end
  return fib(n - 2) + fib(n - 1)
end
闭包

同样,Javascript附体!

function newCounter()
    local i = 0
    return function()     -- anonymous function
       i = i + 1
        return i
    end
end

c1 = newCounter()
print(c1())  --> 1
print(c1())  --> 2

 

function myPower(x)
    return function(y) return y^x end
end

power2 = myPower(2)
power3 = myPower(3)

print(power2(4)) --4的2次方
print(power3(5)) --5的3次方
函数的返回值

Go语言一样,可以一条语句上赋多个值,如:

name, age, bGay = "haoel", 37, false, "[email protected]"

上面的代码中,因为只有3个变量,所以第四个值被丢弃。

函数也可以返回多个值:

function getUserInfo(id)
    print(id)
    return "haoel", 37, "[email protected]", "https://coolshell.cn"
end

name, age, email, website, bGay = getUserInfo()

注意:上面的示例中,因为没有传id,所以函数中的id输出为nil,因为没有返回bGay,所以bGay也是nil。

局部函数

函数前面加上local就是局部函数,其实,Lua中的函数和Javascript中的一个德行。

比如:下面的两个函数是一样的:

function foo(x) return x^2 end
foo = function(x) return x^2 end

Table

所谓Table其实就是一个Key Value的数据结构,它很像Javascript中的Object,或是PHP中的数组,在别的语言里叫Dict或Map,Table长成这个样子:

haoel = {name="ChenHao", age=37, handsome=True}

下面是table的CRUD操作:

haoel.website="https://coolshell.cn/"
local age = haoel.age
haoel.handsome = false
haoel.name=nil

上面看上去像C/C++中的结构体,但是name,age, handsome, website都是key。你还可以像下面这样写义Table:

t = {[20]=100, ['name']="ChenHao", [3.14]="PI"}

这样就更像Key Value了。于是你可以这样访问:t[20],t[“name”], t[3.14]。

我们再来看看数组:

arr = {10,20,30,40,50}

这样看上去就像数组了。但其实其等价于:

arr = {[1]=10, [2]=20, [3]=30, [4]=40, [5]=50}

所以,你也可以定义成不同的类型的数组,比如:

arr = {"string", 100, "haoel", function() print("coolshell.cn") end}

注:其中的函数可以这样调用:arr[4]()。

我们可以看到Lua的下标不是从0开始的,是从1开始的。

for i=1, #arr do
    print(arr[i])
end

注:上面的程序中:#arr的意思就是arr的长度。

注:前面说过,Lua中的变量,如果没有local关键字,全都是全局变量,Lua也是用Table来管理全局变量的,Lua把这些全局变量放在了一个叫“_G”的Table里。

我们可以用如下的方式来访问一个全局变量(假设我们这个全局变量名叫globalVar):

_G.globalVar
_G["globalVar"]

我们可以通过下面的方式来遍历一个Table。

for k, v in pairs(t) do
    print(k, v)
end

MetaTable 和 MetaMethod

MetaTable和MetaMethod是Lua中的重要的语法,MetaTable主要是用来做一些类似于C++重载操作符式的功能。

比如,我们有两个分数:

fraction_a = {numerator=2, denominator=3}
fraction_b = {numerator=4, denominator=7}

我们想实现分数间的相加:2/3 + 4/7,我们如果要执行: fraction_a + fraction_b,会报错的。

所以,我们可以动用MetaTable,如下所示:

fraction_op={}
function fraction_op.__add(f1, f2)
    ret = {}
    ret.numerator = f1.numerator * f2.denominator + f2.numerator * f1.denominator
    ret.denominator = f1.denominator * f2.denominator
    return ret
end

为之前定义的两个table设置MetaTable:(其中的setmetatble是库函数)

setmetatable(fraction_a, fraction_op)
setmetatable(fraction_b, fraction_op)

于是你就可以这样干了:(调用的是fraction_op.__add()函数)

fraction_s = fraction_a + fraction_b

至于__add这是MetaMethod,这是Lua内建约定的,其它的还有如下的MetaMethod:

__add(a, b)                     对应表达式 a + b
__sub(a, b)                     对应表达式 a - b
__mul(a, b)                     对应表达式 a * b
__div(a, b)                     对应表达式 a / b
__mod(a, b)                     对应表达式 a % b
__pow(a, b)                     对应表达式 a ^ b
__unm(a)                        对应表达式 -a
__concat(a, b)                  对应表达式 a .. b
__len(a)                        对应表达式 #a
__eq(a, b)                      对应表达式 a == b
__lt(a, b)                      对应表达式 a < b
__le(a, b)                      对应表达式 a <= b
__index(a, b)                   对应表达式 a.b
__newindex(a, b, c)             对应表达式 a.b = c
__call(a, ...)                  对应表达式 a(...)

“面向对象”

上面我们看到有__index这个重载,这个东西主要是重载了find key的操作。这操作可以让Lua变得有点面向对象的感觉,让其有点像Javascript的prototype。(关于Javascrip的面向对象,你可以参看我之前写的Javascript的面向对象

所谓__index,说得明确一点,如果我们有两个对象a和b,我们想让b作为a的prototype只需要:

setmetatable(a, {__index = b})

例如下面的示例:你可以用一个Window_Prototype的模板加上__index的MetaMethod来创建另一个实例:

Window_Prototype = {x=0, y=0, width=100, height=100}
MyWin = {title="Hello"}
setmetatable(MyWin, {__index = Window_Prototype})

于是:MyWin中就可以访问x, y, width, height的东东了。(注:当表要索引一个值时如table[key], Lua会首先在table本身中查找key的值, 如果没有并且这个table存在一个带有__index属性的Metatable, 则Lua会按照__index所定义的函数逻辑查找)

有了以上的基础,我们可以来说说所谓的Lua的面向对象。

Person={}

function Person:new(p)
    local obj = p
    if (obj == nil) then
        obj = {name="ChenHao", age=37, handsome=true}
    end
    self.__index = self
    return setmetatable(obj, self)
end

function Person:toString()
    return self.name .." : ".. self.age .." : ".. (self.handsome and "handsome" or "ugly")
end

上面我们可以看到有一个new方法和一个toString的方法。其中:

1)self 就是 Person,Person:new(p),相当于Person.new(self, p)
2)new方法的self.__index = self 的意图是怕self被扩展后改写,所以,让其保持原样
3)setmetatable这个函数返回的是第一个参数的值。

于是:我们可以这样调用:

me = Person:new()
print(me:toString())

kf = Person:new{name="King's fucking", age=70, handsome=false}
print(kf:toString())

继承如下,我就不多说了,Lua和Javascript很相似,都是在Prototype的实例上改过来改过去的。

Student = Person:new()

function Student:new()
    newObj = {year = 2013}
    self.__index = self
    return setmetatable(newObj, self)
end

function Student:toString()
    return "Student : ".. self.year.." : " .. self.name
end

模块

我们可以直接使用require(“model_name”)来载入别的lua文件,文件的后缀是.lua。载入的时候就直接执行那个文件了。比如:

我们有一个hello.lua的文件:

print("Hello, World!")

如果我们:require(“hello”),那么就直接输出Hello, World!了。

注意:
1)require函数,载入同样的lua文件时,只有第一次的时候会去执行,后面的相同的都不执行了。
2)如果你要让每一次文件都会执行的话,你可以使用dofile(“hello”)函数
3)如果你要玩载入后不执行,等你需要的时候执行时,你可以使用 loadfile()函数,如下所示:

local hello = loadfile("hello")
... ...
... ...
hello()

loadfile(“hello”)后,文件并不执行,我们把文件赋给一个变量hello,当hello()时,才真的执行。(我们多希望JavaScript也有这样的功能(参看《Javascript 装载和执行》))

当然,更为标准的玩法如下所示。

假设我们有一个文件叫mymod.lua,内容如下:

local HaosModel = {}

local function getname()
    return "Hao Chen"
end

function HaosModel.Greeting()
    print("Hello, My name is "..getname())
end

return HaosModel

于是我们可以这样使用:

local hao_model = require("mymod")
hao_model.Greeting()

其实,require干的事就如下:(所以你知道为什么我们的模块文件要写成那样了)

local hao_model = (function ()
  --mymod.lua文件的内容--
end)()

参考

我估计你差不多到擦屁股的时间了,所以,如果你还比较喜欢Lua的话,下面是几个在线文章你可以继续学习之:

关于Lua的标库,你可以看看官方文档:string,  table, math, io, os

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post Lua简明教程 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/10739.html/feed 125
数据即代码:元驱动编程 https://coolshell.cn/articles/10337.html https://coolshell.cn/articles/10337.html#comments Fri, 09 Aug 2013 02:18:31 +0000 http://coolshell.cn/?p=10337 (感谢 @文艺复兴记(todd) 投递此文) 几个小伙伴在考虑下面这个各个语言都会遇到的问题: 问题:设计一个命令行参数解析API 一个好的命令行参数解析库一般...

Read More Read More

The post 数据即代码:元驱动编程 first appeared on 酷 壳 - CoolShell.]]>
(感谢 @文艺复兴记(todd) 投递此文)

几个小伙伴在考虑下面这个各个语言都会遇到的问题:

问题:设计一个命令行参数解析API

一个好的命令行参数解析库一般涉及到这几个常见的方面:

1) 支持方便地生成帮助信息

2) 支持子命令,比如:git包含了push, pull, commit等多种子命令

3) 支持单字符选项、多字符选项、标志选项、参数选项等多种选项和位置参数

4) 支持选项默认值,比如:–port选项若未指定认为5037

5) 支持使用模式,比如:tar命令的-c和-x是互斥选项,属于不同的使用模式

经过一番考察,小伙伴们发现了这个几个有代表性的API设计:

1. getopt():

getopt()是libc的标准函数,很多语言中都能找到它的移植版本。

//C
while ((c = getopt(argc, argv, "ac:d:")) != -1) {
    int this_option_optind = optind ? optind : 1;
    switch (c) {
    case 'a':
        printf ("option a");
        aopt = 1;
        break;
    case 'c':
        printf ("option c with value '%s'", optarg);
        copt = optarg;
        break;
    case 'd':
        printf ("option d with value '%s'", optarg);
        dopt = optarg;
        break;
    case '?':
        break;
    default:
        printf ("?? getopt returned character code 0%o ??", c);
    }
}

getopt()的核心是一个类似printf的格式字符串的命令行参数描述串,如上面的”ac:d:”定义了”a”, “c”,”d”3个命令行参数,其中,a是一个标志符不需要参数,”c”和”d”需要跟参数。getopt()功能非常弱,只支持单个字符的标志选项和参数选项。如果按上面的5点来比对,基本上只能说是勉强支持第3点,其他几项只能靠程序自己来实现了,所以,想直接基于getopt()实现一个像git这样复杂的命令行参数是不可能的,只有自己来做很多的解析工作。小伙伴们看过getopt()之后一致的评价是:图样图森破。

2. Google gflags

接着,小伙伴们又发现了gflags这个Google出品C++命令行参数解析库。

//C++
DEFINE_bool(memory_pool, false, "If use memory pool");
DEFINE_bool(daemon, true, "If started as daemon");
DEFINE_string(module_id, "", "Server module id");
DEFINE_int32(http_port, 80, "HTTP listen port");
DEFINE_int32(https_port, 443, "HTTPS listen port");

int main(int argc, char** argv) {
    ::google::ParseCommandLineFlags(&argc, &argv, true);

    printf("Server module id: %s", FLAGS_module_id.c_str());

    if (FLAGS_daemon) {
      printf("Run as daemon: %d", FLAGS_daemon);
    }
    if (FLAGS_memory_pool) {
      printf("Use memory pool: %d", FLAGS_daemon);
    }

    Server server;

    return 0;
}

小伙伴们看了后不由得感叹“真心好用啊”!的确,gflags简单地通过几个宏就定义了命令行选项,基本上很好的支持了上面提到的1,3,4这几项,比起getopt()来强多了。对于类似cp这样的小命令,gflags应该是够用了,但要达到git这种级别就显得有些单薄了。

3. Ruby Commander

接下来小伙伴们又发现了Ruby Commander库:

//Ruby
# :name is optional, otherwise uses the basename of this executable
program :name, 'Foo Bar'
program :version, '1.0.0'
program :description, 'Stupid command that prints foo or bar.'
command :bar do |c|
  c.syntax = 'foobar bar [options]'
  c.description = 'Display bar with optional prefix and suffix'
  c.option '--prefix STRING', String, 'Adds a prefix to bar'
  c.option '--suffix STRING', String, 'Adds a suffix to bar'
  c.action do |args, options|
    options.default :prefix => '(', :suffix => ')'
    say "#{options.prefix}bar#{options.suffix}"
  end
end
$ foobar bar
# => (bar)
$ foobar bar --suffix '}' --prefix '{'
# => {bar}

Commander库利用Ruby酷炫的语法定义了一种描述命令行参数的内部DSL,看起来相当高端大气上档次。除了上面的第5项之外,其他几项都有很好的支持,可以说Commander库的设计基本达到了git这种级别命令行参数解析的要求。只是,要搞懂Ruby这么炫的语法和这个库的使用方法恐怕就不如getopt()和gflags容易了。有小伙伴当场表示想要学习Ruby,但是也有小伙伴表示再看看其他库再说。

4. Lisp cmdline库

接下来,小伙伴们发现了Lisp方言Racket的cmdline库

//Lisp
(parse-command-line "compile" (current-command-line-arguments)
  `((once-each
     [("-v" "--verbose")
      ,(lambda (flag) (verbose-mode #t))
      ("Compile with verbose messages")]
     [("-p" "--profile")
      ,(lambda (flag) (profiling-on #t))
      ("Compile with profiling")])
    (once-any
     [("-o" "--optimize-1")
      ,(lambda (flag) (optimize-level 1))
      ("Compile with optimization level 1")]
     [("--optimize-2")
      ,(lambda (flag) (optimize-level 2))
      (("Compile with optimization level 2,"
        "which implies all optimizations of level 1"))])
    (multi
     [("-l" "--link-flags")
      ,(lambda (flag lf) (link-flags (cons lf (link-flags))))
      ("Add a flag <lf> for the linker" "lf")]))
   (lambda (flag-accum file) file)
   '("filename"))

这是神马浮云啊?括号套括号,看起来很厉害的样子,但又不是很明白。看到这样的设计,有的小伙伴连评价都懒得评价了,但也有的小伙伴对Lisp越发崇拜,表示Lisp就是所谓的终极语言了,没有哪门语言能写出这么不明觉历的代码来!小伙伴们正准备打完收工,突然…

5. Node.js的LineParser库

发现了Node.js的LineParser库:

[javascript]
//JavaScript
var meta = {
program : ‘adb’,
name : ‘Android Debug Bridge’,
version : ‘1.0.3’,
subcommands : [ ‘connect’, ‘disconnect’, ‘install’ ],
options : {
flags : [
[ ‘h’, ‘help’, ‘print program usage’ ],
[ ‘r’, ‘reinstall’, ‘reinstall package’ ],
[ ‘l’, ‘localhost’, ‘localhost’ ]
],
parameters : [
[ null, ‘host’, ‘adb server hostname or IP address’, null ],
[ ‘p’, ‘port’, ‘adb server port’, 5037 ]
]
},
usages : [
[ ‘connect’, [‘host’, ‘[port]’], null, ‘connect to adb server’, adb_connect ],
[ ‘connect’, [ ‘l’ ], null, ‘connect to the local adb server’, adb_connect ],
[ ‘disconnect’, null, null, ‘disconnect from adb server’, adb_disconnect ],
[ ‘install’, [‘r’], [‘package’], ‘install package’, adb_install ],
[ null, [‘h’], null, ‘help’, adb_help ],
]
};

try {
var lineparser = require(‘lineparser’);
var parser = lineparser.init(meta);
// adb_install will be invoked
parser.parse([‘install’, ‘-r’, ‘/pkgs/bird.apk’]);
}
catch (e) {
console.error(e);
}
[/javascript]

天啊!?这是什么?我和小伙伴们彻底惊呆了!短短十几行代码就获得了上面5点的全面支持,重要的是小伙伴们居然一下子就看懂了,没有任何的遮遮掩掩和故弄玄虚。本来以为Ruby和Lisp很酷,小伙伴们都想马上去学Ruby和Lisp了,看到这个代码之后怎么感觉前面全是在装呢?有个小伙伴居然激动得哭着表示:我写代码多年,以为再也没有什么代码可以让我感动,没想到这段代码如此精妙,我不由得要赞叹了,实在是太漂亮了!

小伙伴们的故事讲完了,您看懂了吗?如果没有看懂的话,正题开始了:

在绝大多数语言中数据和代码可以说是泾渭分明,习惯C++、Java等主流语言的程序员很少去思考数据和代码之间的关系。与多数语言不同的是Lisp以“数据即代码,代码即数据”著称,Lisp用S表达式统一了数据和代码的形式而独树一帜。Lisp奇怪的S表达式和复杂的宏系统让许多人都感到Lisp很神秘,而多数Lisp教程要么强调函数式编程,要么鼓吹宏如何强大,反而掩盖了Lisp真正本质的东西,为此我曾写过一篇《Lisp的永恒之道》介绍Lisp思想。

设计思想和具体技术的区别在于前者往往可以在不同的环境中以不同的形式展现出来。比如,熟悉函数式编程的程序员在理解了纯函数的优点后即使是用C语言也会更倾向于写出无副作用的函数来,这就是函数式思想在命令式环境的应用。所以,理解Lisp思想一定要能在非Lisp环境应用,才算是融汇贯通。

如果真正理解了Lisp的本质,那所谓的“数据即代码,代码即数据”一点儿也不神秘,这不就是我们每天打交道的配置文件吗!?如果你还不是很理解的话,我们通过下面几个问题慢慢分析:

1) 配置的本质是什么?为什么要在程序中使用配置文件?

不知道你是否意识到了,我们每天都在使用的各种各样的配置本质上是一种元数据也是一种DSL,这和Lisp基于S表达式的“数据即代码,代码即数据”没有本质区别。在C++、Java等程序中引入配置文件的目的正是用DSL弥补通用语言表达能力和灵活性的不足。我知道不少人喜欢从计算的角度来看到程序和语言,似乎只有图灵完备的语言如C++、Java、Python等才叫程序设计语言,而类似CSS和HTML这样的东西根本不能叫做程序设计语言。其实,在我看来这种观点过于狭隘,程序的本质是语义的表达,而语义表达不一定要是计算。

2) 配置是数据还是代码?

很明显,Both!说配置是数据,因为它是声明式的描述,能方便地修改和传输;说配置是代码,因为它在表达逻辑,你的程序实际上就是配置的解释器。

3) 配置的格式是什么?

配置的格式是任意的,可以自己定义语法,只要配以相应的解释器就行。不过更简单通用的做法是基于XML、JSON、或S表达式等标准结构,在此之上进一步定义schema。甚至完全不必是文件,在我们的项目中配置经常是放到用关系数据库中的。另外,下面我们还会看到用语言的Literal数据作为配置。

4) 业务逻辑都可以放到配置中吗?

这个问题的答案显然是:Yes!我没有遇到过不可以放入配置的逻辑,只是问题在于这样做是否值得,能达到什么效果。对于需要灵活变化,重复出现,有复用价值的东西放入作为配置是明智的选择。这篇文章的主要目的就在于介绍把主要业务逻辑都放到配置中,再通过程序解释执行配置的设计方法,我称之为:元驱动编程(Meta Driven Programming)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 数据即代码:元驱动编程 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/10337.html/feed 77
类型的本质和函数式实现 https://coolshell.cn/articles/10169.html https://coolshell.cn/articles/10169.html#comments Mon, 22 Jul 2013 11:46:00 +0000 http://coolshell.cn/?p=10169 (感谢 @文艺复兴记(todd) 投递此文) 在上一篇文章《二叉树迭代器算法》中,我介绍了一种基于栈的二叉树迭代器实现。程序设计语言和Haskell大牛@九瓜 ...

Read More Read More

The post 类型的本质和函数式实现 first appeared on 酷 壳 - CoolShell.]]>
(感谢 @文艺复兴记(todd) 投递此文)

在上一篇文章《二叉树迭代器算法》中,我介绍了一种基于栈的二叉树迭代器实现。程序设计语言和Haskell大牛@九瓜 在看过之后评论到:

这里用了 stack 来做,有点偷懒,所以错失了一个抽象思考机会。如果我们能够理解二叉树到线性表的转换过程,完全可以把 Iterator 当作抽象的线性表来看,只要定义了关于 Iterator 的 empty, singleton, 还有 append 操作,实现二叉树的 Iterator 就变得非常直观。

“错失了一个抽象思考机会”是什么意思呢?我理解九瓜的意思是基于栈的实现虽然是正确的,但它缺乏对于迭代器类型本质的理解,不具有通用性。如果能对迭代器进行合适地抽象就可以像二叉树递归遍历一样自然地得出二叉树迭代器,甚至其他更复杂的数据结构,只要我们能写出它的遍历算法,迭代器算法都可以自然推出。

类型的本质

九瓜提到了通过empty, singleton和append操作对Iterator进行抽象,我本来打算直接根据这个思路介绍函数式的二叉树迭代器实现,但是考虑到其实首要的问题在于理解类型的本质,而并不是所有人都具备这个基础,不如先普及一下类型基础再进入具体实现。那么下面我们就先来认识一下类型到底是什么?我们先以来看看表示元素对的Pair类型,可能有人一提到Pair类型马上就会在脑海中浮现出下面的结构:

struct Pair {
    int left;
    int right;
}

其实,这种理解是非本质的,Pair完全可以用2个元素的数组来表示,第一个元素表示left,第二个元素表示right:

struct Pair {
    int elements[2];
}

上面的两种不同表示是类型的不同实现,而类型的本质是由操作(Operation)和操作间的关系或不变式(Invariant)所定义的,我们称之为类型规范(Type Specification)。比如,Pair类型是这样定义的:

Type Pair:
    Operations:
        Pair make_pair(int x, int y)
        int get_left(Pair pair)
        int get_right(Pair pair)
    Invariants:
        get_left(make_pair(x, y)) == x  //对x, y构造的Pair取左元素等于x
        get_right(make_pair(x, y)) == y  //对x, y构造的Pair取右元素等于y

也就是说只要是满足Pair类型规范,即定义了make_pair,get_left, get_right这3种操作,并且这些操作满足上面两个不变式,那么它这就是Pair类型。我们再来看看稍微复杂一点的Stack类型:

Type Stack:
    Operations:
        Stack make_stack()  //构造空栈
        Stack push(Stack stack, int x)  //将元素x压栈,返回新栈
        int top(stack)  //获取栈顶元素
        Stack pop(Stack stack)  //将栈顶元素弹栈,返回新栈
    Invariants:
        top(push(stack, x)) == x  //栈顶元素为最后一次压栈值
        pop(push(stack, x)) == stack  //对stack压栈后再弹栈等于原来的stack

Stack类型规范简言之就是FILO(先入后出),如果要形式化就是上面的不变式。为了加深理解,我们现在切换到测试视角来看一看,如果请你来编写一个Stack类的单元测试用例,你应该怎么写呢?许多朋友都不清楚单元测试到底测什么?怎么测?我见过不少人用一个测试用例单独对push做测试,用另一个测试用例对pop单独做测试,其主要原因就在于缺乏对类型本质的理解。其实,只要理解了类型的本质我们就知道孤立地看push或pop是没有任何意义的,它们的意义是在FILO关系下相互解释的,所以测试当然是基于类型规范来测试FILO不变式!这种基于类型规范的测试是一种黑盒测试,与类型的内部实现细节无关,只要单元测试覆盖了类型所定义的所有操作和不变式,那么不管内部怎么实现或优化,测试用例都不需要调整。反之,如果深入到了类型的内部实现做白盒测试,那这样的测试用例实际上就不再是反映其类型规范了,它会随着实现细节的调整而失效。

更深一层来看,不仅是在Pair,Stack这样的微观层面,在一个系统的宏观层面同样可以采用类型视角,即考察系统定义了哪些操作?这些操作之间有什么样的关系或不变式?比如,你如何从类型的角度来看待MySQL这样一套数据库系统?MySQL系统定义了哪些操作?这些操作之间必须满足怎样的关系和不变式?不仅如此,类型视角除了可以应用于计算机系统,甚至还可以应用于生活中的事物,比如,你到超市购物可以寄存自己的包,在寄包的时候会获得一张密码条,取包时可以通过密码条打开箱子。你能从超市寄包这个例子中看出类型来吗?如果你看出来了,说明你对类型的理解真正融会贯通了!

类型的函数式实现

上面我们介绍了类型的本质在于操作和操作间的关系,下面我们要关注的是类型的实现。在上面的例子中,Pair的内部结构到底是什么,是一个left和一个right成员?还是一个两元素的数组?没有讲,也没关系,就好像Windows的Handle和Linux的FileDescriptor一样,它们都是一个标识,你并不需要关心它的值本身,你只需要用几个相关的函数创建和操作它就行了(上面超市寄包例子中的密码条和Windows中的Handle是什么关系,你看出来了吗?你需要理解密码条的内容吗?)。换句话说,只要满足类型规范,具体实现是可变的,使用者只依赖于类型规范而不依赖于其具体实现。这在面向对象语言中意味着接口保持不变而具体实现可以变化(这里把public方法视为一种广义的接口)。

下面,我们还会看到的是不仅类型的内部实现可以变化,而且可以根本没有什么所谓的内部实现。这是什么意思呢?让我们来思考一下,是不是Pair内部一定要有什么东西来保存构造函数传入的left和right?我们能跳出这个定势吗?在函数式编程中,我们能做到:

[javascript]
//Javascript
function make_pair(x, y) {
// 返回一个支持get_left和get_right操作的闭包(Closure)
return {
get_left : function() { return x },
get_right : function() { return y }
}
}
function get_left(pair) {
return pair.get_left();
}
function get_right(pair) {
return pair.get_right();
}
// Test case
console.log(get_left(make_pair(1, 2))) //1
console.log(get_right(make_pair(1, 2))) //2
[/javascript]

上面的关键代码在于make_pair的内部返回的不是一种具体的数据结构,而是一个支持get_left和get_right操作的闭包(Closure),将来可以通过get_left和get_right来提取x, y。这种基于闭包的实现和我们通常采用的基于数据结构的实现的本质区别在哪里呢?不难发现,基于闭包的实现和类型规范是直接对应的,它并没有引入类型规范之外的东西,而基于数据结构的实现则隐藏了实现的细节。换句话说,如果要验证实现代码的正确性,对于前者只需要比对着类型规范,对于后者我们可能需要去仔细理解推敲其所采用的数据结构。对于Pair这样简单的结构二者差别不大,甚至基于数据结构的实现更简单,但是对于复杂的类型就容易体现出闭包实现的优势了。为了加深理解,我们再来看一个Stack的函数式实现:

[javascript]
//Javascript
function make_stack() {
return null
}
function push(stack, x) {
return {
top : function() { return x },
pop : function() { return stack }
}
}
function top(stack) {
return stack.top()
}
function pop(stack) {
return stack.pop()
}
// Test case
var stack = make_stack()
stack = push(stack, 1)
stack = push(stack, 2)
stack = push(stack, 3)
console.log(top(stack)) //3
stack = pop(stack)
console.log(top(stack)) //2
stack = push(stack, 4)
console.log(top(stack)) //4
[/javascript]

上面的所有函数都是采用了无副作用的纯函数式设计,可能习惯面向对象编程的朋友不是很习惯,不过这不影响我们对类型的讨论,而且它也很容易改造成面向对象的风格,感兴趣的朋友可以自己尝试对上面的代码进行简单的包装让它看起来像面向对象的样子。

函数式二叉树迭代器

上面我们介绍了类型的本质和函数式实现,下面我们再来看看Iterator类型又该如何定义和实现呢? 思路当然还是从操作入手,考虑Iterator类型对应了哪些操作,它们的关系是什么?上面九瓜提示了Iterator类型可以抽象为线性表List类型,或者说Iterator本质上是一个List。为什么呢?其实,只要跳出“如何表示数据结构”的思维,从类型角度思考就很容易理解,因为Iterator和List都定义了相同的操作,Iterator的使用者完全不关心也不知道它背后到底是链表还是二叉树,你对Iterator的操作和一个List的操作完全一样。正是这个原因,STL等范型库才能通过Iterator将算法和数据结构解耦。

怎么定义一个List类型呢?九瓜提到的empty(), singleton()和append()实际上就是和List打交道最多的Lisp语言的经典定义方式。Lisp是基于s-expression的,s-expression既可以视为线性表又可以视为树,本质上Lisp为List类型了构造、取首元素和取剩余元素等几种操作:

Type List:
    Operations:
        List empty()  //构造空表,通常由()这个文字量表示
        List singleton(Element e)  //构造包含一个元素的表,通常由(e)这个文字量表示
        Element first(List list)   //取list的第一个元素,通常又叫car操作
        List rest(List list)  //取list除第一个元素外的剩余部分,通常又叫cdr操作
        List append(List list1, List list2) //连接两个表
    Invariants:
        append(empty(), list) == list  //空表和表list连接后等于表list
        append(list, empty()) == list  //空表和表list连接后等于表list
        first(singleton(e)) == e  //对singleton(e)取首元素等于e
        rest(singleton(e)) == empty()  //对singleton(e)取首元素外的剩余部分的结果为空表
        append(first(list), rest(list)) == list  //对list的首尾两部分进行连接等于list本身
        if list1 is not empty then
            first(append(list1, list2)) == first(list1)  //对非空表list1于表list2的连接取首元素等于对非空表list1取首元素
        if list1 is not empty then
            rest(append(list1, list2)) == append(rest(list1), list2)  //对非空表list1于表list2的连接取首元素等于对非空表list1取首元素

有了上面的分析,我们相应地写出下面的List实现:

[javascript]
//Javascript
function empty() {
return null
}
function singleton(e) {
return {
first: function() { return e },
rest: function() { return null }
}
}
function first(list) {
return list.first()
}
function rest(list) {
return list.rest()
}
function append(list1, list2) {
if (null == list1) return list2
if (null == list2) return list1

return {
first : function() { return first(list1) },
rest : function() { return append(rest(list1), list2) }
}
}
[/javascript]

在此基础上可以进一步实现二叉树迭代器:

[javascript]
function make_binary_tree_iterator(node) {
return {
first : function() {
return null != node.left ? first(make_binary_tree_iterator(node.left)) : node
},
rest : function() {
var left_it = (null == node.left ? null : make_binary_tree_iterator(node.left))
var root_it = singleton(node)
var right_it = (null == node.right ? null : make_binary_tree_iterator(node.right))
var it = append(append(left_it, root_it), right_it)
return rest(it)
}
}
}
//======== Test case ========
var tree = {
value : 1,
left : {
value : 2,
left : { value : 4, left : null, right : null },
right : null
},
right : {
value : 3,
left : null,
right : { value : 7, left : null, right : null }
}
}
for (var it = make_binary_tree_iterator(tree); null != it; it = rest(it)) {
console.log(first(it).value)
}
[/javascript]

上面的make_binary_tree_iterator在List类型的基础上按照二叉树遍历过程构造了一个List。不知道你是否注意到了,为什么它不像下面这个例子一样直接返回一个List,而要构造一个闭包呢?

[javascript]
function make_binary_tree_iterator(node) {
var left_it = (null == node.left ? null : make_binary_tree_iterator(node.left))
var root_it = singleton(node)
var right_it = (null == node.right ? null : make_binary_tree_iterator(node.right))
return append(append(left_it, root_it), right_it)
}
[/javascript]

这里关键的区别在于闭包是惰性求值的,也就是说只有当真正开始迭代遍历的时候才会逐渐对各个函数进行求值,而上面的函数递归调用是非惰性的,会从一开始就把所有结点展开成线性表。如果你对这一点还不能很好地理解,可以尝试在各个函数中加log跟踪函数的调用过程。

总结

本文介绍了类型的本质在于它所定义的操作以及操作之间的关系和不变式。类型的实现关键在于满足类型规范的要求,而具体实现是可以变化的,使用者和测试用例都应该只依赖于类型规范而不依赖于具体实现。函数式的类型实现往往和类型规范是直接对应的,简单通用,但可能有性能问题,而命令式的类型实现往往会引入复杂的内部数据结构,但是其优点是高效。这两种实现并不是完全互斥的,有时候可以将二者相结合达到简单与高效的结合。

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 类型的本质和函数式实现 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/10169.html/feed 32
Javascript 装载和执行 https://coolshell.cn/articles/9749.html https://coolshell.cn/articles/9749.html#comments Wed, 05 Jun 2013 00:31:06 +0000 http://coolshell.cn/?p=9749 一两个月前在淘宝内网里看到一个优化Javascript代码的竞赛,发现有不少的人对Javascript的执行和装载的基础并不懂,所以,从那天起我就想写一篇文章,...

Read More Read More

The post Javascript 装载和执行 first appeared on 酷 壳 - CoolShell.]]>
一两个月前在淘宝内网里看到一个优化Javascript代码的竞赛,发现有不少的人对Javascript的执行和装载的基础并不懂,所以,从那天起我就想写一篇文章,但一直耽搁了。自上篇《浏览器渲染原理简介》,正好也可以承前启后。

首先,我想说一下Javascript的装载和执行。通常来说,浏览器对于Javascript的运行有两大特性:1)载入后马上执行,2)执行时会阻塞页面后续的内容(包括页面的渲染、其它资源的下载)。于是,如果有多个js文件被引入,那么对于浏览器来说,这些js文件被被串行地载入,并依次执行。

因为javascript可能会来操作HTML文档的DOM树,所以,浏览器一般都不会像并行下载css文件并行下载js文件,因为这是js文件的特殊性造成的。所以,如果你的javascript想操作后面的DOM元素,基本上来说,浏览器都会报错说对象找不到。因为Javascript执行时,后面的HTML被阻塞住了,DOM树时还没有后面的DOM结点。所以程序也就报错了。

传统的方式

所以,当你写在代码中写下如下的代码:

<script type="text/javascript"
        src="https://coolshell.cn/asyncjs/alert.js"></script>

基本上来说,head里的 <script>标签会阻塞后续资源的载入以及整个页面的生成。我专门做了一个示例你可以看看:示例一。 注意:我的alert.js中只有一句话:alert(“hello world”) ,这更容易让你看到javascript是怎么阻塞后面的东西的。

所以,你知道为什么有很多网站把javascript放在网页的最后面了,要么就是动用了window.onload或是docmuemt ready之类的事件。

另外,因为绝大多数的Javascript代码并不需要等页面,所以,我们异步载入的功能。那么我们怎么异步载入呢?

document.write方式

于是,你可能以为document.write()这种方式能够解决不阻塞的方式。你当然会觉得,document.write了的<script>标签后就可以执行后面的东西去了,这没错。对于在同一个script标签里的Javascript的代码来说,是这样的,但是对于整个页面来说,这个还是会阻塞。 下面是一段测试代码:

<script type="text/javascript" language="javascript">
    function loadjs(script_filename) {
        document.write('<' + 'script language="javascript" type="text/javascript"');
        document.write(' src="' + script_filename + '">');
        document.write('<'+'/script'+'>');
        alert("loadjs() exit...");
    }

    var script = 'https://coolshell.cn/asyncjs/alert.js';

    loadjs(script);
    alert("loadjs() finished!");
</script>

<script type="text/javascript" language="javascript">
   alert("another block");
</script>

你觉得alert的顺序是什么?你可以在不同的浏览器里试一试。这里的想关的测试页面:示例二

script的defer和async属性

IE自从IE6就支持defer标签,如:

<script defer type="text/javascript" src="./alert.js" >
</script>

对于IE来说,这个标签会让IE并行下载js文件,并且把其执行hold到了整个DOM装载完毕(DOMContentLoaded),多个defer的<script>在执行时也会按照其出现的顺序来运行。最重要的是<script>被加上defer后,其不会阻塞后续DOM的的渲染。但是因为这个defer只是IE专用,所以一般用得比较少。

而我们标准的的HTML5也加入了一个异步载入javascript的属性:async,无论你对它赋什么样的值,只要它出现,它就开始异步加载js文件。但是, async的异步加载会有一个比较严重的问题,那就是它忠实地践行着“载入后马上执行”这条军规,所以,虽然它并不阻塞页面的渲染,但是你也无法控制他执行的次序和时机。你可以看看这个示例去感受一下

支持 async标签的浏览器是:Firefox3.6+,Chrome 8.0+,Safari 5.0+,IE 10+,Opera还不支持(来自这里)所以这个方法也不是太好。因为并不是所有的浏览器你都能行。

动态创建DOM方式

这种方式可能是用得最多的了。

function loadjs(script_filename) {
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('src', script_filename);
    script.setAttribute('id', 'coolshell_script_id');

    script_id = document.getElementById('coolshell_script_id');
    if(script_id){
        document.getElementsByTagName('head')[0].removeChild(script_id);
    }
    document.getElementsByTagName('head')[0].appendChild(script);
}

var script = 'https://coolshell.cn/asyncjs/alert.js';
loadjs(script);

这个方式几乎成了标准的异步载入js文件的方式,这个方式的演示请参看:示例三。这方式还被玩出了JSONP的东东,也就是我可以为script的src指定某个后台的脚本(如PHP),而这个PHP返回一个javascript函数,其参数是一个json的字符串,返回来调用我们的预先定义好的javascript的函数。你可以看一下这个示例:t.js (这个示例是我之前在微博征集的一个异步ajax调用的小例子

按需异步载入js

上面那个DOM方式的例子解决了异步载入Javascript的问题,但是没有解决我们想让他按我们指定的时机运行的问题。所以,我们只需要把上面那个DOM方式绑到某个事件上来就可以了。

比如:

绑在window.load事件上——示例四 

你一定要比较一下示例四和示例三在执行上有什么不同,我在这两个示例中都专门用了个代码高亮的javascript,看看那个代码高亮的的脚本的执行和我的alert.js的执行的情况,你就知道不同了)

window.load = loadjs("https://coolshell.cn/asyncjs/alert.js")

绑在特定的事件上——示例五

<p style="cursor: pointer" onclick="LoadJS()">Click to load alert.js </p>

这个示例很简单了。当你点击某个DOM元素,才会真正载入我们的alert.js。

更多

但是,绑定在某个特定事件上这个事似乎又过了一点,因为只有在点击的时候才会去真正的下载js,这又会太慢了了。好了,到这里,要抛出我们的终极问题——我们想要异步地把js文件下载到用户的本地,但是不执行,仅当在我们想要执行的时候去执行

要是我们有下面这样的方式就好了:

var script = document.createElement("script");
script.noexecute = true;
script.src = "alert.js";
document.body.appendChild(script);

//后面我们可以这么干
script.execute();

可惜的是,这只是一个美丽的梦想,今天我们的Javascript还比较原始,这个“JS梦”还没有实现呢。

所以,我们的程序员只能使用hack的方式来搞。

有的程序员使用了非标准的script的type来cache javascript。如:

<script type=cache/script src="./alert.js"></script>

因为”cache/script”,这个东西根本就不能被浏览器解析,所以浏览器也就不能把alert.js当javascript去执行,但是他又要去下载js文件,所以就可以搞定了。可惜的是,webkit严格符从了HTML的标准——对于这种不认识的东西,直接删除,什么也不干。于是,我们的梦又破了。

所以,我们需要再hack一下,就像N多年前玩preload图片那样,我们可以动用object标签(也可以动用iframe标签),于是我们有下面这样的代码:

    function cachejs(script_filename){
        var cache = document.createElement('object');
        cache.data = script_filename;
        cache.id = "coolshell_script_cache_id";
        cache.width = 0;
        cache.height = 0;
        document.body.appendChild(cache);
    }

然后,我们在的最后调用一下这个函数。请参看一下相关的示例:示例六

在Chrome下按 Ctrl+Shit+I,切换到network页,你就可以看到下载了alert.js但是没有执行,然后我们再用示例五的方式,因为浏览器端有缓存了,不会再从服务器上下载alert.js了。所以,就能保证执行速度了。

关于这种preload这种东西你应该不会陌生了。你还可以使用Ajax的方式,如:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'new.js');
xhr.send('');

到这里我就不再多说了,也不给示例了,大家可以自己试试去。

最后再提两个js,一个是ControlJS,一个叫HeadJS,专门用来做异步load javascript文件的。

好了,这是所有的内容了,希望大家看过后能对Javascript的载入和执行,以及相关的技术有个了解。同时,也希望各前端高手不吝赐教!

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post Javascript 装载和执行 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/9749.html/feed 104
浏览器的渲染原理简介 https://coolshell.cn/articles/9666.html https://coolshell.cn/articles/9666.html#comments Wed, 22 May 2013 00:17:47 +0000 http://coolshell.cn/?p=9666 看到这个标题大家一定会想到这篇神文《How Browsers Work》,这篇文章把浏览器的很多细节讲得很细,而且也被翻译成了中文。为什么我还想写一篇呢?因为两...

Read More Read More

The post 浏览器的渲染原理简介 first appeared on 酷 壳 - CoolShell.]]>
看到这个标题大家一定会想到这篇神文《How Browsers Work》,这篇文章把浏览器的很多细节讲得很细,而且也被翻译成了中文。为什么我还想写一篇呢?因为两个原因,

1)这篇文章太长了,阅读成本太大,不能一口气读完。

2)花了大力气读了这篇文章后可以了解很多,但似乎对工作没什么帮助。

所以,我准备写下这篇文章来解决上述两个问题。希望你能在上班途中,或是坐马桶时就能读完,并能从中学会一些能用在工作上的东西。

浏览器工作大流程

废话少说,先来看个图:

从上面这个图中,我们可以看到那么几个事:

1)浏览器会解析三个东西:

  • 一个是HTML/SVG/XHTML,事实上,Webkit有三个C++的类对应这三类文档。解析这三种文件会产生一个DOM Tree。
  • CSS,解析CSS会产生CSS规则树。
  • Javascript,脚本,主要是通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree.

2)解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。注意:

  • Rendering Tree 渲染树并不等同于DOM树,因为一些像Header或display:none的东西就没必要放在渲染树中了。
  • CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个Element。也就是DOM结点。也就是所谓的Frame。
  • 然后,计算每个Frame(也就是每个Element)的位置,这又叫layout和reflow过程。

3)最后通过调用操作系统Native GUI的API绘制。

DOM解析

HTML的DOM Tree解析如下:

<html>
<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>

上面这段HTML会解析成这样:

下面是另一个有SVG标签的情况。

CSS解析

CSS的解析大概是下面这个样子(下面主要说的是Gecko也就是Firefox的玩法),假设我们有下面的HTML文档:

<doc>
<title>A few quotes</title>
<para>
  Franklin said that <quote>"A penny saved is a penny earned."</quote>
</para>
<para>
  FDR said <quote>"We have nothing to fear but <span>fear itself.</span>"</quote>
</para>
</doc>

于是DOM Tree是这个样子:

然后我们的CSS文档是这样的:

  /* rule 1 */ doc { display: block; text-indent: 1em; }
/* rule 2 */ title { display: block; font-size: 3em; }
/* rule 3 */ para { display: block; }
/* rule 4 */ [class="emph"] { font-style: italic; }

于是我们的CSS Rule Tree会是这个样子:

注意,图中的第4条规则出现了两次,一次是独立的,一次是在规则3的子结点。所以,我们可以知道,建立CSS Rule Tree是需要比照着DOM Tree来的。CSS匹配DOM Tree主要是从右到左解析CSS的Selector,好多人以为这个事会比较快,其实并不一定。关键还看我们的CSS的Selector怎么写了。

注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,你就会在N多地方看到很多人都告诉你,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去,……

通过这两个树,我们可以得到一个叫Style Context Tree,也就是下面这样(把CSS Rule结点Attach到DOM Tree上):

所以,Firefox基本上来说是通过CSS 解析 生成 CSS Rule Tree,然后,通过比对DOM生成Style Context Tree,然后Firefox通过把Style Context Tree和其Render Tree(Frame Tree)关联上,就完成了。注意:Render Tree会把一些不可见的结点去除掉。而Firefox中所谓的Frame就是一个DOM结点,不要被其名字所迷惑了

注:Webkit不像Firefox要用两个树来干这个,Webkit也有Style对象,它直接把这个Style对象存在了相应的DOM结点上了。

渲染

渲染的流程基本上如下(黄色的四个步骤):

  1. 计算CSS样式
  2. 构建Render Tree
  3. Layout – 定位坐标和大小,是否换行,各种position, overflow, z-index属性 ……
  4. 正式开画

注意:上图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属会导致重新Layout,有些改变不会,就是那些指到天上的箭头,比如,修改后的CSS rule没有被匹配到,等。

这里重要要说两个概念,一个是Reflow,另一个是Repaint。这两个不是一回事。

  • Repaint——屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变。
  • Reflow——意味着元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,需要重新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,在reflow过程中,可能会增加一些frame,比如一个文本字符串必需被包装起来。

下面是一个打开Wikipedia时的Layout/reflow的视频(注:HTML在初始化的时候也会做一次reflow,叫 intial reflow),你可以感受一下:

Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。在一些高性能的电脑上也许还没什么,但是如果reflow发生在手机上,那么这个过程是非常痛苦和耗电的

所以,下面这些动作有很大可能会是成本比较高的。

  • 当你增加、删除、修改DOM结点时,会导致Reflow或Repaint
  • 当你移动DOM的位置,或是搞个动画的时候。
  • 当你修改CSS样式的时候。
  • 当你Resize窗口的时候(移动端没有这个问题),或是滚动的时候。
  • 当你修改网页的默认字体时。

注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。

多说两句关于滚屏的事,通常来说,如果在滚屏的时候,我们的页面上的所有的像素都会跟着滚动,那么性能上没什么问题,因为我们的显卡对于这种把全屏像素往上往下移的算法是很快。但是如果你有一个fixed的背景图,或是有些Element不跟着滚动,有些Elment是动画,那么这个滚动的动作对于浏览器来说会是相当相当痛苦的一个过程。你可以看到很多这样的网页在滚动的时候性能有多差。因为滚屏也有可能会造成reflow。

基本上来说,reflow有如下的几个原因:

  • Initial。网页初始化的时候。
  • Incremental。一些Javascript在操作DOM Tree时。
  • Resize。其些元件的尺寸变了。
  • StyleChange。如果CSS的属性发生变化了。
  • Dirty。几个Incremental的reflow发生在同一个frame的子树上。

好了,我们来看一个示例吧:

[javascript]var bstyle = document.body.style; // cache

bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; //  再一次的 reflow 和 repaint

bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint

bstyle.fontSize = "2em"; // reflow, repaint

// new DOM element – reflow, repaint
document.body.appendChild(document.createTextNode(‘dude!’));[/javascript]

当然,我们的浏览器是聪明的,它不会像上面那样,你每改一次样式,它就reflow或repaint一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行reflow。

但是有些时候,我们的脚本会阻止浏览器这么干,比如:如果我们请求下面的一些DOM值:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. IE中的 getComputedStyle(), 或 currentStyle

因为,如果我们的程序需要这些值,那么浏览器需要返回最新的值,而这样一样会flush出去一些样式的改变,从而造成频繁的reflow/repaint。

减少reflow/repaint

下面是一些Best Practices:

1)不要一条一条地修改DOM的样式。与其这样,还不如预先定义好css的class,然后修改DOM的className。

[javascript]// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// Good
el.className += " theclassname";

// Good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";[/javascript]

2)把DOM离线后修改。如:

  • 使用documentFragment 对象在内存里操作DOM
  • 先把DOM给display:none(有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他显示出来。
  • clone一个DOM结点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。

3)不要把DOM结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。

4)尽可能的修改层级比较低的DOM。当然,改变层级比较底的DOM有可能会造成大面积的reflow,但是也可能影响范围很小。

5)为动画的HTML元件使用fixed或absoult的position,那么修改他们的CSS是不会reflow的。

6)千万不要使用table布局。因为可能很小的一个小改动会造成整个table的重新布局。

In this manner, the user agent can begin to lay out the table once the entire first row has been received. Cells in subsequent rows do not affect column widths. Any cell that has content that overflows uses the ‘overflow’ property to determine whether to clip the overflow content.

Fixed layout, CSS 2.1 Specification

This algorithm may be inefficient since it requires the user agent to have access to all the content in the table before determining the final layout and may demand more than one pass.

Automatic layout, CSS 2.1 Specification

几个工具和几篇文章

有时候,你会也许会发现在IE下,你不知道你修改了什么东西,结果CPU一下子就上去了到100%,然后过了好几秒钟repaint/reflow才完成,这种事情以IE的年代时经常发生。所以,我们需要一些工具帮我们看看我们的代码里有没有什么不合适的东西。

  • Chrome下,Google的SpeedTracer是个非常强悍的工作让你看看你的浏览渲染的成本有多大。其实Safari和Chrome都可以使用开发者工具里的一个Timeline的东东。
  • IE下你可以用一个叫dynaTrace的IE扩展。

最后,别忘了下面这几篇提高浏览器性能的文章:

参考

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 浏览器的渲染原理简介 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/9666.html/feed 157
Javascript 中的 var https://coolshell.cn/articles/7480.html https://coolshell.cn/articles/7480.html#comments Thu, 24 May 2012 06:50:31 +0000 http://coolshell.cn/?p=7480 MelonCard发布了一篇文章——”how one missing var ruined our launch“(”少写了...

Read More Read More

The post Javascript 中的 var first appeared on 酷 壳 - CoolShell.]]>
MelonCard发布了一篇文章——”how one missing var ruined our launch“(”少写了一个var毁了我的网站”),这篇文章是说MelonCard用Node.js做后台,因为出了一个小高峰——有50-100人注册,结果整个网站都不响应了,而且还出现了很多奇怪的问题。当他们调查到问题的要源的时候,他们发现下面的代码少写了一个var。

[javascript]app.all(‘/apps/:user_id/status’, function(req, res, next) {
// …
initial = extractVariables(req.body);
});[/javascript]

为什么inital少写一个var会引发这个问题呢?因为如果你不写var,这个局部的变量会被javascript当成全局变量,而这个变量又是一个函数,所以,当多用户并发的时候,这个本应该在不同用户下互不干扰的变量,成了各个用户共享的东西。试想,用户A的数据被用户B覆盖了,用户A和B的数据还没处理完,结果被新的C给搞乱了,程序的逻辑自然出现了问题。

在stackoverflow.com上有这么一个贴子说明了“有var”和“无var”的差别:

// These are both globals
var foo = 1;
bar = 2;

function test()
{
    var foo = 1; // Local
    bar = 2;     // Global

    // Execute an anonymous function
    (function()
    {
        var wibble = 1; // Local
        foo = 2; // Inherits from scope above (creating a closure)
        moo = 3; // Global
    }())
}

上面这个示例告诉我们,如果你不用var,那么这个js引擎会一层一层地向上找父作用域中的变量,如果找到了,就用,如果找不到了,就会帮你定义一个全局的变量。上面这个例子充分说明了这一点。所以,如果你想在当前的作用域用声明变量,你一定要用var。这对于一些乱写javascript代码的程序员要注意了。这里再给大家介绍一个工具——

JSLint( http://www.jslint.com/ ),一个JS代码质量的分析工具,我们把上述stackoverflow的代码copy到JSLint这个在线工具中,我们可以看到下面的报告:

这个报告说明了源码中的那些变量的情况。

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post Javascript 中的 var first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/7480.html/feed 48
理解Javascript的闭包 https://coolshell.cn/articles/6731.html https://coolshell.cn/articles/6731.html#comments Wed, 07 Mar 2012 00:30:43 +0000 http://coolshell.cn/?p=6731 【感谢 Neo 投递本文 – 微博帐号:_锟_ 】 前言:还是一篇入门文章。Javascript中有几个非常重要的语言特性——对象、原型继承、闭包。...

Read More Read More

The post 理解Javascript的闭包 first appeared on 酷 壳 - CoolShell.]]>
感谢 Neo 投递本文 – 微博帐号:_锟_

前言:还是一篇入门文章。Javascript中有几个非常重要的语言特性——对象、原型继承、闭包。其中闭包对于那些使用传统静态语言C/C++的程序员来说是一个新的语言特性。本文将以例子入手来介绍Javascript闭包的语言特性,并结合一点ECMAScript语言规范来使读者可以更深入的理解闭包。

注:本文是入门文章,例子素材整理于网络,如果你是高手,欢迎针对文章提出技术性建议和意见。本文讨论的是Javascript,不想做语言对比,如果您对Javascript天生不适,请自行绕道。

什么是闭包

闭包是什么?闭包是Closure,这是静态语言所不具有的一个新特性。但是闭包也不是什么复杂到不可理解的东西,简而言之,闭包就是:

  • 闭包就是函数的局部变量集合,只是这些局部变量在函数返回后会继续存在。
  • 闭包就是就是函数的“堆栈”在函数返回后并不释放,我们也可以理解为这些函数堆栈并不在栈上分配而是在堆上分配
  • 当在一个函数内定义另外一个函数就会产生闭包

上面的第二定义是第一个补充说明,抽取第一个定义的主谓宾——闭包是函数的‘局部变量’集合。只是这个局部变量是可以在函数返回后被访问。(这个不是官方定义,但是这个定义应该更有利于你理解闭包)

做为局部变量都可以被函数内的代码访问,这个和静态语言是没有差别。闭包的差别在于局部变变量可以在函数执行结束后仍然被函数外的代码访问。这意味着函数必须返回一个指向闭包的“引用”,或将这个”引用”赋值给某个外部变量,才能保证闭包中局部变量被外部代码访问。当然包含这个引用的实体应该是一个对象,因为在Javascript中除了基本类型剩下的就都是对象了。可惜的是,ECMAScript并没有提供相关的成员和方法来访问闭包中的局部变量。但是在ECMAScript中,函数对象中定义的内部函数(inner function)是可以直接访问外部函数的局部变量,通过这种机制,我们就可以以如下的方式完成对闭包的访问了。

[javascript]
function greeting(name) {
var text = ‘Hello ‘ + name; // local variable
// 每次调用时,产生闭包,并返回内部函数对象给调用者
return function() { alert(text); }
}
var sayHello=greeting("Closure");
sayHello() // 通过闭包访问到了局部变量text
[/javascript]

上述代码的执行结果是:Hello Closure,因为sayHello()函数在greeting函数执行完毕后,仍然可以访问到了定义在其之内的局部变量text。

好了,这个就是传说中闭包的效果,闭包在Javascript中有多种应用场景和模式,比如Singleton,Power Constructor等这些Javascript模式都离不开对闭包的使用。

ECMAScript闭包模型

ECMAScript到底是如何实现闭包的呢?想深入了解的亲们可以获取ECMAScript 规范进行研究,我这里也只做一个简单的讲解,内容也是来自于网络。

在ECMAscript的脚本的函数运行时,每个函数关联都有一个执行上下文场景(Execution Context) ,这个执行上下文场景中包含三个部分

  • 文法环境(The LexicalEnvironment)
  • 变量环境(The VariableEnvironment)
  • this绑定

其中第三点this绑定与闭包无关,不在本文中讨论。文法环境中用于解析函数执行过程使用到的变量标识符。我们可以将文法环境想象成一个对象,该对象包含了两个重要组件,环境记录(Enviroment Recode),和外部引用(指针)。环境记录包含包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。全局的上下文场景中此引用值为NULL。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。

例如上面我们例子的闭包模型应该是这样,sayHello函数在最下层,上层是函数greeting,最外层是全局场景。如下图:

因此当sayHello被调用的时候,sayHello会通过上下文场景找到局部变量text的值,因此在屏幕的对话框中显示出”Hello Closure”
变量环境(The VariableEnvironment)和文法环境的作用基本相似,具体的区别请参看ECMAScript的规范文档。

闭包的样列

前面的我大致了解了Javascript闭包是什么,闭包在Javascript是怎么实现的。下面我们通过针对一些例子来帮助大家更加深入的理解闭包,下面共有5个样例,例子来自于JavaScript Closures For Dummies(镜像)
例子1:闭包中局部变量是引用而非拷贝

[javascript]
function say667() {
// Local variable that ends up within closure
var num = 666;
var sayAlert = function() { alert(num); }
num++;
return sayAlert;
}

var sayAlert = say667();
sayAlert()
[/javascript]

因此执行结果应该弹出的667而非666。

例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。

[javascript]
function setupSomeGlobals() {
// Local variable that ends up within closure
var num = 666;
// Store some references to functions as global variables
gAlertNumber = function() { alert(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals(); // 为三个全局变量赋值
gAlertNumber(); //666
gIncreaseNumber();
gAlertNumber(); // 667
gSetNumber(12);//
gAlertNumber();//12
[/javascript]

例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包

[javascript]
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = ‘item’ + list[i];
result.push( function() {alert(item + ‘ ‘ + list[i])} );
}
return result;
}

function testList() {
var fnlist = buildList([1,2,3]);
// using j only to help prevent confusion – could use i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
[/javascript]

testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.

例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。

[javascript]
function sayAlice() {
var sayAlert = function() { alert(alice); }
// Local variable that ends up within closure
var alice = ‘Hello Alice’;
return sayAlert;
}
var helloAlice=sayAlice();
helloAlice();
[/javascript]

执行结果是弹出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。

例子5:每次函数调用的时候创建一个新的闭包

[javascript]
function newClosure(someNum, someRef) {
// Local variables that end up within closure
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
alert(‘num: ‘ + num +
‘\nanArray ‘ + anArray.toString() +
‘\nref.someVar ‘ + ref.someVar);
}
}
closure1=newClosure(40,{someVar:’closure 1′});
closure2=newClosure(1000,{someVar:’closure 2′});

closure1(5); // num:45 anArray[1,2,3,45] ref:’someVar closure1′
closure2(-10);// num:990 anArray[1,2,3,990] ref:’someVar closure2’
[/javascript]

闭包的应用

Singleton 单件:

[javascript]
var singleton = function () {
var privateVariable;
function privateFunction(x) {
…privateVariable…
}

return {
firstMethod: function (a, b) {
…privateVariable…
},
secondMethod: function (c) {
…privateFunction()…
}
};
}();
[/javascript]

这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()’,如果没有这个'()’就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。

参考:

JavaScript Closures For Dummies(镜像) 可惜都被墙了。
Advance Javascript (Douglas Crockford 大神的视频,一定要看啊)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 理解Javascript的闭包 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/6731.html/feed 91
再谈javascript面向对象编程 https://coolshell.cn/articles/6668.html https://coolshell.cn/articles/6668.html#comments Mon, 27 Feb 2012 00:25:13 +0000 http://coolshell.cn/?p=6668 前言:虽有陈皓《Javascript 面向对象编程》珠玉在前,但是我还是忍不住再画蛇添足的补上一篇文章,主要是因为javascript这门语言魅力。另外这篇文章...

Read More Read More

The post 再谈javascript面向对象编程 first appeared on 酷 壳 - CoolShell.]]>
前言:虽有陈皓《Javascript 面向对象编程》珠玉在前,但是我还是忍不住再画蛇添足的补上一篇文章,主要是因为javascript这门语言魅力。另外这篇文章是一篇入门文章,我也是才开始学习Javascript,有一点心得,才想写一篇这样文章,文章中难免有错误的地方,还请各位不吝吐槽指正

吐槽Javascript

初次接触Javascript,这门语言的确会让很多正规军感到诸多的不适,这种不适来自于Javascript的语法的简练和不严谨,这种不适也来自Javascript这个悲催的名称,我在想网景公司的Javascript设计者在给他起名称那天一定是脑壳进水了,让Javascript这么多年来受了这么多不白之冤,人们都认为他是Java的附属物,一个WEB玩具语言。因此才会有些人会对Javascript不屑,认为Javascript不是一门真正的语言,但是这此他们真的错了。Javascript不仅是一门语言,是一门真真正正的语言,而且他还是一门里程碑式的语言,他独创多种新的编程模式原型继承,闭包(作者注:闭包不是JS首创,应该Scheme首创,prototypal inheritance 和 dynamic objects 是self语言首创,Javascript的首创并不精彩,谢谢网友的指正。),对后来的动态语言产生了巨大的影响。做为当今最流行的语言(没有之一),看看git上提交的最多的语言类型就能明白。随着HTML5的登场,浏览器将在个人电脑上将大显身手,完全有替换OS的趋势的时候,Javascript做为浏览器上的一门唯一真真的语言,如同C之于 unix/linux,java之于JVM,Cobol之于MainFrame,我们也需要来重新的认真地认识和审视这门语言。另外Javascript的正式名称是:ECMAScript,这个名字明显比Javascript帅太多了!

言归正传,我们切入主题——Javascript的面向对象编程。要谈Javascript的面向对象编程,我们第一步要做的事情就是忘记我们所学的面向对象编程。传统C++或Java的面向对象思维来学习Javascript的面向对象会给你带来不少困惑,让我们先忘记我们所学的,从新开始学习这门特殊的面向对象编程。既然是OO编程,要如何来理解OO编程呢,记得以前学C++,学了很久都不入门,后来有幸读了《Inside The C++ Object Model》这本大作,顿时豁然开朗,因此本文也将以对象模型的方式来探讨的Javascript的OO编程。因为Javascript 对象模型的特殊性,所以使得Javascript的继承和传统的继承非常不一样,同时也因为Javascript里面没有类,这意味着Javascript里面没有extends,implements。那么Javascript到底是如何来实现OO编程的呢?好吧,让我们开始吧,一起在Javascript的OO世界里来一次漫游

首先,我们需要先看看Javascript如何定义一个对象。下面是我们的一个对象定义:

[javascript]
var o = {};
[/javascript]

还可以这样定义一个对象

[javascript]
function f() {
}
[/javascript]

对,你们没有看错,在Javascript里面,函数也是对象。
当然还可以

[javascript]
var array1= [ 1,2,3];
[/javascript]

数组也是一个对象。
其他关于对象的基本的概念的描述,还是请各位亲们参见陈皓《Javascript 面向对象编程》文章。
对象都有了,唯一没有的就是class,因为在Javascript里面是没有class关键字的,算好还有function,function的存在让我们可以变通的定义类,在扩展这个主题前,我们还需要了解一个Javascript对象最重要的属性,__proto__成员。

__proto__成员

严格的说这个成员不应该叫这个名字,__proto__是Firefox中的称呼,__proto__只有在Firefox浏览器中才能被访问到。做为一个对象,当你访问其中的一个成员或方法的时候,如果这个对象中没有这个方法或成员,那么Javascript引擎将会访问这个对象的__proto__成员所指向的另外的一个对象,并在那个对象中查找指定的方法或成员,如果不能找到,那就会继续通过那个对象的__proto__成员指向的对象进行递归查找,直到这个链表结束
好了,让我们举一个例子。
比如上上面定义的数组对象array1。当我们创建出array1这个对象的时候,array1实际在Javascript引擎中的对象模型如下:

array1对象具有一个length属性值为3,但是我们可以通过如下的方法来为array1增加元素:

[javascript]
array1.push(4);
[/javascript]

push这个方法来自于array1的__proto__成员指向对象的一个方法(Array.prototye.push())。正是因为所有的数组对象(通过[]来创建的)都包含有一个指向同一个具有push,reverse等方法对象(Array.prototype)的__proto__成员,才使得这些数组对象可以使用push,reverse等方法。

那么这个__proto__这个属性就相当于面向对象中的”has a”关系,这样的的话,只要我们有一个模板对象比如Array.prototype这个对象,然后把其他的对象__proto__属性指向这个对象的话就完成了一种继承的模式。不错!我们完全可以这么干。但是别高兴的太早,这个属性只在FireFox中有效,其他的浏览器虽然也有属性,但是不能通过__proto__来访问,只能通过getPrototypeOf方法进行访问,而且这个属性是只读的。看来我们要在Javascript实现继承并不是很容易的事情啊。

函数对象prototype成员

首先我们先来看一段函数prototype成员的定义,

When a function object is created, it is given a prototype member which is an object containing a constructor member which is a reference to the function object
当一个函数对象被创建时,这个函数对象就具有一个prototype成员,这个成员是一个对象,这个对象包含了一个构造子成员,这个构造子成员会指向这个函数对象。

例如:

[javascript]
function Base() {
this.id = "base"
}
[/javascript]

Base这个函数对象就具有一个prototype成员,关于构造子其实Base函数对象自身,为什么我们将这类函数称为构造子呢?原因是因为这类函数设计来和new 操作符一起使用的。为了和一般的函数对象有所区别,这类函数的首字母一般都大写。构造子的主要作用就是来创建一类相似的对象。

上面这段代码在Javascript引擎的对象模型是这样的

new 操作符

在有上面的基础概念的介绍之后,在加上new操作符,我们就能完成传统面向对象的class + new的方式创建对象,在Javascript中,我们将这类方式成为Pseudoclassical。
基于上面的例子,我们执行如下代码

[javascript]
var obj = new Base();
[/javascript]

这样代码的结果是什么,我们在Javascript引擎中看到的对象模型是:

new操作符具体干了什么呢?其实很简单,就干了三件事情。

[javascript]
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
[/javascript]

第一行,我们创建了一个空对象obj
第二行,我们将这个空对象的__proto__成员指向了Base函数对象prototype成员对象
第三行,我们将Base函数对象的this指针替换成obj,然后再调用Base函数,于是我们就给obj对象赋值了一个id成员变量,这个成员变量的值是”base”,关于call函数的用法,请参看陈皓《Javascript 面向对象编程》文章
如果我们给Base.prototype的对象添加一些函数会有什么效果呢?
例如代码如下:

[javascript]
Base.prototype.toString = function() {
return this.id;
}
[/javascript]

那么当我们使用new创建一个新对象的时候,根据__proto__的特性,toString这个方法也可以做新对象的方法被访问到。于是我们看到了:
构造子中,我们来设置‘类’的成员变量(例如:例子中的id),构造子对象prototype中我们来设置‘类’的公共方法。于是通过函数对象和Javascript特有的__proto__与prototype成员及new操作符,模拟出类和类实例化的效果。

Pseudoclassical 继承

我们模拟类,那么继承又该怎么做呢?其实很简单,我们只要将构造子的prototype指向父类即可。例如我们设计一个Derive 类。如下

[javascript]
function Derive(id) {
this.id = id;
}
Derive.prototype = new Base();
Derive.prototype.test = function(id){
return this.id === id;
}
var newObj = new Derive("derive");
[/javascript]

这段代码执行后的对象模型又是怎么样的呢?根据之前的推导,应该是如下的对象模型

这样我们的newObj也继承了基类Base的toString方法,并且具有自身的成员id。关于这个对象模型是如何被推导出来的就留给各位同学了,参照前面的描述,推导这个对象模型应该不难。
Pseudoclassical继承会让学过C++/Java的同学略微的感受到一点舒服,特别是new关键字,看到都特亲切,不过两者虽然相似,但是机理完全不同。当然不关什么样继承都是不能离不开__proto__成员的。

Prototypal继承

这是Javascript的另外一种继承方式,这个继承也就是之前陈皓文章《Javascript 面向对象编程》中create函数,非常可惜的是这个是ECMAScript V5的标准,支持V5的浏览器目前看来也就是IE9,Chrome最新版本和Firefox。虽然看着多,但是做为IE6的重灾区的中国,我建议各位还是避免使用create函数。好在没有create函数之前,Javascript的使用者已经设计出了等同于这个函数的。例如:我们看看Douglas Crockford的object函数。

[javascript]
function object(old) {
function F() {};
F.prototype = old;
return new F();
}
var newObj = object(oldObject);
[/javascript]

例如如下代码段

[javascript]
var base ={
id:"base",
toString:function(){
return this.id;
}
};
var derive = object(base);
[/javascript]

上面函数的执行后的对象模型是:

如何形成这样的对象模型,原理也很简单,只要把object这个函数扩展一下,就能画出这个模型,怎么画留给读者自己去画吧。
这样的继承方式被称为原型继承。相对来说要比Pseudoclassical继承来的简单方便。ECMAScript V5正是因为这原因也才增加create函数,让开发者可以快速的实现原型继承。
上述两种继承方式是Javascript中最常用的继承方式。通过本文的讲解,你应该对Javascript的OO编程有了一些‘原理’级的了解了吧

参考:

《Prototypes and Inheritance in JavaScript Prototypes and Inheritance in JavaScript》
Advance Javascript (Douglas Crockford 大神的视频,一定要看啊)

题外话:

web2.0后,web应用可谓飞速发展,如今在HTML5发布之际,浏览器的功能被大大强化,我感觉Browser远远在不是一个Browser那么简单了。记得C++之父曾经这样说过JAVA,JAVA不是跨平台,JAVA本身就是一个平台。如今的Browser也本身就是一个平台了,好在这个平台是基于标准的。如果Browser是平台,由于Browser安全沙箱的限制,个人电脑的资源被使用的很少,感觉Browser就是一个NC(Network Computer)?我们居然又回到了Sun最初提出的构想,Sun是不是太强大了些?

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 再谈javascript面向对象编程 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/6668.html/feed 77
Javascript 面向对象编程 https://coolshell.cn/articles/6441.html https://coolshell.cn/articles/6441.html#comments Mon, 09 Jan 2012 00:16:27 +0000 http://coolshell.cn/?p=6441 Javascript是一个类C的语言,他的面向对象的东西相对于C++/Java比较奇怪,但是其的确相当的强大,在 Todd 同学的“对象的消息模型”一文中我们已...

Read More Read More

The post Javascript 面向对象编程 first appeared on 酷 壳 - CoolShell.]]>
Javascript是一个类C的语言,他的面向对象的东西相对于C++/Java比较奇怪,但是其的确相当的强大,在 Todd 同学的“对象的消息模型”一文中我们已经可以看到一些端倪了。这两天有个前同事总在问我Javascript面向对象的东西,所以,索性写篇文章让他看去吧,这里这篇文章主要想从一个整体的角度来说明一下Javascript的面向对象的编程。(成文比较仓促,应该有不准确或是有误的地方,请大家批评指正

另,这篇文章主要基于 ECMAScript 5, 旨在介绍新技术。关于兼容性的东西,请看最后一节。

初探

我们知道Javascript中的变量定义基本如下:

var name = 'Chen Hao';;
var email = 'haoel(@)hotmail.com';
var website = 'https://coolshell.cn';

如果要用对象来写的话,就是下面这个样子:

var chenhao = {
    name :'Chen Hao',
    email : 'haoel(@)hotmail.com',
    website : 'https://coolshell.cn'
};

于是,我就可以这样访问:

//以成员的方式
chenhao.name;
chenhao.email;
chenhao.website;

//以hash map的方式
chenhao["name"];
chenhao["email"];
chenhao["website"];

关于函数,我们知道Javascript的函数是这样的:

var doSomething = function(){
   alert('Hello World.');
};

于是,我们可以这么干:

var sayHello = function(){
   var hello = "Hello, I'm "+ this.name
                + ", my email is: " + this.email
                + ", my website is: " + this.website;
   alert(hello);
};

//直接赋值,这里很像C/C++的函数指针
chenhao.Hello = sayHello;

chenhao.Hello();

相信这些东西都比较简单,大家都明白了。 可以看到javascript对象函数是直接声明,直接赋值,直接就用了。runtime的动态语言。

还有一种比较规范的写法是:

//我们可以看到, 其用function来做class。
var Person = function(name, email, website){
    this.name = name;
    this.email = email;
    this.website = website;

    this.sayHello = function(){
        var hello = "Hello, I'm "+ this.name  + ", \n" +
                    "my email is: " + this.email + ", \n" +
                    "my website is: " + this.website;
        alert(hello);
    };
};

var chenhao = new Person("Chen Hao", "[email protected]",
                                     "https://coolshell.cn");
chenhao.sayHello(); 

顺便说一下,要删除对象的属性,很简单:

delete chenhao['email']

上面的这些例子,我们可以看到这样几点:

  1. Javascript的数据和成员封装很简单。没有类完全是对象操作。纯动态!
  2. Javascript function中的this指针很关键,如果没有的话,那就是局部变量或局部函数。
  3. Javascript对象成员函数可以在使用时临时声明,并把一个全局函数直接赋过去就好了。
  4. Javascript的成员函数可以在实例上进行修改,也就是说不同实例相同函数名的行为不一定一样。

属性配置 – Object.defineProperty

先看下面的代码:

//创建对象
var chenhao = Object.create(null);

//设置一个属性
 Object.defineProperty( chenhao,
                'name', { value:  'Chen Hao',
                          writable:     true,
                          configurable: true,
                          enumerable:   true });

//设置多个属性
Object.defineProperties( chenhao,
    {
        'email'  : { value:  '[email protected]',
                     writable:     true,
                     configurable: true,
                     enumerable:   true },
        'website': { value: 'https://coolshell.cn',
                     writable:     true,
                     configurable: true,
                     enumerable:   true }
    }
);

下面就说说这些属性配置是什么意思。

  • writable:这个属性的值是否可以改。
  • configurable:这个属性的配置是否可以改。
  • enumerable:这个属性是否能在for…in循环中遍历出来或在Object.keys中列举出来。
  • value:属性值。
  • get()/set(_value):get和set访问器。

Get/Set 访问器

关于get/set访问器,它的意思就是用get/set来取代value(其不能和value一起使用),示例如下:

var  age = 0;
Object.defineProperty( chenhao,
            'age', {
                      get: function() {return age+1;},
                      set: function(value) {age = value;}
                      enumerable : true,
                      configurable : true
                    }
);
chenhao.age = 100; //调用set
alert(chenhao.age); //调用get 输出101(get中+1了);

我们再看一个更为实用的例子——利用已有的属性(age)通过get和set构造新的属性(birth_year):

Object.defineProperty( chenhao,
            'birth_year',
            {
                get: function() {
                    var d = new Date();
                    var y = d.getFullYear();
                    return ( y - this.age );
                },
                set: function(year) {
                    var d = new Date();
                    var y = d.getFullYear();
                    this.age = y - year;
                }
            }
);

alert(chenhao.birth_year);
chenhao.birth_year = 2000;
alert(chenhao.age);

这样做好像有点麻烦,你说,我为什么不写成下面这个样子:

var chenhao = {
    name: "Chen Hao",
    email: "[email protected]",
    website: "https://coolshell.cn",
    age: 100,
    get birth_year() {
        var d = new Date();
        var y = d.getFullYear();
        return ( y - this.age );
    },
    set birth_year(year) {
        var d = new Date();
        var y = d.getFullYear();
        this.age = y - year;
    }

};
alert(chenhao.birth_year);
chenhao.birth_year = 2000;
alert(chenhao.age);

是的,你的确可以这样的,不过通过defineProperty()你可以干这些事:
1)设置如 writable,configurable,enumerable 等这类的属性配置。
2)动态地为一个对象加属性。比如:一些HTML的DOM对像。

查看对象属性配置

如果查看并管理对象的这些配置,下面有个程序可以输出对象的属性和配置等东西:

//列出对象的属性.
function listProperties(obj)
{
    var newLine = "<br />";
    var names = Object.getOwnPropertyNames(obj);
    for (var i = 0; i < names.length; i++) {
        var prop = names[i];
        document.write(prop + newLine);

        // 列出对象的属性配置(descriptor)动用getOwnPropertyDescriptor函数。
        var descriptor = Object.getOwnPropertyDescriptor(obj, prop);
        for (var attr in descriptor) {
            document.write("..." + attr + ': ' + descriptor[attr]);
            document.write(newLine);
        }
        document.write(newLine);
    }
}

listProperties(chenhao);

call,apply, bind 和 this

关于Javascript的this指针,和C++/Java很类似。 我们来看个示例:(这个示例很简单了,我就不多说了)

function print(text){
    document.write(this.value + ' - ' + text+ '<br>');
}

var a = {value: 10, print : print};
var b = {value: 20, print : print};

print('hello');// this => global, output "undefined - hello"

a.print('a');// this => a, output "10 - a"
b.print('b'); // this => b, output "20 - b"

a['print']('a'); // this => a, output "10 - a"

我们再来看看call 和 apply,这两个函数的差别就是参数的样子不一样,另一个就是性能不一样,apply的性能要差很多。(关于性能,可到 JSPerf 上去跑跑看看)

print.call(a, 'a'); // this => a, output "10 - a"
print.call(b, 'b'); // this => b, output "20 - b"

print.apply(a, ['a']); // this => a, output "10 - a"
print.apply(b, ['b']); // this => b, output "20 - b"

但是在bind后,this指针,可能会有不一样,但是因为Javascript是动态的。如下面的示例

var p = print.bind(a);
p('a');             // this => a, output "10 - a"
p.call(b, 'b');     // this => a, output "10 - b"
p.apply(b, ['b']);  // this => a, output "10 - b"

继承 和 重载

通过上面的那些示例,我们可以通过Object.create()来实际继承,请看下面的代码,Student继承于Object。

var Person = Object.create(null);

Object.defineProperties
(
    Person,
    {
        'name'  : {  value: 'Chen Hao'},
        'email'  : { value : '[email protected]'},
        'website': { value: 'https://coolshell.cn'}
    }
);

Person.sayHello = function () {
    var hello = "<p>Hello, I am "+ this.name  + ", <br>" +
                "my email is: " + this.email + ", <br>" +
                "my website is: " + this.website;
    document.write(hello + "<br>");
}

var Student = Object.create(Person);
Student.no = "1234567"; //学号
Student.dept = "Computer Science"; //系

//使用Person的属性
document.write(Student.name + ' ' + Student.email + ' ' + Student.website +'<br>');

//使用Person的方法
Student.sayHello();

//重载SayHello方法
Student.sayHello = function (person) {
    var hello = "<p>Hello, I am "+ this.name  + ", <br>" +
                "my email is: " + this.email + ", <br>" +
                "my website is: " + this.website + ", <br>" +
                "my student no is: " + this. no + ", <br>" +
                "my departent is: " + this. dept;
    document.write(hello + '<br>');
}
//再次调用
Student.sayHello();

//查看Student的属性(只有 no 、 dept 和 重载了的sayHello)
document.write('<p>' + Object.keys(Student) + '<br>');

通用上面这个示例,我们可以看到,Person里的属性并没有被真正复制到了Student中来,但是我们可以去存取。这是因为Javascript用委托实现了这一机制。其实,这就是Prototype,Person是Student的Prototype。

当我们的代码需要一个属性的时候,Javascript的引擎会先看当前的这个对象中是否有这个属性,如果没有的话,就会查找他的Prototype对象是否有这个属性,一直继续下去,直到找到或是直到没有Prototype对象。

为了证明这个事,我们可以使用Object.getPrototypeOf()来检验一下:

Student.name = 'aaa';

//输出 aaa
document.write('<p>' + Student.name + '</p>');

//输出 Chen Hao
document.write('<p>' +Object.getPrototypeOf(Student).name + '</p>');

于是,你还可以在子对象的函数里调用父对象的函数,就好像C++里的 Base::func() 一样。于是,我们重载hello的方法就可以使用父类的代码了,如下所示:

//新版的重载SayHello方法
Student.sayHello = function (person) {
    Object.getPrototypeOf(this).sayHello.call(this);
    var hello = "my student no is: " + this. no + ", <br>" +
                "my departent is: " + this. dept;
    document.write(hello + '<br>');
}

这个很强大吧。

组合

上面的那个东西还不能满足我们的要求,我们可能希望这些对象能真正的组合起来。为什么要组合?因为我们都知道是这是OO设计的最重要的东西。不过,这对于Javascript来并没有支持得特别好,不好我们依然可以搞定个事。

首先,我们需要定义一个Composition的函数:(target是作用于是对象,source是源对象),下面这个代码还是很简单的,就是把source里的属性一个一个拿出来然后定义到target中。

function Composition(target, source)
{
    var desc  = Object.getOwnPropertyDescriptor;
    var prop  = Object.getOwnPropertyNames;
    var def_prop = Object.defineProperty;

    prop(source).forEach(
        function(key) {
            def_prop(target, key, desc(source, key))
        }
    )
    return target;
}

有了这个函数以后,我们就可以这来玩了:

//艺术家
var Artist = Object.create(null);
Artist.sing = function() {
    return this.name + ' starts singing...';
}
Artist.paint = function() {
    return this.name + ' starts painting...';
}

//运动员
var Sporter = Object.create(null);
Sporter.run = function() {
    return this.name + ' starts running...';
}
Sporter.swim = function() {
    return this.name + ' starts swimming...';
}

Composition(Person, Artist);
document.write(Person.sing() + '<br>');
document.write(Person.paint() + '<br>');

Composition(Person, Sporter);
document.write(Person.run() + '<br>');
document.write(Person.swim() + '<br>');

//看看 Person中有什么?(输出:sayHello,sing,paint,swim,run)
document.write('<p>' + Object.keys(Person) + '<br>');

Prototype 和 继承

我们先来说说Prototype。我们先看下面的例程,这个例程不需要解释吧,很像C语言里的函数指针,在C语言里这样的东西见得多了。

var plus = function(x,y){
    document.write( x + ' + ' + y + ' = ' + (x+y) + '<br>');
    return x + y;
};

var minus = function(x,y){
    document.write(x + ' - ' + y + ' = ' + (x-y) + '<br>');
    return x - y;
};

var operations = {
    '+': plus,
    '-': minus
};

var calculate = function(x, y, operation){
    return operations[operation](x, y);
};

calculate(12, 4, '+');
calculate(24, 3, '-');

那么,我们能不能把这些东西封装起来呢,我们需要使用prototype。看下面的示例:

var Cal = function(x, y){
    this.x = x;
    this.y = y;
}

Cal.prototype.operations = {
    '+': function(x, y) { return x+y;},
    '-': function(x, y) { return x-y;}
};

Cal.prototype.calculate = function(operation){
    return this.operations[operation](this.x, this.y);
};

var c = new Cal(4, 5);

c.calculate('+');
c.calculate('-');

这就是prototype的用法,prototype 是javascript这个语言中最重要的内容。网上有太多的文章介始这个东西了。说白了,prototype就是对一对象进行扩展,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的(当然,这里没有真正的复制,实际只是委托)。上面的这个例子中,我们扩展了实例Cal,让其有了一个operations的属性和一个calculate的方法。

这样,我们可以通过这一特性来实现继承。还记得我们最最前面的那个Person吧, 下面的示例是创建一个Student来继承Person。

function Person(name, email, website){
    this.name = name;
    this.email = email;
    this.website = website;
};

Person.prototype.sayHello = function(){
    var hello = "Hello, I am "+ this.name  + ", <br>" +
                "my email is: " + this.email + ", <br>" +
                "my website is: " + this.website;
    return hello;
};

function Student(name, email, website, no, dept){
    var proto = Object.getPrototypeOf;
    proto(Student.prototype).constructor.call(this, name, email, website);
    this.no = no;
    this.dept = dept;
}

// 继承prototype
Student.prototype = Object.create(Person.prototype);

//重置构造函数
Student.prototype.constructor = Student;

//重载sayHello()
Student.prototype.sayHello = function(){
    var proto = Object.getPrototypeOf;
    var hello = proto(Student.prototype).sayHello.call(this) + '<br>';
    hello += "my student no is: " + this. no + ", <br>" +
             "my departent is: " + this. dept;
    return hello;
};

var me = new Student(
    "Chen Hao",
    "[email protected]",
    "https://coolshell.cn",
    "12345678",
    "Computer Science"
);
document.write(me.sayHello());

兼容性

上面的这些代码并不一定能在所有的浏览器下都能运行,因为上面这些代码遵循 ECMAScript 5 的规范,关于ECMAScript 5 的浏览器兼容列表,你可以看这里“ES5浏览器兼容表”。

本文中的所有代码都在Chrome最新版中测试过了。

下面是一些函数,可以用在不兼容ES5的浏览器中:

Object.create()函数
function clone(proto) {
    function Dummy() { }

    Dummy.prototype             = proto;
    Dummy.prototype.constructor = Dummy;

    return new Dummy(); //等价于Object.create(Person);
}

var me = clone(Person);
defineProperty()函数
function defineProperty(target, key, descriptor) {
    if (descriptor.value){
        target[key] = descriptor.value;
    }else {
        descriptor.get && target.__defineGetter__(key, descriptor.get);
        descriptor.set && target.__defineSetter__(key, descriptor.set);
    }

    return target
}
keys()函数
function keys(object) { var result, key
    result = [];
    for (key in object){
        if (object.hasOwnProperty(key))  result.push(key)
    }

    return result;
}
Object.getPrototypeOf() 函数
function proto(object) {
    return !object?                null
         : '__proto__' in object?  object.__proto__
         : /* not exposed? */      object.constructor.prototype
}
bind 函数
var slice = [].slice

function bind(fn, bound_this) { var bound_args
    bound_args = slice.call(arguments, 2)
    return function() { var args
        args = bound_args.concat(slice.call(arguments))
        return fn.apply(bound_this, args) }
}

参考

(转载时请注明作者和出处,请勿用于任何商业用途)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post Javascript 面向对象编程 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/6441.html/feed 108
Web开发中需要了解的东西 https://coolshell.cn/articles/6043.html https://coolshell.cn/articles/6043.html#comments Wed, 07 Dec 2011 00:29:49 +0000 http://coolshell.cn/?p=6043 在StackExchange上有人问了这样一个问题:What should every programmer know about web developmen...

Read More Read More

The post Web开发中需要了解的东西 first appeared on 酷 壳 - CoolShell.]]>
在StackExchange上有人问了这样一个问题:What should every programmer know about web development?(关于Web开发,什么是所有程序员需要知道的?)里面给出的答案非常不错,所以,我翻译转载过来。 顺便说一下,StackExchange真是非常好,大家可以对同一个答案做贡献和修订,看看这个问题的修订过程你就知道了——专业的问答网站应该怎么去做。这就是我在这篇文章中也说过真正的用户体验是什么样的

好了,下面是正文(我对原文做了一些批注,也许不对或有误导,请大家指正)

下面的这些东西可能对于大多数人并不陌生,但是可能会有些东西你以前并没有看过,或是没有完全搞懂,甚至都没有听说过。(陈皓注:我相信当你看完这个列表后,你会觉得对于我国的Web开发有点弱了,还是那句话,表面上的东西永远是肤浅的)

接口和用户体验

  • 小心浏览器的实现标准上的不一致,确信让你的网站能够适当地跨浏览器。至少,你的网站需要测试一下下面的浏览器:

最后,你可以使用一下这个工具 来看看你的网页在不同的浏览器下是怎么被显示出来的(陈皓注:这个工具就是以前本站介绍过的在不同浏览器和平台上检查你的网站的兼容性

  • 多考虑一下人们是怎么来访问你的网站而不是那些主流的浏览器:手机,读屏软件和搜索引擎,例如:一些Accessibility的东西: WAI 和  Section508, 移动设备开发:MobiForge.
  • 部署Staging:怎么部署网站的更新而不会影响用户的访问。 Ed Lucas的答案 可以让你了解一些(陈皓注:Ed说了一些如版本控制,自动化build,备份,回滚等机制)。
  • 千万不要直接给用户显示不友好的错误信息。
  • 千万不要把用户的邮件地址以明文显示出来,这样会被爬虫爬走并被让用户的邮箱被垃圾邮件搞死。
  • 为用户的链接加上 rel="nofollow" 的属性以 避免垃圾网站的干扰。(陈皓注:nofollow是HTML的一个属性,用于通知搜索引擎“这个链接所指向的网页非我所能控制,对其内容不予置评”,或者简单地说,该链接不是对目标网站或网页的“投票”,这样搜索引擎不会再访问这个链接。这个是用来减少一些特定垃圾页面对原网站的影响,从而可以改善搜索结果的质量,并且防止垃圾链接的蔓延。)
  • 为网站建立一些的限制 – 这个属于安全性的范畴。(陈皓注:比如你在Google注册邮箱时,你一口气注册超过两个以上的邮箱,gmail要求给你发短信或是给你打电话认证,比如Discuz论坛的会限制你发贴或是搜索的间隔时间等等,更多的网站会用CAPTCHA来确认是人为的操作。 这些限制都是为了防止垃圾和恶意攻击)
  • 学习如何做 Progressive Enhancement. (陈皓注:Progressive Enhancement是一个Web Design的理念,如:1)基础的内容和功能应该可以被所有的浏览器存取,2)页面布局的应该使用外部的CSS链接,3)Javascript也应该是外部链接还应该是 unobtrusive 的,4)应该让用户可以设置他们的偏好)
  • 严重关注Accessibility。因为这是法律上的需求(陈皓注:Section 508是美国的508法案,其是美国劳工复健法的改进,它是一部联邦法律,这个法律要求所有技术要考虑到残障人士的应用,如果某个大众信息传播网站,如果某些用户群体(如残疾人)浏览该网站获取信息时,如果他们无法正常获得所期望的信息(如无法正常浏览),那可以依据相关法规,可以对该网站依法起诉)。 WAI-ARIA 为这方面的事提供很不错的资源.

安全

  • 在网上有很多关于安全的文章,但是 OWASP 开发指导 涵盖了几乎所有关于Web站点安全的东西。(陈皓注:OWASP(开放Web应用安全项目- Open Web Application Security Project)是一个开放的非营利性组织,目前全球有130个分会近万名会员,其主要目标是研议协助解决Web软体安全之标准、工具与技术文件,长期 致力于协助政府或企业了解并改善网页应用程式与网页服务的安全性。OWASP被视为Web应用安全领域的权威参考。2009年下列发布的美国国家和国际立法、标准、准则、委员会和行业实务守则参考引用了OWASP。美国联邦贸易委员会(FTC)强烈建议所有企业需遵循OWASP十大WEB弱点防护守则)
  • 永远不要相信用户的输入(包括Cookies,因为那也是用户的输入)
  • 对用户的口令进行Hash,并使用salt,以防止Rainbow 攻击(陈皓注:Hash算法可用MD5或SHA1等,对口令使用salt的意思是,user 在设定密码时,system 产生另外一个random string(salt)。在datbase 存的​​是与salt + passwd 产的md5sum 及salt。 当要验证密码时就把user 输入的string 加上使用者的salt,产生md5s​​um 来比对。 理论上用salt 可以大幅度让密码更难破解,相同的密码除非刚好salt 相同,最后​​存在database 上的内容是不一样的。google一下md5+salt你可以看到很多文章。关于Rainbow 攻击,其意思是很像密码字典表,但不同的是,Rainbow Table存的是已经被Hash过的密码了,而且其查找密码的速度更快,这样可以让攻击非常快)。使用慢一点的Hash算法来保存口令,如 bcrypt (被时间检证过了) 或是 scrypt (更强,但是也更新一些) (12)。你可以阅读一下 How To Safely Store A Password(陈皓注:酷壳以前曾介绍过bcrypt这个算法,这里,我更建议我们应该让用户输入比较强的口令,比如Apple ID注册的过程需要用户输入超过8位,需要有大小写和数字的口令,或是做出类似于这样的用户体验的东西)。
  • 使用 SSL/HTTPS 来加密传输登录页面或是任可有敏感信息的页面,比如信用卡号等。
  • 知道如何对付session 劫持。(陈皓注:请参看wikipedia的这Session Hijacking,)
  • 保持你的系统里的所有软件更新到最新的patch。
  • 确保你的数据库连接是安全的。
  • 确保你能了解最新的攻击技术,以及你系统的脆弱处。

性能

  • 优化页面 —— 不要使用20KB图片来平铺网页背景。(陈皓注:还有很多网页页面优化性的文章,你可以STFG – Search The Fucking Google一下。如果你要调试的话,你可以使用firebug或是chrome内置的开发人员的工具来查看网页装载的性能)
  • 学习如何 gzip/deflate 网页 (deflate 更好).
  • 把多个CSS文件和Javascript文件合并成一个,这样可以减少浏览器的网络连数,并且使用gzip压缩被反复用到的文件。
  • 为那些小的图片使用 CSS Image Sprites,就像工具条一样。 (参看 “最小化 HTTP 请求” ) (陈皓注:把所有的小图片合并成一个图片,然后用CSS把显示其中的一块,这样,这些小图片只用传输一次,酷壳的Wordpress样式的那个RSS订阅列表中的小图标就是这样做的)
  • 繁忙的网络应该考虑把网页的内容分开存放在不同的域名下。(陈皓注:比如有专门的图片服务器——图片相当耗带宽,或是专门的Ajax服务器)
  • 静态网页 (如,图片,CSS,JavaScript,以及一些不需要访问cookies的网页) 应该放在一个不使用cookies的独立的域名下,因为所有在同一个域名或子域名下的cookie会被这个域名下的请求一同发送。另一个好的选择是使用 Content Delivery Network (CDN).
  • 使用单个页面的HTTP请求数最小化。
  • 为Javascript使用 Google Closure Compiler 或是 其它压缩工具(陈皓注:压缩Javascript代码可以让你的页面减少网络传输从而可以得到很快的喧染。注意,并不是所有的工具都可以正确压缩Javascript的,Google的这个工具甚至还可以帮你优化你的代码)
  • 确认你的网站有一个 favicon.ico 文件放在网站的根下,如 /favicon.ico浏览器会自动请求这个文件,就算这个图标文件没有在你的网页中明显说明,浏览器也会请求。如果你没有这个文件,就会出大量的404错误,这会消耗你的服务器带宽。(陈皓注:服务器返回404页面会比这个ico文件可能还大)

SEO (搜索引擎优化)

  • 使用 “搜索引擎喜欢的” URL,如:使用 example.com/pages/45-article-title 而不是 example.com/index.php?page=45 (陈皓注:这里的URL是说Wordpress的,后者是默认的)
  • 如果你的动态页面要使用 # ,那么请把其改成 #! ,而在服务端,你需要处理$_REQUEST["_escaped_fragment_"] 这是Google搜索引擎需要的。换句话说,./#!page=1 会被Google搜索引擎转成 ./?_escaped_fragments_=page=1。 (陈皓注:通常来说URL中的#后的东西都不会被传到服务器上,所以,为了要让Google可以抓取AJAX的东西,你需要使用#!,而Google会把“#!”转成“_escaped_fragment_”来向服务器发请求,Twitter的大量的链接者是#!的,比如:https://twitter.com/#!/your_activity)。另外,用户也许会使用Firefox 或 Chromium, history.pushState({"foo":"bar"}, "About", "./?page=1"); 是一个很不错的命令。所以,就算是我们的地址栏上的地址改变了,页面也不会重新装载。这可以让你使用 ? 而不是 #! 也能无刷地保住当前的动态的页面,这可以让AJAX的请求被浏览器记住。
  • 别使用 “click here” 这样的链接。这样一来,无法SEO,而且对于一些需要使用读屏人来说很不友好(陈皓注:关于读屏软件,可参看本站的“如果看不见你还能编程吗”)
  • 了解 robots.txt 和搜索引擎爬虫是如何工作的。
  • 重定向请求 (使用 301 重定向网站) ,如果你要把 www.example.com 定向到 example.com(或是其它的变更) 这样可以防止Google的rank因为域名的变化发生改变。(陈皓注:301重定向一般用作域名变更)
  • 知道并不是所有的爬虫都是好的,有些爬虫的行为并不好。(陈皓注:比如向你的网站发大量的请求导致服务器性能低下)
  • 如果你有一些非文本的内容需要在 Google’s sitemap  中,比如视频什么的。Tim Farley的答案,可以让你看到很多有价值的东西。

技术

  • 理解什么是 HTTP 比如 GET, POST, sessions, cookies等,了解什么是 “stateless” 无状态。
  • 让你的 XHTML/HTML 和 CSS 符合 W3C 规范,并确认他们都是 合格的。我们的目标是避免浏览器的 “quirks mode”,并且可以让其更容易地能和非标准的浏览器工作,比如读屏器或移动设备。
  • 理解浏览器是怎么处理 JavaScript 的。(陈皓:你会看到有些Javascript代码在页面上前面,有些则是在后面,所以你需要对其了解清楚为什么是这样)
  • 了解浏览器是怎么装载 JavaScript,CSS和其它资源的,了解其对视觉上的影响。(陈皓注:10年前我做网页的时候因为HTML还很弱,所以只能使用table来布局,使用table布局的问题就是整个table读完后页面才会显示,用户的视觉体验并不好)。在某些情况下,你可能需要把你的脚本放在页面的后面
  • 理解 JavaScript 的 sandbox 是怎么怎么工作的,尤其是你想使用iframes。
  • 请注意 JavaScript 可能会被禁止,这样会让你的AJAX失效。就算是大多数用户都开启了Javascript功能,但是也可能在一些情况下脚本是不被运行的,比如移动终端上,搜索引擎抓网页的时候也并不会执行你的脚本。
  • 尽可能多地学习你的部署平台。(比如:操作系统,Web Server:Apache/Nginx,防火墙,数据库,等等)
  • 把视觉效果和JS框架合在一起讨论,考虑使用一个Service,如:Google Libraries API 来装载框架,这样可以让浏览器可能早就把这个JS框架已经cache了而不需要再从你的网站上下载了。

Bug fixing

  • 明白你会花20%的时间写代码,而80%的时候在维护,所以你要小心编码。(陈皓注:参看本站的“多些时间可以少些代码”一文)
  • 设计一个好的错误报告机制。
  • 设计一个入口可以让人们联系到你并给你建议和批评。
  • 为你开发的东西形成文档,这样可以让后来的人容易维护你的软件和系统。
  • 频繁备份(也可确保你的这些备份功能正常) Ed Lucas 的回答 有一些忠告。你还需要有一个恢复策略,而不只是一个备份策略。
  • 使用一个版本控制系统来保存你的代码,如: Subversion 或 Git.
  • 别忘了做Acceptance Testing,使用 Selenium 能帮到你。
  • 确保你有足够的日志,你可以使用 log4j, log4n 或 log4r。如果出了问题,这是可以让你快速找到问题的方式。
  • 当你写日志的时候,确保你记录了你捕获了处理和未处理异常。报告和分析日志可以让你知道你网站的问题。

这里有多的东西被省略了,并不是因为那些可能不是有帮助的答案,而是因为那些东西都太细节了,超出了这个问题的范围,因为这本来就是一个Web开发需要了解东西的Overview。我想你可以去看一下其它人的答案,我有时间,我也会补充别人的答案进来。请随意编辑这个答案,因为可能有些东西忘了,也有可能有些东西不对。

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post Web开发中需要了解的东西 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/6043.html/feed 150
一些文章资源和趣闻 https://coolshell.cn/articles/5537.html https://coolshell.cn/articles/5537.html#comments Thu, 24 Nov 2011 04:39:04 +0000 http://coolshell.cn/?p=5537 下面是我这段时间来收集的一些有意思的东西。本站这样的文章还很多,如这个,这个,这个。 Javascript Garden,这是学习Javascript最好的网站...

Read More Read More

The post 一些文章资源和趣闻 first appeared on 酷 壳 - CoolShell.]]>
下面是我这段时间来收集的一些有意思的东西。本站这样的文章还很多,如这个这个这个

Javascript Garden,这是学习Javascript最好的网站了。http://bonsaiden.github.com/JavaScript-Garden,这个文档由两具StackOverflow的人写成, Ivo Wetzel(Writing) 和 Zhang Yi Jiang (Design),表示敬意。

想看看Web开发有哪些技术吗?你得看看这个网站:http://stackparts.com/,他对目前几乎所有Web上用得到的技术都分了个类。下面是个抓图。

Mozilla的安全编程规范 https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines Downloads associated to Software development

PHP,Perl, Ruby, Python语法比较http://hyperpolyglot.org/scripting?utm_source

图形游戏编程的电子书 http://ploobs.com.br/?p=766

图形编程黑皮书:http://drdobbs.com/high-performance-computing/184404919

Black Art of 3D Game Programming: Writing Your Own High-Speed 3D Polygon Video Games in C 

想学设计模式吗?这是一个非常好的网站:http://www.vincehuston.org/dp/ 以元素周期表的形式把23个经典模式列出来,让我想到了这几天在看的美剧Breaking Bad,呵呵。

Learn C the Hard Way http://c.learncodethehardway.org/book/

Learn Ruby the Hard Way http://ruby.learncodethehardway.org/book/

Learn Python the Hard Way http://learnpythonthehardway.org/

Learn SQL the Hard Way http://sql.learncodethehardway.org/book/

Linux基础学习:

相了解GIF吗?这里有篇不错的文章:http://matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
如何在PC上通过VirtualBox装一个Android操作系统。 http://www.javacodegeeks.com/2010/06/install-android-os-on-pc-with.html 不过这篇文章有点老了,是去年的,最新的方式是使用Android-x86这个项目。

一些你可能不知道的git的tips:http://mislav.uniqpath.com/2010/07/git-tips/

一个给C/C++程序员用的Vim Plugin,我试用了一下,不是很好用。不过也许你会喜欢:http://www.fortystones.com/vim-plugins-c-cplusplus-developer/

数独游戏的程序算法,140个字节的一段javascript程序: https://gist.github.com/1230481/95f6facb74f51d089bea87eba0f470cf3bbed83a

一个教你用HTML5做一个画图版的教程:http://www.primaryobjects.com/CMS/Article134.aspx 其示例在这里 http://www.primaryobjects.com/paint/

这里又是一个HTML5的演示 http://www.spielzeugz.de/html5/sticky-thing/,一个物理的会粘在浏览器边框上的小方块,在iPad里演示相当有意思。只是其代码好像被搞得非常地不易读,不过,你可以试试这个工具来整理代码:http://jsbeautifier.org/,但是变量命名还是会让你毫无头绪。

HTML5的一个很炫的示例:http://lights.elliegoulding.com/  你可以用鼠标巡航,点左键加速(另,那位朋友知道其背景音乐?)

想用HTML5做股票图吗?看看这个库:http://www.rocketcharts.com/

一个7K的js,可以让你的HTML的列表很灵活的分类,排序,搜索,过滤:http://listjs.com/

一个OOP的PHP处理图片的类库:http://imagine.readthedocs.org

一个Javascript实现的H.264解码器。https://github.com/mbebenita/Broadway (演示地址 – 请用firefox打开,download速度可能很慢)不过,其是用Android C实现的,然后把C转成Javascript的代码。如果你想知道如何把C代码转成Javascript,你可以看看这个项目:https://github.com/kripken/emscripten – LLVM-to-JavaScript compiler。(变态!)

一个可以画流程图的Javascript lib – WireIt:http://neyric.github.com/wireit/

这是一个网站,仅用CSS,没有JS,没有图片做的N多公司的logo。但其可以用纯CSS做个动画,你可以看看:http://www.ecsspert.com/atari.php 研究了一下发现其用到了  -webkit-animation

一个用bash处理JSON的脚本:https://github.com/rcrowley/json.sh

微软VS中的Debug Canvas,相当的不错啊。http://msdn.microsoft.com/en-us/devlabs/debuggercanvas,可惜只在 Visual Studio Ultimate里。

介绍一下很有意思的Firefox插件 Titl 3D,其项目主页在 https://github.com/victorporof/Tilt。这个插件使用WebGL可以3D地显示网页,安装好插件后,简单地按一下Ctrl+Shift+M就可以了。下面我用其显示了新浪微博和WebQQ。目前的功能不是很多,但是这个插件简直是太cool了——可以大胆的设想一下以后会不会有3D的网页。

最后,在网上看到一个笑话,如下:

这是给程序员们女朋友的建议。如果某程序员要和你分手,你可以参照这位国外程序员女友的作法——“你可以在facebook和twitter上拉黑我,也可以不回我的短信,但是,你永远不可能阻止我对你在Reddit上发的所有的贴投反对票!FUCK YOU !”

就这些,希望对你会喜欢。

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

The post 一些文章资源和趣闻 first appeared on 酷 壳 - CoolShell.]]>
https://coolshell.cn/articles/5537.html/feed 81