跳转至

5.0 Core JavaScript

1. 简介

1.1 什么是 JavaScript?

一种高级的 high-level、动态的 dynamic、无类型的 untyped 和解释型 interpreted 的编程语言。

它已在 ECMAScript 语言规范中标准化。

与 HTML 和 CSS 一起,它是万维网 World Wide Web 内容生产的三大基本技术之一。HTML 定义了网页的结构 skeleton,CSS 处理表示 (外观),而 JavaScript 增加了动态性和交互性。

它是基于 prototype-based 的,具有 first-class functions,使其成为一种多范式语言,支持 object-oriented、imperative 和 functional 编程风格。它的设计初衷是易于快速编写。

需要注意的是,它与 Java 毫无关系;这个名字是在 Java 流行时的一个营销决策。

1.2 组成部分

可以分为三个主要组成部分:

  • Core JavaScript:语言的基础,处理数据操作和显示。这是本指南的重点。
  • Client-side JavaScript:涉及支持浏览器控制和用户交互的对象,允许动态修改网页。例如,你可以根据用户输入更改文本或使图片出现/消失。
  • Server-side JavaScript:包含支持在 Web 服务器上使用的对象。像 NodeJS 这样的环境允许在服务器上运行。

1.3 在前端的用途

主要用于前端以实现:

  • 减少 Server Load:通过将计算推送到客户端的浏览器,服务器可以处理更多用户并更好地扩展。
  • 提高 Responsiveness:与将数据发送到服务器并等待响应相比,客户端处理提供了更快的反应时间。
  • Form Handling 和 Validation:可以处理表单并直接在浏览器中验证用户输入。注意:关键的验证 必须 同时在服务器端进行,因为客户端控件很容易被绕过。客户端验证是为了用户体验和减少不必要的服务器请求,而不是为了安全。
  • Dynamic Page Alteration:可以与网页的内部模型(文档对象模型 - DOM)交互,以动态更改其内容和结构。
  • Complex User Interfaces:它能够创建比单独使用 HTML 和 CSS 所能提供的更复杂的用户界面。

1.4 Event-driven Computation

许多程序旨在响应用户操作,这些操作称为 events,例如鼠标点击或按键。

主要任务通常是响应这些 events 并触发相应的操作。

示例:HTML 按钮中的 onclick 属性可以在按钮被点击时执行代码。

<html><body><p> Web 开发者的狗说了什么?</p><button onclick="getElementById('demo').innerHTML='Href href!'"> 告诉我!</button><p id="demo"><p/></body></html>

在此示例中,单击 "告诉我!" 按钮会更改 ID 为 "demo" 的段落内容。

2. 执行和包含

2.1 Execution Environments

运行有两个主要环境:

  • The Browser:每个现代 Web 浏览器都可以执行。许多函数旨在在 HTML 容器或窗口中工作。
  • NodeJS:一个服务器端环境。

本指南将主要关注在浏览器中执行。

2.2 在网页中包含

有几种方法可以将代码包含在 HTML 页面中:

  • Inline in a tag attribute:代码可以直接放在 HTML 标签的属性中,例如 onclick
  • 在文档主体中使用 <script> 标签:代码可以嵌入在 <script> 标签内,通常放在 HTML 文档的 <head><body> 中。
<!DOCTYPE html><html><head><script>function myFunction() {
    document.getElementById("demo").innerHTML = "段落已更改。";
}</script></head><body><h1>一个网页</h1><p id="demo">一个段落</p><button type="button" onclick="myFunction()">试试看</button></body></html>

在此示例中,myFunction<head> 中定义,并由按钮的 onclick 事件调用。

  • From an external file:代码可以存储在一个单独的 .js 文件中,并使用 <script> 标签的 src 属性进行链接。这是组织代码和使用外部库的常见做法。

函数通常应在使用前定义,这就是为什么脚本通常放在 <head> 中或从外部链接的原因。

2.3 在浏览器中调试

错误通常由浏览器检测和处理。

像 Chrome 这样的浏览器提供了 developer tools,具有以下功能:

  • Console:允许你与代码和页面交互,查看日志消息 (console.log),并查看错误消息。错误通常在网页本身上被静默处理,但会显示在控制台中。
  • Sources Tab:允许你浏览代码 (HTML, CSS, JavaScript)。
  • BreakpointsDebugger:允许你在代码中设置断点,单步执行,并检查变量。

2.4 浏览器中的输入/输出 (I/O)

