上篇文章介绍的是系统环境的配置,在这一篇中将会介绍代码编辑器 Sublime Text 3 的简单配置和实用插件。
Sublime Text 是一款跨平台代码编辑器(Not IDE),拥有非常丰富成熟的插件,辅以大量的快捷键,足使开发效率成倍提升。
另外,GitHub 在去年开源了一款叫 Atom 的代码编辑器,基本和 Sublime Text 无异。不过是基于 Node.js 和 Chromium,这个会在下一篇进行介绍。Atom 也是我目前的主力编辑器。
目前最新版本为 Sublime Text 3,在官方网站提供了各个平台的下载。推荐使用此版本,有部分插件只支持 Sublime Text 3。
安装好 Sublime 之后,通过安装 Package Control 来管理其大量的插件。
ctrl + `
打开控制台1 | import urllib.request,os,hashlib; h = 'eb2297e1a458f27d836c04bb0cbaf282' + 'd0e7a3098092775ccb37ca9d6b2e4b7d'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); print('Error validating download (got %s instead of %s), please try manual install' % (dh, h)) if dh != h else open(os.path.join( ipp, pf), 'wb' ).write(by) |
待 Package Control 安装完成之后,用 cmd + shift + p
打开命令面板输入 PC
即可看到和 Package Control 相关的选项。紧接着就可以安装(管理)各种插件了。
Sublime Text 并不提供专门的配置界面,而是通过 JSON 文件来放置各种配置信息。而每种配置信息都分为 Default
和 User
两个版本,即 Default
为默认配置,因此不建议直接修改 Default
配置文件,而是在 User
文件中放置自己的个性化配置。
比如我的部分配置:1
2
3
4
5
6
7
8
9
10
11{
"font_size": 14,
"font_face": "Monaco",
"theme": "Spacegray.sublime-theme",
"color_scheme": "Packages/User/SublimeLinter/base16-ocean.dark (SL).tmTheme",
"tab_size": 2,
"translate_tabs_to_spaces": true,
"trim_automatic_white_space": true,
"trim_trailing_white_space_on_save": true,
"ensure_newline_at_eof_on_save": true
}
sudo npm i -g jscs
sudo npm i -g jshint
sudo npm i -g csslint
sudo npm i -g jsxhint
上篇文章介绍的是系统环境的配置,在这一篇中将会介绍代码编辑器 Sublime Text 3 的简单配置和实用插件。
Sublime Text 是一款跨平台代码编辑器(Not IDE),拥有非常丰富成熟的插件,辅以大量的快捷键,足使开发效率成倍提升。
另外,GitHub 在去年开源了一款叫 Atom 的代码编辑器,基本和 Sublime Text 无异。不过是基于 Node.js 和 Chromium,这个会在下一篇进行介绍。Atom 也是我目前的主力编辑器。
]]>工欲善其事,必先利其器。
目前可供开发人员使用的工具越来越多,合理的使用工具有助于提升开发效率,但种类繁多时选择起来却会让人感觉眼花缭乱。所以我准备在接下来的几篇文章中分享一些我的 Mac 开发环境配置,供大家参考。
如果是一台全新的 Mac 电脑,那第一件事情就是去安装 XCode 吧。然后在终端中用如下命令安装 Xcode command line tools
,按照指引安装即可。
1 | $ xcode-select --install |
如果你不做 Obj-C 开发,可以跳过 XCode 的安装,直接去下载 Xcode command line tools
来安装即可。
Homebrew 是 Mac 上最受欢迎的包管理工具。
在此之前,必须保证 Xcode command line tools
。然后在终端中用如下命令按照指引来安装 Homebrew 。
1 | $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" |
安装完成之后将 Homebrew 的可执行命令添加到环境变量中。1
$ echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bash_profile
1 | $ brew install <package_name> # 安装包 |
如果你不喜欢命令行方式来管理,那么 Cakebrew 是极好的选择。Cakebrew App 提供了可视化的界面来接管一部分 brew 命令,大多数操作都可以直接在界面上点几下来完成。
替换掉 OS X 自带的 Terminal,不是因为自带的很差劲,而是一款强劲的终端程序可以有效的提升效率(即便是少敲点字符也行)。
Zsh 号称终极 Shell,所以愉快的干掉自带的 Bash 吧(自带的又躺了😂)… Zsh 提供了强大的自动补全功能,能自动补全命令、参数、文件名、进程等。
而 oh-my-zsh 是基于 Zsh 的功能做了一个扩展,提供了方便的插件管理、主题自定义,以及漂亮的自动完成效果。
1 | $ curl -L https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh | sh |
在 ~/.zshrc
中按照个人喜好配置主题(ZSH_THEME
),以及各种插件(plugins
)。主题太多,任君选择。另外,以下是我的插件启用情况。
1 | plugins=(git github git-flow git-extras brew osx node npm copydir copyfile cp sublime zsh-syntax-highlighting) |
我们是前卫的前端工程师,不用 Git 怎么好意思和别人打招呼呢。。。😂
1 | $ git --version # 如果提示命令不存在,那用 Homebrew 来安装吧 |
然后配置一些全局的信息。
1 | $ git config --global user.name "Your Name" |
如果使用 SSH 方式来拉取/推送代码,请参考这里。而 HTTPS 方式如果不想每次都输入你的 Git 用户名和密码的话,请继续按照以下配置。
1 | $ git config --global credential.helper osxkeychain |
我们是前卫的前端工程师,没用过 Node.js 肯定也听过 Node.js 吧,否则真的是不好意思和别人打招呼的😂。。。少年,赶紧去官网下载安装吧~
Node 的包管理系统。安装好 Node 之后自带,不过在某些 Node 版本中自带的 NPM 版本较低,你需要执行下面命令来升级。
1 | $ sudo npm up npm -g # 用 NPM 升级 NPM,感觉很酷对吧? |
另外,推荐安装以下包:1
2
3$ sudo npm i gulp -g # 前端构建工具
$ sudo npm i grunt-cli -g # 另一个前端构建工具
$ sudo npm i bower -g # 前端包管理工具(为毛又一个包管理工具?不要在意这些细节)
这一小节算是赠送啦,反正你都看到这里了。。。
Golang 是谷歌开发的一门现代编程语言,强类型、并发、高性能、语言特性简洁高效… 恩,另外,Node 社区的 TJ 大神目前已转投此阵营…
安装就不用说了,上官网下载即可。如果你没办法越过 GFW,那去这里下载吧。
我一般会为 Go 配置两个 GOPATH
,/usr/local/share/go
用于安装第三方包,~/codes/go
用于放置自己项目代码(工作目录)。
1 | $ echo "export GOROOT=/usr/local/go" >> ~/.zshrc |
工欲善其事,必先利其器。
目前可供开发人员使用的工具越来越多,合理的使用工具有助于提升开发效率,但种类繁多时选择起来却会让人感觉眼花缭乱。所以我准备在接下来的几篇文章中分享一些我的 Mac 开发环境配置,供大家参考。
]]>Gruntfile.js
,但都不是很理想,直到前段时间看到load-grunt-tasks这个插件以及More maintainable Gruntfiles这篇文章后,我就把项目中的Gruntfile.js
都按照该文章作者所述的方式重新组织了一遍。
我就暂且把这种方式用自己的文字记录一下并分享给正在使用Grunt
的同学们吧,不过本文也不算是对《More maintainable Gruntfiles》的翻译呐,毕竟我E文太差~
首先介绍下load-grunt-tasks
这个插件。
我们一般都会把所有用到的插件以及插件的配置写到Gruntfile.js
里面,对于小项目来说这个文件最终或许不是很大,但是对于大项目、有很多配置或者很多自定义任务的项目来说,最后这个文件都会变得越来越长,维护起来就成了麻烦。比如下面这样:
1 | module.exports = function(grunt) { |
这是一个很标准的Gruntfile.js
,显然也算是很简短的了,但是看起来也有点累觉不爱。于是load-grunt-tasks
出来帮我们解决了一部分问题。
它会自动读取并加载项目packge.json
文件中devDependencies
配置下以grunt-*
开头的依赖库。于是乎我们就可以用一行代码来搞定上面代码中很多行的loadNpmTasks
了。
1 | require('load-grunt-tasks')(grunt); |
load-grunt-tasks
插件替Gruntfile.js
省去了那些反复书写的方法调用,接下来就是将整个Gruntfile.js
变得干净清爽的步骤了。那就是把上面的各种config
分离出去,让它们各自代表自己是属于哪个插件,而不是一口气全写在一起。当然,还有各种用registerTask
方法定义的自定义任务,也该单独放到相应的文件中。
首先,在项目根目录下建一个名为tasks
的目录,在这个目录下来编写各种自定义任务。可以一个任务一个 js 文件,也可以多个简单任务在一个 js 文件,看个人喜好吧。然后在Gruntfile.js
中用一行代码来载入这些自定义任务:
1 | grunt.loadTasks('tasks'); // 即刚刚新建目录的名称 |
然后再在这个目录下新建一个名为options
的子目录(tasks/options),来存放之前说的那些config
们。为每一类config
建一个 js 文件,并以配置项节点名作为文件名称,比如下面这样:
1 | tasks |
然后在每个文件中导出对应的配置项,拿concat.js
来说:
1 | module.exports = exports = { |
最后在Gruntfile.js
里用require
将配置逐个引入即可,也可以封装一个函数来做这件事情。
1 | function loadConfig(configPath) { |
再改写Gruntfile.js
中initConfig
的调用即可。
1 | var _ = require('lodash'); |
于是乎在每个项目中Gruntfile.js
几乎一致,而且也几乎不会再变更。Gruntfile.js
、自定义任务、任务配置项各司其职,需要变化时只需对相应文件做出调整即可。
就在前些天,又一位 GitHuber 将这个思路封装成了一个库:load-grunt-config,感兴趣的同学可以看看。
最终的Gruntfile.js
可以查看这个例子:https://github.com/heroicyang/cnodeclub/blob/master/Gruntfile.js
load-grunt-tasks: https://npmjs.org/package/load-grunt-tasks
More maintainable Gruntfiles: http://www.thomasboyt.com/2013/09/01/maintainable-grunt.html
Gruntfile.js
,但都不是很理想,直到前段时间看到load-grunt-tasks这个插件以及More maintainable Gruntfiles这篇文章后,我就把项目中的Gruntfile.js
都按照该文章作者所述的方式重新组织了一遍。
我就暂且把这种方式用自己的文字记录一下并分享给正在使用Grunt
的同学们吧,不过本文也不算是对《More maintainable Gruntfiles》的翻译呐,毕竟我E文太差~
1 | var count = 0; |
闭包,维基百科的解释是:指引用了自由变量的函数。而我个人认为前端大牛johnhax的解释更加容易理解:闭包就是内部函数能访问外部的变量。
所以,要理解闭包,只要理清楚变量作用域这个概念就差不多了。我也把对变量作用域的一些个人理解记录在了前两篇文章中,故这里就只简单说说一个函数它可以访问哪些作用域中的变量:
而对于第三点,就是闭包的行为了,用一个简单的例子来说明。
1 | function foo() { |
简单解读下这段代码:
bar
函数是内部函数,即定义在foo
函数内部foo
函数调用之后会将bar
函数的引用作为返回值bar
函数,即bar
函数还处于活动状态于是在foo
函数已经被调用结束之后,其内部的i
变量仍然没被销毁,而且在每次调用bar
函数之后,其值是一直在递增的。
如果用作用域链来解释,那就更加清晰明了了:
1.代码载入全局执行环境,开始运行。
1 | globalScopeChain = { |
2.调用foo
函数。
1 | // 首先,`foo`函数的作用域链变为: |
3.foo
函数调用结束,并将test
变量指向foo
执行返回的bar
函数的引用。
1 | // 此时全局作用域链变为: |
4.第一次调用test
,也就是bar
函数。bar
函数作用域链上有变量i
,且值为0
。
1 | // 作用域上进行变量查找,找到后做增量赋值操作,所以`i = 1` |
5.第二次调用test
,与第4步相同。只是本次调用时,其作用域链上的i
值已经在上一次调用后变成1
了,所以增量赋值操作之后,得到结果为2
。
因此,只要理解了作用域链,再来看闭包就很清晰了。不过对于闭包的解释,我仍然推荐开篇所提到的:内部函数能访问外部的变量,简单易懂。
]]>1 | var count = 0; |
闭包,维基百科的解释是:指引用了自由变量的函数。而我个人认为前端大牛johnhax的解释更加容易理解:闭包就是内部函数能访问外部的变量。
所以,要理解闭包,只要理清楚变量作用域这个概念就差不多了。我也把对变量作用域的一些个人理解记录在了前两篇文章中,故这里就只简单说说一个函数它可以访问哪些作用域中的变量:
而对于第三点,就是闭包的行为了,用一个简单的例子来说明。
]]>var
关键字声明的变量会成为全局变量接下来根据上篇留下的最后一段代码,继续谈谈变量作用域。
1 | var name = 'global'; |
这段代码最终会在控制台打印出'global'
,而并非'foo'
。可以看出,函数运行时能访问到的作用域是它被定义时的作用域,不是被调用时的作用域。
每当谈及JavaScript作用域的时候,基本上都会提到“词法作用域”、“执行环境”、“活动对象”、“作用域链”这几个概念,而了解这些概念将有助于理解JavaScript中的闭包。我也谈谈我对此的理解,如误欢迎指正,不胜感激。
词法作用域,也称静态作用域,也即是说函数的作用域是定义时决定的,而非运行时。最开始的代码就阐明了这一点,所以在源码时期就可以通过分析得出一个函数的作用域。JavaScript正是基于词法作用域的语言。
JavaScript需要一个环境来运行,比如客户端的浏览器和服务端的Node.js。而执行环境有分为全局执行环境和局部执行环境。
在浏览器中,全局执行环境为window
对象。因此当JavaScript代码运行时,所有的全局变量和函数都作为了window
对象的属性和方法创建。全局执行环境关联了一个作用域链,包含了全局执行环境中的变量对象。
每个函数在定义时,都会关联一个初始的作用域链,将当前执行环境作用域链上的变量对象附加到这个关联的作用域链上。每个函数都是有自己的执行环境的,也就是局部执行环境。当一个函数被调用,进入局部执行环境。随之创建了活动对象,包含arguments
和局部变量,然后将其附加到作用域链的前端(即下标为0)。在函数执行之后,退出局部执行环境,把控制权交给之前的执行环境(可能是全局执行环境,也有可能是另一个局部执行环境)。
前面已经反复提到了作用域链,也基本解释了作用域链,它差不多等同于一个对象列表或链表。为了更好的理解作用域链,还是结合最开始的那段代码来进行解释,不然有点不是很好理清楚。
1 | var name = 'global'; |
1.代码运行开始,在全局执行环境中,全局环境关联了一个作用域链,变量对象包含name
、foo
、bar
。然后foo
函数和bar
函数也各自关联了自己作用域链。
1 | globalScopeChain = { |
2.调用foo
函数,进入foo
函数的局部执行环境,创建活动对象包含arguments
、name
,并将活动对象添加到作用域链的前端。
1 | fooScopeChain = [ |
3.调用bar
函数,进入bar
函数的局部执行环境,创建活动对象包含arguments
,然后也将活动对象添加到作用域链的前端。
1 | barScopeChain = [ |
而变量的查找,就是在整个作用域链上进行,从第一个对象开始,直到作用域链的顶端(即全局)。这也就是实例代码中打印出'global'
的原因。
var
关键字声明的变量会成为全局变量接下来根据上篇留下的最后一段代码,继续谈谈变量作用域。
1 | var name = 'global'; |
这段代码最终会在控制台打印出'global'
,而并非'foo'
。可以看出,函数运行时能访问到的作用域是它被定义时的作用域,不是被调用时的作用域。
每当谈及JavaScript作用域的时候,基本上都会提到“词法作用域”、“执行环境”、“活动对象”、“作用域链”这几个概念,而了解这些概念将有助于理解JavaScript中的闭包。我也谈谈我对此的理解,如误欢迎指正,不胜感激。
]]>首先来看看下面的代码:
1 | for (var i = 0; i < 10; i++) { |
从运行结果不难看出,变量i
在循环体结束之后仍然可以访问。而诸如Java、C#等语言中,循环结束之后便不能再访问到循环体中的变量了。继续看下面代码:
1 | var name = 'heroic'; |
结合两段代码可以知道,在JavaScript中是不存在块级作用域的,只存在函数作用域(也称“本地作用域”)和全局作用域。而作用域中的变量我们分别称其为:局部变量和全局变量。
前辈们(前端的长辈们,嚯嚯)常常反复在说,全局变量是魔鬼啊,魔鬼啊。。。(回声)那到底是嘛原因呢?咱接着往下说。
var
关键字来声明变量,则该变量会成为全局变量。 1 | function foo() { |
上面这个例子很好理解,由于在foo
中没有使用var
关键字来声明name
变量,所以可以在整个全局作用域中访问并修改该变量的值,在实际项目中必然会混乱不堪,引入不可控的BUG等等。接下来看看这个可能对于初学者来说有点迷糊的例子。
1 | var name = 'heroic'; |
这就是前面提到的声明提前,JavaScript中变量和函数以及函数的参数的申明都是在一个类似于预编译的时期就做了,而在运行时期才是创建变量赋值表达式和函数表达式等。所以上面的代码等同于:
1 | var name = 'heroic'; |
首先来看看下面的代码:
1 | for (var i = 0; i < 10; i++) { |
从运行结果不难看出,变量i
在循环体结束之后仍然可以访问。而诸如Java、C#等语言中,循环结束之后便不能再访问到循环体中的变量了。继续看下面代码:
1 | var name = 'heroic'; |
结合两段代码可以知道,在JavaScript中是不存在块级作用域的,只存在函数作用域(也称“本地作用域”)和全局作用域。而作用域中的变量我们分别称其为:局部变量和全局变量。
前辈们(前端的长辈们,嚯嚯)常常反复在说,全局变量是魔鬼啊,魔鬼啊。。。(回声)那到底是嘛原因呢?咱接着往下说。
]]>记得此前书中对ES5 bind()
方法的实现就是一个让我感到迷糊的地方(中文版P16):
1 | if (!Function.prototype.bind) { |
bind()
:用来动态改变函数调用时其内部this
对象的引用,使目标函数基于正确的上下文进行调用。jQuery中实现为$.proxy()
方法,ES5则自带了。
上面代码中① ② ③都是我此前不明白的地方,不过在这次阅读的过程中,就豁然开朗了。
让我来实现这个方法的话,或许应该是这样子的:
1 | // 取名为`bind1`好了,方便后面对比 |
也就是不会引入nop
这个空函数,不会做intanceof
判断,不会有原型设置。当然,这个实现是没有啥大问题的,不就是改变函数调用时的上下文环境么,貌似妥妥的呢。
但是现在仔细想想,这个实现是没错,但不完善。因为JavaScript中一个函数除了可以做普通的函数调用以外,还充当了构造函数的作用,所以就可能会有下面这样的代码。
1 | var obj = { |
如此可见,在实现bind()
方法的时候,除了考虑一般的函数调用以外,还得考虑其结合new
关键字使用时的构造函数的特性。因此以我在第一次看这本书时的认知,看不明白也就不奇怪了,嚯嚯。。
那就来继续解最开始的那三个圈住的迷惑:
var nop = function () {}
:这并是一个普通的函数,而是一个构造函数,把它用作返回的bound
函数的原型,以便于代码中做instanceof
判断。instanceof
判断就无可厚非了。bind()
方法时的上下文对象的原型,即目标函数的原型。然后将临时构造函数的一个实例设置为bound
函数的原型,原型就链起来了。这样,在对bind后的函数进行new
关键字操作的时候,就可以获得原目标函数的特性了。关于继承和原型,可以参见我的前几篇文章。
最后,再补一个例子吧:
1 | var Person = function (name) { |
记得此前书中对ES5 bind()
方法的实现就是一个让我感到迷糊的地方(中文版P16):
1 | if (!Function.prototype.bind) { |
bind()
:用来动态改变函数调用时其内部this
对象的引用,使目标函数基于正确的上下文进行调用。jQuery中实现为$.proxy()
方法,ES5则自带了。
上面代码中① ② ③都是我此前不明白的地方,不过在这次阅读的过程中,就豁然开朗了。
]]>0
到记录总数之间的随机数,然后采用skip(yourRandomNumber)
方法。random
字段,插入数据时赋值为Math.random()
,查询时采用$gte
和$lte
。geospatial indexes
)的支持,从而可以在第二种方法的基础上来实现随机记录的获取。因为Mongodb是不建议使用skip
方法的,所以这里就略去第一种方法吧。
1 | > db.twitter.save({ username: 'heroic', random: Math.random(), content: 'balabala0...' }) |
1 | > db.twitter.save({ username: 'heroic', random: [Math.random(), 0], content: 'balabala0...' }) |
更多关于Mongodb地理空间索引资料,请参见这里。
目前这几种方案似乎都不是很理想,但是也没有其他办法了,所以广大程序员们就相约到Mongodb的官方jira提了相应的需求,但是目前仍然没有任何的响应。可以参见这里,围观一下。
]]>0
到记录总数之间的随机数,然后采用skip(yourRandomN]]>
1 | var Parent = function (name) { |
正如代码所见,在为子对象原型添加自己独有方法的时候,父对象也受到了影响,这可不是期望的结果。
1 | var child = new Child('child'); |
同样如代码所示,child
对象持有了两个name
属性,一个是通过构造函数拷贝的,另一个是原型链上的,当删除掉本身的name
属性后,便访问到了原型链上的了。对于这个问题,解决方案很简单直接:
1 | /* ... */ |
但是这种方法也并没有解决最开始的那个问题,即添加或删除子对象原型上的属性时,会一并反映到父对象中。这个时候就需要用到《关于JavaScript中的继承(二):原型式继承》中用到的临时构造函数了。
1 | var inherit = function (subClass, superClass) { |
不再影响父对象的行为了,而且还可以为inherit
方法增加子对象访问父对象行为的特性。
1 | var inherit = function (subClass, superClass) { |
最后,如果说这个类式继承模式还有哪点不够完美的话,那就是在子对象继承父对象之后,子对象的构造函数指向被改写了。
1 | console.log(child.constructor === Parent); // true |
没办法,只有在继承的最后,把constructor
修正回来就是。
1 | var inherit = function () { |
同时,也如《关于JavaScript中的继承(二):原型式继承》中提到的那样,利用闭包来减少每次调用inherit()
都会生成一个临时构造函数的开销。
写在最后,类式继承为我们带来了JavaScript中不存在的完整的类的概念,这对于从面向对象语言转过来的程序员来说,可能是很好的方式。但是它也有可能让我们忽略了JavaScript真正的原型式继承。不过这些模式都没有好与坏之分,应该在适合的场景使用合适的方法才是。
]]>1 | var Parent = function (name) { |
正如代码所见,在为子对象原型添加自己独有方法的时候,父对象也受到了影响,这可不是期望的结果。
1 | var child = new Child('child'); |
同样如代码所示,child
对象持有了两个name
属性,一个是通过构造函数拷贝的,另一个是原型链上的,当删除掉本身的name
属性后,便访问到了原型链上的了。对于这个问题,解决方案很简单直接:
1 | /* ... */ |
但是这种方法也并没有解决最开始的那个问题,即添加或删除子对象原型上的属性时,会一并反映到父对象中。这个时候就需要用到《关于JavaScript中的继承(二):原型式继承》中用到的临时构造函数了。
]]>1 | var parent = { |
上面的代码目前还不能工作,上一篇文章中谈到过JavaScript中对象属性查找机制,如果child
对象本身没有printName()
这个方法,那我们保证其原型上有这个方法,它就能正常工作了。
1 | /* var parent... */ |
通过一个object()
函数,我们就完全可以实现一个对象从另一个对象继承了。其原理很简单,即在object()
函数内部创建一个临时的构造函数,然后修改这个构造函数的原型,最后再返回这个构造函数的一个实例。
当然,parent
对象也并不一定要使用对象字面量,你可以选择任何你能想到的创建对象的方式,如构造函数等。
1 | function Parent () { |
正如前面所述,我们可能并不想继承原对象自己的属性,如上面的例子我们也并不希望它打印出parent
。
原型式继承的规则就是:对象从对象继承,不管父对象是如何而来。而Parent.prototype
也是一个对象,所以
1 | var child = object(Parent.prototype); |
改写之后,打印child
自己的名字时得到的是undefined
,因为创建child
对象后,还没有给它赋予任何的属性,所以这恰是我们想要的结果。
而在ECMAScript 5
中,原型式继承已经成为了语言特性,为我们增加了Object.create()
方法,也就是说可以不用自己实现上面的object
方法了。而且Object.create()
方法更加强劲、适用。
1 | var parent = { |
最后,再谈谈最开始实现的object()
方法吧,其实还可以做一点点简单的优化。那就是每次调用object()
方法时都要创建一个临时的代理构造函数F
,而事实上仅需要创建一次就足够了。
1 | var object = function () { |
我们把代理构造函数放到一个立即执行的函数中创建,然后这个函数返回一个新的、真正实现继承逻辑的函数。
]]>1 | var parent = { |
上面的代码目前还不能工作,上一篇文章中谈到过JavaScript中对象属性查找机制,如果child
对象本身没有printName()
这个方法,那我们保证其原型上有这个方法,它就能正常工作了。
1 | /* var parent... */ |
通过一个object()
函数,我们就完全可以实现一个对象从另一个对象继承了。其原理很简单,即在object()
函数内部创建一个临时的构造函数,然后修改这个构造函数的原型,最后再返回这个构造函数的一个实例。
虽然JavaScript并不是一门真正的面向对象语言,甚至连类的概念都没有。但得益于构造器的存在,在JavaScript中是可以完全模拟出 类-对象 行为的。如:
1 | var person = new Person(); |
看上去除了变量声明时不是强类型之外,完全与面向对象如出一辙。所以谈及继承时,大家首推的也是一种叫“类式继承”的手法了。
1 | var Parent = function (name) { |
上面的伪代码是类式继承的理想状态,但inherit
方法并不存在,需要由自己实现。
1 | var inherit = function (subClass, superClass) { |
和计划中的一样,子对象不仅继承了父对象的属性,也继承了父对象原型上的方法。
1 | var child = new Child(); |
但是这种方式却存在一些问题:
undefined
。由于存在这种读写的不对等性,我们都不会采取从父对象继承属性,而是直接为子对象添加属性即可,而需要继承的方法则放到原型上。var child = new Child('test')
利用原型链实现的类式继承先放一边,为了解决在初始化就能传入参数的问题,便产生了一种叫“借用构造函数”方式的继承。
1 | var Parent = function (name) { |
首先来谈谈这种机制相对于第一种的优点,Talk is cheap, Show me the code.
1 | var Parent = function () { |
显而易见,借用构造函数方式在继承时是采取一份单独的拷贝,而原型链方式则是指向同一个引用。(但是由此可见,原型链上的属性或方法不会在每个实例中都创建一次。)
接下来则是谈谈缺陷了。
1 | var Parent = function () {}; |
借用构造函数其实是在构造时,通过改写方法调用上下文来实现属性的拷贝,所以并未涉及到prototype
,所以就没有办法继承原型了。
由于原型链上的属性或方法不会在每个实例中都创建一次,所以是我们放置需要重用的属性和方法的理想地方;而借用构造函数则可以使子对象拥有自己一份独立的拷贝,不存在意外改写父对象属性的风险。所以两者互补产生了第三种比较完美的继承方式。
1 | var Parent = function (name) { |
近乎完美的实现,子对象继承了父对象的成员,但拥有自己的一份拷贝,不会担心修改自己而影响到父对象;子对象也复用了父对象原型中的方法;且子对象也可以传递任意参数给父对象的构造函数。可谓是面向对象中“类式继承”的准确诠释。
]]>虽然JavaScript并不是一门真正的面向对象语言,甚至连类的概念都没有。但得益于构造器的存在,在JavaScript中是可以完全模拟出 类-对象 行为的。如:
1 | var person = new Person(); |
看上去除了变量声明时不是强类型之外,完全与面向对象如出一辙。所以谈及继承时,大家首推的也是一种叫“类式继承”的手法了。
1 | var Parent = function (name) { |
根据light主题的结构修改而来,但是去掉了侧边栏,改成一栏,所以随之也就没有了light主题的那些widget了。不过我增加了国内的多说评论框的配置,以及更好的响应式支持。
主题已经放到Github上了,也已完全适用于最新版的Hexo。为了更好的使用这个主题,建议clone我fork的hexo项目到本地,使用/path/to/hexo/bin/hexo
来代替之前安装的全局hexo
命令,方法见下面。我主要修改了代码块高亮(highlight)生成,以及修复了始终会生成Read more链接的BUG,不过我会尽快发起pull request到hexo的。
另外,如果需要使用多说评论框,可以使用我的自定义多说评论框样式,主要保持了和Modernist theme的样式统一。请猛戳这里。
]]>而可能有一些地方的翻译让人一开始有点迷糊,比如我们常说的“单例模式”被译为“单体模式”,而“mixin class”被译为“掺元类”。但是对于我这种E文能力弱的人来说,完全没有挑剔的地方。
我在读这本书的时候,采取了一些阅读方式来加深理解,个人感觉还过得去,就简单的分享下。我每两天完成一章节,第一天阅读整个章节的内容,边理解边对照着写实例代码;之后若是一有时间,大脑就温习这次的内容;第二天则主动回忆前一天所阅读的内容,整理简要的笔记纲要,并按照个人的理解和笔记纲要再写一次实例代码。
因此这本书读完之后,我的笔记也就随即产出了。我没有采用很长的篇幅记录,而是思维导图来整理每一章的内容,这样也方便日后温习。
呃,好久也没有写博客了,所以将笔记也一并分享出来,也欢迎大家分享自己的学习方法和笔记方法。
顺便也放张预览图,不过有点大。
]]>而可能有一些地方的翻译让人一开始有点迷糊,比如我们常说的“单例模式”被译为“单体模式”,而“mixin class”被译为“掺元类”。但是对于我这种E文能力弱的人来说,完全没有挑剔的地方。
我在读这本书的时候,采取了一些阅读方式来加深理解,个人感觉还过得去,就简单的分享下。我每两天完成一章节,第一天阅读整个章节的内容,边理解边对照着写实例代码;之后若是一有时间,大脑就温习这次的内容;第二天则主动回忆前一天所阅读的内容,整理简要的笔记纲要,并按照个人的理解和笔记纲要再写一次实例代码。
因此这本书读完之后,我的笔记也就随即产出了。我没有采用很长的篇幅记录,而是思维导图来整理每一章的内容,这样也方便日后温习。
呃,好久也没有写博客了,所以将笔记也一并分享出来,也欢迎大家分享自己的学习方法和笔记方法。
]]>
下面的Demo便用于说明同步事件之一的DOM Mutation events
(注:该事件不支持Chrome浏览器)。
1 | <a href="http://heroicyang.com/"> |
当click
事件触发时,其处理的顺序依次为:
in onclick
onpropchange
onclick
事件处理程序中剩下的 alert('out onclick');
关于DOM Mutation events
,详情请参见:
https://developer.mozilla.org/en-US/docs/DOM/Mutation_events
http://www.w3.org/TR/DOM-Level-3-Events/#events-mutationevents
在浏览器端,有一些方法会立即触发某类事件,而这类事件也是同步的。比如element.focus()
,下面是演示代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<input type="button" value="click me">
<input type="text">
<script type="text/javascript">
var btn = document.getElementsByTagName('input')[0]
, text = document.getElementsByTagName('input')[1];
btn.onclick = function(e) {
console.log('in onclick');
text.focus();
console.log('out onclick');
};
text.onfocus = function(e) {
console.log('onfocus');
};
</script>
执行结果如下:
常规情况下,事件处理都是一个一个执行的,而我们也就假定一个事件开始时,前一个事件是执行完毕了的。而以上这些同步事件不仅打破了我们的常规认识,还会给我们带来一些负面效应。不过我们依旧可以使用上一篇中所使用的setTimeout(func, 0)
来解决。
大多数浏览器中,JavaScript的执行和页面渲染是互斥的,于是JavaScript执行时,浏览器就不会做任何的页面渲染。比如下面的Demo…1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>JavaScript执行与页面渲染</title>
<style type="text/css">
#container {
width: 200px;
height: 100px;
background-color: #A00000;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="container"></div>
<input type="button" value="run" id="run">
<script type="text/javascript">
var runBtn = document.getElementById('run')
, container = document.getElementById('container');
runBtn.onclick = function(e) {
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
container.style.backgroundColor = '#' + i.toString(16);
}
};
</script>
</body>
</html>
运行上面的Demo后,大多数浏览器都会假死了,直到container
的背景颜色变更为#FFFFFF
后才恢复。而有的浏览器(如Firefox)还会弹出警告,告知JavaScript没有响应,是终止还是等待。但是Opera却能正常运行,并不断更改背景颜色。因此不同浏览器对页面渲染和JavaScript执行的实现方式是不一样的。
关于这方面有很大的学问,还需要继续学习,慢慢摸索。So…这个就点到为止了。
浏览器提供的如alert
等的模式对话框是同步调用的,所以当这类对话框工作时,会停止JavaScript线程
,当然如页面渲染等活动也将被冻结。继续下面的Demo…当运行代码下面的iframe
中的进度条后,无论是点击主窗体中的alert
按钮,还是点击iframe中的alert
按钮,都会导致进度条挂起。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<div id="container" style="width: 0px; height: 20px; background-color: #A00000;"></div>
<input type="button" value="run" id="run">
<input type="button" value="stop" id="stop">
<input type="button" value="iframe中的alert" onclick="alert('iframe中的对话框');">
<script type="text/javascript">
var runBtn = document.getElementById('run')
, stopBtn = document.getElementById('stop')
, container = document.getElementById('container');
var timer = null;
runBtn.onclick = function(e) {
timer = setInterval(function() {
var style = container.style;
style.width = (parseInt(style.width) + 2) % 400 + 'px';
}, 50);
};
stopBtn.onclick = function(e) {
clearInterval(timer);
};
</script>
因此,浏览器所提供的alert
、confirm
、prompt
这三类模式对话框,都会阻塞JavaScript线程
和UI线程
。
依旧,Opera有一点点的例外。。。
在Opera中,点击主窗体中的alert
按钮不会阻塞iframe
中的进度条。。。又打破我们的常规认识啊:同一个页面上,iframe
是和主窗体同一个线程的。但Opera的设计并非如此。。。
类似的就是前面那个阻塞我们浏览器的,频繁更改container
背景颜色的例子。最后,我们还是用上一篇文章中的setTimeout(func, 0)
来解决它吧。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<div id="container"></div>
<input type="button" value="run" id="run">
<input type="button" value="stop" id="stop">
<script type="text/javascript">
var runBtn = document.getElementById('run')
, stopBtn = document.getElementById('stop')
, container = document.getElementById('container');
var i = 0xA00000, timer = null;
runBtn.onclick = function(e) {
function run() {
timer = setTimeout(run, 0);
container.style.backgroundColor = '#' + i.toString(16);
if (i++ == 0xFFFFFF) stop();
}
timer = setTimeout(run, 0);
};
stopBtn.onclick = stop;
function stop() {
clearTimeout(timer);
}
</script>
最后,总结一下setTimeout(func, 0)
的使用场景吧:
下面的Demo便用于说明同步事件之一的DOM Mutation events
(注:该事件不支持Chrome浏览器)。
1 | <a href="http://heroicyang.com/"> |
JavaScript引擎线程
的队列中,然后由JavaScript引擎线程
进行调度执行。因此浏览器的很多事件都是和JavaScript
相结合的,但是也有一些内部的限制。
首先我们非常确定JavaScript
是单线程的,对于浏览器来说,一个窗体中只有一个JavaScript引擎线程
。而其他的行为,如:渲染、下载等是由单独的线程进行管理的,且具有不同的优先级。
前面提到大多数事件都是异步的,触发的时候就将回调函数添加到事件队列。浏览器提供了一个内部的回路,也就是之前所谈到的Event Loop
,由它来负责检查队列和处理事件、执行函数等。详细可参考我的前一篇博文。而setTimeout
和setInterval
也是将其需要执行的函数添加到事件队列。
一些情况下,会有多个事件在同一时间附加到事件队列里。
比如,click
事件就会产生两个额外的事件:mousedown
和mouseup
。其中,mouseup
和click
事件会同时被添加到事件队列;而mousedown
事件则很有可能会和另外一个事件重叠:focus
。
再一次解释关于0ms
的误解:如果当前时钟周期内执行队列空闲,则立即执行该定时器,将回调函数加入到事件队列;然后等待下一个时钟周期,再执行该回调函数。不妨来看看下面的测试。
这段代码在我的浏览器中执行结果如下:
在我本地的Nodejs
环境中执行结果如下:
上面的这个测试只是想说明setTimeout(func, 0)
定时任务的回调函数执行时间是有延迟的,而并不是所谓的立即执行。
因此,我们可以利用setTimeout(func, 0)
来解决事件重叠所产生的负面效果,修正执行顺序。
众所周知,浏览器的DOM事件都是采用冒泡的方式,只有个别浏览器是支持事件捕获的。而在实际的开发过程中可能存在需要事件捕获的需求,要求子元素的事件在父元素触发之后才能触发。为了兼容各个浏览器,我们不能使用事件捕获,而setTimeout(func, 0)
在这个时候就很乐意帮忙了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<input type="button" value="click" id="cbtn">
<div id="result"></div>
<script type="text/javascript">
var cbtn = document.getElementById('cbtn')
, result = document.getElementById('result');
cbtn.onclick = function(e) {
setTimeout(function() {
result.innerHTML += 'input click, ';
}, 0);
};
document.body.onclick = function(e) {
result.innerHTML += 'body click -> ';
};
</script>
点击查看运行效果:
大多数情况下,我们可以在浏览器的默认行为之前对事件进行处理,但是有时我们按照常规的思路去做的时候,往往事与愿违。比如下面的例子。1
2
3
4
5
6
7<input type="text" id="wordInput">
<script type="text/javascript">
var wordInput = document.getElementById('wordInput');
wordInput.onkeypress = function(e) {
this.value = this.value.toUpperCase();
};
</script>
看似一个很简单的需求:每输入一个字符,就将其转换为大写。但是上面的代码完全没有按照指示去做,不信你试试看:
如果没有下一次输入,文本框中的小写字母永远都不会转换为大写。Why? 因为浏览器在keypress
事件处理的时候,还没有将我们输入的值添加到文本框。于是乎换一个事件来handle然后再处理吧,既然键按下的时候还木有值,那就等键弹起来之后再处理。1
2
3
4
5
6
7<input type="text" id="wordInput">
<script type="text/javascript">
var wordInput = document.getElementById('wordInput');
wordInput.onkeyup = function(e) {
this.value = this.value.toUpperCase();
};
</script>
运行试试吧。
大概似乎是可行了,可是仔细观察就看出问题了。keyup
事件触发时,文本框已经具备完整的值了,但先是一个小写的值,键完全释放之后转变为大写。这不科学…这太丑陋…
是时候关门放出setTimeout(func, 0)
了。。。1
2
3
4
5
6
7
8
9
10<input type="text" id="wordInput">
<script type="text/javascript">
var wordInput = document.getElementById('wordInput');
wordInput.onkeypress = function(e) {
var self = this;
setTimeout(function() {
self.value = self.value.toUpperCase();
}, 0);
};
</script>
已经完美了。keypress
事件触发时,将转换大写的操作添加到事件队列,紧接着浏览器添加我们输入的值,然后近乎0延迟的执行我们的转换大写操作函数。
上面两个小案例只是冰山一角,so…合理利用setTimeout(func, 0)
,明天更美好!
JavaScript引擎线程
的队列中,然后由JavaScript引擎线程
进行调度执行。因此浏览器的很多事件都是和JavaScript
相结合的,但是也有一些内部的限制。
首先我们非常确定JavaScript
是单线程的,对于浏览器来说,一个窗体中只有一个JavaScript引擎线程
。而其他的行为,如:渲染、下载等是由单独的线程进行管理的,且具有不同的优先级。
前面提到大多数事件都是异步的,触发的时候就将回调函数添加到事件队列。浏览器提供了一个内部的回路,也就是之前所谈到的Event Loop
,由它来负责检查队列和处理事件、执行函数等。详细可参考我的前一篇博文。而setTimeout
和setInterval
也是将其需要执行的函数添加到事件队列。
事实上,大多数交互和活动都得通过事件循环。
]]>
JavaScript
提供的,而是由浏览器(对于前端来说)提供的。所以setTimeout()
和setInterval()
这两个方法均是通过浏览器的顶层对象window
进行调用,可能平时大家在使用的过程中也会省去window
而直接使用这两个方法。
这两个方法所接收的参数都一样:1
2setTimeout(func|code, delay);
setInterval(func|code, delay);
这两个方法总是被简单的认为:在多少毫秒之后就执行里面的函数或者每间隔多少毫秒就执行里面的函数,基于这种理解的话会遇到很多匪夷所思的坑。而结合上篇文章中所提到的执行队列来解释的话,很多疑问都可以迎刃而解。
前者:在指定的毫秒数后,将定时任务处理函数(func|code
)添加到执行队列的队尾。
后者:按照指定的周期(以毫秒计),将定时任务处理函数(func|code
)添加到执行队列的队尾。
下面分别使用了setInterval
和setTimeout
来实现同一个功能,可运行查看效果。
这是相应的源代码:传送门
接下来继续填setInterval
的坑。
假设定时器的上一个回调执行完到下一个回调开始的这段时间为时间间隔,那么对于setTimeout
来说,这个时间间隔理论上是应该>=delay
;而对于setInterval
来说,这个时间间隔理论上是应该<=delay
的。
但事实总会有出人意料的地方,setInterval
就是那个制造意外的东西。
以下是常规的代码:1
2
3
4
5
6
7
8var endTime = null;
setInterval(count, 200);
function count() {
var elapsedTime = endTime ? (new Date() - endTime) : 200;
i++;
console.log('current count: ' + i + '.' + 'elapsed time: ' + elapsedTime + 'ms');
endTime = new Date();
}
其执行结果也比较符合理论时间,见下图。
接下来修改代码,让count()
方法的执行时间变长一点:1
2
3
4
5
6
7function count() {
var elapsedTime = endTime ? (new Date() - endTime) : 200;
i++;
console.log('current count: ' + i + '.' + 'elapsed time: ' + elapsedTime + 'ms');
sleep(100); //sleep 100ms
endTime = new Date();
}
执行结果如下:
结合执行队列,可以用下图对上面两种情况进行直观的解释:
接下来再次修改代码,让count()
方法的执行时间更长,设定为setInterval
中delay
的2
倍,即400ms
:1
2
3
4
5
6
7function count() {
var elapsedTime = endTime ? (new Date() - endTime) : 200;
i++;
console.log('current count: ' + i + '.' + 'elapsed time: ' + elapsedTime + 'ms');
sleep(400); //sleep 400ms
endTime = new Date();
}
其执行效果变为如下:
意外发生了,每个回调之间的时间间隔竟然没有了,或者说缩短到非常小的间隔。事情大概是这样的:如果setInterval
的定时时间到了,而前一个回调还没有执行完时,就会把这次的回调放在执行队列的队尾;如果setInterval
的定时时间已经多次触发,而此时最前一个回调仍然还在执行,那么就会丢弃掉本次的回调。还是用图来直观说明吧。
这是回调处理时间比定时时间稍微长一点点的情况:
这是回调处理时间比定时时间长很多的情况:
所以,如果使用setInterval
的话,其时间间隔总是让人捉摸不定。而使用setTimeout
嵌套,则完全可以解决这个问题,还我们一个固定的时间间隔。
JavaScript
提供的,而是由浏览器(对于前端来说)提供的。所以setTimeout()
和setInterval()
这两个方法均是通过浏览器的顶层对象window
进行调用,可能平时大家在使用的过程中也会省去window
而直接使用这两个方法。
这两个方法所接收的参数都一样:1
2setTimeout(func|code, delay);
setInterval(func|code, delay);
这两个方法总是被简单的认为:在多少毫秒之后就执行里面的函数或者每间隔多少毫秒就执行里面的函数,基于这种理解的话会遇到很多匪夷所思的坑。而结合上篇文章中所提到的执行队列来解释的话,很多疑问都可以迎刃而解。
前者:在指定的毫秒数后,将定时任务处理函数(func|code
)添加到执行队列的队尾。
后者:按照指定的周期(以毫秒计),将定时任务处理函数(func|code
)添加到执行队列的队尾。
]]>
Nodejs
的过程中深入的了解了异步编程
这个概念,为了更好的使用Nodejs
,这些概念不可不知。在以前作为一个JavaScript
用户的时候,完全是不知道它是怎么运行的,对好些概念也是“知其然不知其所以然”。
对于客户端的JavaScript
和Nodejs
来说其实差距不是很大,这回就从客户端方面来说说Event Loop
这个概念吧,算是异步编程
的一个切入点吧。其实jQuery
的作者John Resig在几年前就写了一篇好文章How JavaScript Timers Work,来讲述timer
和事件
在浏览器中是怎样工作的,我也是通过这篇文章才“知其所以然”。
先来看看一段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<a href="#" id="doBtn">do something</a>
<div id="status"></div>
<script type="text/javascript">
void function() {
var doBtn = document.getElementById('doBtn')
, status = document.getElementById('status');
doBtn.onclick = function(e) {
e.preventDefault();
status.innerText = 'doing...please wait...'; //开始啦
sleep(10000); //模拟一个耗时较长的计算过程,10s
status.innerText = 'done'; //完成啦
};
}();
function sleep(ms) {
var start = new Date();
while (new Date() - start <= ms) {}
}
</script>
上面代码主要想完成一个功能:按钮被点击时———>显示一个状态告知用户正在干一些事情———>开始干———>事情干完后状态变更为已完成。
看上去没问题,应该是可以工作的,于是在浏览器运行这个页面。可是现实总是残忍的,没有符合预期效果。当点击按钮之后,浏览器就冻结了,用于显示状态的div
并没有显示,界面上也没有“doing…”这个提示;经过10s
之后,浏览器回过神了,代表耗时较长的计算已经结束,此时用于显示状态的div
显示“done”。
究其原因:JavaScript引擎是单线程的。而此时还有必要再了解下浏览器内核都有哪些主要的常驻线程,才能解上面的疑惑。浏览器内核常驻线程大致包含以下:
而GUI渲染线程和JavaScript引擎线程是互斥的,JavaScript执行时GUI渲染线程是挂起的,页面将停止一切的解析和渲染行为。上面的3、4、5类线程也会产生不同的异步事件。看下面这张图就应该比较直观了。
因为JavaScript引擎是单线程的,所以代码都是先压到队列,然后由引擎采用先进先出的方式运行。事件处理函数、timer执行函数也会排到这个队列中,然后利用一个无穷回圈,不断从队头取出函数执行,这个就是Event Loop
。
接下来还是继续用图来说明上面的代码为什么没有达到预期效果。
于是结果就只看到了”done”。
使用setTimeout()
,下面是修改后的onclick
事件处理函数:
1 | doBtn.onclick = function(e) { |
为什么这样就解决了呢?还是用上面的队列的图来解释。
]]>Nodejs
的过程中深入的了解了异步编程
这个概念,为了更好的使用Nodejs
,这些概念不可不知。在以前作为一个JavaScript
用户的时候,完全是不知道它是怎么运行的,对好些概念也是“知其然不知其所以然”。
对于客户端的JavaScript
和Nodejs
来说其实差距不是很大,这回就从客户端方面来说说Event Loop
这个概念吧,算是异步编程
的一个切入点吧。其实jQuery
的作者John Resig在几年前就写了一篇好文章How JavaScript Timers Work,来讲述timer
和事件
在浏览器中是怎样工作的,我也是通过这篇文章才“知其所以然”。
先来看看一段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<a href="#" id="doBtn">do something</a>
<div id="status"></div>
<script type="text/javascript">
void function() {
var doBtn = document.getElementById('doBtn')
, status = document.getElementById('status');
doBtn.onclick = function(e) {
e.preventDefault();
status.innerText = 'doing...please wait...'; //开始啦
sleep(10000); //模拟一个耗时较长的计算过程,10s
status.innerText = 'done'; //完成啦
};
}();
function sleep(ms) {
var start = new Date();
while (new Date() - start <= ms) {}
}
</script>
上面代码主要想完成一个功能:按钮被点击时———>显示一个状态告知用户正在干一些事情———>开始干———>事情干完后状态变更为已完成。
]]>
Octopress
(其实应该是Jekyll
)的评价,而这一评价是来自一篇几年前的文章。当我将自己的博客抹掉并重新开始的时候,我也准备以这个标题来作为这次的新起点。
其实早就有换掉WordPress
的想法,一是因为它太臃肿了,我只是想简单的写写博客,用不着那么多强大的功能;二是它对插入代码的支持让我绝望了,每次用Markdown
写好文章,复制其HTML
到WordPress
之后,都要调整好半天的样式;三是我之前的博客中太多的碎碎念之类的水文了,可谓杂、乱,并不像一个记录技术的博客。综合这些借口,我每次登录到WordPress
后台都没有再写文章的激情。
而之前我也和fiture聊到用Nodejs来重新写个博客,也当学习练手。经过间歇性的聊聊之后,我提议说不弄数据库了,直接将文章以Markdown
的格式push
到Github
吧等等的初步想法。当时我们觉得还不错,因为也没了解过有没有这样的东西存在,之后我下意识地搜了下。结果发现了OctoPress
这个东西,了解之后感觉完全正合我意啊,所以二话不说就直接给换上了。
于是我总算是终于弃掉了WordPress
,用上Octopress
这个高级货,可谓得心应手。
其实我也就冲着我认为的这几点优势:
Markdown
写文章Markdown
生成文章的静态页面Terminal
把文章push
到我的Github上即可,有版本管理真好Github Page
的支持,虽然有一些些小问题,比如缓存
,但瑕不掩瑜这样,就只需要一个Markdown
编辑器(推荐Mou和Sublime Text 2),再配合终端的git
命令就OK了,其余的都不用管了,交给第三方去。比如:评论系统我就采用了国内的多说;然后用Dropbox来保存文章中会用到的图片,因为Dropbox
被GFW
认证,所以再利用Nginx做个反向代理。一切都妥妥的了。
以前博客的那些废柴文章都不要了,不过还是做了个备份,算是纪念好了。而把近期写的3篇与JavaScript相关的文章转移了过来,重新开始技术博客的历程,见证成长。
]]>Octopress
(其实应该是Jekyll
)的评价,而这一评价是来自一篇几年前的文章。当我将自己的博客抹掉并重新开始的时候,我也准备以这个标题来作为这次的新起点。
其实早就有换掉WordPress
的想法,一是因为它太臃肿了,我只是想简单的写写博客,用不着那么多强大的功能;二是它对插入代码的支持让我绝望了,每次用Markdown
写好文章,复制其HTML
到WordPress
之后,都要调整好半天的样式;三是我之前的博客中太多的碎碎念之类的水文了,可谓杂、乱,并不像一个记录技术的博客。综合这些借口,我每次登录到WordPress
后台都没有再写文章的激情。
]]>
JavaScript
这门技术已经到了一个引爆点,一年前我对它的了解都只停留在肤浅的网页客户端脚本语言,只会简单的玩玩jQuery
和ExtJs
,其实都算不上开发者,而是一个JavaScript
用户。但今年的目标是做一个合格的前端攻城湿,所以恶补是必须的。
在JavaScript
中是其实不存在所谓“类”的概念,因为它并不是面向对象的语言。在面向对象中,一个最常见的说法就是:“类”是“对象”的模板,基本上都是采用语言内置的Class
或class
关键字来定义“类”。而JavaScript
不存在这个概念,所以也没有提供类似的关键字(虽然class
是JavaScript
的关键字,但是至今都没有实现,只是被保留而已)。
因此,在JavaScript
中创建类就唯有使用模拟的方式,而模拟的手法多种多样,何时采用何种方式最合适,需视情况而定。以下就记录下常见的几种模式。
工厂方法是设计模式中非常基础的,也被广泛用于面向对象编程中。而在JavaScript
中,通过工厂方法即能模拟出类的行为。1
2
3
4
5
6
7
8
9
10
11
12function createPerson(name, sex, …) {
var obj = {};
obj.name = name;
obj.sex = sex;
…
obj.getName = function() {
return this.name;
};
return obj;
}
var personA = createPerson('heroicYang', 'male');
var personB = createPerosn('路人甲', 'male');
通过这样类似的工厂方法,就可以创建出多个相似的对象了,但是这样的方式其抽象度极低。面向对象编程中,对象是可以检测出类型的,但是采用上面这种方式,是没有办法进行对象类型识别的。
其实,这应该是很常见的模式了,很多书上基本上一来就是讲这个的,更狠点的可能就只讲这个…1
2
3
4
5
6
7
8
9
10function Person(name, sex) {
this.name = name;
this.sex = sex;
…
this.getName = function() {
return this.name;
}
}
var personA = new Person('heroic', 'male');
var personB = new Person('路人甲', 'male');
这种模拟类方式的特点就是:
this
对象 return
字句在使用这种方式时,创建对象则必须使用new
关键字。当然,好处就是完全解决了对象类型识别问题。
原型应该是JavaScript
中一个很有意思,当然也是很有用的一个概念了。接下来用原型模式来模拟类。1
2
3
4
5
6
7
8
9
10
11
12
13
14function Person() {}
Person.prototype = {
name: null,
sex: null,
getName: function() {
return this.name;
}
};
var personA = new Person;
personA.name = 'heroic';
personA.sex = 'male';
var personB = new Person;
personB.name = '路人甲';
personB.sex = 'male';
由于只用原型模式的话,会带来一些问题,所以常规情况下,都是采用组合构造函数和原型模式来创建类,这也是使用率最高的一种方式。1
2
3
4
5
6
7
8
9
10
11
12
13function Person(name, sex) {
this.name = name;
this.sex = sex;
…
}
Person.prototype.getName = function() {
return this.name;
};
…
var personA = new Person('heroic', 'male');
var personB = new Person('路人甲', 'male');
personA.getName(); //"heroic"
personB.getName(); //"路人甲"
这种模式和工厂模式非常相似。1
2
3
4
5
6
7
8
9
10
11function SpecialArray() {
var values = new Array();
values.push.apply(values, arguments);
values.toPipString = function() {
return this.join('|');
}
return values;
}
var test = new SpecialArray('1', '2', '3');
test.toPipString(); // "1|2|3"
这种模式主要用来扩展一些对象的行为,而又不会对这个对象造成污染。当然,上面的代码也是可以直接为Array.prototype
原型对象添加一个toPipString()
方法来完成的,但是这样就造成了对JavaScript
原生对象的污染。
JavaScript
这门技术已经到了一个引爆点,一年前我对它的了解都只停留在肤浅的网页客户端脚本语言,只会简单的玩玩jQuery
和ExtJs
,其实都算不上开发者,而是一个JavaScript
用户。但今年的目标是做一个合格的前端攻城湿,所以恶补是必须的。
在JavaScript
中是其实不存在所谓“类”的概念,因为它并不是面向对象的语言。在面向对象中,一个最常见的说法就是:“类”是“对象”的模板,基本上都是采用语言内置的Class
或class
关键字来定义“类”。而JavaScript
不存在这个概念,所以也没有提供类似的关键字(虽然class
是JavaScript
的关键字,但是至今都没有实现,只是被保留而已)。
因此,在JavaScript
中创建类就唯有使用模拟的方式,而模拟的手法多种多样,何时采用何种方式最合适,需视情况而定。以下就记录下常见的几种模式。
]]>
Backbone
提供的能力,来继续精简代码。最后的目标就是将上篇中的代码全部重构为Backbone
的MVC
模式。
上篇中最后一次改造就已经使用到了callback
的方式,所以我们索性再加上Event
机制吧,因为Backbone
内置了这个能力。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35var events = _.clone(Backbone.Events);
var TodoList = function(){};
TodoList.prototype.add = function(options){
$.ajax({
url: '/add',
type: 'POST',
dataType: 'json',
data: { todoContent: options.todoContent },
success: options.success
});
};
var NewTodoView = function(options){
this.todoList = options.todoList;
events.on('todo:add', this.appendTodo, this);
events.on('todo:add', this.clearTextArea, this);
$('#new-todo form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add({
todoContent: $('#new-todo').find('textarea').val(),
success: function(data){
events.trigger('todo:add', data.todoContent);
}
});
};
/*后面不变*/
现在既然调用add()
时传入的success
属性已经完全不涉及到DOM
操作了,而是单纯的事件触发,那完全可以把这个行为放置到TodoList
原型的add()
方法中去了,这样重用性更高。
1 | /* … */ |
接下来,咱看看在NewTodoView
这个视图中事件订阅所触发的对应方法appendTodo()
和clearTextArea()
中,涉及到的是处在同一级别的不同的DOM
元素节点,也就是说在NewTodoView
这个视图中,我们处理了两个DOM
元素,这似乎和我们之前提到的“单一职责原则”相违背了,所以还有待进一步的改进。
我们分别把新增Todo
的视图和负责展示Todo Item
的视图分开定义,使其符合“单一职责原则”。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/* 前面不变 */
var NewTodoView = function(options){
this.todoList = options.todoList;
events.on('todo:add', this.clearTextArea, this);
$('#new-todo form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add($('#new-todo').find('textarea').val());
};
NewTodoView.prototype.clearTextArea = function(){
$('#new-todo').find('textarea').val('');
};
/* 用于展示Todo Item */
var TodoView = function(){
events.on('todo:add', this.appendTodo, this);
};
TodoView.prototype.appendTodo = function(todoContent){
$('#todo-list ul').append('<li>' + todoContent + '</li>');
};
/* 应用程序启动 */
$(function(){
var todoList = new TodoList();
new NewTodoView({ todoList: todoList });
new TodoView();
});
现在每个View
里面只依赖一个顶层的HTML Element
了,而在各自的View
里面多次使用到了$('#new-todo')
这样的代码,所以干脆将其在初始化的时候作为View
的一个属性来提供吧。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29/* 前面依旧不变 */
var NewTodoView = function(options){
this.todoList = options.todoList;
this.el = $('#new-todo'); //定义一个el属性ß
events.on('todo:add', this.clearTextArea, this);
this.el.find('form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add(this.el.find('textarea').val());
};
NewTodoView.prototype.clearTextArea = function(){
this.el.find('textarea').val('');
};
var TodoView = function(){
this.el = $('#todo-list');
events.on('todo:add', this.appendTodo, this);
};
TodoView.prototype.appendTodo = function(todoContent){
this.el.find('ul').append('<li>' + todoContent + '</li>');
};
/* 后面不变 */
此时观察发现,两个View
当中还保留着对DOM
节点的依赖,其重用度依然不高,于是可采用实例化View
的时候传入el
参数来解决这个问题。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* 前面不变 */
var NewTodoView = function(options){
this.todoList = options.todoList;
this.el = options.el;
events.on('todo:add', this.clearTextArea, this);
this.el.find('form').submit($.proxy(this.addTodo, this));
};
/* NewTodoView的原型方法也不变 */
var TodoView = function(options){
this.el = options.el;
events.on('todo:add', this.appendTodo, this);
};
/* TodoView的原型方法也不变 */
/* 初始化View的时候传入el */
$(function(){
var todoList = new TodoList();
new NewTodoView({ el: $('#new-todo'), todoList: todoList });
new TodoView({ el: $('#todo-list') });
});View
中我们频繁使用到了jQuery
的find()
方法来查找View
所在el
下面的子元素,所以可以考虑将这作为View
的特性来提供,于是我们为View
定义这样一个名叫$
方法,然后替换掉this.el.find()
这样的写法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37/* … */
var NewTodoView = function(options){
this.todoList = options.todoList;
this.el = options.el;
events.on('todo:add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
};
NewTodoView.prototype.clearTextArea = function(){
this.$('textarea').val('');
};
NewTodoView.prototype.$ = function(selector){
return this.el.find(selector);
};
var TodoView = function(options){
this.el = options.el;
events.on('todo:add', this.appendTodo, this);
};
TodoView.prototype.appendTodo = function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
};
TodoView.prototype.$ = function(selector){
return this.el.find(selector);
};
/* … */
上面的代码越来越多了,看上去好像咱是干的坏事,而不是往好的方向发展啊。是的,如果每个View
都有很多自己的特性(方法),那向上面这样着实太痛苦了。看样子是时候请出Backbone
提供的View
特性了。OK,把我们自己的View
转移到Backbone
的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/* … */
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.el = options.el;
events.on('todo:add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
},
clearTextArea: function(){
this.$('textarea').val('');
},
$: function(selector){
return this.el.find(selector);
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
this.el = options.el;
events.on('todo:add', this.appendTodo, this);
},
appendTodo: function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
},
$: function(selector){
return this.el.find(selector);
}
});
/* … */
由于Backbone
的View
已经提供了我们实现的$()
方法的能力,也叫$
(这也是之前我们自己命名的原因);同时Backbone
的View
也提供了this.el
的能力,所以可以把它们从代码中显示的移除了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/* … */
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
events.on('todo:add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
events.on('todo:add', this.appendTodo, this);
},
appendTodo: function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
}
});
/* 启动代码依然不变 */
现在可以回过头来看看ajax
那部分了,由于Backbone
提供了Model
的能力,这个就是用于和服务端打交道的,所以将长长的ajax
代码改写为这一方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/* … */
var Todo = Backbone.Model.extend({
url: '/add'
});
var TodoList = function(){};
TodoList.prototype.add = function(todoContent){
var todo = new Todo();
todo.save({ todoContent: todoContent },{
success: function(model, data){
events.trigger('todo:add', data.todoContent);
}
});
};
/* … */
同时,Backbone
中还提供了一个Collection
的概念,也就是Model
的集合,比如我们这个案例中,每次创建单条的Todo
,然后形成Todo List
。当然,我们的任何数据都应该是以多条记录的方式存在的。所以,我们同时将上面的TodoList
的实现改为Collection
。
而且,Backbone
的Collection
已经支持了Event
机制,所以我们也无需自定义events
了,于是开头的events
变量也一并移除了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49var Todo = Backbone.Model.extend({
url: '/add'
});
var TodoList = Backbone.Collection.extend({
add: function(todoContent){
var todo = new Todo(),
that = this;
todo.save({ todoContent: todoContent },{
success: function(model, data){
that.trigger('add', data.todoContent);
}
});
}
});
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.todoList.on('add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.todoList.on('add', this.appendTodo, this);
},
appendTodo: function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
}
});
$(function(){
var todoList = new TodoList();
new NewTodoView({ el: $('#new-todo'), todoList: todoList });
new TodoView({ el: $('#todo-list'), todoList: todoList });
});Collection
提供了一个名叫create()
的方法,其可以根据Collection
的Model
属性创建一个Model
的实例,并执行Model
的save()
方法。所以我们的TodoList
中的add()
方法已经可以废去了。我们只需为TodoList
提供Model
属性的值即可,然后在NewTodoView
的addTodo()
方法中,替换this.todoList.add()
方法为this.todoList.create()
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* … */
var TodoList = Backbone.Collection.extend({
model: Todo
});
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.todoList.on('add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.create({ todoContent: this.$('textarea').val() }); //替换为create方法
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
/* … */
这时,我们的Model
、Collection
、View
都已经齐上阵了。由于Backbone
的View
已经内置collection
属性,使得我们可以设置、获取View
对应的Collection
,所以我们完全无需手动在View
的内部来定义一个todoList
的变量了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/* … */
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.collection.on('add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.collection.create({ todoContent: this.$('textarea').val() });
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
this.collection.on('add', this.appendTodo, this);
},
appendTodo: function(todo){
this.$('ul').append('<li>' + todo.get('todoContent') + '</li>');
}
});
$(function(){
var todoList = new TodoList();
new NewTodoView({ el: $('#new-todo'), collection: todoList });
new TodoView({ el: $('#todo-list'), collection: todoList });
});
至此,完整的基于Backbone
的Model
、Collection
、View
模式就构建好了。如果说还有什么瑕疵的话,应该就是一些表层功夫了,那就是咱们的HTML Element
的append
了,需要做一些过滤,比如用户输入JavaScript
代码那就糟糕了。1
2
3this.$('ul').append('<li>' + todo.get('todoContent') + '</li>');
调整为
this.$('ul').append('<li>' + todo.escape('todoContent') + '</li>');
这样就Perfect了。文章忒长了点,但是为了从一个0
变成一个1
,我想应该还是很有意思的。
Backbone
提供的能力,来继续精简代码。最后的目标就是将上篇中的代码全部重构为Backbone
的MVC
模式。
上篇中最后一次改造就已经使用到了callback
的方式,所以我们索性再加上Event
机制吧,因为Backbone
内置了这个能力。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35var events = _.clone(Backbone.Events);
var TodoList = function(){};
TodoList.prototype.add = function(options){
$.ajax({
url: '/add',
type: 'POST',
dataType: 'json',
data: { todoContent: options.todoContent },
success: options.success
});
};
var NewTodoView = function(options){
this.todoList = options.todoList;
events.on('todo:add', this.appendTodo, this);
events.on('todo:add', this.clearTextArea, this);
$('#new-todo form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add({
todoContent: $('#new-todo').find('textarea').val(),
success: function(data){
events.trigger('todo:add', data.todoContent);
}
});
};
/*后面不变*/
现在既然调用add()
时传入的success
属性已经完全不涉及到DOM
操作了,而是单纯的事件触发,那完全可以把这个行为放置到TodoList
原型的add()
方法中去了,这样重用性更高。