JavaScript (简称 JS) 是前端开发“三件套”中至关重要的一环,三者各司其职,共同构建了现代 Web 的基石:

  • HTML (结构):定义网页的骨架和内容。
  • CSS (表现):负责网页的美化和布局。
  • JavaScript (行为):赋予网页灵魂,负责用户交互、动态渲染及逻辑处理。

正如 Jeff Atwood 提出的著名的 Atwood 定律 所言:“Any application that can be written in JavaScript, will eventually be written in JavaScript.

如今的 JS 生态早已超越了浏览器的边界,它的触角延伸到了软件开发的各个领域:

  • Web 前端:传统的网页开发核心。
  • 移动端:借助 React Native、Weex、Uniapp 等框架实现跨平台 App 开发。
  • 桌面端:利用 Electron 开发跨操作系统的桌面应用(如 VS Code, Slack)。
  • 服务端:Node.js 让 JS 能够编写高性能的后端服务。
  • 机器学习:TensorFlow.js 等库让浏览器也能进行 AI 模型训练与推理。

var, let 与 const

在 ES6 之前,var 是声明变量的唯一方式。然而,随着应用规模的扩大,var 的设计缺陷逐渐暴露,导致了许多难以排查的 Bug。ES6 引入了 letconst,从语法层面规范了变量的生命周期。

var 的经典问题与隐患

var 的作用域只有“全局”和“函数”两级,缺乏“块级作用域”的概念。此外,它还存在变量提升 (Hoisting)重复声明静默覆盖的问题。

最经典的莫过于“循环闭包陷阱”,以及对全局对象 (window) 的污染:

实操示例:循环中的陷阱与全局污染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// [隐患] var 的陷阱
// 1. 循环变量泄漏:i 在循环结束后依然存在于作用域中
// 2. 异步执行问题:定时器回调执行时,循环早已结束,i 变成了 10
for (var i = 0; i < 10; i++) {
setTimeout(() => console.log('var output:', i), 0);
}
// 输出结果:打印 10 次 10

// 全局污染示例
var $ = 'I am a poor dollar';
console.log(window.$); // 'I am a poor dollar' -> 意外覆盖了可能的全局库

// [推荐] let 的改进
// 1. 块级作用域:每次循环的 j 都是一个独立的绑定
// 2. 不会挂载到 window 对象上
for (let j = 0; j < 10; j++) {
setTimeout(() => console.log('let output:', j), 0);
}
// 输出结果:依次打印 0 到 9

最佳实践原则

在现代 JavaScript 开发中,建议遵循以下优先级:

  1. const (优先使用):用于声明常量或引用地址不变的对象。这能明确表达“该变量不应被重新赋值”的意图,增加代码可读性与安全性。
  2. let (次之):仅在确实需要修改变量值(如计数器、循环变量)时使用。
  3. var (避免使用):除非维护遗留的老旧项目,否则在现代化开发中应完全废弃。

JavaScript 数据类型与类型转换

JavaScript 是一门弱类型(动态)语言,变量没有静态签名束缚,这虽然带来了灵活性,但也意味着类型检查被推迟到了运行时,并且伴随着大量的隐式类型转换

基本数据类型

JS 包含 7 种基本数据类型,它们的特点是值不可变 (immutable) 且保存在栈内存中:

  1. undefined: 变量声明但未赋值、访问对象不存在的属性、函数无 return 值时的默认结果。
  2. null: 表示“空对象引用”或“无值”。
    • 注意typeof null 返回 'object' 是 JS 早期的一个历史遗留 Bug,实际上它属于基本类型。
  3. boolean: 逻辑上的 true / false
  4. number: 只有一种数字类型,基于 IEEE-754 双精度浮点数标准。
    • 这意味着 JS 中没有 int, float 之分,所有数字本质上都是浮点数。
  5. string: UTF-16 编码的文本序列。
  6. bigint: 用于处理超出 Number 安全范围的任意精度整数(以 n 结尾,如 123n)。
  7. symbol: ES6 引入的唯一、不可变的标识符,常用于定义对象的私有属性键。

实操示例:类型检测与精度陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// undefined vs null
let obj;
// console.log(obj.name); // [报错]:Cannot read properties of undefined
// obj = null; // 建议:手动释放对象引用时设为 null

// Number 的精度问题 (IEEE-754 通病)
// 0.1 和 0.2 在二进制中是无限循环小数,相加会产生精度丢失
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2); // 0.30000000000000004

// 隐式转换的魔幻行为
console.log('3' - 1); // 2 (减法只针对数字,字符串被强制转为数字)
console.log('3' + 1); // "31" (加法优先作为字符串拼接符)
console.log(typeof null); // 'object'
console.log(typeof undefined); // 'undefined'

类型转换技巧

掌握显式转换可以避免隐式转换带来的 Bug:

