第 1 章. 基础 JavaScript
目录
购买本书
(广告,请不要屏蔽。)

第 1 章. 基础 JavaScript

本章介绍“基础 JavaScript”,这是我对 JavaScript 子集的命名,它尽可能简洁,同时又能让你高效工作。当你开始学习 JavaScript 时,我建议你先用它编程一段时间,然后再学习该语言的其余部分。这样,你就不必一次性学习所有内容,避免混淆。

背景

本节介绍 JavaScript 的一些背景知识,帮助你理解其原因。

JavaScript 与 ECMAScript

ECMAScript 是 JavaScript 的官方名称。之所以需要一个新名称,是因为 JavaScript 是一个商标(最初由 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 能够实现函数式编程(高阶函数;内置 mapreduce 等)和面向对象编程(对象、继承)相结合的编程风格也就不足为奇了。

语法

本节 介绍 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

最后,你还应该避免使用标准全局变量的名称(请参阅 第 23 章)。 你可以将它们用于局部变量而不会破坏任何内容,但你的代码仍然会变得混乱。

JavaScript 有许多我们期望编程语言拥有的值 :布尔值、数字、字符串、数组等等。 JavaScript 中的所有值都有 属性[1] 每个属性都有一个 (或 名称)和一个 。你可以将属性视为记录的字段。你使用点 (.) 运算符来读取属性

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 对值进行了一些武断的区分

  • 原始值 是布尔值、数字、字符串、nullundefined
  • 所有其他值都是 对象

两者之间的一个主要区别在于它们的比较方式; 每个对象都有一个唯一的标识,并且 只(严格)等于它自己:

> 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

接下来的两节将更详细地解释原始值和对象。

原始值

以下是所有原始值(或简称 基元):

  • 布尔值:truefalse(请参阅 布尔值
  • 数字:17361.351(请参阅 数字
  • 字符串:'abc'"abc"(请参阅 字符串
  • 两个“非值”:undefinednull(请参阅 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。)

对象

所有非原始 值都是 对象。最常见的对象类型是:

对象具有以下 特点:

按引用比较

比较的是标识;每个值都有自己的标识

> ({} === {})  // two different empty objects
false

> var obj1 = {};
> var obj2 = obj1;
> obj1 === obj2
true
默认情况下可变

你通常可以自由地更改、添加和删除 属性(请参阅 单个对象

> var obj = {};
> obj.foo = 123; // add property `foo`
> obj.foo
123

undefined 和 null

大多数编程语言都有表示缺失信息的值。JavaScript 有两个这样的“非值”,undefinednull

  • undefined 表示“无值”。未初始化的变量为 undefined

    > var foo;
    > foo
    undefined

    缺失的参数为 undefined

    > function f(x) { return x }
    > f()
    undefined

    如果读取不存在的属性,则会得到 undefined

    > var obj = {}; // empty object
    > obj.foo
    undefined
  • null 表示“无对象”。每当预期出现对象时(参数、对象链中的最后一个等),它都被用作非值。

警告

undefinednull 没有属性,甚至没有标准方法,例如 toString()

检查 undefined 或 null

函数通常允许您通过 undefinednull 来指示缺失值。您可以通过显式检查来执行相同的操作:

if (x === undefined || x === null) {
    ...
}

您还可以利用 undefinednull 都被视为 false 的事实

if (!x) {
    ...
}

警告

false0NaN'' 也被视为 false(请参阅真值和假值)。

使用 typeof 和 instanceof 对值进行分类

有两个运算符用于对值进行分类: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

布尔值

原始布尔类型包含值 truefalse。以下运算符产生布尔值:

  • 二元逻辑运算符:&&(与)、||(或)
  • 前缀逻辑运算符:!(非)
  • 比较运算符

    • 相等运算符:===!====!=
    • 排序运算符(用于字符串和数字):>>=<<=

真值和假值

每当 JavaScript 需要布尔值时(例如,对于 if 语句的条件),都可以使用任何值。它将被解释为 truefalse。以下值被解释为 false

  • undefinednull
  • 布尔值:false
  • 数字:0NaN
  • 字符串:''

所有其他值(包括所有对象!)都被视为 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
  • 递增:++variablevariable++
  • 递减:--variablevariable--
  • 取反:-value
  • 转换为数字:+value

全局对象 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
}

我建议始终使用大括号(它们表示零个或多个语句的块)。但是,如果子句只是一个语句,则不必这样做(控制流语句 forwhile 也是如此):

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,它有两个参数 param1param2,并返回两个参数的和。以下是调用该函数的方法

> 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 () {
        // ...
    };
}

特殊变量 arguments

您可以在 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 为真值(不是 nullundefined 等),则 || 运算符返回 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 不是数组,它只是 类数组(请参阅 类数组对象和泛型方法)。它有一个属性 length,您可以通过方括号中的索引访问其元素。 但是,您不能删除元素或在其上调用任何数组方法。因此,您有时需要将 arguments 转换为数组,这就是以下函数的作用(在 类数组对象和泛型方法 中有解释)

function toArray(arrayLikeObject) {
    return Array.prototype.slice.call(arrayLikeObject);
}

异常处理

处理异常(请参阅 第 14 章)的最常见方法如下

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 文件或 <script> 标记中首先键入以下行:

'use strict';

您也可以 为每个函数启用严格模式:

function functionInStrictMode() {
    'use strict';
}

变量作用域和闭包

在 JavaScript 中,您在使用 变量之前通过 var 声明它们:

> var x;
> x
undefined
> y
ReferenceError: y is not defined

您可以使用单个 var 语句声明和初始化多个变量

var x = 1, y = 2, z = 3;

但我建议每个变量使用一个语句(原因在 语法 中有解释)。因此,我将把前面的语句重写为

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() 返回的是一个闭包。

IIFE 模式:引入新的作用域

有时您想引入一个新的 变量作用域——例如,为了防止变量变成全局变量。在 JavaScript 中,您不能使用块来做到这一点;您必须使用函数。但是,有一种模式可以以类似块的方式使用函数。它被称为 IIFE立即调用函数表达式,发音为“iffy”)

(function () {  // open IIFE
    var tmp = ...;  // not a global variable
}());  // close IIFE

请确保完全按照所示键入前面的示例(注释除外)。IIFE 是一个函数表达式,在您定义它之后立即调用。在函数内部,存在一个新的作用域,防止 tmp 变成全局变量。有关 IIFE 的详细信息,请参阅 通过 IIFE 引入新的作用域

IIFE 用例:通过闭包无意中共享

闭包保持与外部变量的 连接,这有时不是您想要的:

var result = [];
for (var i=0; i < 5; i++) {
    result.push(function () { return i });  // (1)
}
console.log(result[1]()); // 5 (not 1)
console.log(result[3]()); // 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;
    }
};

