Hello avilang


  • 首页

  • 归档

RequireJS 笔记

发表于 2016-10-13

概述

RequireJS 是一个工具库,能在网页中进行模块化编程,从而提高代码的性能和可维护性。它的模块管理遵守 AMD 规范。类似的库还有 sea.js 国人开发的一个库,遵守 CMD 规范。

个人推荐使用 RequireJS 库,从功能上说,两者是差不多的,但 RequireJS 有完善的文档和很好的打包工具可用,相比 sea.js 强多了,在这里吐槽下,感觉之前被 sea.js 坑了不少。

使用

加载 javascript 文件

RequireJS 以一个相对于 baseUrl 的地址来加载所有的代码。 页面顶层 <script> 标签含有一个特殊的属性 data-main 使用它来启动脚本加载过程,而 baseUrl 一般设置到与该属性相一致的目录。

1
<script data-main="scripts/main.js" src="scripts/require.js"></script>

baseUrl 亦可通过 RequireJS config 手动设置。如果没有显式指定 config 及 data-main ,则默认的 baseUrl 为包含 RequireJS 的那个 HTML 页面的所属目录。

若符合下述规则之一,则可避开 baseUrl + paths 配置

  • 以 “.js” 结束
  • 以 “/“ 开始
  • 包含 URL 协议, 如 “http:” or “https:”

一般来说,最好还是使用 baseUrl 及 paths 去设置。它会给你带来额外的灵活性,如便于脚本的重命名、重定位等。同时,为了避免凌乱的配置,最好不要使用多级嵌套的目录层次来组织代码。

模块的定义

1.Simple Name/Value Pairs

1
2
3
4
define({
color: "black",
size: "unisize"
});

2.Definition Functions

1
2
3
4
5
6
7
8
define(function () {
// ...

return {
color: "black",
size: "unisize"
}
});

3.Definition Functions with Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
define(["./cart", "./inventory"], function(cart, inventory) {
//return an object
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
}
}
}
);

依赖关系会以参数的形式注入到函数上,参数列表与依赖名称列表一一对应。

4.Define a Module as a Function

1
2
3
4
5
6
7
8
9
10
define(["my/cart", "my/inventory"],
function(cart, inventory) {
//return a function.
//It gets or sets the window title.
return function(title) {
return title ? (window.title = title) :
inventory.storeName + ' ' + cart.name;
}
}
);

5.Define a Module with Simplified CommonJS Wrapper

1
2
3
4
5
6
7
8
define(function(require, exports, module) {
var a = require('a'),
b = require('b');

//Return the module value
return function () {};
}
);

利用 Function.prototype.toString() 实现,但在一些设备如 PS3 及一些老的 Opera 手机浏览器中不起作用。

6.Define a Module with a Name

1
2
3
4
5
6
define("foo/title",
["my/cart", "my/inventory"],
function(cart, inventory) {
//...
}
);

可看到一些 define() 中包含了一个模块名称作为首个参数,这些常由打包工具生成。你也可以自己显式指定模块名称,但这使模块更不具备移植性——就是说若你将文件移动到其他目录下,你就得重命名。

Loader Plugins

有关 Loader Plugins 可跳转阅读 http://requirejs.org/docs/api.html#plugins 有关 requirejs 插件的编写可跳阅 http://requirejs.org/docs/plugins.html#api

CommonJS Compatibility

1
2
3
4
5
6
7
8
//BAD
var mod = require(someCondition ? 'a' : 'b');