转字符串:

  • String(x):最安全,适用于 null/undefined。
  • x.toString():无法处理 null/undefined。
  • 模板字符串 ${x}:推荐写法,直观且兼容性好。

转数字:

  • Number(x):整体转换。
  • parseInt(x) / parseFloat(x):从左向右解析,直到遇到非数字字符。

转布尔: Boolean(x)

  • Falsy 值 (假值):在条件判断中会被视为 false 的值,包括 0, "" (空字符串), null, undefined, NaN, false
  • 注意:空数组 [] 和空对象 {} 在 JS 中属于对象,转换结果为 true

运算符与逻辑控制

相等运算符

== (宽松相等):会先进行隐式类型转换再比较。

  • 例如:1 == '1' (true), null == undefined (true)。这种特性容易导致难以预测的代码行为。

=== (严格相等):只有当类型完全一致时才返回 true。

  • 例如:1 === '1' (false), null === undefined (false)。

为了代码的健壮性,永远优先使用 ===,除非极其明确需要利用隐式转换的特性。

逻辑与短路运算

逻辑运算符不仅用于布尔判断,还常用于值的提取和保护,利用的是短路 (Short-circuit) 机制:

  • && (与):遇到假值立即停止并返回该假值;若全为真,返回最后一个值。
  • || (或):遇到真值立即停止并返回该真值;若全为假,返回最后一个值。
1
2
3
4
5
6
7
8
// 短路运算实操场景
const data = null;

// 1. 保护性访问:防止访问 null 的属性导致报错
const res = data && data.name; // 返回 null,不会报错

// 2. 默认值兜底:如果 data 不存在,则使用默认值
const defaultVal = data || 'Default Value'; // 'Default Value'

数组与对象的高级操作

数组 (Array)

数组在 JS 中是一组按顺序排列的值,本质上属于对象。ES6 提供了极丰富的内置方法来处理数据集合。

实操示例:数组迭代、筛选与变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const methods = ['PayMe', 'LinePay', 'Transfer', null, {a:1}];

// 1. 基础操作
methods.push('Alipay'); // 尾部添加
console.log(methods.length); // 获取长度

// 2. map: 映射 - 将数组每一项转换为新形式,返回新数组 (原数组不变)
// 适用于数据格式转换,如:后端数据 -> 前端 UI 模型
const cleanMethods = ['PayMe', 'LinePay'].map(item => item.toUpperCase());
console.log(cleanMethods); // ['PAYME', 'LINEPAY']

// 3. filter: 过滤 - 返回满足条件的所有元素组成的新数组
const longNames = ['PayMe', 'LinePay', 'Transfer'].filter(item => item.length > 6);
console.log(longNames); // ['LinePay', 'Transfer']

// 4. 查找与检测 (ES6+)
const methodsArr = ['Visa', 'MasterCard'];
// find: 返回第一个符合条件的元素
const visa = methodsArr.find(item => item === 'Visa');
// some: 是否至少有一个满足条件 (返回 boolean)
const hasVisa = methodsArr.some(item => item === 'Visa'); // true
// every: 是否所有元素都满足条件 (返回 boolean)
const allStrings = methodsArr.every(item => typeof item === 'string'); // true

// 5. sort: 排序陷阱
// 默认 sort() 会将元素转为字符串后按 Unicode 编码排序
const nums = [1, 20, 3];
nums.sort(); // [1, 20, 3] (20 排在 3 前面,因为字符串 '2' < '3')
nums.sort((a, b) => a - b); // [1, 3, 20] (传入比较函数修正行为)

对象 (Object)

对象用于描述实体。ES6 极大地简化了对象的创建语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const name = 'Vintor';
const age = 18;

// ES6 增强写法
const person = {
name, // 属性简写 (等同于 name: name)
age,
// 方法简写 (等同于 sayHi: function() {...})
sayHi() {
console.log('Hi!');
},
// 计算属性名:key 可以是动态的
['prop_' + age]: 'Dynamic Property'
};

函数、This 与 闭包

函数与 This 绑定机制

this 指针是 JavaScript 中最复杂也最强大的机制之一。核心原则是:this 的指向取决于函数的调用方式,而不是定义位置。

  1. 普通函数: this 动态指向调用者。如果直接调用,指向全局对象(严格模式下为 undefined)。
  2. 箭头函数: 没有自己的 this,它会捕获定义时外层上下文的 this

实操示例:显式绑定 (call, apply, bind)

JS 提供了三种手动修改 this 指向的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function sayHello(msg, punctuation) {
console.log(`${msg}, ${this.name}${punctuation}`);
}
const p = { name: 'Vintor' };

// 1. call: 逐个传参,立即执行
sayHello.call(p, 'Hello', '!'); // "Hello, Vintor!"

// 2. apply: 数组传参,立即执行
sayHello.apply(p, ['Hi', '...']); // "Hi, Vintor..."

