第2章:语法(Grammar)
一、导读
1.1 本章学习目标
第2章深入讲解 JavaScript 的语法细节。虽然 JavaScript 的语法看起来与 Java 和 C 类似,但其中有很多微妙之处。本章帮助你理解 JavaScript 语法的精华与陷阱。
本章的核心学习目标包括:
- 理解 JavaScript 的数字系统:掌握 IEEE 754 浮点数的特性。
- 掌握字符串的操作:学会使用字符串方法和正则表达式。
- 理解语句的语义:掌握各种控制流语句的特点。
- 认识 JavaScript 的表达式:理解运算符的优先级和结合性。
- 避免常见的语法陷阱:如分号自动插入、浮点精度等。
1.2 为什么第二章如此重要
语法是编程的基础。对语法的深入理解能让你避免很多常见的 bug,写出更加可靠的代码。JavaScript 的语法有一些独特的特性(如分号自动插入、宽松的类型系统),如果不理解这些特性,很容易写出错误的代码。
1.3 核心问题引导
JavaScript 的数字为什么会有精度问题?如何避免?
分号自动插入机制是什么?它会导致什么问题?
typeof 运算符有哪些 surprising 的结果?
字符串的 charCodeAt 和 fromCharCode 有什么用途?
为什么 for in 循环不适合遍历数组?
二、核心概念详解
2.1 数字
JavaScript 只有一种数字类型——双精度64位浮点数(IEEE 754)。
// 所有数字都是浮点数
typeof 42; // "number"
typeof 3.14; // "number"
typeof NaN; // "number"
typeof Infinity; // "number"
// 精度问题
0.1 + 0.2; // 0.30000000000000004
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // true
// 整数运算安全
// 安全整数范围:-(2^53 - 1) 到 2^53 - 1
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MIN_SAFE_INTEGER; // -9007199254740991
// 整数运算方法
Math.floor(3.7); // 3
Math.ceil(3.2); // 4
Math.round(3.5); // 4
Math.trunc(3.7); // 3
// 数字方法
var n = 42;
n.toString(2); // "101010"(二进制)
n.toString(16); // "2a"(十六进制)
n.toFixed(2); // "42.00"2.2 字符串
JavaScript 的字符串是不可变的16位字符序列。
// 字符串长度(按 UTF-16 代码单元计算)
"Hello".length; // 5
"中文".length; // 2
// 字符串方法
var s = "Hello, World!";
s.charAt(0); // "H"
s.charCodeAt(0); // 72
String.fromCharCode(72); // "H"
s.indexOf("World"); // 7
s.lastIndexOf("l"); // 10
s.includes("World"); // true
s.startsWith("Hello"); // true
s.endsWith("!"); // true
s.slice(0, 5); // "Hello"
s.substring(7, 12); // "World"
s.trim(); // 去除两端空白
s.toUpperCase(); // "HELLO, WORLD!"
s.toLowerCase(); // "hello, world!"
s.replace("World", "JavaScript"); // "Hello, JavaScript!"
s.split(", "); // ["Hello", "World!"]2.3 语句
2.3.1 表达式语句
// 表达式语句
x = y + 1;
func();
obj.prop; // 合法但无意义2.3.2 复合语句(块)
// 块语句
{
var x = 1;
var y = 2;
// var 没有块作用域(let/const 有)
}
console.log(x); // 1(var 泄露到外部)
// 使用 let 获得块作用域
{
let a = 1;
const b = 2;
}
// console.log(a); // ReferenceError2.3.3 if 语句
// if 语句
if (condition) {
// ...
} else if (otherCondition) {
// ...
} else {
// ...
}
// 注意:falsy 值
// false, 0, "", null, undefined, NaN 都是 falsy
if (0) { } // 不执行
if ("") { } // 不执行
if (null) { } // 不执行2.3.4 switch 语句
// switch 使用严格相等
switch (value) {
case "1":
// 不会匹配数字 1
break;
case 1:
// 匹配数字 1
break;
}
// 注意:没有 break 会 fall through
switch (value) {
case 1:
console.log("one");
// 没有 break!
case 2:
console.log("two"); // 也会执行
break;
}2.3.5 for 语句
// 传统 for 循环
for (var i = 0; i < 10; i++) { }
// for in(遍历对象的可枚举属性)
var obj = {a: 1, b: 2, c: 3};
for (var key in obj) {
console.log(key + ": " + obj[key]);
}
// 注意:for in 也会遍历原型链上的属性
// 应该使用 hasOwnProperty 过滤
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key + ": " + obj[key]);
}
}
// for of(遍历可迭代对象的值)
var arr = [10, 20, 30];
for (var value of arr) {
console.log(value); // 10, 20, 30
}2.4 表达式
2.4.1 运算符
// 三元运算符
var result = condition ? value1 : value2;
// 逻辑运算符
var a = true && "hello"; // "hello"
var b = false || "world"; // "world"
var c = !true; // false
// 空值合并运算符
var value = nullable ?? "default";
// 可选链
var name = user?.profile?.name;2.4.2 类型转换
// 隐式类型转换(容易出错)
"3" + 4; // "34"(字符串拼接)
"3" - 4; // -1(数字运算)
"3" * "4"; // 12
true + 1; // 2
null + 1; // 1
undefined + 1; // NaN
// 显式类型转换
Number("42"); // 42
Number(""); // 0
Number(" "); // 0
Number("abc"); // NaN
parseInt("42px"); // 42
parseFloat("3.14em"); // 3.14
String(42); // "42"
Boolean(0); // false
Boolean(""); // false
Boolean(null); // false三、实战案例
3.1 安全的数字比较
// 浮点数比较工具
function nearlyEqual(a, b, epsilon) {
epsilon = epsilon || Number.EPSILON;
return Math.abs(a - b) < epsilon;
}
nearlyEqual(0.1 + 0.2, 0.3); // true
// 整数安全的货币计算(以分为单位)
function addCents(a, b) {
return Math.round(a * 100 + b * 100) / 100;
}
addCents(0.1, 0.2); // 0.33.2 字符串模板
// 模板字符串(ES6)
var name = "World";
var greeting = `Hello, ${name}!`;
// 多行字符串
var html = `
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
`;
// 标签模板
function safeHtml(strings, ...values) {
var result = "";
strings.forEach(function(str, i) {
result += str;
if (i < values.length) {
result += escapeHtml(values[i]);
}
});
return result;
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
var userInput = "<script>alert('xss')</script>";
var safe = safeHtml`<div>${userInput}</div>`;四、本章小结
第2章深入讲解了 JavaScript 的语法细节:
数字:所有数字都是浮点数,注意精度问题。
字符串:不可变的字符序列,提供丰富的操作方法。
语句:理解 for in 和 for of 的区别,注意分号自动插入。
表达式:注意隐式类型转换的陷阱。
类型转换:优先使用显式转换,避免隐式转换的意外行为。
理解语法细节是避免 bug 的基础。JavaScript 的语法看似简单,但其中有很多微妙的陷阱需要警惕。
五、思考与练习
思考题
为什么 JavaScript 没有独立的整数类型?
for in 和 for of 有什么区别?各自适用于什么场景?
分号自动插入机制在什么情况下会导致 bug?
如何安全地比较两个浮点数?
parseInt 和 Number 有什么区别?
编程练习
实现精确的货币计算:避免浮点精度问题。
实现字符串工具函数:包括 capitalize、camelCase、kebabCase。
实现安全的类型转换:处理各种边界情况。