//BAD
if (someCondition) {
var a = require('a');
} else {
var b = require('b');

以上的写法在 RequireJS 中是错误的用法,这样做的话, a 和 b 这两模块都会加载到页面中,这里可查看下文给出的列子。详细可点击阅读 CommonJS Compatibility
这种业务场景的解决方案 callback-require

机制

RequireJS 使用 head.appendChild() 将每一个依赖加载为一个 script 标签。
RequireJS 等待所有的依赖加载完毕,加载是异步的,计算出模块定义函数正确调用顺序,然后依次调用它们。

打包

RequireJS 提供一个基于 node.js 的命令行工具 r.js ,用来压缩多个 js 文件。它的主要作用是将多个模块文件压缩合并成一个脚本文件,以减少网页的HTTP请求数。例:

1
node r.js -o baseUrl=. name=main out=main-built.js

除了直接在命令行提供参数设置,也可以将参数写入一个文件,假定文件名为 build.js

1
node r.js -o build.js

有关更多的配置项可查看 example.build.js 和一个打包示例 requirejs/example-multipage

进一步优化,使用 r.js 优化后的代码可以使用 almond 来加载。

RequireJS 用法示例

例子中有对上文内容的一些补充,建议详细阅读代码。大家可点击 例子 打开看看,留意控制台的输出,注意这里为了演示就没有对 js 进行打包。

相关阅读

  • requirejs 官网
  • requirejs 中文官网
  • RequireJS 和 AMD 规范
  • requirejs的插件介绍与制作
  • RequireJS Optimizer 的使用和配置方法
  • SeaJS 与 RequireJS 最大的区别
  • AMD 的 CommonJS wrapping

javascript 正则表达式

发表于 2016-10-08

前言

之前已写《PHP 正则表达式笔记》一文介绍正则表达式,本篇仅是作知识点的补充,文章逻辑编排版较随意。

创建一个正则表达式

你可以通过下面两种方法创建一个正则表达式:

1、使用一个正则表达式字面量,如下所示

1
var re = /ab+c/;

2、调用 RegExp 对象的构造函数,如下所示

1
var re = new RegExp("ab+c");

正则表达式字面量在脚本加载后编译,使用构造函数,提供了对正则表达式运行时的编译。同一个正则表达式可能会被编译多次,所以可将其保存在一个变量中以供后使用。
当你知道正则表达式的模式会发生改变,或者你事先并不了解它的模式或者是从其他地方(比如用户的输入),得到的代码这时比较适合用构造函数的方式。

有时候需要对正则字符串进行转义处理,下面给出一示例:

1
2
3
4
function escapeRegExp(string){
//$&表示被匹配的字符串
return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$&");
}

摘自犀牛书:在 ECMAScript 3 规范中,用正则表达式创建的 RegExp 对象会共享一个实例,而在 ECMAScript 5中则是两个独立实例。

正则对象的属性和方法

javascript 语言里某种程度上可以认为 “一切皆对象”,那么 RegExp 对象也有其属性和方法。如 ignoreCase global lastIndex match() exec() 等属性和方法。

另外字符串的正则方法,如 replace search split match 方法,ES6 将这4个方法,语言内部全部调用 RegExp 的实例方法,从而做到所有与正则相关的方法,全都定义在 RegExp 对象上。

在这不具体介绍 javascript 正则对象的属性和方法的详细用法,读者可自行查阅。

u 修饰符

ES6 对正则表达式添加了 u 修饰符,含义为“Unicode模式”,用来正确处理大于 \uFFFF 的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。

可点击查阅 javascript 字符集 了解更多 javascript 中字符的知识,学学如何处理四个字节的 UTF-16 编码。

y 修饰符

ES6 还为正则表达式添加了 y 修饰符,叫做“粘连”(sticky)修饰符。

y 修饰符的一个应用,是从字符串提取 token(词元),y 修饰符确保了匹配之间不会有漏掉的字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
const TOKEN_G = /\s*(\+|[0-9]+)\s*/g;

tokenize(TOKEN_Y, '3 + 4')
// [ '3', '+', '4' ]
tokenize(TOKEN_G, '3 + 4')
// [ '3', '+', '4' ]

function tokenize(TOKEN_REGEX, str) {
let result = [];
let match;
while (match = TOKEN_REGEX.exec(str)) {
result.push(match[1]);
}
return result;
}

上面代码中,如果字符串里面没有非法字符,y修饰符与g修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。

1
2
3
4
tokenize(TOKEN_Y, '3x + 4')
// [ '3' ]
tokenize(TOKEN_G, '3x + 4')
// [ '3', '+', '4' ]

结语

以下给出简单列子,说明了一些正值表达式的用途。本文中许多有关 javascript 正则中的知识点并未介绍到,如断言、组匹配、反向引用等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var names = "Harry Trump ;Fred Barney; Helen Rigby ; Bill Abel ; Chris Hand ";

var output = ["---------- Original String\n", names + "\n"];

var pattern = /\s*;\s*/;

var nameList = names.split(pattern);


pattern = /(\w+)\s+(\w+)/;


var bySurnameList = [];


output.push("---------- After Split by Regular Expression");

var i, len;
for (i = 0, len = nameList.length; i < len; i++){
output.push(nameList[i]);
bySurnameList[i] = nameList[i].replace(pattern, "$2, $1");
}


output.push("---------- Names Reversed");
for (i = 0, len = bySurnameList.length; i < len; i++){
output.push(bySurnameList[i]);
}


bySurnameList.sort();
output.push("---------- Sorted");
for (i = 0, len = bySurnameList.length; i < len; i++){
output.push(bySurnameList[i]);
}

output.push("---------- End");

console.log(output.join("\n"));
1
2
3
4
// 问:请找出所有在 3 个连续相同字符前的相邻 3 个连续相同字符
var s = 'aaalllsss0tAAAnnn999';
var re = /(\w)\1{2}(?=(\w)\2{2})/g;
console.log(s.match(re)); // [ 'aaa', 'lll', 'AAA', 'nnn' ]

相关阅读

  • javascript - 中的正则表达式
  • 阮一峰 - 正则的扩展
  • 阮一峰 - 字符串
  • 正则表达式之:零宽断言
  • JS正则表达式一条龙讲解
  • 正则表达式前端使用手册

PHP 正则表达式笔记

发表于 2016-10-07

什么是正则表达式

在电脑上我们经常会使用(通配符)找出我们需要的文件,例如:*.doc ,这里的 * 代表匹配零个或多个字符。正则表达式也是用来进行文本匹配的工具,只不过它更加强悍。引用 PHP 手册里的一句话:正则表达式是一个从左到右匹配目标字符串的模式,大多数字符自身就代表一个匹配 它们自身的模式。

下面给出几个简单例子,使对正则表达式有个初步的理解。

1
hi  //匹配英文字符(忽略大小写) hi , HI , Hi , hI
1
\bhi\b  //匹配英文单词 hi  '\b'是正则里的一特殊字符(一种断言),表示单词边界
1
\bhi\b.*\bLucy\b  //匹配如:'hi my name is Lucy'  '.' 表示匹配除换行符以外的任意字符  '*' 是量词,表示重复零次或更多次
1
0\d{2}-\d{8}  //匹配如: 020-12345678  '\d' 匹配一个数字(0-9)    '{n}' 重复n次,如{2} {8}

上面例子中的 \b , . , * , \d , {2} 都有特殊含义,在下文会有说明。

PHP 中正则语法

1.简介

在 PHP 里支持两种正则分别是 POSIX 和 PCRE 。自 PHP 5.3.0起,POSIX 正则表达式扩展被废弃。所以下文讨论的都是基于 PCRE 模式。可点击查看有关与 POSIX 正则表达式的不同和与 perl 的不同之处。

2.分隔符

当使用 PCRE 函数 的时候,模式需要由分隔符闭合包裹。分隔符可以使任意非字母数字、非反斜线、非空白字符。经常使用的分隔符是正斜线 / 、hash符号 # 以及取反符号 ~ 。下面的例子都是使用合法分隔符的模式。

1
2
3
4
/foo bar/
#^[^0-9]$#
+php+
%[a-zA-Z0-9_-]%

如果分隔符需要在模式内进行匹配,它必须使用反斜线进行转义。如果分隔符经常在模式内出现,一个更好的选择就是是用其他分隔符来提高可读性。例:

1
2
/http:\/\//
#http://#

3.元字符

正则表达式的威力源于它可以在模式中拥有选择和重复的能力。一些字符被赋予特殊的含义,使其不再单纯的代表自己,模式中的这种有特殊涵义的编码字符 称为元字符。
共有两种不同的元字符:一种是可以在模式中方括号外任何地方使用的,另外一种是需要在方括号内使用的。

在方括号外使用的元字符如下:

代码 说明
/ 一般用于转义字符
^ 断言目标的开始位置(或在多行模式下是行首)
$ 断言目标的结束位置(或在多行模式下是行尾)
. 匹配除换行符外的任何字符(默认)
[ 开始字符类定义
] 结束字符类定义
| 开始一个可选分支
( 子组的开始标记
) 子组的结束标记
? a:作为量词,表示 0 次或 1 次匹配。b:位于量词后面用于改变量词的贪婪特性。
* 量词,0 次或多次匹配
+ 量词,1 次或多次匹配
{ 自定义量词开始标记
} 自定义量词结束标记

模式中方括号内的部分称为“字符类”。 在一个字符类中仅有以下可用元字符:

代码 说明
\ 转义字符
^ 仅在作为第一个字符(方括号内)时,表明字符类取反
- 标记字符范围

示例:

  • \ba\w*\b 匹配以字母 a 开头的单词,先是某个单词开始处 \b ,然后是字母 a ,然后是任意数量的任意单词字符(单词字符指的是任意字母、数字、下划线) \w* ,最后是单词结束处 \b 。
  • \d+ 匹配1个或更多连续的数字。
  • ^\d{5,12}$ 匹配为5位到12位数字,因为使用了 ^ 和 $ ,所以输入的整个字符串都要用来和 \d{5,12} 来匹配,也就是说整个输入必须是5到12个数字。

4.转义序列(反斜线)

反斜线 \ 有四种用法,详细可点击 转义序列(反斜线)

【1】作为转义字符,比如,如果你希望匹配一个 * 字符,就需要在模式中写为 \* 。这适用于一个字符在不进行转义会有特殊含义的情况下。 但是,对于非数字字母的字符,总是在需要其进行原文匹配的时候在它前面增加一个反斜线,来声明它代表自己,这是安全的。如果要匹配一个反斜线,那么在模式中使用 \\ 。
反斜线在单引号字符串和双引号字符串中都有特殊含义,因此要匹配一个反斜线, 模式中必须写为 \\\\ 。其中的原因:首先它作为字符串,反斜线会进行转义。最后正则表达式引擎也认为反斜线是转义。因此,需要 4 个反斜线才可以匹配一个反斜线。

【2】提供了一种对非打印字符进行可见编码的控制手段

【3】用来描述特定的字符类

代码 说明
\d 任意十进制数字
\D 任意非十进制数字
\h 任意水平空白字符(since PHP 5.2.4)
\H 任意非水平空白字符(since PHP 5.2.4)
\s 任意空白字符
\S 任意非空白字符
\v 任意垂直空白字符(since PHP 5.2.4)
\V 任意非垂直空白字符(since PHP 5.2.4)
\w 任意单词字符,单词字符指的是任意字母、数字、下划线。
\W 任意非单词字符

【4】一些简单的断言。一个断言指定一个必须在特定位置匹配的条件,它们不会从目标字符串中消耗任何字符。反斜线断言包括:

  • \b 单词边界
  • \B 非单词边界
  • \A 目标的开始位置(独立于多行模式)
  • \Z 目标的结束位置或结束处的换行符(独立于多行模式)
  • \z 目标的结束位置(独立于多行模式)
  • \G 在目标中首次匹配位置

5.重复/量词

代码 说明
* 重复零次或更多次,等价于 {0,}
+ 重复一次或更多次,等价于 {1,}
? 重复零次或一次,等价于 {0,1}
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

默认情况下,量词都是”贪婪”的,也就是说,它们会在不导致模式匹配失败的前提下,尽可能多的匹配字符(直到最大允许的匹配次数)。然而,如果一个量词紧跟着一个 ? 标记,它就会成为懒惰(非贪婪)模式, 它不再尽可能多的匹配,而是尽可能少的匹配。
下面直接看示例,理解“贪婪”和“非贪婪”模式是怎么回事。

1
2
3
4
5
对于字符串 "aa<div>test1</div>bb<div>test2</div>cc"

正则表达式 "<div>.*</div>" 匹配结果 "<div>test1</div>bb<div>test2</div>"

正则表达式 "<div>.*?</div>" 匹配结果 "<div>test1</div>"

关于更多“贪婪”和“非贪婪”模式的介绍可查阅 http://php.net/manual/zh/regexp.reference.repetition.php

6.字符类(方括号)

PHP手册中的描述:

  • 左方括号开始一个字符类的描述,并以方中括号结束。单独的一个右方括号没有特殊含义。如果一个右方括号需要作为一个字符类中的成员,那么可以将它写在字符类的首字符处(如果使用了 ^ 取反,那么是第二个)或者使用转义符。

  • 一个字符类在目标字符串中匹配一个单独的字符;该字符必须是字符类中定义的字符集合的其中一个, 除非使用了 ^ 对字符类取反。如果^需要作为一个字符类的成员,确保它不是该字符类的首字符,或者对其进行转义即可。

示例:

1
2
3
4
5
[aeiou]    //匹配所有的小写元音字母

[^aeiou] //匹配所有非元音字母的字符

[.?!] //匹配标点符号(.或?或!)

注意:^ 只是一个通过枚举指定那些不存在字符类之中的字符的便利符号。而不是断言, 它仍然会从目标字符串中消耗一个字符,并且如果当前匹配点在目标字符串末尾, 匹配将会失败。

轻松地指定一个字符范围,范围操作以 ASCII 整理排序。它们可以用于为字符指定数值,比如 [\000-\037]

1
2
[0-9]    //代表的含意与 '\d' 就是完全一致的
[a-z0-9A-Z_] //完全等同于 '\w' 如果只考虑英文的话

下面是一个更复杂的表达式 \(?0\d{2}[) -]?\d{8}
这个表达式可以匹配几种格式的电话号码,像 (010)88886666,或 022-22334455 ,或 02912345678 等。
简单分析:首先是一个转义字符 \( ,它能出现 0 次或 1 次 ? ,然后是一个数字 0 ,后面跟着 2 个数字 \d{2} ,然后是 ) 或 - 或 “空格” 中的一个,它出现 0 次或 1 次,最后是 8 个数字 \d{8} 。

7.分支 ( | )

竖线字符用于分离模式中的可选路径。比如模式 gilbert|Sullivan 匹配 ”gilbert” 或者 ”sullivan”。竖线可以在模式中出现任意多个,并且允许有空的可选路径(匹配空字符串)。匹配的处理从左到右尝试每一个可选路径,并且使用第一个成功匹配的。如果可选路径在子组(下面定义)中,则”成功匹配”表示同时匹配了子模式中的分支以及主模式中的其他部分。

回看上文里的一个例子 \(?0\d{2}[) -]?\d{8} 这个正则也能匹配 010)12345678 或 (022-87654321 这样的 “不正确” 的格式。其实我们可以利用分支就能解决这个问题,如下:

\({1}0\d{2}\){1}[- ]?\d{8}|0\d{2}[- ]?\d{8} 这个表达式匹配 3 位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔。

使用分枝条件时,要注意各个条件的顺序

8.内部选项设置

正则表达式在不同的模式修饰符下匹配出的结果有可能不相同。它的语法是 :(?修饰符)

比如,(?im) 设置表明多行大小写不敏感匹配。同样可以用它来取消这些设置,比如 (?im-sx) 设置了 “PCRE_CASELESS”,”PCRE_MULTILINE”,但是同时取消了 “PCRE_DOTALL” 和 “PCRE_EXTENDED”。如果一个字母即出现在 - 之前, 也出现在 - 之后,这个选项被取消设置。

下面紧例举简单的示例,想要了解更多可点击 内部选项设置 和 模式修饰符

示例:/ab(?i)c/ 仅仅匹配 ”abc” 和 ”abC”

9.子组(子模式)

子组通过圆括号分隔界定,并且它们可以嵌套。

示例:

1
2
3
4
字符串:"the red king"
正则表达式:((red|white) (king|queen))
匹配结果:array("red king", "red king", "red", "king")
描述:其中第 0 个元素是整个模式匹配的结果,后面的三个元素依次为三个子组匹配的结果。 它们的下标分别为 1, 2, 3。

经常我们会有一种需求需要使用子组进行分组,但又不需要(单独的)捕获它们。在子组定义的左括号后面紧跟字符串 ?: 会使得该子组不被单独捕获,并且不会对其后子组序号的计算产生影响。例如:

1
2
3
字符串:"the red king"
正则表达式:((?:red|white) (king|queen))
匹配结果:array("red king", "red king", "king")

为了方便简写,如果需要在非捕获子组开始位置设置选项, 选项字母可以位于 ? 和 : 之间,比如:

1
2
(?i:saturday|sunday)
(?:(?i)saturday|sunday)

上面两种写法实际上是相同的模式。因为可选分支会从左到右尝试每个分支,并且选项没有在子模式结束前被重置,并且由于选项的设置会穿透对后面的其他分支产生影响,因此, 上面的模式都会匹配 ”SUNDAY” 以及 ”Saturday”。

再看一个匹配 IP 地址的正则 ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
相关文章 IP地址的正则表达式

结语

上文中涉及 PHP 正则表达式中常用的语法,有的语法没细说和涉及到的,如:模式修饰符、后向引用、断言、递归模式,等。你可以通过 PHP 手册查看这些内容。

提示:一般而言,对于同样的功能,正则表达式函数运行效率要低于字符串函数。如果应用程序较简单,那么就用字符串表达式。但是,对于可以通过单个正则表达式执行的任务来说,如果使用多个字符串函数,则是不对的。—- 摘自《PHP 和 MySQL Web 开发》一书。

相关阅读

  • PHP - 正则表达式(兼容 Perl)
  • 正则表达式
  • 正则表达式30分钟入门教程
  • 揭开正则表达式的神秘面纱

HTTP 协议

发表于 2016-10-06

HTTP 协议对应 Web 开发者来说都必须要了解的,无论技术背景或首选编程语言是什么,”请求-响应” 对话是驱动 Web 上通信的基础。

HTTP 概述

HTTP 协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写。通常,由 HTTP 客户端发起一个请求,创建一个到服务器指定端口(默认是 80 端口)的 TCP 连接。HTTP 服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如 “HTTP/1.1 200 OK” ,以及返回的内容,如请求的文件、错误消息、或者其它信息。

平时我们在浏览器上输入网址,按回车就能浏览网页内容。这过程中主要分为两个部分 “请求” (浏览器向服务器发送的消息) 和 “响应” (服务器将处理结果返回给浏览器)。

HTTP 请求

HTTP 请求分为四格部分:请求行, 请求头, 空行, 请求主体,下面直接给出一个示例:

1
2
GET /hello.txt HTTP/1.1
Host: www.example.com

上示例中第一行为请求行,第二行为请求头,末尾有一个空行(即空行,必须要有)。

  • 请求行包含:请求方法, 请求资源, 协议版本
    常见的请求方法有 GET POST HEAD 等
    协议版本有 0.9, HTTP/1.0, HTTP/1.1

  • 在 HTTP/1.1协议中,所有的请求头,除 Host 外,都是可选的。点击 HTTP头字段 了解更多头字段。下面给出常用的请求头:

1
2
3
4
5
6
7
host:请求的域名,必须要有
accept-encoding:可以接受的编码类型(不是字符编码,指的是文件格式)
connection:连接方式,1.0版本close,1.1版本keep-alive
accept-language:可以接收的语言类型
cookie:之前由服务器通过 Set-Cookie 发送的一个超文本传输协议 Cookie
user-agent:客户端信息
content-length:表示的请求体的长度,单位是字节

HTTP 响应

HTTP 响应分为四个部分:状态行, 响应头, 空行, 消息体(响应内容) 示例:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

Hello World! My payload includes a trailing CRLF.

上示例中第一行为状态行,第二行至第九行处为响应头,第十行为空行,最后一行为消息体。

  • 状态行包含:协议版本, 状态码, 状态描述。其中有关状态码更多的介绍可移步阅读 HTTP状态码

  • 响应头更多信息可点击上文中的 “HTTP头字段” 链接查看。下面给出常见的响应头:

1
2
3
4
5
6
7
server:服务器信息
date:服务器响应的时间
last-modified:当前请求文件最后被修改的时间
content-length:响应体的数据长度,单位是字节
content-type:响应体的类型
location:重定向
cache-control:缓存控制

其中一个重要的 HTTP 响应头 Content-Type 服务器上的每个资源都可以以不同的格式返回给客户端,如 HTML、XML 或 JSON 等;通过在 Content-Type 里设置如 text/html 这样的互联网媒体类型,可以告知客户端,服务器给出的响应格式是什么。常用的媒体类型可移步阅读 互联网媒体类型

拓展知识

HTTP 长连接说明,还有文中提到的“TCP\IP三次握手连接”
其中还有长轮询、服务器发送事件、WebSocket等知识点都与 HTTP 相关,在这就不拓展开了,读者可自行谷歌

相关阅读

  • 超文本传输协议
  • WebSocket
  • Comet (web技术)
  • 推送技术
  • HTTP 教程
  • 写给前端的http详解
  • HTTP协议头部与Keep-Alive模式详解
  • HTTP 缓存
  • Web缓存机制系列
  • HTTP持久连接

JavaScript 秘密花园

发表于 2016-10-06

前言

本文大部分内容转载自 JavaScript 秘密花园 ,你也可以移步到 GitHub 上阅读。然而在文章末尾,给出几个有关作用域方的例子。

对象

对象使用和属性

JavaScript 中所有变量都可以当作对象使用,除了两个例外 null 和 undefined

1
2
3
4
5
6
false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'

function Foo(){}
Foo.bar = 1;
Foo.bar; // 1

一个常见的误解是数字的字面值(literal)不能当作对象使用。这是因为 JavaScript 解析器的一个错误, 它试图将点操作符解析为浮点数字面值的一部分。

1
2.toString(); // 出错:SyntaxError

有很多变通方法可以让数字的字面值看起来像对象。

1
2
3
2..toString(); // 第二个点号可以正常解析
2 .toString(); // 注意点号前面的空格
(2).toString(); // 2先被计算

对象作为数据类型

JavaScript 的对象可以作为哈希表使用,主要用来保存命名的键与值的对应关系。

使用对象的字面语法 {} 可以创建一个简单对象。这个新创建的对象从 Object.prototype 继承,下面没有任何自定义属性。

1
2
3
4
var foo = {}; // 一个空对象

// 一个新对象,拥有一个值为12的自定义属性'test'
var bar = {test: 12};

访问属性

有两种方式来访问对象的属性,点操作符或者中括号操作符。

1
2
3
4
5
6
7
8
9
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten

var get = 'name';
foo[get]; // kitten

foo.1234; // SyntaxError
foo['1234']; // works

两种语法是等价的,但是中括号操作符在下面两种情况下依然有效

  • 动态设置属性
  • 属性名不是一个有效的变量名(比如属性名中包含空格,或者属性名是 JS 的关键词)

删除属性

删除属性的唯一方法是使用 delete 操作符:设置属性为 undefined 或者 null 并不能真正的删除属性,而仅仅是移除了属性和值的关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
bar: 1,
foo: 2,
baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, '' + obj[i]);
}
}

