jin5354 / 404forest Goto Github PK
View Code? Open in Web Editor NEW个人 Blog,写文做总结。
Home Page: https://www.404forest.com
个人 Blog,写文做总结。
Home Page: https://www.404forest.com
title: 跑起第一个React-Native Android App
categories:
毕设要产出一个安卓上的 APP,于是打算尝试下 React-Native。本文主要记录环境搭建过程,把踩的坑写一下。
安装 homebrew
安装 Git brew install git
。在 mac 上,如果你已经安装了 Xcode
,那么 Git 已经安装好了。
安装 nodejs 4+ 版。
安装 watchman brew install watchman
安装 flow brew install flow
安装最新的 JDK
之前已经用过几次 Android Studio 了,印象不是很好:一是墙的问题,下载 SDK,下载 gradle 都非常慢,体验很差;二是程序本身性能也不佳,下面长期跑一些不明所以的 process,进 preferences 时很容易卡死。这次打算彻底重装一下。
执行以下命令:
rm -Rf /Applications/Android\ Studio.app
rm -Rf ~/Library/Preferences/AndroidStudio*
rm ~/Library/Preferences/com.google.android.studio.plist
rm -Rf ~/Library/Application\ Support/AndroidStudio*
rm -Rf ~/Library/Logs/AndroidStudio*
rm -Rf ~/Library/Caches/AndroidStudio*
如果要删除所有 projects:
rm -Rf ~/AndroidStudioProjects
如果要删除 gradle 相关文件:
rm -Rf ~/.gradle
如果要删除安卓虚拟机(AVD)等等:
rm -Rf ~/.android
删除 android sdk 相关:
rm -Rf ~/Library/Android*
来到官方网站下载软件本体。
下载之后初次运行,会因为网络问题(墙)获取 sdk 仓库时报错,这个时候虽然提醒你设置 proxy,但是我设置了之后无效,依然报错。查了一下,这样处理:
在应用程序中,进入 Android Studio,修改 bin 目录下的 idea.properties
文件,在文件最后追加 disable.android.first.run=true
。
这时由于没有 sdk,新建项目时会要求指定 sdk 路径。我们先手动下载一下 sdk tools。可以来镜像站下载 SDK tools。下载后解压缩,将其中的所有内容移动到~/Library/Android/sdk/
下。(最好放在这个路径。)terminal 中进入,cd
到 tools 文件夹下,执行:
./android sdk
这时我们就能看到 SDK Manager 界面了。
不过因为伟大的墙,我们并不能 fetch 到什么资源.....于是继续使用镜像资源:
在 Preferences 中,设置 Proxy。HTTP Proxy Server 可以使用 android-mirror.bugly.qq.com
,Port 使用 8080
。这个是腾讯提供的镜像站,也可以使用其他站,挑选一个网速比较快的。把 others
中的 Force..
一项勾上。
退出重新进入,此时就能获取到资源列表了。
按照 React-Native 官网上的说明,把这些包安装上:
在 Android Studio 中指定好 sdk 的路径。这些包都安装好,应该可以正常新建一个空项目了。
在 MAC 上,向你的 shell 的配置文件中添加:export ANDROID_HOME=sdk路径。
前去 Genymotion官网,注册个帐号,下载仅限个人使用的免费版。
在主界面点击 Add
添加虚拟机,还是因为伟大的墙,你在下载时可能会遇到 server return HTTP Code 0
的迷之情况。这时配置 proxy:
以下命令,建议都加上sudo执行。
$ npm install -g react-native-cli
$ react-native init AwesomeProject
在运行之前,先打开一个 genymotion 的虚拟机。
$ cd AwesomeProject
$ react-native run-android
然后就是报错与改错时间...
permission denied, open '~/.babel.json'
。修改 .babel.json
的权限,执行 sudo chown 你的用户名 ~/.babel.json
。
Error: query failed: synchronization failed: Permission denied
。修改项目文件夹的权限,执行 sudo chmod 777 AwesomeProject
。
SDK location not found.
这里要指定 SDK location。这里很奇怪的一点是:我明明在zsh中指定了 ANDROID_HOME,但是依然会报错。所以换用另一种方法:
在 AwesomeProject/android 下面新建文件 local.properties
,内容为 sdk.dir=sdk路径
,例如我的是 sdk.dir=/Users/jin/Library/Android/sdk
。
ailed to find Build Tools revision 23.0.1
。虽然 sdk manager 里面 Build Tools 已经出到 23.0.2 了,但是不行哦,你还是要安装一个 23.0.1 版。
这些坑都踩完之后,终于见到了 BUILD SUCCESSFUL
的提示。这时去虚拟机的程序里面看一看把, 一个叫做 AwesomeProject 的 APP 已经躺在里面了。运行起来:
enjoy!
参考资料:
title: 初玩树莓派--安装与第一次配置
categories:
淘宝上看到了树莓派2B开卖,自己这学期也有一些嵌入式课程,所以260+入了一块,简单记录一下初期配置。
非常小非常小——确实仅比饭卡大一圈。包装盒里的东西也确实是简单至极:一个基本没啥用的小册子和树莓派本体。
为了玩耍这个小板子,那么你需要准备好的是:
配置流程:
sudo ifconfig
可以查看本机地址,使用ssh连接即可~~默认用户名:pi 密码:raspberry。调整好了之后,树莓派就可以仅通过一根网线和一根电源线进行正常工作了
~~
ps:配好树莓派后打算立刻装上nodejs的。我记得sudo apt-get install nodejs之后还要装一个包的,否则node命令不能用。记得是个nodejs-为前缀的包,百度查了好久怎么也查不到,去google搜索预测中就出现了..技术问题后问google是我的错。
sudo apt-get install nodejs
sudo apt-get install nodejs-legacy
关于树莓派的更多设置信息可以参考:
Adafruit树莓派系列教程
title: 将项目开源到 Github 时,别忘了做些整理工作
categories:
项目开源时,别忘了把整理工作做好。
有的文件中可能包含了 SSH 帐号密码,内网机器 IP 等敏感信息。这些信息在开源前必须清除。因为将 Git repo 上传到 Github 公开之后任何人都可以访问完整的 commit history,所以仅仅在某个 commit 中删除掉敏感文件是没有用的。我们必须将敏感文件从整个 History 中消除掉才可以,就像它从未存在过一样。
从历史 commit 中清除文件有两种途径:
filter-branch
命令,可以修改历史 commitgit filter-branch
效率更高,声称提速 10 - 720 倍首先访问 BFG Repo-Cleaner 下载 bfg 工具,然后创建要修改的 repo 的 mirror:
$ git clone --mirror git://example.com/repo.git
将下载下来的 bfg.jar
与 repo.git 放在同一目录下,随后执行:
$ java -jar bfg.jar --delete-files ssh.key repo.git
$ cd repo.git
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
处理完毕之后,push
$ git push
让我们查看曾经包含该文件改动的 commit:
ssh.key 文件已经从 commit history 中消失了,仿佛从来没有存在过一样。
一般来说,我们在 GitHub 上和公司内部 Git 仓库上提交时用的是不同身份。比如我,在 Github 上提交用 Gmail 邮箱身份,在公司内部 Git 仓库上提交时用公司企业邮箱身份。有时也会混用,在公司内网提交代码时没注意,用 Gmail 邮箱身份就提交上去了……
项目开源后,之后的开发便主要在 Github 上进行,出于洁癖,我们可以将项目中 author 信息进行统一。
首先 clone 一份目标 bare repo:
$git clone --bare https://github.com/user/repo.git
$cd repo.git
随后在 bare repo 下创建 shell 文件:git-author-rewrite.sh
,将 $OLD_EMAIL
,$CORRECT_NAME
,$CORRECT_EMAIL
变量替换为待替换的 email、替换后的 author name 和替换后的 email。
#!/bin/sh
git filter-branch --env-filter '
OLD_EMAIL="[email protected]"
CORRECT_NAME="Your Correct Name"
CORRECT_EMAIL="[email protected]"
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_COMMITTER_NAME="$CORRECT_NAME"
export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_AUTHOR_NAME="$CORRECT_NAME"
export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags
执行该 shell。
$ sudo bash git-author-rewrite.sh
稍等片刻,author info 修改完毕,push。
$ git push --force --tags origin 'refs/heads/*'
随后再检查项目的 commit history,可以看到 author 信息已经统一了。
很多项目免费支持对 Github 上的 repo 添加持续集成功能。其中一个使用广泛的项目就是 Travis CI。添加 CI 后,每次项目提交后,CI 都会自动进行 build 过程,并将 build 结果向你汇报。可以非常方便有效的监控项目的健康程度。
Travis CI 的添加极为简单:
.travis.yml
,添加配置项内容:sudo: false
language: node_js
node_js: stable
script:
- npm run lint
- npm run test
- npm run build
我的项目构建是基于 nodejs 的,每次 build 之前还要过一遍 lint 和 单元测试,所以这样写。意思就是:使用 stable 版本的 node,在项目下跑 npm run lint
, npm run test
, npm run build
三个命令,如果没有报错,则 build 为 passing。若出错则为 build error。每次提交之后,Travis CI 还会把 build 结果发邮件通知你。
coverage 为项目的单元测试覆盖率信息。如果你的项目有单元测试,且进行了覆盖率统计,那么我们可以将覆盖率信息放到网页上来查看,并且生成一个 badge 展示在项目下。
如何为项目写单测这里不再展开。项目跑完单测,往往会生成一个叫做 lcov.info 的信息文件。coverage 信息获取就靠这个文件了。
Coveralls 也是一个被广泛使用的项目测试覆盖率统计工具,可以监控项目的单测覆盖率情况。使用也极其简单:
lcov.info 是在跑单测之后产生的,也就是在执行 npm run test
之后产生。所以,我们要将传送信息文件的过程放在 CI 步骤里。
npm i coveralls --save-dev
.travis.yml
:sudo: false
language: node_js
node_js: stable
script:
- npm run lint
- npm run test
- npm run build
after_success:
- cat ./coverage/lcov.info | coveralls
这样每次 CI 跑完之后就会自动把单测覆盖率信息送到 Coveralls 了, 在 Coveralls 网站上可以监控代码提交前后单测覆盖率的变动。
http://shields.io/ 这个网站为项目提供各种各样的 Badge。可以挑选适合的加上。
title: 如何区分js中的各种数据类型
categories:
题目:如何判断某个变量是以下中的哪种数据类型?
类型为:String、Number、Boolean、Function、Regexp、Date、Null、Undefined、Error、Array。
刚看到题时第一直觉是用instanceof和typeof来判断。typeof 可用来区分number、string、boolean、function、undefined。
instanceof可用来判定实例,所以可以来判断Array、RegExp、Date、Error。
判断Null用if (!exp && typeof exp != "undefined" && exp != 0)来判断。
关于null要注意的是:
还有一点,对于String和Number。如果直接创建,不使用构造函数,由于变量不是对象,可以使用typeof判断,不能用instanceof判断。如果用new String()构造函数来创造变量,则可以使用instanceof判断,typeof判断为object。
var a = "text1";
console.log(a); //"text1"
console.log(typeof a); //"string"
console.log(a instanceof String); //false
console.log(a instanceof Object); //false
var b = new String('text2');
console.log(b); //"text2"
console.log(typeof b); //"object"
console.log(b instanceof String); //ture
console.log(b instanceof Object); //ture
翻别人答案时发现果然有更简单粗暴的方式:Object.prototype.toString()。这个方法用来返回一个对象的字符串形式。使用call就可以在任意值上调用啦。
不同数据类型的toString()返回值如下~
这样已经可以把以上10种区分出来。上文说的两种String和Number都可以得出正确结果;若要分的再细一点,把NaN(默认在Number里)分出来,用isNaN()辅助判断一下即可。
用正则把返回值中的类型信息提取出来:
var ret = Object.prototype.toString.call(val).match(/[A-Z]\w+/)[0];
判断函数如下:
var typer = (function(){
function type(val, expected) {
var ret = Object.prototype.toString.call(val).match(/[A-Z]\w+/)[0]
return ret != 'Object' ?
ret === expected :
val.constructor.toString().match(/\b\w+/g)[1] === expected;
}
return {
isNumber: function(x){return type(x,'Number') && !isNaN(x)},
isString: function(x){return type(x,'String')},
isArray: function(x){return type(x,'Array')},
isFunction: function(x){return type(x,'Function')},
isDate: function(x){return type(x,'Date')},
isRegExp: function(x){return type(x,'RegExp')},
isBoolean: function(x){return type(x,'Boolean')},
isError: function(x){return type(x,'Error')},
isNull: function(x){return type(x,'Null')},
isUndefined: function(x){return type(x,'Undefined')}
}
})()
title: 为Alfred3编写一个汇率转换workflow——CurrencyConvert
categories:
打算使用 Alfred3 来替换 spotlight,但是网上找了一圈竟然没有好用的汇率转换 workflow,有一些适配 Alfred2 的放到 Alfred3 下面工作不正常,于是自己动手写了一个,顺便作为 ruby 的练手。
Alfred2 版本输出结果列表时使用 xml 语言,Alfred3 开始官方推荐使用json作为结果输出格式,所以该workflow不向下兼容v2版本。
输入 'cy' 来查看基准货币单位的最新汇率。回车将汇率数值发送到剪贴板。
输入 'cy money' 来换算货币。回车将金额数值发送到剪贴板。
输入 'add-cy' 和 'remove-cy' 来调整主显的货币单位。输入 'base-cy' 更改基准货币单位。
包管理是门大学问。
ruby 语法糖真多,Array, String 下挂了七八十个方法.....好厉害
电信网真的差,传 Github release 文件死活也传不上去,后来换成移动手机开热点才可以
参考:
title: (译)如何使用 Chrome Devtools 中的 Timeline 工具
categories:
使用 Chrome Devtools 中的 Timeline 面板可以记录并分析你的页面运行过程中的所有活动。如果你的页面存在性能问题,Timeline 工具是分析问题的最佳入手点。
翻译好难!(ง •̀_•́)ง
原文:How to Use the Timeline Tool - Google Developers
Timeline 面板由以下4部分组成:
你也许会在帧图里看到一到三条垂直的虚线。蓝线代表
DOMContentLoaded
事件。绿线代表第一次绘制发生的时间点。红线代表load
事件。
概览面板包括3个图表:
颜色的含义:HTML 文件为蓝色。脚本文件为黄色。样式文件为紫色。图片等媒体文件为绿色。其他资源为灰色。
想录制页面的加载(load)过程,先打开 Timeline 面板,随后打开想录制的页面,然后刷新。Timeline 工具会自动录制下页面的刷新加载全过程。
想要录制页面的某个交互细节,先打开 Timeline 面板,随后点击 Record 按钮(黑圆点)或者按快捷键(Mac:cmd + e
,Linux、Windows:ctrl + e
)开始录制。录制时录制按钮会变成红色。操作页面,再次点击 Record 按钮或按快捷键结束录制。
当录制完成后,DevTools 会猜测录制的哪一部分最关键,然后自动缩放到该部分。
当你在帧图中选择了一个事件,详情面板就会显示该事件的更多信息。
有些标签,比如 Summary,在所有种类事件中都会显示。其他的标签只在特定种类事件中显示。浏览这篇文章 Timeline 事件参考 可知每种录制事件的细节。
> 一共有4种录制事件,Loading events、Scripting events、Rendering events、Painting events。
Timeline 面板能在页面加载时截图。这个功能又叫幻灯片。
若要在录制时截图,先在控制工具面板中启用 Screenshots 功能。截图会在概览面板下面显示。
把鼠标悬在截图或者概览面板上可以看到缩放后的录制瞬间的截图。鼠标从左向右移可以模拟页面展现过程。
想要在录制时捕获 Javascript 堆栈,在控制工具面板中启用 JS Profile 功能。功能启用后,你的帧图可以显示出调用了的每一个 Javascript 函数。
想要在录制时获得更多的 Paint 事件信息,在控制工具面板中启用 Paint 功能。功能启用后,在帧图中点击一个 Paint 事件,详情面板中会显示一个新的 Paint Profiler 标签,其中会展示更多关于该事件的颗粒状的信息。
打开 Devtools 主菜单, 选择 More tools > Rendering settings 可以看到一些有助于调试 paint 问题的工具。Rendering settings 会紧挨着 Console 抽屉面板打开!一个 Rendering 标签。(通过按 esc
来显示或隐藏 Console 抽屉面板)。
当观察事件时,也许你只想关注某一种事件。例如,你可能只需要检查每一个 Parse HTML
事件的详情。
在 Timeline 面板按快捷键(Mac:cmd + f
或者 Linux/Windows:ctrl + f
)来打开搜索工具栏。输入你想观察的事件种类名,例如 Event
。
搜索只会在选中的时间段内进行。时间段以外的事件不会被搜索。
按上下方向键可以按时间顺序切换搜索结果。所以,第一个结果代表的是时间段内被搜索到的最早的事件,最后一个结果代表时间段内被搜索到的最后一个事件。每次切换搜索结果时,你可以在详情面板中查看详细信息。按上下方向键与直接在帧图中点击事件是等同的。
你可以缩放时间段来使分析更容易。帧图也会自动缩放来匹配时间段。
想要缩放:
一旦选择好了时间段,你还可以使用 W
, A
, S
, D
来调整区间。
在概览和帧图面板右击,就可以存储或者读取录制信息。
title: 《WebKit技术内幕》——脑图笔记
categories:
最近把《WebKit技术内幕》这本书读了一遍。这本书其实更适合浏览器开发工程师来看,对于我这种前端工程师来说,可能没必要深究某处实现的细节,书中有的地方如果能讲的更抽象些就更好了。这里把脑图存一下档。
一开始还是细读,读到后面发现涉及到具体类的实现部分蛮晦涩的,于我收益也不大,于是变成跳读了。
比较涨姿势的地方还是网页资源加载与渲染的流程~
PS. 找配图可真难,特别是文章上方的大图,蛋疼~
title: 常见知识点汇总(四):JSON
categories:
JSON是一种数据格式,而非编程语言。它是基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。
JSON的语法可以表示以下三种类型的值:
例如:
数值
5
字符串
"Hello World!"
js字符串与JSON字符串的最大区别在于:JSON的字符串必须使用双引号(单引号会导致语法错误。)
布尔值和null也是有效的JSON形式。
与js对象字面量相比,JSON对象区别在于:
例:
js对象
var object = {
name: "Nicholas",
age: 29
};
JSON的表示方式:
{
"name": "Nicholas",
"age": 29
}
JSON数组也没有变量和分号。
js中的数组字面量:
var values = [25, "hi", true];
JSON的表示法:
[25, "hi", true]
JSON数据结构可以解析为有用的javascript对象。与XML数据结构要解析成DOM文档并且从中提取数据极为麻烦相比,JSON可以解析为javascript对象的优势极其明显。
IE8+及其他现代浏览器已经定义了全局变量JSON,对于较早版本的浏览器,可以使用一个shim。
JSON对象有两种方法:stringify()和parse()。在最简单的情况下,这两个方法分别用于把js对象序列化为JSON字符串以及把JSON字符串解析为原生javascript值。
var book = {
title: "Professional Javascript",
authors: [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011
};
var jsonText = JSON.stringify(book);
默认情况下,JSON.stringify()输出的JSON字符串不包含任何空格字符和缩进,所以保存在jsonText**年的字符串如下所示:
{"title":"Professional Javascript","authors":["Nicholas C Zakas"],"edition":3,"year":2011}
序列化时,所有函数及原型成员都会被忽略,值为undefined的任何属性也会被跳过。
将JSON字符串传递给JSON.parser()即可得到相应的js值,例如使用下列代码就可以创建与book类似的对象:
var bookCopy = JSON.parser(jsonText);
JSON.stringify()除了要序列化的javascript对象外,还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化Javascript对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数,第二个参数是一个选项,表示是否在JSON字符串中保留缩进。
如果第二个参数是函数,行为会稍有不同。传入的函数接收两个参数,属性(键)名和属性值,根据属性(键)名可以知道应该如何处理要序列化的对象中的属性。
JSON.stringify()方法的第三个参数用于控制结果中的缩进和空白符。
如果JSON.stringify()方法还不能满足对某些对象进行自定义序列化的需求,可以给对象定义toJSON()方法,返回其自身的JSON数据格式。
JSON.parser()方法也可以接收另一个参数,该参数是一个函数,将在每个键值对儿上调用。
如果还原函数返回undefined,则表示要从结果中删除相应的键;如果返回其它值,则将该值插入到结果中。
title: 《WebGL 编程指南》读书笔记脑图 —— Part One
categories:
《WebGL 编程指南》读书笔记第一部分。这本书讲的相当深入浅出、事无巨细,简直令人感动,强烈推荐之。
title: 使用 karma/mocha 全家桶为 Vue 组件编写单元测试
categories:
编写单元测试可以大大提高项目的稳定性和内心的安全感。对于功能点稳定、需长期迭代的项目,编写单元测试可以有效的减少维护成本,降低 Bug 率。最近在为公司内部的 Vue 组件库添加单元测试,配置测试环境、编写测试用例花了一些时间,略作整理。
整理一下配置测试环境所需要的依赖:
官方示例中是使用 karma-coverage
来统计代码覆盖率的,不过很遗憾用来测试 Vue 组件输出结果不太正常,折腾一番无果,参照其他开源项目最终替换为了 istanbul 。
全家桶安装一波:
npm i karma phantomjs-prebuilt mocha chai sinon karma-webpack karma-mocha karma-sinon-chai sinon-chai karma-sourcemap-loader karma-phantomjs-launcher karma-spec-reporter istanbul-instrumenter-loader karma-coverage-istanbul-reporter --save-dev
按照 karma 的文档,运行:
karma init
先后选择使用的测试框架、是否使用 require.js、浏览器环境、测试脚本存放位置、是否有需要 ignore 的文件,等等。很简单,选择完毕之后,该项目根目录下生成名为 karma.conf.js
文件。
接下来就是设置 karma 的各项插件的配置:
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// 手动引入 karma 的各项插件,如果不显式引入,karma 也会自动寻找 karma- 开头的插件并自动引入
plugins: [
'karma-coverage-istanbul-reporter',
'karma-mocha',
'karma-sinon-chai',
'karma-webpack',
'karma-sourcemap-loader',
'karma-spec-reporter',
'karma-phantomjs-launcher'
],
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
// 设定要使用的 frameworks
frameworks: ['mocha', 'sinon-chai'],
// list of files / patterns to load in the browser
// 入口文件,按照 istanbul-instrumenter-loader 的要求来写
files: ['./test/unit/index.js'],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
// 加入 webpack 与 sourcemap 插件
preprocessors: {
'./test/unit/index.js': ['webpack', 'sourcemap'],
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
// 设定报告输出插件: spec 和 coverage-istanbul
reporters: ['spec', 'coverage-istanbul'],
// coverage-istanbul 输出配置,报告文件输出于根目录下的 coverage 文件夹内
coverageIstanbulReporter: {
// reports can be any that are listed here: https://github.com/istanbuljs/istanbul-reports/tree/590e6b0089f67b723a1fdf57bc7ccc080ff189d7/lib
reports: ['html', 'lcovonly', 'text-summary'],
// base output directory
dir: './coverage',
// if using webpack and pre-loaders, work around webpack breaking the source path
fixWebpackSourcePaths: true,
// Most reporters accept additional config options. You can pass these through the `report-config` option
'report-config': {
// all options available at: https://github.com/istanbuljs/istanbul-reports/blob/590e6b0089f67b723a1fdf57bc7ccc080ff189d7/lib/html/index.js#L135-L137
html: {
// outputs the report in ./coverage/html
subdir: 'html'
}
}
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
// 设定终端上不输出 webpack 的打包信息
webpackMiddleware: {
noInfo: true
},
// 用来预编译源代码的 webpack 配置,基本就是项目的 webpack 配置,但要去掉 entry 属性
webpack: {
output: {
path: __dirname + '/lib',
filename: '[name].js',
libraryTarget: 'umd'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
// 为了统计代码覆盖率,对 js 文件加入 istanbul-instrumenter-loader
{
test: /\.(js)$/,
loader: 'istanbul-instrumenter-loader',
exclude: /node_modules/,
include: /src|packages/,
enforce: 'post',
options: {
esModules: true
}
},
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /node_modules/,
include: /src|packages/,
enforce: 'pre',
options: {
eslint: {
configFile: '../.eslintrc.json'
}
}
},
{
test: /\.vue$/,
loaders: [{
loader: 'vue-loader',
options: {
postcss: [autoprefixer({browsers: ['> 1%', 'ie >= 9', 'iOS >= 6', 'Android >= 2.1']}), px2rem({remUnit: 75})],
// 为了统计代码覆盖率,对 vue 文件加入 istanbul-instrumenter-loader
preLoaders: {
js: 'istanbul-instrumenter-loader?esModules=true'
}
}
}]
},
{
test: /\.(scss|sass)$/,
loaders: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.css$/,
loaders: ['style-loader', 'css-loader']
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif|svg)(\?t=\d+)?$/,
loaders: [{
loader: 'url-loader?limit=8192&name=[name]-[hash].[ext]'
}]
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
})
]
}
})
}
├── src/
├── packages/
│
├── test/
│ └── unit/
│ ├── index.js
│ └── *.spec.js
└── karma.conf.js
项目源代码均处于 src 和 packages 两个文件夹下,且 src 文件夹下存在一个总入口文件,其中引入了 src 与 packages 下的全部模块。
测试相关文件均放在 test/unit 文件夹下,总入口文件为 index.js,各个组件的单测文件分别为 组件名.spec.js。
karma 配置文件 karma.conf.js 放置于项目根目录下。
根据 istanbul-instrumenter-loader
文档的说明,测试总入口文件 index.js 内容如下:
// Polyfill fn.bind() for PhantomJS
/* eslint-disable no-extend-native */
Function.prototype.bind = require('function-bind')
// require all test files (files that ends with .spec.js)
// require 所有的测试文件 *.spec.js
const testsContext = require.context('.', true, /\.spec$/)
testsContext.keys().forEach(testsContext)
// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
// require 需要统计覆盖率的源码文件
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)
经过上面三步,karma 全家桶已经配置完毕,现在只需在根目录下运行:
karma start karma.conf.js
即可运行 karma,并自动运行所有 test/unit/*.spec.js 的单测文件,同时监测代码改动,自动重跑测试。不过现在还没有单测文件,我们来写一个。
举例一个标准的 Vue 组件:
根据 vue 单元测试相关的官方文档,我们知道可以这样在测试环境下测试单独组件:
import Button from '../../packages/Button/index.js'
const ButtonConstructor = Vue.extend(Button)
const vm = new ButtonConstructor({
propsData: {
type: 'gray'
}
}).$mount()
console.log(vm.name) //'wd-button'
使用 Vue.extend 方法可以创建出一个组件实例,还可以直接将 prop 数据传进去。我们将这个方法封装下:
// utils.js
import Vue from 'vue'
export const createCompInstance = (Component, propsData) => {
const Constructor = Vue.extend(Component)
return new Constructor({
propsData
}).$mount()
}
然后编写 Button 组件的单元测试:
// Button.spec.js
import Button from '../../packages/Button/index.js'
import {createCompInstance} from './utils.js'
describe('Button', () => {
let vm
it('type 样式', () => {
vm = createCompInstance(Button, {
type: 'gray'
})
expect(Array.prototype.slice.call(vm.$el.classList)).to.include('wd-button-gray')
})
it('diabled 禁用态', () => {
vm = createCompInstance(Button, {
disabled: true
})
expect(vm.$el.hasAttribute('disabled')).to.be.true
})
...
})
组件创建完毕之后,结合断言库对情况进行测试。例如:当给 Button 组件传入 prop:type = 'gray' 时,实例的 DOM 上应该会有 'wd-button-gray' 这个 Class,于是可用断言库进行判断:
expect(Array.prototype.slice.call(vm.$el.classList)).to.include('wd-button-gray')
当给 Button 组件传入 prop:disabled = true 时,实例的 DOM 上应该有一个叫 disabled 的 Attribute。断言:
expect(vm.$el.hasAttribute('disabled')).to.be.true
以此类推,逐步测试各个功能点是否工作正常。如果测试未通过,即行为与预期不服,断言失败,会报错:
如果测试通过,终端结果如下:
src 和 packages 中源码很多,测试只写了一点点,可以看到 Coverage summary 即代码覆盖率很低。
mocha 与 chai 的语法这里不再赘述,编写测试用例时需多多查看文档。
mocha 中涉及异步操作的测试,要使用 done 函数:
it('picker onHide 窗体隐藏回调', done => {
vm = Picker({
slots: [{
type: 'data',
flex: 1,
values: ['北京', '上海', '广州'],
textAlign: 'center'
}],
onHide: (instance) => {
document.body.testToken = 'hide'
instance.value = false
},
})
Vue.nextTick(() => {
const $btn = document.querySelector('.wd-picker-header-cancel')
$btn.click()
setTimeout(() => {
expect(document.body.testToken).to.equal('hide')
delete document.body.testToken
done()
}, 500)
})
})
涉及到 Vue.nextTick
和 setTimeout
等异步操作的测试,要使用 done()
来标记完成时间点。
mocha 提供 beforeEach
afterEach
等方法帮忙做些杂活;比如我想每次测试完组件的其中一个用例之后,清空一下 body 节点,防止留下残余 DOM 影响后续的操作:
beforeEach(() => {
document.body.innerHtml = ''
})
将测试任务集成进 npm scripts:
//package.json
"scripts": {
"test": "karma start karma.conf.js --single-run",
}
为项目中的3个组件添加了单元测试,暂时写了40个测试用例,基本覆盖了文档上的全部功能点。这之后如果对组件做重构/ bug 修复/加新功能时,就不需要担心是否会影响老功能,也不需要自己手动 check,直接跑 npm run test
测试看结果就可以了。安全感 up。
在项目根目录的 coverage 文件夹内可以看到输出的 html 格式代码覆盖率报告:
点击文件可以看到更详细的说明:哪行代码测到了,哪行代码没有测到:
图中标红的地方即为没被测到的代码,有些是在 if 分支内,有的在 watch 里,没有被执行过。另外 phantomJS 对于模拟鼠标键盘事件的能力并不强,没有提供相关的 api,所以图中的有关鼠标拖拽的事件很难模拟,没能进行测试。这些测试更适合用 E2E 测试而非单元测试来做。
另外代码覆盖率有些地方算得并不准确,比如 Button/src 下的 Button.vue
组件,因为比较简单,没有 methods,只有几个 prop,被判断成了 0% 的覆盖率(实际上 Button 组件的5个用例已经把每个 prop 都测过一遍了)。希望未来这个工具能更新的更加准确一些。
其他组件的单测日后逐渐补上。
title: 在玉泉环境下尝试将树莓派改造为路由器(分享过程,未成功)
categories:
几天前曾经想把手头的树莓派2B改造为无线路由器,实现寝室内多设备无线上网。由于奇葩的玉泉寝室网络,我踩了无数的坑,最终差在最后一步NAT上。由于这方面知识薄弱,今天把成功部分记载下来,期望以后有机会能实现。
在玉泉寝室有线上网主要分为两种:闪讯和VPN。闪讯帐号部分可以pppoe拨号,多数不可以。我的帐号属于不可以的类型,所以打算让树莓派先使用VPN连接上有线网络,再通过一个无线网卡将信号发射出来。
本文所述适合于浙江大学玉泉校区寝室网络,树莓派系统为Raspbian。
由于寝室内使用静态ip绑定,每个人的静态ip都绑定了网卡MAC地址,所以先设置静态ip和mac地址。(如果你直接使用树莓派网卡MAC进行申请就无需修改)。
sudo ifconfig eth0 down hw ether xx:xx:xx:xx:xx:xx
sudo ifconfig eth0 up
运行后使用ifconfig可以看到修改结果。
2.设置静态ip、网关等。此时需要修改/etc/network/interfaces,修改前先进行备份:
pi@raspberry:sudo cp /etc/network/interfaces /etc/network/interfaces.sav
打开文件:
pi@raspberry:sudo nano /etc/network/interfaces
关闭DHCP,将此行注释掉:
iface eth0 inet dhcp
修改为:
#iface eth0 inet dhcp
接下来进行更详细的修改:
# The loopback interface
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
#your static IP
address 192.168.1.118 //修改为你的静态ip
#your gateway IP
gateway 192.168.1.1 //修改为你的网关
netmask 255.255.255.0
#your network address "family"
network 192.168.1.0 //修改到同一域下
broadcast 192.168.1.255
修改后重启网络:
pi@raspberry:sudo /etc/init.d/networking restart
设置DNS,在/etc/resolv.conf中添加一行nameserver 10.10.0.21
保存退出。
此时用ifconfig可以查看修改成没成功,正常的话可以ping通10.10.0.21、www.cc98.org,已经可以上内网了。并且至少在校园范围内已经可以使用ssh进行连接了。
如果你的闪讯帐号支持pppoe,那么你可以自行下载pppoe有关包进行连接,本文以下讲述的是l2tp方式连接外网。
1.下载libpcap0.8, ppp, xl2tpd, zjuvpn四个软件包,使用scp命令或者U盘复制到树莓派内。
2.按照顺序安装四个软件包:
sudo dpkg –i libpcap0.8_1.3.0-1_armhf.deb
sudo dpkg –i ppp_2.4.5-5.1_armhf.deb
sudo dpkg –i xl2tpd_1.3.1+dfsg-1_armhf.deb
tar –zxvf zjuvpn-8.2.tar.gz –C /
执行:
sudo zjuvpn –c
按照提示输入vpn的用户名和密码就可以连接上外网了。
此阶段主要参考将树莓派Raspberry Pi设置为无线路由器(WiFi热点AP,RTL8188CUS芯片)这篇文章,我的无线网卡也是RTL8188CUS芯片的。要多走一步兼容性措施。
sudo apt-get install bridge-utils hostapd
需要注意的是官方提供的程序不兼容RTL8188CUS芯片的无线网卡,不过Edimax团队为我们专门编译了兼容的版本,下面的操作需要替换hostapd为兼容版本。
如果你的网卡芯片为RTL8188CUS,需要执行下述指令:
wget http://www.daveconroy.com/wp3/wp-content/uploads/2013/07/hostapd.zip
unzip hostapd.zip
sudo mv /usr/sbin/hostapd /usr/sbin/hostapd.bak
sudo mv hostapd /usr/sbin/hostapd.edimax
sudo ln -sf /usr/sbin/hostapd.edimax /usr/sbin/hostapd
sudo chown root.root /usr/sbin/hostapd
sudo chmod 755 /usr/sbin/hostapd
安装必备程序udhcpd,udhcpd主要为连接到WiFi的设备自动分配IP地址。
sudo apt-get install udhcpd
配置udhcpd,编辑/etc/udhcpd.conf:
start 192.168.42.2 # This is the range of IPs that the hostspot will give to client devices.
end 192.168.42.20
interface wlan0 # The device uDHCP listens on.
remaining yes
opt dns 8.8.8.8 4.2.2.2 # The DNS servers client devices will use.
opt subnet 255.255.255.0
opt router 192.168.42.1 # The Pi's IP address on wlan0 which we will set up shortly.
opt lease 864000 # 10 day DHCP lease time in seconds
编辑/etc/default/udhcpd并且将下面这行注释掉,以使DHCP Server正常工作:
#DHCPD_ENABLED="no"
配置无线网卡,给无线网卡设置一个IP地址:
sudo ifconfig wlan0 192.168.42.1
为了下次启动仍然有效,配置/etc/network/interfaces文件:
sudo nano /etc/network/interfaces
注释掉所有的关于无线网卡的部分,最后应该变成下面所示:
#wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
#iface default inet dhcp
注意保留allow-hotplug wlan0,英文原文是不保留的,但是我操作下来发现如果不保留这段的话,无线网卡有时无法正常配置IP,最后无线网卡IP的配置信息如下:
allow-hotplug wlan0
iface wlan0 inet static
address 192.168.42.1
netmask 255.255.255.0
编辑hostapd配置:
sudo nano /etc/hostapd/hostapd.conf
内容如下:
interface=wlan0
driver=rtl871xdrv
ssid=My_SSID_Name//改为自定义ssid
hw_mode=g
channel=6
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=MYPASSWORD//设置密码
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
启动IP转向功能以便于开通NAT:
sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
配置iptables防火墙
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
sudo iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT
到这里路由的NAT功能已经被启用,保存iptables以便于下次使用
sudo sh -c "iptables-save > /etc/iptables.ipv4.nat"
编辑/etc/network/interfaces然后在最后加上下面这行以使每次启动都自动加载iptables配置:
up iptables-restore < /etc/iptables.ipv4.nat
重启并测试hostapd
sudo reboot
重启完成后,使用下面的命令测试是否正确:
sudo hostapd -dd /etc/hostapd/hostapd.conf
果没有错误的话,你这时应该能搜索到你所配置的无线信号。然后Ctrl+C退出这个测试。
如果一切正常的话,我们可以设置hostapd的配置文件路径了。
sudo nano /etc/default/hostapd
去掉注释符号并改动下面这行为我们的配置文件路径:
DAEMON_CONF="/etc/hostapd/hostapd.conf"
启动相应软件
sudo service hostapd start
sudo service udhcpd start
当我执行到这一步的时候,其他设备可以搜索到树莓派的无线信号,也可以连接上,但是不能上网。此时查看hostapd的日志,wpa的四次握手已经通过,设备与树莓派的连接是没有问题的。
自我感觉问题应该是出在L2TP上,原文作者没有用l2tp,直接在eth0和wlan0间做NAT。实际上在使用l2tp拨号之后,又新生成了ppp0,应该对ppp0再进行一些设置,这方面我懂得知识很少,没能解决。:(
操作的时候足足花了一个晚上加一个上午,出现各种各样奇怪的问题,有的地方是自己马虎,有的地方是奇怪的错误……
比如按照原文设置加入的那些启动项,有的重启后并没有生效,还需要自己手动再敲一遍。随后sudo /etc/init.d/networking restart
的时候会出错,连续输入两遍才能成功……
参考资料:
title: 分享 - Web 开发从入门到放弃 Part One
categories:
这是一次跨部门分享的 PPT,主要面向对前端感兴趣有需求的其他部门同学,做前端入门指引。
<iframe src="//www.slideshare.net/slideshow/embed_code/key/7e1jMbaqbxTZZh" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe>做 PPT 太难了!大概 15 分钟我才能憋出1页,这 50 页的 PPT 憋了我三四天!(泪)
title: 年前写的一个小爬虫
categories:
在这里放一下代码。
有点奇怪,在markdown中开启代码区段之后粘贴大段代码进去,缩进和换行都显示不正常,所以下面的代码的空白行我加了个空格这样似乎显示正常一些。
app.js:
var fs = require('fs');
var cheerio = require('cheerio');
var async = require('async');
var superagent = require('superagent');
var mongodb = require('./db');
var path = require('path');
var http = require('http');
var nhentaiLink = "http://nhentai.net/g/";
var imageLink = "http://i.nhentai.net/galleries/";
var startNum = 500;
var endNum = 500;
var urls = [];
var galleryUrls = [];
makeGalleryUrls(startNum,endNum);
setTimeout(function(){
fetchData(1,galleryUrls.length);
//console.log(galleryUrls);
},2000);
function fetchInfoDB(){
makeUrls(startNum,endNum);
mongodb.open(function(err,db){
if(err){console.error(err);}
console.info('mongodb connected');
async.mapLimit(urls,10,function(url,callback){
ClawInfo(url,db,callback);
},function(err,result){
if(err){console.error(err);}
});
});
}
function fetchData(i,max){
if(i > max){mongodb.close(); return null;}
var imageUrls = [];
//console.log(codea);
//console.log(galleryUrls);
//console.log(i);
imageUrls.push(galleryUrls[i-1].GallleryUrl+'cover.'+galleryUrls[i-1].Format);
for(var j=1;j<=galleryUrls[i-1].Page;j++){
imageUrls.push(galleryUrls[i-1].GallleryUrl+j+'.'+galleryUrls[i-1].Format);
}
//console.log(imageUrls);
console.log('开始抓取第'+galleryUrls[i-1].Code+'项:'+galleryUrls[i-1].GallleryUrl+',共'+galleryUrls[i-1].Page+'页。');
var dir = galleryUrls[i-1].Title?'data/'+galleryUrls[i-1].Code+'. '+galleryUrls[i-1].Title:'data/'+galleryUrls[i-1].Code+'. '+galleryUrls[i-1].EngTitle;
fs.mkdir(dir,function(err){
if(err){
//console.log('error occured.');
dir = 'data/'+galleryUrls[i-1].Code;
fs.mkdirSync(dir);
}
async.mapLimit(imageUrls,4,function(imageUrl,callback){
ClawImage(imageUrl,dir,callback);
},function(err,result){
if(err){console.error(err);}
console.log('抓取第'+galleryUrls[i-1].Code+'项完成。');
i = i+1;
fetchData(i,max);
});
});
}
function ClawImage(url,dir,callback){
http.get(url,function(res){
res.setEncoding('binary');
var imagedata = '';
res.on('data',function(data){imagedata+=data}).on('end',function(){
var imageName = dir+'/'+url.match(/[\w]+.jpg|[\w]+.png|[\w]+.gif/)[0];
fs.writeFileSync(path.join(__dirname,imageName),imagedata,'binary');
console.log('下载'+url+'完成。');
callback(null);
});
});
}
function makeUrls(startNum,endNum){
for(var i=startNum;i<=endNum;i++){
urls.push(nhentaiLink + i +'/');
}
}
function makeGalleryUrls(startNum,endNum){
mongodb.open(function(err,db){
if(err){console.error(err);}
db.collection('items',function(err,collection){
if(err){console.error(err);}
pushGallery(startNum,endNum,collection);
});
});
}
function pushGallery(i,end,collection){
//console.log(i);
if(i>end){return;}
collection.findOne({Code:i},function(err,item){
if(err){console.error(err);}
//console.log(item);
if(!item){pushGallery(++i,end,collection);}
else{
galleryUrls.push({
Code:item.Code,
Title:item.Title,
EngTitle:item.EngTitle,
GallleryCode:item.GallleryCode,
GallleryUrl:imageLink+item.GallleryCode+'/',
Page:item.Page,
Format:item.Format
});
pushGallery(++i,end,collection);
}
})
}
function ClawInfo(url,db,callback){
var clawer = {};
GetInfo(url,clawer,function(clawer){
db.collection('items',function(err,collection){
if(err){mongodb.close();console.error(err);}
collection.insert(clawer,{safe:true},function(err,item){
if(err){return console.error(err);}
console.log('抓取'+url+'完成。');
});
});
});
setTimeout(function(){callback(null)},500);
}
function GetInfo(url,clawer,callback){
superagent.get(url)
.set('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.94 Safari/537.36')
.end(function(err,sres){
if(sres){
var $ = cheerio.load(sres.text);
clawer.Code = Number(url.match(/\d+/)[0]);
clawer.Title = $('#info h2').text();
clawer.EngTitle = $('#info h1').text();
clawer.Comiket = $('#info h1').text().match(/[Cc][5678]\d/)?$('#info h1').text().match(/[Cc][5678]\d/)[0]:'';
clawer.Parody = [];
clawer.Character = [];
clawer.Tag = [];
clawer.Artist = [];
clawer.Group = [];
$('#info div a').each(function(i,e){
var $e = $(e);
if($e.attr('href').search(/parody/)!= -1){clawer.Parody.push($e.attr('href').match(/[a-z][^\/]+/g)[1]);}
if($e.attr('href').search(/character/)!= -1){clawer.Character.push($e.attr('href').match(/[a-z][^\/]+/g)[1]);}
if($e.attr('href').search(/tagged/)!= -1){clawer.Tag.push($e.attr('href').match(/[a-z][^\/]+/g)[1]);}
if($e.attr('href').search(/artist/)!= -1){clawer.Artist.push($e.attr('href').match(/[a-z][^\/]+/g)[1]);}
if($e.attr('href').search(/group/)!= -1){clawer.Group.push($e.attr('href').match(/[a-z][^\/]+/g)[1]);}
});
clawer.Language = $('#info div.buttons').prev().prev().prev().text().match(/[A-Z]\w+/g)?$('#info div.buttons').prev().prev().prev().text().match(/[A-Z]\w+/g)[1]:'';
clawer.Page = $('#info div.buttons').prev().prev().text().match(/\d+/)?Number($('#info div.buttons').prev().prev().text().match(/\d+/)[0]):'';
clawer.GallleryCode = $('#cover img').attr('src')?Number($('#cover img').attr('src').match(/\d+/)[0]):'';
clawer.Format = $('.gallerythumb').eq(1).children('div.spinner').attr('data-src')?$('.gallerythumb').eq(1).children('div.spinner').attr('data-src').match(/\w+$/)[0]:'';
clawer.Favorite = $('div.buttons span.nobold').text()?Number($('div.buttons span.nobold').text().match(/\d+/)[0]):'';
}
callback(clawer);
});
}
db.js
var Db = require('mongodb').Db,
Connection = require('mongodb').Connection,
Server = require('mongodb').Server;
module.exports = new Db('nhentai', new Server('localhost', 27017),
{safe: true});
主要功能是抓取nhentai(咳咳好孩子不要点)上的所有本子存入本地,同时将本子信息存入数据库以便查询和归档。
用法:只需修改startNum和endNum的值,即从第几项抓到第几项。nhentai上现在约有不到13万项,数据库抓完了,本子过于庞大(全抓TB级)还没想好存放在哪里。
主函数:fetchData()与fetchInfoDB(),前者抓取数据并保存在当前目录data文件夹下,后者抓取信息存储在mongodb数据库中。需要先抓取信息,再抓取数据。
现在一想加个命令行界面比较好,当时没有弄。
测试爬虫的时候遇到的一些情况:
抓取信息因为数据量很小,又很快,所以没有什么大情况。所需要的大概只是增加容错性:遇到错误(要抓的数据缺失,存在失效链接)要提供解决方法,大批量任务正在进行,程序不能因此卡住。
抓取数据时,抓取的是大量成组图片,每组保存到某文件夹下。当初的想法时任务队列中异步抓两组--每组异步抓五张图片,使用async库,随后发现无论是map、mapLimit、mapSeries都无法保证顺序。无法保证顺序的后果就是当抓取某组时因错停止,调试完就很难续传了。所以用递归变成了每次抓1组,但是顺序是死的。
说到异步处理,很多时候也是要注意一下处理方式……
比如,许多任务想按顺序完成,使用for:
for (){
//异步函数+callback……
fs.mkdir(dir,callback(){})
}
那么一瞬间for就执行完毕,瞬间注册了大量的异步事件,并没有等上一个完成再进行下一个。
这时候可以把异步函数换成对应的同步函数,否则可以使用递归:
(function makedir(i){
if(i === 任务总数) return;
fs.mkdir(dir,callback(){
makedir(++i);
})
})(1);
这样可以保证按顺序执行,缺点是不好看。app.js中就是使用递归来写的。
明天家里的网似乎能升级到100M带宽,如果电信真的不坑的话好想抓抓试试呢:)
title: flexbox在移动端的兼容性实战
categories:
之前曾经写过文章介绍flexbox布局,这个玩意相比与传统的float布局,简直是大大解放了生产力。但是说到应用于生产的话,flex布局的兼容性实在是让人没信心。PC端彻底没戏,IE10才支持部分混合语法;不过caniuse上查阅到的移动端兼容性还算不错,Android2.1起开始支持旧语法,iOS safari从3.2开始支持旧语法。在v2上查阅了些帖子,很多人说移动端上兼容是坑,不过观察可知,大部分人都没有专门去研究旧语法。flex在移动端表现究竟如何,本文来探讨下这个话题。
先来看一眼Caniuse上的资料吧。
Android | 2.1-4.3 | 4.4+ |
---|---|---|
支持情况 | 旧语法(不支持换行),需要带-webkit-前缀 | 新语法 |
iOS | 3.2-6.1 | 7.1+ |
---|---|---|
支持情况 | 旧语法(不支持换行),需要带-webkit-前缀 | 新语法 |
可以看到对于旧语法的支持还是不错的,不过旧语法相比与新语法,功能如何,有啥缺陷?看下面这个我整理的表吧。
新语法 | 旧语法 | 备注 | |
---|---|---|---|
开启flexbox | display: flex/inline-flex | display: box/inline-box | |
伸缩性 | flex | box-flex | 旧语法的box-flex只有一个参数,作用等同与flex-grow |
flex-grow | / | ||
flex-shrink | / | ||
flex-basis | / | ||
主轴方向与换行的简写 | flex-flow | / | |
主轴方向 | flex-direction | box-direction/box-orient | 可以通过box-orient:horizontal + box-direction:normal 达到新版本 flex-direction:row 的效果; |
换行 | flex-wrap | box-lines | |
主轴对齐方式 | justify-content | box-pack | |
侧轴对齐方式 | align-items | box-align | |
单个伸缩项目侧轴对齐方式 | align-self | / | |
伸缩项目行侧轴对齐方式 | align-content | / | |
显示顺序 | order | box-ordinal-group |
可以看到,旧语法的功能比新语法弱一些,不能支持flex项目的flex-shrink、flex-basis(影响收缩),不支持单个flex项目的侧轴对齐方式,不支持flex项目行侧轴对齐方式。但是依然可以实现大部分功能。
让我们用真机检验一下吧。
测试环境:Android 2.3.7,Android4.1.1,Android4.4,ios6,ios9
Android 4.4+ 与ios9+ 支持新语法,不需要做兼容性即可使用。
表中查阅可知Android 2.1 - Android 4.3 部分支持旧语法,本次使用了2.3.7与4.1.1进行了测试,测试结果如下:
ios6测试结果同上。水平垂直居中与等分功能已经可以实现,表现稳定。
可以看到,严格遵循规则的话,flex的可用性已经足够高了,足够把我们从之前各种繁琐的花式布局解放出来。想当年,做一个完美的垂直居中要烧多少脑细胞~(见我之前整理的6种花式垂直居中做法),用了flex,一句话就搞定啦。
现在在用flexbox布局(配合sass的mixin做兼容)+lib-flexible库做移动端自适应匹配,基本能做到写一次代码,匹配全部移动机型,生产力飞了起来。
常见情况的兼容性写法,包括PC端与移动端:
更多详细请参考该mixin
/* 父元素-flex容器 */
.flex {
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
/* 子元素-等分 */
.flex1 {
-webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: 1; /* OLD - Firefox 19- */
-webkit-flex: 1; /* Chrome */
-ms-flex: 1; /* IE 10 */
flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
/* 父元素-横向排列(主轴) */
.flex-h {
/* 09版 */
-webkit-box-orient: horizontal;
/* 12版 */
-webkit-flex-direction: row;
-moz-flex-direction: row;
-ms-flex-direction: row;
-o-flex-direction: row;
flex-direction: row;
}
/* 父元素-换行 */
.flex-hw {
/* 09版 */
/*-webkit-box-lines: multiple;*/
/* 12版 */
-webkit-flex-wrap: wrap;
-moz-flex-wrap: wrap;
-ms-flex-wrap: wrap;
-o-flex-wrap: wrap;
flex-wrap: wrap;
}
/* 父元素-主轴方向居中 */
.flex-hc {
/* 09版 */
-webkit-box-pack: center;
/* 12版 */
-webkit-justify-content: center;
-moz-justify-content: center;
-ms-justify-content: center;
-o-justify-content: center;
justify-content: center;
/* 其它取值如下:
align-items 主轴原点方向对齐
flex-end 主轴延伸方向对齐
space-between 等间距排列,首尾不留白
space-around 等间距排列,首尾留白
*/
}
/* 父元素-纵向排列(主轴) */
.flex-v {
/* 09版 */
-webkit-box-orient: vertical;
/* 12版 */
-webkit-flex-direction: column;
-moz-flex-direction: column;
-ms-flex-direction: column;
-o-flex-direction: column;
flex-direction: column;
}
/* 父元素-侧轴居中) */
.flex-vc {
/* 09版 */
-webkit-box-align: center;
/* 12版 */
-webkit-align-items: center;
-moz-align-items: center;
-ms-align-items: center;
-o-align-items: center;
align-items: center;
}
/* 子元素-显示在从左向右(从上向下)第1个位置,用于改变源文档顺序显示 */
.flex-1 {
-webkit-box-ordinal-group: 1; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-ordinal-group: 1; /* OLD - Firefox 19- */
-ms-flex-order: 1; /* TWEENER - IE 10 */
-webkit-order: 1; /* NEW - Chrome */
order: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
/* 子元素-显示在从左向右(从上向下)第2个位置,用于改变源文档顺序显示 */
.flex-2 {
-webkit-box-ordinal-group: 2; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-ordinal-group: 2; /* OLD - Firefox 19- */
-ms-flex-order: 2; /* TWEENER - IE 10 */
-webkit-order: 2; /* NEW - Chrome */
order: 2; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
参考资料:
title: 常见知识点汇总(三):XMLHttpRequest
date: 2014-12-18 22:02:41
categories:
XMLHttpRequest(以下简称XMR)是一组API函数集,可被JavaScript、JScript、VBScript以及其它web浏览器内嵌的脚本语言调用,通过HTTP在浏览器和web服务器之间收发XML或其它数据。
function createXMLHTTP(){
if (typeof arguments.callee.activeXString != "string"){
var versions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"];
for (var i=0,len=versions.length; i < len; i++){
try {
var xhr = new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
return xhr;
} catch (ex){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
}
这个函数会尽力根据IE中可用的MSXM库的情况创建最新版本的XHR对象。
这些浏览器都支持原生的XHR对象。在这些浏览器中创建XHR对象要像下面这样使用XMLHttpRequest构造函数:
var xhr = new XMLHttpRequest();
若想兼容所有浏览器:
//一个兼容所有浏览器的创建XHR对象的方法:
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"];
for (var i=0,len=versions.length; i < len; i++){
try {
var xhr = new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
return xhr;
} catch (ex){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
1.open()
使用XHR对象时,要调用的第一个方法是open(),它接收3个参数:要发送的请求的类型(”get“、”post“等)、请求的URL和表示是否异步发送请求的布尔值。例:
xhr.open("get","example.php",false);
这行代码会启动一个针对example.php的GET请求。URL相对于执行代码的当前页面(当然也可以使用绝对路径),调用open()方法并不会真正发送请求,而只是启动一个请求以备发送。
2.send()
要发送特定的请求,必须像下面这样调用send()方法:
xhr.open("get","example.php",false);
xhr.send(null);
这里的send()方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入null,因为这个参数对于某些浏览器是必须的。调用send()之后,请求就会被分派到服务器。
收到响应之后,响应的数据会自动填充XHR对象的属性,相关的属性简介如下:
在接收到响应后,第一步是检查status属性,以确保响应成功返回。建议使用检测status来决定下一步的操作,不要依赖statusText,因为后者在跨浏览器使用时不太可靠。
多数情况下,我们要发送异步请求。此时可以检测XHR对象的readyState属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取值如下:
每当readyState值发生变化,就会触发一次readystatechange事件,可以利用这个事件来检测每次状态变化后readyState的值,通常我们只用检测readyState值为4的阶段。必须在调用open()之前指定onreadystatechange事件处理程序才能保证跨浏览器兼容性。
例:
var xhr = createXHR();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if((xhr.status >=200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsucessful:" + xhr.status);
}
}
};
xhr.open("get","example.txt",true);
xhr.send(null);
3.HTTP头部信息
默认情况下,在发送XHR请求的同时,还会发送下列头部信息:
每个HTTP请求和响应都会带有响应的头部信息,XHR对象提供了操作这两种头部信息的方法。
使用setRequestHeader()方法可以设置自定义的请求头部信息。要成功发送请求头部信息,必须在调用open()方法之后且调用send()方法之前调用setRequestHeader()。
var xhr = createXHR();
xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
调用XHR对象的getResponseHeader()方法并传入头部字段名称,可以取得响应的响应头部信息。
调用getAllResponseHeaders()方法则可以取得一个包含所有头部信息的长字符串。
var xhr3 = createXHR();
xhr3.onreadystatechange = function(event){
if (xhr3.readyState == 4){
if ((xhr3.status >= 200 && xhr3.status < 300) || xhr3.status == 304){
alert(xhr3.getAllResponseHeaders());//获取全部头部信息的字符串
} else {
alert("Request was unsuccessful: " + xhr3.status);
}
}
};
xhr3.open("get", "example.php", true);
xhr3.send(null);
4.GET请求
//get请求格式:xhr.open('get', 'example.php?name1=value1&name2=value2', true);
//创建一个函数用于对url进行编码
function addURLParam(url, name, value){
url += (url.indexOf('?')==-1 ? '?' : '&');
url += encodeURIComponent(name) + '=' + encodeURIComponent(value);
return url;
}
var url = 'get.php';
url = addURLParam(url, 'name', 'Alvin');
url = addURLParam(url, 'age', '23');
var xhr4 = createXHR();
xhr4.onreadystatechange = function(event){
if (xhr4.readyState == 4){
if ((xhr4.status >= 200 && xhr4.status < 300) || xhr4.status == 304){
alert(xhr4.responseText);
} else {
alert("Request was unsuccessful: " + xhr4.status);
}
}
};
xhr4.open('get', url, false);
xhr4.send(null);
5.POST请求
var xhr5 = createXHR();
xhr5.onreadystatechange = function(event){
if (xhr5.readyState == 4){
if ((xhr5.status >= 200 && xhr5.status < 300) || xhr5.status == 304){
alert(xhr5.responseText);
} else {
alert("Request was unsuccessful: " + xhr5.status);
}
}
};
xhr5.open("post", "postexample.php", true);
//模拟浏览器行为,所有由浏览器发出的post请求都将'content-type'头部设置为'application/x-www-form-urlencoded'
xhr5.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr5.send('name=alvin&age=23');
title: 使用 Rx 流式处理数据
categories:
最近频繁用到 Vue 的计算属性(computed property
),对其背后 Reactive Programming 的**很感兴趣。所以去了解了下 RP **与它的优秀的 JavaScript 实现:RxJS。这是一篇简单的 Rx 入门文章。
RP 使用异步数据流(Asynchronous event stream)进行编程。什么是异步数据流?想象一下我们如何使用 DOM 事件来获取 DOM 的状态变化:
HTML:
<input>
JavaScript:
$('input').on('keyup', handleKeyup)
function handleKeyup(e) {
...
}
每当 input
节点触发 keyup
事件时,我们即可获得 keyup
事件的 event
对象,随后通过 handleClick
方法对 event
对象进行操作,最终得出 result
。DOM 事件的监听是持续的,多次触发 keyup
事件,这个流程就会多次执行,绘图如下:
流(stream)其实就是一个按时间排序的 Events 序列(Ongoing events ordered in time),在 Web 中,流可能就是一系列的鼠标点击事件、也可能是 setInterval 生产的 / websocket 拉来的一系列数据等。Event buses 本质上就是异步事件流(Asynchronous event stream),你可以监听并处理这些事件。在 Rx 中,任何东西都可以是一个流:变量、用户输入、数据结构等。你可以监听流的变动并做出响应。处理流就如同处理一个 Array 一样简单——你可以对流应用 forEach、map、filter、merge 等等操作。除此之外, Rx 还提供了更多令人惊艳的操作符。
监听流也被称为订阅(Subscribing),这里使用的就是观察者模式。
观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
让我们观察一个处理异步请求的场景——从服务器端拉取数据并进行处理,我们已经很擅长使用 Callback 和 Promise 来解决这种问题了:
Callback:
function getResultFromServer(term, callback) {
return $.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
search: term,
_: '1'
},
success: (data) => {
callback(data)
}
})
}
getResultFromServer('hello', (data) => {
console.log(data)
})
Promise:
function getResultFromServer(term) {
return $.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
search: term,
_: '1'
}
}).promise()
}
getResultFromServer('hello').then(data => {
console.log(data)
})
让我们试一试使用观察者模式来处理这个异步请求:
function getResultFromServer(term) {
return $.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
search: term,
_: '1'
},
success: (data) => {
$(document).trigger('getData', data)
}
})
}
$(document).on('getData', (data) => {
console.log(data)
})
getResultFromServer('hello')
从上可知,观察者模式同样可以很好的处理异步请求。同时与 promise 相比可以发现:promise 的 then 方法只能调用一次;而观察者模式其实可以多次订阅事件,当新数据到达时所有订阅者都将收到通知。
让我们看一个例子:
这是一个每天都被使用数亿次的组件——百度的搜索框。当输入搜索字符时,会拉取预测搜索字段,并以下拉列表的形式展示。这个功能应该如何实现?
略加思考即可得到大致思路:从 DOM 中获取数据 -> 发起 ajax 请求 -> 得到预测搜索字段 -> 渲染为 DOM -> 插入文档流。如下图所示:
其实,我们平时所做的每一个需求,都可以抽象成为一系列数据的流动:
对于可变的数据则为:
简单讲如下图所示:
我们所看到的每一个页面,背后都是一条条的数据加工流水线。让我们看一下使用 Rx 来解决上文需求的代码:
let app$ = Rx.Observable
.fromEvent(document.querySelector('input'), 'keyup') //观察 keyup 事件触发时的 event 对象流
.pluck('target', 'value') //拿到 event.target.value 值,即输入内容
.switchMap((term) => Rx.Observable.fromPromise(getResultFromServer(term))) //发起请求,并观察响应结果数据流,使用其替换掉当前流
.do(generateDOM) //根据结果生成 DOM 并插入文档
.subscribe() //开始订阅,打开流水线开关
可以看到,使用 Rx 写出的是一串链式操作代码,将该业务需求简明易懂的抽象成了一条数据流水线。该需求的原始数据来源、加工过程、最终输出结果都被封装到了一起,实现了优雅、高内聚的业务逻辑抽象。这种思考方式提高了代码的抽象层级,你可以只关注定义了业务逻辑的那些相互依赖的事件,并非纠缠于大量的实现细节。
现在的页面往往存在着各种各样的实时 Events 来给用户提供具有较高交互性的体验,复杂度的增加对代码的可读性、可维护性带来了新的挑战。而 Rx 则是一个新的工具来帮助我们应对复杂度问题。
通过上面的例子,我们已经可以看到 Rx 对于抽象业务逻辑的能力。然而 Rx 的功能远远不止于此,它提供的一系列操作符可以非常方便的实现强大的异步处理功能。
上一步我们编写的搜索框只实现了最基本的 Autocomplete 功能。假设我们要对其进行优化:
如果不借助 Rx,实现上述3个需求还是比较繁琐的。实现 debounce 功能需要自己定义计时器,在每次 keyup 事件触发后进行时间监测;实现 2 功能需要缓存上一次请求时的搜索内容,发送新请求时进行内容对比,内容有变化时再发送请求;Promise 的超时重试也需要一定代码量。
使用 Rx 的操作符,只需这样写:
let app$ = Rx.Observable
.fromEvent(document.querySelector('input'), 'keyup')
.pluck('target', 'value')
.debounceTime(500) // 加入 debounce 特性,停止输入 500ms 之后再发送请求
.distinctUntilChanged() //内容不变时不再继续流水线
.switchMap((term) => Rx.Observable.fromPromise(getResultFromServer(term)).retry(3)) // 对 Promise 加入重试3次操作,switchMap 后前面的请求会被自动 cancel 掉,天然避免竞态问题
.do(generateDOM)
.subscribe()
借助 debounceTime、distinctUntilChanged、retry 操作符,上述4个功能的实现就非常简单。这个示例仅为 Rx 强大功能的冰山一角。Rx **有百余个操作符,帮助你应对各种异步处理操作。
本文通过一个简单的示例来展示 Rx 强大的逻辑抽象能力和异步处理能力。Rx 可以与框架同时使用,有对应的库来做 binding 工作: rx-react, vue-rx。实时性强、异步操作多的场景下更适合使用 Rx。更多 API 可以查阅参考资料中的 ReactiveX 文档。 Enjoy Rx!
title: 迟来的关于ifetask0003的总结
categories:
不好意思,很久没有更新blog了。ifetask0003大概6月下旬完成,后来面临期末考试等杂七杂八的事项,一直没能拿出时间写文。
ifetask0003是一个webapp的应用,在写之前我曾犹豫过要不要使用angular这种框架,后来看要求说不推荐使用框架,那么也就自己纯js实现一下吧。
时间拖了有点久了,现在看了看,也没回忆起多少,大概就这2点:
平稳退化
编写这个webapp时用到了一些html5的标签,比如上方的标题用header简直再合适不过了。而对于兼容性则使用html5shiv来处理。
提到这些,是想表达我的一个观点:技术,在有shim方案的情况下,能用新的就用新的。之前在公司的一个项目中,有一个小小的打分模块:
背后的圆环要根据分数显示。同事跟我讲,之前做过这个东西了,可以拿来复用,我看了下之前是通过CSS做的,很复杂,两张图片交叠,高级浏览器使用CSS3的旋转,低级浏览器好像是使用了什么数学函数,结果IE7还不支持,蛋疼菊紧。
其实这个东西一眼看上去就可以用canvas做的。canvas的shim方案大佬谷歌有提供:excanvas。这个库的功能是在低版本IE将canvas转变为VML对象进行绘制。当然,过于复杂的操作是不支持,不过画个弧,设置个线宽,设置个颜色还是小轻松的——而且学习成本非常低廉。于是最终用canvas + excanvas,不需要其他学习成本,顺利实现IE7+的圆弧绘制。
这也是我逐渐感悟到的:熟悉IE6、IE7的各种bug各种缺陷各种hack有个卵用,这些浏览器逐渐都被淘汰,现在为了掌握各种奇技淫巧所花的时间最后终将成为无用的沉默成本。能用高级技巧+shim就要抛弃低级的奇技淫巧,能用ES6+babel就大胆用ES6,开发者的时间应该花费在美好的事物上,而不是给IE擦屁股。
数据结构
很明显这个webapp有这种树形结构:分类-子分类-具体任务,于是我建了个总object,下属cates-cateChilds-tasks。
而相对应的则有解析数据结构并显示的函数,renderCates(),renderTasks(),renderDetail()。这里就提到了数据传递的问题。比如我把某个任务设置成“已完成了”,那么对应分类列表和任务列表都要有更新,也就是重新渲染。重新render的话,参数去哪里找呢?这里我用的方法就是在每次渲染时,把此次渲染的参数写在对应wrapper的data-*中,以后在这里存取数据。
在公司做后台开发时,很多时候也有这种需求,大概也就两种方式:①把数据存在某个稳定的dom元素的data-*中,②干脆js中新建个变量存储。
这两种方法我都不甚满意,在操作时会产生大量存-取-存-取这种非常无聊的“脏代码”,严重增加了代码冗余度。
也许angular这种MVVM框架的数据绑定适合解决这种问题吧……我了解的不多,就不多说了。
ifetask0004打算找时间补一下,恩,就最近!
title: 常见知识点汇总(一):DOM结构与DOM操作
date: 2014-12-08 23:58:41
categories:
节点关系可用上图概括。
由图知常见的移动方式:
每个节点都有一个名为childNodes的属性,其中保存着一个NodeList对象。访问保存在其中的节点可以通过方括号或者item()来表示。
var firstChild = someNode.childNodes[0];
var firstChild = someNode.childNodes.item(0);
关于NodeList
NodeList是一种类数组对象,可以保存一组有序的节点。虽然其可以使用方括号语法来访问,并且这个对象也有length属性,但并不是Array的示例。NodeList是基于DOM结构动态执行查询的结果,能够自动反映DOM结构变化并实时更新,而不是在第一次访问它的某个瞬间拍摄下来的一张快照。
使用Array.prototype.slice()方法可以将NodeList及arguments转换为数组。
关于子节点
元素的childNodes属性中包含了它的所有子节点,子节点包括元素、文本节点、注释或处理指令等等,不同浏览器在看待这些节点方面存在显著不同。
<ul id="testlist">
<li>LI1</li>
<li>LI2</li>
</ul>
若代码为上,在IE中,ul元素有2个子节点,分别为两个li元素;在其他浏览器中,ul元素有5个子节点,包括2个li元素和3个文本节点(表示li元素间的空白符。)
示例:
如果像下面这样去除空白符:
<ul id="testlist"><li>LI1</li><li>LI2</li></ul>
那么在所有浏览器中都会有相同的结果:ul元素有2个子节点,分别为2个li元素。
示例:
创建元素节点
var div = document.createElement("div");
在IE中,可以传入完整元素标签来创建:
var div = document.createElement("<div id=\"NewDiv\" class=\"box\"></div>");
创建文本节点
var textNode = document.createTextNode("<strong>Hello</strong> world!");
一般情况下每个元素只有一个文本子节点,但是可以为其添加多个,如果两个文本节点是相邻的同胞节点,那么这两个节点中的文本就会连起来显示。中间不会有空格。
创建注释节点
var comment = document.createComment("A comment");
浏览器不会识别位于标签之后的注释,所以如果要访问注释节点,一定要保证其为元素的后代(即位于和之间)。
创建文档片段节点
var fragment = document.createDocumentFragment();
创建特性节点
var attr = document.createAttribute("align");
特性节点不被认为是文档树的部分。相比直接引用特性节点,getAttribute()、setAttribute()、removeAttribute()方法更为常用。
删除元素节点
document.body.removeChild(thisDiv);
删除特性节点
thisDIV.removeAttribute('color');
thisDIV.removeAttributeNode(color);
var returnNode = someNode.appendChild(newNode);
appendChild()用于向childNodes列表的末尾添加一个节点。更新完成之后,appendChild()会返回新增的节点。
若传入appendChild()中的节点已经是文档的一部分,那么结果则是将该节点从原来的位置转移到新位置上。
如果要将节点插入至childNodes列表中某特定位置,可以使用:
insertBefore(要插入的节点,作为参照的节点);
replaceChild(要插入的节点,要替换的节点);
removeChild(要移除的节点);
cloneNode()用于创建调用这个方法的节点的另一个完全相同的副本。它接受一个布尔值参数,在参数为true时,执行深复制,复制节点及整个子节点树;在参数为false时,执行浅复制,只复制节点本身。复制后的节点副本属于文档所有,为孤儿状态。
var deepList = myList.cloneNode(true);
var shallowList = myList.cloneNode(false);
cloneNode()方法不会复制添加到DOM节点中的Javascript属性,例如事件处理程序等。
getElementsByTagName():返回带有指定标签名的对象的集合。
getElementsByName():返回带有指定名称的对象的集合。
getElementById() :返回对拥有指定 ID 的第一个对象的引用。
getElementsByClassName():返回带有指定类名的对象的集合。(HTML5新增)
title: 在mac下配置wine环境——自己wine一个小游戏
categories:
当我们想要在 mac 上运行 labview 等 windows 下的大型软件时,使用 PD 等虚拟机可以很好的解决需求;然而如果我们只想玩些同人小游戏什么的,再开一个操作系统就觉得有点累赘了,这时我们可以选择wine,更加轻量的执行 windows 应用。
wine 的另一个好处时可以将 windows 应用打包成 APP,这样就可以直接拷贝给别人玩儿了!
下面是踩过很多坑后总结出来的比较方便的在 mac 上配置 wine 的流程。
安装 Xcode
从 Mac Apple Store 安装
找百度网盘下的都是坏孩子哼!
安装 XQuzrtz
在这里下载安装。
安装 homebrew
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
温馨提示:homebrew 的源可选择使用清华大学TUNA镜像源提升速度
安装 wine
brew install wine
安装 winetricks
brew install winetricks
现在我们已经可以执行 windows 应用程序啦。终端下输入
wine setup.exe
就可以执行 setup.exe 咯。
执行
msiexec /i setup.msi
可以执行msi格式的安装程序。
虽然理论上我们已经可以跑 windows 应用程序了,但是游戏还不能玩哦。因为很多依赖都没有安装,比如 .NET Framework,比如 DirectX9,比如 VCRUNTIME 等等。这时候我们需要 winetricks 来帮助我们安装库。
winetricks dotnet35 d3dx9 xna31
东方幕华祭主要需要这三个库。已有库的列表可通过winetricks list
来查询。加参数-q
可以静默安装。
不过 winetricks 这东西内置了复杂的 conflict 系统,有冲突时就不给安装。。比如先装完 .net 2.0再装 .net 3.5会报错。只能选择直接装 .net 3.5。
到这里至少东方幕华祭已经可以运行了,不过不能完美运行。可以进行正常的单人游戏,但是在 music room 等地方会报错,没办法。。。
wine 有一份应用程序的兼容性文档,在装软件时可以先来查阅一下,会有兼容性评级和一些安装说明,十分好用!
简易的 wineskin 小教程!
安装 engine 和 wrapper。不过 wineskin 自带下载没速度,还是到mediafire下载吧。
下载时注意版本要和wine对应哦!
点击 Create new blank wrapper,输入文件名创建新包,弹出的提示都选 cancel,先不安装。
在 wrapper 里,运行 wineskin.app
选择 Advanced, tool 里有 winetricks 来装常用库,Configuration 中的 Windows EXE 中输入的应用程序路径就是最终打包成品 APP 双击运行的应用。如果有安装程序的话,路径填进去,选择 Test Run 跑一下。最后再把启动 exe 填进去。
PS. 如果之前在 wine 中装过程序了,直接把~/.wine
下的 driver_c
文件夹彻底拷贝到 wrapper/Content/Resources
下,然后指定下 exe 路径就可以直接拿来用。
wine 坑多坑大,很多程序不能完美运行,报错是免不了的,不要慌,google 多查一查!解决不了的,还是打开 PD 吧。
title: 《HTTP权威指南》笔记——各章脑图
categories:
《HTTP权威指南》看起来厚厚的600多页,读起来确意外的轻松。不过对于这种工具书,通读一遍之后往往有种“学了好多新知识,但啥也没记住”的感觉。于是速读了第二遍并做了些笔记。
PS. 众所周知前几天shadowsocks项目被爆破了,我在linode上搭的ss也屡遭针对,后来不得已试着切换到了conoha东京机房,但是ss在使用了不到1天后依然被墙。后来尝试其他姿势,lantern时断时续的根本不好用,日本机房的VPS几乎都无法搭可行的ss了,后来试着用squid搭了个https代理,结果真的是秒封(笑),搭建完之后上了一下youtube,就30秒不到,就再也ping不通服务器了。现在在蹭同学搬瓦工的ss,速度不理想,勉强用用,唉,真惨。
不过前几天因为兴趣倒是研究了些shadowsocks、VPN的原理,正好也看了些HTTP、TCP知识,打算写篇有关翻墙的文整理一下。大概包括常用的翻墙手段的原理,以及针对某些梯子的GFW的封锁手段。
title: JS中的模块规范
categories:
在介绍JS模块化开发,CommonJS、AMD、CMD规范之前,我们有必要弄清楚一下:为什么需要模块化开发?
模块化是指在解决一个庞大的复杂问题或者一系列杂糅问题时,依照一种分类的思维将问题系统分解以之处理。模块化是一种将复杂系统分解为一系列代码结构更合理、可维护性可管理性更高的模块的方式。将系统分解为一组高内聚、低耦合的模块,使得无论多么大的系统,也可以在管理、开发、维护上有理可循。
采用模块化开发之后,理想状态下我们只需完成自己部分的核心业务逻辑代码,其他方面的依赖加载别人写好的模块即可。
JS并不是一种模块化编程语言,它不支持类,更不要提模块了。(ES6将正式支持类和模块,但还需要很长时间才能投入使用。)Javascript社区做了很多努力,在现有的运行环境中,实现"模块"的效果。
1.原始写法:
function A(){
//...
}
function B(){
//...
}
缺点:污染全局变量,无法保证不和其他模块发生变量名冲突,模块成员之间看不出关系。
2.对象写法
var module1 = new Obejct({
_count: 0,
m1: function(){
//...
}
m2: function(){
//...
}
});
缺点:暴露模块成员,可以被直接修改
3.立即执行函数写法
var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
}
var m2 = function(){
//...
}
return {
m1: m1,
m2: m2
}
})();
这样外部代码就无法读取内部成员。
4.放大模式
var module1 = (function(mod){
mod.m3 = function(){
//...
}
return mod;
})(module1 || {});
这段代码为module1模块增添了一个新方法m3,然后返回一个新module1模块。由于在浏览器模块中可能无法知道哪个模块先加载,所以传入参数为(module1 || {}),也就是立即执行函数的参数可以是空对象
5.输入全局变量
独立性是模块的重要特点,模块内部最好不要与程序其他部分直接交互。
var module1 = (function($, YAHOO){
//...
})(JQuery, YAHOO);
module1模块需要JQuery和YUI库,就把和两个库(两个模块)当做参数输入module1中。这样做可以保证模块独立性,使得模块中的依赖关系更加明显。
CommonJS是服务器端模块的规范,Node.js采用了这个规范。 根据CommonJS规范,一个单独的文件就是一个模块。在CommonJS中,有一个全局性方法require(),用于加载模块。该方法读取一个文件并执行,最后返回文件内部的exports对象。假定有一个数学模块math.js,就可以像下面这样加载。
var math = require('math');
math.add(2,3);
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
默认时候,一个网页加载多个JS文件很常见的:
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>
这样的写法有很大的缺点。首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。
使用requireJS则可以解决这两个问题:
require.js用法:
先加载:
<script src="js/require.js"></script>
如果觉得这样加载也可能造成网页失去响应。那么一可以把它放在网页底部加载,二可以这么写:
<script src="js/require.js" defer async="true" ></script>
async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。
加载require.js以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。那么,只需要写成下面这样就行了:
<script src="js/require.js" data-main="js/main"></script>
data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。
主模块的写法:下面就来看,怎么写main.js。
如果我们的代码不依赖任何其他模块,那么可以直接写入javascript代码。
// main.js
alert("加载成功!");
但这样的话,就没必要使用require.js了。真正常见的情况是,主模块依赖于其他模块,这时就要使用AMD规范定义的的require()函数。
// main.js
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// some code here
});
require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
假定主模块依赖jquery、underscore和backbone这三个模块,main.js就可以这样写:
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
// some code here
});
使用require.config()方法,我们可以对模块的加载行为进行自定义。
require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
假定现在有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:
// math.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
加载方法如下:
// main.js
require(['math'], function (math){
alert(math.add(1,1));
});
如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
加载非规范的模块:是可以的,这样的模块在用require()加载之前,要先用require.config()方法,定义它们的一些特征。详见参考资料。
在CMD中,一个模块就是一个文件,格式为:define(factory);
全局函数define,用来定义模块。参数factory可以是一个函数,也可以为对象或者字符串。当factory为对象、字符串时,表示模块的接口就是该对象、字符串。
其与 AMD 规范用法不同。require 是 factory 的第一个参数。require(id);接受模块标识作为唯一的参数,用来获取其他模块提供的接口:
define(function(require, exports ){
var a = require('./a');
a.doSomething();
});
require是同步往下执行的,需要的异步加载模块可以使用 require.async 来进行加载:
define(function(require, exports, module) {
require.async('.a', function(a){
a.doSomething();
});
});
更多见参考资料,这里主要注意AMD与CMD的异同。
sea.js 核心特征:
seajs.use
用来在页面中加载一个或者多个模块
// 加载一个模块
seajs.use('./a');
// 加载模块,加载完成时执行回调
seajs.use('./a',function(a){
a.doSomething();
});
// 加载多个模块执行回调
seajs.use(['./a','./b'],function(a , b){
a.doSomething();
b.doSomething();
});
其define 与 require 使用方式基本就是CMD规范中的示例。
这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的。目前这些规范的实现都能达成浏览器端模块化开发的目的。
区别:
1.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
2.CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此处略去 100 行
var b = require('./b') // 依赖可以就近书写
b.doSomething()
// ...
})
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
// 此处略去 100 行
b.doSomething()
// ...
})
3.AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
参考资料
title: 使用 requestAnimationFrame 实现性能优化与懒执行
categories:
去年在《谈谈网页中的 Animation 》中曾经提到了 requestAnimationFrame 这个 API,非常适合用来做流畅的动画效果。其实除了做动画的优化,requestAnimationFrame 也可以做其他方面的性能优化。
requestAnimationFrame 的执行时机是在页面重绘之前,我们知道浏览器中 JS 的执行是会阻塞页面渲染的,所以 requestAnimationFrame 的执行时机同样代表着当前 JS 线程的空闲。这样的话,在一些 JS 负荷较重的页面,我们可以通过对 JS 任务区分优先级,实现关键任务的优先加载,非关键业务的懒加载。
例如一个店铺首页页面,由多个JS模块构成:店铺信息、店长信息、商品详情、导航条……等等。为了缩短首屏可交互时间,优化体验,我们可以把首屏出现的3个模块定为“关键模块”,立刻加载并渲染;其他模块推送到懒加载序列,等待空闲时再运行。相比与所有模块一同加载并渲染,首屏模块先加载先展现,可以有效缩短首屏的等待时间。
try {
for (var i = 0; i < componentList.length; i++) {
if(/601|602|701|702|703|801|802|803|/.test(componentList[i].sectionId)){
loadComponent(componentList[i].sectionId);
}else {
window.requestAnimationFrame(function(){ //直接这么写真的好么?请看下文
loadComponent(componentList[i].sectionId);
});
}
};
} catch (e) {
VTrace.report('error');
}
这是一个比较常被提起的话题,有时我们会写出一些高频率事件(常见于 mouseover、resize 事件或者 scroll 事件):
拿常见的 lazyload(图片懒加载)举个例子:
LazyLoad = {
// ... 省略掉无关的代码 ...
processScroll: function(){
for (var i = 0; i < LazyLoad.images.length; i++) {
if (LazyLoad.elementInViewport(LazyLoad.images[i])) {
(function(i){
LazyLoad.loadImage(LazyLoad.images[i]);
})(i)
}
};
}
}
window.addEventListener('scroll', LazyLoad.processScroll);
上面是 lazyload 的一段必备逻辑:在滚动时,判断懒加载的图片有没有出现在视口中,如果出现了,那么真正加载图片。scroll 事件就是一个非常常见的高频率事件。当浏览器滚动时,一秒可以触发 scroll 事件几十上百次,若 scroll 绑定的事件内部处理较复杂,耗时较多,非常容易出现 CPU 占用率飙升,网页 FPS 突降的现象。
使用防抖(Debouncing)处理,代码如下:
LazyLoad = {
// ... 省略掉无关的代码 ...
timer: null,
processScroll: function() {
if(LazyLoad.timer){
clearTimeout(LazyLoad.timer);
LazyLoad.timer = null;
}
LazyLoad.timer = setTimeout(function(){
for (var i = 0; i < LazyLoad.images.length; i++) {
if (LazyLoad.elementInViewport(LazyLoad.images[i])) {
(function(i){
LazyLoad.loadImage(LazyLoad.images[i]);
})(i)
}
};
},200);
},
}
window.addEventListener('scroll', LazyLoad.processScroll);
每次滚动后不是立即执行操作,而是使用 setTimeout 延时 200ms 后执行;如果连续多次触发 scroll 事件,后执行的操作会取消并重置计时器,也就是说连续滚动时不会执行操作,停手后的 0.2s 之后会执行操作。
这样做可以将多次的、连续的调用合并为一次。有效的防止了频繁触发事件带来的性能问题。缺点时:当用户一直滚动不放时,函数就一直不执行,用户就一直看不到图片。最佳做法是:高频率调用函数时,不能执行太频繁,但也不能一直不执行,至少要每 X 毫秒执行一次。这就是节流(Throttling)。
LazyLoad = {
// ... 省略掉无关的代码 ...
timer: null,
baseTimeStamp: null,
processScroll: function() {
var now = Date.now();
if(!LazyLoad.baseTimeStamp) LazyLoad.baseTimeStamp = now;
if(now - LazyLoad.baseTimeStamp > 200) {
LazyLoad.loadImages();
LazyLoad.baseTimeStamp = now;
clearTimeout(LazyLoad.timer);
LazyLoad.timer = null;
}else {
clearTimeout(LazyLoad.timer);
LazyLoad.timer = null;
LazyLoad.timer = setTimeout(LazyLoad.loadImages, 200);
}
},
//抽出操作函数来复用..
loadImages: function() {
for (var i = 0; i < LazyLoad.images.length; i++) {
if (LazyLoad.elementInViewport(LazyLoad.images[i])) {
(function(i){
LazyLoad.loadImage(LazyLoad.images[i]);
})(i)
}
};
}
}
window.addEventListener('scroll', LazyLoad.processScroll);
上面代码就实现了节流(Throttling),即使用户一直滚动不放手,也会每 200ms 执行一次,完美实现高频率事件的性能优化。
这个 200ms 是由我们掌控的,一般会设置一个保守一点的数字。如果想榨干浏览器性能,可以使用 requestAnimationFrame 来做节流,让浏览器自动在空闲时执行操作,可以在性能优化的前提下,尽量多执行操作,增强用户体验。(0.2秒毕竟也要顿一下...)
requestAnimationFrame 只会将操作延迟到下次重绘前执行,并不会主动做节流;所以网上某些参考资料的实例代码是错误的,如:http://www.ghugo.com/requestanimationframe-best-practice/中所说的:
var $box = $('#J_num2'),
$point = $box.find('i');
$box.on('mousemove',function(e){
requestAnimationFrame(function(){
$point.css({
top : e.pageY,
left : e.pageX
})
})
});
仔细一看,mousemove 事件每秒被触发的次数依旧是数十次,这只是把这几十次的操作延迟到下一帧操作之前执行而已,使用 rAF 只能算做了个延时,并没有减少函数执行次数。真正做到节流,需要引入一个“锁”来控制。
var $box = $('#J_num2'),
$point = $box.find('i');
var locked = false;
$box.on('mousemove',function(e){
if(!locked){
requestAnimationFrame(function(){
changeCSS(e);
});
locked = true;
}else {
return;
}
});
function changeCSS(e) {
$point.css({
top : e.pageY,
left : e.pageX
})
locked = false;
}
引入一个 locked 变量表示当前能否响应操作;如果能,执行 rAF 后锁住,此时不再响应新请求;在 rAF 注册的函数执行完毕后解锁。这样即可保证一帧内只用 rAF 注册一次函数。
http://www.ghugo.com/requestanimationframe-best-practice/中的用法4提到了这一点,然而示例代码也有同样的问题:
//页面中有4个模块,A、B、C、D,在页面加载时进行实例化,一般的写法类似于:
$(function(){
new A();
new B();
new C();
new D();
})
//使用raf可将每个模块分别初始化,即每个模块都有16ms的初始化时间
$(function(){
var lazyLoadList = [A,B,C,D];
$.each(lazyloadList, function(index, module){
window.requestAnimationFrame(function(){new module()}); //不太对
});
})
同理,直接在循环里面使用 rAF 只是瞬间注册了多个操作,这些操作会全部累积起来到下一帧重绘前执行,实际上阻塞了下一帧。这种写法与不使用 rAF 的写法相比,只增加了一帧。要真的做到分帧操作,要这么写:
$(function(){
var lazyLoadList = [A,B,C,D];
var load = function() {
var module = lazyLoadList.shift();
if(module) {
new module();
window.requestAnimationFrame(load); //写个递归
}
}
window.requestAnimationFrame(load);
})
在第一节中提出的问题也就很好回答了:既然都打算使用懒执行了,直接加入递归实现分帧操作是最好的选择。
构建一个测试,看一看效果如何:
首先在页面上用 rAF 不停的跑一个修改UI的操作,触发 chrome timeline 面板对 frame 的持续记录。假设我们有个 JS 操作密集的页面,比如要串行加载许多模块:
function module(time) { //构建耗时函数
var startTime = Date.now();
while(Date.now() - startTime < time){}
}
module(20);
module(15);
module(10);
module(10);
module(16);
module(5);
module(15);
module(18);
module(7);
module(30);
module(13);
module(10);
module(2);
module(9);
执行效果如下:
执行时狠狠的阻塞了一下,fps陡降至6,页面明显停顿一下。
试用一下 rAF 分帧执行:
var lazyLoadList = [
function(){
module(20);
},function(){
module(15);
},function(){
module(10);
},function(){
module(10);
},function(){
module(16);
},function(){
module(5);
},function(){
module(15);
},function(){
module(18);
},function(){
module(7);
},function(){
module(30);
},function(){
module(13);
},function(){
module(10);
},function(){
module(2);
},function(){
module(9);
}
];
var load = function() {
var module = lazyLoadList.shift();
if(module) {
module();
window.requestAnimationFrame(load); //写个递归
}else {
return;
}
};
window.requestAnimationFrame(load);
跑跑看结果:
虽然帧率也会下降,但是最低也能保证 20fps 以上(受限于那几个执行时间为20ms以上的模块),页面不会赶到明显的顿卡,使用 rAF 可以比较有效的将阻塞“稀释”掉。进一步的优化则是要针对执行时间达到 20ms 以上的模块,尝试打碎逻辑;使每个模块的执行时间保证在 16.7ms 以内是最佳的。
requestAnimationFrame最主要的意义,是降帧而非升帧,以防止丢帧。它的目的更类似于垂直同步,而非越快越好。
MSDN: 帧率不等或跳帧会使人感觉你的站点速度缓慢。如果降低动画速度可以减少跳帧并有助于保持帧率一致,它可以使人感觉站点速度更快。
阅读更多:http://creativejs.com/resources/requestanimationframe/
rAF 在部分浏览器上还是需要使用polyfill,下面记录一个被广泛使用的版本:
// https://gist.github.com/miksago/3035015
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
})();
1.requestAnimationFrame最佳实践
2.实例解析防抖动(Debouncing)和节流阀(Throttling)
3.淘宝首页性能优化实践
4.聊一聊淘宝首页和它背后的一套
5.naf.js
title: 说说遇到的那些跨域,access control、jsonp、cookie、iframe
categories:
想当年在学生时代时,自己一个人抱着前端书啃,写demo,也没个后端搭档,自然也没实际遇到过什么跨域问题,看书上讲的跨域问题和解决方案都是囫囵吞枣。现在工作了,各种跨域问题接触了个七七八八,今天再拎出来说一说。本文不求全,只说一些实用方案。
如果两个页面拥有相同的协议(protocol),端口(如果指定),和主机,那么这两个页面就属于同一个源(origin)。
下表给出了相对http://store.company.com/dir/page.html同源检测的示例:
URL 结果 原因
http://store.company.com/dir2/other.html 成功
http://store.company.com/dir/inner/another.html 成功
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同
http://news.company.com/dir/other.html 失败 主机名不同
CORS背后的基本**就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。
服务器端需要支持CORS,通过配置Access-Control-Allow-Origin来实现。
浏览器在发起请求时要注意语法:比如在使用XHR时,URL路径应使用绝对路径而非相对路径。IE9及以下浏览器应换用XDomainRequest进行跨域操作。
iconfont字体文件若缓存在cdn上,会出现跨域问题,可以使用这种方式解决。
当使用ajax请求跨域内容时,jsonp是最流行、最实用的解决方案。jsonp利用了script标签可以引入外部js资源的特点。
服务器端需要支持jsonp。返回一个由指定callback函数名包裹的json数据。例:
callback({
data: 'test',
result: '0'
})
jsonp的执行方式是:动态生成一个script标签,将url填入src中,将script标签append入body。由于script标签下载js资源无跨域限制,所以数据得到下载。随后由本地定义的callback函数对数据进行处理。
如果使用jsonp或zepto等库,其$.ajax方法均对jsonp做了处理,我们指定数据类型为jsonp后可以直接使用,免去了我们手动生成插入script标签的过程。
跨顶级域名的cookie操作需要一定的hack手段,可以参考这里;
通过在创建cookie时设置domain参数,二级域名网页中可以直接写入顶级域名的cookie。例如,在a.404forest.com中可以直接读写404forest.com的cookie。可以通过将cookie写入公共的父域,实现跨子域名cookie读写。
跨顶级域名的iframe跨域操作需要一定的hack手段,可以参考这里;
如果iframe跨自域名,需要先将父子iframe的document.domain均设置为相同的父级域名。随后父页面通过iframe的contentDocument或document属性访问到文档对象,进而可以取得子页面的信息;子页面可以通过parent访问到父页面。
上面几种均是自己用过的,整理一些其他方案:
使用window.name来进行跨域:window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。
使用HTML5的window.postMessage方法跨域:
window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。
参考资料:
title: 收集整理《JS高程》中用以解决跨浏览器兼容性问题的JS代码
categories:
大坑,边看边填。
跨浏览器的事件处理程序:
var eventUtil = {
addHandler: function(element, type, handler){
//先检查DOM2级事件处理程序,再检查IE事件处理程序,最后使用DOM0级事件处理程序
if(element.addEventListener){
element.addEventListener(type, handler ,false);
}else if(element.attachEvent){
element.attachEvent("on"+type, handler);
}else{
element["on"+type] = handler;
}
},
removeHandler: function(element, type, handler){
if(element.removeEventListener){
element.removeEventListener(type, handler ,false);
}else if(element.detachEvent){
element.detachEvent("on"+type, handler);
}else{
element["on"+type] = null;
}
}
}
跨浏览器的事件对象:
var eventUtil = {
addHandler: function(){
...
},
removeHandler: function(){
...
},
getEvent: function(event){
return event?event:window.event;
},
getTarget: function(event){
return event.target || event.srcElement;
},
preventDefault: function(){
if(event.preventDefault){
event.preventDefault();
}else{
event.eturnValue = false;
}
},
stopPropagation: function(){
if(event.stopPropagation){
event.stopPropagation();
}else{
event.cancelBubble = true;
}
}
}
textbox.value = "Hello World!";
function selectText(textbox, startIndex, endIndex){
if(textbox.setSelectionRange){
textbox.setSelectionRange(startIndex, endIndex);
}else if(textbox.createTextRange){
var range = textbox.createTextRange();
range.collapse(true);
range.moveStart("character", startIndex);
range.moveEnd("character", startIndex - endIndex);
range.select();
}
textbox.focus();
}
var eventUtil = {
getCharCode: function(event){
if(typeof event.charCode == "number"){
return event.charCode;
}else{
return event.keyCode;
}
}
}
var eventUtil = {
getClipboardText: function(event){
var clipboardData = event.clipboardData || window.clipboardData;
return clipboardData.getData("tetx");
},
setClipboardText: function(event, value){
if(event.clipboardData){
return event.clipboardData.setData("text/plain", value);
}else if(window.clipboardData){
return window.clipboardData.setData("text", value);
}
}
}
function parseXml(xml){
var xmldom = null;
if(typeof DOMParser != "undefined"){
xmldom = (new DOMParser()).parseFromString(xml, "text/xml");
var errors = xmldom.getElementByTagName("parsererror");
if(errors.length){
throw new Error("XML parsing error:" + errors[0].textContent);
}
}else if(typeof ActiveXObject != "undefined"){
xmldom = createDocument();//关于此函数见P524
xmldom.loadXML(xml);
if(xmldom.parseError != 0){
throw new Error("XML parsing error:" + xml.parseError.reason);
}
}else{
throw new Error("No XML parser available.");
}
return xmldom;
}
function serializeXml(xmldom){
if(typeof XMLSerializer != "undefined"){
return (new XMLSerializer()).serializerToString(xmldom);
}else if(typeof xmldom.xml != "undefined"){
return xmldom.xml;
}else{
throw new Error("Could not serialize XML DOM.");
}
}
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if("withCredentials" in xhr){ //通过检查是否支持带凭据请求判断XHR对象是否原生支持CORS
xhr.open(method, url, true);
}else if(typeof XDomainRequests != "undefined"){
xhr = new XDomainRequest();
xhr.open(method, url);
}else{
xhr = null;
}
return xhr;
}
title: AngularJS即学即用 —— 读书笔记
categories:
框架乱人眼,敲码别走神。
通读完全书,感觉 Angular1 类似一个大语法糖,没有改变网页中数据流动的方式,数据流动一复杂依然会觉得混乱。Angular1 的设计确实存在一些缺点,如指令与嵌套做模块化不够清晰等等;Angular2 做了很大幅度的改变,很多地方在向 vue 靠拢。
之前用过 react,配合 flux 写过几千行代码,个人明显更喜欢 react 的风格:极致组件化、单向数据流,我认为这是网页开发从本质上进行的进化,从根本上解决痛点;至于有些人说的 flux 流程太多很麻烦(现在有 redux 了),all in js 的风格难以适应,我觉得这些都是小问题,多封装一些语法糖出来就能有改善。
其实 all in js 的风格我也不太喜欢,在这方面我觉得 vue 的单文件组件的组织方式是最棒的。
随口谈谈,接下来可能要自己造点轮子,会再深入的对这3个框架研究一下。
最近危机感有点重啊,哈哈。
参考:
title: 常见知识点汇总(二):事件
date: 2014-12-17 13:58:41
categories:
Javascript与HTML之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。
IE的事件流叫做事件冒泡,即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。
所有的现代浏览器都支持事件冒泡。
事件捕获的**是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件达到预订目标之前捕获它。
仅在有特殊需要时再使用事件捕获。
DOM2级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。
即使DOM2级事件规范明确要求捕获阶段不会设计事件目标,但IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上面操作事件。
<input type="button" value="Click Me" onClick="alert('Clicked')">
<script type="text/javascript">
function showMessage(){
alert('Hello world!');
}
</script>
<input type="button" value="Click Me" onClick="showMessage()">
事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
在HTML中指定事件处理程序有2个缺点:
<input type="button" value="Click Me" onClick="try{showMessage();}catch(ex){}">
var btn = document.getElementById('myBtn');
btn.onclick = function(){
// this指向当前节点
alert(this.id);
};
btn.onclick = null; //删除事件处理程序
DOM2级事件定义了两个方法,用于处理指定和删除处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名,作为事件处理程序的函数和一个布尔值。最后一个布尔值如果是true,标示在捕获阶段调用事件处理函数,flase则表示在冒泡阶段调用事件处理程序。
var btn = document.getElementById('myBtn');
btn.addEventListener('click', function(),{
alert(this.id);
},false);
//如果为同一个节点添加多个事件处理函数,会按照添加它们的顺序触发。所以在此例中,会先显示元素ID再输出“Hello World!"
btn.addEventListener('click', function(){
alert('Hello word!!');
}, false);
//删除一个监听
btn.removeEventListener('click', handleClick, false);
通过addEventListener()添加的事件处理程序只能通过removeEventListener()移除。移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除。
IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。由于IE只支持事件冒泡,所以通过attachEvent添加的事件处理程序都会被添加到冒泡阶段。
var btn = document.getElementById('myBtn');
var handleClick = function(){
alert(this===window);//输出true;注意!IE处理函数的作用域是全局
};
btn.attachEvent('onclick', handleClick);
//也可以为同一节点添加多个事件处理函数,不过与DOM2方法不同的是,这些事件不是以添加它们的顺序执行,而是以相反的顺序被触发。
//所以在此列中,首先看到的是hello world然后才是id
btn.attachEvent('onclick', function(){
alert('Hello word!');
});
//使用detachEvent移除事件处理程序
btn.detachEvent('onclick', handleClick);
在触发DOM上某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素,事件的类型,以及其他与特定事件相关的信息。
兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0级或DOM2级),都会传入event对象。
var btn = document.getElementById('myBtn');
btn.onclick = function(event){
alert(event.type); //"click"
};
btn.addEventListener('click', function(event){
alert(event.type); //"click"
}, false);
与访问DOM中的Event对象不同,要访问ie中的event对象有几种不同的方式,取决于指定事件处理程序的方法。
在使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。
var btn = document.getElementById('myBtn');
btn.onclick = function(){
var event = window.event;
alert(event.type); //"click"
};
若事件处理程序是使用attachEvent()添加的,那么就会有一个event对象作为参数被传入事件处理程序函数中。
var btn = document.getElementById('myBtn');
btn.attachEvent('onclick', function(event){
alert(event.type); // "click"
alert(window.event.type); //也可以通过window.event来访问
});
常用的event属性:
对”事件处理程序过多“问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
以下面的HTML代码为例:
<ul id="ul">
<li>aaaaaaaa</li>
<li>bbbbbbbb</li>
<li>cccccccc</li>
</ul>
其中包含3个鼠标移入/移出后执行操作的列表项。按照传统的做法,需要想下面这样为它们添加3个事件处理程序。
window.onload = function(){
var oUl = document.getElementById("ul");
var aLi = oUl.getElementsByTagName("li");
for(var i=0; i<aLi.length; i++){
aLi[i].onmouseover = function(){
this.style.background = "red";
}
aLi[i].onmouseout = function(){
this.style.background = "";
}
}
}
如果在复杂的web应用程序中,对所有li都采用这种方式,那么li一多就会有大量的代码用于添加事件处理程序。使用事件委托,只需在DOM树中尽量最高的层次上添加一个事件处理程序:
window.onload = function(){
var oUl = document.getElementById("ul");
var aLi = oUl.getElementsByTagName("li");
/*
这里要用到事件源:event 对象,事件源,不管在哪个事件中,只要你操作的那个元素就是事件源。
ie:window.event.srcElement
标准下:event.target
nodeName:找到元素的标签名
*/
oUl.onmouseover = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
//alert(target.innerHTML);
if(target.nodeName.toLowerCase() == "li"){
target.style.background = "red";
}
}
oUl.onmouseout = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
//alert(target.innerHTML);
if(target.nodeName.toLowerCase() == "li"){
target.style.background = "";
}
}
}
这段代码里,我们使用事件委托只为ul元素添加了一个onclick事件处理程序,只取得了一个DOM元素,只添加了一个事件处理程序,这样占用内存更少,性能更高。
并且当ul内动态添加新的li元素时,事件也可以生效。
title: 谈谈网页中的Animation
categories:
一个有关 Animation 的小小分享。
<iframe src="//slides.com/jin5354/css3-animations/embed" width="576" height="420" scrolling="no" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>声明式动画,如 CSS3 Animation。 CSS3中的动画大多以 Animation 和 Transition 的形式来组成。
优点:
缺点:
命令式动画,如 Javascript 动画、Canvas、WebGL 动画。主要特点是显式调用动画函数。
优点:
缺点:
示例代码:
方块下移400像素,时长1秒
window.onload = function() {
var div1 = document.getElementById('test1');
var top = 0;
var endTime;
var startTime = Date.now();
var ani1 = setInterval(function(){
if(top <= 400){
div1.style.top = top + 'px';
top += 400/60;
}else {
clearInterval(ani1);
endTime = Date.now();
console.log(endTime - startTime);
}
}, 1000/60);
};
可以看到这个动画的执行过程是:
每1/60秒执行1次,每次移动400/60像素,共移动400像素。如果帧间隔能稳定在1/60秒,那么总执行时长就是稳定1秒。
可惜帧间隔是无法稳定的,当你执行别的操作(比如点点面板显示按钮触发一下重绘)还会带来严重的性能损失。最终动画总执行时长总是略多于1秒,性能损失严重时可能达到1.5 ~ 2秒。
示例代码:
window.onload = function() {
var div1 = document.getElementById('test1');
var top = 0;
var start = null;
var ani1 = setInterval(function(){
if(!start) {start = Date.now();}
var progress = Date.now() - start;
if(progress <= 1000){
div1.style.top = progress/1000*400 + 'px';
}else {
div1.style.top = '400px';
clearInterval(ani1);
console.log(progress);
}
}, 1000/60);
};
这个动画的执行过程:
每1/60秒执行一次,执行时先算出此时刻的动画已进行时长,折算为动画进行百分比,根据百分比决定移动位置。
由于是基于时间的动画算法,执行总时长一定是1秒(误差非常小)。
进化版:使用requestAnimationFrame
window.onload = function() {
var div1 = document.getElementById('test1');
var div2 = document.getElementById('test2');
var top = 0;
var start = null;
var ani1 = setInterval(function(){
if(!start) {start = Date.now();}
var progress = Date.now() - start;
if(progress <= 1000){
div1.style.top = progress/1000*400 + 'px';
}else {
div1.style.top = '400px';
clearInterval(ani1);
console.log(progress);
}
}, 1000/60);
var start2 = null;
function step(timestamp) {
if (!start2) start2 = timestamp;
var progress = timestamp - start2;
div2.style.top = progress/1000*400 + "px";
if (progress <= 1000) {
window.requestAnimationFrame(step);
}else{
div2.style.top = '400px';
}
}
window.requestAnimationFrame(step);
};
使用 requestAnimationFrame 浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果。
对比动画可以看到,不使用 requestAnimationFrame 的动画可能存在颠簸效果,使用 requestAnimationFrame 的动画更加平滑。
title: 打造一套专用的iconfont
categories:
iconfont 是一个解决图标问题的优秀方案。
iconfont的优点在于:
缺点:
本站首页左边列表中的各种小图标就都是iconfont。
首先需要将做好的图标输出为SVG文件。如果要制作iconfont的话,对SVG本身存在一些要求,具体可以看图标制作说明。
图标绘制规范:
所有图标请在16X16的AI画布中绘制,(iconfont图标在绘制时均以标准图标大小16像素X16像素绘制,因为在制作成字体时文件需要设置较高的清晰度,所以图标路径也需要等比例放大。)
为了确保图标的清晰显示,在绘制时须避免水平、垂直的边缘出现半个单位。(半个单位的路径会导致图标在最终显示时边缘模糊,不清晰),弧线在绘制时要保证弧度饱满。
LOGO(包括图形和字体)类图标要基于基线对齐,同时需要综合考虑LOGO在组合使用时的水平位置关系。
字体或线条类图标绘制完成后,须对对象执行“扩展”操作,以矢量图形输出,未进行“扩展”操作的路径会影响图标的正常显示。
在绘制图形的时候,曲线必须是闭合的。如果曲线是不闭合的在字体转换的过程中是无发转译未闭合曲线的图形的。
在绘制好封闭图形后,要给图形填充颜色。
有了做好的svg文件之后,我们就可以开始制作iconfont字体了!
有很多网站提供好用的在线iconfont制作服务。我一般使用icomoon,如果使用国产服务比如http://iconfont.cn/当然也可以。
进入icomoon后,点击右上角的icomoon app就进入制作主界面了。通过左上角的import icons,把svg都导进去。
每个set的右边是有设置按钮的,眼睛按钮右侧。可以调换图标位置。满意后点击右下角的generate即可生成字体。
在接下来的预览页面,我们已经可以直接看到iconfont化的字体————以及其所对应的unicode编码。
这时要注意,每个图标都只应对应1个unicode编码,如果某个图标对应了多个unicode编码,说明生成失败————输出的svg文件不合规范。需要重新输出svg。
检查无误,就可以点击download下载了。
打开下载的文件,fonts文件夹中躺着的4个文件就是我们所需要的字体文件了。
打开看一眼,感觉萌萌哒。
将字体文件存放妥当后,CSS中引入iconfont字体。(如果使用sass等预处理器解决方案更简单一点,将所有iconfont相关模块抽象成一个iconfont.scss,随后在有iconfont使用需求的项目中直接import即可)
@font-face {
font-family: 'wdiconfont';
src: url('../../fonts/wdiconfont.eot'); /* IE9 Compat Modes */
src: url('../../fonts/wdiconfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('../../fonts/wdiconfont.woff') format('woff'), /* 所有现代浏览器 */
url('../../fonts/wdiconfont.ttf') format('truetype'), /* Safari, Android, iOS */
url('../../fonts/wdiconfont.svg#svgFontName') format('svg'); /* Legacy iOS */
}
CSS中进行配置:
.iconfont {
font-family: "wdiconfont" !important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-chat::before {
content: '\e900';
}
.icon-cart::before {
content: '\e901';
}
.icon-wait-confirm::before {
content: '\e902';
}
.icon-wait-rate::before {
content: '\e903';
}
....
HTML中一般使用i,span之类的标签作为图标载体
<span class="iconfont icon-chat"></span>
这样配置好之后,网页上就可以正常显示了。
PS: 字体编辑工具:FontLab Studio
当我们已经有一个表现良好的iconfont之后,如果遇到新增图标,删除图标,两个iconfont合并之类的任务,可以使用FontLab Studio解决。
title: 理解web语义化
categories:
语义化,简单说来就是让机器可以读懂内容。
随着 Web 规模的不断扩大,信息量之大已经不在人肉处理的范围之内了。这个时候人们开始用机器来处理 Web 上发布的各种内容,搜索引擎就诞生了。再后来,人们又设计了各种智能程序来对索引好的内容作各种处理和挖掘。所以让机器能够更好地读懂 Web 上发布的各种内容就变得越来越重要。
HTML 规范其实一直在往语义化的方向上努力,许多元素、属性在设计的时候,就已经考虑了如何让各种用户代理甚至网络爬虫更好地理解 HTML 文档。HTML5 更是在之前规范的基础上,将所有表现层(presentational)的语义描述都进行了修改或者删除,增加了不少可以表达更丰富语义的元素。为什么这样的语义元素是有意义的?因为它们被广泛认可。所谓语义本身就是对符号的一种共识,被认可的程度越高、范围越广,人们就越可以依赖它实现各种各样的功能。
参考资料
title: ES6重点概览
categories:
本文是阮一峰书籍《ECMAScript 6 入门》的读书笔记,并进行了精简和整合,用20%的篇幅覆盖80%的功能,可以用来速查ES6知识。
let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
const也用来声明变量,但是声明的是常量。一旦声明,常量的值就不能改变。
ES6一方面规定,var命令和function命令声明的全局变量,依旧是全局对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。只要等号两边的模式相同,左边的变量就会被赋予对应的值。如果解构不成功,变量的值就等于undefined。
let [foo, [[bar], baz]] = [1, [[2], 3]];
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
解构赋值适用于var、let、const命令、Set结构。事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
解构赋值允许指定默认值。如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
var [foo = true] = [];
foo // true
解构不仅可以用于数组,还可以用于对象。对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。如果变量名与属性名不一致,必须写成下面这样。
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
函数的参数也可以使用解构赋值。
function add([x, y]){
return x + y;
}
add([1, 2]) // 3
以下三种解构赋值不得使用圆括号。可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。
主要用途:
[x, y] = [y, x]
function example() {
return [1, 2, 3];
}
var [a, b, c] = example();
JavaScript允许采用\uxxxx形式表示一个字符,其中“xxxx”表示字符的码点。但是,这种表示法只限于\u0000——\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表达。如果直接在“\u”后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScript会理解成“\u20BB+7”。ES6对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。`\u{20BB7
正确处理4个字节储存的字符,返回一个字符的码点。charCodeAt()的升级版。
正确识别大于0xFFFF的码点,从码点返回对应字符。fromCharCode()的升级版。
ES6为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of循环遍历。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。
返回字符串给定位置的字符,可以识别Unicode编号大于0xFFFF的字符。charAt的升级版。
将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。
indexOf的升级,用来确定一个字符串是否包含在另一个字符串中。
repeat方法返回一个新字符串,表示将原字符串重复n次。 'hello'.repeat(2) // "hellohello"
ES7推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart用于头部补全,padEnd用于尾部补全。一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。
String.raw方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。
var a = 5;
var b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
function tag(stringArr, ...values){
// ...
}
ES6允许RegExp构造函数接受正则表达式作为参数,这时会返回一个原有正则表达式的拷贝。如果使用RegExp构造函数的第二个参数指定修饰符,则返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/ig, 'i').flags
// "i"
ES6对正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于\uFFFF的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。
ES6还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
var s = "aaa_aa_a";
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
var r3 = /a+_/y;
r3.exec(s) // ["aaa_"]
r3.exec(s) // ["aa_"]
与y修饰符相匹配,ES6的正则对象多了sticky属性,表示是否设置了y修饰符。
ES6为正则表达式新增了flags属性,会返回正则表达式的修饰符。
ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。如果要将0b和0x前缀的字符串数值转为十进制,要使用Number方法。
ES6在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法,用来检查Infinite和NaN这两个特殊值。
ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。
ES6在Number对象上面,新增一个极小的常量Number.EPSILON。引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。但是如果这个误差能够小于Number.EPSILON,我们就可以认为得到了正确结果。因此,Number.EPSILON的实质是一个可以接受的误差范围。
JavaScript能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。
Array.of(3, 11, 8) // [3,11,8]
这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
它接受三个参数。
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 4, -5, 10].find((n) => n < 0)
// -5
用法与find方法非常类似。
fill方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
ES6提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
[1, 2, 3].includes(2); // true
数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。Array(3) // [, , ,]
ES5对空位的处理,已经很不一致了,大多数情况下会忽略空位。
ES6则是明确将空位转为undefined。
由于空位的处理规则非常不统一,所以建议避免出现空位。
数组推导(array comprehension)提供简洁写法,允许直接通过现有数组生成新数组。
var a1 = [1, 2, 3, 4];
var a2 = [for (i of a1) i * 2];
a2 // [2, 4, 6, 8]
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);
}
默认声明的参数变量在函数体中不能用let或const再次声明,否则会报错。
参数默认值可以与解构赋值的默认值,结合起来使用。
function foo({x, y = 5}) {
console.log(x, y);
}
如果传入undefined,将触发该参数等于默认值,null则没有这个效果。
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
(function(a){}).length // 1
(function(a = 5){}).length // 0
(function(a, b, c = 5){}).length // 2
一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
扩展运算符(spread)是三个点(...)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
可替代数组的apply方法。
应用:
[1, 2, ...more]
[a, ...rest] = list
函数的name属性,返回该函数的函数名。
function foo() {}
foo.name // "foo"
ES6允许使用“箭头”(=>)定义函数。
var f = v => v;
var f = function(v) {
return v;
};
箭头函数有几个使用注意点:
箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。
函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
var birth = '2000/01/01';
var Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
ES6允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。
ES6提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign方法用来将源对象(source)的所有可枚举属性,复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出TypeError错误。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
用途:
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。
ES5有三个操作会忽略enumerable为false的属性。
ES6新增了两个操作,会忽略enumerable为false的属性。
ES6一共有6种方法可以遍历对象的属性。
for...in
for...in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。Reflect.enumerate(obj)
Reflect.enumerate返回一个Iterator对象,遍历对象自身的和继承的所有可枚举属性(不含Symbol属性),与for...in循环相同。__proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象。目前,所有浏览器(包括IE11)都部署了这个属性。该属性没有写入ES6的正文,而是写入了附录,原因是__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的API,只是由于浏览器广泛支持,才被加入了ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象。它是ES6正式推荐的设置原型对象的方法。
Object.getPrototypeOf方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象。
ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
let s = Symbol();
typeof s
// "symbol"
注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
Symbol值不能与其他类型的值进行运算,会报错。Symbol值可以显式转为字符串和布尔值。
实例:消除魔术字符串
function getArea(shape, options) {
var area = 0;
switch (shape) {
case 'Triangle': // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
上面代码中,字符串“Triangle”就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量。
var shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
var area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
上面代码中,我们把“Triangle”写成shapeType对象的triangle属性,这样就消除了强耦合。
如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用Symbol值。
const shapeType = {
triangle: Symbol()
};
上面代码中,除了将shapeType.triangle的值设为一个Symbol,其他地方都不用修改。
ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成Set数据结构。
var s = new Set();
[2,3,5,4,5,2,2].map(x => s.add(x))
for (let i of s) {console.log(i)}
// 2 3 5 4
Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
var set = new Set([1, 2, 3, 4, 4])
[...set]
// [1, 2, 3, 4]
另外,两个对象总是不相等的。
JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是只能用字符串当作键。这给它的使用带来了很大的限制。为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。
var m = new Map();
var o = {p: "Hello World"};
m.set(o, "content")
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
添加某个值,返回Set结构本身。
删除某个值,返回一个布尔值,表示删除是否成功。
返回一个布尔值,表示该值是否为Set的成员。
清除所有成员,没有返回值。
size属性返回Map结构的成员总数。
set方法设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。
get方法读取key对应的键值,如果找不到key,返回undefined。
has方法返回一个布尔值,表示某个键是否在Map数据结构中。
delete方法删除某个键,返回true。如果删除失败,返回false。
clear方法清除所有成员,没有返回值。
遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。
在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。
ES6借鉴C++、Java、C#和Python语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象(比如arguments对象、DOM NodeList对象)、后文的Generator对象,以及字符串。
for...of循环可以代替数组实例的forEach方法。
const arr = ['red', 'green', 'blue'];
let iterator = arr[Symbol.iterator]();
for(let v of arr) {
console.log(v); // red green blue
}
for(let v of iterator) {
console.log(v); // red green blue
}
JavaScript原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6提供for...of循环,允许遍历获得键值。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
myArray.forEach(function (value) {
console.log(value);
});
for (var index in myArray) {
console.log(myArray[index]);
}
for...in循环有几个缺点。
for...of循环相比上面几种做法,有一些显著的优点。
for (let value of myArray) {
console.log(value);
}
基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
ES5版本:
function Point(x,y){
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
}
ES6的类,完全可以看作构造函数的另一种写法。
类的内部所有定义的方法,都是不可枚举的(enumerable)。这一点与ES5的行为不一致。类的属性名,可以采用表达式。
生成实例对象的写法,与ES5完全一样,也是使用new命令。
// 报错
var point = Point(2, 3);
// 正确
var point = new Point(2, 3);
与函数一样,Class也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代码表示,Me只在Class内部有定义。
如果Class内部没用到的话,可以省略Me,也就是可以写成下面的形式。
const MyClass = class { /* ... */ };
采用Class表达式,可以写出立即执行的Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("张三");
person.sayName(); // "张三"
Class不存在变量提升(hoist)这一点与ES5完全不同。类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
super关键字,它指代父类的实例。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些。
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。
ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
lass MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: undefined is not a function
静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的写法为Foo类定义了一个静态属性prop。
目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。
在ES6之前,社区制定了一些模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6模块的设计**,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性。
ES6的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"。
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个JS文件,里面使用export命令输出变量。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
//另一种写法
export {firstName, lastName, year};
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。
使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。
// main.js
import {firstName, lastName, year} from './profile';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile';
import语句会执行所加载的模块,因此可以有下面的写法。
import 'lodash';
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
// main.js
import * as circle from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
// export-default.js
export default function () {
console.log('foo');
}
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
import customName from './export-default';
customName(); // 'foo'
title: 玉泉通过ipv6来使用shadowsocks
categories:
今年暑假期间GFW大升级,在机器学习、数据挖掘、深度包检测等装备全面部署下的GFW火力大大提升,导致杭州电信环境下shadowsocks可用性大幅降低,在Linode、DO、conoha等家的vps上架设的shadowsocks被打击严重,难以正常使用。试用ipv6,发现效果还可以,可能功夫网gfw对ipv6的针对力度还不大。下面说一下流程。
首先你需要分配到一个ipv6地址。
据说玉泉的有线网络会自动分配一个ipv6地址,我是一直使用无线(ZJUWLAN)的,有线的情况不清楚,不过到底有没有拿到ipv6访问后文的网址就知道了。其实我个人感觉用ZJUWLAN + 自建VPN方式通用性大一点,寝室教室都能用。如果你说wifi信号不好,那就没办法了..
第一步,使用ZJUWLAN的话,通过自建VPN方式登录。(不能使用网页认证方式登录,那种方式拿不到ipv6)
在偏好设置-网络中,新建一个l2tp的vpn,按图配置。点击鉴定设置,密码中填入VPN密码。高级中,确保“通过VPN连接发送所有流量”为非勾选状态。
这时在接入ZJUWLAN后,连接这个VPN就可以接入外网了。偶尔会出鉴定失败什么的错误,多试几次就可以上了。
这里如果有问题可以参考下98的这个帖子 网络问题日经帖之终结帖!
第二步,点击左边的 + 号,新建一个6 to 4连接,什么也不用配置,直接点应用。正常情况下立刻就能看到一个ipv6地址了。
点击左边的齿轮,选择设定服务顺序,把VPN(L2TP)拖动到最上方。
第三步,访问test-ipv6,看看是不是一路绿灯。
看一下自己vps的ipv6地址,ping6一下试试。
如果都能正常连接表示离成功就八九不离十了。我第一次配置时,ipv6地址获取成功,但是不能访问ipv6网站,也不能ping通自己vps的ipv6地址。我也没什么头绪,后来放置一天再尝试就正常了。囧。神奇玉泉。
第四步,微调一下服务器端ss,开启对ipv6的监听。
方式非常简单:如果使用json配置文件,改server一行为:
“server”:”::”
表示同时监听ipv4和ipv6;
如果使用命令行形式启动ss,添加“-s ::”启动就可以了。
最后一步别忘了,本地ss客户端改一下ipv6地址。
Enjoy!
PS: 感谢v2ex上遇到的xiangtianxiao同学的分享。
title: 理解flex属性——flex-grow、flex-shrink、flex-basis都是干啥的
categories:
flex的大部分属性都简明易懂,比如对齐属性justify-content、align-items、align-self之类,然而决定项目本身伸缩性质的flex属性,没有想象中容易理解。这里结合一些常用的布局,把自己的理解写一下。
先介绍一下flex属性。
flex是一个复合属性,它的语法如下:
flex: none | auto | initial | [ <flex-grow> <flex-shrink> ? || <flex-basis> ]
flex条目由3个css属性来确定,分别是flex-basis、flex-grow和flex-shrink。他们的相关说明如下:
属性 | 可选值 | 默认值 | 含义 | 备注 |
---|---|---|---|---|
flex-basis | length, percentage, auto | auto | 弹性条目的初始主轴尺寸 | 在「flex」属性中该值如果被省略则默认为「0%」,在「flex」属性中该值如果被指定为「auto」,则伸缩基准值的计算值是自身的 <' width '> 设置,如果自身的宽度没有定义,则长度取决于内容。 |
flex-grow | integer | 1 | 当容器有多余的空间时,这些空间在不同条目之间的分配比例 | 在「flex」属性中该值如果被省略则默认为「1」,不能取负值 |
flex-shrink | integer | 1 | 当容器的空间不足时,各个条目尺寸缩小的比例 | 在「flex」属性中该值如果被省略则默认为「1」,不能取负值 |
Example: flexbox等分布局
一般的教程,往往告诉你项目都设置成flex:1就能做等分了。如果某个元素设置flex:2,它得到的尺寸会是flex:1的2倍。然而当你在项目中兴高采烈的使用flex:1后,却发现自动生成的-webkit-box-flex: 1;在低版本浏览器中可以识别,但是尺寸却不正常了,这是为啥呢?
一个伸缩项目最终得到的尺寸,是由三个属性共同决定的:flex-grow,flex-shrink,flex-basis。而我们常用的flex: 1,实际上是flex-grow:1;flex-shrink:1;flex-basis:0%的简写。
最终宽度计算如下:
这里很重要的一点是:初始尺寸如何得来?根据flex-basis的说明可以知道:默认为0%,即0,如果手动指定为auto,则flex-basis值与元素指定的width值相同;如果元素没有被指定width值,则由内容计算得出。(类似于inline-block中内容自动撑开的宽度)
JS Bin on jsbin.com<script src="http://static.jsbin.com/js/embed.min.js?3.35.9"></script>
上面是一个简单的例子:父元素宽度300px,3个子元素宽度为50px、60px、40px,此时多余空间为150px。默认情况下(flex初始值:0 1 auto),3个子元素左对齐,剩余空间不分配。
如果我们给这三个元素均指定为flex:1;结果如下:
JS Bin on jsbin.com<script src="http://static.jsbin.com/js/embed.min.js?3.35.9"></script>
这是一个标准的等分情况,此时每个元素的尺寸为 0+300*(1/3) = 100px。当flex=1时,每个元素的初始尺寸为0,则剩余空间为300px,进行三等分。
这里我们可知:使用flex:1能够如此简单的实现不同尺寸元素的等分的关键在于————其指定flex-basis为0,此时元素本身宽度会被忽视。
而在低版本安卓、ios中flex:1不能坐到等分的原因也在这里:旧语法中根本没有flex-basis这个选项——而在计算中一律将flex-basis当做auto在处理。所以当你在使用flex:1时,实际你在使用的是flex-grow:1,flex-basis: auto。这是的结果则是:
JS Bin on jsbin.com<script src="http://static.jsbin.com/js/embed.min.js?3.35.9"></script>
此时元素尺寸 = 初始尺寸(50,60,40) + 150 * (1/3) = 100, 110, 90px
JS Bin on jsbin.com<script src="http://static.jsbin.com/js/embed.min.js?3.35.9"></script>
父元素宽度300px,3个子元素宽度为110px、120px、130px,此时多出空间为60px。默认情况下(flex初始值:0 1 auto),每个元素的尺寸为:
初始尺寸(110, 120, 130) - 60 * (1/3) = 90, 100, 110px
JS Bin on jsbin.com<script src="http://static.jsbin.com/js/embed.min.js?3.35.9"></script>
若给这三个元素指定flex-shrink分别为1,3,5 可知:
元素尺寸 = 初始尺寸(110, 120, 130) - 60 * ((1, 3, 5)/9) = 104, 100, 95px;
PS: 由于旧语法中的flex仅支持flex-grow,不支持flex-shrink和flex-basis,所以在旧语法中无法实现收缩(同时由于旧版本移动端浏览器不支持flex的换行,子元素会冲出父容器)。同时由于flex-basis的缺失,初始尺寸一律按照auto情况计算,所以直接写flex:1是无法实现不同初始尺寸元素的等分的,这时需要将所有子元素手动指定width:0;才可以实现等分。
title: Ruby元编程——小记
categories:
之前在微博上看大佬们讨论 ES6、ES7 等新规范,经常谈论一些“元编程”、“λ 表达式”,小白不懂啊,于是去图书馆看了几天《Ruby 元编程》,努力学习一下新姿势,遂有此文。
作为入门,看了一点点 《Programming Ruby》,可以说 Ruby 是一门相当易学、相当灵活的语言,语法糖也很多,适合我这种甜食爱好者,可以扩展思路。
大佬们是这么说的:元编程是编写能写代码的代码。这个“能写代码的代码”真是抽象极了。具体点说,元编程是:“编写能在运行时操作语言构件的代码”。一个语言的语言构件包括它的变量、类、方法,……等等一切。
什么叫操作语言构件?简单的说,就是“操纵”语言构件的行为。当然,语言构件都有着其默认职责。比如,对象 obj
下有一个属性 prop
,那么我们可以通过 obj.prop
来访问该属性的值,通过 obj.prop=
来改写该属性的值。这就是该语言构件(对象的属性)的默认行为。对其默认行为进行操纵,即为元编程。以下为两个例子:
语言本身对其语言构件都是有着一定的行为约束的。约束越少,允许我们“自定义”的程度越大,那么这门语言就越灵活、越不可控、越容易进行元编程。
在 JS 中,访问一个未定义变量 a
会报:Uncaught ReferenceError: a is not defined
,访问一个未定义对象属性会返回 undefined
,访问一个未定义方法 f
会报:Uncaught TypeError: f is not a function
。如果你表示不满,想要自定义出错的默认行为,那么除了 try-catch
外没有什么办法了。然而在 Ruby 中这些行为也是可以操纵的:通过修改 method_missing
和 const_missing
就可以自定义出错的默认行为,这使语言更灵活,同时衍生出“幽灵方法”、“动态代理”等一系列高级技巧,这些也是典型的元编程。
当然,JS 使用 try-catch
也不是不能实现同样的功能,只是很麻烦———— Ruby 中提供了语法糖一样的存在,对于元编程更加友好。Ruby 这门语言实在是太灵活了,你几乎可以操纵一切。
ES7 中的 Proxy 用于拦截、修改某些操作的默认行为,是典型的元编程增强。
“根本没有什么元编程,只有编程而已。”
lambda 表达式,通常指一个匿名函数。比如:function(a,b) {return a+b}
。ES6 中可以这样写了:(a,b) => a+b
。一个听起来高大上然而我们每天都在用的东西。
其实我们写 JS 的更容易理解什么是 λ 表达式呢。
附两张看书时所记的脑图。
title: 《Css揭秘》简单笔记
categories:
久闻这本书大名,季度购书时买下了,花了一个下午翻完,简单做点记录。
总体来说还不错,不过大多数内容都是“展望未来”的,含有大量不能用于生产环境的新特性,比如 -webkit-clip-path、calc(这货竟然到 Android 5 才完全支持)等等,这些都略读了,所以读的很快。真正有用实用现在就能用的不是很多,但还是有些确实起到了点拨的作用。作为一名实用主义者,给这本书装帧100分,内容80分。
小记一些实用的东西。
问题:背景色会延伸到边框下层。
border: 10px solid rgba(255,255,255,0.5);
background-clip: padding-box; //浏览器会用内边距的外沿把背景裁剪掉,兼容性出色
配合使用 border 与 box-shadow / outline。box-shadow 可以创建无限数量的边框,但行为与边框不一样。需要修改内外边距来额外提供空间。box-shadow 可选属性为 inset。
outline-offset 可以控制 outline 与元素边缘的间距,甚至可以接收负值。outline 不贴合圆角。
border + border-radius + box-shadow + outline 同时使用,主要利用特性:box-shadow 贴合圆角,outline 不贴合圆角。
默认情况下背景图像于 padding-box 左上角对齐。可以修改:
background-origin: content-box;
background: linear-gradient(#fb3 50%, #58a 50%);
更好的斜向条纹: background: repeating-linear-gradient(45deg, #fb3, #58a 30px);
transform: skewX(-45deg);
内容也跟着倾倒了?对内容应用反向: skewX(45deg);
不愿意使用双层 DOM? 使用伪元素。
clip-path: polygon(50% 0, 100% 50%, 50%, 100%, 0 50%); //这货兼容性不行啊。
万能的 background: linear-gradient 搭配 background-size/background-repeat
transform: perspective(.5em) rotateX(5deg) //transform 3D 的兼容性不知道有没有坑,用 border 实现梯形肯定没问题,最妥。
有一个 Unicode 字符用来换行:0x000A/"\A",将其用于伪元素中,并设置white-space: pre。
dd::after {
content: '\A';
white-space: pre; //这个非常实用
}
tab-size: 4; //兼容性一般般,需求不大。一般调整代码缩放展示
@font-face {
font-family: AAA;
src: url(...);
unicode-range: U+26; //兼容性很差,但是很有用。
}
table-layuot: fixed 可以给你更多控制权。 //比较实用,css2.1的内容。
title: 谈谈居中与flexbox(7.18修订~)
categories:
之前的版本主要是自己搜集网上资料整理所得,最近偶然看到大搜车的这篇有关居中的文章,总结的真是详细级了,于是对自己的这篇5月份的文章进行一下补充。
居中分为水平居中与垂直居中。
水平居中常用的解决方案有:
至于垂直居中,大概常用的就是:
如果允许使用css3的新特性,flexbox布局以及tranform中的translate都是很好用的。
比较简单,一般是父元素设置text-align:center;就够了。
<iframe width="100%" height="300" src="//jsfiddle.net/c05vjL6w/5/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe>父元素左右等padding或子元素左右等margin即可。
父元素宽度未知的情况比较常见,比如一般页面的container都限制在960px左右,无论浏览器视口宽度有多大都要保证居中:
text-align:center;
来做。父元素设定text-align:center;
,子元素设为display:inline-block
。意思是父元素先相对定位至50%处,子元素再左偏父元素的50%;达到居中的效果。
<iframe width="100%" height="300" src="//jsfiddle.net/tp0b9c45/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe>这里的float和上个方法的absolute的功能都是收缩这个未知宽度元素的宽度,这样子元素的left的百分比数值才会得出正确结果。
对于定宽元素这几种方法也适用。
对于一行文字,直接令line-height值===height值即可。
父元素上下等padding或子元素上下等margin即可。
当然,换用绝对定位就不用担心bfc了,父元素position:relative;子元素position:absolute;top:50%;margin-top:-高度一半;最终效果相同。
<iframe width="100%" height="300" src="//jsfiddle.net/3eq1upod/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe>使用table实现居中是非常方便的,单独拿出来说一下。子元素设置display:table-cell;结合text-align:center;vertical-align:middle;即可。
<iframe width="100%" height="300" src="//jsfiddle.net/rtq24yof/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe>子元素:position:absolute;margin:auto;left:0;right:0;top:0;bottom:0;
text-align:center + vertical-align:middle 利用占位符作为vertical-align:middle的参照,实现水平与垂直居中,父元素需要定高height:inherit;详见参考资料最后一条
text-align:center + font-size(87.3%) 通过匿名文字节点的高度作为vertical-align:middle的参照,实现水平与垂直居中,父元素需要定高。详见参考资料最后一条
CSS grid下的水平垂直居中,近IE10/11支持
使用button标签自带的水平垂直功能。详见参考资料最后一条
居中这部分坑大无比,情况非常复杂多变,还会有许多特殊情况,随见到随收集。参考资料里会先放一些。
flexbox是css3中的一个新的布局模式。久闻大名,最近在一个页面中大量应用,写起来真是爽啊——,现在小小总结一下。
详细资料可见页底的参考资料,这里只把干货提炼一下。
要使用flexbox布局,首先要定义伸缩容器(flex containers)与伸缩项目(flex items)。设置display: flex;即可将一个元素定义为伸缩容器,伸缩容器内的元素自动成为伸缩项目。
伸缩容器的常用属性:
设置的flex-direction(row | row-reverse | column | column-reverse)可定义伸缩项目的布局方式。
默认的情况下,flex items会尝试在一行中显示,通过修改flex-wrap(nowrap | wrap | wrap-reverse),定义是否换行。
flex-flow是上述两个属性的简写,默认值为row nowrap。
justify-content(flex-start | flex-end | center | space-between | space-around;)定义相对主轴的对齐方式。水平居中一句搞定。
align-content(flex-start | flex-end | center | space-between | space-around | stretch)定义相对十字轴的对齐方式。多行情况下垂直方向上剩下的空间的分配方式。
align-items(flex-start | flex-end | center | baseline | stretch)定义flex item在十字轴(垂直于主轴)的当前行上的默认行为。垂直居中一句搞定。
很方便的实现水平垂直居中的例子:
<iframe width="100%" height="300" src="//jsfiddle.net/1n4n0j5e/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe>伸缩项目的常用属性:
flex-grow()让flex items根据数值比例自适应分配空间。
flex-shrink()定义flex items的伸缩能力。
flex-basis( | auto)定义了在有剩余的空间可分布时一个元素的默认大小。
flex(<'flex-grow'> <'flex-shrink'>? || <'flex-basis'>)是 flex-grow, flex-shrink和flex-basis的缩写。默认值是 0 1 auto。
align-self(auto | flex-start | flex-end | center | baseline | stretch;)这个元素与flex items自己的对齐方式有关。
参考资料:
title: 使用 expect/git hooks 实现项目在服务器端的自动部署
categories:
项目在本地被开发,之后肯定要被送到服务器上。每次都用 scp
/rsync
送上去随后重跑服务未免过于繁琐;特别是在公司内网还存在着堡垒机的情况下,如果要把文件送上目标机器,必须要通过堡垒机做中转,每次手打长串命令很麻烦。使用 expect
/git hooks
可以简化流程,实现一行指令让项目自动部署在服务器上。
Git hooks
通过利用 Git 服务器端钩子(hook)中的 post-receive
挂钩,可以实现在 git push
流程结束后执行自定义脚本,如部署文件、发送邮件、持续集成、更新系统等。
举例一个典型的部署 node.js 后端服务的流程:项目开发完先在本地 git push
,随后登录到服务器上,进入服务器端仓库执行 git pull
, 更新数据,然后做一些操作,比如重新启动服务/启动服务器/启动守护进程之类的...直到部署完毕。使用 git hooks
后,你只需一步 git push
,其他流程将自动完成。
由于要部署的前端页面一般都在项目中的 dist 文件夹中,而 dist 一般不加入版本控制系统,所以这种方法一般不用来部署前端页面。部署前端页面可以使用下文的 expect 自动部署脚本。
另一个注意点是,你应该能直接从本机连接服务器的 git 仓库。公司内网可能会对 git 仓库的访问设限。
带有 --bare
参数的初始化命令创建的仓库没有工作路径,无法进行编辑提交与更改,这样的仓库称为裸仓库。由于对非裸仓库(non-bare repository)进行分支推送有重写更改的潜在风险,因此远端的**仓库通常被创建为裸仓库。几乎在所有的 Git 工作方式下,**仓库都是裸仓库,而开发者的本地仓库都是非裸仓库。
在服务器上找个地方,创建个裸仓库。
cd ~
git init --bare test.git
在本地 git 项目下
git remote add deploy user@ip_address/root/test.git
之后就可以将项目文件 push 进裸仓库了。
git push deploy master
在服务器上找个地方创建服务器仓库。同样将裸仓库添加至其 remote 源中。
cd ~
mkdir deploy
cd deploy
git init
git remote add deploy ../test.git # 裸仓库和服务器仓库都在服务器上,这里写相对路径就行。
每当本地 git push
之后,在服务器仓库就可以运行 git pull
拉一份最新数据下来。
git pull deploy master
进入服务器上 test.git/hooks 文件夹,里面已经提供了一些示例钩子文件。这些示例文件去掉 .smaple
后缀名后才会生效。这里我们使用 post-receive
这个钩子,该钩子在每次接收完新 push 后执行。更多的钩子介绍可以查看这篇文章:How To Use Git Hooks To Automate Development and Deployment Tasks。
cd test.git
cd hooks
vi post-receive
编辑脚本内容,在这里我们可以运行部署文件(让服务器仓库自动 pull 数据,发邮件,重启服务器...等等操作)
#!/bin/sh
cd /root/deploy
unset GIT_DIR
git pull deploy master # 让服务器仓库自动 pull 数据
exit 0
请注意 unset GIT_DIR
这句指令一定要写,否则会报 remote: fatal: Not a git repository: '.'
错误。post-receive
钩子会默认把 $GIT_DIR
设置为 .
,这会导致问题,详见 Git checkout in post-receive hook: “Not a git repository '.'”。
最后为该脚本文件提权。
chmod +x post-receive
钩子配置完毕。
现在自动部署的流程已经搭建完毕。在本地 git 项目做修改,git push
之后,可以看到 log 如下:
可以看到返回的 log 里已经包含了执行钩子时的日志。在这里就可以清晰的看到钩子执行后,服务器仓库已经自动 pull 新数据下来啦。
expect 是 linux 下的一个命令,可以帮助你自动化处理 ssh/ftp 登录后在服务器上的操作。
以下以部署一个前端单页项目为例,说明下 expect 如何实现自动将待部署文件送上堡垒机,再送上内网服务器。
expect 语法非常简单,只有4个指令:
spawn ssh [email protected]
expect "password"
send "hahaha\r"
interact
代码与实际流程是一一对应的:
其实很简单,只要搞懂 expect 命令如何使用:即等待屏幕出现指定字样后再执行下一步操作。
拿一个典型的前端项目举例,部署流程如下:
npm run build
;代码如下:(分为两个脚本执行,本地操作由 shell 脚本执行,ssh 登录之后的操作由 expect 脚本执行)
upload.sh:
npm run build
cd dist
zip -r public *
cd ..
rsync -rvP dist/public.zip [email protected]:/home/example/
echo "upload to 10.x.x.x done."
rm public.zip
/usr/bin/expect ./expect.exp #执行 expect 脚本
expect.sh
spawn ssh @10.x.x.xxx #由于配置了 ssh 认证免密码登录,不需要输密码
send "scp /home/example/public.zip [email protected]:/root/fe-platform-backend\r"
expect "total" #scp命令执行完毕,屏幕会出现含 "total" 字样的提示
send "ssh [email protected]\r"
expect "bash"
send "unzip -o /root/fe-platform-backend/public.zip -d /root/fe-platform-backend/public\r"
expect "inflating" #scp命令执行完毕,屏幕会出现含 "inflating" 字样的提示
send "rm /root/fe-platform-backend/public.zip\r"
expect "bash" #最后再加一个 expect 是为了看到 rm 操作是否成功,否则运行完 send 会直接退出
在编写 expect 脚本时,你可以使用 set
来设置变量,使用 [expr]
来执行表达式,使用 if
/for
/while
写判断和循环。这些语法可以帮助你完成功能更多更复杂的脚本。语法文档
title: JS自我梳理:闭包、私有变量
categories:
继续整理。
闭包,指有权访问另一个函数作用域内变量的函数。创建闭包的常见方式就是在一个函数内部创建另一个函数。
function a(A){
return function(B){
return A+B;
};
}
var be = a(5);
console.log(be(10));//15
例子中匿名函数访问了外部函数中的变量A。即使这个匿名函数被返回了,或者在其他地方被调用了,它依然可以访问变量A,因为匿名函数的作用域链中包含a()的作用域。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其它命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直至作为作用域链终点的全局执行环境。
后台的每个执行环境都有一个表示变量的对象——变量对象。全局的变量对象始终存在,而像a()函数这样的局部环境的变量对象只在函数执行的过程中存在。一般来讲当函数执行完毕时,局部活动变量就会被销毁,内存中仅保存全局作用域。但是闭包的情况又有所不同。如刚才的例子,a()函数在执行完毕之后,其活动变量也不会呗销毁,因为匿名函数的作用域链依然在引用着这个活动对象。实际上,当a()函数在执行完毕后,其作用域链会被销毁,但它的活动对象仍然会留在内存中。直到匿名函数被销毁后,a()的活动对象才会被销毁。
var be = null;
解除对匿名函数的引用。
因为闭包保存的是整个变量对象而不是某个特殊的变量,所以闭包只能取得包含函数中任何变量的最后一个值。
function a(){
var result = [];
for(var i=0;i<10;i++){
result[i] = function(){
return i;
}
}
}
这个函数会返回一个函数数组。表面上看应该每个函数都应该返回自己的索引值,然而10个函数返回的均为10。这就因为每个函数的作用域链保存的不是i这个变量的值,而是a()函数的活动对象。每个函数都引用着保存变量i最后一个值的变量对象,所以每个函数内部i都是10。这种情况下,我们可以创建另一个匿名函数强制让闭包的行为符合预期:
function a(){
var result = [];
for(var i=0;i<10;i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
}
这样定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。由于函数参数是按值传递的,所以会将变量i的当前值赋值给num。而在这个匿名函数的内部又创建并返回了一个访问num的闭包。这样一来,result数组的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。
在全局函数中,this指向window;而当函数被作为某个对象的方法调用时,this指向那个对象。匿名函数的执行环境具有全局性,因此其this对象通常指向window。
var a = {
Name: 'hello',
getName: function(){
console.log(this.Name);
}
};
a.getName();//'hello'
getName作为a对象的方法调用,this指向a,输出'hello'。如果getName内加入闭包函数会怎样呢?
var a = {
Name: 'hello',
getName: function(){
return function(){
console.log(this.Name);
};
}
};
a.getName()();//undefined
因为匿名函数的this指向了window,所以输出undefined。那如果闭包想要访问Name属性要怎么做呢?
var a = {
Name: 'hello',
getName: function(){
var that = this;
return function(){
console.log(that.Name);
};
}
};
a.getName()();//'hello'
定义匿名函数之前,我们把this变量赋值给that,闭包也可以访问that,以此为桥梁进行访问。
js没有块级作用域,只有函数作用域。在块语句中定义的变量,实际上是在包含函数中创建的。
function a(){
for(var i = 0;i < 5; i++){
....
}
alert(i);//5
}
如果在c、java等其他语言中,i只在for循环中有定义,循环结束后i就会销毁。而js没有块级作用域只有函数作用域,所以i在a()函数内都可以访问到。
如果需要块级作用域的话, 那么可以通过立即执行的匿名函数来实现。
(function(){
....块级作用域
})();
function a(){
(function(){
for(var i = 0;i < 5; i++){
....
}
})();
alert(i);//ReferenceError: i is not defined
}
无论是在什么地方,只要临时需要一些变量,都可以使用私用作用域。
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。
任何在函数内部定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。我们把有权访问私有变量和私有函数的公有方法叫做特权方法。
使用构造函数主要是为了自定义类型创建私有变量和私有方法。
function MyObject(name){
//定义私有变量和私有函数
var privateVariable = "10";
function privateFunction(){
return false;
}
//特权方法
this.getName = function(){
return name;
}
this.setName = function(newName){
name = newName;
}
this.publicMethod = function(){
privateVariable++;
return pricateFunction();
}
}
不使用特权方法的话,没有其他办法可以访问name、privateVariable、privateFunction。将私有变量和函数写在构造函数内部的问题是,每创建一个示例都会创建同样一组新方法。使用静态私有变量可以避免这个问题。
(function(){
//定义私有变量和私有函数
var privateVariable = "10";
function privateFunction(){
return false;
}
//定义构造函数
MyObject = function(){};
//特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return pricateFunction();
}
})();
使用原型,所有实例引用相同的私有变量和私有函数。
模块模式是为单例创建私有变量和私有方法。单例指的就是只有一个实例的对象。
普通方式创建单例对象:
var single = {
pro : "str..",//属性
method: function(){
//方法
}
}
使用模块方式创建单例对象:
var single = function(){
//定义私有变量和私有方法
var privateVariable = "10";
function privateFunction(){
return false;
}
//特权方法和公有属性/特权
return {
publicVariable: true,
publicMethod: function(){
privateVariable++;
return pricateFunction();
}
}
}
在匿名函数的内部定义私有变量和私有方法,在返回的对象字面量中定义公有属性/方法和特权方法。由于这个对象是在匿名函数内定义的,所以其公有方法可以访问私有变量和私有方法。
还有一种叫“增强的模块方式”,理解起来很简单:如果要求返回的对象属于某种类型,比如Person类型,就这样写:
var single = function(){
//定义私有变量和私有方法
var privateVariable = "10";
function privateFunction(){
return false;
}
var obj = new Person();
//增强
//特权方法和公有属性/特权
Person.publicVariable = true;
Person.publicMethod: function(){
privateVariable++;
return pricateFunction();
}
return obj;
}
接下来打算整理下坑坑哒变量提示和函数声明提升:)
title: 该用async写异步代码了
categories:
最近在写一个自动化脚本时,由于需要多个异步任务逐步进行,所以对 ES6 的 generator 和 ES7 的 async 都做了尝试,发现 async 处理异步操作极为优雅好用,记录如下,顺带回顾一下异步处理的进化。
回调函数是我们见过的最多的了,也就是使用 callback。
以下是一段示例代码,主要执行了3项任务:
//打开数据
mongodb.open((err,db) => {
if(err){console.error(err);}
//读取 users 集合
db.collection('users',(err,collection) => {
if(err){
mongodb.close();
console.error(err);
}
//处理集合数据
process(collection);
});
});
相信写过数据库相关操作的大家都很熟悉,数据库的增删查改基本都是异步操作。从这里也就能看到回调函数的弊端:层数一多,容易产生 callback hell,可读性急剧下降。(尤其是最后的连成一排的反括号、反花括号和分号的结合体,很容易造成语法错误..)
Promise 为了解决这个问题,提出了一种新的语法,允许将回调函数的横向加载,改成纵向加载。ES6 中也原生提供了 Promise 对象。
//需要将 mongodb.open、db.collection 都提前 promise 化
mongodb.open()
.then((db) => {
return db.collection('users');
})
.then((collection) => {
process(collection);
})
.catch((err) => {
console.error(err);
});
这样写法看起来清楚一些,不过之前的函数每一个都要 Promise 化。纵向排布的代码流被许多 then
分割开,还不够优雅。
Generator 最大的特点是引入了 yield 语法,这个语法提供了暂停功能。Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。
function* gen() {
let db = yield mongodb.open();
let collection = yield db.collection('users');
process(collection);
}
let genPlayer = gen();
genPlayer.next();
genPlayer.next();
genPlayer.next();
这种同步式写法非常棒。然而有一个关键的问题是:我们需要自己手动使用 next()来推动流程的进度,这太蛋疼了。我们所想的是,当上一个异步函数执行完毕时自动进入下一个异步函数,也就是能自动推进流程。
co模块是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行。其实就是为了弥补上面说到的缺陷的一个语法糖。
let co = require('co');
function* gen() {
let db = yield mongodb.open();
let collection = yield db.collection('users');
process(collection);
}
co(gen);
这么方便的功能,为什么 Generator 之中没有原生携带呢。(吐槽)
async 函数就是 Generator 函数的语法糖。你想要的功能,这里都有。
//需要将 mongodb.open、db.collection 都提前 promise 化
let auto = async function () {
let db = await mongodb.open();
let collection = await db.collection('users');
process(collection);
}
let player = auto();
语法上,只是把 * 变成了 async,yield 变成了 await。await 后面跟的可以是 promise 对象或者原始类型值(此时就是同步操作),async 函数执行完毕后返回的也是 promise 对象,这样可以方便多个 async 互相调用。async 带有自动执行器,promise 对象的 resolve/reject 标志着某一个异步流程的结束,通知 async 进入下一个异步任务。
可以看到,使用async的话基本上已经是同步式写法了。
参考:
title: ifetask0001的一些总结点
categories:
这倒是我第一次遇见这种问题,解决方案是使用margin负边距,学习了。
子元素设置padding-bottom:9999px;margin-bottom:-9999px;先通过padding把盒子扩展到足够高,然后通过margin负边距把它给拉回来,最后父元素设置overflow:hidden隐藏溢出,这样多栏布局中就会以最高栏为其他栏的视觉高度。
关于margin负边距的更多应用可以看margin负值 – 一个秘密武器这篇文章。
设计稿有一点要求:Github ICON距离右侧10px,点击后新打开页面进入Github。页面宽度小于980px时隐藏图标。
点击后新打开页面:给a标签增加target="_blank"。
页面宽度小于980px时隐藏图标就用媒体查询来做吧:
@media (max-width: 980px){
.icon-github {
visibility: hidden;
}
}
更多关于媒体查询的可以从参考这篇文章使用 CSS 媒体查询创建响应式网站
上网查了一些资料,发现瀑布流一般有2种方案:float布局(inline-block布局)、绝对定位布局。pinterest使用绝对定位布局,每个格子位置动态计算而来。
参考资料:瀑布流布局(基于多栏列表流体布局实现)
自己一般是啥都用ul,这次总结一下。ol的好处是在css资源未能加载上时能提供1、2、3这种语义化的信息...
<ul>
<li>无序列表</li>
<li>无序列表</li>
<li>无序列表</li>
</ul>
<ol>
<li>有序列表</li>
<li>有序列表</li>
<li>有序列表</li>
</ol>
<dl>
<dt>列表头</dt>
<dd>列表内容</dd>
<dd>列表内容</dd>
</dl>
刚看到时仔细想了想怎么实现,后来觉得中轴线还是做在背景比较好。切一个4x1的图片,设置background-repeat:repeat-y;background-position:top center。就好了,物件放在时间轴上面,做好对齐。
用table做方便一些,日期那里用rowspan。
title: 常见知识点汇总(五):正则
categories:
正则表达式是一个描述字符模式的对象。
Javascript的RegExp类表示正则表达式,String和RegExp都定义了方法,后者使用正则表达式进行强大的模式匹配和文本检索与替换功能。
可以使用RegExp()构造函数来创建RegExp对象,不过RegExp对象更多的是通过一种特殊的直接量语法来创建。
正则表达式直接量定义为包含在一对斜杠(/)之间的字符,如:
var pattern = /s$/;
若使用构造函数:
var pattern = new RegExp("s$");
正则表达式的模式规则是由一个字符序列组成的。包括所有字母和数字在内,大多数的字符都是按照直接量仅描述待匹配的字符的。/java/可以匹配任何带有”java“子串的字符串。除此之外,正则表达式中还可以含有其它具有具体特殊语义的字符。
正则表达式中所有字母和数字都是按照字面含义进行匹配的。非字母的字符匹配需要通过\作为前缀进行转义。如下:
此外还有一些标点符号有特殊含义:
^$.*+?=!:|/()[]{}
若想匹配这些字符的直接量也要使用前缀\,其他标点符号如@、引号则没有特殊含义。
如果不记得哪些标点符号需要反斜线转义,可以在每个标点符号之前都加上反斜线。
将直接量字符单独放进方括号内就组成了字符类。一个字符类可以匹配它所包含的任意字符。/[abc]/可以匹配"a"、"b"、"c"中任意一个。^符号可以否定字符类。/[^abc]/匹配非"a"、"b"、"c"的任意字符。字符类使用连字符来表示字符范围。
例:
上表列出的匹配重复字符为贪婪匹配,为尽可能多的匹配;非贪婪匹配只需要在待匹配的字符后跟随一个问号即可。"??","+?","*?","{1,5}?"。
例:
/a+/及/a+?/都可以匹配1个或多个连续的字母a,当字符串为"aaa"时,/a+/会匹配3个字符,/a+?/会匹配1个字符。
| 用于分隔供选择的字符
/ab|cd|ef/ 可以匹配"ab"、"cd"、"ef"。
注意:选择项的匹配顺序是从左到右,直到发现了匹配项。如果左边的符合匹配,就忽略右边的匹配项。
/a|ab/匹配"ab"时,只匹配第一个字符。
()圆括号的作用:
把单独项组合成子表达式
/java(script)?/ 加上圆括号可以对"script"应用?操作
定义子模式
允许同一正则表达式的后部引用前面的子表达式。这是通过在字符""后加一位或多为数字来实现的。
例如:
/['"][^'"]*['"]/ 匹配位于单引号或双引号之内的0个或多个字符,但并不要求两个引号匹配。
/(['"])[^'"]*\1/ 匹配位于单引号或双引号之内的0个或多个字符,同时要求两个引号匹配。
如果使用(?:)那么意味着仅仅用于分组,引用并不带有数字编号。
正则表达式中的锚字符:
例如:
/[Jj]ava([Ss]cript)?(?=:))/ 可以匹配"JavaScript:",不能匹配"Java";
/Java(?! Script)([A-Z]\w*)/ 可以匹配"Java"猴跟随一个大写字母和任意多个ASC2单词,但Java后面不能跟随Script。
修饰符放在第二条/之后。
search(),参数为一个正则表达式,返回第一个与之匹配的子串的起始位置,如果找不到匹配的子串,它将返回-1.
"JavaScript".search(/script/i) //返回4
search()方法不支持全局检索,忽略修饰符g。
replace()方法用以执行检索和替换操作。第一个参数是正则表达式,第二个参数是要进行替换的字符串。
//将所有不区分大小写的javascript都替换成大小写正确的Javascript
text.replace(/javascript/gi, "JavaScript");
* match()方法是最常用的String正则表达式方法。它的唯一参数就是一个正则表达式(或通过RegExp()构造函数将其转换为正则表达式),返回的是一个由匹配结果组成的数组。如果该正则表达式设置了修饰符g,则该方法返回的数组包含字符串中的所有匹配结果。
``` javascript
"1 plus 2 equals 3".match(/\d+/g) // 返回["1", "2", "3"]
如果正则没有设置修饰符g,match()不会全局检索,而是只检索第一个匹配,返回一个数组。数组的第一个元素就是匹配的字符串,余下的元素则是正则表达式中用圆括号括起来的子表达式。
最后一个是split(),可以将调用它的字符串拆分为一个子串组成的数组,使用的分隔符是split()的参数。如:
"123,456,789".split(","); //返回["123","456","789"];
split()的参数也可以是一个正则表达式,例如可以指定分隔符,允许两边留有任意多的空白符。
``` javascript
"1, 2, 3, 4, 5".split(/\s*,\s*/); //返回["1","2","3","4","5"]
```
RegExp对象也可以通过RegExp()构造函数生成。它包括一个或者两个字符串参数。第一个参数是正则表达式的内容,第二个参数是标志(可选),例如:g, i, m等。注意,需要转义时用\代替\。
var zipcode = new RegExp("\\d{5}", "g");
RegExp对象定义了两个用于执行模式匹配操作的方法。
第一个方就是exec()方法,类似于match()方法。不同于match()的是,exec()方法无论是否有 g 标志,它都只返回同样的数组array。array的第一个元素array[0]储存完全匹配的字符串,随后的元素一次储存与子字符类想匹配的子字符串。当模式有 g 标志的时候,exec()方法执行一次以后,会自动将RegExp对象的一个特殊属性lastIndex置为此次匹配的字符串的最后一个字母的后一个位置。
当同一个正则表达式再次执行的时候,会在lastIndex位置开始查找,而不是0位置开始查找。如果exec()没有找到匹配的字符串,它将自动将lastIndex置为0。这个特殊的方法,可以很方便的循环遍历整个字符串,以找到所有匹配的子字符串。当然,你也可以在找到最后一个匹配子字符串以前的任意时刻将lastIndex置为0,然后用该RegExp对象执行另外的字符串。
var pattern = /Java/g;
var text = "JavaScript is more fun than Java!";
var result;
while((result = pattern.exec(text)) != null)
{
alert("Matched '" + result[0] + "'" + " at position " + result.index + "; next search begins at " + pattern.lastIndex);
}
RegExp对象的另外一个执行匹配的方法是test(),它要比exec()简单的多。它只有一个字符串作为唯一的参数,返回true或者在没有找到匹配字符串是返回null。当RegExp有g标志时,test()与exec()对lastIndex执行同样的操作。
title: 使用pi-gpio控制树莓派GPIO
categories:
尝试使用pi-gpio来控制树莓派GPIO,踩坑一晚,特此记录。
我如果直接用sudo apt-get install nodejs
安装得到的nodejs发现是0.6版本的,npm版本才1.x,太落后了,各种无法使用,所以使用了其他方法。
//Adding the Package Repository
curl -sLS https://apt.adafruit.com/add | sudo bash
sudo apt-get install node
pi@raspberrypi ~ $ node -v
v0.12.6
使用这种方法安装的nodejs起码到了0.12版,足够用了。
也可以寻找为arm平台准备的debian用源码,自行编译。
gpio-admin是一个命令行工具,可以让你在命令行下export或unexport树莓派的GPIO引脚。
git clone git://github.com/quick2wire/quick2wire-gpio-admin.git
cd quick2wire-gpio-admin
make
sudo make install
sudo adduser $USER gpio
随后注销用户并重新登录。
gpio-admin这里有坑。
在工程目录里,运行
sudo npm install pi-gpio
当你兴高采烈的按照例程写完代码之后,跑起来会发现open时直接报错:
failed to change group ownership of /sys/devices/virtual/gpio/
这是因为raspbian系统内核更新到3.18.x后移动了gpio的存放位置,我们需要相应修改gpio-admin的配置。
打开之前clone下来的quick2wire-gpio-admin/src/gpio-admin.c文件,将第30行左右的
int size = snprintf(path, PATH_MAX, "/sys/devices/virtual/gpio/gpio%u/%s", pin, filename);
修改为
int size = snprintf(path, PATH_MAX, "/sys/class/gpio/gpio%u/%s", pin, filename);
然后重新编译安装quick2wire-gpio-admin。
好,让我们再跑一次。
这次又报错:
[Error: ENOENT, open '/sys/devices/virtual/gpio/gpio17/value']...
看来又变成pi-gpio的路径出错了,我们又要改pi-gpio的配置。
在node_modules文件夹下找到pi-gpio.js,打开,修改第7行
sysFsPath = "/sys/class/gpio"
看起来0.0.7版本的pi-gpio尝试着自动匹配路径,它分为了sysFsPathOld和sysFsPathNew,判断内核版本高于3.18.x时使用新地址,否则使用老地址。
但是在我电脑上它并没有使用新地址,依然在用老路径。所以直接改
sysFsPathOld = "/sys/class/gpio"
把new和old地址都改成这个。
再运行一次,终于可以正常跑通了。
我只想测试个倾斜传感器能否正常联通,查了一晚上issue....
参考资料:
title: 微信浏览器实现后退记忆浏览位置
categories:
不知道你有没有注意过,当点击浏览器工具栏上的后退按钮,亦或是在移动设备上触发浏览器的后退键,浏览器会在当前窗口打开前一个页面,不同浏览器“打开”前一个页面的方式是不同的。chrome 浏览器点击后退按钮时,会准确返回到前一个页面离开时的状态,连 input 中输入的字都会还原,而微信浏览器的行为则大不一样。
当触发浏览器的后退按钮时,用户希望页面能返回到与上次离开时分毫不差的状态,这样便可继续浏览与操作;然而这并不是所有浏览器都能做到的。在移动端对浏览器做测试,结果如下:
浏览器 | 行为 |
---|---|
移动端 chrome | 全部使用缓存,不发送新请求,不执行 js,准确返回到上次浏览位置( bfcache ) |
移动端 safari | 全部使用缓存,不发送新请求,不执行 js,准确返回到上次浏览位置( bfcache ) |
ios 微信浏览器 | 重新请求页面本体,图片 / js / css 等没有改变的静态资源使用缓存(有小几率重新请求页面上全部的图片资源),重新执行 js,尝试返回到上次浏览位置 |
安卓微信浏览器 | 不重新请求页面本体,图片 / js / css 等没有改变的静态资源使用缓存(有小几率重新请求页面上全部的图片资源),重新执行 js,尝试返回到上次浏览位置 |
相比之下,chrome 和 safari 因为具有 BFcache,页面前进时会把页面状态完整保存在内存里,后退时直接取缓存,不用做任何操作,即可完美实现“后退时返回到上次浏览位置”。
调查了一下其他大型网站,如京东、今日头条、饿了么、腾讯新闻等,都没有对微信浏览器中的这个问题做出修复;我司大部分流量来自于微信浏览器,这个问题一定要克服,所以就需要自己写逻辑了,让微信浏览器也能实现后退时准确返回到上次浏览位置。
将问题简化一下,则为“后退时,使上一个页面准确返回到上次浏览位置”。由于JS要重新执行,所以可以在 JS 中加两部分的逻辑:一是在页面前进时,将当前状态缓存下来;二是后退到该页面时,取出状态并手动复原。
这种方案的难点有两个:
起初打算使用 document.referrer 来判断,后来经过实际测试,发现在“后退”场景下 document.referrer 不能返回正确的结果。比如,A 页面跳转到 B 页面,B 页面的 document.referrer 为 A;A 页面跳转到 B 页面跳转到 C 页面,再后退到 B 页面,这时查看 B 页面的 document.referrer,发现还是 A ....恩,这条路是走不通的。
随后考虑根据浏览器的一个特性——“后退时 URL 不会改变”来实现判断。这个特性是:如果你从 A URL 跳转到了 B 页面,那么点击后退,再次访问的一定是同样的 A URL。在用户点击链接,跳转离开页面之前(比如写在 unload 事件里),改变 URL,如将 A 变成 A#token,这样从 B 页面后退时,访问的就是 A#token;在 JS 中再加一段逻辑,判断 token 是否存在,即可得知是否是后退进入。
经过测试,这种方法很稳定,但是改变 hash 有两个不可忽视的缺点:一个 URL 只能有一个 hash,如果页面的业务逻辑代码需要操作 hash,那么很容易出现冲突的现象。二是修改 hash 会将当前 url 推入 history 栈,那么用户在连续后退时会出现这种情况:B 页面 --> A#token --> A,A#token 状态下点击后退到 A,用户会觉得“咦这次后退怎么没反应”,体验也不是很好。
再次思考,除了 hash 以外改变URL的方式——那就是html5中新出的historyAPI了。通过history.replace这个API,可以方便的修改URL,同时还不会影响history栈,简直完美。可惜查看兼容性:ios兼容良好,安卓4.1+开始支持historyAPI。
虽然安卓4.1-的用户已经很少了,但是能解决的问题就要尽量解决。不能用historyAPI,还有别的办法吗?这时,我注意到了安卓微信浏览器的一个特性:后退时不会重新请求页面本体。利用这个特点,可以将token整合在服务器端返回的html中。由于我做的项目前后端中间存在一个PHP层,那么使用php在页面中打入一个时间戳timestamp;每次执行JS时,获取页面上的时间戳,并试图与缓存中的时间戳进行比较(若缓存中无时间戳则将本次获得的时间戳写入);若为后退进入,则比对结果则为相同;若非后退进入,则获取到的时间戳必大于缓存中保存的时间戳。
我们要实现的是“缓存状态并还原”,说起来容易做起来难。比如这个页面有个下滑加载,当前加载到第3页了,那么当你后退回来时,不但页面样式要立刻还原为第3页的状态,还要保证接下来下滑出来的是第4页,而不是第1页。
由于项目中模块化做的不错,每个页面基本都是一个独立的模块,并且模块中一般划分为init、render(构建并渲染HTML字符串)、bindEvent(绑定事件)几部分,相关变量均为模块的私有参数。所以使用了简单粗暴的方式:直接将DOM结构与需要保存的状态参数(例如当前看到第几页)缓存入sessionStorage中。页面init时会检测是否为后退进入,非后退进入则依次调用render、bindEvent,后退进入则不再调用render,直接取缓存的DOM填入页面,并将上下文变量还原,再执行一遍bindEvent,最后调用scrollTo,将滚动条定位到离开时浏览到的位置。
离开页面时:
进入页面时:
beforeunload事件被微信浏览器阉割掉了,完全不能使用。
title: 《The Linux Command Line》个人整理
categories:
平时在 macOS 下工作,shell 里的命令一直没好好研究过,其实很多命令都很有趣。之前遇到 ">"、"|"、"cat" 这些命令时都一脸懵逼,这次好好补习一下这些基础知识。
顺便回忆一下,平时用的比较多的命令大概有:
Linux 下的命令和 macOS 里的还是有些区别,比如 TLCL 中说 Linux 下 md5sum 可以算文件的 md5 值,而 macOS 里是 md5 命令来算。
参考:
title: 《JS精粹》学习手记
categories:
/*
var rm_a = /a*/.match(s);
*/
其他所有值都被当做真,包括true,字符串'false',和所有的对象。
a&&b a为true,返回b。a不为true,返回a。
a||b a为true,返回a。a不为true,返回b。
方法调用模式
当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。
函数调用模式
当一个函数并非对象的属性时,那么它就是被当做一个函数来调用的。以此模式来调用函数时,this被绑定到全局对象。
构造器调用模式
如果一个函数前面带上new来调用,那么背地里将会创建一个连接到该函数的prototype成员的新对象,同时this会被绑定到那个新对象上。
Apply调用模式
apple方法让我们构建一个参数数组传递给调用函数。它也允许我们选择this的值。apply方法接受两个参数,第一个是要绑定给this的值,第二个是参数数组。
蝴蝶书上说可以给类似Function.prototype增加方法来给基本类型扩充功能,实际上是不可取的。修改基本类型构造函数原型是一道高压线,万万不可触及,因为在使用框架和库时,很容易带来不可预知的兼容性问题。
使用递归可以非常高效的操作树形结构。比如浏览器端的文档对象模型(DOM)。每次递归调用时处理指定的树的一小段。
var walk_the_DOM = function(node, func){
func(node);
node = node.firstChild;
while(node){
walk_the_DOM(node, func);
node = node.nextSibling;
}
}
函数也是值,从而我们可以用有趣的方式去操作函数值。柯里化允许我们把函数与传递给它的参数相结合,产生出一个新的函数。
Function.prototype.curry = function(){
var args = Array.prototype.slice(arguments);
var that = this;
return function(){
return that.apply(null, args.concat(Array.prototype.slice(arguments)));
}
}
function add(a, b){
return a+b;
}
var add1 = add.curry(1);
add1(5);//6
全局变量
全局变量就是在所有作用域中都可见的变量。因为一个全局变量可以被程序的任意部分任意时间更改,它们使得程序的行为变的极度复杂。在程序中使用全局变量降低了程序的可靠性。
Unicode
JavaScript的字符是16位的。
null
简单的检测null: my_value === null
typeof不能分辨出null,它的返回值是object。但是除了null所有对象值均为真,从而可以判断:
if(!my_value && typeof my_value === 'object') //为null
判断是否是有效数字
var isNumber = function(num){
return typeof num === 'number' && isFinite(num);//isFinite函数可以过滤NaN和无限数字
}
title: Javascript中如何遍历属性
categories:
JS中遍历对象的属性有多种情况。对象的属性即有属于自身的,也有从原型链上继承而来的。并且有的属性是可以枚举的,有的属性是不可枚举的,这由对象属性的可枚举性[[enumerable]]来确定。
遍历时我们可能用到的手段:
title: 诡异的字号异变现象——你知道font boosting吗?
categories:
最近在写一个移动端项目时,其中的一个页面出现了诡异的问题:浏览器显示的字号与 css 指定的字号不符。定义字号为28px,显示出来却为33px,具体示例图如下:
而且并非是局部元素字号异常,整个页面大多数的字号都产生了混乱。写了这么多年 css 代码,头一次遇到这种问题。由于当时是在此页面上增加了 dom 结构,增加前页面显示正常,所以优先怀疑是增加的部分产生了问题。但是令人诧异的是,增加的部分产生了问题为什么会影响了全局,这是怎么回事!
一点一点排查。我们写css时,只要选择器限定的准确,是不会影响到其他部分的。这里的选择器使用没有出现问题。随后又想到,影响全局的又一个理由可能是 html 标签书写错误,比如<span>abc</span>
手误或者误操作写成了<span>abc<span>
, 这种未正常闭合的标签,可能导致浏览器对接下来的dom结构构建异常。查了一遍,html也没有问题。
所写的页面是使用lib-flexible适配方案的,改变了dpr查看页面渲染结果,惊讶的发现,在dpr=1的情况下,页面显示正常!只有在dpr=2和dpr=3的情况下,这个问题才会出现。后来不停的对新增代码进行删改,缩小目标范围,意图找到触发错误的地点,然而发现触发条件十分刁钻:只有在某些特定的dom结构,某一行的文字大于一定数量时才会出现这个问题。
没能清楚确定下来渲染错误产生的原因,只能暂时想方法避免:给p标签内加入white-space: nowrap
可以使字号恢复正常(white-space是控制是否折行的,与字号有什么关系?)。容器换用flex布局@include flexbox
也可以解决问题。暂时采取后者,给所有容器换用了flex布局,临时解决了这个问题。当时是觉得触发了chrome浏览器的bug,上网试图搜了搜,无果。
后来,在查询lib-flexible的issues时,碰巧发现有人遇到了相同问题,在回答里看到了真实原因“Font Boosting”,又称“Text Autosizer”。是 Webkit 给移动端浏览器提供的一个特性:当我们在手机上浏览网页时,很可能因为原始页面宽度较大,在手机屏幕上缩小后就看不清其中的文字了。而 Font Boosting 特性在这时会自动将其中的文字字体变大,保证在即不需要左右滑动屏幕,也不需要双击放大屏幕内容的前提下,也可以让人们方便的阅读页面中的文本。
意思就是:在移动端页面缩放情况下(initial-scale!=1),chrome有可能重新调整字号。
使用lib-flexible布局的移动端页面,如果使用默认配置,在dpr=2或dpr=3的情况下,会修改initial-scale为0.5和0.333来扩大页面的CSS像素数至于物理像素数相同,以期实现苹果retina屏下的“物理1px”。而将initial-scale修改为小于1的值恰巧类同于缩放操作,满足了font-boosting触发条件之一。
但是其他同样使用lib-flexible布局的页面就不会出现这个问题,触发font-boosting的详细条件到底是什么?感兴趣的同学可以阅读这篇文章Chromium's Text Autosizer
Ali的mingelz同学分享了他的研究:
简单说来,Font Boosting 的计算规则伪代码如下:
multiplier = Math.max(1, deviceScaleAdjustment * textScalingSlider * systemFontScale * clusterWidth / screenWidth);
if (originFontSize < 16) {
computedFontSize = originFontSize * multiplier;
}
else if (16 <= originFontSize <= (32 * multiplier - 16)) {
computedFontSize = (originFontSize / 2) + (16 * multiplier - 8);
}
else if (originFontSize > (32 * multiplier - 16)) {
computedFontSize = originFontSize;
}
其中变量名解释如下,更具体的说明可以参考上边的两个链接。
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 或 -->
<meta name ="viewport" content ="initial-scale=1, maximum-scale=1, minimum-scale=1">
参考资料:
title: JS自我梳理:创建对象与继承
categories:
在批量创建对象时,使用普通的Object构造函数和对象字面量会造成大量重复代码。为了解决这个问题,出现了一些创建模式。
1.工厂模式
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}
var person1 = createPerson('john', 27, 'engineer');
在工厂模式中,createPerson函数封装了创建一个具有特定属性和方法的对象的操作,并返回该对象。多次调用该函数便可批量生成相似对象。
这种模式的缺点是不能解决对象识别的问题。
2.寄生构造函数模式
我将寄生构造函数模式放到工厂模式之后写,是因为除了寄生构造函数模式在调用构造函数时使用了new,其他都是完全一样的。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}
var person1 = new createPerson('john', 27, 'engineer');
最终返回的对象其实是完全一样的。只是用了new感觉更像构造函数的写法。
工厂模式与寄生构造函数模式返回的对象与构造函数或者构造函数的原型都没有关系,不能通过constructor或instanceof来判断对象类型。使用这种方法可以创建拓展功能的数组等等。
3.构造函数模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person('john', 27, 'engineer');
构造函数模式则解决了对象识别的问题。创建的person1对象是Person的一个实例。默认情况下person1有一个constructor属性(来自于prototype),其值指向Person。
alert(person1.constructor === Person); //true
但是由于prototype可重写,所以用constructor来进行对象识别是不稳定的;更妥当的方法是使用instanceof 操作符。instanceof可以检测实例。
alert(person1 instanceof Person); //true
alert(person1 instanceof Object); //true
使用构造函数模式可以将它的实例标记为特定的类型。但是依然有可以改进的地方:每一个实例都拥有一个独自的sayName方法。不同实例上的同名函数是不相等的,这在很多情况下是没有必要的。
这个问题可以使用原型模式来解决。
4.原型模式
function Person(){}
Person.prototype.name = 'john';
Person.prototype.age = 27;
Person.prototype.job = 'engineer';
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
使用原型模式可以让所有实例共享属性与方法。
当然,更简单是使用对象字面量方法。
Person.prototype = {
name: 'john',
age:27,
job: 'engineer’,
sayName: function(){
alert(this.name);
}
}
但是要注意的是,使用对象字面量方法会完全重写prototype,这样将去掉默认的constructor属性(并且默认是不可枚举的)。如果认为constructor属性非常重要,那么需要自己手动补上这个属性。
一般情况下不会是所有属性和方法都是共享的——构造函数所生成的实例,应该部分属性和方法是共享的,其它属性和方法是非共享的。共享部分放在原型中,非共享部分写在构造函数中,即为组合使用构造函数模式与原型模式。
5.组合模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype.sayName = function(){
alert(this.name);
}
使用最广泛、认同度最高的创建自定义类型的方式~
6.动态原型模式
动态原型模式继承了组合模式的优点。它将所有信息都封装在了构造函数之内,在构造函数之内初始化原型。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName!=='function’){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
7.稳妥构造函数模式
稳妥对象指的是没有公共属性,方法也不引用this的对象。
function Person(name, age, job){
var o = new Object();
o.sayName = function(){
alert(name);
}
return o;
}
var person1 = Person('john', 27, 'engineer');
console.log(person1.name);//undefined
console.log(person1.sayName());//'john'
除了调用sayName()方法,没有别的方式可以再访问其数据成员。
这种实现私有变量的方式涉及到闭包,打算之后在闭包相关内容再仔细写写:)
ECMAscript只支持实现继承,并且其实现继承主要是依靠原型链来实现的。
以下的示例都设定为:超类型SuperType与子类型SubType。
SuperType类型具有属性name、colors,方法sayName。
SubType类型具有属性name、colors、age,方法sayName,sayAge。
1.原型链继承。
//声明超类型,通过构造函数添加属性。
function SuperType(name){
this.name = name;
this.colors = ["red”, "blue”, "green”];
}
//通过原型添加方法。
SuperType.prototype.sayName = function(){
alert(this.name);
}
//声明子类型,通过构造函数添加属性。
function SubType(age){
this.age = age;
}
//将子类型的原型指向超类型的实例。
SubType.prototype = new SuperType();
//通过原型添加子类型的方法。
SubType.prototype.sayAge = function(){
alert(this.name);
}
var instance = new SubType();
画图如下:
PS: processon的在线作图工具还不错哈,我懒得下载本地作图软件,直接在这个网站上画的。
可以看出,通过将子类型的原型指向超类型的实例,子类型获得了超类型的属性和方法。(总感觉自己表述的并不清楚,还是看图说话吧。)
原型链继承的几点说明:
instance instanceof SubType;//true
instance instanceof SuperType;//true
instance instanceof Object;//true
SuperType.prototype.isPrototypeOf(instance);//true
子类型重写超类型中的方法或者添加新方法时,代码一定要写在替换原型的语句后面。而且不能用对象字面量写法,否则会重写原型链。
原型链的问题有二:1.子类型的原型重写为超类型的实例后,超类型的示例属性自然就成为子类型的原型属性了。如果属性的值为引用类型的话(比如colors属性)就会出现问题——由于是原型属性是共享一个colors属性,那么一处修改,在所有子类型示例中都会生效。2.创建子类型的实例时,不能向超类型的构造函数中传递参数。
实践中很少单独使用原型链。
2.借用构造函数
function SuperType(name){
this.name = name;
this.colors = ["red”, "blue”, "green”];
}
function SubType(name){
SuperType.call(this, name);
}
借用构造函数也很少有单独使用的情况,主要是提供一种思路:在子类型构造函数中通过call()或apply()调用超类型构造函数。这种方法就可以在子类型构造函数中向超类型构造函数传递参数。缺点是:函数复用困难,超类型原型中定义的方法对子类型不可见。
3.组合继承
在原型链继承模式中加入借用构造函数模式,多加一行代码即可。
//声明超类型,通过构造函数添加属性。
function SuperType(name){
this.name = name;
this.colors = ["red”, "blue”, "green”];
}
//通过原型添加方法。
SuperType.prototype.sayName = function(){
alert(this.name);
}
//声明子类型,通过构造函数添加属性。
function SubType(name,age){
//**加入下面这行代码:继承属性**
SuperType.call(this,name)
this.age = age;
}
//将子类型的原型指向超类型的实例。
SubType.prototype = new SuperType();
//通过原型添加子类型的方法。
SubType.prototype.sayAge = function(){
alert(this.name);
}
var instance1 = new SubType();
var instance2 = new SubType();
上图:
这样的话在创建不同子类型SubType实例时就会分别拥有自己的属性,又可以使用相同的方法了。
不过这样也有一个问题:既然每个子类型示例上都有独立的name和colors等属性,也就是覆盖了子类型原型上的同名属性。那么子类型原型上的name和colors属性就没有用处了,应该去除。下面介绍的寄生组合继承会涉及到这个问题~
4.原型式继承
原型式继承主要提供的也是一种思路:当并不需要构造函数,而是想要凭借已有的对象创建相似对象时,可以:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
上图:
实际上,object()函数对传入的对象进行了一次浅复制。返回的对象以传入对象o为原型,获得o的属性和方法。和原型链继承一样,包含引用类型值的属性始终会被共享。
ECMAScript5新增的Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
var anotherO = Object.create(o);
5.寄生式继承
寄生式继承是原型式继承的强化版。首先使用原型式继承进行浅复制创建新对象,随后对新对象进行强化,然后封装整个过程。
function createO(o){
var temp = object(o);
temp.sayHi = function(){
alert('hi!');
}
return temp;
}
var anotherO = createO(o);
不能做到函数复用。
6.寄生组合式继承。
终于说到这个看起来最厉害的。上面提到过,组合继承方式中子类型原型上的超类型实例属性是没有用的。使用寄生组合式继承则可以解决这个问题。寄生组合式继承通过借用构造函数来继承属性。通过原型链的混成形式继承方法。不再像组合继承一样,将子类型原型重写为超类型实例。而是使用寄生式继承,将子类型原型重写为超类型原型的一个副本。
function inheritPrototype(SubType, SuperType){
var temp = SuperType.prototype;//创建浅复制副本
temp.prototype.constructor = SubType;//增强属性和方法
SubType.prototype = temp;//将子类型原型指向该增强后的副本
}
function SuperType(name){
this.name = name;
this.colors = ["red”, "blue”, "green”];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
SuperType.call(this,name)
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.name);
}
上图:
子类型原型不再有多余的超类型实例属性,而且可以自己进行增强。最理想的继承方式~~
番外:
在调用构造函数时,如果忘记加new操作符,this会映射到全局变量上,导致全局变量属性的意外增加。为了避免这个问题,可以把构造函数稍加改造,成为作用域安全的构造函数。
function Person(name, age, job){
if(this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
}else{
return new Person(name, age, job);
}
}
作用域安全的构造函数在创建新的实例之前,首先确认this对象是正确类型的实例。如果漏加了new操作符,那么会创建新的实例并返回。
**问题:**作用域安全的构造函数不能应用与借用构造函数的继承方法。借用构造函数时,使用call()方法改变了this的指向,无法通过验证,也就不会在this上添加新的属性。解决这个问题的方法就是要同时应用原型链继承或寄生组合时继承,这样子类型的实例同时是子类型构造函数和超类型构造函数的实例,可以通过验证。
function Person(name, age, job){
if(this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
}else{
return new Person(name, age, job);
}
}
function Men(name, age ,job){
Person.call(this, name, age, job);//instanceof验证失败
}
function Person(name, age, job){
if(this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
}else{
return new Person(name, age, job);
}
}
function Men(name, age ,job){
Person.call(this, name, age, job);
}
Men.prototype = new Person();//现在Men的实例同时也是Person的实例,instanceof验证通过
title: 老生常谈——BFC与haslayout
categories:
“Layout”是一个 IE/Win 的私有概念,它决定了一个元素如何显示以及约束其包含的内容、如何与其他元素交互和建立联系、如何响应和传递应用程序事件/用户事件等。
这种渲染特性可以通过某些 CSS 属性被不可逆转地触发。而有些 HTML 元素则默认就具有“layout”。
微软的开发者们认为元素都应该可以拥有一个“属性(property)”(这是面向对象编程中的一个概念),于是他们便使用了 hasLayout,这种渲染特性生效时也就是将 hasLayout 设成了 true 之时。
当我们说一个元素“得到 layout”,或者说一个元素“拥有 layout” 的时候,我们的意思是指它的微软专有属性 hasLayout 为此被设为了 true 。使用object.currentStyle.hasLayout可获取到ture值,否则将获取到false。一个“layout元素”可以是一个默认就拥有 layout 的元素或者是一个通过设置某些 CSS 属性得到 layout 的元素。
而“无layout元素”,是指 hasLayout 未被触发的元素,比如一个未设定宽高尺寸的干净 div 元素就可以做为一个“无layout祖先”。
给一个默认没有 layout 的元素赋予 layout 的方法包括设置可触发 hasLayout = true 的 CSS 属性。参考默认 layout 元素以及这些属性列表。没有办法设置 hasLayout = false , 除非把一开始那些触发 hasLayout = true 的 CSS 属性去除。
下列元素应该是默认具有 layout 的:
<html>, <body>
<table>, <tr>, <th>, <td>
<img>
<hr>
<input>, <button>, <select>, <textarea>, <fieldset>, <legend>
<iframe>, <embed>, <object>, <applet>
<marquee>
下列 CSS 属性和取值将会让一个元素获得 layout:
IE7特有的触发Layout的属性
具有“layout” 的元素如果同时也 display: inline ,那么它的行为就和标准中所说的 inline-block 很类似了:在段落中和普通文字一样在水平方向和连续排列,受 vertical-align 影响,并且大小可以根据内容自适应调整。这也可以解释为什么单单在 IE/Win 中内联元素可以包含块级元素而少出问题,因为在别的浏览器中 display: inline 就是内联,不像 IE/Win 一旦内联元素拥有 layout 还会变成 inline-block。
BFC,块格式化上下文( Block formatting context ),是指初始化块级元素定义的环境。在CSS中,元素定义的环境有两种,一种是块格式化上下文( Block formatting context ),另一种是行内格式化上下文( Inline formatting context )。
触发条件如下:
title: 继续梳理-js中坑坑哒词法作用域、变量及函数声明提升
categories:
先来题目,猜猜运行结果是什么:
console.log(a);
var a = 1;
第二道:
myName = "global";
function foo() {
alert(myName);
var myName = "local";
alert(myName);
}
foo();
OK,再来一道:
var x = 0;
var test = function(){
x=1;
};
test();
console.log(x);
function test(){
x = 2;
}
test();
console.log(x);
最后一道阮老师出的:
function a(x,y){
y = function() { x = 2; };
return function(){
var x = 3;
y();
console.log(x);
}.apply(this, arguments);
}
a(1);
公布答案:
第一道:undefined
第二道:undefined "local"
第三道:1 1
第四道:3
解释答案前,先来看个概念吧:
变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)。js中,当定义了一个函数后,当前的作用域就会被保存下来,并且成为函数内部状态的一部分。
额外补充知识:
解释型语言,通过词法分析和语法分析得到语法分析树后,就可以开始解释执行了。这里是一个简单原始的关于解析过程的原理,仅作为参考,详细的解析过程(各种JS引擎还有不同)还需要更深一步的研究
JavaScript执行过程,如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:
步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)
步骤2. 做词法分析和语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
步骤3. 对【var】变量和【function】定义做“预解析“(永远不会报错的,因为只解析正确的声明)
步骤4. 执行代码段,有错则报错(比如变量未定义)
步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
步骤6. 结束
步骤3中的预解析时,将所有的变量的声明语句和函数声明提升到代码的头部,这叫做变量提升和函数声明提升。
第一道题,实际是这样的:
//var a;
console.log(a);//undefined
var a = 1;
第二道题:
myName = "global";
function foo() {
//var myName;
alert(myName);//undefined
var myName = "local";
alert(myName);//"local"
}
foo();
第三道题:
//var test
function test(){
x = 2;//函数声明也被提到前面来了。
}
var x = 0;
var test = function(){
x=1;//重写了函数声明~~~
};
test();
console.log(x);
test();
console.log(x);
第四道题涉及的是静态作用域。
function a(x,y){
y = function() { x = 2; };
return function(){
var x = 3;
y();
console.log(x);
}.apply(this, arguments);
}
a(1);
根据js的词法作用域,函数y的作用域在定义时确定。所以我们可知,y函数的作用域链包括其自身、函数a()、全局函数。即使将其放在匿名函数中执行,其作用域链也是不变的。所以,在匿名函数中执行时,其要更改的是a()函数中的x变量。然而我们可以看到,匿名函数中重新定义了x。此时的x是属于匿名函数的,所以y()的操作并不能对其生效。
如果我们把题改一改:
function a(x,y){
y = function() { x = 2; };
return function(){
x = 3; //去掉var
y();
console.log(x);
}.apply(this, arguments);
}
a(1);
此时输出为2.因为在匿名函数中并没有重新定义x,而是执行了修改x的值的操作。此时的x即为a()中的x——那么y()函数的操作自然也对其生效了。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.