字符串
字符串方法(String methods)
字符串有许多有用的方法。例如:
1 | > 'abc'.slice(1) // 复制子字符串 |
函数
函数声明提升(Function declarations are hoisted)
函数声明会被提升,他们全被移动到当前作用域开始之处。这允许你在函数声明之前调用它们:
1 | function foo() { |
注意:虽然变量声明也会被提升,但赋值的过程不会被提升:
1 | function foo() { |
特殊变量arguments(The special variable arguments)
在JavaScript中你可以调用任意函数并传递任意数量的参数——语言绝不会抱怨(参数检测)。都可以正常工作,然而,使所有参数可访问需要通过特殊变量 arguments。arguments 看起来像数组,但它没有数组的方法(称为类数组 array-like)。
1 | > function f() { return arguments } |
太多或太少参数(Too many or too few arguments)
让我们通过下面的函数探索JavaScript中传递太多或太少参数时如何处理(函数 toArray在后面提到)
1 | function f(x, y) { |
多出的参数将被忽略(可以通过arguments访问):
1 | > f('a', 'b', 'c') |
缺少的参数将会是undefined:
1 | > f('a') |
可选参数(Optional parameters)
下面是一个常见模式,给参数设置默认值:
1 | function pair(x, y) { |
在(*)这行,如果x是真值(除了:null,undefined 等), 操作符返回x。否则,它返回第二个操作数。
1 | > pair() |
强制数量(Enforcing an arity)
如果你想强制参数的数量,你可以检测arguments.length:
1 | function pair(x, y) { |
将arguments 转换为数组(Converting arguments to an array)
arguments 不是一个数组,它仅仅是类数组(array-like):它有一个length属性,并且你可以通过方括号索引方式访问它的元素。然而,你不能移除元素,或在它上面调用任何数组方法。因此,有时你需要将其转换为数组。这就是下面函数的作用。
1 | function toArray(arrayLikeObject) { |
严格模式
严格模式(Strict mode)
严格模式开启检测和一些其他措施,使JavaScript变成更整洁的语言。推荐使用严格模式。为了开启严格模式,只需在JavaScript文件或script标签第一行添加如下语句:
1 | ; |
你也可以在每个函数上选择性开启严格模式,只需将上面的代码放在函数的开头:
1 | function functionInStrictMode() { |
下面的两小节看下严格模式的三大好处。
明确错误(Explicit errors)
让我们看一个例子,严格模式给我们明确的错误,否则JavaScript总是静默失败:下面的函数 f() 执行一些非法操作,它试图更改所有字符串都有的只读属性——length:
1 | function f() { |
当你调用上面的函数,它静默失败,赋值操作被简单忽略。让我们将 f() 在严格模式下运行:
1 | function f_strict() { |
现在浏览器报给我们一些错误:
1 | > f_strict() |
不是方法的函数中的this(this in non-method functions)
在严格模式下,不作为方法的函数中的this值是undefined:
1 | function f_strict() { |
在非严格模式下,this的值是被称作全局对象(global object)(在浏览器里是window):
1 | function f() { |
不再自动创建全局变量(No auto-created global variables)
在非严格模式下,如果你给不存在的变量赋值,JavaScript会自动创建一个全局变量:
1 | > function f() { foo = 5 } |
在严格模式下,这会产生一个错误:
1 | > function f_strict() { 'use strict'; foo2 = 4; } |
变量作用域和闭包
变量和函数作用域(Variables are function-scoped)
变量的作用域总是整个函数(没有块级作用域)。例如:
1 | function foo() { |
我们可以看到tmp变量不仅在(*)所在行的语句块存在,它在整个函数内都存在。
变量提升(Variables are hoisted)
变量声明会被提升:声明会被移到函数的顶部,但赋值过程不会。举个例子,在下面的函数中(*)行位置声明了一个变量。
1 | function foo() { |
在内部,上面的函数被执行像下面这样:
1 | function foo() { |
闭包(Closures)
每个函数保持和函数体内部变量的连接,甚至离开创建它的作用域之后。例如:
1 | function createIncrementor(start) { |
在(*)行开始的函数在它创建时保留上下文,并在内部保存一个start活动值:
1 | > var inc = createIncrementor(5); |
闭包是一个函数加上和其作用域链的链接。因此,createIncrementor() 返回的是一个闭包。
IIFE:模拟块级作用域(IIFE: Simulating block scoping)
有时你想模拟一个块,例如你想将变量从全局作用域隔离。完成这个工作的模式叫做 IIFE(立即执行函数表达式(Immediately Invoked Function Expression)):
1 | (function () { // 块开始 |
上面你会看到函数表达式被立即执行。外面的括号用来阻止它被解析成函数声明;只有函数表达式能被立即调用。函数体产生一个新的作用域并使 tmp 变为局部变量。
闭包实现变量共享(Inadvertent sharing via closures)
下面是个经典问题,如果你不知道,会让你费尽思量。因此,先浏览下,对问题有个大概的了解。
闭包保持和外部变量的连接,有时可能和你想像的行为不一致:
1 | var result = []; |
(*)行的返回值总是当前的i值,而不是当函数被创建时的i值。当循环结束后,i的值是5,这是为什么数组中的所有函数的返回值总是一样的。如果你想捕获当前变量的快照,你可以使用 IIFE:
1 | for (var i=0; i < 5; i++) { |
对象和继承
任意键属性(Arbitrary property keys)
属性的键可以是任意字符串。到目前为止,我们看到的对象字面量中的和点操作符后的属性关键字。按这种方法你只能使用标识符。如果你想用其他任意字符串作为键名,你必须在对象字面量里加上引号,并使用方括号获取和设置属性。
1 | > var obj = { 'not an identifier': 123 }; |
方括号允许你动态计算属性关键字:
1 | > var x = 'name'; |
引用方法(Extracting methods)
如果你引用一个方法,它将失去和对象的连接。就其本身而言,函数不是方法,其中的this值为undefined(严格模式下)。
1 | > var func = jane.describe; |
解决办法是使用函数内置的bind()方法。它创建一个新函数,其this值固定为给定的值。
1 | > var func2 = jane.describe.bind(jane); |
方法内部的函数(Functions inside a method)
每个函数都有一个特殊变量this。如果你在方法内部嵌入函数是很不方便的,因为你不能从函数中访问方法的this。下面是一个例子,我们调用forEach循环一个数组:
1 | var jane = { |
调用 logHiToFriends 会产生错误:
1 | > jane.logHiToFriends() |
有两种方法修复这问题。
1:将this存储在不同的变量。
1 | logHiToFriends: function () { |
2:forEach的第二个参数允许提供this值。
1 | logHiToFriends: function () { |
在JavaScript中函数表达式经常被用作函数参数。时刻小心函数表达式中的this。
构造函数:对象工厂(Constructors: factories for objects)
目前为止,你可能认为JavaScript的对象仅是键值的映射,通过JavaScript对象字面量可以得出这个观点,看起来很像其他语言中的地图/字典(map/dictionary)。然而,JavaScript对象也支持真正意义上的面向对象特性:继承(inheritance)。这里只作简单介绍。
除了作为“真正”的函数和方法,函数还在JavaScript中扮演第三种角色:如果通过new操作符调用,他们会变为构造函数,对象的工厂。构造函数是对其他语言中的类的粗略模拟。约定俗成,构造函数的第一个字母大写。例如:
1 | // 设置实例数据 |
我们看到构造函数分为两部分:首先,Point函数设置实例数据。其次,Point.prototype属性包含对象的方法。前者的数据是每个实例私有的,后面的数据是所有实例共享的。
我们通过new操作符调用Point:
1 | > var p = new Point(3, 5); |
p是Point的一个实例:
1 | > p instanceof Point |
数组
数组方法(Array methods)
数组有许多方法。举些例子:
1 | > var arr = [ 'a', 'b', 'c' ]; |
遍历数组(Iterating over arrays)
有几种方法可以遍历数组元素。其中两个最重要的是 forEach 和 map。
forEach遍历整个数组,并将当前元素和它的索引传递给一个函数:
1 | [ 'a', 'b', 'c' ].forEach( |
上面代码的输出
1 | 0. a |
注意(*)行的函数参数是可省略的。例如:它可以只有一个参数 elem。
map创建一个新数组,通过给每个存在数组元素应用一个函数:
1 | > [1,2,3].map(function (x) { return x*x }) |
正则表达式(Regular expressions)
JavaScript内建支持正则表达式。他们被双斜线分隔:
1 | /^abc$/ |
方法 test():测试是否匹配(Method test(): is there a match?)
1 | > /^a+b+$/.test('aaab') |
方法 exec():匹配和捕获组(Method exec(): match and capture groups)
1 | > /a(b+)a/.exec('_abbba_aba_') |
返回的数组第一项(索引为0)是完整匹配,捕获的第一个分组在第二项(索引为1),等。有一种方法可以反复调用获取所有匹配。
方法 replace():搜索并替换(Method replace(): search and replace)
1 | > '<a> <bbb>'.replace(/<(.*?)>/g, '[$1]') |
replace的第一个参数必须是正则表达式,并且开启全局搜索(/g 标记),否则仅第一个匹配项会被替换。有一种方法使用一个函数来计算替换项。
数学(Math)
Math是一个有算数功能的对象。例如:
1 | > Math.abs(-2) |
标准库的其他功能(Other functionality of the standard library)
JavaScript标准库相对简单,但有很多其他东西你可以使用:
Date:日期构造函数,主要功能有转换和创建日期字符串,访问日期组成部分(年,小时等)。
JSON:一个对象,功能是转换和生成JSON数据。
console.* 方法:浏览器的具体方法,不是语言成分的部分,但他们也可以在Node.js中工作。