JavaScript 是一门跨平台、面向对象的脚本语言,它能使网页可交互(例如拥有复杂的动画,可点击的按钮,通俗的菜单等)。另外还有高级的服务端Javascript版本,例如Node.js,它可以让你在网页上添加更多功能,不仅仅是下载文件(例如在多台电脑之间的协同合作)。在宿主环境(例如 web 浏览器)中, JavaScript 能够通过其所连接的环境提供的编程接口进行控制。
JavaScript 内置了一些对象的标准库,比如数组(Array),日期(Date),数学(Math)和一套核心语句,包括运算符、流程控制符以及声明方式等。JavaScript 的核心部分可以通过添加对象来扩展语言以适应不同用途;例如:
- 客户端的 JavaScript 通过提供对象,控制浏览器及其文档对象模型(DOM),来扩展语言核心。例如:客户端的拓展代码允许应用程序将元素放在某个 HTML 表单中,并且支持响应用户事件,比如鼠标点击、表单提交和页面导航。
- 服务端的 JavaScript 则通过提供有关在服务器上运行 JavaScript 的对象来可扩展语言核心。例如:服务端版本直接支持应用和数据库通信,提供应用不同调用间的信息连续性,或者在服务器上执行文件操作。
这意味着,在浏览器中,JavaScript 可以改变网页(DOM)的外观与样式。同样地,在服务器上,Node.js 中的 JavaScript 可以对浏览器上编写的代码发出的客户端请求做出响应。
代码规范
命名规则
变量名和函数名
变量和函数为小驼峰法标识, 即除第一个单词之外,其他单词首字母大写( lowerCamelCase )
全局变量和常量
全局变量和常量( 如PI )为大写 ( UPPERCASE )
符号使用规则
空格与运算符
通常运算符 ( = + - * / ) 前后需要添加空格:
var x = y + z;
var values = ["Volvo", "Saab", "Fiat"];
代码缩进
通常使用 4 个空格符号来缩进代码块:
function toCelsius(fahrenheit) {
return (5 / 9) * (fahrenheit - 32);
}
不推荐使用 TAB 键来缩进,因为不同编辑器 TAB 键的解析不一样。
每行代码字符小于80个
为了便于阅读每行字符建议小于数 80 个。
如果一个 JavaScript 语句超过了 80 个字符,建议在 运算符或者逗号后换行。
document.getElementById("demo").innerHTML =
"Hello Runoob.";
语句规则
简单语句
一条语句通常以分号作为结束符。
var values = ["Volvo", "Saab", "Fiat"];
var person = {
firstName: "John",
lastName: "Doe",
age: 50,
eyeColor: "blue"
};
复杂语句
- 将左花括号放在第一行的结尾。
- 左花括号前添加一空格。
函数
function toCelsius(fahrenheit) {
return (5 / 9) * (fahrenheit - 32);
}
循环
for (i = 0; i < 5; i++) {
x += i;
}
条件语句
if (time < 20) {
greeting = "Good day";
} else {
greeting = "Good evening";
}
对象规则
- 将左花括号与类名放在同一行。
- 冒号与属性值间有个空格。
- 字符串使用双引号,数字不需要。
- 最后一个属性-值对后面不要添加逗号。
- 将右花括号独立放在一行,并以分号作为结束符号。
var person = {
firstName: "John",
lastName: "Doe",
age: 50,
eyeColor: "blue"
};
短的对象代码可以直接写成一行:
var person = {firstName:"John", lastName:"Doe", age:50, eyeColor:"blue"};
语法和数据类型
Javascript借鉴了Java的大部分语法,但同时也受到了Awk,Perl和Python的影响。
JavaScript 是区分大小写的,并使用 Unicode 字符集。举个例子,可以将单词 Früh (在德语中意思是“早”)用作变量名。
在 JavaScript 中,指令被称为语句 (Statement),并用分号(;)进行分隔。推荐但不是必须,因为ECMAScript规定在语句末尾自动加上分号。
Javascript 源码从左往右被扫描并转换成一系列由 token 、控制字符、行终止符、注释和空白字符组成的输入元素。空白字符指的是空格、制表符和换行符等。
注释
// 单行注释
/* 多行注释 */
变量
var
: 声明一个变量,可选初始化为一个值。let
: 声明一个块作用域对象,可选初始化为一个值。const
: 声明一个块作用域只读常亮,可选初始化为一个值。
必须以字母、下划线(_)或者美元符号($)开头;后续的字符也可以是数字(0-9)。因为 JavaScript 语言是区分大小写的,所以字母可以是从“A”到“Z”的大写字母和从“a”到“z”的小写字母。
变量求值
用var
或let
语句声明的变量,如果没有赋初始值,则其值为undefined
。
如果访问一个未声明的变量会导致抛出一个引用错误(ReferenceError
)异常:
var a;
console.log("The value of a is " + a); // a 的值是 undefined
console.log("The value of b is " + b);// b 的值是 undefined
var b;
// 在你阅读下面的‘变量声明提升’前你可能会疑惑
console.log("The value of c is " + c); // 未捕获的引用错误: c 未被定义
let x;
console.log("The value of x is " + x); // x 的值是 undefined
console.log("The value of y is " + y);// 未捕获的引用错误: y 未被定义
let y;
你可以使用undefined
来判断一个变量是否已赋值。在以下的代码中,变量input未被赋值,因此if
条件语句的求值结果是true
。
var input;
if(input === undefined){
doThis();
} else {
doThat();
}
一个未被赋值的变量,在bool
语境下会被当作false
。下面的代码会执行myFunction()
。
var myArray = [];
if (!myArray[0]) myFunction();
在数值语境下会被当作NaN
。
var a;
a + 2; // 计算为 NaN
当对一个null
变量求值时,null
在数值语境下会被当作0
,而在bool
语境下会被当作false
。
var n = null;
console.log(n * 32); // 在控制台中会显示 0
当一个变量值为0
时,在bool
语境下会被当作false
。当值不为0
,在(−∞ ~ 0)或(0 ~ +∞)时,在bool
语境下会被当作true
。
var i = 0;
if (i) {
DoThis(); // if条件语句结果为false,执行DoThat()
} else {
DoThat();
}
i = -1;
if (i) {
DoThis(); // if条件语句结果为true,执行DoThis()
}
i = 1;
if (i) {
DoThis(); // if条件语句结果为true,执行DoThis()
}
作用域
在函数之外声明的变量,叫做全局变量,因为它可被当前文档中的任何其他代码所访问。
在函数内部声明的变量,叫做局部变量,因为它只能在当前函数的内部访问。
ECMAScript6 之前的 JavaScript 没有 语句块 作用域;相反,语句块中声明的变量将成为语句块所在函数(或全局作用域)的局部变量。
if (true) {
var x = 5;
}
console.log(x); // 5
如果使用 ECMAScript6 中的let
声明,上述行为将发生变化。
if (true) {
let y = 5;
}
console.log(y); // ReferenceError: y 没有被声明
变量提升
JavaScript 变量的另一个不同寻常的地方是,你可以先使用变量稍后再声明变量而不会引发异常。这一概念称为变量提升;JavaScript 变量感觉上是被“提升”或移到了函数或语句的最前面。但是,提升后的变量将返回undefined
值。因此在使用或引用某个变量之后进行声明和初始化操作,这个被提升的变量仍将返回undefined
值。
/**
* 例子1
*/
console.log(x === undefined); // true
var x = 3;
/**
* 例子2
*/
// will return a value of undefined
var myvar = "my value";
(function() {
console.log(myvar); // undefined
var myvar = "local value";
})();
上面的例子,也可写作:
/**
* 例子1
*/
var x;
console.log(x === undefined); // true
x = 3;
/**
* 例子2
*/
var myvar = "my value";
(function() {
var myvar;
console.log(myvar); // undefined
myvar = "local value";
})();
由于存在变量提升,一个函数中所有的var
语句应尽可能地放在接近函数顶部的地方。这个习惯将大大提升代码的清晰度。
在 ECMAScript6 中,let(const)
同样会被提升变量到代码块的顶部但是不会被赋予初始值。在变量声明之前引用这个变量,将抛出引用错误(ReferenceError)。这个变量将从代码块一开始的时候就处在一个“暂时性死区”,直到这个变量被声明为止。
console.log(x); // ReferenceError
let x = 3;
函数提升
对于函数来说,只有函数声明会被提升到顶部,而函数表达式不会被提升。
/* 函数声明 */
foo(); // "bar"
function foo() {
console.log("bar");
}
/* 函数表达式 */
baz(); // 类型错误:baz 不是一个函数
var baz = function() {
console.log("bar2");
};
全局变量
全局变量是全局对象的属性。在网页中,(译注:缺省的)全局对象是 window ,所以你可以用形如 window.variable 的语法来设置和访问全局变量。
因此,你可以通过指定 window 或 frame 的名字,在当前 window 或 frame 访问另一个 window 或 frame 中声明的变量。例如,在文档里声明一个叫phoneNumber
的变量,那么你就可以在子框架里使用parent.phoneNumber
的方式来引用它。
常量(Constants)
你可以用关键字const
创建一个只读的常量。常量标识符的命名规则和变量相同:必须以字母、下划线(_)或美元符号($)开头并可以包含有字母、数字或下划线。
const PI = 3.14;
常量不可以通过重新赋值改变其值,也不可以在代码运行时重新声明。它必须被初始化为某个值。
常量的作用域规则与let
块级作用域变量相同。若省略const
关键字,则该标识符将被视为变量。
在同一作用域中,不能使用与变量名或函数名相同的名字来命名常量。例如:
// 这会造成错误
function f() {};
const f = 5;
// 这也会造成错误
function f() {
const g = 5;
var g;
//语句
}
然而,对象属性被赋值为常量是不受保护的,所以下面的语句执行时不会产生错误。
const MY_OBJECT = {"key": "value"};
MY_OBJECT.key = "otherValue";
同样的,数组的被定义为常量也是不受保护的,所以下面的语句执行时也不会产生错误。
const MY_ARRAY = ['HTML','CSS'];
MY_ARRAY.push('JAVASCRIPT');
console.log(MY_ARRAY); //logs ['HTML','CSS','JAVASCRIPT'];
数据类型
最新的 ECMAScript 标准定义了8种数据类型:
七种基本数据类型:
- 布尔值(Boolean),有2个值分别是:true 和 false.
null
, 一个表明 null 值的特殊关键字。 JavaScript 是大小写敏感的,因此 null 与 Null、NULL或变体完全不同。undefined
,和 null 一样是一个特殊的关键字,undefined 表示变量未赋值时的属性。- 数字(Number),整数或浮点数,例如: 42 或者 3.14159。
- 任意精度的整数 (BigInt) ,可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。
- 字符串(String),字符串是一串表示文本值的字符序列,例如:“Howdy” 。
- 代表(Symbol) ( 在 ECMAScript 6 中新添加的类型).。一种实例是唯一且不可改变的数据类型。
- 以及对象(Object)。
虽然这些数据类型相对来说比较少,但是通过他们你可以在程序中开发有用的功能。对象(Objects)和函数(functions)是这门语言的另外两个基本元素。你可以把对象当作存放值的一个命名容器,然后将函数当作你的程序能够执行的步骤。
数据类型的转换
JavaScript是一种动态类型语言(dynamically typed language)。这意味着你在声明变量时可以不必指定数据类型,而数据类型会在代码执行时会根据需要自动转换。因此,你可以按照如下方式来定义变量:
var answer = 42;
然后,你还可以给同一个变量赋予一个字符串值,例如:
answer = "Thanks for all the fish...";
因为 JavaScript 是动态类型的,这种赋值方式并不会提示出错。
在包含的数字和字符串的表达式中使用加法运算符(+),JavaScript 会把数字转换成字符串。例如,观察以下语句:
x = "The answer is " + 42 // "The answer is 42"
y = 42 + " is the answer" // "42 is the answer"
在涉及其它运算符(译注:如下面的减号’-’)时,JavaScript语言不会把数字变为字符串。例如(译注:第一例是数学运算,第二例是字符串运算):
"37" - 7 // 30
"37" + 7 // "377"
字符串转换为数字
有一些方法可以将内存中表示一个数字的字符串转换为对应的数字。
parseInt
方法只能返回整数,所以使用它会丢失小数部分。另外,调用parseInt
时最好总是带上进制(radix
) 参数,这个参数用于指定使用哪一种进制。
将字符串转换为数字的另一种方法是使用一元加法运算符。
"1.1" + "1.1" = "1.11.1"
(+"1.1") + (+"1.1") = 2.2
// 注意:加入括号为清楚起见,不是必需的。
字面量 (Literals)
(译注:字面量是由语法表达式定义的常量;或,通过由一定字词组成的语词表达式定义的常量)
在JavaScript中,你可以使用各种字面量。这些字面量是脚本中按字面意思给出的固定的值,而不是变量。(译注:字面量是常量,其值是固定的,而且在程序脚本运行中不可更改,比如false,3.1415,thisIsStringOfHelloworld ,invokedFunction: myFunction(“myArgument”)。
数组字面量(Array literals)
数组字面值是一个封闭在方括号对([])中的包含有零个或多个表达式的列表,其中每个表达式代表数组的一个元素。当你使用数组字面值创建一个数组时,该数组将会以指定的值作为其元素进行初始化,而其长度被设定为元素的个数。
下面的示例用3个元素生成数组coffees,它的长度是3。
var coffees = ["French Roast", "Colombian", "Kona"];
var a=[3];
console.log(a.length); // 1
console.log(a[0]); // 3
若在顶层(全局)脚本里用字面值创建数组,JavaScript语言将会在每次对包含该数组字面值的表达式求值时解释该数组。另一方面,在函数中使用的数组,将在每次调用函数时都会被创建一次。
数组字面值同时也是数组对象。
数组字面值中的多余逗号
(译注:声明时)你不必列举数组字面值中的所有元素。若你在同一行中连写两个逗号(,),数组中就会产生一个没有被指定的元素,其初始值是undefined。以下示例创建了一个名为fish的数组:
var fish = ["Lion", , "Angel"];
在这个数组中,有两个已被赋值的元素,和一个空元素(fish[0]是"Lion",fish[1]是undefined,而fish[2]是"Angel";译注:此时数组的长度属性fish.length是3)。
如果你在元素列表的尾部添加了一个逗号,它将会被忽略。在下面的例子中,数组的长度是3,并不存在myList[3]这个元素(译注:这是指数组的第4个元素噢,作者是在帮大家复习数组元素的排序命名方法)。元素列表中其它所有的逗号都表示一个新元素的开始。
理解多余的逗号(在脚本运行时会被如何处理)的含义,对于从语言层面理解JavaScript是十分重要的。但是,在你自己写代码时:显式地将缺失的元素声明为undefined
,将大大提高你的代码的清晰度和可维护性。
布尔字面量(Boolean literals)
(译注:即逻辑字面量)
布尔类型有两种字面量:true和false。
不要混淆作为布尔对象的真和假与布尔类型的原始值true和false。布尔对象是原始布尔数据类型的一个包装器。参见布尔对象。
整数(Integers)
整数可以用十进制(基数为10)、十六进制(基数为16)、八进制(基数为8)以及二进制(基数为2)表示。
- 十进制整数字面量由一串数字序列组成,且没有前缀0。
- 八进制的整数以 0(或0O、0o)开头,只能包括数字0-7。
- 十六进制整数以0x(或0X)开头,可以包含数字(0-9)和字母 a~f 或 A~F。
- 二进制整数以0b(或0B)开头,只能包含数字0和1。
严格模式下,八进制整数字面量必须以0o或0O开头,而不能以0开头。
整数字面量举例:
0, 117 and -345 (十进制, 基数为10)
015, 0001 and -0o77 (八进制, 基数为8)
0x1123, 0x00111 and -0xF1A7 (十六进制, 基数为16或"hex")
0b11, 0b0011 and -0b11 (二进制, 基数为2)
浮点数字面量(Floating-point literals)
浮点数字面值可以有以下的组成部分:
- 一个十进制整数,可以带正负号(即前缀“+”或“ - ”),
- 小数点(“.”),
- 小数部分(由一串十进制数表示),
- 指数部分。
指数部分以“e”或“E”开头,后面跟着一个整数,可以有正负号(即前缀“+”或“-”)。浮点数字面量至少有一位数字,而且必须带小数点或者“e”(大写“E”也可)。
简言之,其语法是:
[(+|-)][digits][.digits][(E|e)[(+|-)]digits]
例如:
3.14
-.2345789 // -0.23456789
-3.12e+12 // -3.12*10^12
.1e-23 // 0.1*10^(-23)=10^(-24)=1e-24
对象字面量(Object literals)
对象字面值是封闭在花括号对({})中的一个对象的零个或多个"属性名-值"对的(元素)列表。你不能在一条语句的开头就使用对象字面值,这将导致错误或产生超出预料的行为, 因为此时左花括号({)会被认为是一个语句块的起始符号。(译者:这里需要对语句statement、块block等基本名词的解释)
以下是一个对象字面值的例子。对象car的第一个元素(译注:即一个属性/值对)定义了属性myCar;第二个元素,属性getCar,引用了一个函数(即CarTypes(“Honda”));第三个元素,属性special,使用了一个已有的变量(即Sales)。
var Sales = "Toyota";
function CarTypes(name) {
return (name === "Honda") ?
name :
"Sorry, we don't sell " + name + "." ;
}
var car = { myCar: "Saturn", getCar: CarTypes("Honda"), special: Sales };
console.log(car.myCar); // Saturn
console.log(car.getCar); // Honda
console.log(car.special); // Toyota
更进一步的,你可以使用数字或字符串字面值作为属性的名字,或者在另一个字面值内嵌套上一个字面值。如下的示例中使用了这些可选项。
var car = { manyCars: {a: "Saab", "b": "Jeep"}, 7: "Mazda" };
console.log(car.manyCars.b); // Jeep
console.log(car[7]); // Mazda
对象属性名字可以是任意字符串,包括空串。如果对象属性名字不是合法的javascript标识符,它必须用"“包裹。属性的名字不合法,那么便不能用.访问属性值,而是通过类数组标记(”[]")访问和赋值。
var unusualPropertyNames = {
"": "An empty string",
"!": "Bang!"
}
console.log(unusualPropertyNames.""); // 语法错误: Unexpected string
console.log(unusualPropertyNames[""]); // An empty string
console.log(unusualPropertyNames.!); // 语法错误: Unexpected token !
console.log(unusualPropertyNames["!"]); // Bang!
增强的对象字面量 (Enhanced Object literals)
在ES2015,对象字面值扩展支持在创建时设置原型,简写了 foo: foo 形式的属性赋值,方法定义,支持父方法调用,以及使用表达式动态计算属性名。总之,这些也使对象字面值和类声明更加紧密地联系起来,让基于对象的设计从这些便利中更加受益。
var obj = {
// __proto__
__proto__: theProtoObj,
// Shorthand for ‘handler: handler’
handler,
// Methods
toString() {
// Super calls
return "d " + super.toString();
},
// Computed (dynamic) property names
[ 'prop_' + (() => 42)() ]: 42
};
请注意:
var foo = {a: "alpha", 2: "two"};
console.log(foo.a); // alpha
console.log(foo[2]); // two
//console.log(foo.2); // SyntaxError: missing ) after argument list
//console.log(foo[a]); // ReferenceError: a is not defined
console.log(foo["a"]); // alpha
console.log(foo["2"]); // two
RegExp literals
一个正则表达式是字符被斜线(译注:正斜杠“/”)围成的表达式。下面是一个正则表达式文字的一个例子。
var re = /ab+c/;
字符串字面量(String literals)
字符串字面量是由双引号(")对或单引号(’)括起来的零个或多个字符。字符串被限定在同种引号之间;也即,必须是成对单引号或成对双引号。下面的例子都是字符串字面值:
"foo"
'bar'
"1234"
"one line \n another line"
"John's cat"
你可以在字符串字面值上使用字符串对象的所有方法——JavaScript会自动将字符串字面值转换为一个临时字符串对象,调用该方法,然后废弃掉那个临时的字符串对象。你也能用对字符串字面值使用类似String.length的属性:
console.log("John's cat".length)
// 将打印字符串中的字符个数(包括空格)
// 结果为:10
在ES2015中,还提供了一种模板字面量(template literals),模板字符串提供了一些语法糖来帮你构造字符串。这与Perl、Python还有其他语言中的字符串插值(string interpolation)的特性非常相似。除此之外,你可以在通过模板字符串前添加一个tag来自定义模板字符串的解析过程,这可以用来防止注入攻击,或者用来建立基于字符串的高级数据抽象。
// Basic literal string creation
`In JavaScript '\n' is a line-feed.`
// Multiline strings
`In JavaScript this is
not legal.`
// String interpolation
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// Construct an HTTP request prefix is used to interpret the replacements and construction
POST`http://foo.org/bar?a=${a}&b=${b}
Content-Type: application/json
X-Credentials: ${credentials}
{ "foo": ${foo},
"bar": ${bar}}`(myOnReadyStateChangeHandler);
控制流和错误处理
语句块
最基本的语句是用于组合语句的语句块。该块由一对大括号界定:
{
statement_1;
statement_2;
statement_3;
.
.
.
statement_n;
}
if…else
if (condition_1) {
statement_1;
}else if (condition_2) {
statement_2;
}else if (condition_n_1) {
statement_n;
}else {
statement_last;
}
错误的值
下面这些值将被计算出false
:
- false
- undefined
- null
- 0
- NaN
- 空字符串("")
当传递给条件语句所有其他的值,包括所有对象会被计算为真。
请不要混淆原始的布尔值true和false 与 Boolean对象的真和假。例如:
var b = new Boolean(false);
if (b) //结果视为真
if (b == true) // 结果视为假
switch
switch 语句允许一个程序求一个表达式的值并且尝试去匹配表达式的值到一个 case 标签。如果匹配成功,这个程序执行相关的语句。switch 语句如下所示:
switch (expression) {
case label_1:
statements_1
[break;]
case label_2:
statements_2
[break;]
...
default:
statements_def
[break;]
}
JavaScript的switch语句判断变量可以为string,例如:
switch (fruittype) {
case "Oranges":
document.write("Oranges are $0.59 a pound.<br>");
break;
case "Apples":
document.write("Apples are $0.32 a pound.<br>");
break;
case "Bananas":
document.write("Bananas are $0.48 a pound.<br>");
break;
case "Cherries":
document.write("Cherries are $3.00 a pound.<br>");
break;
case "Mangoes":
case "Papayas":
document.write("Mangoes and papayas are $2.79 a pound.<br>");
break;
default:
document.write("Sorry, we are out of " + fruittype + ".<br>");
}
document.write("Is there anything else you'd like?<br>");
throw
使用throw语句抛出一个异常。当你抛出异常,你规定一个含有值的表达式要被抛出。
throw expression;
你可以抛出任意表达式而不是特定一种类型的表达式。下面的代码抛出了几个不同类型的表达式:
throw "Error2"; // String type
throw 42; // Number type
throw true; // Boolean type
throw {toString: function() { return "I'm an object!"; } };
try…catch…finally
try...catch
语句标记一块待尝试的语句,并规定一个以上的响应应该有一个异常被抛出。如果我们抛出一个异常,try…catch语句就捕获它。
下面的例子使用了try…catch语句。示例调用了一个函数用于从一个数组中根据传递值来获取一个月份名称。如果该值与月份数值不相符,会抛出一个带有"InvalidMonthNo"值的异常,然后在捕捉块语句中设monthName变量为unknown。
function getMonthName(mo) {
mo = mo - 1; // Adjust month number for array index (1 = Jan, 12 = Dec)
var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul",
"Aug","Sep","Oct","Nov","Dec"];
if (months[mo]) {
return months[mo];
} else {
throw "InvalidMonthNo"; //throw keyword is used here
}
}
try { // statements to try
monthName = getMonthName(myMonth); // function could throw exception
}
catch (e) {
monthName = "unknown";
logMyErrors(e); // pass exception object to error handler -> your own function
}
finally
块包含了在try和catch块完成后,下面接着try…catch的语句之前执行的语句。finally块无论是否抛出异常都会执行。如果抛出了一个异常,就算没有异常处理,finally块里的语句也会执行。
你可以用finally块来令你的脚本在异常发生时优雅地退出;举个例子,你可能需要在绑定的脚本中释放资源。接下来的例子用文件处理语句打开了一个文件(服务端的JavaScript允许你进入文件)。如果在文件打开时一个异常抛出,finally块会在脚本错误之前关闭文件。
openMyFile();
try {
writeMyFile(theData); //This may throw a error
}catch(e){
handleError(e); // If we got a error we handle it
}finally {
closeMyFile(); // always close the resource
}
使用Error对象
根据错误类型,你也许可以用’name’和’message’获取更精炼的信息。'name’提供了常规的错误类(如 ‘DOMException’ 或 ‘Error’),而’message’通常提供了一条从错误对象转换成字符串的简明信息。
function doSomethingErrorProne () {
if (ourCodeMakesAMistake()) {
throw (new Error('The message'));
} else {
doSomethingToGetAJavascriptError();
}
}
....
try {
doSomethingErrorProne();
}
catch (e) {
console.log(e.name); // logs 'Error'
console.log(e.message); // logs 'The message' or a JavaScript error message)
}
循环和迭代
循环提供了一种快速和简单的方式去做一些重复的事。
for
<form name="selectForm">
<p>
<label for="musicTypes">Choose some music types, then click the button below:</label>
<select id="musicTypes" name="musicTypes" multiple="multiple">
<option selected="selected">R&B</option>
<option>爵士</option>
<option>布鲁斯</option>
<option>新纪元</option>
<option>古典</option>
<option>歌剧</option>
</select>
</p>
<p><input id="btn" type="button" value="选择了多少个选项?" /></p>
</form>
<script>
function howMany(selectObject) {
var numberSelected = 0;
for (var i = 0; i < selectObject.options.length; i++) {
if (selectObject.options[i].selected) {
numberSelected++;
}
}
return numberSelected;
}
var btn = document.getElementById("btn");
btn.addEventListener("click", function(){
alert('选择选项的数量是: ' + howMany(document.selectForm.musicTypes))
});
</script>
do…while
var i = 0;
do {
i += 1;
console.log(i);
} while (i < 5);
while
var n = 0;
var x = 0;
while (n < 3) {
n++;
x += n;
}
label
一个label
提供了一个让你在程序中其他位置引用它的标识符。例如,你可以用label
标识一个循环,然后使用break
或者continue
来指出程序是否该停止循环还是继续循环。
label 语句的语法看起来像这样:
label :
statement
label
的值可以是任何的非保留字的JavaScript标识符,statement
可以是任意你想要标识的语句(块)。
在如下例子里,标记 markLoop 标识了一个 while 循环。
markLoop:
while (theMark == true) {
doSomething();
}
举一个比较典型的例子,看完后即明白 Label 的应用:
-
未添加
label
:var num = 0; for (var i = 0 ; i < 10 ; i++) { // i 循环 for (var j = 0 ; j < 10 ; j++) { // j 循环 if( i == 5 && j == 5 ) { break; // i = 5,j = 5 时,会跳出 j 循环 } // 但 i 循环会继续执行,等于跳出之后又继续执行更多次 j 循环 num++; } } alert(num); // 输出 95
-
添加
label
后:var num = 0; outPoint: for (var i = 0 ; i < 10 ; i++){ for (var j = 0 ; j < 10 ; j++){ if( i == 5 && j == 5 ){ break outPoint; // 在 i = 5,j = 5 时,跳出所有循环, // 返回到整个 outPoint 下方,继续执行 } num++; } } alert(num); // 输出 55
使用continue
语句,则可达到与未添加label
相同的效果,但在这种有多层循环的情况下,循环的跳出进入流程更为明晰一些:
var num = 0;
outPoint:
for(var i = 0; i < 10; i++) {
for(var j = 0; j < 10; j++) {
if(i == 5 && j == 5) {
continue outPoint;
}
num++;
}
}
alert(num); // 95
从alert(num)
的值可以看出,continue outPoint;
语句的作用是跳出当前循环,并跳转到outPoint(标签)下的for
循环继续执行。
break
使用break
语句来终止循环,switch
,或者是链接到label
语句。
-
当你使用不带
label
的 break 时,它会立即终止当前所在的while
,do-while
,for
,或者switch
并把控制权交回这些结构后面的语句。 -
当你使用带
label
的break
时,它会终止指定的带标记(label)的语句。
break
语句的语法看起来像这样:
break [label];
在语法中,被[]
包裹的内容是可省略的,也就是label
可以省略。若省略,则终止当前所在的循环或switch
;若不省略,则终止指定的label
语句。
continue
continue
语句可以用来继续执行(跳过代码块的剩余部分并进入下一循环)一个while
、do-while
、for
,或者label
语句。
-
当你使用不带
label
的continue
时,它终止当前while
,do-while
,或者for
语句到结尾的这次的循环并且继续执行下一次循环。 -
当你使用带
label
的continue
时, 它会应用被label
标识的循环语句。
continue 语句的语法看起来像这样:
continue [label];
for…in – 遍历索引
for...in
语句循环一个指定的变量来循环一个对象所有可枚举的属性。JavaScript会为每一个不同的属性执行指定的语句。
for (variable in object) {
statements
}
例子:
下面的函数通过它的参数得到一个对象和这个对象的名字。然后循环这个对象的所有属性并且返回一个列出属性名和该属性值的字符串。
function dump_props(obj, obj_name) {
var result = "";
for (var i in obj) {
result += obj_name + "." + i + " = " + obj[i] + "<br>";
}
result += "<hr>";
return result;
}
对于一个拥有 make 和 model 属性的 car 对象来说,执行结果 result 是:
car.make = Ford
car.model = Mustang
for…of – 遍历值
for...of
语句在可迭代对象(包括Array、Map、Set、arguments 等等)上创建了一个循环,对值的每一个独特属性调用一次迭代。
for (variable of object) {
statement
}
下面的这个例子展示了for...of
和for...in
两种循环语句之间的区别。for...in
循环遍历的结果是数组元素的下标,而for...of
遍历的结果是元素的值:
let arr = [3, 5, 7];
arr.foo = "hello";
for (let i in arr) {
console.log(i); // 输出 "0", "1", "2", "foo"
}
for (let i of arr) {
console.log(i); // 输出 "3", "5", "7"
}
// 注意 for...of 的输出没有出现 "hello"
// 译者:官方文档不知为何在此使用三个空格来缩进…
函数
定义函数
函数声明
一个函数定义(也称为函数声明,或函数语句)由一系列的function关键字组成,依次为:
- 函数的名称。
- 函数参数列表,包围在括号中并由逗号分隔。
- 定义函数的 JavaScript 语句,用大括号{}括起来。
例如,以下的代码定义了一个简单的square函数:
function square(number) {
return number * number;
}
函数表达式
虽然上面的函数声明在语法上是一个语句,但函数也可以由函数表达式创建。这样的函数可以是匿名的;它不必有一个名称。例如,函数square也可这样来定义:
const square = function(number) { return number * number; };
var x = square(4); // x gets the value 16
调用函数
定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。例如,一旦你定义了函数square,你可以如下这样调用它:
square(5);
函数作用域
在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。换言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。
// 下面的变量定义在全局作用域(global scope)中
var num1 = 20,
num2 = 3,
name = "Chamahk";
// 本函数定义在全局作用域
function multiply() {
return num1 * num2;
}
multiply(); // 返回 60
// 嵌套函数的例子
function getScore() {
var num1 = 2,
num2 = 3;
function add() {
return name + " scored " + (num1 + num2);
}
return add();
}
getScore(); // 返回 "Chamahk scored 5"
作用域和函数堆栈
递归
有些算法并不能简单的用迭代来实现。例如,获取树结构中所有的节点时,使用递归实现要容易得多:
function walkTree(node) {
if (node == null) //
return;
// do something with node
for (var i = 0; i < node.childNodes.length; i++) {
walkTree(node.childNodes[i]);
}
}
跟loop函数相比,这里每个递归调用都产生了更多的递归。
将递归算法转换为非递归算法是可能的,不过逻辑上通常会更加复杂,而且需要使用堆栈。事实上,递归函数就使用了堆栈:函数堆栈。
这种类似堆栈的行为可以在下例中看到:
function foo(i) {
if (i < 0)
return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(3);
// 输出:
// begin:3
// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2
// end:3
嵌套函数和闭包
你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数)。
既然嵌套函数是一个闭包,就意味着一个嵌套函数可以”继承“容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。
可以总结如下:
- 内部函数只可以在外部函数中访问。
- 内部函数形成了一个闭包:它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。
- 下面的例子展示了嵌套函数:
function addSquares(a, b) {
function square(x) {
return x * x;
}
return square(a) + square(b);
}
a = addSquares(2, 3); // returns 13
b = addSquares(3, 4); // returns 25
c = addSquares(4, 5); // returns 41
由于内部函数形成了闭包,因此你可以调用外部函数并为外部函数和内部函数指定参数:
function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // 可以这样想:给一个函数,使它的值加3
result = fn_inside(5); // returns 8
result1 = outside(3)(5); // returns 8
保存变量
注意到上例中 inside 被返回时 x 是怎么被保留下来的。一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的 inside 没有再被引用时,内存才会被释放。
这与在其他对象中存储引用没什么不同,但是通常不太明显,因为并不能直接设置引用,也不能检查它们。
多层嵌套函数
函数可以被多层嵌套。例如,函数A可以包含函数B,函数B可以再包含函数C。B和C都形成了闭包,所以B可以访问A,C可以访问B和A。因此,闭包可以包含多个作用域;他们递归式的包含了所有包含它的函数作用域。这个称之为作用域链。(稍后会详细解释)
思考一下下面的例子:
function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z);
}
C(3);
}
B(2);
}
A(1); // logs 6 (1 + 2 + 3)
在这个例子里面,C可以访问B的y和A的x。这是因为:
- B形成了一个包含A的闭包,B可以访问A的参数和变量
- C形成了一个包含B的闭包
- B包含A,所以C也包含A,C可以访问B和A的参数和变量。换言之,C用这个顺序链接了B和A的作用域
反过来却不是这样。A不能访问C,因为A看不到B中的参数和变量,C是B中的一个变量,所以C是B私有的。
命名冲突
当同一个闭包作用域下两个参数或者变量同名时,就会产生命名冲突。更近的作用域有更高的优先权,所以最近的优先级最高,最远的优先级最低。这就是作用域链。链的第一个元素就是最里面的作用域,最后一个元素便是最外层的作用域。
看以下的例子:
function outside() {
var x = 5;
function inside(x) {
return x * 2;
}
return inside;
}
outside()(10); // returns 20 instead of 10
命名冲突发生在return x上,inside的参数x和outside变量x发生了冲突。这里的作用链域是{inside, outside, 全局对象}。因此inside的x具有最高优先权,返回了20(inside的x)而不是10(outside的x)。
闭包
闭包是 JavaScript 中最强大的特性之一。JavaScript 允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。
但是,外部函数却不能够访问定义在内部函数中的变量和函数。这给内部函数的变量提供了一定的安全性。
此外,由于内部函数可以访问外部函数的作用域,因此当内部函数生存周期大于外部函数时,外部函数中定义的变量和函数的生存周期将比内部函数执行时间长。当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。
var pet = function(name) { //外部函数定义了一个变量"name"
var getName = function() {
//内部函数可以访问 外部函数定义的"name"
return name;
}
//返回这个内部函数,从而将其暴露在外部函数作用域
return getName;
};
myPet = pet("Vivie");
myPet(); // 返回结果 "Vivie"
实际上可能会比上面的代码复杂的多。在下面这种情形中,返回了一个包含可以操作外部函数的内部变量方法的对象。
var createPet = function(name) {
var sex;
return {
setName: function(newName) {
name = newName;
},
getName: function() {
return name;
},
getSex: function() {
return sex;
},
setSex: function(newSex) {
if(typeof newSex == "string"
&& (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) {
sex = newSex;
}
}
}
}
var pet = createPet("Vivie");
pet.getName(); // Vivie
pet.setName("Oliver");
pet.setSex("male");
pet.getSex(); // male
pet.getName(); // Oliver
在上面的代码中,外部函数的name变量对内嵌函数来说是可取得的,而除了通过内嵌函数本身,没有其它任何方法可以取得内嵌的变量。内嵌函数的内嵌变量就像内嵌函数的保险柜。它们会为内嵌函数保留“稳定”——而又安全——的数据参与运行。而这些内嵌函数甚至不会被分配给一个变量,或者不必一定要有名字。
var getCode = (function(){
var secureCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify...
return function () {
return secureCode;
};
})();
getCode(); // Returns the secret code
尽管有上述优点,使用闭包时仍然要小心避免一些陷阱。如果一个闭包的函数定义了一个和外部函数的某个变量名称相同的变量,那么这个闭包将无法引用外部函数的这个变量。
var createPet = function(name) { // Outer function defines a variable called "name" return { setName: function(name) { // Enclosed function also defines a variable called "name" name = name; // ??? How do we access the "name" defined by the outer > function ??? } } }
使用arguments对象
函数的实际参数会被保存在一个类似数组的arguments对象中。在函数内,你可以按如下方式找出传入的参数:
arguments[i]
其中i是参数的序数编号(译注:数组索引),以0开始。所以第一个传来的参数会是arguments[0]。参数的数量由arguments.length表示。
使用arguments对象,你可以处理比声明的更多的参数来调用函数。这在你事先不知道会需要将多少参数传递给函数时十分有用。你可以用arguments.length来获得实际传递给函数的参数的数量,然后用arguments对象来取得每个参数。
例如,设想有一个用来连接字符串的函数。唯一事先确定的参数是在连接后的字符串中用来分隔各个连接部分的字符(译注:比如例子里的分号“;”)。该函数定义如下:
function myConcat(separator) {
var result = ''; // 把值初始化成一个字符串,这样就可以用来保存字符串了!!
var i;
// iterate through arguments
for (i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}
你可以给这个函数传递任意数量的参数,它会将各个参数连接成一个字符串“列表”:
// returns "red, orange, blue, "
myConcat(", ", "red", "orange", "blue");
// returns "elephant; giraffe; lion; cheetah; "
myConcat("; ", "elephant", "giraffe", "lion", "cheetah");
// returns "sage. basil. oregano. pepper. parsley. "
myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");
arguments变量只是 ”类数组对象“,并不是一个数组。称其为类数组对象是说它有一个索引编号和length属性。尽管如此,它并不拥有全部的Array对象的操作方法。
函数参数
从ECMAScript 6开始,有两个新的类型的参数:默认参数,剩余参数。
默认参数
在JavaScript中,函数参数的默认值是undefined。然而,在某些情况下设置不同的默认值是有用的。这时默认参数可以提供帮助。
在过去,用于设定默认参数的一般策略是在函数的主体中测试参数值是否为undefined,如果是则赋予这个参数一个默认值。如果在下面的例子中,调用函数时没有实参传递给b,那么它的值就是undefined,于是计算a*b得到、函数返回的是 NaN。但是,在下面的例子中,这个已经被第二行获取处理:
function multiply(a, b) {
b = (typeof b !== 'undefined') ? b : 1;
return a*b;
}
multiply(5); // 5
使用默认参数,在函数体的检查就不再需要了。现在,你可以在函数头简单地把1设定为b的默认值:
function multiply(a, b = 1) {
return a*b;
}
multiply(5); // 5
剩余参数
剩余参数语法允许将不确定数量的参数表示为数组。在下面的例子中,使用剩余参数收集从第二个到最后参数。然后,我们将这个数组的每一个数与第一个参数相乘。这个例子是使用了一个箭头函数,这将在下一节介绍。
function multiply(multiplier, ...theArgs) {
return theArgs.map(x => multiplier * x);
}
var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]
箭头函数
箭头函数表达式(也称胖箭头函数)相比函数表达式具有较短的语法并以词法的方式绑定 this。箭头函数总是匿名的。
有两个因素会影响引入箭头函数:更简洁的函数和 this。
更简洁的函数
在一些函数模式中,更简洁的函数很受欢迎。对比一下:
var a = [
"Hydrogen",
"Helium",
"Lithium",
"Beryllium"
];
var a2 = a.map(function(s){ return s.length });
console.log(a2); // logs [ 8, 6, 7, 9 ]
var a3 = a.map( s => s.length );
console.log(a3); // logs [ 8, 6, 7, 9 ]
this
的词法
在箭头函数出现之前,每一个新函数都重新定义了自己的 this 值(在构造函数中是一个新的对象;在严格模式下是未定义的;在作为“对象方法”调用的函数中指向这个对象;等等)。以面向对象的编程风格,这样着实有点恼人。
function Person() {
// 构造函数Person()将`this`定义为自身
this.age = 0;
setInterval(function growUp() {
// 在非严格模式下,growUp()函数将`this`定义为“全局对象”,
// 这与Person()定义的`this`不同,
// 所以下面的语句不会起到预期的效果。
this.age++;
}, 1000);
}
var p = new Person();
在ECMAScript 3/5里,通过把this的值赋值给一个变量可以修复这个问题。
function Person() {
var self = this; // 有的人习惯用`that`而不是`self`,
// 无论你选择哪一种方式,请保持前后代码的一致性
self.age = 0;
setInterval(function growUp() {
// 以下语句可以实现预期的功能
self.age++;
}, 1000);
}
另外,创建一个约束函数可以使得 this值被正确传递给 growUp() 函数。
箭头函数捕捉闭包上下文的this值,所以下面的代码工作正常。
function Person(){
this.age = 0;
setInterval(() => {
this.age++; // 这里的`this`正确地指向person对象
}, 1000);
}
var p = new Person();
普通函数与箭头函数的区别
普通函数 | 箭头函数 | |
---|---|---|
this | 默认指向window,严格模式下指向undefined | 与上一级作用域中的this指向同一个地方 |
arguments | 可以通过arguments来实现重载 | 没有arguments,代替它功能是剩余参数rest(…) |
原型对象 | 是有自己的原型对象 | 没有原型对象 |
new | 可以作为构造函数,通过new实例化出子函数 | 不能作为构造函数,使用new会报错 |
简易程度 | function add(a,b){return a+b}; | (a,b)=>a+b; |
预定义函数
表达式和运算符
赋值运算符(Assignment Operators)
名字 | 简写的操作符 | 含义 |
---|---|---|
赋值(Assignment) | x = y | x = y |
加法赋值(Addition assignment) | x += y | x = x + y |
减法赋值(Subtraction assignment) | x -= y | x = x - y |
乘法赋值(Multiplication assignment) | x *= y | x = x * y |
除法赋值(Division assignment) | x /= y | x = x / y |
求余赋值(Remainder assignment) | x %= y | x = x % y |
求幂赋值(Exponentiation assignment) | x **= y | x = x ** y |
左移位赋值(Left shift assignment) | x <<= y | x = x << y |
右移位赋值(Right shift assignment) | x >>= y | x = x >> y |
无符号右移位赋值(Unsigned right shift assignment) | x >>>= y | x = x >>> y |
按位与赋值(Bitwise AND assignment) | x &= y | x = x & y |
按位异或赋值(Bitwise XOR assignment) | x ^= y | x = x ^ y |
按位或赋值(Bitwise OR assignment) | x |= y | x = x | y |
解构
对于更复杂的赋值,解构赋值语法是一个能从数组或对象对应的数组结构或对象字面量里提取数据的
var foo = ["one", "two", "three"];
// 不使用解构
var one = foo[0];
var two = foo[1];
var three = foo[2];
// 使用解构
var [one, two, three] = foo;
比较运算符(Comparison Operators)
运算符 | 描述 | 返回true的示例 |
---|---|---|
等于 Equal (== ) | 如果两边操作数相等时返回true。 | 3 == var1
3 == '3' |
不等于 Not equal (!= ) | 如果两边操作数不相等时返回true | var1 != 4 |
全等 Strict equal (=== ) | 两边操作数相等且类型相同时返回true。 参见 sameness in JS. | 3 === var1 |
不全等 Strict not equal (!== ) | 两边操作数不相等或类型不同时返回true。 | var1 !== "3" |
大于 Greater than (> ) | 左边的操作数大于右边的操作数返回true | var2 > var1 |
大于等于 Greater than or equal (>= ) | 左边的操作数大于或等于右边的操作数返回true | var2 >= var1 |
小于 Less than (< ) | 左边的操作数小于右边的操作数返回true | var1 < var2 |
小于等于 Less than or equal (<= ) | 左边的操作数小于或等于右边的操作数返回true | var1 <= var2 |
算数运算符(Arithmtic Operators)
除了标准的算术运算符(+, - ,* /),JavaScript还提供了下表中的算术运算符。
Operator | Description | Example |
---|---|---|
求余(% ) | 二元运算符. 返回相除之后的余数. | 12 % 5 返回 2。 |
自增(++ ) | 一元运算符. 将操作数的值加一. 如果放在操作数前面 (++x ), 则返回加一后的值; 如果放在操作数后面 (x++ ), 则返回操作数原值,然后再将操作数加一. |
|
自减(-- ) | 一元运算符. 将操作数的值减一. 前后缀两种用法的返回值类似自增运算符. | var x=3; console.log(--x); //输入2,x=2 var y=3;console.log(y--);//输出3,x=2; |
一元负值符(- ) | 一元运算符,返回操作数的负值. | var x=3; console.log(-x); //输入-3 |
一元正值符(+) | 一元运算符, 如果操作数在之前不是number,试图将其转换为number |
console.log(+true); // 1 |
指数运算符(**) | 计算 base(底数) 的 exponent( 指数)次方 , 表示为baseexponent |
|
位运算符(Bitwise Operators)
Operator | Usage | Description |
---|---|---|
按位与 AND | a & b | 在a,b的位表示中,每一个对应的位都为1则返回1, 否则返回0. |
按位或 OR | a | b | 在a,b的位表示中,每一个对应的位,只要有一个为1则返回1, 否则返回0. |
按位异或 XOR | a ^ b | 在a,b的位表示中,每一个对应的位,两个不相同则返回1,相同则返回0. |
按位非 NOT | ~ a | 反转被操作数的位。 |
左移 shift | a << b | 将a的二进制串向左移动b位,右边移入0. |
算术右移 | a >> b | 把a的二进制表示向右移动b位,丢弃被移出的所有位. (译注:算术右移左边空出的位是根据最高位是0和1来进行填充的) |
无符号右移 (左边空出位用0填充) | a >>> b | 把a的二进制表示向右移动b位,丢弃被移出的所有位,并把左边空出的位都填充为0 |
移位运算符
移位运算符带两个操作数:第一个是待移位的数,第二个是指定第一个数要被移多少位的数。移位的方向由运算符来控制.
运算符 | 描述 | 范例 |
---|---|---|
<< (左移位) | 将第一个操作数向左移动指定数量的位. 左边移出位被抛弃. 左边移出的几位被丢弃.右边多出的空位由0补齐 | 9<<2产生36,因为1001移位2比特向左变为100100,它是36。 |
>> (带符号右移) | 将第一个操作数向右移动指定数量的位. 右边移出位被抛弃. 左边多出的空位由原值的最左边数字补齐. | 9>>2产生2,因为1001移位2位向右变为10,其是2。同样,-9>>2产生-3,由于符号被保留。 |
>>> (补零右移) | 将第一个操作数向右移动指定数量的位. 右边移出位被抛弃. 左边多出的空位由0补齐. | 19>>>2 产生4,因为10011移位2位向右变为100,它是4。对非负数值,补零右移和带符号右移产生相同结果。 |
逻辑运算符(Logical Operators)
运算符 | 范例 | 描述 |
---|---|---|
逻辑与 (&& ) | expr1 && expr2 | (逻辑与) 如果expr1能被转换为false,那么返回expr1;否则,返回expr2 。因此,&& 用于布尔值时,当操作数都为true时返回true;否则返回false. |
逻辑或 (|| ) | expr1 || expr2 | (逻辑或) 如果expr1能被转换为true,那么返回expr1;否则,返回expr2 。因此,||用于布尔值时,当任何一个操作数为true则返回true;如果操作数都是false则返回false。 |
逻辑非 (!) | !expr | (逻辑非) 如果操作数能够转换为true则返回false;否则返回true。 |
字符串运算符(String Operators)
除了比较操作符,它可以在字符串值中使用,连接操作符(+)连接两个字符串值相连接,返回另一个字符串,它是两个操作数串的结合。
例如,
console.log("my " + "string"); // console logs the string "my string".
简写操作符 += 也可以用来拼接字符串,例如:
var myString = "alpha";
myString += "bet"; // 返回 "alphabet"
条件(三元)运算符(Conditional Operator)
条件运算符是JavaScript中唯一需要三个操作数的运算符。运算的结果根据给定条件在两个值中取其一。语法为:
条件 ? 值1 : 值2
如果条件为真,则结果取值1。否则为值2。你能够在任何允许使用标准运算符的地方使用条件运算符。
例如,
var status = (age >= 18) ? "adult" : "minor";
当 age 大于等于18的时候,将“adult”赋值给 status;否则将“minor”赋值给 status。
逗号运算符(Comma Operator)
逗号操作符(,)对两个操作数进行求值并返回最终操作数的值。它常常用在 for 循环中,在每次循环时对多个变量进行更新。
例如,假如 a 是一个二维数组,每个维度各有10个元素,以下代码利用逗号操作符来同时改变两个变量的值。这段代码的功能是打印出该二维数组的对角线元素的值:
var x = [0,1,2,3,4,5,6,7,8,9]
var a = [x, x, x, x, x];
for (var i = 0, j = 9; i <= j; i++, j--)
console.log('a[' + i + '][' + j + ']= ' + a[i][j]);
一元运算符(Unary Operator)
一元操作符仅对应一个操作数。
delete
delete操作符,删除一个对象的属性或者一个数组中某一个键值。语法如下:
delete objectName.property;
delete objectName[index];
delete property; // legal only within a with statement
typeof
typeof
操作符可通过下面2种方式使用:
typeof operand
typeof (operand)
typeof
操作符返回一个表示operand
类型的字符串值。operand
可为字符串、变量、关键词或对象,其类型将被返回。operand
两侧的括号为可选。
假设你定义了如下的变量:
var myFun = new Function("5 + 2");
var shape = "round";
var size = 1;
var today = new Date();
typeof
操作符将会返回如下的结果:
typeof myFun; // returns "function"
typeof shape; // returns "string"
typeof size; // returns "number"
typeof today; // returns "object"
typeof dontExist; // returns "undefined"
void
void
运算符运用方法如下:
void (expression)
void expression
void
运算符,表明一个运算没有返回值。expression
是javaScript表达式,括号中的表达式是一个可选项,当然使用该方式是一种好的形式。
你可以使用void
运算符指明一个超文本链接。该表达式是有效的,但是并不会在当前文档中进行加载。
如下创建了一个超链接文本,当用户单击该文本时,不会有任何效果。
Click here to do nothing
下面的代码创建了一个超链接,当用户单击它时,提交一个表单。
Click here to submit
关系运算符(Relational Operator) – in, instanceof
in
in操作符,如果所指定的属性确实存在于所指定的对象中,则会返回true,语法如下:
propNameOrNumber in objectName
在这里propNameOrNumber
可以是一个代表着属性名的字符串或者是一个代表着数组索引的数值表达式,而objectName
则是一个对象名。
下面的例子是 in 操作的常见用法。
// Arrays
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees; // returns true
3 in trees; // returns true
6 in trees; // returns false
"bay" in trees; // returns false (you must specify the index number,
// not the value at that index)
"length" in trees; // returns true (length is an Array property)
// Predefined objects
"PI" in Math; // returns true
var myString = new String("coral");
"length" in myString; // returns true
// Custom objects
var mycar = {make: "Honda", model: "Accord", year: 1998};
"make" in mycar; // returns true
"model" in mycar; // returns true
instanceof
如果所判别的对象确实是所指定的类型,则返回true。其语法如下:
objectName instanceof objectType
objectName
是需要做判别的对象的名称,而objectType
是假定的对象的类型, 例如Date
或Array
.
当你需要确认一个对象在运行时的类型时,可使用instanceof
. 例如,需要catch
异常时,你可以针对抛出异常的类型,来做不同的异常处理。
例如, 下面的代码使用instanceof
去判断theDay
是否是一个Date
对象. 因为theDay
是一个Date
对象, 所以if
中的代码会执行.
var theDay = new Date(1995, 12, 17);
if (theDay instanceof Date) {
// statements to execute
}
运算符优先级
运算符的优先级,用于确定一个表达式的计算顺序。在你不能确定优先级时,可以通过使用括号显式声明运算符的优先级。
下表列出了描述符的优先级,从最高到最低。
Operator type | Individual operators |
---|---|
member | . [] |
call / create instance | () new |
negation/increment | ! ~ - + ++ -- typeof void delete |
multiply/divide | * / % |
addition/subtraction | + - |
bitwise shift | << >> >>> |
relational | < <= > >= in instanceof |
equality | == != === !== |
bitwise-and | & |
bitwise-xor | ^ |
bitwise-or | | |
logical-and | && |
logical-or | || |
conditional | ?: |
assignment | = += -= *= /= %= <<= >>= >>>= &= ^= |= |
comma | , |
数字对象和日期对象和数学对象
Number对象
在 JavaScript 里面,数字均为双精度浮点类型(double-precision 64-bit binary format IEEE 754),即一个介于±2−1023和±2+1024之间的数字,或约为±10−308到±10+308,数字精度为53位。整数数值仅在±(253 - 1)的范围内可以表示准确。
除了能够表示浮点数,数字类型也还能表示三种符号值: +Infinity(正无穷)、-Infinity(负无穷)和 NaN (not-a-number,非数字)。
JavaScript最近添加了 BigInt 的支持,能够用于表示极大的数字。使用 BigInt 的时候有一些注意事项,例如,你不能让 BigInt 和 Number 直接进行运算,你也不能用 Math 对象去操作 BigInt 数字。
请参见Javascript指南中的JavaScript数据类型和数据结构,了解其他更多的基本类型。
您可以使用四种数字进制:十进制,二进制,八进制和十六进制。
-
十进制数字(Decimal numbers)
1234567890 42 // 以零开头的数字的注意事项: 0888 // 888 将被当做十进制处理 0777 // 在非严格格式下会被当做八进制处理 (用十进制表示就是511)
请注意,十进制可以以0开头,后面接其他十进制数字,但是假如下一个接的十进制数字小于8,那么该数字将会被当做八进制处理。
-
二进制数字(Binary numbers)
二进制数字语法是以零为开头,后面接一个小写或大写的拉丁文字母B(0b或者是0B)。 假如0b后面的数字不是0或者1,那么就会提示这样的语法错误(SyntaxError): “Missing binary digits after 0b(0b之后缺失二有效的二进制数据)”。
var FLT_SIGNBIT = 0b10000000000000000000000000000000; // 2147483648 var FLT_EXPONENT = 0b01111111100000000000000000000000; // 2139095040 var FLT_MANTISSA = 0B00000000011111111111111111111111; // 8388607
-
八进制数字(Octal numbers)
八进制数字语法是以0为开头的。假如0后面的数字不在0到7的范围内,该数字将会被转换成十进制数字。
var n = 0755; // 493 var m = 0644; // 420
在ECMAScript 5 严格模式下禁止使用八进制语法。八进制语法并不是ECMAScript 5规范的一部分,但是通过在八进制数字添加一个前缀0就可以被所有的浏览器支持:
0644 === 420
。在ECMAScript 6中使用八进制数字是需要给一个数字添加前缀"0o"。var a = 0o10; // ES6 :八进制
-
十六进制(Hexadecimal numbers)
十六进制数字语法是以零为开头,后面接一个小写或大写的拉丁文字母X(0x或者是0X)。假如0x后面的数字超出规定范围(0123456789ABCDEF),那么就会提示这样的语法错误(SyntaxError):“Identifier starts immediately after numeric literal”.
0xFFFFFFFFFFFFFFFFF // 295147905179352830000 0x123456789ABCDEF // 81985529216486900 0XA // 10
-
指数形式(Exponentiation)
1E3 // 1000 2e6 // 2000000 0.1e2 // 10
内置的Number
对象有一些有关数字的常量属性,如最大值、不是一个数字和无穷大的。你不能改变这些属性,但可以按下边的方式使用它们:
var biggestNum = Number.MAX_VALUE;
var smallestNum = Number.MIN_VALUE;
var infiniteNum = Number.POSITIVE_INFINITY;
var negInfiniteNum = Number.NEGATIVE_INFINITY;
var notANum = Number.NaN;
属性 | 描述 |
---|---|
Number.MAX_VALUE | 可表示的最大值 |
Number.MIN_VALUE | 可表示的最小值 |
Number.NaN | 特指”非数字“ |
Number.NEGATIVE_INFINITY | 特指“负无穷”;在溢出时返回 |
Number.POSITIVE_INFINITY | 特指“正无穷”;在溢出时返回 |
Number.EPSILON | 表示1和比最接近1且大于1的最小 |
Number.MIN_SAFE_INTEGER | JavaScript最小安全整数. |
Number.MAX_SAFE_INTEGER | JavaScript最大安全整数. |
方法 | 描述 |
---|---|
Number.parseFloat() | 把字符串参数解析成浮点数, 和全局方法 parseFloat() 作用一致. |
Number.parseInt() | 把字符串解析成特定基数对应的整型数字,和全局方法 |
Number.isFinite() | 判断传递的值是否为有限数字。 |
Number.isInteger() | 判断传递的值是否为整数。 |
Number.isNaN() | 判断传递的值是否为 isNaN() . |
Number.isSafeInteger() | 判断传递的值是否为安全整数。 |
方法 | 描述 |
---|---|
toExponential() | 返回一个数字的指数形式的字符串,形如:1.23e+2 |
toFixed() | 返回指定小数位数的表示形式, var a=123,b=a.toFixed(2)//b="123.00" |
toPrecision() | 返回一个指定精度的数字。如下例子中,a=123中,3会由于精度限制消失 var a=123,b=a.toPrecision(2)//b="1.2e+2" |
Math对象
对于内置的Math
数学常项和函数也有一些属性和方法。比方说,Math
对象的PI
属性会有属性值pi (3.141...)
,你可以像这样调用它:
Math.PI // π
同理,标准数学函数也是Math
的方法。这些包括三角函数,对数,指数,和其他函数。比方说你想使用三角函数sin
, 你可以这么写:
Math.sin(1.56)
需要注意的是Math
的所有三角函数参数都是弧度制。
下面的表格总结了Math
对象的方法
方法 | 描述 |
---|---|
abs() | 绝对值 |
tan() | 标准三角函数;参数为弧度 |
atan2() | 反三角函数; 返回值为弧度 |
tanh() | 双曲三角函数; 参数为弧度. |
atanh() | 反双曲三角函数;返回值为弧度. |
| 指数与对数函数 |
ceil() | 返回小于等于参数的最大整数;返回大于等于参数的最小整数 |
max() | 返回一个以逗号间隔的数字参数列表中的较小或较大值(分别地) |
random() | 返回0和1之间的随机数。 |
trunc() , | 四舍五入和截断函数 |
hypot() | 平方根,立方根,所有参数平方和的平方根 两个参数平方和的平方根 |
sign() | 数字的符号, 说明数字是否为正、负、零。 |
clz32() ,imul() | 在32位2进制表示中,开头的0的数量. 返回传入的两个参数相乘结果的类C的32位表现形式 |
和其他对象不同,你不能够创建一个自己的Math
对象。你只能使用内置的Math
对象。
Date对象
JavaScript没有日期数据类型。但是你可以在你的程序里使用Date
对象和其方法来处理日期和时间。Date
对象有大量的设置、获取和操作日期的方法。 它并不含有任何属性。
JavaScript 处理日期数据类似于Java。这两种语言有许多一样的处理日期的方法,也都是以1970年1月1日00:00:00以来的毫秒数来储存数据类型的。
Date
对象的范围是相对距离 UTC 1970年1月1日 的前后 100,000,000 天。
创建一个日期对象:
var dateObjectName = new Date([parameters]);
前边的语法中的参数(parameters)可以是一下任何一种:
- 无参数 : 创建今天的日期和时间,例如:
today = new Date();
. - 一个符合以下格式的表示日期的字符串: “月 日, 年 时:分:秒.” 例如:
var Xmas95 = new Date("December 25, 1995 13:30:00");
。如果你省略时、分、秒,那么他们的值将被设置为0。 - 一个年,月,日的整型值的集合,例如:
var Xmas95 = new Date(1995, 11, 25);
。 - 一个年,月,日,时,分,秒的集合,例如:
var Xmas95 = new Date(1995, 11, 25, 9, 30, 0);
.
Date对象的方法
处理日期时间的Date对象方法可分为以下几类:
-
“set” 方法, 用于设置Date对象的日期和时间的值。
-
“get” 方法,用于获取Date对象的日期和时间的值。
-
“to” 方法,用于返回Date对象的字符串格式的值。
-
parse 和UTC 方法, 用于解析Date字符串。
通过“get”和“set”方法,你可以分别设置和获取秒,分,时,日,星期,月份,年。这里有个getDay方法可以返回星期,但是没有相应的setDay方法用来设置星期,因为星期是自动设置的。这些方法用整数来代表以下这些值: -
秒,分: 0 至 59
-
时: 0 至 23
-
星期: 0 (周日) 至 6 (周六)
-
日期:1 至 31
-
月份: 0 (一月) to 11 (十二月)
-
年份: 从1900开始的年数
例如, 假设你定义了如下日期:
var Xmas95 = new Date("December 25, 1995");
Then Xmas95.getMonth() 返回 11, and Xmas95.getFullYear() 返回 1995.
getTime 和 setTime 方法对于比较日期是非常有用的。getTime方法返回从1970年1月1日00:00:00的毫秒数。
例如,以下代码展示了今年剩下的天数:
var today = new Date();
var endYear = new Date(1995, 11, 31, 23, 59, 59, 999); // 设置日和月,注意,月份是0-11
endYear.setFullYear(today.getFullYear()); // 把年设置为今年
var msPerDay = 24 * 60 * 60 * 1000; // 每天的毫秒数
var daysLeft = (endYear.getTime() - today.getTime()) / msPerDay;
var daysLeft = Math.round(daysLeft); //返回今年剩下的天数
这个例子中,创建了一个包含今天的日期的Date对象,并命名为today,然后创建了一个名为endYear的Date对象,并把年份设置为当前年份,接着使用today和endYear的getTime分别获取今天和年底的毫秒数,再根据每一天的毫秒数,计算出了今天到年底的天数,最后四舍五入得到今年剩下的天数。
parse方法对于从日期字符串赋值给现有的Date对象很有用,例如:以下代码使用parse和setTime分配了一个日期值给IPOdate对象
var IPOdate = new Date();
IPOdate.setTime(Date.parse("Aug 9, 1995"));
例子:
在下边的例子中,JSClock()
函数返回了用数字时钟格式的时间:
function JSClock() {
var time = new Date();
var hour = time.getHours();
var minute = time.getMinutes();
var second = time.getSeconds();
var temp = "" + ((hour > 12) ? hour - 12 : hour);
if (hour == 0)
temp = "12";
temp += ((minute < 10) ? ":0" : ":") + minute;
temp += ((second < 10) ? ":0" : ":") + second;
temp += (hour >= 12) ? " P.M." : " A.M.";
return temp;
}
JSClock函数首先创建了一个叫做time的新的Date对象,因为没有参数,所以time代表了当前日期和时间。然后调用了getHours, getMinutes以及getSeconds方法把当前的时分秒分别赋值给了hour, minute,second。
接下来的4句在time的基础上创建了一个字符串,第一句创建了一个变量temp,并通过一个条件表达式进行了赋值,如果小时大于12,就为 (hour - 12), 其他情况就为 hour, 除非 hour 为 0, 这种情况下,它会变成 12.
接下来的语句拼接了minute的值到temp后。如果minute小于10,条件表达式就会在minute前边加个0,其他情况下加一个分号。然后按同样的方式在temp后拼接上了秒。
最后,如果hour是12或者更大,条件表达式会在temp后拼接"P.M.",否则拼接"A.M." 。
文本格式化
JavaScript中的String
类型用于表示文本型的数据. 它是由无符号整数值(16bit)作为元素而组成的集合. 字符串中的每个元素在字符串中占据一个位置. 第一个元素的index值是0, 下一个元素的index值是1, 以此类推. 字符串的长度就是字符串中所含的元素个数.你可以通过String字面值或者String对象两种方式创建一个字符串。
String字面量及转义字符串
可以使用单引号或双引号创建简单的字符串:
'foo'
"bar"
可以使用转义序列来创建更复杂的字符串:
-
16进制转义序列
\x
之后的数值将被认为是一个16进制数.'\xA9' // "©"
-
Unicode转义序列
Unicode转义序列在\u
之后需要至少4个字符.'\u00A9' // "©"
-
Unicode字元逸出
这是ECMAScript 6中的新特性。有了Unicode字元逸出,任何字符都可以用16进制数转义, 这使得通过Unicode转义表示大于0x10FFFF的字符成为可能。使用简单的Unicode转义时通常需要分别写字符相应的两个部分(译注:大于0x10FFFF的字符需要拆分为相应的两个小于0x10FFFF的部分)来达到同样的效果。'\u{2F804}' // the same with simple Unicode escapes '\uD87E\uDC04'
字符串对象方法
String
对象是对原始string
类型的封装 .
const foo = new String('foo'); // 创建一个 String 对象
console.log(foo); // 输出: [String: 'foo']
typeof foo; // 返回 'object'
你可以在String字面值上使用String对象的任何方法—JavaScript自动把String字面值转换为一个临时的String对象, 然后调用其相应方法,最后丢弃此临时对象.在String字面值上也可以使用String.length属性.
除非必要, 应该尽量使用String
字面值,因为String对象的某些行为可能并不与直觉一致。举例:
const firstString = '2 + 2'; //创建一个字符串字面量
const secondString = new String('2 + 2'); // 创建一个字符串对象
eval(firstString); // 返回数字 4
eval(secondString); // 返回包含 "2 + 2" 的字符串对象
方法 | 描述 |
---|---|
codePointAt | 返回字符串指定位置的字符或者字符编码。 |
lastIndexOf | 分别返回字符串中指定子串的位置或最后位置。 |
includes | 返回字符串是否以指定字符串开始、结束或包含指定字符串。 |
concat | 连接两个字符串并返回新的字符串。 |
fromCodePoint | 从指定的Unicode值序列构造一个字符串。这是一个String类方法,不是实例方法。 |
split | 通过将字符串分离成一个个子串来把一个String对象分裂到一个字符串数组中。 |
slice | 从一个字符串提取片段并作为新字符串返回。 |
substr | 分别通过指定起始和结束位置,起始位置和长度来返回字符串的指定子集。 |
search | 通过正则表达式来工作. |
toUpperCase | 分别返回字符串的小写表示和大写表示。 |
normalize | 按照指定的一种 Unicode 正规形式将当前字符串正规化。 |
repeat | 将字符串内容重复指定次数后返回。 |
trim | 去掉字符串开头和结尾的空白字符。 |
多行模板字符串
-
多行
console.log(`string text line 1 string text line 2`); // "string text line 1 // string text line 2"
-
嵌入表达式
const five = 5; const ten = 10; console.log(`Fifteen is ${five + ten} and not ${2 * five + ten}.`); // "Fifteen is 15 and not 20."
日期和时间国际格式化
DateTimeFormat
对象在日期和时间的格式化方面很有用. 下面的代码把一个日期格式化为美式英语格式. (不同时区结果不同.)
const msPerDay = 24 * 60 * 60 * 1000;
// July 17, 2014 00:00:00 UTC.
const july172014 = new Date(msPerDay * (44 * 365 + 11 + 197));//2014-1970=44年
//这样创建日期真是醉人。。。还要自己计算天数。。。11是闰年中多出的天数。。。
//197是6×30+16(7月的16天)+3(3个大月)-2(2月少2天)
const options = { year: "2-digit", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", timeZoneName: "short" };
const americanDateTime = new Intl.DateTimeFormat("en-US", options).format;
console.log(americanDateTime(july172014)); // 07/16/14, 5:00 PM PDT
数字国际格式化
NumberFormat
对象在数字的格式化方面很有用, 比如货币数量值.
var gasPrice = new Intl.NumberFormat("en-US",
{ style: "currency", currency: "USD",
minimumFractionDigits: 3 });
console.log(gasPrice.format(5.259)); // $5.259
var hanDecimalRMBInChina = new Intl.NumberFormat("zh-CN-u-nu-hanidec",
{ style: "currency", currency: "CNY" });
console.log(hanDecimalRMBInChina.format(1314.25)); // ¥ 一,三一四.二五
Intl定序
Collator
对象在字符串比较和排序方面很有用.
有些德语词包含变音, 所以在字典中忽略变音进行排序是合理的 (除非待排序的单词只有变音部分不同: schon 先于 schön).
var germanDictionary = new Intl.Collator("de-DE-u-co-dict");
// as if sorting ["Hochberg", "Honigswald", "Holzman"]:
console.log(names.sort(germanDictionary.compare).join(", "));
// logs "Hochberg, Holzman, Hönigswald"
关于Intl
API的更多信息, 请参考Introducing the JavaScript Internationalization API
正则表达式
创建一个正则表达式
你可以使用以下两种方法构建一个正则表达式:
使用一个正则表达式字面量,其由包含在斜杠之间的模式组成,如下所示
var re = /ab+c/;
脚本加载后,正则表达式字面量就会被编译。当正则表达式保持不变时,使用此方法可获得更好的性能。
或者调用RegExp
对象的构造函数,如下所示:
var re = new RegExp("ab+c");
在脚本运行过程中,用构造函数创建的正则表达式会被编译。如果正则表达式将会改变,或者它将会从用户输入等来源中动态地产生,就需要使用构造函数来创建正则表达式。
编写一个正则表达式的模式
使用简单模式
简单模式是由你想直接找到的字符构成。比如,/abc/
这个模式就能且仅能匹配"abc"
字符按照顺序同时出现的情况。例如在"Hi, do you know your abc's?"
和"The latest airplane designs evolved from slabcraft."
中会匹配成功。在上述两个例子中,匹配的子字符串是"abc"
。但是在"Grab crab"
中会匹配失败,因为它虽然包含子字符串"ab c"
,但并不是准确的"abc"
。
使用特殊字符
当你需要匹配一个不确定的字符串时,比如寻找一个或多个"b"
,或者寻找空格,可以在模式中使用特殊字符。比如,你可以使用/ab*c/
去匹配一个单独的"a"
后面跟了零个或者多个"b"
,同时后面跟着"c"
的字符串:*
的意思是前一项出现零次或者多次。在字符串"cbbabbbbcdebc"
中,这个模式匹配了子字符串"abbbbc"
。
下面的表格列出了一个正则表达式中可以利用的特殊字符的完整列表和描述。
字符 | 含义 |
---|---|
\ | 依照下列规则匹配: 在非特殊字符之前的反斜杠表示下一个字符是特殊字符,不能按照字面理解。例如,前面没有 "\" 的 "b" 通常匹配小写字母 "b",即字符会被作为字面理解,无论它出现在哪里。但如果前面加了 "\",它将不再匹配任何字符,而是表示一个字符边界。 在特殊字符之前的反斜杠表示下一个字符不是特殊字符,应该按照字面理解。详情请参阅下文中的 "转义(Escaping)" 部分。 如果你想将字符串传递给 RegExp 构造函数,不要忘记在字符串字面量中反斜杠是转义字符。所以为了在模式中添加一个反斜杠,你需要在字符串字面量中转义它。 |
^ | 匹配输入的开始。如果多行标志被设置为 true,那么也匹配换行符后紧跟的位置。 例如, 当 ' |
$ | 匹配输入的结束。如果多行标志被设置为 true,那么也匹配换行符前的位置。 例如, |
* | 匹配前一个表达式 0 次或多次。等价于 例如, |
+ | 匹配前面一个表达式 1 次或者多次。等价于 例如, |
? | 匹配前面一个表达式 0 次或者 1 次。等价于 例如, 如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。例如,对 "123abc" 使用 还用于先行断言中,如本表的 |
. | (小数点)默认匹配除换行符之外的任何单个字符。 例如, 如果 |
(x) | 像下面的例子展示的那样,它会匹配 'x' 并且记住匹配项。其中括号被称为捕获括号。 模式 |
(?:x) | 匹配 'x' 但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。看看这个例子 |
x(?=y) | 匹配'x'仅仅当'x'后面跟着'y'.这种叫做先行断言。 例如,/Jack(?=Sprat)/会匹配到'Jack'仅当它后面跟着'Sprat'。/Jack(?=Sprat|Frost)/匹配‘Jack’仅当它后面跟着'Sprat'或者是‘Frost’。但是‘Sprat’和‘Frost’都不是匹配结果的一部分。 |
(?<=y) x | 匹配'x'仅当'x'前面是'y'.这种叫做后行断言。 例如,/(?<=Jack)Sprat/会匹配到' Sprat '仅仅当它前面是' Jack '。/(?<=Jack|Tom)Sprat/匹配‘ Sprat ’仅仅当它前面是'Jack'或者是‘Tom’。但是‘Jack’和‘Tom’都不是匹配结果的一部分。 |
x(?!y) | 仅仅当'x'后面不跟着'y'时匹配'x',这被称为正向否定查找。 例如,仅仅当这个数字后面没有跟小数点的时候,/\d+(?!\.)/ 匹配一个数字。正则表达式/\d+(?!\.)/.exec("3.141")匹配‘141’而不是‘3.141’ |
(?<!y)x | 仅仅当'x'前面不是'y'时匹配'x',这被称为反向否定查找。 例如, 仅仅当这个数字前面没有负号的时候, |
x|y | 匹配‘x’或者‘y’。 例如,/green|red/匹配“green apple”中的‘green’和“red apple”中的‘red’ |
{n} | n 是一个正整数,匹配了前面一个字符刚好出现了 n 次。 比如, /a{2}/ 不会匹配“candy”中的'a',但是会匹配“caandy”中所有的 a,以及“caaandy”中的前两个'a'。 |
{n,} | n是一个正整数,匹配前一个字符至少出现了n次。 例如, /a{2,}/ 匹配 "aa", "aaaa" 和 "aaaaa" 但是不匹配 "a"。 |
{n,m} | n 和 m 都是整数。匹配前面的字符至少n次,最多m次。如果 n 或者 m 的值是0, 这个值被忽略。 例如,/a{1, 3}/ 并不匹配“cndy”中的任意字符,匹配“candy”中的a,匹配“caandy”中的前两个a,也匹配“caaaaaaandy”中的前三个a。注意,当匹配”caaaaaaandy“时,匹配的值是“aaa”,即使原始的字符串中有更多的a。 |
[xyz] | 一个字符集合。匹配方括号中的任意字符,包括转义序列。你可以使用破折号(-)来指定一个字符范围。对于点(.)和星号(*)这样的特殊符号在一个字符集中没有特殊的意义。他们不必进行转义,不过转义也是起作用的。 例如,[abcd] 和[a-d]是一样的。他们都匹配"brisket"中的‘b’,也都匹配“city”中的‘c’。/[a-z.]+/ 和/[\w.]+/与字符串“test.i.ng”匹配。 |
[^xyz] | 一个反向字符集。也就是说, 它匹配任何没有包含在方括号中的字符。你可以使用破折号(-)来指定一个字符范围。任何普通字符在这里都是起作用的。 例如,[^abc] 和 [^a-c] 是一样的。他们匹配"brisket"中的‘r’,也匹配“chop”中的‘h’。 |
[\b] | 匹配一个退格(U+0008)。(不要和\b混淆了。) |
\b | 匹配一个词的边界。一个词的边界就是一个词不被另外一个“字”字符跟随的位置或者前面跟其他“字”字符的位置,例如在字母和空格之间。注意,匹配中不包括匹配的字边界。换句话说,一个匹配的词的边界的内容的长度是0。(不要和[\b]混淆了) 使用"moon"举例: 注意: JavaScript的正则表达式引擎将é”或“ü”,被视为断词。 |
\B | 匹配一个非单词边界。匹配如下几种情况:
例如,/\B../匹配"noonday"中的'oo', 而/y\B../匹配"possibly yesterday"中的’yes‘ |
\cX | 当X是处于A到Z之间的字符的时候,匹配字符串中的一个控制符。 例如, |
\d | 匹配一个数字 例如, |
\D | 匹配一个非数字字符 例如, |
\f | 匹配一个换页符 (U+000C)。 |
\n | 匹配一个换行符 (U+000A)。 |
\r | 匹配一个回车符 (U+000D)。 |
\s | 匹配一个空白字符,包括空格、制表符、换页符和换行符。等价于[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]。 例如, 经测试,\s不匹配"\u180e",在当前版本Chrome(v80.0.3987.122)和Firefox(76.0.1)控制台输入/\s/.test("\u180e")均返回false。 |
\S | 匹配一个非空白字符。等价于 例如, |
\t | 匹配一个水平制表符 (U+0009)。 |
\v | 匹配一个垂直制表符 (U+000B)。 |
\w | 匹配一个单字字符(字母、数字或者下划线)。等价于 例如, |
\W | 匹配一个非单字字符。等价于 例如, |
\n | 在正则表达式中,它返回最后的第n个子捕获匹配的子字符串(捕获的数目以左括号计数)。 比如 |
\0 | 匹配 NULL(U+0000)字符, 不要在这后面跟其它小数,因为 \0<digits> 是一个八进制转义序列。 |
\xhh | 匹配一个两位十六进制数(\x00-\xFF)表示的字符。 |
\uhhhh | 匹配一个四位十六进制数表示的 UTF-16 代码单元。 |
| (仅当设置了u标志时)匹配一个十六进制数表示的 Unicode 字符。 |
使用正则表达式
正则表达式可以被用于RegExp
的exec
和test (en-US)
方法以及String
的match (en-US)
、replace
、search (en-US)
和split (en-US)
方法。这些方法在JavaScript手册中有详细的解释。
方法 | 描述 |
---|---|
exec | 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。 |
test | 一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。 |
match | 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。 |
matchAll | 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。 |
search | 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。 |
replace | 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。 |
split | 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。 |
当你想要知道在一个字符串中的一个匹配是否被找到,你可以使用test
或search
方法;想得到更多的信息(但是比较慢)则可以使用exec
或match
方法。如果你使用exec
或match
方法并且匹配成功了,那么这些方法将返回一个数组并且更新相关的正则表达式对象的属性和预定义的正则表达式对象(详见下)。如果匹配失败,那么exec
方法返回null
(也就是false
)。
在接下来的例子中,脚本将使用exec
方法在一个字符串中查找一个匹配。
var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");
如果你不需要访问正则表达式的属性,这个脚本通过另一个方法来创建myArray
:
var myArray = /d(b+)d/g.exec("cdbbdbsbz");
// 和 "cdbbdbsbz".match(/d(b+)d/g); 相似。
// 但是 "cdbbdbsbz".match(/d(b+)d/g) 输出数组 [ "dbbd" ],
// 而 /d(b+)d/g.exec('cdbbdbsbz') 输出数组 [ "dbbd", "bb", index: 1, input: "cdbbdbsbz" ].
如果你想通过一个字符串构建正则表达式,那么这个脚本还有另一种方法:
var myRe = new RegExp("d(b+)d", "g");
var myArray = myRe.exec("cdbbdbsbz");
通过这些脚本,匹配成功后将返回一个数组并且更新正则表达式的属性,如下表所示。
对象 | 属性或索引 | 描述 | 在例子中对应的值 |
---|---|---|---|
myArray | 匹配到的字符串和所有被记住的子字符串。 | ["dbbd", "bb"] | |
index | 在输入的字符串中匹配到的以0开始的索引值。 | 1 | |
input | 初始字符串。 | "cdbbdbsbz" | |
[0] | 最近一个匹配到的字符串。 | "dbbd" | |
myRe | lastIndex | 开始下一个匹配的起始索引值。(这个属性只有在使用g参数时可用在 通过参数进行高级搜索 一节有详细的描述.) | 5 |
source | 模式字面文本。在正则表达式创建时更新,不执行。 | "d(b+)d" |
使用括号的子字符串匹配
一个正则表达式模式使用括号,将导致相应的子匹配被记住。例如,/a(b)c /
可以匹配字符串“abc”
,并且记得“b”
。回调这些括号中匹配的子串,使用数组元素[1],……[n]
。
使用括号匹配的子字符串的数量是无限的。返回的数组中保存所有被发现的子匹配。下面的例子说明了如何使用括号的子字符串匹配。
下面的脚本使用replace()
方法来转换字符串中的单词。在匹配到的替换文本中,脚本使用替代的$1
,$2
表示第一个和第二个括号的子字符串匹配。
var re = /(\w+)\s(\w+)/;
var str = "John Smith";
var newstr = str.replace(re, "$2, $1");
console.log(newstr);
这个表达式输出 “Smith, John”。
通过标志进行高级搜索
正则表达式有六个可选参数 (flags) 允许全局和不分大小写搜索等。这些参数既可以单独使用也能以任意顺序一起使用, 并且被包含在正则表达式实例中。
标志 | 描述 |
---|---|
g | 全局搜索。 |
i | 不区分大小写搜索。 |
m | 多行搜索。 |
s | 允许 . 匹配换行符。 |
u | 使用unicode码的模式进行匹配。 |
y | 执行“粘性(sticky )”搜索,匹配从目标字符串的当前位置开始。 |
例子
改变输入字符串的顺序
以下例子解释了正则表达式的构成和string.split()
以及string.replace()
的用途。它会整理一个只有粗略格式的含有全名(名字首先出现)的输入字符串,这个字符串被空格、换行符和一个分号分隔。最终,它会颠倒名字顺序(姓氏首先出现)和list
的类型。
// 下面这个姓名字符串包含了多个空格和制表符,
// 且在姓和名之间可能有多个空格和制表符。
var names = "Orange Trump ;Fred Barney; Helen Rigby ; Bill Abel ; Chris Hand ";
var output = ["---------- Original String\n", names + "\n"];
// 准备两个模式的正则表达式放进数组里。
// 分割该字符串放进数组里。
// 匹配模式:匹配一个分号及紧接其前后所有可能出现的连续的不可见符号。
var pattern = /\s*;\s*/;
// 把通过上述匹配模式分割的字符串放进一个叫做nameList的数组里面。
var nameList = names.split(pattern);
// 新建一个匹配模式:匹配一个或多个连续的不可见字符及其前后紧接着由
// 一个或多个连续的基本拉丁字母表中的字母、数字和下划线组成的字符串
// 用一对圆括号来捕获该模式中的一部分匹配结果。
// 捕获的结果稍后会用到。
pattern = /(\w+)\s+(\w+)/;
// 新建一个数组 bySurnameList 用来临时存放正在处理的名字。
var bySurnameList = [];
// 输出 nameList 的元素并且把 nameList 里的名字
// 用逗号接空格的模式把姓和名分割开来然后存放进数组 bySurnameList 中。
//
// 下面的这个替换方法把 nameList 里的元素用 $2, $1 的模式
// (第二个捕获的匹配结果紧接着一个逗号一个空格然后紧接着第一个捕获的匹配结果)替换了
// 变量 $1 和变量 $2 是上面所捕获的匹配结果。
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"));
用特殊字符检验输入
在以下例子中,我们期望用户输入一个电话号码。当用户点击“Check”按钮,我们的脚本开始检查这些数字是否合法。如果数字合法(匹配正则表达式所规定的字符序列),脚本显示一条感谢用户的信息并确认该数字。如果这串数字不合法,脚本提示用户电话号码不合法。.
包含非捕获括号(?
: 这个正则表达式寻找三个数字字符\d{3}
或者|
一个左半括号\(
跟着三位数字\d{3}
, 跟着一个封闭括号\)
, (
结束非捕获括号))
,后跟着一个短破折号或正斜杠或小数点,随后跟随三个数字字符,当记忆字符([-\/\.])
捕获并记住,后面跟着三位小数\d{3}
,再后面跟随记住的破折号、正斜杠或小数点\1
,最后跟着四位小数\d{4}
。
当用户按下Enter
设置RegExp.input
,这些变化也能被激活。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<script type="text/javascript">
var re = /(?:\d{3}|\(\d{3}\))([-\/\.])\d{3}\1\d{4}/;
function testInfo(phoneInput) {
var OK = re.exec(phoneInput.value);
if (!OK)
window.alert(phoneInput.value + ' isn\'t a phone number with area code!');
else
window.alert('Thanks, your phone number is ' + OK[0]);
}
</script>
</head>
<body>
<p>Enter your phone number (with area code) and then click "Check".
<br>The expected format is like ###-###-####.</p>
<form action="#">
<input id="phone"><button onclick="testInfo(document.getElementById('phone'));">Check</button>
</form>
</body>
</html>
索引集合
数组对象
创建数组
以下语句创建等效的数组:
var arr = new Array(element0, element1, ..., elementN);
var arr = Array(element0, element1, ..., elementN);
var arr = [element0, element1, ..., elementN];
// 译者注: var arr=[4] 和 var arr=new Array(4)是不等效的,
// 后者4指数组长度,所以使用字面值(literal)的方式应该不仅仅是便捷,同时也不易踩坑
括号语法被称为 “数组字面值” 或 “数组初始化器”, 它比其他创建数组的方式更便捷,所以通常是首选。
为了创建一个长度不为0,但是又没有任何元素的数组,可选以下任何一种方式:
var arr = new Array(arrayLength);
var arr = Array(arrayLength);
// 这样有同样的效果
var arr = [];
arr.length = arrayLength;
填充数组
你可以通过给元素赋值来填充数组,例如:
var emp = [];
emp[0] = "Casey Jones";
emp[1] = "Phil Lesh";
emp[2] = "August West";
你也可以在创建数组的时候去填充它:
var myArray = new Array("Hello", myVar, 3.14159);
var myArray = ["Mango", "Apple", "Orange"]
引用数组元素
你可以用 myArray[0]引用第一个元素,myArray[1]引用第二个元素。元素的索引是从0开始的。
var arr = ["one", "two", "three"];
arr[2]; // three
arr["length"]; // 3
理解length
在实施层面, JavaScript实际上是将元素作为标准的对象属性来存储,把数组索引作为属性名。长度属性是特殊的,它总是返回最后一个元素的索引值加1(下例中, Dusty 的索引是30,所以cats.length 返回 30 + 1)。记住, JavaScript 数组索引是基于0的: 他们从0开始,而不是1。这意味着数组长度属性将比最大的索引值大1:var cats = []; cats[30] = ['Dusty']; console.log(cats.length); // 31
你也可以分配length属性。写一个小于数组元素数量的值会缩短数组,写0会彻底清空数组:
var cats = ['Dusty', 'Misty', 'Twiggy']; console.log(cats.length); // 3 cats.length = 2; console.log(cats); // logs "Dusty,Misty" - Twiggy has been removed cats.length = 0; console.log(cats); // logs nothing; the cats array is empty cats.length = 3; console.log(cats); // [undefined, undefined, undefined]
遍历数组
遍历数组元素并以某种方式处理每个元素是一个常见的操作。以下是最简单的方式:
var colors = ['red', 'green', 'blue'];
for (var i = 0; i < colors.length; i++) {
console.log(colors[i]);
}
forEach()
方法提供了遍历数组元素的其他方法:
var colors = ['red', 'green', 'blue'];
colors.forEach(function(color) {
console.log(color);
});
被传递给forEach
的函数会在数组的每个元素像上执行一次,元素作为参数传递给该函数。未赋值的值不会在forEach
循环迭代。
注意,在数组定义时省略的元素不会在forEach
遍历时被列出,但是手动赋值为undefined
的元素是会被列出的:
var array = ['first', 'second', , 'fourth'];
// returns ['first', 'second', 'fourth'];
array.forEach(function(element) {
console.log(element);
})
if(array[2] === undefined) { console.log('array[2] is undefined'); } // true
var array = ['first', 'second', undefined, 'fourth'];
// returns ['first', 'second', undefined, 'fourth'];
array.forEach(function(element) {
console.log(element);
})
一旦 JavaScript 元素被保存为标准的对象属性,通过for...in
循环来迭代数组将变得不明智,因为正常元素和所有可枚举的属性都会被列出。
数组的方法
Array
对象具有下列方法:
-
concat()
: 连接两个数组并返回一个新的数组。var myArray = new Array("1", "2", "3"); myArray = myArray.concat("a", "b", "c"); // myArray is now ["1", "2", "3", "a", "b", "c"]
-
join(deliminator = ',')
: 将数组的所有元素连接成一个字符串。var myArray = new Array("Wind", "Rain", "Fire"); var list = myArray.join(" - "); // list is "Wind - Rain - Fire"
-
push()
: 在数组末尾添加一个或多个元素,并返回数组操作后的长度。var myArray = new Array("1", "2"); myArray.push("3"); // myArray is now ["1", "2", "3"]
-
pop()
: 从数组移出最后一个元素,并返回该元素。var myArray = new Array("1", "2", "3"); var last = myArray.pop(); // myArray is now ["1", "2"], last = "3"
-
shift()
: 从数组移出第一个元素,并返回该元素。var myArray = new Array ("1", "2", "3"); var first = myArray.shift(); // myArray is now ["2", "3"], first is "1"
-
unshift()
: 在数组开头添加一个或多个元素,并返回数组的新长度。var myArray = new Array ("1", "2", "3"); myArray.unshift("4", "5"); // myArray becomes ["4", "5", "1", "2", "3"]
-
slice(start_index, upto_index)
: 从数组提取一个片段,并作为一个新数组返回。var myArray = new Array ("a", "b", "c", "d", "e"); myArray = myArray.slice(1, 4); // 包含索引1,不包括索引4 // returning [ "b", "c", "d"]
-
splice(index, count_to_remove, addElement1, addElement2, ...)
: 从数组移出一些元素,(可选)并替换它们。var myArray = new Array ("1", "2", "3", "4", "5"); myArray.splice(1, 3, "a", "b", "c", "d"); // myArray is now ["1", "a", "b", "c", "d", "5"] // This code started at index one (or where the "2" was), // removed 3 elements there, and then inserted all consecutive // elements in its place.
-
reverse()
: 颠倒数组元素的顺序:第一个变成最后一个,最后一个变成第一个。var myArray = new Array ("1", "2", "3"); myArray.reverse(); // transposes the array so that myArray = [ "3", "2", "1" ]
-
sort()
: 给数组元素排序。var myArray = new Array("Wind", "Rain", "Fire"); myArray.sort(); // sorts the array so that myArray = [ "Fire", "Rain", "Wind" ]
sort()
也可以带一个回调函数来决定怎么比较数组元素。这个回调函数比较两个值,并返回3个值中的一个:例如,下面的代码通过字符串的最后一个字母进行排序:
var sortFn = function(a, b){ if (a[a.length - 1] < b[b.length - 1]) return -1; if (a[a.length - 1] > b[b.length - 1]) return 1; if (a[a.length - 1] == b[b.length - 1]) return 0; } myArray.sort(sortFn); // sorts the array so that myArray = ["Wind","Fire","Rain"]
-
indexOf(searchElement[, fromIndex])
: 在数组中搜索searchElement 并返回第一个匹配的索引。var a = ['a', 'b', 'a', 'b', 'a']; console.log(a.indexOf('b')); // logs 1 // Now try again, starting from after the last match console.log(a.indexOf('b', 2)); // logs 3 console.log(a.indexOf('z')); // logs -1, because 'z' was not found
-
lastIndexOf(searchElement[, fromIndex])
和indexOf
差不多,但这是从结尾开始,并且是反向搜索。var a = ['a', 'b', 'c', 'd', 'a', 'b']; console.log(a.lastIndexOf('b')); // logs 5 // Now try again, starting from before the last match console.log(a.lastIndexOf('b', 4)); // logs 1 console.log(a.lastIndexOf('z')); // logs -1
-
forEach(callback[, thisObject])
: 在数组每个元素项上执行callback。var a = ['a', 'b', 'c']; a.forEach(function(element) { console.log(element);} ); // logs each item in turn
-
map(callback[, thisObject])
: 在数组的每个单元项上执行callback函数,并把返回包含回调函数返回值的新数组(译者注:也就是遍历数组,并通过callback对数组元素进行操作,并将所有操作结果放入数组中并返回该数组)。var a1 = ['a', 'b', 'c']; var a2 = a1.map(function(item) { return item.toUpperCase(); }); console.log(a2); // logs A,B,C
-
filter(callback[, thisObject])
: 返回一个包含所有在回调函数上返回为true的元素的新数组(译者注:callback在这里担任的是过滤器的角色,当元素符合条件,过滤器就返回true,而filter则会返回所有符合过滤条件的元素)var a1 = ['a', 10, 'b', 20, 'c', 30]; var a2 = a1.filter(function(item) { return typeof item == 'number'; }); console.log(a2); // logs 10,20,30
-
every(callback[, thisObject])
: 当数组中每一个元素在callback上被返回true时就返回true(译者注:同上,every其实类似filter,只不过它的功能是判断是不是数组中的所有元素都符合条件,并且返回的是布尔值)。function isNumber(value) { return typeof value == 'number'; } var a1 = [1, 2, 3]; console.log(a1.every(isNumber)); // logs true var a2 = [1, '2', 3]; console.log(a2.every(isNumber)); // logs false
-
some(callback[, thisObject])
: 只要数组中有一项在callback上被返回true,就返回true(译者注:同上,类似every,不过前者要求都符合筛选条件才返回true,后者只要有符合条件的就返回true)。function isNumber(value){ return typeof value == 'number'; } var a1 = [1, 2, 3]; console.log(a1.some(isNumber)); // logs true var a2 = [1, '2', 3]; console.log(a2.some(isNumber)); // logs true var a3 = ['1', '2', '3']; console.log(a3.some(isNumber)); // logs false
以上方法都带一个被称为迭代方法的的回调函数,因为他们以某种方式迭代整个数组。都有一个可选的第二参数thisObject
,如果提供了这个参数,thisObject
变成回调函数内部的this
关键字的值。如果没有提供,例如函数在一个显示的对象上下文外被调用时,this
将引用全局对象(window
).
实际上在调用回调函数时传入了3个参数。第一个是当前元素项的值,第二个是它在数组中的索引,第三个是数组本身的一个引用。 JavaScript 函数忽略任何没有在参数列表中命名的参数,因此提供一个只有一个参数的回调函数是安全的,例如alert
。
-
reduce(callback[, initialValue])
: 使用回调函数 callback(firstValue, secondValue) 把数组列表计算成一个单一值(译者注:他数组元素两两递归处理的方式把数组计算成一个值)。var a = [10, 20, 30]; var total = a.reduce(function(first, second) { return first + second; }, 0); console.log(total) // Prints 60
-
reduceRight(callback[, initalvalue])
和reduce()
相似,但这从最后一个元素开始的。
reduce
和reduceRight
是迭代数组方法中最不被人熟知的两个函数.。他们应该使用在那些需要把数组的元素两两递归处理,并最终计算成一个单一结果的算法。
多维数组
数组是可以嵌套的, 这就意味着一个数组可以作为一个元素被包含在另外一个数组里面。利用JavaScript数组的这个特性, 可以创建多维数组。
以下代码创建了一个二维数组。
var a = new Array(4);
for (i = 0; i < 4; i++) {
a[i] = new Array(4);
for (j = 0; j < 4; j++) {
a[i][j] = "[" + i + "," + j + "]";
}
}
这个例子创建的数组拥有以下行数据:
Row 0: [0,0] [0,1] [0,2] [0,3]
Row 1: [1,0] [1,1] [1,2] [1,3]
Row 2: [2,0] [2,1] [2,2] [2,3]
Row 3: [3,0] [3,1] [3,2] [3,3]
使用类数组对象
一些 JavaScript 对象, 例如document.getElementsByTagName()
返回的NodeList
或者函数内部可用的arguments
对象,他们表面上看起来,外观和行为像数组,但是不共享他们所有的方法。例如arguments
对象就提供一个length
属性,但是不实现forEach()
方法。
Array
的原生(prototype)方法可以用来处理类似数组行为的对象,例如:
function printArguments() {
Array.prototype.forEach.call(arguments, function(item) {
console.log(item);
});
}
Array
的常规方法也可以用于处理字符串,因为它提供了序列访问字符转为数组的简单方法:
Array.prototype.forEach.call("a string", function(chr) {
console.log(chr);
});
数组推导式
在JavaScript 1.7 被介绍并计划在 ECMAScript 7, array comprehensions 被规范化并提供一个有用的快捷方式,用来实现如何在另一个数组的基础上构造一个新的数组。推导式可以经常被用在那些需要调用 map() 和 filter()函数的地方,或作为一种结合这两种方式。
下面的推导式创建一个数字数组并且创建一个新的数组,数组的每个元素都是原来数值的两倍(译者注:这种形式类似于Python的列表推导式)。
var numbers = [1, 2, 3, 4];
var doubled = [for (i of numbers) i * 2];
console.log(doubled); // logs 2,4,6,8
这跟下面的map()
方法的操作是等价的。
var doubled = numbers.map(function(i){return i * 2;});
推导式也可以用来筛选满足条件表达式的元素. 下面的推导式用来筛选是2的倍数的元素:
var numbers = [1, 2, 3, 21, 22, 30];
var evens = [i for (i of numbers) if (i % 2 === 0)];
console.log(evens); // logs 2,22,30
filter()
也可以达到相同的目的:
var evens = numbers.filter(function(i){return i % 2 === 0;});
类型化数组
JavaScript typed arrays 是类数组对象(array-like object),其提供访问原始二进制数据的机制。 就像你知道的那样, Array 对象动态增长和收缩,可以有任何JavaScript值。但对于类型化数组,JavaScript引擎执行优化使得这些数组访问速度快速。 随着Web应用程序变得越来越强大,添加音频和视频处理等功能、可以使用 WebSockets 、使用原始数据, 这都需要访问原始的二进制数据,所以专门的优化将有助于JavaScript代码能够快速和容易地操纵原始二进制数据类型的数组。
缓冲区和视图:类型化的数组结构
为了实现最大的灵活性和效率,JavaScript类型数组被分解为缓冲(Buffer)和视图(views)。缓冲(由ArrayBuffer
实现)是代表数据块的对象,它没有格式可言,并没有提供任何机制来访问其内容。为了访问包含在缓冲区中的内存,您需要使用视图。视图提供了一个上下文,即数据类型、起始偏移量和元素数,这些元素将数据转换为实际类型数组。
ArrayBuffer
是一种数据类型,用于表示一个通用的、固定长度的二进制数据缓冲区。你不能直接操纵一个ArrayBuffer
中的内容;你需要创建一个数组类型视图或DataView来代表特定格式的缓冲区,并从而实现读写缓冲区的内容。
类型数组视图
类型数组视图具有自描述性的名字,并且提供数据类型信息,例如Int8, Uint32, Float64等等。如一个特定类型数组视图Uint8ClampedArray. 它意味着数据元素只包含0到255的整数值。它通常用于Canvas数据处理,例如.
Type | Value Range | Size in bytes | Description | Web IDL type | Equivalent C type |
---|---|---|---|---|---|
Int8Array | -128 to 127 | 1 | 8-bit two's complement signed integer | byte | int8_t |
Uint8Array | 0 to 255 | 1 | 8-bit unsigned integer | octet | uint8_t |
Uint8ClampedArray | 0 to 255 | 1 | 8-bit unsigned integer (clamped) | octet | uint8_t |
Int16Array | -32768 to 32767 | 2 | 16-bit two's complement signed integer | short | int16_t |
Uint16Array | 0 to 65535 | 2 | 16-bit unsigned integer | unsigned short | uint16_t |
Int32Array | -2147483648 to 2147483647 | 4 | 32-bit two's complement signed integer | long | int32_t |
Uint32Array | 0 to 4294967295 | 4 | 32-bit unsigned integer | unsigned long | uint32_t |
Float32Array | -3.4E38 to 3.4E38 and 1.2E-38 is the min positive number | 4 | 32-bit IEEE floating point number (7 significant digits e.g., 1.234567 ) | unrestricted float | float |
Float64Array | -1.8E308 to 1.8E308 and 5E-324 is the min positive number | 8 | 64-bit IEEE floating point number (16 significant digits e.g., 1.23456789012345 ) | unrestricted double | double |
BigInt64Array | -2^63 to 2^63 - 1 | 8 | 64-bit two's complement signed integer | bigint | int64_t (signed long long) |
BigUint64Array | 0 to 2^64 - 1 | 8 | 64-bit unsigned integer | bigint | uint64_t (unsigned long long) |
带键集合
Map
对象
ECMAScript 2015 引入了一个新的数据结构来将一个值映射到另一个值。一个Map
对象就是一个简单的键值对映射集合,可以按照数据插入时的顺序遍历所有的元素。
下面的代码演示了使用Map
进行的一些基本操作。请参考Map以获取更多的样例和完整的API。你可以使用for...of
循环来得到所有的[key, value]
。
var sayings = new Map();
sayings.set('dog', 'woof');
sayings.set('cat', 'meow');
sayings.set('elephant', 'toot');
sayings.size; // 3
sayings.get('fox'); // undefined
sayings.has('bird'); // false
sayings.delete('dog');
sayings.has('dog'); // false
for (var [key, value] of sayings) {
console.log(key + ' goes ' + value);
}
// "cat goes meow"
// "elephant goes toot"
sayings.clear();
sayings.size; // 0
Object
和Map
的对比
一般地,objects会被用于将字符串类型映射到数值。Object
允许设置键值对、根据键获取值、删除键、检测某个键是否存在。而Map
具有更多的优势。
Object
的键均为Strings
类型,在Map
里键可以是任意类型。- 必须手动计算
Object
的尺寸,但是可以很容易地获取使用Map
的尺寸。 Map
的遍历遵循元素的插入顺序。Object
有原型,所以映射中有一些缺省的键。(可以用map = Object.create(null)
回避)。
这三条提示可以帮你决定用Map还是Object:
- 如果键在运行时才能知道,或者所有的键类型相同,所有的值类型相同,那就使用Map。
- 如果需要将原始值存储为键,则使用Map,因为Object将每个键视为字符串,不管它是一个数字值、布尔值还是任何其他原始值。
- 如果需要对个别元素进行操作,使用Object。
WeakMap
对象
WeakMap
对象也是键值对的集合。它的键必须是对象类型,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被GC回收掉。WeakMap
提供的接口与Map
相同。
与Map
对象不同的是,WeakMap
的键是不可枚举的。不提供列出其键的方法。列表是否存在取决于垃圾回收器的状态,是不可预知的。
可以在"Why WeakMap?"WeakMap查看更多信息和示例。
WeakMap
对象的一个用例是存储一个对象的私有数据或隐藏实施细节。Nick Fitzgerald 的博文"Hiding Implementation Details with ECMAScript 6 WeakMaps"提供了一个例子。对象内部的私有数据和方法被存储在WeakMap
类型的privates变量中。所有暴露出的原型和情况都是公开的,而其他内容都是外界不可访问的,因为模块并未导出privates对象。
const privates = new WeakMap();
function Public() {
const me = {
// Private data goes here
};
privates.set(this, me);
}
Public.prototype.method = function () {
const me = privates.get(this);
// Do stuff with private data in `me`...
};
module.exports = Public;
Set
对象
Set
对象是一组值的集合,这些值是不重复的,可以按照添加顺序来遍历。
这里演示了Set
的基本操作,更多示例和完整API可以参考Set。
var mySet = new Set();
mySet.add(1);
mySet.add("some text");
mySet.add("foo");
mySet.has(1); // true
mySet.delete("foo");
mySet.size; // 2
for (let item of mySet) console.log(item);
// 1
// "some text"
Array
和Set
的转换
可以使用Array.from或展开操作符来完成集合到数组的转换。同样,Set
的构造器接受数组作为参数,可以完成从Array
到Set
的转换。需要重申的是,Set
对象中的值不重复,所以数组转换为集合时,所有重复值将会被删除。
Array.from(mySet);
[...mySet2];
mySet2 = new Set([1,2,3,4]);
Array
和Set
的对比
一般情况下,在JavaScript中使用数组来存储一组元素,而新的集合对象有这些优势:
- 数组中用于判断元素是否存在的indexOf 函数效率低下。
- Set对象允许根据值删除元素,而数组中必须使用基于下标的 splice 方法。
- 数组的indexOf方法无法找到NaN值。
- Set对象存储不重复的值,所以不需要手动处理包含重复值的情况。
WeakSet
对象
WeakSet对象是一组对象的集合。WeakSet
中的对象不重复且不可枚举。
与Set
对象的主要区别有:
WeakSets
中的值必须是对象类型,不可以是别的类型WeakSet
的“weak”指的是,对集合中的对象,如果不存在其他引用,那么该对象将可被垃圾回收。于是不存在一个当前可用对象组成的列表,所以WeakSets不可枚举WeakSet
的用例很有限,比如使用DOM元素作为键来追踪它们而不必担心内存泄漏。
Map
键和Set
值的等值判断
Map的键和Set的值的等值判断都基于same-value-zero algorithm:
- 判断使用与
===
相似的规则。 -0
和+0
相等。NaN
与自身相等(与===
有所不同)。
处理对象
JavaScript 的设计是一个简单的基于对象的范式。一个对象就是一系列属性的集合,一个属性包含一个名和一个值。一个属性的值可以是函数,这种情况下属性也被称为方法。除了浏览器里面预定义的那些对象之外,你也可以定义你自己的对象。
一个 javascript 对象有很多属性。一个对象的属性可以被解释成一个附加到对象上的变量。对象的属性和普通的 javascript 变量基本没什么区别,仅仅是属性属于某个对象。你可以通过点符号来访问一个对象的属性。
objectName.propertyName
和其他 javascript 变量一样,对象的名字(可以是普通的变量)和属性的名字都是大小写敏感的。你可以在定义一个属性的时候就给它赋值。例如,我们创建一个myCar的对象然后给他三个属性,make,model,year。具体如下所示:
var myCar = new Object();
myCar.make = "Ford";
myCar.model = "Mustang";
myCar.year = 1969;
对象中未赋值的属性的值为undefined
(而不是null
)。
myCar.noProperty; // undefined
JavaScript 对象的属性也可以通过方括号访问或者设置(更多信息查看property accessors). 对象有时也被叫作关联数组, 因为每个属性都有一个用于访问它的字符串值。例如,你可以按如下方式访问 myCar 对象的属性:
// 同时创建四个变量,用逗号分隔
var myObj = new Object(),
str = "myString",
rand = Math.random(),
obj = new Object();
myObj.type = "Dot syntax";
myObj["date created"] = "String with space";
myObj[str] = "String value";
myObj[rand] = "Random Number";
myObj[obj] = "Object";
myObj[""] = "Even an empty string";
console.log(myObj);
请注意,方括号中的所有键都将转换为字符串类型,因为JavaScript中的对象只能使用String类型作为键类型。 例如,在上面的代码中,当将键obj添加到myObj时,JavaScript将调用obj.toString()方法,并将此结果字符串用作新键。
你可以在for...in
语句中使用方括号标记以枚举一个对象的所有属性。为了展示它如何工作,下面的函数当你将对象及其名称作为参数传入时,显示对象的属性:
function showProps(obj, objName) {
var result = "";
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
result += objName + "." + i + " = " + obj[i] + "\n";
}
}
return result;
}
因而,对于函数调用showProps(myCar, "myCar")
将返回以下值:
myCar.make = Ford
myCar.model = Mustang
myCar.year = 1969
枚举一个对象的所有属性
从 ECMAScript 5 开始,有三种原生的方法用于列出或枚举对象的属性:
for...in
循环
该方法依次访问一个对象及其原型链中所有可枚举的属性。Object.keys(o)
该方法返回对象o
自身包含(不包括原型中)的所有可枚举属性的名称的数组。Object.getOwnPropertyNames(o)
该方法返回对象o
自身包含(不包括原型中)的所有属性(无论是否可枚举)的名称的数组。
创建新对象
JavaScript 拥有一系列预定义的对象。另外,你可以创建你自己的对象。从 JavaScript 1.2 之后,你可以通过对象初始化器(Object Initializer)创建对象。或者你可以创建一个构造函数并使用该函数和 new 操作符初始化对象。
使用对象初始化器
除了通过构造函数创建对象之外,你也可以通过对象初始化器创建对象。使用对象初始化器也被称作通过字面值创建对象。对象初始化器与 C++ 术语相一致。
通过对象初始化器创建对象的语法如下:
var obj = { property_1: value_1, // property_# 可以是一个标识符...
2: value_2, // 或一个数字...
["property" +3]: value_3, // 或一个可计算的key名...
// ...,
"property n": value_n }; // 或一个字符串
使用构造函数
作为另一种方式,你可以通过两步来创建对象:
- 通过创建一个构造函数来定义对象的类型。首字母大写是非常普遍而且很恰当的惯用法。
- 通过 new 创建对象实例。
为了定义对象类型,为对象类型创建一个函数以声明类型的名称、属性和方法。例如,你想为汽车创建一个类型,并且将这类对象称为 car ,并且拥有属性 make, model, 和 year,你可以创建如下的函数:
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
注意通过使用 this 将传入函数的值赋给对象的属性。
现在你可以象这样创建一个 mycar 对象:
var mycar = new Car("Eagle", "Talon TSi", 1993);
使用Object.create
方法
对象也可以用Object.create()
方法创建。该方法非常有用,因为它允许你为创建的对象选择一个原型对象,而不用定义构造函数。
// Animal properties and method encapsulation
var Animal = {
type: "Invertebrates", // 属性默认值
displayType : function() { // 用于显示type属性的方法
console.log(this.type);
}
}
// 创建一种新的动物——animal1
var animal1 = Object.create(Animal);
animal1.displayType(); // Output:Invertebrates
// 创建一种新的动物——Fishes
var fish = Object.create(Animal);
fish.type = "Fishes";
fish.displayType(); // Output:Fishes
该函数更多的信息及详细用法,参见Object.create method
继承
所有的 JavaScript 对象至少继承于一个对象。被继承的对象被称作原型,并且继承的属性可通过构造函数的 prototype 对象找到。查看更多详细Inheritance and the prototype chain
对象属性索引
在 JavaScript 1.0 中,你可以通过名称或序号访问一个属性。但是在 JavaScript 1.1 及之后版本中,如果你最初使用名称定义了一个属性,则你必须通过名称来访问它;而如果你最初使用序号来定义一个属性,则你必须通过索引来访问它。
这个限制发生在你通过构造函数创建一个对象和它的属性(就象我们之前通过 Car 对象类型所做的那样)并且显式地定义了单独的属性(如myCar.color = "red"
)之时。如果你最初使用索引定义了一个对象属性,例如myCar[5] = "25"
,则你只可能通过myCar[5]
引用它。
这条规则的例外是从与HTML对应的对象,例如forms
数组。对于这些数组的元素,你总是既可以通过其序号(依据其在文档中出现的顺序),也可以按照其名称(如果有的话)访问它。举例而言,如果文档中的第二个<form>
标签有一个NAME
属性且值为"myForm"
,访问该form
的方式可以是document.forms[1]
,document.forms["myForm"]
或document.myForm
。
为对象类型定义属性
你可以通过prototype
属性为之前定义的对象类型增加属性。这为该类型的所有对象,而不是仅仅一个对象增加了一个属性。下面的代码为所有类型为car
的对象增加了color
属性,然后为对象car1
的color
属性赋值:
Car.prototype.color = null;
car1.color = "black";
定义方法
一个方法 是关联到某个对象的函数,或者简单地说,一个方法是一个值为某个函数的对象属性。定义方法就像定义普通的函数,除了它们必须被赋给对象的某个属性。查看method definitions了解更多详情例如:
objectName.methodname = function_name;
var myObj = {
myMethod: function(params) {
// ...do something
}
// 或者 这样写也可以
myOtherMethod(params) {
// ...do something else
}
};
通过this
引用对象
JavaScript 有一个特殊的关键字this
,它可以在方法中使用以指代当前对象。例如,假设你有一个名为validate
的函数,它根据给出的最大与最小值检查某个对象的value
属性:
function validate(obj, lowval, hival) {
if ((obj.value < lowval) || (obj.value > hival)) {
alert("Invalid Value!");
}
}
然后,你可以在每个元素的onchange
事件处理器中调用validate
,并通过this
传入相应元素,代码如下:
<input type="text" name="age" size="3"
onChange="validate(this, 18, 99)">
总的说来,this
在一个方法中指调用的对象。
当与form
属性一起使用时,this
可以指代当前对象的父窗体。在下面的例子中,窗体myForm
包含一个Text
对象和一个按钮,当用户点击按键,Text
对象的值被设为窗体的名称。按钮的onclick
事件处理器使用this.form
以指代其父窗体,即myForm
。
<form name="myForm">
<p><label>Form name:<input type="text" name="text1" value="Beluga"></label>
<p><input name="button1" type="button" value="Show Form Name"
onclick="this.form.text1.value = this.form.name">
</p>
</form>
定义getters
与setters
一个getter
是一个获取某个特定属性的值的方法。一个setter
是一个设定某个属性的值的方法。你可以为预定义的或用户定义的对象定义getter
和setter
以支持新增的属性。定义getter
和setter
的语法采用对象字面量语法。
下面例子描述了getters
和setters
是如何为用户定义的对象o
工作的。
var o = {
a: 7,
get b() {
return this.a + 1;
},
set c(x) {
this.a = x / 2
}
};
console.log(o.a); // 7
console.log(o.b); // 8
o.c = 50;
console.log(o.a); // 25
o
对象的属性如下:
o.a
-— 数字o.b
-— 返回o.a + 1
的getter
o.c
-— 由o.c
的值所设置o.a
值的setter
下面这个例子展示使用getter
和setter
方法扩展Date
原型,为预定义好的Date
类添加一个year
的属性。定义属性year
的getter
和setter
方法用到了Date
类中已存在的getFullYear
和setFullYear
方法。
定义属性year
的getter
和setter
:
var d = Date.prototype;
Object.defineProperty(d, "year", {
get: function() { return this.getFullYear() },
set: function(y) { this.setFullYear(y) }
});
通过一个Date
对象使用getter
和setter
:
原则上,getter
和setter
既可以:
- 使用使用对象初始化器定义
- 也可以之后随时使用
getter
和setter
添加方法添加到任何对象
当使用对象初始化器的方式定义getter
和setter
时,只需要在getter
方法前加get
,在setter
方法前加set
,当然,getter
方法必须是无参数的,setter
方法只接受一个参数(设置为新值)。
删除属性
你可以用delete
操作符删除一个不是继承而来的属性。下面的例子说明如何删除一个属性:
//Creates a new object, myobj, with two properties, a and b.
var myobj = new Object;
myobj.a = 5;
myobj.b = 12;
//Removes the a property, leaving myobj with only the b property.
delete myobj.a;
如果一个全局变量不是用var
关键字声明的话,你也可以用delete
删除它:
g = 17;
delete g;
比较对象
在 JavaScript 中objects
是一种引用类型。两个独立声明的对象永远也不会相等,即使他们有相同的属性,只有在比较一个对象和这个对象的引用时,才会返回true
.
// 两个变量, 两个具有同样的属性、但不相同的对象
var fruit = {name: "apple"};
var fruitbear = {name: "apple"};
fruit == fruitbear // return false
fruit === fruitbear // return false
“===” 运算符用来检查数值是否相等: 1 === "1"返回false,而1 == “1” 返回true
// 两个变量, 同一个对象
var fruit = {name: "apple"};
var fruitbear = fruit; // 将fruit的对象引用(reference)赋值给 fruitbear
// 也称为将fruitbear“指向”fruit对象
// fruit与fruitbear都指向同样的对象
fruit == fruitbear // return true
fruit === fruitbear // return true
对象模型的细节
JavaScript 是一种基于原型而不是基于类的基于对象(object-based)语言。正是由于这一根本的区别,其如何创建对象的层级结构以及对象的属性与属性值是如何继承的并不是那么清晰。本节试着阐明。
基于类 vs 基于原型的语言
基于类的面向对象语言,比如 Java 和 C++,是构建在两个不同实体之上的:类和实例。
- 一个类(class)定义了某一对象集合所具有的特征性属性(可以将 Java 中的方法和域以及 C++ 中的成员都视作属性)。类是抽象的,而不是其所描述的对象集合中的任何特定的个体。例如 Employee 类可以用来表示所有雇员的集合。
- 另一方面,一个实例(instance)是一个类的实例化。例如, Victoria 可以是 Employee 类的一个实例,表示一个特定的雇员个体。实例具有和其父类完全一致的属性,不多也不少。
基于原型的语言(如 JavaScript)并不存在这种区别:它只有对象。基于原型的语言具有所谓原型对象(prototypical object)的概念。原型对象可以作为一个模板,新对象可以从中获得原始的属性。任何对象都可以指定其自身的属性,既可以是创建时也可以在运行时创建。而且,任何对象都可以作为另一个对象的原型(prototype),从而允许后者共享前者的属性。
定义类
在基于类的语言中,需要专门的类定义(class definition)来定义类。在定义类时,允许定义被称为构造器(constructor)的特殊的方法来创建该类的实例。在构造器方法中,可以指定实例的属性的初始值并做一些其他的操作。你可以通过使用new
操作符来创建类的实例。
JavaScript 大体上与之类似,但并没有专门的类定义,你通过定义构造函数的方式来创建一系列有着特定初始值和方法的对象。任何JavaScript函数都可以被用作构造函数。你也可以使用new
操作符来创建一个新对象。
在ES6中引入了类定义 ,但它实际上是已有的原型继承方式的语法糖而已,并没有引入新的面向对象继承模型。
子类和继承
基于类的语言是通过对类的定义中构建类的层级结构的。在类定义中,可以指定新的类是一个现存的类的子类。子类将继承父类的全部属性,并可以添加新的属性或者修改继承的属性。例如,假设 Employee 类只有 name 和 dept 属性,而 Manager 是 Employee 的子类并添加了 reports 属性。这时,Manager 类的实例将具有所有三个属性:name,dept和reports。
JavaScript 通过将构造器函数与原型对象相关联的方式来实现继承。这样,您可以创建完全一样的 Employee — Manager 示例,不过需要使用略微不同的术语。首先,定义Employee构造函数,在该构造函数内定义name、dept属性;接下来,定义Manager构造函数,在该构造函数内调用Employee构造函数,并定义reports属性;最后,将一个获得了Employee.prototype(Employee构造函数原型)的新对象赋予manager构造函数,以作为Manager构造函数的原型。之后当你创建新的Manager对象实例时,该实例会从Employee对象继承name、dept属性。
添加和移除属性
在基于类的语言中,通常在编译时创建类,然后在编译时或者运行时对类的实例进行实例化。一旦定义了类,无法对类的属性进行更改。然而,在 JavaScript 中,允许运行时添加或者移除任何对象的属性。如果您为一个对象中添加了一个属性,而这个对象又作为其它对象的原型,则以该对象作为原型的所有其它对象也将获得该属性。
差异总结
下面的表格摘要给出了上述区别。本节的后续部分将描述有关使用 JavaScript 构造器和原型创建对象层级结构的详细信息,并将其与在 Java 中的做法加以对比。
基于类的(Java) | 基于原型的(JavaScript) |
---|---|
类和实例是不同的事物。 | 所有对象均为实例。 |
通过类定义来定义类;通过构造器方法来实例化类。 | 通过构造器函数来定义和创建一组对象。 |
通过 new 操作符创建单个对象。 | 相同。 |
通过类定义来定义现存类的子类,从而构建对象的层级结构。 | 指定一个对象作为原型并且与构造函数一起构建对象的层级结构 |
遵循类链继承属性。 | 遵循原型链继承属性。 |
类定义指定类的所有实例的所有属性。无法在运行时动态添加属性。 | 构造器函数或原型指定实例的初始属性集。允许动态地向单个的对象或者整个对象集中添加或移除属性。 |
Promises
Promise
是一个对象,它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的Promise
实例对象,所以本教程将首先说明怎样使用Promise
,再说明如何创建Promise
。
本质上Promise
是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。
假设现在有一个名为createAudioFileAsync()
的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时被调用,另一个则在出现异常时被调用。
以下为使用createAudioFileAsync()
的示例:
// 成功的回调函数
function successCallback(result) {
console.log("音频文件创建成功: " + result);
}
// 失败的回调函数
function failureCallback(error) {
console.log("音频文件创建失败: " + error);
}
createAudioFileAsync(audioSettings, successCallback, failureCallback);
更现代的函数会返回一个Promise
对象,使得你可以将你的回调函数绑定在该Promise
上。
如果函数createAudioFileAsync()
被重写为返回Promise
的形式,那么我们可以像下面这样简单地调用它:
const promise = createAudioFileAsync(audioSettings);
promise.then(successCallback, failureCallback);
或者简写为:
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
我们把这个称为异步函数调用,这种形式有若干优点,下面我们将会逐一讨论。
约定
不同于“老式”的传入回调,在使用Promise
时,会有以下约定:
在本轮事件循环运行完成之前,回调函数是不会被调用的。
即使异步操作已经完成(成功或失败),在这之后通过then()
添加的回调函数也会被调用。
通过多次调用then()
可以添加多个回调函数,它们会按照插入顺序进行执行。
Promise
棒的一点就是链式调用(chaining)。
链式调用
连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个Promise
链来实现这种需求。
见证奇迹的时刻:then()
函数会返回一个和原来不同的新的Promise
:
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
或者
const promise2 = doSomething().then(successCallback, failureCallback);
promise2
不仅表示doSomething()
函数的完成,也代表了你传入的successCallback
或者failureCallback
的完成,这两个函数也可以返回一个Promise
对象,从而形成另一个异步操作,这样的话,在promise2
上新增的回调函数会排在这个Promise
对象的后面。
基本上,每一个Promise
都代表了链中另一个异步过程的完成。
在过去,要想做多重的异步操作,会导致经典的回调地狱:
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
现在,我们可以把回调绑定到返回的Promise
上,形成一个Promise
链:
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
then
里的参数是可选的,catch(failureCallback)
是then(null, failureCallback)
的缩略形式。如下所示,我们也可以用箭头函数来表示:
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。(如果使用箭头函数,() => x 比 () => { return x; } 更简洁一些,但后一种保留 return 的写法才支持使用多个语句。)。
Catch
的后续链式操作
有可能会在一个回调失败之后继续使用链式操作,即,使用一个catch
,这对于在链式操作中抛出一个失败之后,再次进行新的操作会很有用。请阅读下面的例子:
new Promise((resolve, reject) => {
console.log('初始化');
resolve();
})
.then(() => {
throw new Error('有哪里不对了');
console.log('执行「这个」”');
})
.catch(() => {
console.log('执行「那个」');
})
.then(() => {
console.log('执行「这个」,无论前面发生了什么');
});
输出结果如下:
初始化
执行“那个”
执行“这个”,无论前面发生了什么
错误传递
在之前的回调地狱示例中,你可能记得有 3 次failureCallback
的调用,而在Promise
链中只有尾部的一次调用。
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
通常,一遇到异常抛出,浏览器就会顺着Promise
链寻找下一个onRejected
失败回调函数或者由.catch()
指定的回调函数。这和以下同步代码的工作原理(执行过程)非常相似。
try {
let result = syncDoSomething();
let newResult = syncDoSomethingElse(result);
let finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
在 ECMAScript 2017 标准的async/await语法糖中,这种异步代码的对称性得到了极致的体现:
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}
Promise
拒绝事件
当 Promise
被拒绝时,会有下文所述的两个事件之一被派发到全局作用域(通常而言,就是window;如果是在web worker中使用的话,就是 Worker 或者其他 worker-based 接口)。这两个事件如下所示:
- rejectionhandled
当Promise
被拒绝、并且在reject
函数处理该rejection
之后会派发此事件。 - unhandledrejection
当Promise
被拒绝,但没有提供reject
函数来处理该rejection
时,会派发此事件。
以上两种情况中,PromiseRejectionEvent事件都有两个属性,一个是promise属性,该属性指向被驳回的Promise
,另一个是reason (en-US)属性,该属性用来说明Promise
被驳回的原因。
因此,我们可以通过以上事件为Promise
失败时提供补偿处理,也有利于调试Promise
相关的问题。在每一个上下文中,该处理都是全局的,因此不管源码如何,所有的错误都会在同一个处理函数中被捕捉并处理。
一个特别有用的例子:当你使用Node.js
时,有些依赖模块可能会有未被处理的rejected promises
,这些都会在运行时打印到控制台。你可以在自己的代码中捕捉这些信息,然后添加与unhandledrejection
相应的处理函数来做分析和处理,或只是为了让你的输出更整洁。举例如下:
window.addEventListener("unhandledrejection", event => {
/* 你可以在这里添加一些代码,以便检查
event.promise 中的 promise 和
event.reason 中的 rejection 原因 */
event.preventDefault();
}, false);
调用event
的preventDefault()方法是为了告诉 JavaScript 引擎当Promise
被拒绝时不要执行默认操作,默认操作一般会包含把错误打印到控制台,Node 就是如此的。
理想情况下,在忽略这些事件之前,我们应该检查所有被拒绝的Promise
,来确认这不是代码中的bug。
在旧式回调 API 中创建Promise
可以通过Promise
的构造器从零开始创建Promise
。 这种方式(通过构造器的方式)应当只在封装旧 API 的时候用到。
理想状态下,所有的异步函数都已经返回Promise
了。但有一些 API 仍然使用旧方式来传入的成功(或者失败)的回调。典型的例子就是setTimeout() (en-US)函数:
setTimeout(() => saySomething("10 seconds passed"), 10000);
混用旧式回调和Promise
可能会造成运行时序问题。如果saySomething
函数失败了,或者包含了编程错误,那就没有办法捕获它了。这得怪setTimeout
。
幸运地是,我们可以用Promise
来封装它。最好的做法是,将这些有问题的函数封装起来,留在底层,并且永远不要再直接调用它们:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);
通常,Promise
的构造器接收一个执行函数(executor
),我们可以在这个执行函数里手动地resolve
和reject
一个Promise
。既然setTimeout
并不会真的执行失败,那么我们可以在这种情况下忽略reject
。
组合
Promise.resolve()和Promise.reject()是手动创建一个已经resolve
或者reject
的Promise
快捷方法。它们有时很有用。
Promise.all()和Promise.race()是并行运行异步操作的两个组合式工具。
我们可以发起并行操作,然后等多个操作全部结束后进行下一步操作,如下:
Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });
可以使用一些聪明的 JavaScript 写法实现时序组合:
[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });
通常,我们递归调用一个由异步函数组成的数组时,相当于一个Promise
链:
Promise.resolve().then(func1).then(func2).then(func3);
我们也可以写成可复用的函数形式,这在函数式编程中极为普遍:
const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
composeAsync()
函数将会接受任意数量的函数作为其参数,并返回一个新的函数,该函数接受一个通过composition pipeline
传入的初始值。这对我们来说非常有益,因为任一函数可以是异步或同步的,它们能被保证按顺序执行:
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
在 ECMAScript 2017 标准中, 时序组合可以通过使用async/await
而变得更简单:
let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result (i.e. result3) */
时序
为了避免意外,即使是一个已经变成resolve
状态的Promise
,传递给then()
的函数也总是会被异步调用:
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
传递到then()
中的函数被置入到一个微任务队列中,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,且事件队列被清空之后,才开始执行:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4
嵌套
简便的Promise
链式编程最好保持扁平化,不要嵌套Promise
,因为嵌套经常会是粗心导致的。可查阅下一节的常见错误中的例子。
嵌套Promise
是一种可以限制catch
语句的作用域的控制结构写法。明确来说,嵌套的catch
仅捕捉在其之前同时还必须是其作用域的failureres
,而捕捉不到在其链式以外或者其嵌套域以外的error
。如果使用正确,那么可以实现高精度的错误修复。
doSomethingCritical()
.then(result => doSomethingOptional()
.then(optionalResult => doSomethingExtraNice(optionalResult))
.catch(e => {console.log(e.message)})) // 即使有异常也会忽略,继续运行;(最后会输出)
.then(() => moreCriticalStuff())
.catch(e => console.log("Critical failure: " + e.message));// 没有输出
注意,有些代码步骤是嵌套的,而不是一个简单的纯链式,这些语句前与后都被括号()
包裹着。
这个内部的catch
语句仅能捕获到doSomethingOptional()
和doSomethingExtraNice()
的失败,之后就恢复到moreCriticalStuff()
的运行。重要提醒:如果doSomethingCritical()
失败,这个错误仅会被最后的(外部)catch
语句捕获到。
常见错误
在编写Promise
链时,需要注意以下示例中展示的几个错误:
// 错误示例,包含 3 个问题!
doSomething().then(function(result) {
doSomethingElse(result) // 没有返回 Promise 以及没有必要的嵌套 Promise
.then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// 最后,是没有使用 catch 终止 Promise 调用链,可能导致没有捕获的异常
第一个错误是没有正确地将事物相连接。当我们创建新Promise
但忘记返回它时,会发生这种情况。因此,链条被打破,或者更确切地说,我们有两个独立的链条竞争(同时在执行两个异步而非一个一个的执行)。这意味着doFourthThing()
不会等待doSomethingElse()
或doThirdThing()
完成,并且将与它们并行运行,可能是无意的。单独的链也有单独的错误处理,导致未捕获的错误。
第二个错误是不必要地嵌套,实现第一个错误。嵌套还限制了内部错误处理程序的范围,如果是非预期的,可能会导致未捕获的错误。其中一个变体是Promise 构造函数反模式,它结合了Promise
构造函数的多余使用和嵌套。
第三个错误是忘记用catch
终止链。这导致在大多数浏览器中不能终止的Promise
链里的rejection
。
一个好的经验法则是总是返回或终止Promise
链,并且一旦你得到一个新的Promise
,返回它。下面是修改后的平面化的代码:
doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));
() => x
是() => { return x; }
的简写。
上述代码的写法就是具有适当错误处理的简单明确的链式写法。
用async/await可以解决以上大多数错误,使用async/await
时,最常见的语法错误就是忘记了await关键字。
迭代器和生成器
处理集合中的每个项是很常见的操作。JavaScript 提供了许多迭代集合的方法,从简单的for
循环到map()
和filter()
。迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义for...of
循环的行为。
迭代器
在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 更具体地说,迭代器是通过使用next()
方法实现Iterator protocol
的任何一个对象,该方法返回具有两个属性的对象:value
,这是序列中的next
值;和 done
,如果已经迭代到序列中的最后一个值,则它为true
。如果value
和done
一起存在,则它是迭代器的返回值。
一旦创建,迭代器对象可以通过重复调用next()
显式地迭代。 迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。 在产生终止值之后,对next()
的额外调用应该继续返回{done:true}
。
Javascript中最常见的迭代器是Array
迭代器,它只是按顺序返回关联数组中的每个值。 虽然很容易想象所有迭代器都可以表示为数组,但事实并非如此。 数组必须完整分配,但迭代器仅在必要时使用,因此可以表示无限大小的序列,例如0和无穷大之间的整数范围。
这是一个可以做到这一点的例子。 它允许创建一个简单的范围迭代器,它定义了从开始(包括)到结束(独占)间隔步长的整数序列。 它的最终返回值是它创建的序列的大小,由变量iterationCount
跟踪。
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next: function() {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false }
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true }
}
};
return rangeIterator;
}
使用这个迭代器看起来像这样:
let it = makeRangeIterator(1, 10, 2);
let result = it.next();
while (!result.done) {
console.log(result.value); // 1 3 5 7 9
result = it.next();
}
console.log("Iterated over sequence of size: ", result.value); // 5
生成器函数
虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 生成器函数使用 function*
*语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator
的迭代器。 通过调用生成器的下一个方法消耗值时,Generator
函数将执行,直到遇到yield
关键字。
可以根据需要多次调用该函数,并且每次都返回一个新的Generator
,但每个Generator
只能迭代一次。
我们现在可以调整上面的例子了。 此代码的行为是相同的,但实现更容易编写和读取。
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
var a = makeRangeIterator(1,10,2)
a.next() // {value: 1, done: false}
a.next() // {value: 3, done: false}
a.next() // {value: 5, done: false}
a.next() // {value: 7, done: false}
a.next() // {value: 9, done: false}
a.next() // {value: undefined, done: true}
可迭代对象
若一个对象拥有迭代行为,比如在for...of
中会循环哪些值,那么那个对象便是一个可迭代对象。一些内置类型,如Array
或Map
拥有默认的迭代行为,而其他类型(比如Object
)则没有。
为了实现可迭代,一个对象必须实现@@iterator
方法,这意味着这个对象(或其原型链中的任意一个对象)必须具有一个带Symbol.iterator
键(key)的属性。
可以多次迭代一个迭代器,或者只迭代一次。 程序员应该知道是哪种情况。 只能迭代一次的Iterables
(例如Generators
)通常从它们的@@iterator
方法中返回它本身,其中那些可以多次迭代的方法必须在每次调用@@iterator
时返回一个新的迭代器。
自定义的可迭代对象
我们可以像这样实现自己的可迭代对象:
var myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
}
for (let value of myIterable) {
console.log(value);
}
// 1
// 2
// 3
// 或者
[...myIterable]; // [1, 2, 3]
内置可迭代对象
String
、Array
、TypedArray
、Map
和Set
都是内置可迭代对象,因为它们的原型对象都拥有一个Symbol.iterator
方法。
用于可迭代对象的语法
一些语句和表达式专用于可迭代对象,例如for-of循环,展开语法,yield*和解构赋值。
for (let value of ['a', 'b', 'c']) {
console.log(value);
}
// "a"
// "b"
// "c"
[...'abc']; // ["a", "b", "c"]
function* gen() {
yield* ['a', 'b', 'c'];
}
gen().next(); // { value: "a", done: false }
[a, b, c] = new Set(['a', 'b', 'c']);
a; // "a"
高级生成器
生成器会按需计算它们的产生值,这使得它们能够有效的表示一个计算成本很高的序列,甚至是如上所示的一个无限序列。
Thenext()
方法也接受一个参数用于修改生成器内部状态。传递给next()
的参数值会被yield
接收。要注意的是,传给第一个next()
的值会被忽略。
下面的是斐波那契数列生成器,它使用了next(x)
来重新启动序列:
function* fibonacci() {
var fn1 = 0;
var fn2 = 1;
while (true) {
var current = fn1;
fn1 = fn2;
fn2 = current + fn1;
var reset = yield current;
if (reset) {
fn1 = 0;
fn2 = 1;
}
}
}
var sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
你可以通过调用其throw()
方法强制生成器抛出异常,并传递应该抛出的异常值。这个异常将从当前挂起的生成器的上下文中抛出,就好像当前挂起的yield
是一个throw value
语句。
如果在抛出的异常处理期间没有遇到yield
,则异常将通过调用throw()
向上传播,对next()
的后续调用将导致done
属性为true
。
生成器具有return(value)
方法,返回给定的值并完成生成器本身。
元编程
从ECMAScript 2015 开始,JavaScript
获得了Proxy和Reflect对象的支持,允许你拦截并定义基本语言操作的自定义行为(例如,属性查找,赋值,枚举,函数调用等)。借助这两个对象,你可以在 JavaScript 元级别进行编程。
Proxy代理
在 ECMAScript 6 中引入的Proxy
对象可以拦截某些操作并实现自定义行为。例如获取一个对象上的属性:
let handler = {
get: function(target, name){
return name in target ? target[name] : 42;
}};
let p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, 42
Proxy
对象定义了一个目标(这里是一个空对象)和一个实现了get
陷阱的handler
对象。这里,代理的对象在获取未定义的属性时不会返回undefined
,而是返回42
。
术语
句柄和陷阱
撤销Proxy
Proxy.revocable()方法被用来创建可撤销的Proxy
对象。这意味着proxy
可以通过revoke
函数来撤销,并且关闭代理。此后,代理上的任意的操作都会导致TypeError。
var revocable = Proxy.revocable({}, {
get: function(target, name) {
return "[[" + name + "]]";
}
});
var proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"
revocable.revoke();
console.log(proxy.foo); // TypeError is thrown
proxy.foo = 1 // TypeError again
delete proxy.foo; // still TypeError
typeof proxy // "object", typeof doesn't trigger any trap
Reflect反射
Reflect是一个内置对象,它提供了可拦截 JavaScript 操作的方法。该方法和代理句柄 (en-US)类似,但Reflect
方法并不是一个函数对象。
Reflect
有助于将默认操作从处理程序转发到目标。
以Reflect.has()]为例,你可以将in 运算符作为函数:
Reflect.has(Object, "assign"); // true
更好的apply
函数
在 ES5 中,我们通常使用Function.prototype.apply()方法调用一个具有给定this
值和arguments
数组(或类数组对象)的函数。
Function.prototype.apply.call(Math.floor, undefined, [1.75]);
使用Reflect.apply,这变得不那么冗长和容易理解:
Reflect.apply(Math.floor, undefined, [1.75]);
// 1;
Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hello"
Reflect.apply(RegExp.prototype.exec, /ab/, ['confabulation']).index;
// 4
Reflect.apply(''.charAt, 'ponies', [3]);
// "i"
检查属性定义是否成功
使用Object.defineProperty,如果成功返回一个对象,否则抛出一个TypeError
,你将使用try...catch
块来捕获定义属性时发生的任何错误。因为Reflect.defineProperty返回一个布尔值表示的成功状态,你可以在这里使用if...else
块:
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}