上面的输出结果有 bar undefined 和 foo null - 只有 baz 被真正的删除了,所以从输出结果中消失。

属性名的语法

1
2
3
4
var test = {
'case': 'I am a keyword so I must be notated as a string',
delete: 'I am a keyword too so me' // 出错:SyntaxError
};

对象的属性名可以使用字符串或者普通字符声明。但是由于 JavaScript 解析器的另一个错误设计,上面的第二种声明方式在 ECMAScript 5 之前会抛出 SyntaxError 的错误。

这个错误的原因是 delete 是 JavaScript 语言的一个关键词;因此为了在更低版本的 JavaScript 引擎下也能正常运行,必须使用字符串字面值声明方式。

原型

JavaScript 不包含传统的类继承模型,而是使用 prototype 原型模型。

虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大。 实现传统的类继承模型是很简单,但是实现 JavaScript 中的原型继承则要困难的多。(It is for example fairly trivial to build a classic model on top of it, while the other way around is a far more difficult task.)

由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的。

第一个不同之处在于 JavaScript 使用“原型链”的继承方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};

function Bar() {}

// 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;

var test = new Bar() // 创建Bar的一个新实例

// 原型链
test [Bar的实例]
Bar.prototype [Foo的实例]
{ foo: 'Hello World' }
Foo.prototype
{method: ...};
Object.prototype
{toString: ... /* etc. */};

上面的例子中,test 对象从 Bar.prototype 和 Foo.prototype 继承下来:因此,它能访问 Foo 的原型方法 method 。同时,它也能够访问那个定义在原型上的 Foo 实例属性 value 。需要注意的是 new Bar() 不会创造出一个新的 Foo 实例,而是 重复使用它原型上的那个实例:因此,所有的 Bar 实例都会共享相同的 value 属性。

注意:简单的使用 Bar.prototype = Foo.prototype 将会导致两个对象共享相同的原型。 因此,改变任意一个对象的原型都会影响到另一个对象的原型,在大多数情况下这不是希望的结果。

属性查找

当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。到查找到达原型链的顶部,也就是 Object.prototype 但是仍然没有找到指定的属性,就会返回 undefined 。

原型属性

当原型属性用来创建原型链时,可以把任何类型的值赋给它(prototype)。 然而将原子类型赋给 prototype 的操作将会被忽略。

1
2
function Foo() {}
Foo.prototype = 1; // 无效

而将对象赋值给 prototype,正如上面的例子所示,将会动态的创建原型链。

性能

如果一个属性在原型链的上端,则对于查找时间将带来不利影响。特别的,试图获取一个不存在的属性将会遍历整个原型链。

并且,当使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问。

扩展内置类型的原型

一个错误特性被经常使用,那就是扩展 Object.prototype 或者其他内置类型的原型对象。

这种技术被称之为 monkey patching 并且会破坏封装。虽然它被广泛的应用到一些 JavaScript 类库中比如 Prototype , 但是我仍然不认为为内置类型添加一些非标准的函数是个好主意。

扩展内置类型的唯一理由是为了和新的 JavaScript 保持一致,比如 Array.forEach

总结

在写复杂的 JavaScript 应用之前,充分理解原型链继承的工作方式是每个 JavaScript 程序员必修的功课。
要提防原型链过长带来的性能问题,并知道如何通过缩短原型链来提高性能。 更进一步,绝对不要扩展内置类型的原型,除非是为了和新的 JavaScript 引擎兼容。

hasOwnProperty 函数

为了判断一个对象是否包含自定义属性而不是原型链上的属性,我们需要使用继承自 Object.prototype 的 hasOwnProperty 方法。

hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

1
2
3
4
5
6
7
8
9
// 修改Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

只有 hasOwnProperty 可以给出正确和期望的结果,这在遍历对象的属性时会很有用。 没有其它方法可以用来排除原型链上的属性,而不是定义在对象自身上的属性。

注意: 通过判断一个属性是否 undefined 是不够的。因为一个属性可能确实存在,只不过它的值被设置为 undefined。

hasOwnProperty 作为属性

JavaScript 不会保护 hasOwnProperty 被非法占用,因此如果一个对象碰巧存在这个属性,就需要使用外部的 hasOwnProperty 函数来获取正确的结果。

1
2
3
4
5
6
7
8
9
10
11
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 总是返回 false

// 使用其它对象的 hasOwnProperty,并将其上下文设置为foo
({}).hasOwnProperty.call(foo, 'bar'); // true

结论

当检查对象上某个属性是否存在时,hasOwnProperty 是唯一可用的方法。同时在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法,这将会避免原型对象扩展带来的干扰。

for in 循环

和 in 操作符一样,for in 循环同样在查找对象属性时遍历原型链上的所有属性。

1
2
3
4
5
6
7
// 修改 Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
console.log(i); // 输出两个属性:bar 和 moo
}

注意: for in 循环不会遍历那些 enumerable 设置为 false 的属性:比如数组的 length 属性。

由于不可能改变 for in 自身的行为,因此有必要过滤出那些不希望出现在循环体中的属性,这可以通过 Object.prototype 原型上的 hasOwnProperty 函数来完成。

使用 hasOwnProperty 过滤

1
2
3
4
5
6
// foo 变量是上例中的
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}

这个版本的代码是唯一正确的写法。由于我们使用了 hasOwnProperty,所以这次只输出 moo。 如果不使用 hasOwnProperty,则这段代码在原生对象原型(比如 Object.prototype)被扩展时可能会出错。

一个广泛使用的类库 Prototype 就扩展了原生的 JavaScript 对象。 因此,当这个类库被包含在页面中时,不使用 hasOwnProperty 过滤的 for in 循环难免会出问题。

总结

推荐总是使用 hasOwnProperty 不要对代码运行的环境做任何假设,不要假设原生对象是否已经被扩展了。

函数

函数声明与表达式

函数是 JavaScript 中的一等对象,这意味着可以把函数像其它值一样传递。一个常见的用法是把匿名函数作为回调函数传递到异步函数中。

函数声明

1
function foo() {}

上面的方法会在执行前被解析(hoisted),因此它存在于当前上下文的任意一个地方,即使在函数定义体的上面被调用也是对的。

1
2
foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}

函数赋值表达式

1
var foo = function() {};

这个例子把一个匿名的函数赋值给变量 foo

1
2
3
foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

由于 var 定义了一个声明语句,对变量 foo 的解析是在代码运行之前,因此 foo 变量在代码运行时已经被定义过了。

但是由于赋值语句只在运行时执行,因此在相应代码执行之前,foo 的值缺省为 undefined

命名函数的赋值表达式

另外一个特殊的情况是将命名函数赋值给一个变量

1
2
3
4
var foo = function bar() {
bar(); // 正常运行
}
bar(); // 出错:ReferenceError

bar 函数声明外是不可见的,这是因为我们已经把函数赋值给了 foo 然而在 bar 内部依然可见。这是由于 JavaScript 的命名处理所致,函数名在函数内总是可见的。

注意:在 IE8 及 IE8 以下版本浏览器 bar 在外部也是可见的,是因为浏览器对命名函数赋值表达式进行了错误的解析,解析成两个函数 foo 和 bar

this 的工作原理

JavaScript 有一套完全不同于其它语言的对 this 的处理机制。在五种不同的情况下,this 指向的各不相同。

1. 全局范围内

1
this;

当在全部范围内使用 this ,它将会指向全局对象。浏览器中运行的 JavaScript 脚本,这个全局对象是 window

2. 函数调用

1
foo();

这里 this 也会指向全局对象。

ES5 注意:在严格模式下(strict mode),不存在全局变量。这种情况下 this 将会是 undefined

3. 方法调用

1
test.foo();

这个例子中,this 指向 test 对象。

4. 调用构造函数

1
new foo();

如果函数倾向于和 new 关键词一块使用,则我们称这个函数是构造函数。在函数内部,this 指向新创建的对象。

5. 显式的设置 this

1
2
3
4
5
function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // 数组将会被扩展,如下所示
foo.call(bar, 1, 2, 3); // 传递到foo的参数是:a = 1, b = 2, c = 3

当使用 Function.prototype 上的 call 或者 apply 方法时,函数内的 this 将会被显式设置为函数调用的第一个参数。

因此函数调用的规则在上例中已经不适用了,在 foo 函数内 this 被设置成了 bar

注意:在对象的字面声明语法中,this 不能用来指向对象本身。因此 var obj = {me: this} 中的 me 不会指向 obj 因为 this 只可能出现在上述的五种情况中。这个例子中,如果是在浏览器中运行,obj.me 等于 window 对象。

常见误解