在浏览器中,主要输出是网页(HTML canvas)本身;结果通常通过修改页面内容来显示。

  • console.log():用于通过向浏览器控制台写入消息来进行简单调试。现在认为将大量日志或错误消息直接写入 document object 是不好的做法。
  • alert():向用户显示一个简单的弹出消息框。
  • confirm():显示一个带有“确定”和“取消”按钮的弹出框,允许用户进行布尔响应。
  • prompt():显示一个提示用户输入的弹出框。

这些 I/O 方法(alertconfirmprompt)通常用于快速原型制作,而不是用于最终的生产网站,在生产网站中,更倾向于使用表单和按钮等 HTML 元素进行用户交互。

3. Core JavaScript 基础

本节对 Core JavaScript 进行“旋风式游览”,重点介绍其特性以及与 Python 和 Java 等语言的差异。

3.1 Variables

  • Naming Rules:必须以字母、美元符号 ($) 或下划线 (_) 开头。可以继续使用字母、数字、美元符号或下划线。
  • Case-sensitive
  • 习惯上使用 camelCase 书写(例如,myVariableName),这不同于 Python 的 snake_case,但与 Java 的约定类似。
  • Assignment:使用标准的等号 (=) 符号。
  • Declaration Keywords:提供了四种声明变量的方式:
  • let:推荐的默认关键字。
  • const:用于值不能被重新赋值的常量。
  • var:具有不同作用域规则的旧关键字。
  • 无关键字 Undeclared:创建 global variable;通常不鼓励。
let z = x + y;
const x = 6;
var stopFlag = true;
zz = z; // 未声明 (成为全局变量)
  • Multiple Declarations:可以在同一行声明多个变量。
let counter, index, pi = 3.14159265, rover = "Palmer";

(标准的行为是只有 pi 会被初始化为 3.14159265;counterindex 将是 undefined。)

3.2 Keywords

某些词是保留的,不能用作变量名。

3.3 Syntax 和 Error Tolerance

  • Comments
  • 单行注释://
  • 多行注释:/* ... */
  • Semicolons - ;:语句 应该 以分号结束。

然而,如果分号缺失且语句看起来完整,解释器会尝试自动插入分号(自动分号插入 - ASI)。

  • Banana Skin:ASI 可能导致意外行为和错误,使调试变得困难。例如:
return  // ASI 在此插入分号:return;
x;      // 这变成了一条不可达的语句:x;

这实际上会返回 undefined 而不是 x

  • Error Handling Philosophy:被设计成非常“健壮”且能容忍错误。它通常会尝试“猜测”程序员的意图并继续执行,而不是停止。这可能使调试具有挑战性。

3.4 Primitives 和 Objects

区分原始数据类型和对象:

  • Primitive Data Types:这些是直接存储的不可变值(通常在 stack 上)。主要的 primitives 是:
  • Number
  • String
  • Boolean
  • null
  • undefined

    (ES6 引入了 Symbol,后来又添加了 BigInt。)

  • Objects:如果一个值不是 primitive,它就是一个 object。这包括:
  • Function
  • Array
  • Date
  • Math 等。

    Objects 作为对内存(heap)中存储其属性的位置的 reference 或 pointer 存储。

  • Object Properties:使用点 (.) 语法访问(例如,"abc".lengthMath.sin(...))。
  • Garbage Collection:自动管理内存。当不再有对 objects 的引用时,它们会被垃圾回收。

3.5 数据类型详解

3.5.1 Numbers

默认情况下,所有 Number 值都表示为双精度 64 位浮点数(IEEE 754 标准)。(注意:BigInt 是用于任意大整数的单独类型)。

  • Arithmetic Precision:由于浮点表示,进行算术运算时要小心,因为可能会出现舍入错误。
  • Operators
  • 标准:+-*/% (modulo)
  • increment/decrement++--
  • compound assignment+=-=*=/=%=
  • 特殊数字值 (Number 对象的属性):
  • Number.MAX_VALUE:可表示的最大数字。
  • Number.MIN_VALUE:可表示的最小正数。
  • Number.NaN: "Not a Number" - 表示无效的数值结果。
  • Number.POSITIVE_INFINITYNumber.NEGATIVE_INFINITY:表示 Infinity
  • Banana Skin:除以零会导致 Infinity,而不是错误。
  • Number.PI:π 的值。(数学常数更常通过 Math 对象访问,例如 Math.PI)。
  • Math 对象:提供高级数学函数和常量(例如,Math.sin()Math.cos())。
  • parseInt():将字符串转换为整数。如果字符串无法转换为有效的数字,parseInt() 返回 NaN
  • NaN ("Not a Number"):因无效操作而返回。
  • Banana SkinNaN 具有“传染性”:任何以 NaN 作为输入的数学运算也将导致 NaN
  • isNaN():用于检查值是否为 NaN 的内置函数。