前面的对象具有属性 namedescribe。您可以读取(“获取”)和写入(“设置”)属性

> 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。因此,前面的两个检查也可以像这样执行:[2]

> jane.newProperty !== undefined
true
> jane.foo !== undefined
false

delete 运算符 删除属性:

> delete jane.newProperty
true
> 'newProperty' in jane
false

任意属性键

属性键可以是任何字符串。 到目前为止,我们已经在对象字面量和点运算符之后看到了属性键。但是,只有当它们是 标识符时,您才能以这种方式使用它们(请参阅 标识符和变量名)。如果要使用其他字符串作为键,则必须在对象字面量中将其引起来,并使用方括号 获取和设置属性:

> 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 继承的工作原理,但它向您展示了一个简单的模式来帮助您入门。如果您想了解更多信息,请参阅 第 17 章

除了是“真正的”函数和方法之外,函数 在 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

pPoint 的一个实例

> 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

数组方法

数组有许多 方法(参见 数组原型方法)。以下是一些示例

> 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'

迭代数组

有几种数组 方法可用于迭代元素(参见 迭代(非破坏性))。最重要的两个是 forEachmap

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 内置支持正则表达式(第 19 章 涉及教程并更详细地解释了它们的工作原理)。它们由 斜杠分隔:

/^abc$/
/[A-Za-z0-9]+/

方法 test():是否存在匹配项?

> /^a+b+$/.test('aaab')
true
> /^a+b+$/.test('aaa')
false

方法 exec():匹配和捕获组

> /a(b+)a/.exec('_abbba_aba_')
[ 'abbba', 'bbb' ]

返回的 数组在索引 0 处包含完全匹配,在索引 1 处包含第一个组的捕获,依此类推。有一种方法(在 RegExp.prototype.exec:捕获组 中讨论)可以重复调用此方法以获取所有匹配项。

方法 replace():搜索和替换

> '<a> <bbb>'.replace(/<(.*?)>/g, '[$1]')
'[a] [bbb]'

replace 的第一个参数必须是带有 /g 标志的正则表达式;否则,只会替换第一个匹配项。还有一种方法(如 String.prototype.replace:搜索和替换 中所述)可以使用函数来计算替换。

数学

Math(参见 第 21 章)是一个具有算术 函数的对象。以下是一些示例:

> 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第 20 章
日期的构造函数,其主要功能是解析和创建日期字符串以及访问日期的组成部分(年、小时等)。
JSON第 22 章
一个包含用于解析和生成 JSON 数据的函数的对象。
console.* 方法(参见 控制台 API
这些特定于浏览器的 方法不是语言本身的一部分,但其中一些方法也适用于 Node.js。


[1] 两个“非值” undefinednull 没有属性。

[2] 注意:此检查会将确实存在的属性报告为不存在,但其值为 undefined

下一页:II. 背景