// 3. bind: 返回一个绑定了 this 的新函数,不立即执行 (常用于事件回调)
const newSay = sayHello.bind(p, 'Hola');
newSay('?'); // "Hola, Vintor?"

闭包 (Closure)

闭包是指有权访问另一个函数作用域中变量的函数。可以把它理解为一个“背包”,函数被返回时,背着它定义时的作用域一起走了。

应用场景:数据隐藏(私有变量)、函数工厂、状态保持(如防抖节流)。

实操示例:数据隐藏与计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createCounter() {
let count = 0; // 这个变量被闭包“捕获”了,无法从外部直接访问

return {
increment() {
count++;
console.log(count);
},
getCount() {
return count;
}
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
// console.log(counter.count); // undefined (实现了私有变量)

异步编程:从 Callback 到 Promise

JavaScript 是单线程语言,异步编程是其核心。传统 AJAX 使用 XMLHttpRequest 配合回调函数,容易陷入多层嵌套的“回调地狱 (Callback Hell)”。

ES6 引入 Promise 代表未来的值,ES2017 引入 async/await,让异步代码写起来像同步代码。

实操示例:Fetch API 与 Promise 链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Fetch 是现代浏览器替代 XMLHttpRequest 的标准 API
fetch('/api/user')
.then(res => {
// Fetch 只有在网络故障时才 reject,404/500 状态码仍视为成功,需手动判断
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json(); // 解析 JSON 数据,返回一个新的 Promise
})
.then(data => {
console.log('User Data:', data);
// 链式调用:处理完数据后可以继续操作
return data.id;
})
.catch(err => {
// 统一捕获整个链条中的错误
console.error('Fetch Error:', err);
});

ES6 核心特性详解

ES6 是一次彻底的现代化升级,解决了 ES5 的诸多痛点,为大型应用开发提供了语法支撑。

解构赋值 (Destructuring)

从数组或对象中提取值,代码更简洁,不再需要反复使用点语法。

1
2
3
4
5
6
7
8
9
// 数组解构:按位置提取
const [first, , third] = [10, 20, 30]; // first=10, third=30

// 对象解构:按名称提取,支持别名和默认值
const user = { id: 1, type: 'admin', config: { theme: 'dark' } };

// 提取 type,并将 config.theme 重命名为 userTheme
const { type, config: { theme: userTheme }, role = 'guest' } = user;
console.log(type, userTheme, role); // 'admin', 'dark', 'guest'

展开运算符 (Spread Operator)

使用 ... 将可迭代对象“打散”。

1
2
3
4
5
6
7
8
// 数组合并与克隆
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4]; // [1, 2, 3, 4]

// 对象浅拷贝与属性覆盖
const defaultSettings = { theme: 'light', debug: false };
const userSettings = { ...defaultSettings, debug: true };
// { theme: 'light', debug: true }

箭头函数 (Arrow Function)

除了语法简洁,箭头函数最重要的特性是词法作用域的 this,即它自动捕获外层的 this,非常适合用作回调函数。

1
2
3
4
5
6
// 传统写法
const sum = function(a, b) { return a + b; };

// 箭头函数 (单参数可省括号,单行返回可省 return 和花括号)
const sumArrow = (a, b) => a + b;
const double = x => x * 2;

类 (Class) 与 继承

ES6 的 class 使得面向对象编程更加直观,但本质上它仍是构造函数 + 原型链的语法糖。

实操示例:类的定义、继承与静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Person {
constructor(name) {
this.name = name;
}

sayHi() {
console.log(`Hi, I am ${this.name}`);
}

// 静态方法:挂载在类本身而非实例上,通过 Person.create() 调用
static create(name) {
return new Person(name);
}
}

// 继承 extends
class Employee extends Person {
constructor(name, job) {
super(name); // 必须在访问 this 之前调用 super()
this.job = job;
}

// 方法重写
sayHi() {
console.log(`Hi, I am ${this.name}, a ${this.job}`);
}
}

const dev = new Employee('Vintor', 'Developer');
dev.sayHi(); // "Hi, I am Vintor, a Developer"

Getter / Setter

利用存取器属性(Accessor Properties),我们可以对对象属性的读取和赋值行为进行拦截和控制,实现更加健壮的数据封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle {
constructor(r) {
this.r = r;
}

// getter: 读取 area 时自动计算
get area() {
return Math.PI * this.r ** 2;
}

// setter: 赋值 area 时反推半径
set area(value) {
if (value < 0) throw new Error('Area cannot be negative');
this.r = Math.sqrt(value / Math.PI);
}
}

const c = new Circle(2);
console.log(c.area.toFixed(2)); // 12.57 (调用 get)
c.area = 50; // (调用 set)
console.log(c.r.toFixed(2)); // 3.99