3.5.2 Strings

字符串是 Unicode 字符序列。

  • Literals:用单引号 ('...') 或双引号 ("...") 界定。
  • Escaping Characters:在字符串中使用反斜杠 (\) 来转义特殊字符:\'\"\\\n\t
  • Empty String""''
  • No Character Data Type:单个字符表示为长度为 1 的字符串。
  • Properties and Methods
  • length:返回字符串中字符数的属性。
  • 常用方法包括:charAt(index)indexOf(substring)substring(startIndex, endIndex)toLowerCase()toUpperCase()
  • String Concatenation+ operator 连接字符串。
  • Implicit/Explicit conversion:一元加号 + 可用于将字符串转换为数字(例如,+ "123" 变为 123)。
  • toString() 方法可以将数字(或其他类型)转换为字符串。

3.5.3 Booleans

具有 Boolean 类型,字面值为 truefalse

  • Logical Operators
  • Logical NOT!
  • Logical AND&&
  • Logical OR||
  • &&|| 运算符使用 short-circuit evaluation:仅在必要时才对第二个操作数求值。

3.5.4 null 和 undefined

Banana Skin:有两种不同的值来表示有意义值的缺失,这可能令人困惑。

  • null:表示有意的非值或“空”值。它是一个赋值。
  • undefined:意味着变量已声明但尚未赋值,或者正在访问不存在的变量,或者未提供函数参数。

在实践中,你通常会测试 null。意外遇到 undefined 可能表示存在错误。

3.6 Dynamic Typing 和 Type Coercion

3.6.1 Dynamic Typing

是动态类型的,这意味着变量在声明时没有固定的关联类型。

一个变量可以在程序执行期间的不同时间保存不同类型的值。

let value = 5;       // value 是一个 Number
value = "Hello";   // value 现在是一个 String。没有错误。
  • typeof operator:返回一个字符串,指示其操作数的当前类型。
  • typeof null 返回 "object" (这是一个历史遗留的怪癖)。
  • typeof function(){} 返回 "function"
  • typeof undefinedVariable 返回 "undefined"

3.6.2 Implicit Type Coercion

Banana Skin:如果类型不同,经常尝试自动将值从一种类型转换为另一种类型以执行操作。这称为 type coercion,可能导致非常不直观且容易出错的行为。

  • 期望 Number 时的转换:
  • true 变为 1false 变为 0
  • 如果字符串表示有效数字,则将其转换为数字;否则,它们将变为 NaN
  • 注意:空字符串 '' 转换为 0,而不是 NaN
  • null 转换为 0
  • undefined 转换为 NaN
  • 期望 String 时的转换:
  • 数字转换为其字符串表示形式。
  • 布尔值转换为 "true""false"
  • null 转换为 "null"
  • undefined 转换为 "undefined"
  • 期望 Boolean 时的转换 (Truthy/Falsy values):
  • Falsy 值 (转换为 false):0-0NaN"" (空字符串)、nullundefined
  • Truthy 值 (所有其他值都转换为 true):任何非零数字、任何非空字符串、objects (包括 arraysfunctions)。
  • 强制转换的有用案例(谨慎使用):
  • 在访问属性之前检查 null/undefined 对象:

    var property = object && object.getProperty(); // 如果 'object' 为 null/undefined,则其为 falsy,发生短路。
    
  • 设置默认值:

    let name = otherName || "default"; // 如果 'otherName' 为 falsy,则 'name' 变为 "default"。
    
  • Manual Coercion:可以使用 Number()String()Boolean() 函数显式执行这些转换,这通常更清晰。
Number("42");    // 42
String(123);   // "123"
Boolean("");   // false

3.6.3 Comparisons 和 Coercion

比较运算符(><>=<===!====!==)适用于数字和字符串。

  • Loose Equality - ==不等 (!=)
  • Banana Skin:如果操作数类型不同,这些运算符会在比较它们之前执行类型强制转换。这会导致许多不直观的结果。
  • 1 == truetrue
  • '' == 0true
  • Non-Transitivity:使用 == 的相等性不具有传递性。
  • Strict Equality - ===不等 (!==)
  • Best Practice:仅当两个操作数相等 类型相同时,这些运算符才返回 true。不执行类型强制转换。
  • 1 === truefalse
  • 默认使用 ===!==

