企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
> http://speakingjs.com/es5/ch01.html [TOC] # 第一章 javascript基础知识 本章讲述的是“基本的JavaScript”,我选择了一个JavaScript子集,这个名字尽可能简洁,同时也能让你变得富有成效。当你开始学习JavaScript,我建议你先用它程序编程,然后再移动到其余的语言。这样,你不必马上学习所有的东西,因为这可能会让人困惑。 ## 背景 本节给出了JavaScript的一些背景知识,以帮助您理解它为什么是现在这个样子。 ### JavaScript与ECMAScript ECMAScript是JavaScript的官方名称。一个JavaScript的商标(最初由Sun持有,现在由甲骨文),那就需要新的名字。 目前,Mozilla是为数不多的几家允许正式使用JavaScript的公司之一,因为它很久以前就收到了许可证。 对于常见用法,本规则适用: * JavaScript 指的是javascript编程语言。 * ECMAScript的语言规范使用的名称。因此,当指的是语言的版本,有人说ECMAScript。JavaScript的当前版本是ECMAScript ECMAScript 5;6是目前正在开发 。 ### 语言的影响与本质 JavaScript的创造者,Brendan Eich,别无选择,只能很快的创造语言(否则,网景公司就要采用其他更烂的语言)。他从几种编程语言借来的语法:Java (语法, 原始值和引用值), Scheme and AWK (头等函数[^first-class-functions]), Self (原型式继承), and Perl and Python (字符串、数组和正则表达式). JavaScript没有异常处理直到ECMAScript 3,这解释了为什么该语言经常自动转换值并且失败的静默处理:它最初不抛出异常。 一方面,JavaScript有怪癖和丢失了相当多的功能(块作用域的变量,模块,支持子类化,等)。另一方面,它有几个功能强大的功能,让你解决这些问题。在其他语言中,你学习语言特征。在JavaScript中,你经常学习模式. 考虑到它的影响,JavaScript使编程风格成为函数式编程(高阶函数,内置映射,减少等)和面向对象编程(对象,继承)的混合并不奇怪。 ## 语法 本节解释JavaScript的基本语法规则。 ### 语法的概述 语法的一些例子: ```js // 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语法,你应该知道,它有两个主要的语法范畴:语句和表达式: * 语句“do things.”程序是一系列语句。下面是声明的一个例子,它声明(创建)变量: `var foo;` * 表达式产生值。它们是函数参数,赋值的右边,这里是表达式的一个例子: `3 * 7` 语句和表达式之间的区别最好的说明是JavaScript有两种不同的方法," if-then-else"示例,用语句表达: ```js var x; if (y >= 0) { x = y; } else { x = -y; } ``` 或作为一个表达式: ```js var x = y >= 0 ? y : -y; ``` 您可以使用后者作为函数参数(但不是前者): ```js myFunction(y >= 0 ? y : -y) ``` 最后,无论JavaScript在哪里期望一个语句,你也可以使用一个表达式;例如: ```js foo(7, 1); ``` 整行是一个语句(一个所谓的表达式语句),但函数调用`foo(7,1)`是一个表达式。 ### 分号 分号在JavaScript中是可选的。不过,我建议总是包括它们,因为否则JavaScript可以猜错语句的结束位置。详细说明在[自动分号插入](###)。 分号终止语句,但不是用来终止块的。有一个例子,在一个块之后你会看到一个分号:函数表达式是用一个块结束的表达式。如果这样的表达式在语句中最后出现,则后跟分号: ```js // Pattern: var _ = ___; var x = 3 * 7; var f = function () { }; // function expr. inside var decl. ``` ### 注释 JavaScript有两种注释:单行注释和多行注释。 * 单行注释:以`//`开始,到行结束终止。 ```js x++; // single-line comment ``` * 多行注释:以`/*`和`*/`分隔: ```js /*This is a multiline comment. */ ``` ## 变量和赋值 JavaScript中的变量在使用前声明 ```js var foo; // declare variable `foo` ``` ### 赋值 你可以声明一个变量,同时赋值: ```js var foo = 6; ``` 你也可以对现有的变量赋值: ```js foo = 4; // change variable `foo` ``` ### 复合赋值运算符 有复合赋值运算符,如`+=`。以下两个赋值效果是一样的: ```js x += 1; x = x + 1; ``` ### 标识符和变量名 标识符是在JavaScript中扮演各种语法角色的名称。例如,变量名是标识符。 **标识符是区分大小写的。** 粗略地说,一个标识符的第一个字符可以是任何Unicode字母,一个美元符号($),或一个下划线(_)。随后的字符可以是任何Unicode数字。因此,以下是所有合法标识符: ```js 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 | 以下三个标识符不是保留字,但你应该把它们当作是javascript的保留字: ``` Infinity NaN undefined ``` 最后,你也应该远离标准的全局变量的名称(见[23章](###))。你可以将它们用于局部变量而不破坏任何东西,但你的代码仍然会变得混乱。 ## 值 JavaScript有许多我们期望从编程语言中得到的值:booleans, numbers, strings, arrays, 等等。JavaScript中的所有值都具有属性[^properties]。每个属性都有一个键(或名称)和一个值。你可以想到像记录字段的属性。您使用点(.)运算符读取属性: ```js value.propKey ``` 例如,string“ABC”具有属性length: ```js > var str = 'abc'; > str.length 3 ``` 前面也可以写为: ```js > 'abc'.length 3 ``` 点运算符也用于将值赋给属性: ```js > var obj = {}; // empty object > obj.foo = 123; // create property `foo`, set it to 123 123 > obj.foo 123 ``` 你可以用它来调用方法: ```js > 'hello'.toUpperCase() 'HELLO' ``` 在前面的示例中,我们调用了值'hello'的方法`touppercase()`。 ### 原始值与对象 JavaScript中值之间的区别有点特别: * 原始值有:booleans, numbers, strings, null, and undefined. * 其他所有的值是对象。 两者之间的一个主要区别是如何比较,每个对象都有一个唯一的身份,只有(严格)等于自己: ```js > var obj1 = {}; // an empty object > var obj2 = {}; // another empty object > obj1 === obj2 false > obj1 === obj1 true ``` 与此相反,所有被赋值相同值的原始值都被认为是相同的: ```js > var prim1 = 123; > var prim2 = 123; > prim1 === prim2 true ``` 接下来的两个部分将更详细地解释原始值和对象。 ### 原始值 以下都是原始值: - Booleans: true, false (see [Booleans](###)) - Numbers: 1736, 1.351 (see [Numbers](###)) - Strings: 'abc', "abc" (see [Strings](###)) - Two “nonvalues”: undefined, null (see [undefined and null](###)) 原始值有以下特点: **通过值相比** 比较“内容”: ```js > 3 === 3 true > 'abc' === 'abc' true ``` **永远不变的** 原始值的属性不能更改、添加或删除: ```js > 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。) ### 对象 所有的非原始值是对象。最常见的对象是: * 普通对象,可以由对象字面量 (见[单个对象](###)): ```js { firstName: 'Jane', lastName: 'Doe' } ``` 上面的对象有两个属性: firstName的值是“Jane”,lastName属性的值是“Doe”。 * 数组,可以由数组文本来创建(见[数组](###)): ```js [ 'apple', 'banana', 'cherry' ] ``` 上面的数组有三个元素,可以通过访问数字指标。例如,“apple”的index为0。 * 正则表达式,正则表达式,其可以通过正则表达式文本(见正则表达式)来创建(见[正则表达式](###)): ```js /^a+b+$/ ``` 对象有以下特点: **通过引用相比** 身份标识进行比较,每个值都有自己的身份标识: ```js > ({} === {}) // two different empty objects false > var obj1 = {}; > var obj2 = obj1; > obj1 === obj2 true ``` **默认可以被改变** 通常可以自由地更改、添加和移除属性(参见[单个对象](###)): ```js > var obj = {}; > obj.foo = 123; // add property `foo` > obj.foo 123 ``` ### undefined和null 大多数编程语言都有表示缺少信息的值。JavaScript有两个这样的"nonvalues",`undefined` 和`null`: * `undefined`的意思是“没有值”。未初始化的变量是undefined的: ```js > var foo; > foo undefined ``` 缺少参数未定义: ```js > function f(x) { return x } > f() undefined ``` 如果您读取一个不存在的属性,您将得到`undefined`: ```js > var obj = {}; // empty object > obj.foo undefined ``` * `null`的意思是“no object.”。每当你预期需要一个对象时,它作为nonvalue (参数,链中的对象,等)。 | <span style="color:#C67171">警告</span> | |--------| | undefined和null没有属性,甚至没有标准方法如toString()。 | #### 检查 undefined 或 null 函数通常允许您通过`undefined`或`null`指示丢失的值。你可以通过一个明确的检查做同样的检查: ```js if (x === undefined || x === null) { ... } ``` 您还可以利用这一事实,即`undefined`和`null` 被认为是`false`: | <span style="color:#C67171">警告</span> | |--------| | false,0,NaN和 '' 也被认为是`false`(见[Truthy和Falsy]())。 | ### 使用 typeof和 instanceof分类 有两个进行区分值的操作符:`typeof`运算主要用于原始值,而的`instanceof`用于对象。 `typeof`是这样运算的: ```js typeof value ``` 它返回一个字符串,描述值的“类型”。以下是一些例子: ```js > typeof true 'boolean' > typeof 'abc' 'string' > typeof {} // empty object literal 'object' > typeof [] // empty array literal 'object' ``` 下表列出了所有结果类型: | 操作对象 | 结果 | |--------|--------| | undefined | 'undefined' | | null | 'object' | | Boolean value | 'boolean' | | Number value | 'number' | | String value | 'string' | | Function | 'function' | | All other normal values | 'object' | | (引擎创造的值) | JavaScript引擎允许创建针对的typeof返回任意的字符串(从该表中列出的所有结果不同)值。 | `typeof`运算返回`null`“object”是一个错误,无法修复,因为它会破坏现有代码。这并不意味着`null`是一个对象。 instanceof是这样的: `value instanceof Constr` 如果'value'是已经由'Constr'构造方法创建的一个实例对象,则返回true(请参见[构造函数:对象的工厂](###))。这里有些例子: ```js > 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。下面的运算符会产生布尔值: * 二进制逻辑运算符:`&&`(与),`||`(或) * 前缀逻辑运算符:`!`(非) * 比较运算符: 相等运算符: `===, !==, ==, !=` 排序运算符(字符串和数字):`>, >=, <, <=` ### 真值(Truthy)与假值(Falsy) 每当JavaScript期望一个布尔值(例如,if语句的条件)时,可以使用任何值进行转换。该值将被解释为是`true`或者`false`。下列值被解释为`false`: * undefined,null * 布尔值:false * 数值:0,NaN * 字符串:'' 所有其他值(包括所有对象!)被认为是true 。 被解释为false值被称为false ,被解释为true值被称为true的。调用Boolean()可以将其参数转换为布尔值。您可以使用它来测试一个值如何被解释执行为布尔型: ```js > Boolean(undefined) false > Boolean(0) false > Boolean(3) true > Boolean({}) // 空对象 true > Boolean([]) // 空数组 true ``` ### 二元逻辑运算符 JavaScript中的二元逻辑运算符会造成**`短路`**。也就是说,如果第一个操作数足以确定结果,则不计算第二个操作数。例如,下面的表达式,不会执行到`函数foo()`: ```js false && foo() true || foo() ``` 此外,二元逻辑运算符返回它们的操作数之一,不一定是布尔值。可以查看下面的例子确定: **与 (&&)** 如果第一个操作数是false的,直接就会返回该值,否则,返回第二个操作数: ```js > NaN && 'abc' NaN > 123 && 'abc' 'abc' ``` **或 (||)** 如果第一个操作数为true,返回该值,否则,就返回第二个操作数: ```js > 'abc' || 123 'abc' > '' || 123 123 ``` ### 相等运算符 JavaScript有两种相等类型: * 常规的,或者“比较宽松的”,相等:== 和 != * 严格的相等:=== 和 !== 常规的相等一般就是值相等(细节在 [Normal( Lenient )Equality(==,!=)](###)中解释 ),但是往往会导致隐藏错误存在。 因此,**建议始终使用严格的相等(===)**。 ## 数值 在JavaScript中所有的数字都是浮点: ```js > 1 === 1.0 true ``` 以下为特殊的数值: **NaN (“非数值”)** ```js > 1 === 1.0 true ``` **无穷(Infinity)** ```js > 3 / 0 Infinity > Math.pow(2, 1024) // 数值太大 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还具有用于按位运算的运算符(例如,按位运算符;请参见[按位运算符](###))。 ## 字符串 字符串可以通过字符串文字直接创建。这些文字是由单或双引号分隔。反斜杠(\)转义字符,并产生一些控制字符。这里有些例子: ```js 'abc' "abc" 'Did she say "Hello"?' "Did she say \"Hello\"?" 'That\'s nice!' "That's nice!" 'Line 1\nLine 2' // newline 'Backlash: \\' ``` 字符串中的单个字符可以通过方括号访问: ```js > var str = 'abc'; > str[1] 'b' ``` 字符串的属性`length`计算字符串中的字符数: ```js > 'abc'.length 3 ``` 与所有原始类型一样,字符串是不可变的;如果要更改现有字符串,则需要创建新字符串。 ### 字符串运算符 字符串通过加号( + )运算符连接,如果其中一个操作数是字符串,则将其他操作数转换为字符串: ```js > var messageCount = 3; > 'You have ' + messageCount + ' messages' 'You have 3 messages' ``` 要在多个步骤中串联字符串,请使用+=运算符: ```js > var str = ''; > str += 'Multiple '; > str += 'pieces '; > str += 'are concatenated.'; > str 'Multiple pieces are concatenated.' ``` ### 字符串方法 字符串有很多有用的方法(参见 [String Prototype Methods](###))。 这里有些例子: ```js > '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子句: ```js 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的控制流语句一样): ```js if (x < 0) return -x; ``` 以下是一个switch语句。 fruit的值决定了哪个case执行: ```js switch (fruit) { case 'banana': // ... break; case 'apple': // ... break; default: // all other cases // ... } ``` case后的“操作数”可以是任何表达式;**通过===来比较switch的参数**。 ### 循环 for循环具有以下格式: ```js for ([[«init»]]; [[«condition»]]; [[«post_iteration»]]) «statement» ``` `init` 再这个循环开始之前会被执行,`condition`会在每个循环迭代前被判断检查,如果它变为false,则循环终止。`post_iteration`在每个循环迭代后执行。 这个例子在控制台打印arr数组的所有元素: ```js for (var i=0; i < arr.length; i++) { console.log(arr[i]); } ``` `while`循环条件语句,只要在条件成立时,会继续进行循环迭代。 ```js // Same as for loop above: var i = 0; while (i < arr.length) { console.log(arr[i]); i++; } ``` `do-while`循环在条件成立时,会继续进行循环迭代。当条件依据循环体改变,但是至少会执行一次循环体: ```js do { // ... } while (condition); ``` 在所有的循环语句中: 1. `break` 会离开循环。 2. `continue` 开始一个新的循环迭代。 ## 函数 定义函数的一种方法是通过**函数声明** ```js function add(param1, param2) { return param1 + param2; } ``` 上面的代码定义了一个函数`add`,有两个参数,`param1`和`param2`,并返回参数的总和。需要这样调用函数: ```js > add(6, 1) 7 > add('a', 'b') 'ab' ``` 另一种定义`add()`的方式,是给变量`add`赋值一个**函数表达式**: ```js var add = function (param1, param2) { return param1 + param2; }; ``` 函数表达式产生了一个值,因此可以直接将函数作为参数传递给其他函数: ```js someOtherFunction(function (p1, p2) { ... }); ``` ### 函数声明被提升 函数声明会被全部移动到当前作用域范围的开始处。这允许您引用后来声明的函数: ```js function foo() { bar(); // OK, bar is hoisted(bar 被提升到此处) function bar() { ... } } ``` 注意:`var`的声明也会被提升(查看[Variables Are Hoisted](###)),但是通过`var`赋值的函数表达式却不会: ```js function foo() { bar(); // Not OK, bar is still undefined var bar = function () { // ... }; } ``` ### 特殊变量arguments 你可以用任意数量的参数调用JavaScript中的任何函数,JS语言永远不会抱怨。然而,它将通过特殊变量`arguments`使所有参数可用。`arguments`看起来像数组,但没有数组方法: ```js > function f() { return arguments } > var args = f('a', 'b', 'c'); > args.length 3 > args[0] // read element at index 0 'a' ``` ### 过多或过少的Arguments 让我们用下面的函数来探讨如何用JavaScript处理`过多或过少的Arguments`(函数`toarray()`查看[将参数转换为数组](###)): ```js function f(x, y) { console.log(x, y); return toArray(arguments); } ``` 多余的参数将被忽略(除了`arguments`,arguments对象代表实参。): ```js > f('a', 'b', 'c') a b [ 'a', 'b', 'c' ] ``` 缺少参数将得到`undefined`: ```js > f('a') a undefined [ 'a' ] > f() undefined undefined [] ``` ### 可选参数 以下是为参数分配默认值的常见形式: ```js function pair(x, y) { x = x || 0; // (1) y = y || 0; return [ x, y ]; } ``` 在第一行,如果`x`是真值(不是`null`,`undefined`,等),操作符`||` 返回`x`,否则,返回第二个操作数: ```js > pair() [ 0, 0 ] > pair(3) [ 3, 0 ] > pair(3, 5) [ 3, 5 ] ``` ### 强制数量 如果你想强制参数数量(一个特定数量的参数),你可以检查`arguments.length`: ```js function pair(x, y) { if (arguments.length !== 2) { throw new Error('Need exactly 2 arguments'); } ... } ``` ### 将参数转换为数组 参数不是数组,只是类似数组(见[Array-Like Objects and Generic Methods](###))。它有`length`属性,并且您可以通过在方括号内的索引访问其元素。但是,您不能删除元素或在其上调用任何数组的方法。因此,你有时需要将参数转换为一个数组,这就是下面的函数的作用([Array-Like Objects and Generic Methods](###)进行了解释): ```js function toArray(arrayLikeObject) { return Array.prototype.slice.call(arrayLikeObject); } ``` ## 异常处理 处理异常的最常见的方式(见[14章](###))如下: ```js 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`子句。使用前面的代码: ```js > getPersons([2, -5, 137]) [Error: ID must not be negative: -5] [ { id: 2 }, { id: 137 } ] ``` ## 严格模式 严格模式(见[Strict Mode](###))使更多的警告,让JavaScript成为更干净,好用的语言(非严格模式有时被称为“懒散”模式)。要启用它,首先在JavaScript文件或`<script>`标签中键入以下行: ~~~ 'use strict'; ~~~ 您也可以在每个函数中开启严格模式: ```js function functionInStrictMode() { 'use strict'; ... } ``` ## 变量作用域和闭包 在JavaScript中,使用变量之前先用`var`声明变量: ```js > var x; > x undefined > y ReferenceError: y is not defined ``` 可以用单个var语句声明和初始化多个变量: ```js var x = 1, y = 2, z = 3; ``` 但是,我建议每个变量使用一个语句(查看:[Syntax](###))。因此,我会重写前面的语句: ```js var x = 1; var y = 2; var z = 3; ``` 由于变量提升(见[Variables Are Hoisted](###))的,通常最好在函数的开头声明变量。 ### 变量的作用域 变量的作用域始终是在整个函数内的(相对于当前块)。例如: ```js function foo() { var x = -512; if (x < 0) { // (1) var tmp = -x; ... } console.log(tmp); // 512 } ``` 我们可以看到,变量`tmp`不限制在开始`(1)`的块内,它一直存在直到函数的结束。 ### 变量被提升 每一个变量声明会被提升:声明移动到函数的开始处,但赋值部分依然在原处。例如,在下面的函数中考虑行`(1)`中的变量声明: ```js function foo() { console.log(tmp); // undefined if (false) { var tmp = 3; // (1) } } ``` 在内部,上述的函数是这样执行的: ```js function foo() { var tmp; // hoisted declaration(声明被提升) console.log(tmp); if (false) { tmp = 3; // assignment stays put (赋值停留原处) } } ``` ### 闭包 每一个函数都会保持连接到包裹它的外部函数中的变量,即使在它被创建的作用域之外。例如: ```js function createIncrementor(start) { return function () { // (1) start++; return start; } } ``` 开始`(1)`的函数离开了创建它的上下文,但保持连接到`start`的live版本: ```js > var inc = createIncrementor(5); > inc() 6 > inc() 7 > inc() 8 ``` 闭包是函数加上与其包围作用域的变量的连接。因此,`createincrementor()`的返回是一个闭包。 ### IIFE(立即执行函数表达式):引入新的作用域 IIFE(Immediately-Invoked Function Expression) 有时,您希望引入一个新的变量范围,例如,以防止变量成为全局变量。在JavaScript中,不能使用块来执行,必须使用函数。但有一个模式,用一种类似块的方式使用函数。被称为IIFE ([立即调用函数表达式](http://benalman.com/news/2010/11/immediately-invoked-function-expression/),发音为“iffy”): ```js (function () { // open IIFE var tmp = ...; // not a global variable }()); // close IIFE ``` 一定要按上面的示例(除了注释之外)输入。一个`IIFE`是一个函数表达式,在定义之后就被立即调用。在函数中,一个新的作用域存在,阻止了`tmp`成为全局变量。查看:[Introducing a New Scope via an IIFE](###),获取更多的`IIFEs`信息。 >IIFE使用案例:不经意的通过闭包共享。 闭包保持它们与外部变量的连接,这有时不是你想要的: ```js 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)`的函数接收我的当前值的快照,你可以使用一个IIFE: ```js for (var i=0; i < 5; i++) { (function () { var i2 = i; // copy current i result.push(function () { return i2 }); }()); } ``` ## 对象和构造函数 本节介绍了JavaScript的两种基本面向对象机制:单个对象和构造函数(这是对象的工厂,类似于其他语言的类)。 ### 单一对象 像所有值一样,对象具有属性。实际上,您可以考虑一个对象是一组属性,其中每个属性是一个(键,值)对。键是一个字符串,这个值是任何JavaScript值,在JavaScript中,可以通过对象文字直接创建普通对象: ```js 'use strict'; var jane = { name: 'Jane', describe: function () { return 'Person named '+this.name; } }; ``` 前面的对象具有属性`name`和`describe`。您可以读取(“get”)和写入(“set”)属性: ```js > jane.name // get 'Jane' > jane.name = 'John'; // set > jane.newProperty = 'abc'; // property created automatically ``` 值为函数的属性,如`describe`被称为方法。他们用`this`引用被调用的对象: ```js > jane.describe() // call method 'Person named John' > jane.name = 'Jane'; > jane.describe() 'Person named Jane' ``` 可以使用`in`来检查某个属性是否存在: ```js > 'newProperty' in jane true > 'foo' in jane false ``` 如果读取不存在的属性,则将得到`undefined`。因此,上述的两次检查也可以这样做[^2]: ```language > jane.newProperty !== undefined true > jane.foo !== undefined false ``` `delete`操作符可以删除一个属性: ```js > delete jane.newProperty true > 'newProperty' in jane false ``` ### 任意属性名 属性键可以是任意字符串。到目前为止,我们已经看到了对象字面量的属性名和在点运算符之后。但是,只有当它们是标识符时,才可以使用它们(查看[标识符与变量名](###))。 如果你想使用其他字符串作为键,你必须引用它们在一个对象字面和使用方括号获取和设置属性: ```js > var obj = { 'not an identifier': 123 }; > obj['not an identifier'] 123 > obj['not an identifier'] = 456; ``` 方括号还允许您计算属性的键: ```js > var obj = { hello: 'world' }; > var x = 'hello'; > obj[x] 'world' > obj['hel'+'lo'] 'world' ``` ### 提取方法 如果你提取了一个方法,它失去了它与对象的连接。就其本身而言,函数不再是一个方法,`this`变为`undefined`(在严格的模式下)。 举个例子,让我们回到先前的对象`jane`: ```js 'use strict'; var jane = { name: 'Jane', describe: function () { return 'Person named '+this.name; } }; ``` 我们想从`jane`中提取方法`describe`,把它放到一个变量`func`中,并且调用它。然而,这不起作用: ```js > var func = jane.describe; > func() TypeError: Cannot read property 'name' of undefined ``` 解决的方法是采用所有函数都有的`bind()`方法。用它创建一个新函数,这个函数的`this`总是具有给定值: ```js > var func2 = jane.describe.bind(jane); > func2() 'Person named Jane' ``` ### 方法中的函数 每个函数都有自己的特殊变量`this`。这是不方便的,如果你在函数内嵌套一个函数,因为你不能从函数中访问方法的`this`。下面是一个例子,我们调用`forEach`使用一个函数来遍历数组: ```js 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`会产生一个错误: ```js > jane.logHiToFriends() TypeError: Cannot read property 'name' of undefined ``` 让我们看看两种修复方法。首先,我们可以把`this`存储在一个不同的变量: ```js logHiToFriends: function () { 'use strict'; var that = this; this.friends.forEach(function (friend) { console.log(that.name+' says hi to '+friend); }); } ``` 或者,`forEach`有第二参数允许你为`this`提供值: ```js logHiToFriends: function () { 'use strict'; this.friends.forEach(function (friend) { console.log(this.name+' says hi to '+friend); }, this); } ``` 函数表达式经常用作JavaScript中函数调用的参数。当你从这些函数表达式中引用(refer to)`this`时,一定要小心。 ### 构造函数:对象工厂 现在为止,你可能认为JavaScript对象只是从字符串到值的映射,JavaScript对象字面表示了一个概念,就好像其他语言的映射/字典字面量。然而,JavaScript对象也支持一个真正面向对象的功能:继承。本节不完全解释JavaScript的继承如何工作,但为了让你了解,向您展示了一个简单的模式。如果你想知道更多的信息,查看 [第17章](###)。 除了是“真正的”函数和方法外,函数在JavaScript中扮演另一个角色:如果通过`new`操作符调用函数对象,它们就成为对象的构造函数工厂。因此,构造函数是对其他语言类的粗略模拟。按照惯例,构造函数的名称以大写字母开头。 例如: ```js // 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`操作符去调用它: ```js > var p = new Point(3, 5); > p.x 3 > p.dist() 5.830951894845301 ``` `p`是一个`Point`的一个实例: ```js > p instanceof Point true ``` ## 数组 数组是可以通过在0开始的整数索引访问的元素序列。 ### 数组字面量 数组字面量很方便创建数组: ```js > var arr = [ 'a', 'b', 'c' ]; ``` 上述的数组有三个元素:字符串“a”、“B”和“c”。您可以通过整数索引访问它们: ```js > arr[0] 'a' > arr[0] = 'x'; > arr [ 'x', 'b', 'c' ] ``` `length`属性表示数组有多少个元素。可以使用它来添加元素或者移除元素: ```js > var arr = ['a', 'b']; > arr.length 2 > arr[arr.length] = 'c'; > arr [ 'a', 'b', 'c' ] > arr.length 3 > arr.length = 1; > arr [ 'a' ] ``` `in`操作符也可以对数组使用: ```js > 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 ``` 注意:数组是对象,因此可以具有对象属性: ```js > var arr = []; > arr.foo = 123; > arr.foo 123 ``` ### 数组方法 数组有很多方法(见[数组原型方法](##))。这里有几个例子: ```js > 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' ``` ### 遍历数组 有几种遍历元素的数组方法(参见[迭代(无损)](###))。两个最重要的是`forEach`和`map`。 foreach遍历数组,传递了当前元素和它的索引: ```js [ 'a', 'b', 'c' ].forEach( function (elem, index) { // (1) console.log(index + '. ' + elem); }); ``` 注意,行`(1)`中的函数可以忽略参数。它可以,例如,只有参数`elem`元素。 `map`通过将函数应用于现有数组的每个元素,来创建一个新数组: ```js > [1,2,3].map(function (x) { return x*x }) [ 1, 4, 9 ] ``` ## 正则表达式 JavaScript有内置支持正则表达式(第19章教程,详细解释他们是如何工作的)。他们以斜线(/)分隔: ```js /^abc$/ /[A-Za-z0-9]+/ ``` ### test()方法:匹配吗 ```js > /^a+b+$/.test('aaab') true > /^a+b+$/.test('aaa') false ``` ### exec()方法:匹配以及捕获分组 ```js > /a(b+)a/.exec('_abbba_aba_') [ 'abbba', 'bbb' ] ``` 返回的数组中,索引为0的项包含了完整匹配,索引为1的项为第一组的捕获,等等。有一个办法(讨论[RegExp.prototype.exec: Capture Groups](###))来多次调用此方法获取所有匹配。 ### replace()方法:搜索和替换 ```js > '<a> <bbb>'.replace(/<(.*?)>/g, '[$1]') '[a] [bbb]' ``` `replace`的第一个参数必须是带有`/g`标志的正则表达式。否则,只有在第一次发生时被替换(也就是替换一次,后面满足条件的字符就不替换了)。还有一个办法(讨论[String.prototype.replace: Search and Replace](###))使用函数计算替换。 ## Math Math (查看 [第21章](###)) 是一个具有算术函数的对象。下面的示例: ```js > 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**.* methods** (查看[Console的接口](###)) 这些浏览器的特定方法不是该语言的一部分,但其中的一些方法可以在Node.js使用。 *** [^first-class-functions]: 在计算机科学中,如果一门编程语言把函数看做头等公民就可以这门语言支持头等函数。具体也就是说,函数能像参数那样被传递到另一个函数、从另一个函数那像值一样被返回出来、函数可以赋值给变量或者存在数据结构中。 [^2]:注意: 此检查将报告不存在的属性,但存在`undefined`的值。