原文:I. JavaScript Quick Start
译者:飞龙
协议:CC BY-NC-SA 4.0
这部分是 JavaScript 的一个独立快速介绍。你可以在不阅读本书中的其他内容的情况下理解它,本书的其他部分也不依赖于它的内容。然而,阅读本书的提示在阅读本书的提示中适用。
原文:1. Basic JavaScript
译者:飞龙
协议:CC BY-NC-SA 4.0
本章是关于“基本 JavaScript”,这是我为 JavaScript 的一个子集选择的名称,尽可能简洁,同时仍然能让你高效地工作。当你开始学习 JavaScript 时,我建议你在学习其他语言之前先在其中编程一段时间。这样,你就不必一次学习所有内容,这可能会让人困惑。
本节简要介绍了 JavaScript 的背景,以帮助你理解它为什么是这样的。
ECMAScript是 JavaScript 的官方名称。之所以需要一个新名称,是因为Java有商标(最初由 Sun 持有,现在由 Oracle 持有)。目前,Mozilla 是少数几家被允许正式使用JavaScript名称的公司之一,因为它很久以前就获得了许可证。对于常见用法,以下规则适用:
JavaScript意味着编程语言。
ECMAScript是语言规范的官方名称。因此,每当提到语言的版本时,人们都说ECMAScript。JavaScript 的当前版本是 ECMAScript 5;ECMAScript 6 目前正在开发中。
JavaScript 的创造者 Brendan Eich 别无选择,只能很快地创建这种语言(否则,Netscape 可能会采用其他更糟糕的技术)。他从几种编程语言中借鉴了一些东西:Java(语法,原始值与对象),Scheme 和 AWK(一级函数),Self(原型继承),以及 Perl 和 Python(字符串,数组和正则表达式)。
JavaScript 在 ECMAScript 3 之前没有异常处理,这就解释了为什么语言经常自动转换值并经常悄悄失败:最初它无法抛出异常。
一方面,JavaScript 有一些怪癖,缺少相当多的功能(块作用域变量,模块,支持子类等)。另一方面,它有几个强大的功能,可以让你解决这些问题。在其他语言中,你学习语言特性。在 JavaScript 中,你经常学习模式而不是语言特性。
鉴于它的影响,毫不奇怪 JavaScript 可以实现一种混合了函数式编程(高阶函数;内置的map
,reduce
等)和面向对象编程(对象,继承)的编程风格。
本节解释了 JavaScript 的基本语法原则。
一些语法的例子:
// Two slashes start single-line comments
var x; // declaring a variable
x = 3 + y; // assigning a value to the variable `x`
foo(x, y); // calling function `foo` with parameters `x` and `y`
obj.bar(3); // calling method `bar` of object `obj`
// A conditional statement
if (x === 0) { // Is `x` equal to zero?
x = 123;
}
// Defining function `baz` with parameters `a` and `b`
function baz(a, b) {
return a + b;
}
注意等号的两种不同用法:
单个等号(=
)用于将值赋给变量。
三个等号(===
)用于比较两个值(参见相等运算符)。
要理解 JavaScript 的语法,你应该知道它有两个主要的语法类别:语句和表达式:
语句“做事情”。程序是一系列语句。这是一个语句的例子,它声明(创建)一个变量foo
:
var foo;
表达式产生值。它们是函数参数,赋值的右侧等等。这是一个表达式的例子:
3 * 7
语句和表达式之间的区别最好通过 JavaScript 有两种不同的if-then-else
的方式来说明——作为语句:
var x;
if (y >= 0) {
x = y;
} else {
x = -y;
}
或作为一个表达式:
var x = y >= 0 ? y : -y;
你可以将后者用作函数参数(但不能使用前者):
myFunction(y >= 0 ? y : -y)
最后,无论 JavaScript 在哪里期望一个语句,你也可以使用一个表达式;例如:
foo(7, 1);
整行是一个语句(所谓的表达式语句),但函数调用foo(7, 1)
是一个表达式。
在 JavaScript 中,分号是可选的。但是,我建议始终包括它们,因为否则 JavaScript 可能会错误猜测语句的结束。详细信息请参见自动分号插入。
分号终止语句,但不终止块。有一种情况下,您会在块后看到一个分号:函数表达式是以块结尾的表达式。如果这样的表达式出现在语句的最后,它后面会跟着一个分号:
// Pattern: var _ = ___;
var x = 3 * 7;
var f = function () { }; // function expr. inside var decl.
JavaScript 有两种注释:单行注释和多行注释。单行注释以//
开头,并在行尾终止:
x++; // single-line comment
多行注释由/*
和*/
界定:
/* This is
a multiline
comment.
*/
在 JavaScript 中,变量在使用之前被声明:
var foo; // declare variable `foo`
您可以声明一个变量并同时赋值:
var foo = 6;
您也可以给现有变量赋值:
foo = 4; // change variable `foo`
有复合赋值运算符,比如+=
。以下两个赋值是等价的:
x += 1;
x = x + 1;
标识符是在 JavaScript 中扮演各种语法角色的名称。例如,变量的名称是标识符。标识符区分大小写。
大致而言,标识符的第一个字符可以是任何 Unicode 字母、美元符号($
)或下划线(_
)。随后的字符还可以是任何 Unicode 数字。因此,以下都是合法的标识符:
arg0
_tmp
$elem
π
以下标识符是保留字——它们是语法的一部分,不能用作变量名(包括函数名和参数名):
arguments |
break |
case |
catch |
---|---|---|---|
class |
const |
continue |
debugger |
default |
delete |
do |
else |
enum |
export |
extends |
false |
finally |
for |
function |
if |
implements |
import |
in |
instanceof |
interface |
let |
new |
null |
package |
private |
protected |
public |
return |
static |
super |
switch |
this |
throw |
true |
try |
typeof |
var |
void |
while |
以下三个标识符不是保留字,但您应该将它们视为保留字:
| Infinity
|
| NaN
|
| undefined
|
最后,您还应该避免使用标准全局变量的名称(参见第二十三章)。您可以将它们用于局部变量而不会破坏任何东西,但您的代码仍然会变得混乱。
JavaScript 有许多我们从编程语言中期望的值:布尔值、数字、字符串、数组等等。JavaScript 中的所有值都有属性。每个属性都有一个键(或名称)和一个值。您可以将属性视为记录的字段。您可以使用点(.
)运算符来读取属性:
value.propKey
例如,字符串'abc'
具有属性length
:
> var str = 'abc';
> str.length
3
前面的也可以写成:
> 'abc'.length
3
点运算符也用于给属性赋值:
> var obj = {}; // empty object
> obj.foo = 123; // create property `foo`, set it to 123
123
> obj.foo
123
您也可以用它来调用方法:
> 'hello'.toUpperCase()
'HELLO'
在上面的例子中,我们已经在值'hello'
上调用了方法toUpperCase()
。
JavaScript 在值之间做了一个相当武断的区分:
原始值是布尔值、数字、字符串、null
和undefined
。
所有其他值都是对象。
两者之间的一个主要区别是它们的比较方式;每个对象都有唯一的标识,并且只有(严格)等于自身:
> var obj1 = {}; // an empty object
> var obj2 = {}; // another empty object
> obj1 === obj2
false
> obj1 === obj1
true
相反,所有编码相同值的原始值都被视为相同:
> var prim1 = 123;
> var prim2 = 123;
> prim1 === prim2
true
接下来的两节将更详细地解释原始值和对象。
以下是所有原始值(或简称原始值):
布尔值:true
,false
(参见布尔值)
数字:1736
,1.351
(参见数字)
字符串:'abc'
,"abc"
(参见字符串)
两个“非值”:undefined
,null
(参见undefined 和 null)
原始值具有以下特征:
按值比较
“内容”进行比较:
> 3 === 3
true
> 'abc' === 'abc'
true
始终不可变
属性无法更改,添加或删除:
> var str = 'abc';
> str.length = 1; // try to change property `length`
> str.length // ⇒ no effect
3
> str.foo = 3; // try to create property `foo`
> str.foo // ⇒ no effect, unknown property
undefined
(读取未知属性始终返回undefined
。)
所有非原始值都是对象。最常见的对象类型是:
普通对象,可以通过对象字面量创建(参见单个对象):
{
firstName: 'Jane',
lastName: 'Doe'
}
前面的对象有两个属性:属性firstName
的值为'Jane'
,属性lastName
的值为'Doe'
。
数组,可以通过数组字面量创建(参见数组):
[ 'apple', 'banana', 'cherry' ]
前面的数组有三个元素,可以通过数字索引访问。例如,'apple’的索引是 0。
正则表达式,可以通过正则表达式字面量创建(参见正则表达式):
/^a+b+$/
对象具有以下特征:
按引用比较
进行身份比较;每个值都有自己的身份:
> {} === {} // two different empty objects
false
> var obj1 = {};
> var obj2 = obj1;
> obj1 === obj2
true
默认可变
通常可以自由更改,添加和删除属性(参见单个对象):
> var obj = {};
> obj.foo = 123; // add property `foo`
> obj.foo
123
大多数编程语言都有表示缺少信息的值。JavaScript 有两个这样的“非值”,undefined
和null
:
undefined
表示“没有值”。未初始化的变量是undefined
:
> var foo;
> foo
undefined
缺少参数是undefined
:
> function f(x) { return x }
> f()
undefined
```
如果读取不存在的属性,将得到`undefined`:
```js
> var obj = {}; // empty object
> obj.foo
undefined
```
+ `null`表示“没有对象”。每当期望对象时(参数,对象链中的最后一个等),它被用作非值。
### 警告
`undefined`和`null`没有属性,甚至没有标准方法,如`toString()`。
#### 检查 undefined 或 null
函数通常允许您通过`undefined`或`null`指示缺少值。您可以通过显式检查来做相同的事情:
```js
if (x === undefined || x === null) {
...
}
您还可以利用undefined
和null
都被视为false
的事实:
if (!x) {
...
}
false
,0
,NaN
和''
也被视为false
(参见真值和假值)。
有两个用于对值进行分类的运算符:typeof
主要用于原始值,而instanceof
用于对象。
typeof
看起来像这样:
typeof value
它返回描述value
“类型”的字符串。以下是一些示例:
> typeof true
'boolean'
> typeof 'abc'
'string'
> typeof {} // empty object literal
'object'
> typeof [] // empty array literal
'object'
以下表列出了typeof
的所有结果:
操作数 | 结果 |
---|---|
undefined |
'undefined' |
null |
'object' |
布尔值 | 'boolean' |
数字值 | 'number' |
字符串值 | 'string' |
函数 | 'function' |
所有其他正常值 | 'object' |
(引擎创建的值) | JavaScript 引擎允许创建值,其typeof 返回任意字符串(与此表中列出的所有结果都不同)。 |
typeof null
返回'object'
是一个无法修复的错误,因为这会破坏现有的代码。这并不意味着null
是一个对象。
instanceof
看起来像这样:
value instanceof Constr
如果value
是由构造函数Constr
创建的对象,则返回true
(参见构造函数:对象的工厂)。以下是一些示例:
> var b = new Bar(); // object created by constructor Bar
> b instanceof Bar
true
> {} instanceof Object
true
> [] instanceof Array
true
> [] instanceof Object // Array is a subconstructor of Object
true
> undefined instanceof Object
false
> null instanceof Object
false
原始布尔类型包括值true
和false
。以下运算符产生布尔值:
二进制逻辑运算符:&&
(与),||
(或)
前缀逻辑运算符:!
(非)
比较运算符:
相等运算符:===
,!==
,==
,!=
排序运算符(用于字符串和数字):>
, >=
, <
, <=
每当 JavaScript 期望布尔值(例如if
语句的条件)时,可以使用任何值。它将被解释为true
或false
。以下值被解释为false
:
undefined
,null
布尔值:false
数字:-0
,NaN
字符串:''
所有其他值(包括所有对象!)都被认为是true
。被解释为false
的值称为假值,被解释为true
的值称为真值。Boolean()
作为函数调用,将其参数转换为布尔值。您可以使用它来测试值的解释方式:
> Boolean(undefined)
false
> Boolean(0)
false
> Boolean(3)
true
> Boolean({}) // empty object
true
> Boolean([]) // empty array
true
JavaScript 中的二进制逻辑运算符是短路的。也就是说,如果第一个操作数足以确定结果,第二个操作数将不会被评估。例如,在以下表达式中,函数foo()
永远不会被调用:
false && foo()
true || foo()
此外,二进制逻辑运算符返回它们的操作数之一,这些操作数可能是布尔值也可能不是。使用真值检查来确定哪一个:
和(&&
)
如果第一个操作数为假值,则返回它。否则,返回第二个操作数:
> NaN && 'abc'
NaN
> 123 && 'abc'
'abc'
或(||
)
如果第一个操作数为真值,则返回它。否则,返回第二个操作数:
> 'abc' || 123
'abc'
> '' || 123
123
JavaScript 有两种相等性:
普通,或“宽松”,(不)相等:==
和!=
严格(不)相等:===
和!==
普通相等性认为太多的值是相等的(详细内容在普通(宽松)相等性(==,!=)中有解释),这可能会隐藏错误。因此,建议始终使用严格相等性。
JavaScript 中的所有数字都是浮点数:
> 1 === 1.0
true
特殊数字包括以下内容:
NaN
(“不是一个数字”)
一个错误值:
> Number('xyz') // 'xyz' can’t be converted to a number
NaN
Infinity
也是大多数错误值:
> 3 / 0
Infinity
> Math.pow(2, 1024) // number too large
Infinity
Infinity
大于任何其他数字(除了NaN
)。同样,-Infinity
小于任何其他数字(除了NaN
)。这使得这些数字在作为默认值时非常有用(例如,当你正在寻找最小值或最大值时)。
JavaScript 有以下算术运算符(参见算术运算符):
加法:number1 + number2
减法:number1 - number2
乘法:number1 * number2
除法:number1 / number2
余数:number1 % number2
增量:++variable
, variable++
递减:--variable
, variable--
否定:-value
转换为数字:+value
全局对象Math
(参见Math)通过函数提供更多的算术运算。
JavaScript 还有位操作的运算符(例如,位与;参见位运算符)。
字符串可以直接通过字符串字面量创建。这些字面量由单引号或双引号括起来。反斜杠(\
)转义字符并产生一些控制字符。以下是一些例子:
'abc'
"abc"
'Did she say "Hello"?'
"Did she say \"Hello\"?"
'That\'s nice!'
"That's nice!"
'Line 1\nLine 2' // newline
'Backlash: \\'
单个字符通过方括号访问:
> var str = 'abc';
> str[1]
'b'
属性length
计算字符串中的字符数:
> 'abc'.length
3
与所有原始值一样,字符串是不可变的;如果要更改现有字符串,需要创建一个新字符串。
字符串通过加号(+
)操作符进行连接,如果其中一个操作数是字符串,则将另一个操作数转换为字符串:
> var messageCount = 3;
> 'You have ' + messageCount + ' messages'
'You have 3 messages'
要在多个步骤中连接字符串,使用+=
操作符:
> var str = '';
> str += 'Multiple ';
> str += 'pieces ';
> str += 'are concatenated.';
> str
'Multiple pieces are concatenated.'
字符串有许多有用的方法(参见字符串原型方法)。以下是一些例子:
> 'abc'.slice(1) // copy a substring
'bc'
> 'abc'.slice(1, 2)
'b'
> '\t xyz '.trim() // trim whitespace
'xyz'
> 'mjölnir'.toUpperCase()
'MJÖLNIR'
> 'abc'.indexOf('b') // find a string
1
> 'abc'.indexOf('x')
-1
JavaScript 中的条件和循环在以下部分介绍。
if
语句有一个then
子句和一个可选的else
子句,根据布尔条件执行:
if (myvar === 0) {
// then
}
if (myvar === 0) {
// then
} else {
// else
}
if (myvar === 0) {
// then
} else if (myvar === 1) {
// else-if
} else if (myvar === 2) {
// else-if
} else {
// else
}
我建议始终使用大括号(它们表示零个或多个语句的块)。但如果一个子句只是一个语句,你不必这样做(对于控制流语句for
和while
也是如此):
if (x < 0) return -x;
以下是一个switch
语句。fruit
的值决定执行哪个case
:
switch (fruit) {
case 'banana':
// ...
break;
case 'apple':
// ...
break;
default: // all other cases
// ...
}
case
后的“操作数”可以是任何表达式;它通过===
与switch
的参数进行比较。
for
循环的格式如下:
for (⟦«init»⟧; ⟦«condition»⟧; ⟦«post_iteration»⟧)
«statement»
init
在循环开始时执行。在每次循环迭代之前检查condition
;如果变为false
,则终止循环。post_iteration
在每次循环迭代后执行。
此示例在控制台上打印数组arr
的所有元素:
for (var i=0; i < arr.length; i++) {
console.log(arr[i]);
}
while
循环在其条件成立时继续循环其主体:
// Same as for loop above:
var i = 0;
while (i < arr.length) {
console.log(arr[i]);
i++;
}
do-while
循环在其条件成立时继续循环其主体。由于条件跟随主体,因此主体始终至少执行一次:
do {
// ...
} while (condition);
在所有循环中:
break
离开循环。
continue
开始新的循环迭代。
定义函数的一种方式是通过函数声明:
function add(param1, param2) {
return param1 + param2;
}
前面的代码定义了一个函数add
,它有两个参数param1
和param2
,并返回这两个参数的总和。这是如何调用该函数的:
> add(6, 1)
7
> add('a', 'b')
'ab'
定义add()
的另一种方式是通过将函数表达式分配给变量add
:
var add = function (param1, param2) {
return param1 + param2;
};
函数表达式产生一个值,因此可以直接用于将函数作为参数传递给其他函数:
someOtherFunction(function (p1, p2) { ... });
函数声明是提升的-完整地移动到当前范围的开头。这允许您引用稍后声明的函数:
function foo() {
bar(); // OK, bar is hoisted
function bar() {
...
}
}
请注意,虽然var
声明也被提升(参见变量被提升),但是它们执行的赋值不会:
function foo() {
bar(); // Not OK, bar is still undefined
var bar = function () {
// ...
};
}
您可以使用任意数量的参数调用 JavaScript 中的任何函数;语言永远不会抱怨。但是,它将使所有参数通过特殊变量arguments
可用。arguments
看起来像一个数组,但没有数组方法:
> function f() { return arguments }
> var args = f('a', 'b', 'c');
> args.length
3
> args[0] // read element at index 0
'a'
让我们使用以下函数来探索 JavaScript 中如何处理太多或太少的参数(函数toArray()
显示在将参数转换为数组中):
function f(x, y) {
console.log(x, y);
return toArray(arguments);
}
将忽略额外的参数(除了arguments
):
> f('a', 'b', 'c')
a b
[ 'a', 'b', 'c' ]
缺少参数将获得值undefined
:
> f('a')
a undefined
[ 'a' ]
> f()
undefined undefined
[]
以下是为参数分配默认值的常见模式:
function pair(x, y) {
x = x || 0; // (1)
y = y || 0;
return [ x, y ];
}
在第(1)行,||
运算符返回x
,如果它是真值(不是null
,undefined
等)。否则,它将返回第二个操作数:
> pair()
[ 0, 0 ]
> pair(3)
[ 3, 0 ]
> pair(3, 5)
[ 3, 5 ]
如果要强制执行arity(特定数量的参数),可以检查arguments.length
:
function pair(x, y) {
if (arguments.length !== 2) {
throw new Error('Need exactly 2 arguments');
}
...
}
arguments
不是数组,它只是类似数组(参见类似数组对象和通用方法)。它有一个length
属性,您可以通过方括号中的索引访问其元素。但是,您无法删除元素或调用其中任何数组方法。因此,有时需要将arguments
转换为数组,这就是以下函数所做的事情(它在类似数组对象和通用方法中有解释):
function toArray(arrayLikeObject) {
return Array.prototype.slice.call(arrayLikeObject);
}
处理异常的最常见方法(参见第十四章)如下:
function getPerson(id) {
if (id < 0) {
throw new Error('ID must not be negative: '+id);
}
return { id: id }; // normally: retrieved from database
}
function getPersons(ids) {
var result = [];
ids.forEach(function (id) {
try {
var person = getPerson(id);
result.push(person);
} catch (exception) {
console.log(exception);
}
});
return result;
}
try
子句包围关键代码,如果在try
子句内抛出异常,则执行catch
子句。使用前面的代码:
> getPersons([2, -5, 137])
[Error: ID must not be negative: -5]
[ { id: 2 }, { id: 137 } ]
严格模式(参见严格模式)启用更多警告,并使 JavaScript 成为一种更干净的语言(非严格模式有时被称为“松散模式”)。要打开它,请首先在 JavaScript 文件或标记中键入以下行:
'use strict';
您还可以为每个函数启用严格模式:
function functionInStrictMode() {
'use strict';
}
在 JavaScript 中,你在使用变量之前通过var
声明变量:
> var x;
> x = 3;
> y = 4;
ReferenceError: y is not defined
你可以使用单个var
语句声明和初始化多个变量:
var x = 1, y = 2, z = 3;
但我建议每个变量使用一个语句(原因在Syntax中有解释)。因此,我会重写上一个语句为:
var x = 1;
var y = 2;
var z = 3;
由于提升(参见变量被提升),通常最好在函数的开头声明变量。
变量的作用域总是整个函数(而不是当前的块)。例如:
function foo() {
var x = -512;
if (x < 0) { // (1)
var tmp = -x;
...
}
console.log(tmp); // 512
}
我们可以看到变量tmp
不仅限于从第(1)行开始的块;它存在直到函数的结束。
每个变量声明都是提升的:声明被移动到函数的开头,但它所做的赋值保持不变。例如,考虑下面函数中第(1)行的变量声明:
function foo() {
console.log(tmp); // undefined
if (false) {
var tmp = 3; // (1)
}
}
在内部,前面的函数是这样执行的:
function foo() {
var tmp; // hoisted declaration
console.log(tmp);
if (false) {
tmp = 3; // assignment stays put
}
}
每个函数都与包围它的函数的变量保持连接,即使它离开了被创建的作用域。例如:
function createIncrementor(start) {
return function () { // (1)
start++;
return start;
}
}
从第(1)行开始的函数离开了它被创建的上下文,但仍然连接到start
的一个活动版本:
> var inc = createIncrementor(5);
> inc()
6
> inc()
7
> inc()
8
闭包是一个函数加上与其周围作用域的变量的连接。因此,createIncrementor()
返回的是一个闭包。
有时你想引入一个新的变量作用域——例如,防止一个变量成为全局的。在 JavaScript 中,你不能使用块来做到这一点;你必须使用一个函数。但是有一种使用函数的块状方式的模式。它被称为IIFE(立即调用函数表达式,发音为“iffy”):
(function () { // open IIFE
var tmp = ...; // not a global variable
}()); // close IIFE
确保按照前面的示例精确地输入(除了注释)。IIFE 是一个在定义后立即调用的函数表达式。在函数内部,存在一个新的作用域,防止tmp
成为全局的。请参阅IIFE 引入新的作用域了解 IIFE 的详细信息。
闭包保持与外部变量的连接,有时这并不是你想要的:
var result = [];
for (var i=0; i < 5; i++) {
result.push(function () { return i }); // (1)
}
console.log(result1; // 5 (not 1)
console.log(result3; // 5 (not 3)
第(1)行返回的值始终是i
的当前值,而不是函数创建时的值。循环结束后,i
的值为 5,这就是为什么数组中的所有函数都返回该值。如果你想让第(1)行的函数接收当前i
值的快照,你可以使用 IIFE:
for (var i=0; i < 5; i++) {
(function () {
var i2 = i; // copy current i
result.push(function () { return i2 });
}());
}
本节涵盖了 JavaScript 的两种基本面向对象的机制:单个对象和构造函数(它们是对象的工厂,类似于其他语言中的类)。
像所有的值一样,对象都有属性。实际上,你可以把对象看作是一组属性,其中每个属性都是一个(键,值)对。键是一个字符串,值是任何 JavaScript 值。
在 JavaScript 中,你可以直接通过对象字面量创建普通对象:
'use strict';
var jane = {
name: 'Jane',
describe: function () {
return 'Person named '+this.name;
}
};
前面的对象有属性name
和describe
。你可以读取(“获取”)和写入(“设置”)属性:
> jane.name // get
'Jane'
> jane.name = 'John'; // set
> jane.newProperty = 'abc'; // property created automatically
像describe
这样的函数值属性被称为方法。它们使用this
来引用调用它们的对象:
> jane.describe() // call method
'Person named John'
> jane.name = 'Jane';
> jane.describe()
'Person named Jane'
in
运算符检查一个属性是否存在:
> 'newProperty' in jane
true
> 'foo' in jane
false
如果读取一个不存在的属性,你会得到值undefined
。因此,前面的两个检查也可以这样执行:
> jane.newProperty !== undefined
true
> jane.foo !== undefined
false
delete
运算符移除一个属性:
> delete jane.newProperty
true
> 'newProperty' in jane
false
属性键可以是任何字符串。到目前为止,我们已经在对象文字和点运算符之后看到了属性键。但是,只有在它们是标识符时,才能以这种方式使用它们(参见Identifiers and Variable Names)。如果要使用其他字符串作为键,必须在对象文字中对其进行引用,并使用方括号来获取和设置属性:
> var obj = { 'not an identifier': 123 };
> obj['not an identifier']
123
> obj['not an identifier'] = 456;
方括号还允许您计算属性的键:
> var obj = { hello: 'world' };
> var x = 'hello';
> obj[x]
'world'
> obj['hel'+'lo']
'world'
如果提取一个方法,它将失去与对象的连接。单独使用时,该函数不再是一个方法,this
的值为 undefined
(在严格模式下)。
例如,让我们回到之前的对象 jane
:
'use strict';
var jane = {
name: 'Jane',
describe: function () {
return 'Person named '+this.name;
}
};
我们想从 jane
中提取方法 describe
,将其放入变量 func
中,并调用它。但是,这样做不起作用:
> var func = jane.describe;
> func()
TypeError: Cannot read property 'name' of undefined
解决方案是使用所有函数都具有的 bind()
方法。它创建一个新函数,其 this
始终具有给定值:
> var func2 = jane.describe.bind(jane);
> func2()
'Person named Jane'
每个函数都有自己的特殊变量 this
。如果在方法内部嵌套函数,这是不方便的,因为您无法从函数中访问方法的 this
。以下是一个示例,我们调用 forEach
以使用函数遍历数组:
var jane = {
name: 'Jane',
friends: [ 'Tarzan', 'Cheeta' ],
logHiToFriends: function () {
'use strict';
this.friends.forEach(function (friend) {
// `this` is undefined here
console.log(this.name+' says hi to '+friend);
});
}
}
调用 logHiToFriends
会产生一个错误:
> jane.logHiToFriends()
TypeError: Cannot read property 'name' of undefined
让我们看看修复这个问题的两种方法。首先,我们可以将 this
存储在不同的变量中:
logHiToFriends: function () {
'use strict';
var that = this;
this.friends.forEach(function (friend) {
console.log(that.name+' says hi to '+friend);
});
}
或者,forEach
有一个第二个参数,允许您为 this
提供一个值:
logHiToFriends: function () {
'use strict';
this.friends.forEach(function (friend) {
console.log(this.name+' says hi to '+friend);
}, this);
}
在 JavaScript 中,函数表达式经常用作函数调用中的参数。当您从这些函数表达式之一引用 this
时,一定要小心。
到目前为止,您可能认为 JavaScript 对象 只 是从字符串到值的映射,这是 JavaScript 对象文字所暗示的概念,它看起来像其他语言的映射/字典文字。但是,JavaScript 对象还支持一项真正面向对象的功能:继承。本节并未完全解释 JavaScript 继承的工作原理,但它向您展示了一个简单的模式,以便您开始。如果您想了解更多,请参阅第十七章。
除了作为“真正的”函数和方法外,函数在 JavaScript 中还扮演另一个角色:如果通过 new
运算符调用,它们将成为 构造函数——对象的工厂。因此,构造函数在其他语言中是类的粗略类比。按照惯例,构造函数的名称以大写字母开头。例如:
// Set up instance data
function Point(x, y) {
this.x = x;
this.y = y;
}
// Methods
Point.prototype.dist = function () {
return Math.sqrt(this.x*this.x + this.y*this.y);
};
我们可以看到构造函数有两个部分。首先,函数 Point
设置实例数据。其次,属性 Point.prototype
包含一个具有方法的对象。前者数据对每个实例都是特定的,而后者数据在所有实例之间共享。
要使用 Point
,我们通过 new
运算符调用它:
> var p = new Point(3, 5);
> p.x
3
> p.dist()
5.830951894845301
p
是 Point
的一个实例:
> p instanceof Point
true
数组是可以通过从零开始的整数索引访问的元素序列。
数组文字对于创建数组很方便:
> var arr = [ 'a', 'b', 'c' ];
前面的数组有三个元素:字符串 'a'
、'b'
和 'c'
。您可以通过整数索引访问它们:
> arr[0]
'a'
> arr[0] = 'x';
> arr
[ 'x', 'b', 'c' ]
length
属性指示数组有多少个元素。您可以使用它来追加元素和删除元素:
> var arr = ['a', 'b'];
> arr.length
2
> arr[arr.length] = 'c';
> arr
[ 'a', 'b', 'c' ]
> arr.length
3
> arr.length = 1;
> arr
[ 'a' ]
in
运算符也适用于数组:
> var arr = [ 'a', 'b', 'c' ];
> 1 in arr // is there an element at index 1?
true
> 5 in arr // is there an element at index 5?
false
请注意,数组是对象,因此可以具有对象属性:
> var arr = [];
> arr.foo = 123;
> arr.foo
123
数组有许多方法(参见Array Prototype Methods)。以下是一些示例:
> var arr = [ 'a', 'b', 'c' ];
> arr.slice(1, 2) // copy elements
[ 'b' ]
> arr.slice(1)
[ 'b', 'c' ]
> arr.push('x') // append an element
4
> arr
[ 'a', 'b', 'c', 'x' ]
> arr.pop() // remove last element
'x'
> arr
[ 'a', 'b', 'c' ]
> arr.shift() // remove first element
'a'
> arr
[ 'b', 'c' ]
> arr.unshift('x') // prepend an element
3
> arr
[ 'x', 'b', 'c' ]
> arr.indexOf('b') // find the index of an element
1
> arr.indexOf('y')
-1
> arr.join('-') // all elements in a single string
'x-b-c'
> arr.join('')
'xbc'
> arr.join()
'x,b,c'
有几种用于遍历元素的数组方法(参见Iteration (Nondestructive))。最重要的两个是 forEach
和 map
。
forEach
遍历数组并将当前元素及其索引传递给函数:
[ 'a', 'b', 'c' ].forEach(
function (elem, index) { // (1)
console.log(index + '. ' + elem);
});
前面的代码产生以下输出:
0\. a
1\. b
2\. c
请注意,第(1)行中的函数可以忽略参数。例如,它可能只有参数elem
。
map
通过将函数应用于现有数组的每个元素来创建一个新数组:
> [1,2,3].map(function (x) { return x*x })
[ 1, 4, 9 ]
JavaScript 内置支持正则表达式(第十九章是教程,更详细地解释了它们的工作原理)。它们由斜杠分隔:
/^abc$/
/[A-Za-z0-9]+/
> /^a+b+$/.test('aaab')
true
> /^a+b+$/.test('aaa')
false
> /a(b+)a/.exec('_abbba_aba_')
[ 'abbba', 'bbb' ]
返回的数组包含索引 0 处的完整匹配项,索引 1 处的第一个组的捕获,依此类推。还有一种方法(在RegExp.prototype.exec: Capture Groups中讨论)可以重复调用此方法以获取所有匹配项。
> ' ' .replace(/<(.*?)>/g, '[$1]')
'[a] [bbb]'
replace
的第一个参数必须是带有/g
标志的正则表达式;否则,只会替换第一个匹配项。还有一种方法(如在String.prototype.replace: Search and Replace中讨论的)可以使用函数来计算替换。
Math
(参见第二十一章)是一个具有算术函数的对象。以下是一些示例:
> Math.abs(-2)
2
> Math.pow(3, 2) // 3 to the power of 2
9
> Math.max(2, -1, 5)
5
> Math.round(1.9)
2
> Math.PI // pre-defined constant for π
3.141592653589793
> Math.cos(Math.PI) // compute the cosine for 180°
-1
JavaScript 的标准库相对简陋,但还有更多可以使用的东西:
Date
(第二十章)
一个日期的构造函数,其主要功能是解析和创建日期字符串以及访问日期的组件(年、小时等)。
JSON
(第二十二章)
一个具有解析和生成 JSON 数据功能的对象。
console.*
方法(参见控制台 API)
这些特定于浏览器的方法不是语言本身的一部分,但其中一些也适用于 Node.js。
原文:II. Background
译者:飞龙
协议:CC BY-NC-SA 4.0
这部分解释了 JavaScript 的历史和性质。它对语言进行了广泛的初步介绍,并解释了它存在的背景(不过不涉及太多技术细节)。
这部分不是必读的;你可以在没有阅读它的情况下理解本书的其余部分。
原文:2. Why JavaScript?
译者:飞龙
协议:CC BY-NC-SA 4.0
有很多编程语言。为什么你要使用 JavaScript?本章将从七个重要方面来看,这些方面在你选择编程语言时很重要,并且认为 JavaScript 总体上做得很好:
它是免费提供的吗?
它是一种优雅的编程语言吗?
在实践中有用吗?
它有好的工具,特别是好的集成开发环境(IDE)吗?
它对你想做的事情来说足够快吗?
它被广泛使用吗?
它有未来吗?
JavaScript 可以说是最开放的编程语言:它的规范 ECMA-262 是 ISO 标准。许多独立方实现都紧密遵循这一规范。其中一些实现是开源的。此外,语言的演变由 TC39 委员会负责,该委员会由包括所有主要浏览器供应商在内的几家公司组成。其中许多公司通常是竞争对手,但他们为了语言的利益而共同合作。
是和不是。我用不同范式的几种编程语言写了大量代码。因此,我很清楚 JavaScript 并不是优雅的巅峰。然而,它是一种非常灵活的语言,有一个相当优雅的核心,并且使你能够使用面向对象编程和函数式编程的混合。
JavaScript 引擎之间的语言兼容性曾经是一个问题,但现在不再是了,部分得益于测试 262 套件,该套件检查引擎是否符合 ECMAScript 规范。相比之下,浏览器和 DOM 的差异仍然是一个挑战。这就是为什么通常最好依赖框架来隐藏这些差异。
世界上最美丽的编程语言是无用的,除非它能让你编写你需要的程序。
在图形用户界面领域,JavaScript 受益于成为HTML5的一部分。在本节中,我使用 HTML5 这个术语来表示“浏览器平台”(HTML、CSS 和浏览器 JavaScript API)。HTML5 得到了广泛的部署,并不断取得进展。它正在慢慢地成为一个完整的层,用于编写功能齐全的跨平台应用程序;类似于 Java 平台,它几乎像一个嵌入式操作系统。HTML5 的卖点之一是它让你编写跨平台的图形用户界面。这些总是一种妥协:你放弃了一些质量,以换取不受限于单一操作系统。过去,“跨平台”意味着 Windows、Mac OS 或 Linux。但现在我们有了两个额外的交互平台:Web 和移动。使用 HTML5,你可以通过诸如PhoneGap、Chrome Apps和TideSDK等技术来针对所有这些平台。
此外,一些平台将 Web 应用程序作为本地应用程序或允许你本地安装它们——例如 Chrome OS、Firefox OS 和 Android。
除了 HTML5 之外,还有更多的技术可以补充 JavaScript,使语言更有用:
库
JavaScript 有大量的库,可以让你完成各种任务,从解析 JavaScript(通过Esprima)到处理和显示 PDF 文件(通过PDF.js)。
Node.js
Node.js 平台让你编写服务器端代码和 shell 脚本(构建工具、测试运行器等)。
JSON(JavaScript 对象表示法,在第二十二章中介绍)
JSON 是一种根植于 JavaScript 的数据格式,在 Web 上交换数据变得流行(例如网络服务的结果)。
NoSQL 数据库(例如CouchDB和MongoDB)
这些数据库紧密集成了 JSON 和 JavaScript。
JavaScript 正在获得更好的构建工具(例如Grunt)和测试工具(例如mocha)。Node.js 使得可以通过 shell 运行这些类型的工具(不仅仅在浏览器中)。在这个领域的一个风险是分裂,因为我们逐渐得到了太多这样的工具。
JavaScript 的 IDE 空间仍处于萌芽阶段,但正在迅速成长。网络开发的复杂性和动态性使得这个空间成为创新的肥沃土壤。两个开源的例子是Brackets和Light Table。
此外,浏览器正变得越来越强大的开发环境。特别是 Chrome 最近取得了令人印象深刻的进展。有趣的是看到 IDE 和浏览器在未来将整合到多大程度。
JavaScript 引擎取得了巨大的进步,从缓慢的解释器发展为快速的即时编译器。它们现在已经足够快,适用于大多数应用。此外,已经在开发新的想法,使 JavaScript 足够快以适用于其余的应用:
asm.js是 JavaScript 的(非常静态的)子集,在当前引擎上运行速度很快,大约相当于编译后的 C++的 70%。例如,它可以用于实现网络应用的性能关键算法部分。它还被用于将基于 C++的游戏移植到网络平台上。
ParallelJS可以并行化使用新数组方法mapPar
、filterPar
和reducePar
(现有数组方法map
、filter
和reduce
的可并行化版本)的 JavaScript 代码。为了使并行化工作,回调必须以特殊的方式编写;主要限制是不能改变在回调中未创建的数据。
通常广泛使用的语言有两个好处。首先,这样的语言有更好的文档和支持。其次,更多的程序员知道它,这在你需要雇佣某人或者寻找基于该语言的工具的客户时非常重要。
JavaScript 被广泛使用,并获得了前述两个好处:
如今,JavaScript 的文档和支持以各种形式呈现:书籍、播客、博客文章、电子邮件通讯、论坛等等。第三十三章指引您前往重要资源。
JavaScript 开发人员需求量大,但他们的人数也在不断增加。
有几件事表明 JavaScript 有一个光明的未来:
语言正在稳步发展;ECMAScript 6 看起来不错。
有许多与 JavaScript 相关的创新(例如前述的 asm.js 和 ParallelJS,微软的 TypeScript 等)。
JavaScript 作为一个不可或缺的部分所在的网络平台正在迅速成熟。
JavaScript 得到了众多公司的支持,没有单个人或公司控制它。
考虑到使一种语言具有吸引力的前述因素,JavaScript 的表现非常出色。它当然并不完美,但目前很难超越它,而且情况只会变得更好。
原文:3. The Nature of JavaScript
译者:飞龙
协议:CC BY-NC-SA 4.0
JavaScript 的本质可以总结如下:
它是动态的
许多东西都可以改变。例如,你可以自由地添加和删除对象的属性(字段)。而且你可以直接创建对象,而不需要先创建对象工厂(例如类)。
它是动态类型的
变量和对象属性始终可以保存任何类型的值。
它是功能性的和面向对象的
JavaScript 支持两种编程语言范式:函数式编程(一流函数、闭包、通过bind()
进行部分应用、数组的内置map()
和reduce()
等)和面向对象编程(可变状态、对象、继承等)。
它默默失败
直到 ECMAScript 3,JavaScript 才没有异常处理。这就解释了为什么语言经常默默失败并自动转换参数和操作数的值:它最初无法抛出异常。
它部署为源代码
JavaScript 始终以源代码部署,并由 JavaScript 引擎编译。源代码具有灵活的交付格式和抽象引擎之间的差异的好处。为了保持文件大小小,使用了两种技术:压缩(主要是 gzip)和最小化(通过重命名变量、删除注释等使源代码更小;有关详细信息,请参见第三十二章)。
它是 Web 平台的一部分
JavaScript 是 Web 平台(HTML5 API、DOM 等)的一个重要组成部分,以至于很容易忘记前者也可以在没有后者的情况下使用。然而,JavaScript 在非浏览器环境中的使用越多(如 Node.js),它就越明显。
一方面,JavaScript 有一些怪癖和缺失的功能(例如,它没有块作用域变量,没有内置模块,也不支持子类化)。因此,在其他语言中学习语言特性的地方,你需要在 JavaScript 中学习模式和解决方法。另一方面,JavaScript 包括非正统的特性(如原型继承和对象属性)。这些也需要学习,但更像是一种特性而不是错误。
请注意,JavaScript 引擎已经变得非常智能,并在幕后修复了一些怪癖。例如:
就规范而言,JavaScript 没有整数,只有浮点数。在内部,大多数引擎尽可能使用整数。
可以说,JavaScript 中的数组太灵活了:它们不是元素的索引序列,而是从数字到元素的映射。这样的映射可以有空洞:数组“内部”没有关联值的索引。再次,引擎通过使用优化表示来帮助数组不具有空洞。
但 JavaScript 也有许多优雅的部分。Brendan Eich 最喜欢的是:¹
一流函数
闭包
原型
对象字面量
数组字面量
最后两个项目,对象字面量和数组字面量,让你可以从对象开始,并在后来引入抽象(比如构造函数,JavaScript 中类的类比)。它们还支持 JSON(见第二十二章)。
请注意,优雅的部分可以帮助你解决怪癖。例如,它们允许你在语言内部实现块作用域、模块和继承 API。
JavaScript 受到了几种编程语言的影响(如[图 3-1](ch03.html#fig3-1 “图 3-1: 影响 JavaScript 的编程语言。”)所示):
Java 是 JavaScript 语法的榜样。它还导致 JavaScript 将值分为原始值和对象,并引入了Date
构造函数(这是java.util.Date
的一个移植)。
AWK 启发了 JavaScript 的函数。实际上,关键字function
来自 AWK。
Scheme 是 JavaScript 拥有一流函数(它们被视为值并且可以作为参数传递给函数)和闭包(见第十六章)的原因。
Self 对 JavaScript 不寻常的对象导向风格负有责任;它支持对象之间的原型继承。
Perl 和 Python 影响了 JavaScript 对字符串、数组和正则表达式的处理。
除了实际的语言之外,HyperTalk 影响了 JavaScript 如何集成到 Web 浏览器中。这导致 HTML 标签具有事件处理属性,如onclick
。
¹ Brendan Eich,“JavaScript 简史”,2010 年 7 月 21 日,bit.ly/1lKkI0M
。
原文:4. How JavaScript Was Created
译者:飞龙
协议:CC BY-NC-SA 4.0
了解 JavaScript 的创建原因和方式有助于我们理解它的特点。
1993 年,NCSA 的 Mosaic 是第一个广受欢迎的 Web 浏览器。1994 年,成立了一家名为网景的公司,以利用新兴的万维网的潜力。网景创建了专有的 Web 浏览器 Netscape Navigator,在 1990 年代占主导地位。许多最初的 Mosaic 作者继续在 Navigator 上工作,但两者故意没有共享代码。
网景很快意识到 Web 需要变得更加动态。即使你只是想检查用户在表单中输入的正确值,也需要将数据发送到服务器以便提供反馈。1995 年,网景聘请了 Brendan Eich,并承诺让他在浏览器中实现 Scheme(一种 Lisp 方言)。²在他开始之前,网景与硬件和软件公司 Sun(后来被 Oracle 收购)合作,将其更静态的编程语言 Java 包含在 Navigator 中。因此,网景内部激烈争论的一个问题是为什么 Web 需要两种编程语言:Java 和一种脚本语言。脚本语言的支持者提出了以下解释:³
我们旨在为 Web 设计师和兼职程序员提供“粘合语言”,他们正在从图像、插件和 Java 小程序等组件构建 Web 内容。我们认为 Java 是由高价程序员使用的“组件语言”,而粘合程序员——Web 页面设计师——将组件组装起来,并使用[一种脚本语言]自动化它们的交互。
当时,网景管理层决定脚本语言必须具有类似于 Java 的语法。这排除了采用现有的语言,如 Perl、Python、TCL 或 Scheme。为了捍卫 JavaScript 的想法,网景需要一个原型。艾奇在 1995 年 5 月的 10 天内写了一个原型。JavaScript 的第一个代号是 Mocha,由马克·安德森创造。后来,网景营销部门出于商标原因和因为几个产品的名称已经带有前缀“Live”,将其更改为 LiveScript。1995 年 11 月底,Navigator 2.0B3 发布,包括原型,继续在早期没有进行重大更改的情况下存在。1995 年 12 月初,Java 的势头增长,Sun 授权商标 Java 给网景。语言再次更名为最终名称 JavaScript。⁴
² Brendan Eich,“流行程度”,2008 年 4 月 3 日,bit.ly/1lKl6fG
。
³ Naomi Hamilton,“编程语言 A-Z:JavaScript”,Computerworld,2008 年 7 月 30 日,bit.ly/1lKldIe
。
⁴ Paul Krill,“JavaScript 创造者思考过去和未来”,InfoWorld,2008 年 6 月 23 日,bit.ly/1lKlpXO
;Brendan Eich,“JavaScript 简史”,2010 年 7 月 21 日,bit.ly/1lKkI0M
。
原文:5. Standardization: ECMAScript
译者:飞龙
协议:CC BY-NC-SA 4.0
JavaScript 推出后,微软在 Internet Explorer 3.0(1996 年 8 月)中实现了相同的语言,但名称不同,称为 JScript。为了限制微软,网景决定标准化 JavaScript,并要求标准组织 Ecma International 托管标准。ECMA-262 的规范工作始于 1996 年 11 月。由于 Sun(现在是 Oracle)对 Java 一词拥有商标,因此标准化的官方名称不能是 JavaScript。因此,选择了 ECMAScript,源自 JavaScript 和 Ecma。但是,该名称仅用于指代语言的版本(其中一个指的是规范)。每个人仍然称该语言为 JavaScript。
ECMA-262 由 Ecma 的 技术委员会 39(TC39)管理和发展。其成员是微软、Mozilla 和 Google 等公司,它们指定员工参与委员会工作;例如 Brendan Eich、Allen Wirfs-Brock(ECMA-262 的编辑)和 David Herman。为了推进 ECMAScript 的设计,TC39 在开放渠道(如邮件列表 es-discuss)上举行讨论,并定期举行会议。会议由 TC39 成员和受邀专家参加。2013 年初,与会人数从 15 到 25 人不等。
以下是 ECMAScript 版本(或 ECMA-262 的 版本)及其主要特性的列表:
ECMAScript 1(1997 年 6 月)
第一版
ECMAScript 2(1998 年 8 月)
编辑更改以使 ECMA-262 与标准 ISO/IEC 16262 保持一致
ECMAScript 3(1999 年 12 月)
do-while
,正则表达式,新的字符串方法(concat
,match
,replace
,slice
,使用正则表达式的split
等),异常处理等
ECMAScript 4(2008 年 7 月放弃)
ECMAScript 4 被开发为 JavaScript 的下一个版本,原型是用 ML 编写的。然而,TC39 无法就其功能集达成一致。为防止僵局,委员会于 2008 年 7 月底举行会议,并达成了一致,总结在 四点 中:
开发了 ECMAScript 3 的增量更新(成为 ECMAScript 5)。
开发了一个比 ECMAScript 4 更少,但比 ECMAScript 3 的增量更新更多的主要新版本。新版本的代号是 Harmony,因为它的构思是在一个和谐的会议中产生的。Harmony 将分为 ECMAScript 6 和 ECMAScript 7。
ECMAScript 4 中将被删除的特性包括包,命名空间和早期绑定。
其他想法将与 TC39 共识开发。
因此,ECMAScript 4 的开发人员同意使 Harmony 比 ECMAScript 4 更少激进,而 TC39 的其他成员同意继续推动事情向前发展。
ECMAScript 5(2009 年 12 月)
添加了严格模式,获取器和设置器,新的数组方法,对 JSON 的支持等(参见 第二十五章)
ECMAScript 5.1(2011 年 6 月)
编辑更改以使 ECMA-262 与国际标准 ISO/IEC 16262:2011 的第三版保持一致
ECMAScript 6
目前正在开发中,预计将在 2014 年底得到批准。大多数引擎可能会在批准时支持最重要的 ECMAScript 6 特性。完整支持可能需要更长时间。
达成共识并创建标准并不总是容易的,但由于前述各方的协作努力,JavaScript 是一种真正开放的语言,由多个供应商实现,具有非常高的兼容性。这种兼容性是通过非常详细但具体的规范实现的。例如,规范经常使用伪代码,并且它由一个测试套件test262补充,该测试套件检查 ECMAScript 实现的兼容性。有趣的是,ECMAScript 并不由万维网联盟(W3C)管理。TC39 和 W3C 在 JavaScript 和 HTML5 之间存在重叠时进行合作。