3.6.4 Type Coercion 疯狂示例

由于类型强制转换而产生的一些“疯狂”结果:

  • 依赖于运算符的强制转换方向:
  • 2 - "1" 结果为 1
  • 2 + "1" 结果为 "21"
  • 意外的 NaN
  • "b" + "a" + + "a" + "a" 结果为 "baNaNa"

3.7 Control Structures

支持类似于 C/C++/Java 和 Python 中的标准控制流语句。

3.7.1 Loops

  • for 循环 (C-style)
let triangle = 0;
for (let i = 1; i <= 3; i++) {
    triangle += i;
}
// triangle 将为 6
  • for...in 循环:遍历对象的属性(包括数组索引或对象键)。
// 对于对象:
const person = {fname: "Quentin", lname: "Coldwater", age: 30};
let text = "";
for (let x in person) { // x 将是 "fname", "lname", "age"
    text += person[x] + " ";
}
// text 可能为 "Quentin Coldwater 30 " (对象属性的顺序不保证)

// 对于数组 (遍历索引):
const arr = ["a", "b", "c"];
for (let index in arr) { // index 将是 "0", "1", "2" (字符串!)
    console.log(arr[index]);
}
  • while 循环
let countdown = "";
let i = 3;
while (i >= 0) {
    countdown += i + "!";
    i--;
}
// countdown 将为 "3!2!1!0!"
  • do...while 循环:在检查条件之前执行循环体一次。
let result = '';
let j = 0;
do {
    j = j + 1;
    result = result + j;
} while (j < 5);
// result 将为 "12345"

3.7.2 Conditional Statements

  • if...else if...else 语句
let greeting;
let time = new Date().getHours();
if (time < 10) {
    greeting = "Good morning";
} else if (time < 20) {
    greeting = "Good day";
} else {
    greeting = "Good evening";
}

即使对于单语句块,通常也建议使用大括号 {}

  • switch 语句:根据表达式的值提供多路分支。
let text;
switch (new Date().getDay()) { // getDay() 返回 0 代表星期日,1 代表星期一,依此类推。
    case 0:
        text = "Today is Sunday";
        break; // 重要,防止 fall-through
    case 6:
        text = "Today is Saturday";
        break;
    default: // 可选
        text = "Looking forward to the Weekend!";
}
  • Ternary Operator - condition ? exprIfTrue : exprIfFalse:编写 if-else 表达式的简洁方法。
let isMember = true;
let price = isMember ? '$2.00' : '$10.00'; // price 将为 '$2.00'

4. Arrays

中的数组是动态的、有序的值集合。它们是一种特殊类型的 object

4.1 Array 基础

  • Definition:由数值索引的元素列表。
  • IndexingArray 索引从 0 开始。
  • length 属性Arraylength 比最高索引大一。它会动态更新。
  • Dynamic SizeArray 的大小可以在创建后修改。
  • Heterogeneous Elements
  • Banana SkinArray 的元素不必是同一类型。
var person = []; // 创建一个空数组
person[0] = "John";
person[1] = "Doe";
person[2] = 46; // 同一个数组中包含 String、String 和 Number
// person.length 将为 3
// person[0] 将返回 "John"
  • Array 字面量 (Square Bracket Notation):创建 arrays 最常用的方法。
var fruits = ["Banana", "Orange", "Apple", "Mango"];

4.2 Array() Constructor Method

也可以使用 Array() 构造函数创建 arrays,但其行为因参数而异,这可能令人困惑:

  • 单个数字参数:let a = new Array(10); 创建一个 empty array,其 length 属性设置为指定的数字。Array 元素最初是 undefined
  • 无参数:let b = new Array(); 创建一个 length 为 0 的 empty array
  • 多个参数或单个非数字参数:let c = Array(10, 2, "three", "four"); 创建一个以指定参数作为其元素的 array
  • Banana SkinArray(10) 创建一个长度为 10 的 empty array,但 Array(10, 2) 创建一个 array [10, 2],长度为 2。