尽管大部分的情况都说的过去,不过第一个规则(译者注:这里指的应该是第二个规则,也就是直接调用函数时,this 指向全局对象)被认为是 JavaScript 语言另一个错误设计的地方,因为它从来就没有实际的用途。

1
2
3
4
5
6
Foo.method = function() {
function test() {
// this 将会被设置为全局对象(译者注:浏览器环境中也就是 window 对象)
}
test();
}

一个常见的误解是 test 中的 this 将会指向 Foo 对象,实际上不是这样子的。

为了在 test 中获取对 Foo 对象的引用,我们需要在 method 方法内部创建一个局部变量指向 Foo 对象。

1
2
3
4
5
6
7
Foo.method = function() {
var that = this;
function test() {
// 使用 that 来指向 Foo 对象
}
test();
}

that 只是我们随意起的名字,不过这个名字被广泛的用来指向外部的 this 对象。在闭包一节,我们可以看到 that 可以作为参数传递。

方法的赋值表达式

另一个看起来奇怪的地方是函数别名,也就是将一个方法赋值给一个变量。

1
2
var test = someObject.methodTest;
test();

上例中,test 就像一个普通的函数被调用。因此,函数内的 this 将不再被指向到 someObject 对象。

虽然 this 的晚绑定特性似乎并不友好,但这确实是基于原型继承赖以生存的土壤。

1
2
3
4
5
6
7
function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

当 method 被调用时,this 将会指向 Bar 的实例对象。

闭包和引用

闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。因为 函数是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。

模拟私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},

get: function() {
return count;
}
}
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

这里 Counter 函数返回两个闭包,函数 increment 和函数 get 这两个函数都维持着对外部作用域 Counter 的引用,因此总可以访问此作用域内定义的变量 count

为什么不可以在外部访问私有变量

因为 JavaScript 中不可以对作用域进行引用或赋值,因此没有办法在外部访问 count 变量。唯一的途径就是通过那两个闭包。

1
2
3
4
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};

上面的代码不会改变定义在 Counter 作用域中的 count 变量的值,因为 foo.hack 没有定义在那个作用域内。它将会创建或者覆盖全局变量 count

循环中的闭包

一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号。

1
2
3
4
5
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}

上面的代码不会输出数字 0 到 9 ,而是会输出数字 10 十次。

当 console.log 被调用的时候,匿名函数保持对外部变量 i 的引用,此时 for 循环已经结束,i 的值被修改成了 10

为了得到想要的结果,需要在每次循环中创建变量 i 的拷贝。

避免引用错误

为了正确的获得循环序号,最好使用匿名包装器(译者注:其实就是我们通常说的自执行匿名函数)。

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}

外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。

当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

有另一个方法完成同样的工作,那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}

arguments 对象

JavaScript 中每个函数内都能访问一个特别变量 arguments ,这个变量维护着所有传递到这个函数中的参数列表。

注意:由于 arguments 已经被定义为函数内的一个变量。因此通过 var 关键字定义 arguments 或者将 arguments 声明为一个形式参数,都将导致原生的 arguments 不会被创建。

arguments 变量不是一个数组(Array)。尽管在语法上它有数组相关的属性 length ,但它不从 Array.prototype 继承,实际上它是一个对象(Object)。

因此,无法对 arguments 变量使用标准的数组方法,比如 push, pop 或者 slice 。虽然使用 for 循环遍历也是可以的,但是为了更好的使用数组方法,最好把它转化为一个真正的数组。

转化为数组

下面的代码将会创建一个新的数组,包含所有 arguments 对象中的元素。

1
Array.prototype.slice.call(arguments);

这个转化比较慢,在性能不好的代码中不推荐这种做法。

传递参数

下面是将参数从一个函数传递到另一个函数的推荐做法。

1
2
3
4
5
6
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// 干活
}

另一个技巧是同时使用 call 和 apply ,创建一个快速的解绑定包装器。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo() {}

Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};

// 创建一个解绑定的 "method"
// 输入参数为: this, arg1, arg2...argN
Foo.method = function() {

// 结果: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};

译者注:上面的 Foo.method 函数和下面代码的效果是一样的:

1
2
3
4
Foo.method = function() {
var args = Array.prototype.slice.call(arguments);
Foo.prototype.method.apply(args[0], args.slice(1));
};

自动更新

arguments 对象为其内部属性以及函数形式参数创建 getter 和 setter 方法。

因此,改变形参的值会影响到 arguments 对象的值,反之亦然。

1
2
3
4
5
6
7
8
9
10
11
12
function foo(a, b, c) {
arguments[0] = 2;
a; // 2

b = 4;
arguments[1]; // 4

var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);

性能真相

不管它是否有被使用,arguments 对象总会被创建,除了两个特殊情况作为“局部变量声明”和作为“形式参数”。

arguments 的 getters 和 setters 方法总会被创建:因此使用 arguments 对性能不会有什么影响。除非是需要对 arguments 对象的属性进行多次访问。

ES5 提示: 这些 getters 和 setters 在严格模式下(strict mode)不会被创建。

译者注:在 MDC 中对 strict mode 模式下 arguments 的描述有助于我们的理解,请看下面代码:

1
2
3
4
5
6
7
8
9
// 阐述在 ES5 的严格模式下 `arguments` 的特性
function f(a) {
"use strict";
a = 42;
return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42); //true
console.assert(pair[1] === 17); //true

然而,的确有一种情况会显著的影响现代 JavaScript 引擎的性能。这就是使用 arguments.callee

1
2
3
4
5
6
7
8
9
10
function foo() {
arguments.callee; // do something with this function object
arguments.callee.caller; // and the calling function object
}

function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // Would normally be inlined...
}
}

上面代码中,foo 不再是一个单纯的内联函数 inlining(译者注:这里指的是解析器可以做内联处理),因为它需要知道它自己和它的调用者。这不仅抵消了内联函数带来的性能提升,而且破坏了封装,因此现在函数可能要依赖于特定的上下文。

因此强烈建议大家不要使用 arguments.callee 和它的属性。

ES5 提示:在严格模式下,arguments.callee 会报错 TypeError ,因为它已经被废除了。

构造函数

JavaScript 中的构造函数和其它语言中的构造函数是不同的。通过 new 关键字方式调用的函数都被认为是构造函数。

在构造函数内部,也就是被调用的函数内 this 指向新创建的对象 Object 这个新创建的对象的 prototype 被指向到构造函数的 prototype

如果被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象,也就是新创建的对象。

1
2
3
4
5
6
7
8
9
function Foo() {
this.bla = 1;
}

Foo.prototype.test = function() {
console.log(this.bla);
};

var test = new Foo();

上面代码把 Foo 作为构造函数调用,并设置新创建对象的 prototype 为 Foo.prototype

显式的 return 表达式将会影响返回结果,但仅限于返回的是一个“对象”。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Bar() {
return 2;
}
new Bar(); // 返回新创建的对象

function Test() {
this.value = 2;

return {
foo: 1
};
}
new Test(); // 返回的对象

译者注:new Bar() 返回的是新创建的对象,而不是数字的字面值 2 。因此 new Bar().constructor === Bar ,但是如果返回的是数字对象,结果就不同了,如下所示

1
2
3
4
function Bar() {
return new Number(2);
}
new Bar().constructor === Number

译者注:这里得到的 new Test() 是函数返回的对象,而不是通过 new 关键字新创建的对象,因此:

1
2
(new Test()).value === undefined
(new Test()).foo === 1

如果 new 被遗漏了,则函数不会返回新创建的对象。

1
2
3
4
function Foo() {
this.bla = 1; // 获取设置全局参数
}
Foo(); // undefined

虽然上例在有些情况下也能正常运行,但是由于 JavaScript 中 this 的工作原理,这里的 this 指向全局对象。

工厂模式

为了不使用 new 关键字,构造函数必须显式的返回一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};

new Bar();
Bar();

上面两种对 Bar 函数的调用返回的值完全相同,一个新创建的拥有 method 属性的对象被返回,其实这里创建了一个闭包。

还需要注意,new Bar() 并不会改变返回对象的原型(译者注:也就是返回对象的原型不会指向 Bar.prototype )。因为构造函数的原型会被指向到刚刚创建的新对象,而这里的 Bar 没有把这个新对象返回(译者注:而是返回了一个包含 method 属性的自定义对象)。

在上面的例子中,使用或者不使用 new 关键字没有功能性的区别。

译者注:上面两种方式创建的对象不能访问 Bar 原型链上的属性,如下所示:

1
2
3
4
5
6
7
var bar1 = new Bar();
typeof(bar1.method); // "function"
typeof(bar1.foo); // "undefined"

var bar2 = Bar();
typeof(bar2.method); // "function"
typeof(bar2.foo); // "undefined"

通过工厂模式创建新对象

我们常听到的一条忠告是不要使用 new 关键字来调用函数,因为如果忘记使用它就会导致错误。

为了创建新对象,我们可以创建一个工厂方法,并且在方法内构造一个新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Foo() {
var obj = {};
obj.value = 'blub';

var private = 2;
obj.someMethod = function(value) {
this.value = value;
}

obj.getPrivate = function() {
return private;
}
return obj;
}

虽然上面的方式比起 new 的调用方式不容易出错,并且可以充分利用私有变量带来的便利,但是随之而来的是一些不好的地方。

  1. 会占用更多的内存,因为新创建的对象不能共享原型上的方法。
  2. 为了实现继承,工厂方法需要从另外一个对象拷贝所有属性,或者把一个对象作为新创建对象的原型。
  3. 放弃原型链仅仅是因为防止遗漏 new 带来的问题,这似乎和语言本身的思想相违背。

总结

虽然遗漏 new 关键字可能会导致问题,但这并不是放弃使用原型链的借口。最终使用哪种方式取决于应用程序的需求,选择一种代码书写风格并坚持下去才是最重要的。

作用域与命名空间

尽管 JavaScript 支持一对花括号创建的代码段,但是并不支持块级作用域:而仅仅支持函数作用域。

1
2
3
4
5
6
function test() { // 一个作用域
for(var i = 0; i < 10; i++) { // 不是一个作用域
// count
}
console.log(i); // 10
}

注意:如果不是在赋值语句中,而是在 return 表达式或者函数参数中,{...} 将会作为代码段解析,而不是作为对象的字面语法解析。如果考虑到自动分号插入,这可能会导致一些不易察觉的错误。

译者注:如果 return 对象的左括号和 return 不在一行上就会出错。

1
2
3
4
5
6
// 译者注:下面输出 undefined
function add(a, b) {
return
a + b;
}
console.log(add(1, 2));

JavaScript 中没有显式的命名空间定义,这就意味着所有对象都定义在一个全局共享的命名空间下面。

每次引用一个变量,JavaScript 会向上遍历整个作用域直到找到这个变量为止。如果到达全局作用域但是这个变量仍未找到,则会抛出 ReferenceError 异常。

隐式的全局变量

1
2
3
4
5
// 脚本 A
foo = '42';

// 脚本 B
var foo = '42'

上面两段脚本效果不同。脚本 A 在全局作用域内定义了变量 foo,而脚本 B 在当前作用域内定义变量 foo。

再次强调,上面的效果完全不同,不使用 var 声明变量将会导致隐式的全局变量产生。

1
2
3
4
5
6
7
8
// 全局作用域
var foo = 42;
function test() {
// 局部作用域
foo = 21;
}
test();
foo; // 21

在函数 test 内不使用 var 关键字声明 foo 变量将会覆盖外部的同名变量。起初这看起来并不是大问题,但是当有成千上万行代码时,不使用 var 声明变量将会带来难以跟踪的 BUG

1
2
3
4
5
6
7
8
9
10
11
12
// 全局作用域
var items = [/* 数组 */];
for(var i = 0; i < 10; i++) {
subLoop();
}

function subLoop() {
// subLoop 函数作用域
for(i = 0; i < 10; i++) { // 没有使用 var 声明变量
// 干活
}
}

