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)。
- Breakpoints 和 Debugger:允许你在代码中设置断点,单步执行,并检查变量。
2.4 浏览器中的输入/输出 (I/O)
在浏览器中,主要输出是网页(HTML canvas)本身;结果通常通过修改页面内容来显示。
console.log()
:用于通过向浏览器控制台写入消息来进行简单调试。现在认为将大量日志或错误消息直接写入 document object 是不好的做法。alert()
:向用户显示一个简单的弹出消息框。confirm()
:显示一个带有“确定”和“取消”按钮的弹出框,允许用户进行布尔响应。prompt()
:显示一个提示用户输入的弹出框。
这些 I/O 方法(alert
、confirm
、prompt
)通常用于快速原型制作,而不是用于最终的生产网站,在生产网站中,更倾向于使用表单和按钮等 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;通常不鼓励。
- Multiple Declarations:可以在同一行声明多个变量。
(标准的行为是只有 pi
会被初始化为 3.14159265;counter
和 index
将是 undefined。)
3.2 Keywords
某些词是保留的,不能用作变量名。
3.3 Syntax 和 Error Tolerance
- Comments:
- 单行注释:
//
- 多行注释:
/* ... */
- Semicolons - ;:语句 应该 以分号结束。
然而,如果分号缺失且语句看起来完整,解释器会尝试自动插入分号(自动分号插入 - ASI)。
- Banana Skin:ASI 可能导致意外行为和错误,使调试变得困难。例如:
这实际上会返回 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".length
,Math.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_INFINITY
、Number.NEGATIVE_INFINITY
:表示 Infinity。- Banana Skin:除以零会导致
Infinity
,而不是错误。 Number.PI
:π 的值。(数学常数更常通过Math
对象访问,例如Math.PI
)。- Math 对象:提供高级数学函数和常量(例如,
Math.sin()
、Math.cos()
)。 parseInt()
:将字符串转换为整数。如果字符串无法转换为有效的数字,parseInt()
返回NaN
。NaN
("Not a Number"):因无效操作而返回。- Banana Skin:
NaN
具有“传染性”:任何以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
类型,字面值为 true
和 false
。
- 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
是动态类型的,这意味着变量在声明时没有固定的关联类型。
一个变量可以在程序执行期间的不同时间保存不同类型的值。
typeof
operator:返回一个字符串,指示其操作数的当前类型。typeof null
返回"object"
(这是一个历史遗留的怪癖)。typeof function(){}
返回"function"
。typeof undefinedVariable
返回"undefined"
。
3.6.2 Implicit Type Coercion
Banana Skin:如果类型不同,经常尝试自动将值从一种类型转换为另一种类型以执行操作。这称为 type coercion,可能导致非常不直观且容易出错的行为。
- 期望
Number
时的转换:
true
变为1
,false
变为0
。- 如果字符串表示有效数字,则将其转换为数字;否则,它们将变为
NaN
。 - 注意:空字符串
''
转换为0
,而不是NaN
! null
转换为0
。undefined
转换为NaN
。
- 期望
String
时的转换:
- 数字转换为其字符串表示形式。
- 布尔值转换为
"true"
或"false"
。 null
转换为"null"
。undefined
转换为"undefined"
。
- 期望
Boolean
时的转换 (Truthy/Falsy values):
- Falsy 值 (转换为
false
):0
、-0
、NaN
、""
(空字符串)、null
、undefined
。 - Truthy 值 (所有其他值都转换为
true
):任何非零数字、任何非空字符串、objects (包括 arrays 和 functions)。
- 强制转换的有用案例(谨慎使用):
在访问属性之前检查
null
/undefined
对象:
设置默认值:
- Manual Coercion:可以使用
Number()
、String()
和Boolean()
函数显式执行这些转换,这通常更清晰。
3.6.3 Comparisons 和 Coercion
比较运算符(>
、<
、>=
、<=
、==
、!=
、===
、!==
)适用于数字和字符串。
- Loose Equality -
==
和 不等 (!=
): - Banana Skin:如果操作数类型不同,这些运算符会在比较它们之前执行类型强制转换。这会导致许多不直观的结果。
1 == true
为true
。'' == 0
为true
。- Non-Transitivity:使用
==
的相等性不具有传递性。 - Strict Equality -
===
和 不等 (!==
): - Best Practice:仅当两个操作数相等 且 类型相同时,这些运算符才返回
true
。不执行类型强制转换。 1 === true
为false
。- 默认使用
===
和!==
。
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):
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
表达式的简洁方法。
4. Arrays
中的数组是动态的、有序的值集合。它们是一种特殊类型的 object。
4.1 Array 基础
- Definition:由数值索引的元素列表。
- Indexing:
Array
索引从 0 开始。
length
属性:Array
的length
比最高索引大一。它会动态更新。
- Dynamic Size:
Array
的大小可以在创建后修改。
- Heterogeneous Elements:
- Banana Skin:
Array
的元素不必是同一类型。
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 最常用的方法。
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 Skin:
Array(10)
创建一个长度为 10 的 empty array,但Array(10, 2)
创建一个 array[10, 2]
,长度为 2。
4.3 访问和修改 Array 元素
- Iteration:
使用带有
length
属性的标准for
循环:
使用
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
的索引会增加 array 的length
。中间的任何元素都将变为 undefined。
- Accessing Non-existent Indices:
- Banana Skin:查询不存在的 array 索引将返回 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]
:通过将其值设置为 undefined 从 array 中删除元素(留下一个空槽)。这不会更改 array 的长度或重新索引后续元素。
4.5 Associative Arrays - 即 Objects
中的术语“associative array”通常指将 objects 用作键值存储,其中字符串值(属性名)用作“索引”而不是数字。
这些从根本上说是 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):当基本类型值作为参数传递时,它们的值会被复制。函数内部对参数的修改不会影响外部的原始变量。
Object Types (Call by Sharing/Reference):当对象(包括 arrays 和 functions)作为参数传递时,传递的是对原始对象的引用。函数可以修改原始对象的属性。但是,如果函数内部的参数本身被重新赋值给一个 新 对象,则此重新赋值不会影响外部的原始变量。
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
函数:
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 作为参数来定义排序顺序。
- 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、其他 objects 或 functions。
- 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。这意味着 arrays 和 functions 从根本上说也是具有特殊特征的 objects。
6.2 访问和修改 Object Properties
- Dot Notation:访问属性的标准方法:
objectName.propertyName
。
- Bracket Notation - 类数组:也可以使用方括号和作为字符串的属性名来访问属性:
objectName["propertyName"]
。
- Advantages of Bracket Notation:
Dynamic Property Names:属性名可以在运行时计算。
- Reserved Words as Property Names:可用于设置或获取名称为保留关键字的属性。
- Methods:作为对象属性的函数称为 methods。
- Updating Methods at Runtime:由于函数是 first-class objects,而方法只是保存函数的属性,因此你可以在运行时通过将新函数分配给属性来更改对象的方法。这种动态特性允许对对象进行“hot patching”或修改行为。
6.3 对象的 Dynamic Nature
由于对象是名称-值对的集合,因此其结构不是固定的:
- Adding New Properties:你可以在创建对象后的任何时间向其添加新属性。
- Deleting Properties:
delete
运算符可以从对象中删除属性。
- 遍历属性 (
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:在顶层声明的未使用
let
或const
的变量将成为 global object 的属性。
- Caution:通常不鼓励过度使用全局变量。
6.5 this 关键字
this
keyword 是一个特殊的标识符,其值由函数的调用方式(其 execution context)确定。
- Common Contexts:
In a Method:当函数作为对象的方法 (
object.method()
) 调用时,this
指向调用该方法的对象(“owner object”)。
- Outside any Function, Top Level:
this
指向 global object (window
或globalThis
)。
在常规函数中 (Not a Method, Not Arrow Function):
- Non-Strict Mode:
this
也指向 global object。 - Strict Mode -
"use strict";
:在直接调用的常规函数中,this
是undefined
。
- Non-Strict Mode:
- In an Event Handler:
this
通常指接收到 event 的 HTML 元素。
- Arrow Functions:箭头函数没有自己的
this
binding。它们在定义时从其周围(词法 - lexical) 作用域继承this
。
call()
、apply()
、bind()
:这些方法可用于在调用函数时显式设置this
的值。
6.6 Constructors 和 new 关键字
当你需要创建多个具有相似结构和行为的对象时,可以使用 constructor functions。
- Constructor Functions:与
new
keyword 一起使用以创建和初始化对象的常规函数。
- 按照约定,它们的名称大写(例如,
Drink
)。
- 在构造函数内部,
this
指向正在创建的新对象。
示例:
new
关键字:当执行new ConstructorFunction(...)
时:
- 创建一个全新的空对象。
- 此新对象的
prototype
设置为ConstructorFunction.prototype
。 - 使用指定的参数调用
ConstructorFunction
,并将this
绑定到新创建的对象。 - 如果构造函数没有显式
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
syntax 是 prototypes 之上的 syntactic sugar)。 - 行为重用(inheritance)是通过对象从其 prototype 对象继承属性和方法来实现的。
- 对象是通过“cloning”或链接到现有对象(prototypes)来创建的。
- 也称为 class-less、prototype-oriented 或 instance-based 的编程。
7.1 使用 Prototypes 重用函数
为了避免为每个对象实例复制函数,可以将方法添加到构造函数的 prototype
property 中。
- 中的每个函数都自动拥有一个
prototype
property,它是一个对象。
- 当函数与
new
一起用作构造函数时,新创建的对象会获得一个到构造函数的prototype
对象的内部链接(通常称为[[Prototype]]
或__proto__
)。
- Prototype Chain:当你尝试访问对象上的属性时:
首先检查对象本身是否直接拥有该属性。
如果未找到,则检查对象的 prototype。
如果仍未找到,则检查 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()); // 也有效
- Inheritance:Prototypes 是中实现继承的机制。“subclass”(构造函数)可以将其 prototype 设置为“superclass”构造函数的实例,或者更常见的是,使用
Object.create()
来链接 prototypes。
本指南的下一部分将继续介绍 Variable Scoping 和 Closures。