4.3 访问和修改 Array 元素

  • Iteration
  • 使用带有 length 属性的标准 for 循环:

    let theBestFruits = ["Banana", "Pomegranate", "Mulberry", "Pear"];
    for (let i = 0; i < theBestFruits.length; i++) {
        console.log(theBestFruits[i]);
    }
    
  • 使用 for...in 循环(将索引作为字符串迭代):

    for (let fruitIndex in theBestFruits) { // fruitIndex 将是 "0", "1", "2", "3"
        console.log(theBestFruits[fruitIndex]);
    }
    

    (通常首选 for...of (ES6) 来迭代 array 值,或传统的 for 循环或像 forEach 这样的 array 方法。)

  • Dynamic Length Modification:分配给大于或等于当前 length 的索引会增加 arraylength。中间的任何元素都将变为 undefined
theBestFruits[99] = "Yuzu"; // 如果 theBestFruits 有 4 个元素,其长度现在为 100。
// 索引 4 到 98 的元素是 undefined。
  • Accessing Non-existent Indices
  • Banana Skin:查询不存在的 array 索引将返回 undefined,而不是错误。
console.log(theBestFruits[102]); // 如果索引 102 不存在,则返回 undefined

4.4 Array 方法 (Methods)

Arrays 带有许多用于操作的内置方法。一些常见的方法包括:

  • push(element1, ..., elementN):将一个或多个元素添加到 array 的末尾,并返回新的长度。
  • pop():从 array 中删除最后一个元素并返回该元素。
  • shift():从 array 中删除第一个元素并返回该元素。
  • unshift(element1, ..., elementN):将一个或多个元素添加到 array 的开头,并返回新的长度。
  • join(separator):将 array 的所有元素连接成一个字符串。
  • reverse():就地反转 array 的元素。
  • sort(compareFunction):就地对 array 的元素进行排序。接受一个可选的比较器函数。
  • concat(array2, ..., arrayN):返回一个新 array,由该 array 与其他 array(和/或值)连接而成。
  • slice(beginIndex, endIndex):将 array 的一部分的浅拷贝返回到一个新的 array 对象中。
  • splice(startIndex, deleteCount, item1, ..., itemN):通过删除或替换现有元素和/或就地添加新元素来更改 array 的内容。
  • delete array[index]:通过将其值设置为 undefinedarray 中删除元素(留下一个空槽)。这不会更改 array 的长度或重新索引后续元素。

4.5 Associative Arrays - 即 Objects

中的术语“associative array”通常指将 objects 用作键值存储,其中字符串值(属性名)用作“索引”而不是数字。

let arr = {}; // 创建一个空对象
arr["name"] = "Bob"; // 将值 "Bob" 分配给键 "name"
// arr.name 在这里也有效。

这些从根本上说是 objects

5. Functions

函数是中的基本构建块,允许代码重用和过程抽象。它们也是 first-class objects

5.1 定义函数 (Defining a JavaScript Function)

  • Syntax
function <name>(<param1>, <param2>, ...) {
    // <statement1>
    // ...
    return <value>; // 可选的 return 语句
}

使用 function 关键字。参数没有显式定义的类型。如果省略 return 语句,或使用 return; 而没有值,则函数返回 undefined

  • Placement:函数通常必须在调用之前定义。
  • 示例:
<body><h2>JavaScript Functions</h2><p id="demo"></p><script>
function myFunction(p1, p2) {
  return p1 * p2;
}
document.getElementById("demo").innerHTML = myFunction(4, 3); // 调用函数并显示 12
</script></body>

5.2 调用函数 (Calling a JavaScript Function)

  • Parameter Passing
  • Primitive Types (Call by Value):当基本类型值作为参数传递时,它们的值会被复制。函数内部对参数的修改不会影响外部的原始变量。

    function run(x) {
        x += 1;
        return x;
    }
    let u = 1;
    let v = run(u);
    // 此处,u 仍为 1,v 为 2
    
  • Object Types (Call by Sharing/Reference):当对象(包括 arraysfunctions)作为参数传递时,传递的是对原始对象的引用。函数可以修改原始对象的属性。但是,如果函数内部的参数本身被重新赋值给一个 对象,则此重新赋值不会影响外部的原始变量。

    function setOutOnAdventure(party1) { // party1 是一个数组
        party1.push("Ivan"); // 修改原始数组
        party1.push("Mia");  // 修改原始数组
    
        let party2 = ["Felix", "Jenna", "Sheba"]; // 一个新的局部数组
        party1 = party2; // 函数内部的 'party1' 现在指向 'party2'。这不会改变传入的原始 'party1' 数组。
        party1.push("Piers"); // 修改局部的 'party2'
        return party1; // 返回局部的 'party1'
    }
    let myParty = ["Isaac", "Garet"];
    let adventureParty = setOutOnAdventure(myParty);
    console.log(myParty);        // 输出: ["Isaac", "Garet", "Ivan", "Mia"] (原始数组被修改)
    console.log(adventureParty); // 输出: ["Felix", "Jenna", "Sheba", "Piers"] (返回新数组)
    

    函数可以对其传递的对象产生 side effects

  • Parameter Laxity - Banana Skin
  • Formal Parameters:函数定义中命名的参数。
  • Actual Parameters:调用函数时提供的参数。
  • Too Few Arguments:如果传递的参数少于形参,则函数内部缺失的参数将具有值 undefined
  • Too Many Arguments:如果传递的参数多于形参,则额外的参数不能通过其形参名称直接访问,但可以使用 arguments 对象访问它们。
  • arguments 对象
  • 在任何非箭头 (non-arrow) 函数内部,都可以使用一个名为 arguments 的特殊 array-like 对象。
  • 它包含传递给函数的所有 实参,而不管定义的形参数量如何。
  • 示例:findMax 函数:

    function findMax() { // 未定义形参
        let max = -Infinity;
        for (let i = 0; i < arguments.length; i++) { // 遍历所有传递的参数
            if (arguments[i] > max) {
                max = arguments[i];
            }
        }
        return max;
    }
    console.log(findMax(4, 5, 6, 2, 10)); // 输出: 10
    console.log(findMax(1, 2));          // 输出: 2
    