外部循环在第一次调用 subLoop 之后就会终止,因为 subLoop 覆盖了全局变量 i 在第二个 for 循环中使用 var 声明变量可以避免这种错误。 声明变量时绝对不要遗漏 var 关键字,除非这就是期望的影响外部作用域的行为。

局部变量

JavaScript 中局部变量只可能通过两种方式声明,一个是作为函数参数,另一个是通过 var 关键字声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 全局变量
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
// 函数 test 内的局部作用域
i = 5;

var foo = 3;
bar = 4;
}
test(10);

foo 和 i 是函数 test 内的局部变量,而对 bar 的赋值将会覆盖全局作用域内的同名变量。

变量声明提升(Hoisting)

JavaScript 会提升变量声明。这意味着 var 表达式和 function 声明都将会被提升到当前作用域的顶部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
if (false) {
goo = 1;

} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}

上面代码在运行之前将会被转化。JavaScript 将会把 var 表达式 和 function 声明 提升到当前作用域的顶部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// var 表达式被移动到这里
var bar, someValue; // 缺省值是 'undefined'

// 函数声明也会提升
function test(data) {
var goo, i, e; // 没有块级作用域,这些变量被移动到函数顶部
if (false) {
goo = 1;

} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}

bar(); // 出错:TypeError,因为 bar 依然是 'undefined'
someValue = 42; // 赋值语句不会被提升规则(hoisting)影响
bar = function() {};

test();

没有块级作用域不仅导致 var 表达式被从循环内移到外部,而且使一些 if 表达式更难看懂。

在原来代码中,if 表达式看起来修改了全局变量 goo ,实际上在提升规则被应用后,却是在修改局部变量。

如果没有提升规则(hoisting),下面的代码看起来会抛出异常 ReferenceError

1
2
3
4
// 检查 SomeImportantThing 是否已经被初始化
if (!SomeImportantThing) {
var SomeImportantThing = {};
}

实际上,上面的代码正常运行,因为 var 表达式会被提升到全局作用域的顶部。

1
2
3
4
5
6
7
8
var SomeImportantThing;

// 其它一些代码,可能会初始化 SomeImportantThing,也可能不会

// 检查是否已经被初始化
if (!SomeImportantThing) {
SomeImportantThing = {};
}
1
2
3
4
5
6
7
8
// 译者注:来自 Nettuts+ 的一段代码,生动的阐述了 JavaScript 中变量声明提升规则
// http://code.tutsplus.com/tutorials/quick-tip-javascript-hoisting-explained--net-15092
var myvar = 'my value';

(function() {
alert(myvar); // undefined
var myvar = 'local value';
})();

名称解析顺序

JavaScript 中的所有作用域,包括全局作用域,都有一个特别的名称 this 指向当前对象。

函数作用域内也有默认的变量 arguments ,其中包含了传递到函数中的参数。

比如,当访问函数内的 foo 变量时,JavaScript 会按照下面顺序查找:

  1. 当前作用域内是否有 var foo 的定义
  2. 函数形式参数是否有使用 foo 名称的
  3. 函数自身是否叫做 foo
  4. 回溯到上一级作用域,然后从 #1 重新开始

注意:自定义 arguments 参数将会阻止原生的 arguments 对象的创建。

命名空间

只有一个全局作用域导致的常见错误是命名冲突。在 JavaScript中,这可以通过 匿名包装器轻松解决。

1
2
3
4
5
6
7
8
(function() {
// 函数创建一个命名空间

window.foo = function() {
// 对外公开的函数,创建了闭包
};

})(); // 立即执行此匿名函数

匿名函数被认为是表达式,因此为了可调用性,它们首先会被执行。

1
2
3
4
( // 小括号内的函数首先被执行
function() {}
) // 并且返回函数对象
() // 调用上面的执行结果,也就是函数对象

有一些其他的调用函数表达式的方法,比如下面的两种方式语法不同,但是效果一模一样

1
2
3
// 另外两种方式
+function(){}();
(function(){}());

结论

推荐使用匿名包装器(译者注:也就是自执行的匿名函数)来创建命名空间。这样不仅可以防止命名冲突,而且有利于程序的模块化。

另外,使用全局变量被认为是不好的习惯。这样的代码容易产生错误并且维护成本较高。

数组

数组遍历与属性

虽然在 JavaScript 中数组是对象,但是没有好的理由去使用 for in 循环遍历数组。相反,有一些好的理由不去使用 for in 遍历数组。

注意:JavaScript 中数组不是关联数组。JavaScript 中只有对象来管理键值的对应关系。但是关联数组是保持顺序的,而对象不是。

由于 for in 循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 hasOwnProperty 函数,因此会比普通的 for 循环慢上好多倍。

遍历

为了达到遍历数组的最佳性能,推荐使用经典的 for 循环。

1
2
3
4
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}

上面代码有一个处理,就是通过 l = list.length 来缓存数组的长度。

虽然 length 是数组的一个属性,但是在每次循环中访问它还是有性能开销。 可能最新的 JavaScript 引擎在这点上做了优化,但是我们没法保证自己的代码是否运行在这些最近的引擎之上。

实际上,不使用缓存数组长度的方式比缓存版本要慢很多。

length 属性

length 属性的 getter 方式会简单的返回数组的长度,而 setter 方式会截断数组。

1
2
3
4
5
6
var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]

foo.length = 6;
foo; // [1, 2, 3]

译者注:在 Firebug 中查看此时 foo 的值是:[1, 2, 3, undefined, undefined, undefined] 但是这个结果并不准确,如果你在 Chrome 的控制台查看 foo 的结果,你会发现是这样的:[1, 2, 3] 因为在 JavaScript 中 undefined 是一个变量,注意是变量不是关键字,因此上面两个结果的意义是完全不相同的。

1
2
3
4
// 译者注:为了验证,我们来执行下面代码,看序号 5 是否存在于 foo 中。
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 false
foo[5] = undefined;
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 true

为 length 设置一个更小的值会截断数组,但是增大 length 属性值不会对数组产生影响。

结论

为了更好的性能,推荐使用普通的 for 循环并缓存数组的 length 属性。使用 for in 遍历数组被认为是不好的代码习惯并倾向于产生错误和导致性能问题。

Array 构造函数

由于 Array 的构造函数在如何处理参数时有点模棱两可,因此总是推荐使用数组的字面语法 [] 来创建数组。

译者注:这里的模棱两可指的是数组的两种构造函数语法

1
2
3
4
5
6
7
8
9
10
[1, 2, 3]; // 结果: [1, 2, 3]
new Array(1, 2, 3); // 结果: [1, 2, 3]

[3]; // 结果: [3]
new Array(3); // 结果: []
new Array('3') // 结果: ['3']

// 译者注:因此下面的代码将会使人很迷惑
new Array(3, 4, 5); // 结果: [3, 4, 5]
new Array(3) // 结果: [],此数组长度为 3

由于只有一个参数传递到构造函数中(译者注:指的是 new Array(3); 这种调用方式),并且这个参数是数字,构造函数会返回一个 length 属性被设置为此参数的空数组。需要特别注意的是,此时只有 length 属性被设置,真正的数组并没有生成。

1
2
3
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 数组还没有生成

这种优先于设置数组长度属性的做法只在少数几种情况下有用,比如需要重复字符串,可以避免 for 循环的麻烦。

1
2
//new Array(3).join('#') 将会返回 ##
new Array(count + 1).join(stringToRepeat);

结论

应该尽量避免使用数组构造函数创建新数组。推荐使用数组的字面语法。它们更加短小和简洁,因此增加了代码的可读性。

类型

相等与比较

JavaScript 有两种方式判断两个值是否相等。

等于操作符

等于操作符由两个等号组成:==

JavaScript 是弱类型语言,这就意味着,等于操作符会为了比较两个值而进行强制类型转换。

1
2
3
4
5
6
7
8
9
""           ==   "0"           // false
0 == "" // true
0 == "0" // true
false == "false" // false
false == "0" // true
false == undefined // false
false == null // false
null == undefined // true
" \t\r\n" == 0 // true

上面的表格展示了强制类型转换,这也是使用 == 被广泛认为是不好编程习惯的主要原因,由于它的复杂转换规则,会导致难以跟踪的问题。

此外,强制类型转换也会带来性能消耗,比如一个字符串为了和一个数字进行比较,必须事先被强制转换为数字。

严格等于操作符

严格等于操作符由三个等号组成:===

不像普通的等于操作符,严格等于操作符不会进行强制类型转换。

1
2
3
4
5
6
7
8
9
""           ===   "0"           // false
0 === "" // false
0 === "0" // false
false === "false" // false
false === "0" // false
false === undefined // false
false === null // false
null === undefined // false
" \t\r\n" === 0 // false

上面的结果更加清晰并有利于代码的分析。如果两个操作数类型不同就肯定不相等也有助于性能的提升。

比较对象

虽然 == 和 === 操作符都是等于操作符,但是当其中有一个操作数为对象时,行为就不同了。

1
2
3
4
5
{} === {};                   // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true

这里等于操作符比较的不是值是否相等,而是是否属于同一个身份。也就是说,只有对象的同一个实例才被认为是相等的。这有点像 Python 中的 is 和 C 中的指针比较。

结论

强烈推荐使用严格等于操作符。如果类型需要转换,应该在比较之前显式的转换,而不是使用语言本身复杂的强制转换规则。

typeof 操作符

typeof 操作符(和 instanceof 一起)或许是 JavaScript 中最大的设计缺陷,因为几乎不可能从它们那里得到想要的结果。

尽管 instanceof 还有一些极少数的应用场景,typeof 只有一个实际的应用(译者注:这个实际应用是用来检测一个对象是否已经定义或者是否已经赋值),而这个应用却不是用来检查对象的类型。

注意:由于 typeof 也可以像函数的语法被调用,比如 typeof(obj) 但这并不是一个函数调用。那两个小括号只是用来计算一个表达式的值,这个返回值会作为 typeof 操作符的一个操作数。实际上不存在名为 typeof 的函数。

JavaScript 类型表格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Value               Class      Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object

上面表格中,Type 一列表示 typeof 操作符的运算结果。可以看到,这个值在大多数情况下都返回 object

Class 一列表示对象的内部属性 [[Class]] 的值。

为了获取对象的 [[Class]] ,我们需要使用定义在 Object.prototype 上的方法 toString

对象的类定义

JavaScript 标准文档只给出了一种获取 [[Class]] 值的方法,那就是使用 Object.prototype.toString

注意:JavaScript 标准文档中定义: [[Class]] 的值只可能是下面字符串中的一个:Arguments , Array , Boolean , Date , Error , Function , JSON , Math , Number , Object , RegExp , String

1
2
3
4
5
6
7
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true

上面例子中,Object.prototype.toString 方法被调用,this 被设置为了需要获取 [[Class]] 值的对象。

译者注:Object.prototype.toString 返回一种标准格式字符串,所以上例可以通过 slice 截取指定位置的字符串,如下所示:

1
2
3
Object.prototype.toString.call([])    // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(2) // "[object Number]"

译者注:这种变化可以从 IE8 和 Firefox 4 中看出区别,如下所示:

1
2
3
4
5
6
7
// IE8
Object.prototype.toString.call(null) // "[object Object]"
Object.prototype.toString.call(undefined) // "[object Object]"

// Firefox 4
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"

ES5 提示:在 ECMAScript 5 中,为了方便,对 null 和 undefined 调用 Object.prototype.toString 方法,其返回值由 Object 变成了 Null 和 Undefined

测试为定义变量

1
typeof foo !== 'undefined'

上面代码会检测 foo 是否已经定义。如果没有定义而直接使用会导致 ReferenceError 的异常。这是 typeof 唯一有用的地方。

结论

为了检测一个对象的类型,强烈推荐使用 Object.prototype.toString 方法,因为这是唯一一个可依赖的方式。正如上面表格所示,typeof 的一些返回值在标准文档中并未定义,因此不同的引擎实现可能不同。

