字符串有许多有用的方法。例如:
> 'abc'.slice(1) // 复制子字符串
'bc'
> 'abc'.slice(1, 2)
'b'
> '\t xyz '.trim() // 移除空白字符
'xyz'
> 'mjölnir'.toUpperCase()
'MJÖLNIR'
> 'abc'.indexOf('b') // 查找字符串
1
> 'abc'.indexOf('x')
-1
函数声明会被提升,他们全被移动到当前作用域开始之处。这允许你在函数声明之前调用它们:
function foo() {
bar(); // 没问题,bar被提升
function bar() {
...
}
}
注意:虽然变量声明也会被提升,但赋值的过程不会被提升:
function foo() {
bar(); // 有问题,bar是undefined
var bar = function () {
// ...
};
}
在JavaScript中你可以调用任意函数并传递任意数量的参数——语言绝不会抱怨(参数检测)。都可以正常工作,然而,使所有参数可访问需要通过特殊变量 arguments。arguments 看起来像数组,但它没有数组的方法(称为类数组 array-like)。
> function f() { return arguments }
> var args = f('a', 'b', 'c');
> args.length
3
> args[0] // 获取索引为0的元素
'a'
让我们通过下面的函数探索JavaScript中传递太多或太少参数时如何处理(函数 toArray在后面提到)
function f(x, y) {
console.log(x, y);
console.log(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; // (*)
y = y || 0;
return [ x, y ];
}
在(*)这行,如果x是真值(除了:null,undefined 等), 操作符返回x。否则,它返回第二个操作数。
> pair()
[ 0, 0 ]
> pair(3)
[ 3, 0 ]
> pair(3, 5)
[ 3, 5 ]
如果你想强制参数的数量,你可以检测arguments.length:
function pair(x, y) {
if (arguments.length !== 2) {
throw new Error('Need exactly 2 arguments');
}
...
}
arguments 不是一个数组,它仅仅是类数组(array-like):它有一个length属性,并且你可以通过方括号索引方式访问它的元素。然而,你不能移除元素,或在它上面调用任何数组方法。因此,有时你需要将其转换为数组。这就是下面函数的作用。
function toArray(arrayLikeObject) {
return [].slice.call(arrayLikeObject);
}
严格模式开启检测和一些其他措施,使JavaScript变成更整洁的语言。推荐使用严格模式。为了开启严格模式,只需在JavaScript文件或script标签第一行添加如下语句:
'use strict';
你也可以在每个函数上选择性开启严格模式,只需将上面的代码放在函数的开头:
function functionInStrictMode() {
'use strict';
}
下面的两小节看下严格模式的三大好处。
让我们看一个例子,严格模式给我们明确的错误,否则JavaScript总是静默失败:下面的函数 f() 执行一些非法操作,它试图更改所有字符串都有的只读属性——length:
function f() {
'abc'.length = 5;
}
当你调用上面的函数,它静默失败,赋值操作被简单忽略。让我们将 f() 在严格模式下运行:
function f_strict() {
'use strict';
'abc'.length = 5;
}
现在浏览器报给我们一些错误:
> f_strict()
TypeError: Cannot assign to read only property 'length' of abc
在严格模式下,不作为方法的函数中的this值是undefined:
function f_strict() {
'use strict';
return this;
}
console.log(f_strict() === undefined); // true
在非严格模式下,this的值是被称作全局对象(global object)(在浏览器里是window):
function f() {
return this;
}
console.log(f() === window); // true
在非严格模式下,如果你给不存在的变量赋值,JavaScript会自动创建一个全局变量:
> function f() { foo = 5 }
> f() // 不会报错
> foo
5
在严格模式下,这会产生一个错误:
> function f_strict() { 'use strict'; foo2 = 4; }
> f_strict()
ReferenceError: foo2 is not defined
变量的作用域总是整个函数(没有块级作用域)。例如:
function foo() {
var x = -3;
if (x < 0) { // (*)
var tmp = -x;
...
}
console.log(tmp); // 3
}
我们可以看到tmp变量不仅在(*)所在行的语句块存在,它在整个函数内都存在。
变量声明会被提升:声明会被移到函数的顶部,但赋值过程不会。举个例子,在下面的函数中(*)行位置声明了一个变量。
function foo() {
console.log(tmp); // undefined
if (false) {
var tmp = 3; // (*)
}
}
在内部,上面的函数被执行像下面这样:
function foo() {
var tmp; // declaration is hoisted
console.log(tmp);
if (false) {
tmp = 3; // assignment stays put
}
}
每个函数保持和函数体内部变量的连接,甚至离开创建它的作用域之后。例如:
function createIncrementor(start) {
return function () { // (*)
return start++;
}
}
在(*)行开始的函数在它创建时保留上下文,并在内部保存一个start活动值:
> var inc = createIncrementor(5);
> inc()
5
> inc()
6
> inc()
7
闭包是一个函数加上和其作用域链的链接。因此,createIncrementor() 返回的是一个闭包。
有时你想模拟一个块,例如你想将变量从全局作用域隔离。完成这个工作的模式叫做 IIFE(立即执行函数表达式(Immediately Invoked Function Expression)):
(function () { // 块开始
var tmp = ...; // 非全局变量
}()); // 块结束
上面你会看到函数表达式被立即执行。外面的括号用来阻止它被解析成函数声明;只有函数表达式能被立即调用。函数体产生一个新的作用域并使 tmp 变为局部变量。
下面是个经典问题,如果你不知道,会让你费尽思量。因此,先浏览下,对问题有个大概的了解。
闭包保持和外部变量的连接,有时可能和你想像的行为不一致:
var result = [];
for (var i=0; i < 5; i++) {
result.push(function () { return i }); // (*)
}
console.log(result[1]()); // 5 (不是 1)
console.log(result[3]()); // 5 (不是 3)
(*)行的返回值总是当前的i值,而不是当函数被创建时的i值。当循环结束后,i的值是5,这是为什么数组中的所有函数的返回值总是一样的。如果你想捕获当前变量的快照,你可以使用 IIFE:
for (var i=0; i < 5; i++) {
(function (i2) {
result.push(function () { return i2 });
}(i)); // 复制当前的i
}
属性的键可以是任意字符串。到目前为止,我们看到的对象字面量中的和点操作符后的属性关键字。按这种方法你只能使用标识符。如果你想用其他任意字符串作为键名,你必须在对象字面量里加上引号,并使用方括号获取和设置属性。
> var obj = { 'not an identifier': 123 };
> obj['not an identifier']
123
> obj['not an identifier'] = 456;
方括号允许你动态计算属性关键字:
> var x = 'name';
> jane[x]
'Jane'
> jane['na'+'me']
'Jane'
如果你引用一个方法,它将失去和对象的连接。就其本身而言,函数不是方法,其中的this值为undefined(严格模式下)。
> 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”是undefined
console.log(this.name+' says hi to '+friend);
});
}
}
调用 logHiToFriends 会产生错误:
> jane.logHiToFriends()
TypeError: Cannot read property 'name' of undefined
有两种方法修复这问题。
1:将this存储在不同的变量。
logHiToFriends: function () {
'use strict';
var that = this;
this.friends.forEach(function (friend) {
console.log(that.name+' says hi to '+friend);
});
}
2:forEach的第二个参数允许提供this值。
logHiToFriends: function () {
'use strict';
this.friends.forEach(function (friend) {
console.log(this.name+' says hi to '+friend);
}, this);
}
在JavaScript中函数表达式经常被用作函数参数。时刻小心函数表达式中的this。
目前为止,你可能认为JavaScript的对象仅是键值的映射,通过JavaScript对象字面量可以得出这个观点,看起来很像其他语言中的地图/字典(map/dictionary)。然而,JavaScript对象也支持真正意义上的面向对象特性:继承(inheritance)。这里只作简单介绍。
除了作为“真正”的函数和方法,函数还在JavaScript中扮演第三种角色:如果通过new操作符调用,他们会变为构造函数,对象的工厂。构造函数是对其他语言中的类的粗略模拟。约定俗成,构造函数的第一个字母大写。例如:
// 设置实例数据
function Point(x, y) {
this.x = x;
this.y = y;
}
// 方法
Point.prototype.dist = function () {
return Math.sqrt(this.x*this.x + this.y*this.y);
};
我们看到构造函数分为两部分:首先,Point函数设置实例数据。其次,Point.prototype属性包含对象的方法。前者的数据是每个实例私有的,后面的数据是所有实例共享的。
我们通过new操作符调用Point:
> var p = new Point(3, 5);
> p.x
3
> p.dist()
5.830951894845301
p是Point的一个实例:
> p instanceof Point
true
> typeof p
'object'
数组有许多方法。举些例子:
> var arr = [ 'a', 'b', 'c' ];
> arr.slice(1, 2) // 复制元素
[ 'b' ]
> arr.slice(1)
[ 'b', 'c' ]
> arr.push('x') // 在末尾添加一个元素
4
> arr
[ 'a', 'b', 'c', 'x' ]
> arr.pop() // 移除最后一个元素
'x'
> arr
[ 'a', 'b', 'c' ]
> arr.shift() // 移除第一个元素
'a'
> arr
[ 'b', 'c' ]
> arr.unshift('x') // 在前面添加一个元素
3
> arr
[ 'x', 'b', 'c' ]
> arr.indexOf('b') // 查找给定项在数组中的索引,若不存在返回-1
1
> arr.indexOf('y')
-1
> arr.join('-') // 将元素拼接为一个字符串
'x-b-c'
> arr.join('')
'xbc'
> arr.join()
'x,b,c'
有几种方法可以遍历数组元素。其中两个最重要的是 forEach 和 map。
forEach遍历整个数组,并将当前元素和它的索引传递给一个函数:
[ 'a', 'b', 'c' ].forEach(
function (elem, index) { // (*)
console.log(index + '. ' + elem);
});
上面代码的输出
0. a
1. b
2. c
注意(*)行的函数参数是可省略的。例如:它可以只有一个参数 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),等。有一种方法可以反复调用获取所有匹配。
> ' ' .replace(/<(.*?)>/g, '[$1]')
'[a] [bbb]'
replace的第一个参数必须是正则表达式,并且开启全局搜索(/g 标记),否则仅第一个匹配项会被替换。有一种方法使用一个函数来计算替换项。
Math是一个有算数功能的对象。例如:
> Math.abs(-2)
2
> Math.pow(3, 2) // 3^2
9
> Math.max(2, -1, 5)
5
> Math.round(1.9)
2
> Math.cos(Math.PI) // 预定义常量π
-1
JavaScript标准库相对简单,但有很多其他东西你可以使用:
Date:日期构造函数,主要功能有转换和创建日期字符串,访问日期组成部分(年,小时等)。
JSON:一个对象,功能是转换和生成JSON数据。
console.* 方法:浏览器的具体方法,不是语言成分的部分,但他们也可以在Node.js中工作。
先整理这些。