5.3 函数作为 First-Class Objects

在中,函数是 first-class citizens。它们像任何其他对象一样对待:可以赋值给变量,作为参数传递给其他函数,从函数返回,并具有属性和方法。

  • Accessing a Function Definition:在不带圆括号 () 的情况下引用函数名会返回函数对象本身。
  • Assigning Functions to Variables
function announce() {
    console.log("It's Groundhog day!");
}
let reannounce = announce; // 'reannounce' 现在引用与 'announce' 相同的函数对象
announce();
reannounce();
  • 作为属性的函数 Methods:函数可以是对象的属性,在这种情况下,它们被称为 methods
  • Casting to String:当函数转换为字符串时,其源代码(定义)将作为文本返回。
  • Banana Skin:将函数添加到字符串将导致与函数代码的字符串连接,而不是错误。

! 5.4 Anonymous Functions - 函数表达式 和 Arrow Functions

  • Anonymous Functions:可以在没有名称的情况下定义函数。这些通常在需要临时使用函数时使用,例如 callback
  • 示例:sort() 方法的比较器:Array.prototype.sort() 方法可以接受一个可选的 comparator function 作为参数来定义排序顺序。

    let points = [2, 8, 1, 5, 3, 1];
    points.sort(); // 默认排序
    console.log(points); // 输出: [1, 1, 2, 3, 5, 8] (对于数字)
    
    // 使用匿名函数按降序排序
    points.sort(function(a, b) { // 无名函数
        return b - a;
    });
    console.log(points); // 输出: [8, 5, 3, 2, 1, 1]
    
  • Arrow Functions - => (ES6):为编写匿名函数提供了更简洁的语法。它们在 this keyword 方面也有不同的行为(词法 this)。
points.sort((a, b) => { return b - a; }); // 箭头函数等效形式
// 如果函数体是单个表达式,则可以省略大括号和 'return':
points.sort((a, b) => b - a);

这种将函数作为参数传递的能力非常强大,特别是对于 event handling

// 传统函数
let double = function(x) {
  return x * 2;
};

// 箭头函数
let doubleArrow = x => x * 2;

console.log(doubleArrow(5)); // 输出: 10

5.5 Recursive Functions

支持递归函数(调用自身的函数)。

  • Named Anonymous Functions for Recursion:如果匿名函数需要调用自身,则它需要在其自身作用域内具有用于自引用的名称。
var ninja = {
    yell: function cry(n) { // 'cry' 是此匿名函数内部自引用的名称
        return n > 0 ? cry(n - 1) + "a" : "hiy";
    }
};
console.log(ninja.yell(5)); // 输出: "hiyaaaaa"

6. Objects

对象是的核心。它们是名称-值对(属性)的集合。

6.1 作为名称-值对的 Objects

  • Structure
  • 名称(keys)是字符串。
  • 值(values)可以是任何值,包括 primitives、其他 objectsfunctions
  • Analogy:类似于 Java 中的 HashMap<String, Object> 或 Python 中的 dict
  • Object Literals - {...}:创建 objects 最快、最常用的方法。