除非为了检测一个变量是否已经定义,我们应尽量避免使用 typeof 操作符。

instanceof 操作符

instanceof 操作符用来比较两个操作数的构造函数。只有在比较自定义的对象时才有意义。如果用来比较内置类型,将会和 typeof 操作符一样用处不大。

比较自定义对象

1
2
3
4
5
6
7
8
9
10
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true

// 如果仅仅设置 Bar.prototype 为函数 Foo 本身,而不是 Foo 构造函数的一个实例
Bar.prototype = Foo;
new Bar() instanceof Foo; // false

instanceof 比较内置类型

1
2
3
4
5
new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

有一点需要注意,instanceof 用来比较属于不同 JavaScript 上下文的对象(比如,浏览器中不同的文档结构)时将会出错,因为它们的构造函数不会是同一个对象。

结论

instanceof 操作符应该仅仅用来比较来自同一个 JavaScript 上下文的自定义对象。正如 typeof 操作符一样,任何其它的用法都应该是避免的。

类型转换

JavaScript 是弱类型语言,所以会在任何可能的情况下应用强制类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
// 下面的比较结果是:true
new Number(10) == 10; // Number.toString() 返回的字符串被再次转换为数字

10 == '10'; // 字符串被转换为数字
10 == '+10 '; // 同上
10 == '010'; // 同上
isNaN(null) == false; // null 被转换为数字 0
// 0 当然不是一个 NaN(译者注:否定之否定)

// 下面的比较结果是:false
10 == 010;
10 == '-10';

为了避免上面复杂的强制类型转换,强烈推荐使用严格的等于操作符。虽然这可以避免大部分的问题,但 JavaScript 的弱类型系统仍然会导致一些其它问题。

ES5 提示:以 0 开头的数字字面值会被作为八进制数字解析。而在 ECMAScript 5 严格模式下,这个特性被移除了。

内置类型的构造函数

内置类型(比如 Number 和 String )的构造函数在被调用时,使用或者不使用 new 的结果完全不同。

1
2
3
new Number(10) === 10;     // False, 对象与数字的比较
Number(10) === 10; // True, 数字与数字的比较
new Number(10) + 0 === 10; // True, 由于隐式的类型转换

使用内置类型 Number 作为构造函数将会创建一个新的 Number 对象,而在不使用 new 关键字的 Number 函数更像是一个数字转换器。

另外,在比较中引入对象的字面值将会导致更加复杂的强制类型转换。

最好的选择是把要比较的值显式的转换为三种可能的类型之一。

转换为字符串

1
'' + 10 === '10'; // true

将一个值加上空字符串可以轻松转换为字符串类型。

转换为数字

1
+'10' === 10; // true

使用一元的加号操作符,可以把字符串转换为数字。

译者注:字符串转换为数字的常用方法:

1
2
3
4
5
6
7
+'010' === 10
Number('010') === 10
parseInt('010', 10) === 10 // 用来转换为整数

+'010.2' === 10.2
Number('010.2') === 10.2
parseInt('010.2', 10) === 10

转换为布尔型

通过使用 ! 操作符两次,可以把一个值转换为布尔型。

1
2
3
4
5
6
7
!!'foo';   // true
!!''; // false
!!'0'; // true
!!'1'; // true
!!'-1' // true
!!{}; // true
!!true; // true

核心

为什么不要使用 eval

eval 函数会在当前作用域中执行一段 JavaScript 代码字符串。

1
2
3
4
5
6
7
8
var foo = 1;
function test() {
var foo = 2;
eval('foo = 3');
return foo;
}
test(); // 3
foo; // 1

但是 eval 只在被直接调用并且调用函数就是 eval 本身时,才在当前作用域中执行。

1
2
3
4
5
6
7
8
9
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;
}
test(); // 2
foo; // 3

译者注:上面的代码等价于在全局作用域中调用 eval ,和下面两种写法效果一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 写法一:直接调用全局作用域下的 foo 变量
var foo = 1;
function test() {
var foo = 2;
window.foo = 3;
return foo;
}
test(); // 2
foo; // 3

// 写法二:使用 call 函数修改 eval 执行的上下文为全局作用域
var foo = 1;
function test() {
var foo = 2;
eval.call(window, 'foo = 3');
return foo;
}
test(); // 2
foo; // 3

在任何情况下我们都应该避免使用 eval 函数。99.9% 使用 eval 的场景都有不使用 eval 的解决方案。

伪装的 eval

定时函数 setTimeout 和 setInterval 都可以接受字符串作为它们的第一个参数。这个字符串总是在全局作用域中执行,因此 eval 在这种情况下没有被直接调用。

安全问题

eval 也存在安全问题,因为它会执行任意传给它的代码,在代码字符串未知或者是来自一个不信任的源时,绝对不要使用 eval 函数。

结论

绝对不要使用 eval 任何使用它的代码都会在它的工作方式,性能和安全性方面受到质疑。如果一些情况必须使用到 eval 才能正常工作,首先它的设计会受到质疑,这不应该是首选的解决方案,一个更好的不使用 eval 的解决方案应该得到充分考虑并优先采用。

undefined 和 null

JavaScript 有两个表示‘空’的值,其中比较有用的是 undefined

undefined 的值

undefined 是一个值为 undefined 的类型。

这个语言也定义了一个全局变量,它的值是 undefined 这个变量也被称为 undefined 但是这个变量不是一个常量,也不是一个关键字。这意味着它的值可以轻易被覆盖。

ES5 提示:在 ECMAScript 5 的严格模式下,undefined 不再是可写的了。但是它的名称仍然可以被隐藏,比如定义一个函数名为 undefined

下面的情况会返回 undefined 值:

  • 访问未修改的全局变量 undefined
  • 由于没有定义 return 表达式的函数隐式返回
  • return 表达式没有显式的返回任何内容
  • 访问不存在的属性
  • 函数参数没有被显式的传递值
  • 任何被设置为 undefined 值的变量

处理 undefined 值的改变

由于全局变量 undefined 只是保存了 undefined 类型实际值的副本,因此对它赋新值不会改变类型 undefined 的值。

然而,为了方便其它变量和 undefined 做比较,我们需要事先获取类型 undefined 的值。

为了避免可能对 undefined 值的改变,一个常用的技巧是使用一个传递到匿名包装器的额外参数。在调用时,这个参数不会获取任何值。

1
2
3
4
5
var undefined = 123;
(function(something, foo, undefined) {
// 局部作用域里的 undefined 变量重新获得了 `undefined` 值

})('Hello World', 42);

另外一种达到相同目的方法是在函数内使用变量声明。

1
2
3
4
5
6
var undefined = 123;
(function(something, foo) {
var undefined;
...

})('Hello World', 42);

这里唯一的区别是,在压缩后并且函数内没有其它需要使用 var 声明变量的情况下,这个版本的代码会多出 4 个字节的代码。

译者注:这里有点绕口,其实很简单。如果此函数内没有其它需要声明的变量,那么 var 总共 4 个字符(包含一个空白字符)就是专门为 undefined 变量准备的,相比上个例子多出了 4 个字节。

null 的用处

JavaScript 中的 undefined 的使用场景类似于其它语言中的 null ,实际上 JavaScript 中的 null 是另外一种数据类型。

它在 JavaScript 内部有一些使用场景(比如声明原型链的终结 Foo.prototype = null ),但是大多数情况下都可以使用 undefined 来代替。

自动分号插入

尽管 JavaScript 有 C 的代码风格,但是它不强制要求在代码中使用分号,实际上可以省略它们。

JavaScript 不是一个没有分号的语言,恰恰相反上它需要分号来就解析源代码。因此 JavaScript 解析器在遇到由于缺少分号导致的解析错误时,会自动在源代码中插入分号。

1
2
3
var foo = function() {
} // 解析错误,分号丢失
test()

自动插入分号,解析器重新解析。

1
2
3
var foo = function() {
}; // 没有错误,解析继续
test()

自动的分号插入被认为是 JavaScript 语言最大的设计缺陷之一,因为它能改变代码的行为。

工作原理

下面的代码没有分号,因此解析器需要自己判断需要在哪些地方插入分号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function(window, undefined) {
function test(options) {
log('testing!')

(options.list || []).forEach(function(i) {

})

options.value.test(
'long string to pass here',
'and another long string to pass'
)

return
{
foo: function() {}
}
}
window.test = test

})(window)

(function(window) {
window.someLibrary = {}
})(window)

下面是解析器”猜测”的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function(window, undefined) {
function test(options) {

// 没有插入分号,两行被合并为一行
log('testing!')(options.list || []).forEach(function(i) {

}); // <- 插入分号

options.value.test(
'long string to pass here',
'and another long string to pass'
); // <- 插入分号

return; // <- 插入分号, 改变了 return 表达式的行为
{ // 作为一个代码段处理
foo: function() {}
}; // <- 插入分号
}
window.test = test; // <- 插入分号

// 两行又被合并了
})(window)(function(window) {
window.someLibrary = {}; // <- 插入分号
})(window); //<- 插入分号

解析器显著改变了上面代码的行为,在另外一些情况下也会做出错误的处理。

前置括号

在前置括号的情况下,解析器不会自动插入分号。

1
2
log('testing!')
(options.list || []).forEach(function(i) {})

上面代码被解析器转换为一行。

1
log('testing!')(options.list || []).forEach(function(i) {})

log 函数的执行结果极大可能不是函数,这种情况下就会出现 TypeError 的错误,详细错误信息可能是 undefined is not a function

结论

建议绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行,对于只有一行代码的 if 或者 else 表达式,也不应该省略花括号。这些良好的编程习惯不仅可以提到代码的一致性,而且可以防止解析器改变代码行为的错误处理。

注意:JavaScript 不能正确的处理 return 表达式紧跟换行符的情况,虽然这不能算是自动分号插入的错误,但这确实是一种不希望的副作用。

其它

setTimeout 和 setInterval

由于 JavaScript 是异步的,可以使用 setTimeout 和 setInterval 来计划执行函数。

1
2
function foo() {}
var id = setTimeout(foo, 1000); // 返回一个大于零的数字

当 setTimeout 被调用时,它会返回一个 ID 标识并且计划在将来大约 1000 毫秒后调用 foo 函数。 foo 函数只会被执行一次。

基于 JavaScript 引擎的计时策略,以及本质上的单线程运行方式,所以其它代码的运行可能会阻塞此线程。 因此没法确保函数会在 setTimeout 指定的时刻被调用。

作为第一个参数的函数将会在全局作用域中执行,因此函数内的 this 将会指向这个全局对象。

1
2
3
4
5
6
7
8
9
function Foo() {
this.value = 42;
this.method = function() {
// this 指向全局对象
console.log(this.value); // 输出:undefined
};
setTimeout(this.method, 500);
}
new Foo();

setInterval 的堆调用

setTimeout 只会执行回调函数一次,不过 setInterval 正如名字建议的,会每隔 X 毫秒执行函数一次。 但是却不鼓励使用这个函数。

当回调函数的执行被阻塞时,setInterval 仍然会发布更多的回调指令。在很小的定时间隔情况下,这会导致回调函数被堆积起来。

1
2
3
4
function foo(){
// 阻塞执行 1 秒
}
setInterval(foo, 100);

上面代码中,foo 会执行一次随后被阻塞了一秒钟。

在 foo 被阻塞的时候,setInterval 仍然在组织将来对回调函数的调用。因此,当第一次 foo 函数调用结束时,已经有 10 次函数调用在等待执行。

处理可能的阻塞调用

最简单也是最容易控制的方案,是在回调函数内部使用 setTimeout 函数。

1
2
3
4
5
function foo(){
// 阻塞执行 1 秒
setTimeout(foo, 100);
}
foo();

这样不仅封装了 setTimeout 回调函数,而且阻止了调用指令的堆积,可以有更多的控制。foo 函数现在可以控制是否继续执行还是终止执行。

手工清空定时器

可以通过将定时时产生的 ID 标识传递给 clearTimeout 或者 clearInterval 函数来清除定时,至于使用哪个函数取决于调用的时候使用的是 setTimeout 还是 setInterval

1
2
var id = setTimeout(foo, 1000);
clearTimeout(id);

清除所有定时器

由于没有内置的清除所有定时器的方法,可以采用一种暴力的方式来达到这一目的。

1
2
3
4
// 清空"所有"的定时器
for(var i = 1; i < 1000; i++) {
clearTimeout(i);
}

可能还有些定时器不会在上面代码中被清除(译者注:如果定时器调用时返回的 ID 值大于 1000),因此我们可以事先保存所有的定时器 ID,然后一把清除。

隐藏使用 eval

setTimeout 和 setInterval 也接受第一个参数为字符串的情况。这个特性绝对不要使用,因为它在内部使用了 eval

1
2
3
4
5
6
7
8
9
10
11
function foo() {
// 将会被调用
}

function bar() {
function foo() {
// 不会被调用
}
setTimeout('foo()', 1000);
}
bar();

由于 eval 在这种情况下不是被直接调用,因此传递到 setTimeout 的字符串会自全局作用域中执行。因此,上面的回调函数使用的不是定义在 bar 作用域中的局部变量 foo

建议不要在调用定时器函数时,为了向回调函数传递参数而使用字符串的形式。

1
2
3
4
5
6
7
8
9
function foo(a, b, c) {}

// 不要这样做
setTimeout('foo(1,2, 3)', 1000)

// 可以使用匿名函数完成相同功能
setTimeout(function() {
foo(1, 2, 3);
}, 1000)

结论

绝对不要使用字符串作为 setTimeout 或者 setInterval 的第一个参数,这么写的代码明显质量很差。当需要向回调函数传递参数时,可以创建一个匿名函数,在函数内执行真实的回调函数。

另外,应该避免使用 setInterval ,因为它的定时执行不会被 JavaScript 阻塞。

注意: 虽然也可以使用这样的语法 setTimeout(foo, 1000, 1, 2, 3) , 但是不推荐这么做,因为在使用对象的属性方法时可能会出错。(译者注:这里说的是属性方法内,this 的指向错误)

相关 DEMO

示例 1

1
2
3
4
5
6
7
8
9
10
11
12
13
var a=100;
var b=true;
function test(){
alert(a); // undefined
alert(b); // true
b=false;
alert(b); // false
var a=200;
alert(a/2); // 100
alert(++Math.PI); //4.14159...
alert(Math.PI++); //3.14159....
}
test();

示例 2

1
2
3
4
5
6
Object.test = "bbb";
Object.prototype.test = 'ccc';
window.test = "aaa";
(function f() {
alert(test); //aaa 若把 window.test 注释,则弹出结果为 ccc
})();

示例 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function empty(){}
empty.prototype = {a:0};
var o = new empty();
o.a = 1
a = 2;
with(o)
{
alert(a); // 1
var a=3;
alert(a); // 3
delete o.a;
alert(a); // 0
delete o.a;
alert(a); // 0
delete empty.prototype.a;
alert(a); //2
}

上面给出的三个例子均来自 javascript变量的作用域 和 对象闭包 这两篇文章,想详细了解 JS 作用域相关知识可点击查看。

示例 4

1
2
3
4
5
6
7
8
9
10
11
12
13
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}

//三行a,b,c的输出分别是什么?
var a = fun(0); a.fun(1); a.fun(2); a.fun(3); // a : undefined, 0, 0, 0
var b = fun(0).fun(1).fun(2).fun(3); // b : undefined, 0, 1, 2
var c = fun(0).fun(1); c.fun(2); c.fun(3); // c : undefined, 0, 1, 1

相关阅读

深入学习 javascript
ECMA-262-3 in detail In dmitrysoshnikov.com
pjs
js.class
Simple JavaScript Inheritance

MarkDown 学习笔记

发表于 2016-10-06

MarkDown是一种适用于网络的书写语言,可以帮助你快速书写文档,不必再纠结文档排版的问题。并且它的语法简单,学习成本低,程序员必备技能…助你快速书写技术文档、文章。

用于书写 MarkDown 的编辑器有很多,这里就不在述说,不知道的码农可自行百度。在这我推荐使用 Sublime Text 3 这款编辑器,速度快,并且有大量的插件可用,能满足你日常编程的需求。个人使用 MarkdownEditing 和 MarkdownPreview 这两个插件来帮助编写 MarkDown 文件。

可以利用 Chrome 浏览器,点击打印另存为 PDF 格式文件,这样分享给其他人阅读。

MarkDown 语法

一、标题

行首插入 1 到 6 个 #,对应到标题 h1 到 h6

1
2
3
# 这是 H1
## 这是 H2
###### 这是 H6

二、段落和换行

一个 MarkDown 段落是由一个或多个连续的文本行组成,它的前后要有一个以上的空行(空行的定义是显示上看起来像是空的,便会被视为空行。比方说,若某一行只包含空格和制表符,则该行也会被视为空行)。普通段落不该用空格或制表符来缩进。

如果你确实想要依赖 MarkDown <br> 来插入标签的话,在插入处先按入两个以上的空格然后回车。

三、区块引用 Blockquotes

MarkDown 文件中建立一个区块引用语法很简单,只需在每行的最前面加上 > 符号。区块引用还可以嵌套(例如:引用内的引用),只要根据层次加上不同数量的 >

1
2
3
4
5
6
7
8
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.

> This is the first level of quoting.
>
> > This is nested blockquote.
>
> Back to the first level

四、列表

MarkDown 支持有序列表和无序列表,无序列表使用星号、加号或是减号作为列表标记。

1
2
3
4
5
6
7
8
9
10
11
*   Red
* Green
* Blue

+ Red
+ Green
+ Blue

- Red
- Green
- Blue

有序列表则使用数字接着一个英文句点。

1
2
3
1.  Bird
2. McHale
3. Parish

五、代码

和程序相关的写作或是标签语言原始码通常会有已经排版好的代码区块,通常这些区块我们并不希望它以一般段落文件的方式去排版,而是照原来的样子显示 MarkDown 会用 <pre> 和 <code> 标签来把代码区块包起来。

要在 Markdown 中建立代码区块很简单,只要简单地缩进 4 个空格或是 1 个制表符就可以,例如,下面的输入:

1
2
3
这是一个普通段落

这是一个代码区块

MarkDown 会转换成:

1
2
3
<p>这是一个普通段落</p>

<pre>这是一个代码区块</pre>

如果要标记一小段行内代码,你可以用反引号把它包起来

1
Use the `printf()` function.

如果要在代码区段内插入反引号,你可以用多个反引号来开启和结束代码区段

1
``There is a literal backtick (`) here.``

六、连接

行内式 要建立一个行内式的链接,只要在方块括号后面紧接着圆括号并插入网址链接即可,如果你还想要加上链接的 title 文字,只要在网址后面,用双引号把 title 文字包起来即可。

1
2
This is [an example](http://example.com/ "Title") inline link.  
See my [About](/about/) page for details.

参考式 的链接是在链接文字的括号后面再接上另一个方括号,而在第二个方括号里面要填入用以辨识链接的标记,你也可以选择性地在两个方括号中间加上一个空格。接着,在文件的任意处,你可以把这个标记的链接内容定义出来。例:

1
2
This is [an example] [id] reference-style link.
[id]: http://example.com/ "Optional Title Here"

链接内容定义的形式为:

  1. 方括号(前面可以选择性地加上至多三个空格来缩进),里面输入链接文字
  2. 接着一个冒号
  3. 接着一个以上的空格或制表符
  4. 接着链接的网址
  5. 选择性地接着 title 内容,可以用单引号、双引号或是括弧包着,有一个已知的问题是 Markdown.pl 1.0.1 会忽略单引号包起来的链接 title

七、图片

1
2
3
4
5
行内式

![Alt text](//www.baidu.com/img/bd_logo1.png "百度")

![](file:///D:/xampp/htdocs/test/tmp/lessoo2o/20160402092920.png)
  1. 一个惊叹号!
  2. 接着一个方括号,里面放上图片的替代文字
  3. 接着一个普通括号,里面放上图片的网址,最后还可以用引号包住并加上 选择性的 ‘title’ 文字。
1
2
3
4
参考式

![Alt text][id]
[id]: url/to/image "Optional title attribute"

「id」是图片参考的名称,图片参考式的定义方式则和连接参考式一样。

八、反斜杠

MarkDown 可以利用反斜杠来插入一些在语法中有其它意义的符号,例如:如果你想要用星号加在文字旁边的方式来做出强调效果(但不用<em>标签),你可以在星号的前面加上反斜杠:

1
\*literal asterisks\*

MarkDown 支持以下这些符号前面加上反斜杠来帮助插入普通的符号:

1
2
3
4
5
6
7
8
9
10
11
12
\   反斜线
` 反引号
* 星号
_ 底线
{} 花括号
[] 方括号
() 括弧
# 井字号
+ 加号
- 减号
. 英文句点
! 惊叹号

九、自动链接

MarkDown 支持以比较简短的自动链接形式来处理网址和电子邮件信箱,只要是用方括号包起来, MarkDown 就会自动把它转成链接。一般网址的链接文字就和链接地址一样,例如:

1
2
<http://example.com/>
<address@example.com>

相关阅读

  • Markdown 语法说明

常用 HTML 头部标签解析

发表于 2016-10-06

作为一名 Web 开发者,对头部标签必须要了解,本篇着重介绍一些我们容易忽视和用错的头部标签,特别是 iOS 等针对移动设备的一些标签。了解这些标签的意义,写出满足自己需求的 head 头标签,是本文的目的。本文以一丝的文章和 FEX 的 HTML head 头标签为基础,介绍常用的 head 中各个标签、元素的意义。

DOCTYPE

DOCTYPE(Document Type),该声明位于文档中最前面的位置,处于 html 标签之前,此标签告知浏览器文档使用哪种 HTML 或者 XHTML 规范。

DTD(Document Type Definition) 声明以 <!DOCTYPE> 开始,不区分大小写,前面没有任何内容,如果有其他内容(空格除外)会使浏览器在 IE 下开启怪异模式(quirks mode)渲染网页。公共 DTD,名称格式为注册//组织//类型 标签//语言,注册指组织是否由国际标准化组织(ISO)注册,+ 表示是,- 表示不是。组织即组织名称,如:W3C。类型一般是 DTD。标签是指定公开文本描述,即对所引用的公开文本的唯一描述性名称,后面可附带版本号。最后语言是 DTD 语言的 ISO 639 语言标识符,如:EN 表示英文,ZH 表示中文。XHTML 1.0 可声明三种 DTD 类型。分别表示严格版本,过渡版本,以及基于框架的 HTML 文档。

  • HTML 4.01 strict
1
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
  • HTML 4.01 Transitional
1
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  • HTML 4.01 Frameset
1
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
  • 最新 HTML5 推出更加简洁的书写,它向前向后兼容,推荐使用
1
<!DOCTYPE html>

在 HTML中 doctype 有两个主要作用

  1. 对文档进行有效性验证
    它告诉用户代理和校验器这个文档是按照什么 DTD 写的。这个动作是被动的,每次页面加载时,浏览器并不会下载 DTD 并检查合法性,只有当手动校验页面时才启用。

  2. 决定浏览器的呈现模式
    对于实际操作,通知浏览器读取文档时用哪种解析算法。如果没有写,则浏览器则根据自身的规则对代码进行解析,可能会严重影响 html 排版布局。浏览器有三种方式解析 HTML 文档。
    (1) 非怪异(标准)模式
    (2) 怪异模式
    (3) 部分怪异(近乎标准)模式 关于IE浏览器的文档模式,浏览器模式,严格模式,怪异模式,DOCTYPE 标签,可详细阅读 模式?标准!的内容

charset

声明文档使用的字符编码

1
<meta charset="utf-8">

在 html5 之前网页中会这样写

1
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

lang属性

1
2
3
<html lang="zh-cmn-Hans">  //简体中文

<html lang="zh-cmn-Hant"> //繁体中文

很少情况才需要加地区代码,通常是为了强调不同地区汉语使用差异。例如:

1
2
3
<p lang="zh-cmn-Hans">
<strong lang="zh-cmn-Hans-CN">菠萝</strong>和<strong lang="zh-cmn-Hant-TW">鳳梨</strong>其实是同一种水果。只是大陆和台湾称谓不同,且新加坡、马来西亚一带的称谓也是不同的,称之为<strong lang="zh-cmn-Hans-SG">黄梨</strong>。
</p>

为什么 lang=”zh-cmn-Hans” 而不是我们通常写的 lang=”zh-CN” 呢?请移步阅读:网页头部的声明应该是用 lang=”zh” 还是 lang=”zh-cn”

优先使用 IE 最新版本和 Chrome

1
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

360 使用Google Chrome Frame

1
<meta name="renderer" content="webkit">

360 浏览器就会在读取到这个标签后,立即切换对应的极速核。 另外为了保险起见再加入

1
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">

这样写可以达到的效果是如果安装了 Google Chrome Frame,则使用 GCF 来渲染页面,如果没有安装 GCF,则使用最高版本的 IE 内核进行渲染。
相关链接:浏览器内核控制 Meta 标签说明文档

百度禁止转码

过百度手机打开网页时,百度可能会对你的网页进行转码,为此可在 head 内添加

1
<meta http-equiv="Cache-Control" content="no-siteapp">

相关链接:SiteApp 转码声明

SEO 优化部分

  • 页面标题 <title> 标签( head 头部必须)
1
<title>your title</title>
  • 页面关键词 keywords
1
<meta name="keywords" content="your keywords">
  • 页面描述内容 description
1
<meta name="description" content="your description">
  • 定义网页作者 author
1
<meta name="author" content="author,email address">
  • 定义网页搜索引擎索引方式,robotterms 是一组使用英文逗号「,」分割的值,通常有如下几种取值:none,noindex,nofollow,all,index和follow。
1
<meta name="robots" content="index,follow">

相关链接:WEB1038 - 标记包含无效的值.aspx)