let bubbleTea = {
    ingredients: ["Tea", "Milk", "Tapioca", "Honey"], // 值是一个数组
    taste: "Delicious",                             // 值是一个字符串
    timeToDrinkInSeconds: function() {                // 值是一个函数 (方法)
        return 41;
    }
};
  • Everything Non-Primitive is an Object:如果变量不是 primitive,它就是一个 object。这意味着 arraysfunctions 从根本上说也是具有特殊特征的 objects

6.2 访问和修改 Object Properties

  • Dot Notation:访问属性的标准方法:objectName.propertyName
bubbleTea.taste = "Sublime"; // 修改 'taste' 属性
  • Bracket Notation - 类数组:也可以使用方括号和作为字符串的属性名来访问属性:objectName["propertyName"]
bubbleTea["taste"] = "Sublime"; // 等效于点表示法示例
  • Advantages of Bracket Notation
  • Dynamic Property Names:属性名可以在运行时计算。

    let myProperty = "ingredients";
    console.log(bubbleTea[myProperty]); // 访问 bubbleTea.ingredients
    
  • Reserved Words as Property Names:可用于设置或获取名称为保留关键字的属性。
  • Methods:作为对象属性的函数称为 methods
  • Updating Methods at Runtime:由于函数是 first-class objects,而方法只是保存函数的属性,因此你可以在运行时通过将新函数分配给属性来更改对象的方法。这种动态特性允许对对象进行“hot patching”或修改行为。
bubbleTea.timeToDrinkInSeconds = function() {
    return "Far too quick"; // 更改方法的行为和返回类型
};

6.3 对象的 Dynamic Nature

由于对象是名称-值对的集合,因此其结构不是固定的:

  • Adding New Properties:你可以在创建对象后的任何时间向其添加新属性。
let team = {};
team.attacker = "Cloud";
team.tank = "Barret";
team.healer = "Aerith"; // 动态添加的属性
  • Deleting Propertiesdelete 运算符可以从对象中删除属性。
delete team.healer; // 删除 'healer' 属性
  • 遍历属性 (for...in 循环):你可以遍历对象的可枚举属性名。
for (let role in team) { // 'role' 将是 "attacker",然后是 "tank"
    console.log(role + ": " + team[role]); // 使用 team[role] 访问属性值
}
// 输出:
// attacker: Cloud
// tank: Barret

(现代引擎通常对非整数键按插入顺序迭代)。

6.4 The Global Object

环境有一个可从代码中任何位置访问的 global object

  • Names
  • 在浏览器中:window
  • 在 NodeJS 和其他环境中(更具可移植性):globalThis
  • Implicit Global Variables:在顶层声明的未使用 letconst 的变量将成为 global object 的属性。
globalThis.x = 5; // 显式创建全局属性
alert(x); // 隐式访问 globalThis.x
  • Caution:通常不鼓励过度使用全局变量。

6.5 this 关键字

this keyword 是一个特殊的标识符,其值由函数的调用方式(其 execution context)确定。

  • Common Contexts
  • In a Method:当函数作为对象的方法 (object.method()) 调用时,this 指向调用该方法的对象(“owner object”)。

    let amberPearlLatte = {
        basePrice: 10,
        getPrice: function() {
            let tax = 1.25;
            return this.basePrice * tax; // 'this' 指向 amberPearlLatte
        }
    };
    console.log(amberPearlLatte.getPrice()); // 输出: 12.5
    
  • Outside any Function, Top Levelthis 指向 global object (windowglobalThis)。
  • 在常规函数中 (Not a Method, Not Arrow Function)

    • Non-Strict Modethis 也指向 global object
    • Strict Mode - "use strict";:在直接调用的常规函数中,thisundefined
  • In an Event Handlerthis 通常指接收到 event 的 HTML 元素。
  • Arrow Functions:箭头函数没有自己的 this binding。它们在定义时从其周围(词法 - lexical) 作用域继承 this
  • call()apply()bind():这些方法可用于在调用函数时显式设置 this 的值。

6.6 Constructors 和 new 关键字

当你需要创建多个具有相似结构和行为的对象时,可以使用 constructor functions

  • Constructor Functions:与 new keyword 一起使用以创建和初始化对象的常规函数。
  • 按照约定,它们的名称大写(例如,Drink)。
  • 在构造函数内部,this 指向正在创建的新对象。
  • 示例:

    function Drink(basePrice) { // 构造函数
        this.basePrice = basePrice; // 在新对象上初始化属性
        this.getPrice = function() { // 为新对象分配一个方法
            let tax = 1.125; // 示例税率
            return this.basePrice * tax;
        };
    }
    
  • new 关键字:当执行 new ConstructorFunction(...) 时:
  1. 创建一个全新的空对象。
  2. 此新对象的 prototype 设置为 ConstructorFunction.prototype
  3. 使用指定的参数调用 ConstructorFunction,并将 this 绑定到新创建的对象。
  4. 如果构造函数没有显式 return 一个对象,则新创建的对象将自动作为 new 表达式的结果返回。
  • 用法示例:
let amberPearlLatte = new Drink(10); // 创建一个 Drink 对象
let winterMelonTea = new Drink(11);  // 创建另一个 Drink 对象
console.log(amberPearlLatte.getPrice()); // 调用此实例上的 getPrice 方法
  • Issue with Simple Constructors and Methods:在上面的 Drink 示例中,每次创建 Drink 对象时,都会为 getPrice 创建并分配一个 函数对象给该实例。如果所有实例的方法逻辑相同,则效率低下。Prototypes 提供了一种共享方法的方法。

7. Prototypes 和 OOP

OOP (Object-Oriented Programming) 方法不同于像 Java 或 C# 这样的 class-based 语言。它使用基于 prototype 的系统。

  • Prototype-Based Programming:传统意义上没有显式的 classes (ES6 class syntaxprototypes 之上的 syntactic sugar)。
  • 行为重用(inheritance)是通过对象从其 prototype 对象继承属性和方法来实现的。
  • 对象是通过“cloning”或链接到现有对象(prototypes)来创建的。
  • 也称为 class-lessprototype-orientedinstance-based 的编程。

7.1 使用 Prototypes 重用函数

为了避免为每个对象实例复制函数,可以将方法添加到构造函数的 prototype property 中。

  • 中的每个函数都自动拥有一个 prototype property,它是一个对象。
  • 当函数与 new 一起用作构造函数时,新创建的对象会获得一个到构造函数的 prototype 对象的内部链接(通常称为 [[Prototype]]__proto__)。
  • Prototype Chain:当你尝试访问对象上的属性时:
  1. 首先检查对象本身是否直接拥有该属性。

  2. 如果未找到,则检查对象的 prototype

  3. 如果仍未找到,则检查 prototype 的 prototype,依此类推,形成一个“prototype chain”。

    此链将继续,直到找到该属性或到达链的末尾(通常是 Object.prototype,其自身的 prototype 为 null)。

  • Prototype 添加方法:
function Drink(basePrice) {
    this.basePrice = basePrice; // 实例特定的属性
}
// 将 getPrice 添加到 Drink.prototype 以便所有 Drink 实例共享它
Drink.prototype.getPrice = function() {
    let tax = 1.125; // 示例税率
    return this.basePrice * tax; // 'this' 将正确引用调用 getPrice 的实例
};

let amberPearlLatte = new Drink(10);
let winterMelonTea = new Drink(11);
console.log(amberPearlLatte.getPrice()); // 有效!在 Drink.prototype 上查找 getPrice
console.log(winterMelonTea.getPrice());  // 也有效,使用相同的共享函数

现在,amberPearlLatte 和 winterMelonTea 没有自己的 getPrice 属性。当调用 amberPearlLatte.getPrice() 时,会沿着 prototype chain 查找到 Drink.prototype 并在那里找到它。

Object.prototype 是大多数 prototype chains 的根。

7.2 运行时向 Prototypes 添加方法

Banana Skin / 强大功能Prototypes 可以在程序执行期间的 任何时候 进行修改。这意味着你可以向 prototype 添加新方法,所有从该 prototype 继承的现有对象(以及将来的对象)都将立即获得对这些新方法的访问权限。

  • 示例:向 String.prototype 添加 reversed 方法:
const s = "live on"; // 一个现有的字符串实例
// 向所有字符串的 prototype 添加一个新方法
String.prototype.reversed = function() {
    let r = "";
    for (var i = this.length - 1; i >= 0; i--) {
        r += this[i];
    }
    return r;
};
console.log(s.reversed()); // 输出: "no evil"
                           // 's' 现在可以使用 'reversed',即使它是在 'reversed' 被添加到 prototype 之前创建的。
                           // 查找是在运行时进行的。
console.log("another string".reversed()); // 也有效
  • InheritancePrototypes 是中实现继承的机制。“subclass”(构造函数)可以将其 prototype 设置为“superclass”构造函数的实例,或者更常见的是,使用 Object.create() 来链接 prototypes

本指南的下一部分将继续介绍 Variable ScopingClosures