为移动设备添加 viewport

1
<meta name ="viewport" content ="initial-scale=1, maximum-scale=3, minimum-scale=1, user-scalable=no">

width=device-width 会导致 iPhone 5 添加到主屏后以 WebApp 全屏模式打开页面时出现黑边 http://bigc.at/ios-webapp-viewport-meta.orz

content 参数:

  • width viewport 宽度(数值/device-width)
  • height viewport 高度(数值/device-height)
  • initial-scale 初始缩放比例
  • maximum-scale 最大缩放比例
  • minimum-scale 最小缩放比例
  • user-scalable 是否允许用户缩放(yes/no)
  • minimal-ui iOS 7.1 beta 2 中新增属性(注意:iOS8 中已经删除),可以在页面加载时最小化上下状态栏。这是一个布尔值,可以直接这样写:<meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">

适配 iPhone 6 和 iPhone 6plus 则需要写:

1
2
<meta name="viewport" content="width=375">
<meta name="viewport" content="width=414">

大部分 4.7~5 寸的安卓设备的 viewport 宽设为 360px,iPhone 6 上却是 375px,大部分 5.5 寸安卓机器(比如说三星 Note)的 viewport 宽为 400,iPhone 6 plus 上是 414px。

ios 设备

【1】添加到主屏后的标题(iOS 6 新增)

1
<meta name="apple-mobile-web-app-title" content="标题"> <!-- 添加到主屏后的标题(iOS 6 新增) -->

【2】是否启用 WebApp 全屏模式

1
<meta name="apple-mobile-web-app-capable" content="yes"> <!-- 是否启用 WebApp 全屏模式 -->

【3】设置状态栏的背景颜色

1
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /><!-- 设置状态栏的背景颜色,只有在 `"apple-mobile-web-app-capable" content="yes"` 时生效 -->

只有在 "apple-mobile-web-app-capable" content="yes" 时生效

content 参数:

  • default 默认值
  • black 状态栏背景是黑色
  • black-translucent 状态栏背景是黑色半透明。

如果设置为 default 或 black , 网页内容从状态栏底部开始。
如果设置为 black-translucent , 网页内容充满整个屏幕,顶部会被状态栏遮挡

【4】禁止数字自动识别为电话号码

1
<meta name="format-detection" content="telephone=no"> <!-- 禁止数字识自动别为电话号码 -->

【5】禁止自动自动识别地址

1
<meta name="format-detection" content="address=no"> <!-- 禁止自动自动识别地址 -->

【6】禁止自动自动识别日期

1
<meta name="format-detection" content="date=no">  <!-- 禁止自动自动识别日期 -->

【7】禁止自动自动识别 Email

1
<meta name="format-detection" content="email=no">  <!-- 禁止自动自动识别 Email -->

【8】iOS 图标

rel 参数:
apple-touch-icon 图片自动处理成圆角和高光等效果
apple-touch-icon-precomposed 禁止系统自动添加效果,直接显示设计原图

  • iPhone 和 iTouch,默认 57x57 像素,必须有
1
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-57x57-precomposed.png">
  • iPad,72x72 像素,可以没有,但推荐有
1
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/apple-touch-icon-72x72-precomposed.png">
  • Retina iPhone 和 Retina iTouch,114x114 像素,可以没有,但推荐有
1
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/apple-touch-icon-114x114-precomposed.png" />
  • Retina iPad,144x144 像素,可以没有,但推荐有
1
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144x144-precomposed.png">
  • IOS 图标大小在iPhone 6 plus上是180×180,iPhone 6 是120x120。 适配iPhone 6 plus,则需要在中加上这段
1
<link rel="apple-touch-icon-precomposed" sizes="180x180" href="retinahd_icon.png">

【9】iOS 启动画面

官方文档:https://developer.apple.com/library/ios/qa/qa1686/_index.html

iPad 的启动画面是不包括状态栏区域的

1.iPad 竖屏 768 x 1004(标准分辨率)

1
<link rel="apple-touch-startup-image" sizes="768x1004" href="/splash-screen-768x1004.png">

2.iPad 竖屏 1536x2008(Retina)

1
<link rel="apple-touch-startup-image" sizes="1536x2008" href="/splash-screen-1536x2008.png">

3.iPad 横屏 1024x748(标准分辨率)

1
<link rel="apple-touch-startup-image" sizes="1024x748" href="/Default-Portrait-1024x748.png">

4.iPad 横屏 2048x1496(Retina)

1
<link rel="apple-touch-startup-image" sizes="2048x1496" href="/splash-screen-2048x1496.png">

iPhone 和 iPod touch 的启动画面是包含状态栏区域的

1.iPhone/iPod Touch 竖屏 320x480 (标准分辨率)

1
<link rel="apple-touch-startup-image" href="/splash-screen-320x480.png">

2.iPhone/iPod Touch 竖屏 640x960 (Retina)

1
<link rel="apple-touch-startup-image" sizes="640x960" href="/splash-screen-640x960.png">

3.iPhone 5/iPod Touch 5 竖屏 640x1136 (Retina)

1
<link rel="apple-touch-startup-image" sizes="640x1136" href="/splash-screen-640x1136.png">

4.iPhone 6对应的图片大小是750×1294,iPhone 6 Plus 对应的是1242×2148

1
2
3
<link rel="apple-touch-startup-image" href="launch6.png" media="(device-width: 375px)">

<link rel="apple-touch-startup-image" href="launch6plus.png" media="(device-width: 414px)">

【10】添加智能 App 广告条 Smart App Banner(iOS 6+ Safari)

1
<meta name="apple-itunes-app" content="app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL">

Android

Android Lollipop 中的 Chrome 39 增加 theme-color meta 标签,用来控制选项卡颜色

1
<meta name="theme-color" content="#db5945">

Windows 8

  • Windows 8 磁贴颜色
1
<meta name="msapplication-TileColor" content="#000">
  • Windows 8 磁贴图标
1
<meta name="msapplication-TileImage" content="icon.png">

360 浏览器

设置 360 浏览器渲染模式,webkit 为极速内核,ie-comp 为 IE 兼容内核,ie-stand 为 IE 标准内核。

1
<meta name="renderer" content="webkit|ie-comp|ie-stand">

UC 浏览器

  • 设置屏幕方向,portrait 为横屏,landscape 为竖屏。
1
<meta name="screen-orientation" content="portrait|landscape">
  • 设置全屏
1
<meta name="full-screen" content="yes">
  • 设置适应屏幕排版(缩放是否显示滚动条)
    UC 浏览器在标准排版效果实现的基础上,提供适应屏幕的排版方式,当设置为 uc-fitscreen=yes,页面进行缩放操作时,仅放大图片和文字等元素,但不放大屏幕宽度,保持不出现水平(横向)滚动条。
1
<meta name="viewport" content="uc-fitscreen=no|yes">
  • 排版模式
    UC 浏览器提供两种排版模式,分别是适屏模式(fitscreen)及标准模式(standard),其中适屏模式简化了一些页面的处理,使得页面内容更适合进行页面阅读、节省流量及响应更快,而标准模式则能按照标准规范对页面进行排版及渲染。
1
<meta name="layoutmode" content="fitscreen|standard">
  • 夜间模式
    可以帮助用户在低亮度或黑暗情况下更舒适的进行页面浏览。由于基于网页的应用愈加复杂,由浏览器实现的单一夜间模式不一定能够适应所有情况(例如游戏应用),因此 UC 浏览器允许网页设计者对其设计的页面禁用浏览器的夜间模式,自行设计更适合用户使用的夜间模式。
    注意:页面内的 frame/iframe 中的夜间模式的 meta 不生效。
1
<meta name="nightmode" content="enable|disable">
  • 整页图片强制显示
    为了节省流量及加快速度,UC 为用户提供了无图模式,在实际使用中存在页面中的图片是不可缺少的,例如验证码,地图等。通过强制图片显示的功能可以保证图片显示不受用户的设置影响。
    注意:整页图片强制显示仅对当前页面生效,对页面内的 frame/iframe 不生效,也不影响前进后退的页面*
1
<meta name="imagemode" content="force">
  • QQ 浏览器(X5 内核)
1
2
3
4
5
6
7
8
<!-- 设置横屏、竖屏显示,portrait 横屏,landscape 竖屏-->
<meta name="x5-orientation" content="portrait|landscape">

<!-- 设置全屏显示 -->
<meta name="x5-fullscreen" content="true">

<!-- 开启应用模式 -->
<meta name="x5-page-mode" content="app">

其他

  • 添加 RSS 订阅
1
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml">
  • 添加 favicon icon
1
<link rel="shortcut icon" type="image/ico" href="/favicon.ico">
  • 禁止 Chrome 浏览器中自动提示翻译
1
<meta name="google" value="notranslate">

最后给出一个移动端的 meta 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection"content="telephone=no, email=no">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"><!-- 删除苹果默认的工具栏和菜单栏 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black"><!-- 设置苹果工具栏颜色 -->
<meta name="format-detection" content="telphone=no, email=no"><!-- 忽略页面中的数字识别为电话,忽略email识别 -->
<!-- 启用360浏览器的极速模式(webkit) -->
<meta name="renderer" content="webkit">
<!-- 避免IE使用兼容模式 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓 -->
<meta name="HandheldFriendly" content="true">
<!-- 微软的老式浏览器 -->
<meta name="MobileOptimized" content="320">
<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait">
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">
<!-- UC应用模式 -->
<meta name="browsermode" content="application">
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">
<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no">
<!-- 适应移动端end -->

相关阅读

  • WEB标准系列-HTML元素嵌套
  • Allowed nesting of elements in HTML 4 Strict and XHTML 1.0 Strict
12
avilang

avilang

爱折腾,爱分享,爱学习

17 日志
GitHub
© 2024 avilang
由 Hexo 强力驱动
主题 - NexT.Mist